DensanCTFのRev問を作りました・解説
TL;dr
- 一関高専電算部主催 #DensanCTF のお手伝いをした
- 初心者向けReversing問題3つ作った
- 解いてもらえると…嬉しい!
やったこと
一関高専電算部主催のCTF初心者高専生対象のCTF DensanCTF のお手伝いとして宇部高専からReversingの問題を3問ほど作らせていただきました。
CTFはぼくも初心者で,作問をするのは初めての経験でした。
初心者向けとのことで作問やらないかと声を掛けていただいて,直前になって急いで作ったのでクオリティが高いとは言えない感じでした(はい)(反省しています)。
以下,ぼくの作った問題の想定解による解説をさせていただきます。
Hello, Reversing World! (100)
100点のwarm-up問題です。
問題文は以下の通り。
「あれ?フラグはこの形式で間違いないはずなのに…」
えい,えいっ。 何故だ,何度フラグを入力しても正解にならない。タイプミス…は,していないはずだし。
どうやらこの問題を作ったやつは酷く捻くれているらしい。 偽のフラグで私たちを騙そうとしているんだ!
フラグは,本当のフラグはどこ? お願い,一緒に正しいフラグを探すのを手伝って!
解法として,まず問題のファイルをダウンロードし,fileコマンドを使ってファイル形式を特定します。
$ file hello_reversing_world hello_reversing_world: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamicallylinked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=7e89c845c01192e6e7335f170c083d8add217bc7, not stripped
実行結果より,このファイルがelfファイルであることがわかります。
次に,実際に実行して動きを確かめてみます。
$ ./hello_reversing_world FLAG{This_is_Dummy_FLAG...}
実行結果は確かにFLAG{XXXXXXX}
の形式を満たしていますが,これをsubmitしても正解にはなりません。
どうやら,問題文の少女はこの問題に直面して困っているようです。
そこで,ファイルの中に正しいフラグが隠されていることを疑います。
ここでは,stringsコマンドとgrepコマンドを使います。
$ strings hello_reversing_world | grep FLAG FLAG{Bravo!This_is_True_FLAG}
こうして正しいフラグを得ることができました。
More, more, more (300)
300点,実行ファイルを動的解析してみようという問題です。
問題文は以下の通り。
「もっと,もっと頂戴」
うわ言のように彼女は繰り返す。
「まだ足りないの」
何が?何が足りない。私は何を与えれば良い? 一刻も早く彼女を静めたいのに,彼女の求めているものが何なのか私にはわからない。知る由もない。
そうだ,そこの君。私は彼女の面倒を見るので手一杯なんだ。 そこに何か手がかりはない?
彼女が何を求めているのか,それを見つけ出して教えてほしいの。
想定解法として,まずfileコマンドでこのファイルがelfファイルであることがわかります。
そこで,実際に実行してみます。
$ ./more_more_more Input pass phrase...
パスフレーズを求めるメッセージが出力された後,入力待ちになります。とりあえず何かを入力してみましょう。
$ ./more_more_more Input pass phrase... hoge Oh, no... Your Answer is Wrong. Retry many times!
この実行結果より,hogeはパスフレーズとして相応しくなかったということがわかります。
さて,正しいパスフレーズを探さなければなりません。
stringsコマンドで見てみても,パスフレーズらしきものは出力されないことがわかるでしょう(もちろん,フラグも出力されません)。
ここで,プログラムを動かしながら解析する動的解析という手法を用います。
使うコマンドは,ltraceコマンドです。
$ ltrace ./more_more_more puts("Input pass phrase..."Input pass phrase... ) = 21 __isoc99_scanf(0x555a0825101d, 0x7ffe6d6a72d0, 0x7fddb5848720, 0x7fddb5774818
__isoc99_scanfという関数が表示された後,動作が止まります。入力待ちです。
再度適当な文字列を入力して動作を追ってみます。
$ ltrace ./more_more_more puts("Input pass phrase..."Input pass phrase... ) = 21 __isoc99_scanf(0x555a0825101d, 0x7ffe6d6a72d0, 0x7fddb5848720, 0x7fddb5774818hoge ) = 1 strcat("I_w", "ann") = "I_wann" strcat("I_wann", "a_e") = "I_wanna_e" strcat("I_wanna_e", "at_") = "I_wanna_eat_" strcat("I_wanna_eat_", "Mik") = "I_wanna_eat_Mik" strcat("I_wanna_eat_Mik", "an_") = "I_wanna_eat_Mikan_" strcat("I_wanna_eat_Mikan_", "Nab") = "I_wanna_eat_Mikan_Nab" strcat("I_wanna_eat_Mikan_Nab", "e!!") = "I_wanna_eat_Mikan_Nabe!!" strcat("I_wanna_eat_Mikan_Nabe!!", "!") = "I_wanna_eat_Mikan_Nabe!!!" strcmp("hoge", "I_wanna_eat_Mikan_Nabe!!!") = 31 puts("Oh, no... Your Answer is Wrong. "...Oh, no... Your Answer is Wrong. Retry many times! ) = 50 +++ exited (status 0) +++
すると,数回strcatという関数が呼ばれた後,strcmpという関数に"hoge","I_wanna_eat_Mikan_Nabe!!!"という文字列が与えられていることがわかります。
ここで,"hoge"の部分には自分の入力した文字列が入っています(数回別の文字列を入力して見るとわかるでしょう)。
strcmpというのは,C言語のstring.hで提供される文字列を比較する関数です。
よって,比べられている"I_wanna_eat_Mikan_Nabe!!!"という文字列と同じ文字列が正しい文字列なのではないか?というように推測ができます。
実際に試してみると,以下のようになります。
$ ./more_more_more Input pass phrase... I_wanna_eat_Mikan_Nabe!!! Good job! Your Answer is correct! The flag is ... FLAG{I_need_more_orange}
問題文の少女はみかんが食べたかったようです。ぼくかな?
Health care (300)
300点,静的解析の問題です。
問題文はこちら。
「ふぁ~あ,もう21時か」
ここのところずっと残業続きだ。肩も凝るし,何より足のむくみが酷い。 何やら良いリフレッシュはないものか。できることなら1日30時間寝てやりたい。
頭の中でひとりごちながら帰宅の準備を進めていたそのとき,スッコココ,と聞き慣れたSEが静かなオフィスに響く。
『お疲れ様。頑張る後輩へのプレゼントだ』
上司からのメッセージと共に謎のファイルを受信する。 全く,誰がこんな難解なプレゼントを送るんだ。
愉快そうに笑う上司の顔が脳裏に過る。
一体どんなプレゼントなんだろうな。思わず口角が上がる。 …やってやろうじゃないか。
elfファイルが与えられます。実行すると以下のように動作します。
$ ./health_care Stage 1: Put your answer
何かしらの入力を求められています。
$ ./health_care Stage 1: Put your answer hoge Oh, no... Retry more times!
ダメみたいですね。
今回も同じようにstringsコマンドやltraceコマンドなどを使ってみますが,有効な情報は得られそうにありません。
そこでプログラムを逆アセンブルし,実際にソースを読みながら実行の流れを追ってみます。
$ objdump -M intel -d health_care
出力結果は長いので全ては載せませんが,注目スべきはmain関数の部分です。
00000000000012ab <main>:
と書かれている行を探します。
その中を眺めていくと,
12e1: e8 8a fd ff ff call 1070 <__isoc99_scanf@plt> 12e6: 8b 45 ec mov eax,DWORD PTR [rbp-0x14] 12e9: 83 f8 0a cmp eax,0xa 12ec: 74 18 je 1306 <main+0x5b>
という箇所があることがわかります。
call命令でscanf関数を呼び出していたり,cmp命令で何かを比較していたりします。明らかに怪しいですね。
ここで,scanf関数によって取得された入力値はcallの次の行にあるmov命令によってeaxというレジスタに格納されます。
その後,cmp命令によりeaxと0xaという値が比較され,それが等しいならmain+0x5bの位置にジャンプしていることがわかります。
よって,入力に0xa(つまり10進数で10)を渡せばStage1を通過できそうです。
実際に試してみると,以下のようになります。
$ ./health_care Stage 1: Put your answer 10 Stage 2: Put your answer
どうやら,Stage1はこれで正解のようです。次はStage2を見てみます。
逆アセンブル結果のmain関数内を追っていくと,もう1つscanf関数が呼ばれている箇所があります。
1325: e8 46 fd ff ff call 1070 <__isoc99_scanf@plt> 132a: 8b 45 f0 mov eax,DWORD PTR [rbp-0x10] 132d: 83 f0 0d xor eax,0xd 1330: 89 45 f4 mov DWORD PTR [rbp-0xc],eax 1333: 83 7d f4 03 cmp DWORD PTR [rbp-0xc],0x3 1337: 74 18 je 1351 <main+0xa6>
このかたまりです。
先ほどと同じように,scanfで取得した値がeaxに格納されます。
その後,xor命令でeaxと0xd(つまり,10進数で13)の排他的論理和を取っていることがわかります。
その後mov命令でDWORD PTR [rbp-0xc]という場所にその結果を格納し,その値と0x3(10進数でも3ですね)を比較していることがわかります。
よって,0xdでXORを取ると3になる値を入力すれば良いことがわかります。
つまり,入力すべき値は14です。
これを踏まえて,もう一度実行してみます。
$ ./health_care Stage 1: Put your answer 10 Stage 2: Put your answer 14 Good job! The flag is...FLAG{Recovering_from_my_foot's_fatigue_by_foot_bath}
上司からの『足湯で疲れをふっとばす』というリフレッシュ方法の提案がフラグでした。
Reversing問題の作問
Reversing問題の作問はこれが初めてでしたが,思った以上に情報がなくて大変でした。
以下問題ごとにソースコードと作問中試行錯誤したことなどを書いておきたいと思います。今後Rev問作ってみたい人は参考にしてください。
Hello, Reversing World! (100)
stringsコマンドでフラグを得る問題にしようというのは決めていました。
はじめはprintfにフラグを置いてコメントアウトしてみたのですが,コンパイル時にコメント部分は無視されるので実行ファイルからフラグは得られません(それはそう!)。
次に,define文にフラグを置いてmain関数は空のままコンパイルしてみましたがこれもダメでした。最適化とかが働いてるのかな。
ということで一度どこかで出力などして文字列を使わなければなりません。
結局,printfで一旦正解のフラグを出力し,行頭まで遡って偽のフラグを被せて出力するという方法にしました。
ただ,grep FLAG
したときに偽のフラグも一緒に出力されるのは不格好だったため,偽のフラグは1文字ずつ出力する形を取りました。
(にしても別にASCIIでデコードする必要なくて普通にchar配列作れば良かった気がするな)
ソースコードは以下の通りです。
More, more, more (300)
この問題は,どうやってパスフレーズをstringsから隠しつつ比較に使うかを考えていました。
結果的にはデフォルトのstringsコマンドで出力されない3文字ずつの塊に分けてプログラム中でstrcatを使ってひとつの文字列につなぎ直すという手段を取りました。
が,普通に1文字ずつの方がいろいろと良くないですか?という気持ちが今はしています。何故3文字。
ソースコードは以下の通り。
Health care (300)
この問題は一番適当に作りました本当に申し訳ありませんでした。足湯に浸かりながらうわ眠〜とか言いながら作ってました。
はじめは四則演算とかにしようかなと思ったのですが,素直に
if(a-3 == 10){ 処理; }
とかを書くとコンパイラに最適化されてアセンブリではaと13を比較しているというようになってしまいます。
ちなみにちょうど今日ある本を読んでいたときに最適化を外せるオプションがあることを知りました。
これを回避するために,一度変数を噛ませてあげると良かったりします。
以下のような感じです。
int b = a-3; if(b == 10){ 処理; }
また,1問目や2問目のようにフラグをmain関数中で1文字ずつ表示する方法を取ると逆アセンブルしたときmain関数内に大量の
mov DWORD PTR [rbp-0xXX], 0xYY
みたいな命令が吐かれてしまいます。
これは単純にわかる人には第二引数をASCIIで解釈して並べればフラグが得られるというのがわかってしまう上に,初心者がmain関数を読む上では大変邪魔なノイズになり得ます。
ということで,フラグの表示は別の関数に分割しました。
また,そちらを読まれてもフラグがわかるなどということにならないように,表示する文字列は3文字ずつに分割してdefine文に入れました。
もうちょっと頭の良い方法が無いんでしょうか。考えればありそうですが考えるのがしんどかったのでこれで妥協してしまった。コードは目も当てられない状態になっています。
感想
自分の作った問題を解いてもらっていたり「これ解けなかったー」って言われてたりするのちょっとドキドキしちゃうなという気持ちになりました。
初心者向けということでそんなに複雑なことはしなかったのですが,もう少し配布資料を作り込めば良かったかもしれません。
どんな要素を入れようかなとかどんな関数使うとどんな挙動するかなとか考えるのが楽しくて,久々に低レイヤの機運が高まるなどをしました。
個人的にはHello, Reversing World!の挙動が気に入っているのですが,それとは別にMore, more, moreはTwitter上で「はすみさんの作問だとすぐわかった」みたいな感想がたくさんあってちょっと嬉しかったです。
また機会があれば作問とかしたいなあと思っています。
もし出番がありそうなら誰か呼んでください。初心者向けしか作れませんよろしくお願いします。
DensanCTF運営のみなさん,参加者のみなさん,お疲れ様でした!