exploit入門
本ページでは、とても簡単な問題を通してバイナリ解析やexploitの入門を行います。 本ページの内容は以下のとおりです:
Challengeのダウンロード
このリンクからChallengeをダウンロードします: DOWNLOAD.
ダウンロード後、以下のコマンドでファイルを展開してください:
| |
また、リモートサーバに繋ぐことができるかどうかも以下のコマンドで確認してください:
| |
Warning: Challengeファイルの安全性
CTF等では、運営が用意したファイルをダウンロードして実行することになります。 CTFをやっているような人間が全員倫理的でsafeな人間であることは保証できないため、知らない人から渡されたファイルはしっかり事前に確認してください。 本サイトに関しては、ファイル名にSHA256ハッシュを含めているため少なくとも改ざんされていないことは保証できます。 その上でそのファイルが安全であるかどうかは、@smallkirbyを信頼するかどうかに依存します。Ghidraを用いたバイナリの解析
Ghidraに解析させる
本講義で扱うchallngeは全てソースコードを添付するため、バイナリ解析自体はあまりしません。 でも折角なのでここで少しGhidraに触ってみましょう。
まずはGhidraを開いて、File > Import Fileからダウンロードしたバイナリsimplestをインポートします。

趣味の悪いドラゴンのアイコンをクリックすると、プロジェクトが開きます。バイナリを解析しますか?的なプロンプトが表示されるため、Analyzeを押してください。

画面左のSymbol Treesからmainを選択すると、main関数の逆アセンブリが表示されます。

main関数の解読
1つずつmain関数を読んでいきましょう。
| |
最初のsetvbufはpwn問題のおまじないみたいなもので、入出力のバッファリングを無効化してくれます。
続くfopenでは/dev/urandomをRモードで開いて、失敗したらperror > exitしていますね。テンプレです。
その後、freadでlocal_164に対して4byteだけ/dev/urandomから読んでいますね。
Note: 変数のrename
Ghidraはデコンパイル時に、一定の規則名に従って変数名をつけてくれます。local_164などは、スタックの高さ164にある変数名です。
Ghidraでは、デコンパイル画面で変数名をRename Variableを選択すると変数名を変更することができます。
デフォルトの名前はとても読みにくいので、適宜変数名を変更することをおすすめします。urandomから読み込んだ値を使ってsrandをして、rand()の返り値をlocal_15cに代入しています。これはrand()で乱数を取得する際の常套手段です。
| |
次に、scanfでlocal_148に対して文字列を読み込んでいます。DAT_00102037は%30s文字列です。

その後、strcmpで入力文字列がAAAAAAAAかどうかをチェックしています。異なる場合にはexitしていますね。
| |
次も同じような感じです。先程生成した乱数local_15cを表示して、ユーザの入力が乱数と一致するかどうかを確認しています。
| |
最後のプロンプトです。最初にバッファとして使っていたlocal_148をクリアしています。そのあと、0x12345678ULをリトルエンディアンで入力するように促し、入力が正しいかをチェックしています。
Note: デコンパイルと型
Ghidraのデコンパイル時の変数型推測はベストエフォートです。 実は上の例において、long local_148[6]はソースコードではchar buf[0x30]と定義されています。
しかし、ソースコード中で*(unsigned long*)bufとしてアクセスする箇所があるため、Ghidraくんはこの型をlongとした方が都合が良いと解釈したみたいです。 | |
最後に、flagをRモードで読み込んで出力しています。ここまでプログラムの実行が到達するとフラグが得られるみたいですね!
| |
こいつは関数から返る時のお決まりみたいなもので、スタック上のカナリアを見てしてスタックが壊れていないかどうかをチェックしています。
pwntoolsを用いたExpoloit
ローカルサーバを建てる
さて、ここまででバイナリの解析は終わりました。入門プログラムなので、簡単な入力を3回するだけでフラグが貰えるようです。 ここからはpwntoolsを用いてローカルでExploitを書いていきます。
まずはローカルサーバでchallengeを動かしましょう。以下のスクリプトをsocatという名前で保存し、実行します。
| |
これを実行するとローカルホストのポート12300でchallengeサーバが起動するため、ncで接続してみましょう。
| |
無事に接続できたらOKです。
pwntools template
pwntoolsを用いたexploitのテンプレートは以下のようになります:
| |
以下、exploitコードはexploit()関数の中身だけを抜粋して書いていきます。
ひとまず本コードをexploit.pyという名前で保存してください。
その後、EDIT HEREと書いてある部分を適宜修正します。
chmod +x ./exploit.py && ./exploit.pyで実行してみましょう。以下のような画面が出力されればサーバとの接続に成功し、exploitの準備ができています。

プロンプト1: 出力を待って文字列を入力
最初のプロンプトはInput 'AAAAAAAA' > でした。先程バイナリを読んだ通り、この文字列を入力すれば次のプロンプトに進めます。
pwntoolsでは、recvuntil()メソッドで特定の文字列が出力されるまで出力を消費することができます。
また、sendline()メソッドで任意バイトを改行付きで出力することができます。
なお、sendlineメソッドはbyte型を受け付けますが、Python3では文字列をバイトにするためにbを付ける必要があることに注意してください。ptrlibを使うと、そのへんをいい感じにラップしてくれるらしいです。
| |
プロンプト2: 出力をパースして入力
続くプロンプトは最初に生成した乱数を0x%Xフォーマットで出力し、10進文字列として入力させるものでした。
まずはrecvuntil()メソッドで乱数の手前まで出力を消費します。その後、recvuntil(" ")で乱数を読みます。" "は、乱数の終わりを識別するためのトークンで、[:-1]とすることで消去します。読み込んだ出力はあくまでも文字列なので、int()メソッドで文字列に変換します。最後に、sendline()で10進文字列として入力を与えます。先ほどと同様、sendline()の引数はbyte型である必要があることに注意しましょう。
| |
プロンプト3: エンディアンに注意
最後のプロンプトは、0x12345678という整数値をリトルエンディアンで出力するというものでした。
pwntoolsでは、context(os='linux', arch='amd64')とすることでアーキテクチャを指定できます。
amd64を指定した場合には各種メソッドが整数値をリトルエンディアンとしてエンコードしてくれるようになります。
この設定の後に、p64メソッドを使うことで整数を64bit整数型としてエンコードすることができます。
| |
Local Exploit
ここまででexploitが作成できました。実際にローカルで動かしてみましょう。
| |
ローカルにflagファイルを置いていない場合には、socatを動かしているターミナルにcan't open fileのような表示が出るはずです。これが出力されればOKです!
GDBでデバッグ
この問題では特にGDBを使うまでもありませんが、せっかくなので使っておきましょう。 今回は3つ目のプロンプトに対してexploitで入力を与えた直後をデバッグしたいと想定します。
まず、exploit側のデバッグしたい箇所にinput()を入れて実行を一時停止します:
| |
exploitを行うと、WAITINGと表示された後に実行が止まります。これが表示されたら、psコマンドを用いてsimplestプロセスを探します:
| |
続いて、他のターミナルを開いてPID 129883 のプロセスにアタッチします。
| |
pwndbgやgefを利用している場合には以下のようなイカつい🦑画面が出ます:

スタックトレース(btコマンド)から分かるように、現在はscanfの途中で実行を止めています。まずはmain関数に戻りたいため、finコマンドを実行します。finは現在の関数から抜けるまで実行を進めてくれます。finの実行後、GDBが以下の表示をして止まります:

これは先程input()でexploitの実行を止めたためです。exploitをしているターミナルでEnterを押して実行を再開させましょう。mainまで戻ってくると、続く命令は以下のようになります:
0x000056380a2b751a e8b1fcffff <main+0x231> call 0x56380a2b71d0 <__isoc99_scanf@plt>
0x000056380a2b751f 488d85c0feffff <main+0x236> lea rax, [rbp - 0x140]
-> 0x000056380a2b7526 488b00 <main+0x23d> mov rax, QWORD PTR [rax]
Ghidraで見た感じだと、scanfの次は入力を0x12345678と比べるはずです。そのため、lea rax, [rbp - 0x140]のrbp - 0x140こそがユーザ入力が格納されるアドレスであることが分かります。このアドレスに何が入っているかを見てみましょう。
GDBではx/<num><format>コマンドでメモリをダンプすることができます。
| param | 説明 |
|---|---|
<num> | 何個分表示するか |
<format> | 表示フォーマット。b/h/w/gでバイト/ハーフワード/ワード/ダブルワード。sで文字列。iで命令。xで16進数。 |
まずはx/16bx $rbp - 0x140で、rbp - 0x140から16bytes分を1byteずつ表示しましょう:
gef> x/16bx $rbp - 0x140
0x7fff2ceff940: 0x78 0x56 0x34 0x12 0x00 0x00 0x00 0x00
0x7fff2ceff948: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
入力には0x12345678を入れたはずですが、上の表示では0x78 0x56...と逆順で入っています。
各CPUアーキテクチャはそれぞれエンディアンというメモリ上のデータ配置に関する決まりを持っています。
x64はリトルエンディアンのため、8byte整数値は上記のように逆順で入れられることになります。
なお、GDBのxコマンドはエンディアンをいい感じに解釈して出力してくれます。例として、x/2gxで2QWORD文を表示してみると以下のように0x12345678が表示されることが分かります。
gef> x/2gx $rbp - 0x140
0x7fff2ceff940: 0x0000000012345678 0x0000000000000000
リモートサーバへの攻撃
さて、ローカルでexploitが動いたので実際にリモートサーバに攻撃してみましょう。
上記のテンプレートを利用している場合には、hosts / portsをリモートサーバのものに変更したあと、以下のようにrを引数に渡して実行するとリモートサーバに繋げることができます:
| |
| |