WhiteHat Contest 13 writeup
個人で参加してました。2問解いて300pt、48位くらい(忘れた)でした。
なお、開催時間を48時間と勘違いして終了20分後くらいに3問目をsubmitしました。
あと、説明を全然読んでなかったのでフラグ形式がわからずしばらく悪戦苦闘してました。
解いた3問のwriteupを書きます。
・Mui Ne (pwn 100)
・DaNang (crypto 200)
・Cat Ba island (crypto 200)
Mui Ne (pwn 100)
入力したものをそのまま出力して終了というプログラム。
NX, canaryが有効。
問題文に書いてある通り、FSBが存在します。また、入力をgetsで受け付けているためBoFも存在します。
以下解いた流れ
その1.
まず、1回の入出力では足りないのでmain関数をループさせることを考えます。
mainからのリターンアドレス書き換えが真っ先に思いつきますが、canaryが有効な上に、下のようなよくわからない処理(espのすり替え)をしていてスタックのアドレスがわからないとうまくEIPをとれません。
leave
lea esp, [ecx-0x4]
ret
そこで、FSBによりスタックのアドレスをリークしつつ、__stack_chk_failのgotを上書きしてmainに飛ばしてやります。これにより、適当にBoFするだけでmain関数がループするようになります。
その2.
スタックのアドレスがわかったので、BoFによりmainからのリターンアドレスをうまく書き換えてEIPを奪います。
ちなみに、EIPを奪うときに__stack_chk_failが邪魔になってくるので、再びgotを書き換えてどこかのretに飛ばすことでスルーしてます。
その3.
EIPが奪えたら、あとはシェルをとるだけになります。
今回はlibcが与えられていないので、return to dl-resolveを目指します。
32bitなので、ももテクの記事を参考にstack pivotしてからreturn to dl-resolveして終わり。
flag:WhiteHat{196841c60f4408dd151af3ce7f4dc84f9583cc7a}
DaNang (crypto 200)
encryptというテキストファイルと、makefile.pycが与えられます。
pycは人間には読めないので、easy_python_decompilerでデコンパイルします。
すると、encryptの中身は以下のようになっていることがわかります。
まず、m1,m2がわかれば Common Modulus Attackによりflagがわかりそうなことが自明です。なのでm1(m2)をどうにかして求めることを考えます。
c1,c2を見てみると、平文が44444違うだけの関係であることが見てとれます。
つまり、
という関係の2つの暗号文から平文を求める必要があります。
これは、Franklin-Reiter Related Message Attack により求めることができます。
まとめると、
・Related Message Attack により m1, m2 を求める
・Common Modulus Attack により flag を求める
flag:WhiteHat{4111c011bc640806ef98a32a5caaf9d169a960a0}
Cat Ba island (crypto 200)
どうにかして500億手に入れる問題。 今流行りの5000兆円じゃなくてよかった。
なお、moneyなのかただの数値なのか混同しそうなのでwriteup中ではmoneyの単位を円として書きます。
選べるコマンドは2つで、
1: Get secret code : ランダムな秘密鍵、IVを元にしたCBCモードのAESオブジェクトを作成し、queryという文を暗号化する。 IVのみプレイヤーに対して知らされる。
2: Get money : プレイヤーが送信したIVによって1で作成した暗号文を復号する。このとき、平文に数字が含まれていればその分のmoneyが手に入る。 厳密に言えば正規表現でマッチした最初の部分を手に入れるので、xxx5xxx3xxx みたいに復号されたら5円が手に入る、はず。
これらのコマンドを選びつつ、50回以内に目標金額を貯めるゲーム。
IVをいい感じにいじってどうにかする問題ということは漠然と考えるものの、よくわからないのでまずはIVをそのまま送って復号を繰り返します。すると、必ず1円手に入ることがわかります。つまり元の平文のどこかに必ず1があるということなので、何文字目にあるのかを知りたくなります。
やり方としては、IVのnバイト目を IVn xor 0x31 xor 0x39 として送ります。
結局下位4bitだけが変化するわけですが、元の平文が1のバイトであれば0x39となるので復号結果が9になります。
一方、元の平文が1でない、つまり数字にならない箇所では、下位4bitだけが変化しても数字になることはありません。(asciiで数字の範囲は0x30~0x39なので)
この方法で確かめると、必ず6バイト目に1が来ることがわかります。
なので、この6バイト目を基準に前後1バイトずつを数字になるようにIVをいじっていきます。
数字でないということは、平文の該当バイトの上位4bitが0x3ではない・0x3a~0x3fのいずれかということになります。したがって、IVの該当バイトの上位4bitをいじってみて数字になればok、ならなければ下4bitを調整した上で再び上位4bitをいじる、ということをしてやれば1桁ずつ増やすことができます。
これを、6バイト目を基準に前後1バイトずつ順番に増やしていき、11桁くらいになれば目標金額に届きます。
あとは、なるべく試行回数が少なくなるようにPPCをして終わり。
ちなみに、桁を増やそうと適当にやってると0円を獲得することがあるので、ソルバのleft_flagとかはそれを回避して逆からループを回してるだけです。(平文が制御文字になってる?)
ひたすら読みにくいだけのソルバだけど、2回くらいまわしてたら目標金額達成しました。
flag:WhiteHat{13643c9b5ceece81bd5a6f9b2f8813e4d7b6dab1}
所感
各問題1行感想
・Mui Ne:他の人のwriteupみたらふつうにsystem呼んでた。つらい。
・Da Nang:申し訳程度のrev要素と50点くらいのcrypto
・Cat Ba island:PPC 200
完全に時間を勘違いしていたので、適当に他の問題もこれから見ていきたいなぁという感じです。
あと、昔にstack pivot + return to dl-resolveやったときにbss領域の上のほうにpivotしすぎて死んでたのに今回もやらかした。
DEF CON 2017 smashme writeup
1問だけ解いたのでそのwriteupです。
smashme (Baby's First)
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : disabled PIE : disabled RELRO : Partial
なんでもできそうな感じ。
動作としては、入力を受け付けてstrstrで特定の文字列があるかを調べる。なかったらexit、あったらleave; ret;という感じ。
普通にBOF可能なので、特定の文字列を入れてexitを回避しつつROPを組む。
おそらく色々な方法で解けるが、本番中は豊富なgadgetを見てから何も考えられなくなったので思考停止で組んだ。
でも今にして思えば、NXないしreadもあるんだから、ret2readで.bssにシェルコード送り込んでそこにretすればよかった。
flag
You must be at least this tall to play DEF CON CTF 5b43e02608d66dca6144aaec956ec68d
所感
はじめてDEFCONで得点できたのでうれしかったです(小並感)
他にも何問かpwnを触っていたものの、まともに脆弱性がわからなかったのでつらい。精進。
ASIS CTF Quals 2017 writeup
m1z0r3で参加、1006ptで51位でした。cryptoを3問解いて523ptを入れました。
なお、終了後すぐDLPも解けたので一応載せておく。
A fine OTP server (crypto 79pt)
RSA暗号の暗号文が与えられるが、e=3であり平文も比較的短いので3乗根をとるだけ。後のSecured OTP serverで言及するのでここではソルバは省略。(3乗根で解けることに最初は気づかず、Secured OTP serverと同じソルバで解いた)
ASIS{0f4ae19fefbb44b37f9012b561698d19}
F-4 Phantom (crypto 176pt)
適当な素数pを元に、ランダムな2bitを反転させたもののnext_primeをqとしたRSA暗号。またeは65537ではなく大き目な値。
(なお、最初はwiener's attackを試したりもしたがダメだった)
反転させるランダムな2bitに注目すると、pとqが比較的近い値になる場合があることがわかる。この場合フェルマー法により素因数分解できるので、このようなnを引けるまでガチャる。
都合の良いnを素因数分解すると、φ(n)とeが互いに素ではないため秘密鍵dが計算できないことがわかる。計算してみると、素数pに対してeはp-1の素因数となっている。
なので、ひとまず素数qを用いてを計算し、を導く。
同様に、素因数分解可能なnをもう1つ入手してを計算する。
あとは、これらのをCRTにより持ち上げるとmが求まる。いわゆるRSA-CRTの計算である。
ASIS{Still____We_Can_Solve_Bad_F4!}
Secured OTP server (crypto 268pt)
ある平文の末尾に、ランダムな18文字を連結したものを平文としたRSA暗号の暗号文を2つ与えられる。なお、2つの平文の差は、より小さい。
わざわざこの条件が与えられていることから、Short Pad AttackやFranklin-Reiter Related Message Attackをすると考えられる。が、手持ちのソルバを用いてもShort Pad Attackで解が得られなかった。
そこで、平文の上位bitがわかっていることから、シンプルにCoppersmith's theoremから解いた。
ASIS{gj____Finally_y0u_have_found_This_is_Franklin-Reiter's_attack_CongratZ_ZZzZ!_!!!}
おまけ。DLP
開催中には解けなかったやつ。
暗号文として、とnが得られる。
以上の項はすべて消える。つまり、s=11としたとき、となる。なので、でmodをとればのみ残るので、あとは計算して終わり。
ASIS{Congratz_You_are_Discrete_Logarithm_Problem_Solver!!!}
まとめ
都合上日曜日しか参戦できなかったとはいえ、DLPとunsecure ASIS sub-dは解けるべきだと思った。start_hardも少し手をつけたので気になるところ。
House of Force をやってみる
過去の問題を元にHouse of Forceの練習をしてみたので、そのwriteupのような覚書です。
how2heapにも載っている、Boston Key Party 2016 の cookbook という問題を解いてみました。
House of Forceとは
以下のような状況を想定。
- main_arena.top のアドレスがわかる
- main_arena.top を書き換え可能
- 任意サイズでmallocすることができる
このとき、main_arena.topを0xffffffffなどの大きな値で書き換える。その後適当な大きさでmallocをすることで、次回のmallocで任意のアドレスを返すことができるというもの。
ここで適当な大きさとは、(確保したいアドレス-8)-現在のtop-8
となる。
前者のマイナス8は、malloc_chunk構造体によるもの(prev_size, sizeを考慮)である。
後者のマイナス8は、topからchunkを切り出すか判断する際のmallocの仕様によるものであり、細かくは気にしなくてよい(と思われる)。
通常はmallocで確保した領域に対して書き込みが可能なアプリケーションがほとんであるため、任意のアドレスを確保できるようになることはすなわち、任意のアドレスを上書き可能となったに等しい。
cookbook writeup
名前の通り料理系のアプリ。素材の登録・削除、レシピの登録・削除、cookbook名の登録・削除ができる。
それぞれのデータはヒープで管理されている。
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
解析パートは省くが、様々な脆弱性がある(heap oveflow, double free, use-after-free)
このうち、use-after-freeとheap oveflowを使って解く。
main_arena.top 、ヒープのアドレスリーク
まずは、House of Forceの条件の1つ目である、現在のmain_arena.topのアドレスリークを行う。これは、レシピ登録にあるuse-after-freeを用いる。具体的には、一旦レシピに適当な素材を適当数登録した後discardすると、素材の個数へのポインタが入っていたアドレスにbkメンバが入る。discard後もprint current recipeすることができ、素材の個数の欄にbkの情報が漏洩する。
特にfree listがない状態でこれを行うと、bkはmain_arena.topを指すため、main_arena.topのアドレスをリークすることができる。
また、main_arena.topのアドレスを元にヒープのベースアドレスも計算することができる。
main_arena.top overwrite
次に、2つ目の条件であるmain_arena.topの書き換えを行う。これは、レシピ登録にあるheap oveflowを用いる。具体的には、レシピ名でoverflowが可能なので、main_arena.topのある位置まで書き込み、0xffffffffで上書きをする。
任意サイズでのmalloc
これは脆弱性を使うことなく、cookbook名の登録機能で実現できる。16進数で入力した値をそのまま引数にmallocしてくれるため、任意のサイズ(負数も可)でmallocを呼べる。
さて、以上よりHouse of Forceの条件がすべて整った。
あとは書き換えたいアドレスから確保すべきサイズを計算し実際に確保するだけである。
この問題はFULL RELROではないので、GOT overwriteをしてsystem(/bin/sh)を呼ぶのがおそらく一番簡単である。入力をそのまま引数にとるatoiがあるので、これのGOTを書き換える。が、systemのアドレスを求めるためにlibcのベースアドレスを漏洩させる必要がある。したがって、atoiのgotを一旦printf@pltに書き換えることでFSBを発生させてlibcのアドレスを計算する。ちなみにatoiはadd ingredientのpriceやcal設定で呼ばれる。
なお、ユーザの入力をそのまま引数にとるという点ではmallocもそうだが、このプログラムはいたる所でmalloc/freeを呼ぶため、libc計算のためにmallocを壊すとうまく動かなくなるためatoiを使った。
以上のことをすべてまとめると以下の手順になる。
- create_recipeのuse-after-freeによりmain_arena.topのアドレスをleak
- create_recipeのoveflowによりmain_arena.topを0xffffffffでoverwrite
- House of ForceによりmallocでatoiのGOT付近を返す
- GOT overwriteによりatoiをprintfにする
- FSBによりlibcのアドレスをleak
- 再びHouse of ForceによりatoiのGOTをsystemにoverwrite
以下ソルバ
工夫点(苦戦点)
- 一番はじめにingredient用chunkを確保
House of Force後にやると変なところに確保されるのでは?という気がしたため。ただしこの後の工夫によって杞憂となったと思う。
- 1回目のGOT overwriteをしつつtopをheap領域に戻すようなsizeを指定
GOTの破壊が進行してしまうのを防ぐため。ここに適当なsizeを指定していたときは、GOT内のアドレスをfreeに渡してエラー終了していたが、工夫することで正常に動くようになった。
- GOT overwriteの際、4バイト手前から書き込んでいる点
これは、mallocの8バイトアラインメントによりピッタリのアドレスを返せないことが原因だと思われる。はじめは丁度のアドレスを返そうと試していたが、入力sizeを4バイトずらすと確保される領域は8バイトずれてしまう。
まとめ
というわけでHouse of Forceをやってみました。m1z0r3内の勉強会でcookbookをやっていたときはヒープがよくわからないのでスルーしていたものの、unlink attackで解いてたような気がするので時間があったらそっちもやってみます。
次はHouse of Einherjarをやってみたいです。
おわり。
参考
0CTF 2017 writeup その3
その他writeup
0CTF 2017 writeup その2 - きゅうり。 (char)
0CTF 2017 writeup - きゅうり。 (integrity, oneTimePad)
残り解く予定(oneTimePad2、BabyHeap2017、diethard)
今回は、EasiestPrintfのwriteupです。と言っても、自力では解けないので他の人のwriteupを参考に勉強した記録です。
tl;dr
今回得られた知見
- printf内でのmalloc/free呼び出し
- hook関数
- one-gadget RCE
writeup
動作としては、任意のアドレスを1つだけ教えてくれた後、FSBのあるleave関数が呼び出されて即exitという流れ。
NX、Full RELROで制約は厳しい。
まず、任意のアドレスをリークできる部分で適当にlibcのアドレスを特定。
本来ならばここからデストラクタを書き換えたりGOT overwriteしたりしたいところだが、Full RELROなので思いつくことが諸々できない。
現状ではEIPを奪えていないので、まずはEIPを奪うことを考える。
結論から述べると、printf内で呼ばれるmalloc/freeを利用する。printfを追って行くとvfprintf.cなるものが呼ばれることがわかるが、ソースコードを読んでみると、widthという値がWORK_BUFFER_SIZE-32以上のときmallocが呼ばれる様子がわかる。(1475行目前後)
要するに、printfで表示する文字列がある程度長いときには一旦mallocで確保した領域に文字列を置き、表示後にfreeしているものと考えられる。
ではこのmalloc/freeをどう利用するかだが、各種hook関数(__malloc_hook, __free_hook)に注目する。hook関数を用いるとmallocなどの関数を独自関数に置き換えることができ、通常はデバッグ用などに使われる。ユーザが定義するので当然と言えば当然だが、この各種hook関数はlibcのうちwritableな領域にある。(画像一番下)
未定義の時は該当するアドレスには0が入っているため、おそらく最初にhook関数のアドレスが0かどうかを確認し、0でない時はそのアドレス先が呼ばれると思われる。
なので、FSBで__free_hookをoverwriteしつつ、一定量表示させるようにすることでEIPを奪うことができる。
最後にどうやってシェルを取るかだが、one-gadget RCEを用いた。いわゆる一発でシェルをとれるgadgetである。なおこのようなgadgetがlibcのどこにあるかは外部によくまとまっているブログがあったのでそこを参考にした。どうやら本番環境のlibcにも同様のgadgetはあるようなので問題ない、と思う。
EIPを奪えているので、スタックのアドレスをリークしつつmainに再び飛ばしても解けるとは思うが、せっかくなので新たなものを使ってみた。
0CTF 2017 writeup その2
その他writeup
0CTF 2017 writeup その3 - きゅうり。 (EasiestPrintf)
0CTF 2017 writeup - きゅうり。 (integrity, oneTimePad)
今更ですが、本番中には解けず、後から解いたもののwriteupシリーズです。
char
自明なbuffer over readがあり、offset=32で簡単にEIPが奪える。ただし入力はprintableなもの(asciiで0x20〜0x7e)しか受け付けないので、その範囲内でROPを組む必要がある。
libcが固定アドレスでprintableなアドレスに読み込まれるので、その中の使えるgadgetを組み合わせて頑張る。rp++の結果からprintableなものだけ抽出するスクリプトを適当に書いて、あとは泥臭く組み立てていっただけなので特に説明はないです。
ecxは{"/bin/sh",NULL}ってずっと思ってたものの、特になにもしなくてもシェルとれた。知見。
flag{Asc11_ea3y_d0_1t???}