#1. はじめに
前回の投稿からかなり間が空いてしまいましたが、今回もかなり雑にCTFを紹介しようかと思います。今回はbin編ということで、文字通りバイナリやらアセンブリやらデバッガのお話になります。LinuxやWindowsにおけるバイナリが大体どんな感じのものかをわかって頂ければなーと思います。
#2. 理論
今回、基本的なアセンブリの解説はしません。非常にわかりやすく解説してるサイト、書籍は腐るほど有りますので…
逆アセンブルの結果を見ていく上で、あまりマニアックな命令を覚えても仕方ないので基本的によく見るアセンブリ命令(10個程度)がわかっていれば簡単なCTFの問題を解く上では問題ないと思います。(個人的なオススメは某ハリネズミ本ですが、これにもう少し詳しいバイナリに関する本が一冊あれば良いんじゃないかと思います)
#3. 実際にやってみよう
それではまず簡単に、バイナリファイルを直接書き換えることでプログラムに自分の思い通りの動作をさせてみましょう。
~環境~
ubuntu 16.04 x86
#include<stdio.h>
#include<string.h>
int main(){
char input[50];
char answer[50] = "binbinbin";
char flag[50] = "FLAG{CTF_binary}";
do{
printf("input:");
scanf("%s", input);
if(strcmp(input,answer)==0){
printf("%s\n", flag);
break;
}
else
printf("Wrong answer...\n");
}while(1);
return 0;
}
はい。内容は簡単で「秘密のパスワード『binbinbin』を入力するとflagが手に入るよ」というものです。
・パスワード平文で書くなよ…
・これ頑張ってデバッガとかコマンド使えばバイナリいじらなくてもパスかflag手に入らない?
というツッコミは無しで…今回はあくまで簡略化のために雑コードで御座います。まあいつも雑なんですけど…
というわけでこいつをコンパイルして(名前をbeforeとします)、objdumpで中身を見てみましょう。
gcc -o before bin.c
CTFではこのようにバイナリファイルだけをポンと渡されて「これ解析してflag奪取してね~」と言われる事が多いです。勿論そのファイルにデバッグ情報などは付与されておらず、元のソースコードは不明なままです。
非常に簡単な問題ならstringsで答えがわかってしまうのですが、今回はどうもそうは行かなさそうです。
shir0@shir0:~/src$ objdump -d -M intel before
~省略~
0804851b <main>:
804851b: 8d 4c 24 04 lea ecx,[esp+0x4]
804851f: 83 e4 f0 and esp,0xfffffff0
8048522: ff 71 fc push DWORD PTR [ecx-0x4]
8048525: 55 push ebp
8048526: 89 e5 mov ebp,esp
8048528: 57 push edi
8048529: 53 push ebx
804852a: 51 push ecx
804852b: 81 ec ac 00 00 00 sub esp,0xac
8048531: 65 a1 14 00 00 00 mov eax,gs:0x14
8048537: 89 45 e4 mov DWORD PTR [ebp-0x1c],eax
804853a: 31 c0 xor eax,eax
804853c: c7 45 80 62 69 6e 62 mov DWORD PTR [ebp-0x80],0x626e6962
8048543: c7 45 84 69 6e 62 69 mov DWORD PTR [ebp-0x7c],0x69626e69
804854a: c7 45 88 6e 00 00 00 mov DWORD PTR [ebp-0x78],0x6e
8048551: 8d 55 8c lea edx,[ebp-0x74]
8048554: b8 00 00 00 00 mov eax,0x0
8048559: b9 09 00 00 00 mov ecx,0x9
804855e: 89 d7 mov edi,edx
8048560: f3 ab rep stos DWORD PTR es:[edi],eax
8048562: 89 fa mov edx,edi
8048564: 66 89 02 mov WORD PTR [edx],ax
8048567: 83 c2 02 add edx,0x2
804856a: c7 45 b2 46 4c 41 47 mov DWORD PTR [ebp-0x4e],0x47414c46
8048571: c7 45 b6 7b 43 54 46 mov DWORD PTR [ebp-0x4a],0x4654437b
8048578: c7 45 ba 5f 62 69 6e mov DWORD PTR [ebp-0x46],0x6e69625f
804857f: c7 45 be 61 72 79 7d mov DWORD PTR [ebp-0x42],0x7d797261
8048586: c7 45 c2 00 00 00 00 mov DWORD PTR [ebp-0x3e],0x0
804858d: 8d 45 c6 lea eax,[ebp-0x3a]
8048590: b9 1e 00 00 00 mov ecx,0x1e
8048595: bb 00 00 00 00 mov ebx,0x0
804859a: 89 18 mov DWORD PTR [eax],ebx
804859c: 89 5c 08 fc mov DWORD PTR [eax+ecx*1-0x4],ebx
80485a0: 8d 50 04 lea edx,[eax+0x4]
80485a3: 83 e2 fc and edx,0xfffffffc
80485a6: 29 d0 sub eax,edx
80485a8: 01 c1 add ecx,eax
80485aa: 83 e1 fc and ecx,0xfffffffc
80485ad: 83 e1 fc and ecx,0xfffffffc
80485b0: b8 00 00 00 00 mov eax,0x0
80485b5: 89 1c 02 mov DWORD PTR [edx+eax*1],ebx
80485b8: 83 c0 04 add eax,0x4
80485bb: 39 c8 cmp eax,ecx
80485bd: 72 f6 jb 80485b5 <main+0x9a>
80485bf: 01 c2 add edx,eax
80485c1: 83 ec 0c sub esp,0xc
80485c4: 68 d0 86 04 08 push 0x80486d0
80485c9: e8 f2 fd ff ff call 80483c0 <printf@plt>
80485ce: 83 c4 10 add esp,0x10
80485d1: 83 ec 08 sub esp,0x8
80485d4: 8d 85 4e ff ff ff lea eax,[ebp-0xb2]
80485da: 50 push eax
80485db: 68 d7 86 04 08 push 0x80486d7
80485e0: e8 1b fe ff ff call 8048400 <__isoc99_scanf@plt>
80485e5: 83 c4 10 add esp,0x10
80485e8: 83 ec 08 sub esp,0x8
80485eb: 8d 45 80 lea eax,[ebp-0x80]
80485ee: 50 push eax
80485ef: 8d 85 4e ff ff ff lea eax,[ebp-0xb2]
80485f5: 50 push eax
80485f6: e8 b5 fd ff ff call 80483b0 <strcmp@plt> ///strcmpの呼び出し///
80485fb: 83 c4 10 add esp,0x10
80485fe: 85 c0 test eax,eax
8048600: 75 23 jne 8048625 <main+0x10a> ///怪しいところ///
8048602: 83 ec 0c sub esp,0xc
8048605: 8d 45 b2 lea eax,[ebp-0x4e]
8048608: 50 push eax
8048609: e8 d2 fd ff ff call 80483e0 <puts@plt>
804860e: 83 c4 10 add esp,0x10
8048611: 90 nop
8048612: b8 00 00 00 00 mov eax,0x0
8048617: 8b 7d e4 mov edi,DWORD PTR [ebp-0x1c]
804861a: 65 33 3d 14 00 00 00 xor edi,DWORD PTR gs:0x14
8048621: 74 19 je 804863c <main+0x121>
8048623: eb 12 jmp 8048637 <main+0x11c>
8048625: 83 ec 0c sub esp,0xc
8048628: 68 da 86 04 08 push 0x80486da
804862d: e8 ae fd ff ff call 80483e0 <puts@plt>
8048632: 83 c4 10 add esp,0x10
8048635: eb 8a jmp 80485c1 <main+0xa6>
8048637: e8 94 fd ff ff call 80483d0 <__stack_chk_fail@plt>
804863c: 8d 65 f4 lea esp,[ebp-0xc]
804863f: 59 pop ecx
8048640: 5b pop ebx
8048641: 5f pop edi
8048642: 5d pop ebp
8048643: 8d 61 fc lea esp,[ecx-0x4]
8048646: c3 ret
8048647: 66 90 xchg ax,ax
8048649: 66 90 xchg ax,ax
804864b: 66 90 xchg ax,ax
804864d: 66 90 xchg ax,ax
804864f: 90 nop
~省略~
objdumpを使ってmain関数の逆アセンブル結果を表示しています。
この結果から、怪しい分岐命令に当たりをつけていきます。
80485f6でstrcmpを呼び出していますね。その後80485feでtest eax eaxを経て、8048600でstrcmpを呼び出してから初めてのjne(分岐命令)が登場します。そこで、jne命令のオペコード75 23をnop命令(何もしない、という命令)のオペコード2つ、90 90で上書きしてみましょう。すると、処理は次の8048602に移るわけですが、このまま流れを追っていくとどうやらプログラムは正常に終了しそうですね。このプログラムの場合、正解のパスを入力するかCtrl-Cで終了するかをしない限り、whileループによって半永久的にパスの入力を求めます。なので、正常に終了している⇢パスが正解の場合に分岐している と考えられます。
さて、それでは実際にバイナリエディタを使ってファイルを編集してみましょう。(編集後のバイナリファイルの名前をafterとします)今回はGHexを利用していますが、他に使い慣れたバイナリエディタがあればそちらでも構いません。
x86-LinuxにおけるPT_LOAD Segmentはデフォルトで0x0804000から開始されるので(リンカ自作とかをすれば変更は可能ですが)、jne命令のあるアドレス0x08048600との差を取って、GHexでオフセットが600のあたりを調べると…
jne命令のオペコード 75 23がありましたね。こいつをnop命令×2に書き換えてあげましょう。
それでは編集前後のバイナリファイルを実行してみましょう。
shir0@shir0:~/src$ ./before
input:aa
Wrong answer...
input:bb
Wrong answer...
input:binbinbin
FLAG{CTF_binary}
shir0@shir0:~/src$ ./after
input:aa
FLAG{CTF_binary}
shir0@shir0:~/src$ ./after
input:bb
FLAG{CTF_binary}
shir0@shir0:~/src$ ./after
input:binbinbin
FLAG{CTF_binary}
beforeが正解を入力しないとflagを表示しないのに対して、afterではどんなパスを入力してもflagが獲得できている様子が確認できますね。
非常に適当な解説でしたが、バイナリ編集による制御の変更って大体こんな流れなんだなあ 程度に思って頂ければ幸いです。通常レベルになると、まずsshで指定されたサーバーに接続⇢あるディレクトリにある編集等ができないバイナリの脆弱性を調査・攻撃する⇢flagを手に入れる という流れのpwn問題も出てきたりするので、自分的にもまだまだ先の道のりは長い…と思わざるを得ませんね。
#4. 実践
それでは、実際にCTFで出題された問題を解いてみましょう。
Anti-Debugging
Reverse it.
bin (←バイナリファイルのダウンロードリンク セキュリティソフトに引っかかるので今回はリンク先を貼っていません)
may some AV will alert,but no problem.
~SECCON CTF 2016 オンライン予選より "Anti-Debugging"~
少々わかりづらいですが、謎のバイナリファイルが渡され、こいつを解析しろ!という問題みたいですね。bin問題というよりrev問題のような気もしますが…
とりあえず先程の環境で、このファイルの正体を探ってみましょう。
shir0@shir0:~/ctf$ file bin
bin: PE32 executable (console) Intel 80386, for MS Windows
32bit-Windowsのバイナリみたいですね。ここから環境をWindows 10 Homeに移します。
とりあえず名前に.exeを付け加えて端末で実行してみましょう。
Input password>
うーん パスワードの入力を求められて、適当に入力してみてもプログラムが終了してしまいますね。
そこで今回はIDAと呼ばれる逆アセンブラのデモ版を利用してみましょう。
画像はIDAで問題のバイナリファイルを開いたところです。
このツールはプログラムの流れや分岐命令をツリー表示でわかりやすく示してくれる優れものなので、バイナリ解析の際に手元にあると大分心強いですね。
それでは、各命令群の終わりにある分岐命令にブレークポイントを置きながら、流れを追っていきましょう。eipが分岐命令のブレークポイントに達すると、画像では分かりづらいですが矢印が点滅して次の分岐先を示してくれるので、それが正解の流れかどうかを検証しながら先に進んでいきます。
まず上の画像では、最初のパスワードの入力受付とその合否を判断して、分岐しています。分岐命令をクリックして、Hex-view(バイナリの16進数ダンプ)タブに切り替えると、該当命令に当たる部分をハイライトしてくれます。(黄色くハイライトされているjne命令が、Hex-viewタブでは濃い緑色でハイライトされています)
IDA Demo版にはバイナリの編集機能がついてないので、IDAで該当部分のアドレスをチェックしながら、適宜stirlings等のバイナリエディタで編集を行います。
次の命令群との整合性を考えると、ここでパスワードを間違えると16進数ダンプしたときの順番通りの流れから外れ、プログラムは終了してしまいます。なので、先程と同様に分岐命令(jnz)をnop命令に上書きしてしまいましょう。そうすれば、パスワードの合否に関わらず、プログラムは正解の場合の分岐に向かうはずです。
タイトルからだいたい予想はできていましたが、IsDebuggerPresent()等のデバッガ検知が見受けられます。これらは、プログラムがデバッガ上で動いているかどうかを検出してくれるapi関数です。今回の場合、プログラムがデバッガ上で動いていることが検知されると、その時点で終了する命令群に分岐する仕組みになっているようですね。
もう一つ厄介な点は、今度は16進数ダンプした時の順番通りではなく、分岐命令でジャンプしている先が正解のルートだということです。つまり、これらの命令群の最後の分岐命令をnop命令に書き換えても、無条件に不正解のルートに送られるだけ、ということです。そしてやはりどの分岐でも不正解ルートに行くことが実際に動かしてみるとわかります。(ちなみに、このプログラムはデバッガ外で最初のパスワードを正しく入力しても、「Your password is correct!」と表示され、終了してしまいます。)
そこで、もう少し分岐命令について踏み込んでみましょう。
今回分岐に使われている命令はjz命令、又はjnz命令ですね。前半での説明は省いていましたがこれらの命令は
jz ... (Jump if Zero命令:ゼロフラグ(ZF)が1のときに、指定先に分岐する)
jnz ... (Jump if Not Zero命令:ゼロフラグ(ZF)が0のときに、指定先に分岐する)
という意味の命令なのです。しかし、このゼロフラグとはどういうものなのでしょうか?
今までのアセンブリコードを見返してみると、分岐命令の前に、よくtest,cmpといった命令があるのがお分かりでしょうか?これらの命令は2つの引数をとり、それら2つを比較し、結果によってフラグレジスタという部分の値を変化させます。(厳密に言うとtestはオペランド2つの論理積を取り、cmpはsub命令と同じように減算を行います。両方共結果はオペランドには格納されず、フラグレジスタの値を変更させるに留まります)そのフラグレジスタの一種として、ゼロフラグと呼ばれるものが存在し、後置されたjz、jnzの各分岐命令はその値を読み取り分岐することで、アセンブリコード上で条件分岐を成り立たせている、ということです。
お気づきかとは思いますが、これらの分岐命令は全く逆の意味の命令ですね。そして画像から、当分は分岐命令でジャンプしている先が正解ルートの連続だと考えられるため、「jz命令はjnz命令に、jnz命令はjz命令に」書き換えてあげれば、デバッガ検知による条件分岐は全て裏目に出て、書き換える前とは常に逆の分岐をしていくことになります。(もちろんjmp命令のような無条件分岐命令に書き換えるのも有りかもしれませんが)
このように、この先も命令群の最後の分岐命令にブレークポイントを置きつつ、分岐の方向を確かめ、それぞれの場合によって「nopによる命令の上書き」「jnz jz命令の入れ替え」をIDA(+stirlings)で地道に行っていくと…
flagが表示されましたね。というわけで今回のflagは
「SECCON{check_Ascii85}」
でした。
実は、この問題。抜け道があります。高機能なGUIデバッガの使い方を知っていることで、手間を省略できるのです。
使用するのは「Immunity Debugger」です。
まずは先程と同じように、IDAで分岐の流れを追っていき、大量のデバッガ検知の網の先にある、ある命令群に目をつけます。
他の命令群と比べて、比較的長いですね。それにIDAがコメントで謎の文字列を扱っていることを示しています。ここでflagの生成をしてるのかな?と当たりを付けてこの命令群の開始アドレス(画像だと見えていませんが、00401663)を記録しておきます。次に、それまでの流れとはちょっと変わったものを呼び出している箇所をその分岐先に見つけます。
赤文字の部分、MessageBoxA を呼び出していますね。これは一体何でしょうか?(すっとぼけ)
はい。これが先程flagを表示するために使われたダイアログボックスの正体ですね。こいつのアドレス(00401737)も控えておき、Immunity Debuggerでファイルを開いてみましょう。
そして先程のflag生成箇所と思われる部分の先頭アドレスを探し、右クリックして「New Origin here」を選択します。これでプログラムをこのアドレスから実行することができます。
後はもう一つのMessageBoxの命令の近辺のアドレスにブレークポイントを置いて、実行してみると
flagが表示されましたね。(2回目)
flag生成とそれを表示するための処理の間にデバッガ検知が存在しない、ということに気づくと、このようにflagを得るために必要な部分を切り抜いて実行して、案外あっさりと解いてしまうことが可能というわけです。
5.終わりに
如何でしたでしょうか?
今回はbin編ということで、ローカルな環境でバイナリファイルを編集・解析することでflagを奪取しました。もう少しアセンブリやツールの使い方について掘り下げたかったのですが、如何せんキリがないので大分雑な紹介になってしまいました。そんな中でも、大体の流れから、バイナリの編集・解析によってプログラムに自分の思い通りの動作をさせる技術とその楽しさを知って頂ければ幸いです。
また、悪い人たちは今回扱ったような知識(ここまで簡単ではありませんが)を悪用してゲームのチートや海賊版などを作成したりしているわけです。(勿論今回の内容は悪用厳禁です)いくら優秀なパッカーや難読化を施しても、IDAのような高機能な逆アセンブラや、優秀な人達の前では絶対安全な対策をするのは難しいのが現実です。しかし、そういった人達の目線に立った時、「~をされたら解析しづらいなあ」「こういうことをされると面倒だなあ」ということを知っているのと知っていないのとでは大きな違いがあります。そういったことを学ぶのがCTFの目的である、と私はどっかで読んだ記憶があります。(適当)実際私の周りにもゲームを作っている方が多いので、今度はそういった解析からの防衛技術なんかについても何処かで取り上げたいなあと思っています。全ては私のこれからの勉強の進捗によりますが…
まあいつも通り雑な解説でしたが、ここまでお付き合い頂き、有難うございました。間違っている点・意見等御座いましたらコメント等でご指摘頂けると幸いです。