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
を引数に渡して実行するとリモートサーバに繋げることができます:
|
|
|
|