CPUのアーキテクチャから徹底解説 バッファオーバーフローで何が起こるか 第7回~対策をすりぬける攻撃(後編②)
前回、第6回では、バッファオーバーフロー対策をすべてすりぬける攻撃例の紹介をしました。今回は、前回紹介した攻撃に使うコードのPythonスクリプトについての詳細をまずは解説するところからスタートします。
コードは第6回を確認ください。
Step1 データ漏えいを意図した文字列を準備する
ここでは脆弱性コードに下記の値を漏えいさせる「仕掛け」を準備します。
- SSP対策をすり抜けるためのcanary値
- main関数のリターンアドレス (libcアドレス算出に使用)
以下、図6の①から④に従って説明します。
図6 ①の数値を入力し②のbuf_sizeにセット
脆弱性コード12行目が求める最初の文字列入力には、図6の①の数値(buf_size=21=0x15)を入力します。図6の②の状態は、脆弱性コード12行目を実行され、buf_sizeに21がセットされた状態を表します。
図6 ③の文字列を準備
図6 ①のbuf_sizeに相当するbufferには、図6の③の文字列を入力します。この入力文字列のうち“%21$08x”(= 0x25323124303878)は、脆弱性コード17行目のprintf()関数実行時に書式文字列として動作します。この書式文字列は、printfが解釈する21番目の文字を出力するのですが、終端文字 “0x00” を改ざんすることで、スタック上で制御情報のフレームポインタに書かれたmain関数のリターンアドレスが出力されます。また終端文字の改ざんにより、canary値も出力されます。詳細は[脆弱性コード17行目のprintf()実行で漏えいが起こる]で後述します。
図6 ④の通りbuffer領域を超えてline領域までセット
脆弱性コード15行目の実行時に、脆弱性No.1 (前編の表1参照)が起こり、図6の④のようにbufferの大きさを超えてlineの領域までbuffer領域がセットされます。また図6 ③は、脆弱性コード16行目の実行時に脆弱性No.2とNo.3 (前編の表1参照) を顕在化させるため、冒頭に書式文字列 “%21$08x” (7バイト) と“A” の14連結文字の計21バイトで構成しています。
図7 ⑤で示す範囲がコピーされ⑥の状態に
図6 ①から④によるbuf_sizeとbufferの状態で、脆弱性コード16行目のstrncpy()関数実行で何が起こるでしょうか。脆弱性コード 12行目で文字数 (=21) の入力を受けたあと、16行目でbufferの内容をlineにコピーしますが、図7の⑤のとおりbufferに書かれた文字は21文字でありコピー文字数(buf_sizeの値)も21です。図7 ⑥のように20文字の大きさのlineに21文字をコピーされると、結果としてcanaryのリトルエンディアン最上位バイト 0x00が文字 “A” (=0x41) で上書きされます。
以上からわかるように、脆弱性コード15行目まで (図6 ①から④) の状況で、脆弱性コードの16行目(strncpy())を実行すると図7 ⑥の状態となります。この状態で脆弱性コード17行目 (printf(line))が実行されると、制御情報のリターンアドレスとcanaryの漏えいが起こります。これについて以下で説明します。
脆弱性コード17行目のprintf()実行で漏えいが起こる
脆弱性コード17行目のprintf()関数は、冒頭の”%21$08x” (= 0x25323124303878; 図8の□で囲った全7バイト部分)を書式文字列として解釈し、21番目の変数を “08x” で出力されます。1番目の変数は、printf()実行時のスタック上のarglistが指すアドレスとなり、21番目はちょうど、printf()実行後にフレームポインタが指すアドレス、つまり、__libc_start_main() へのリターンアドレスとなります。 またline領域には終端文字 “0x00” がすでに “0x41” に改ざんされていますので、書式文字以降の出力と共にcanaryも出力されます。以上より、ここでは__libc_start_main()へのリターンアドレスである図8⑨の「0xb6e55718」と、書式文字以降の”A” 14連結に続き、canary3バイトも併せて出力され、ここでの出力全体は図8 ⑨となります。
Step2 漏えいしたデータをヒントに脆弱性コードに実行させたい標準関数のアドレスを計算する
攻撃コードは、脆弱性コード17行目のprintf()実行で漏えいしたリターンアドレスを利用して、標準Cライブラリlibcのアドレスを求めます。リターンアドレスは図8の⑨のとおり “0xb6e55718”であり、これはmain()関数を呼び出したアドレス、すなわちlibcの__libc_start_main() への戻りアドレスとなります。なお、ここではC言語のmain()関数とlibc、__libc_start_main()関数の詳細な説明は割愛します。
この呼び出し元がlibcの先頭アドレスからのオフセット、つまりどれだけ離れているかは、libcのアセンブリコードから事前に求めておきます。アセンブリコードの見方などの説明は割愛します。
さて、ASLR(+PIE)対策(前編の表1参照)では、libcの配置アドレスはランダムではあるものの、オフセットは不変です。これより、__libc_start_main()関数 への戻りアドレスからlibcが配置されているアドレスが求められます (詳細は攻撃コード内のコメント行を参考にしてください)。
Step3 バッファオーバーフロー攻撃の文字列を組み立てる
攻撃コードは、libcの配置アドレスをもとに、攻撃用のデータを作っていきます。バッファオーバーフローのNX対策 (前編表2を参照) によって「/bin/sh」の実行コードを直接スタックには配備できませんので、「rop (Return Oriented Programming) gadget」命令群 “pop {r0, r4, pc}” を持つlibc関数を利用し、この「ROP gadget」から「/bin/sh」を間接的に実行させることにします。そして、脆弱性コード23行目のreturn時に、main()に戻るアドレスを「ROP gadget」実行アドレスに改ざんし、return実行時に不正実行させるようにします。
表4に、攻撃データをまとめています。
No. | 用途 | 生成方法 |
Atk-1 | SSPをすり抜けるための、実行時のcanary値。 | 漏えいしたデータの末尾3バイト(図2 ③の赤字)を得る。正しいcanary値として、このデータ+”00”をセットする。 |
Atk-2 | リターンアドレス上書き用の、libc内のrop gadget “pop {r0,r4,PC}”命令アドレス。Atk-4を実行させるために使う。 | 事前にrop gadget命令のオフセットを調べておく。今回判明したlibc配置アドレスにオフセットを足せば、目的のrop gadgetのアドレスとなる。 |
Atk-3 | libc内にある”/bin/sh”文字列のアドレス。System()にこの文字列を渡し、「/bin/sh」を実行させるために使う。 | “/bin/sh”文字列があるアドレスのオフセットを調べておく。アドレスの求め方はAtk-2と同じ。 |
Atk-4 | libcのsystem()関数のアドレス。No.3の「/bin/sh」を実行させるために使う。 | System()関数のオフセットを調べておく。アドレスの求め方はAtk-2と同じ。 |
これらのデータを元に、バッファオーバーフロー攻撃の文字列を作ります。脆弱性コード20行目のgets()実行時に図9の状態になります。これは、脆弱性No.5 (前編表1を参照ください) の脆弱性を利用した、いわば攻撃直前の状態です。
Step4 バッファオーバーフロー攻撃の実行
攻撃コードが仕掛けた図9の状態で、脆弱性コード23行目のreturn実行によって、オーバーフロー攻撃が開始されます。バッファオーバーフロー攻撃を実行します。実行時の動作の様子を図10に示します。
図10⑩〜⑫のとおり、この攻撃データはバッファオーバーフローの対策を、すべてすり抜けます。攻撃実行後の動きは以下のとおりです。
- 脆弱性コード23行目のreturn実行時に、vuln関数からmain()関数に戻ることなく、リターンアドレスに従い、図5 ⑪のアドレスの「rop gadget」を実行
- 「rop gadget」は図5 ⑫の値をそれぞれr0,r4,pcレジスタに格納。r0レジスタには「/bin/sh」文字列のアドレスが入り、pcレジスタにはsystem()関数のアドレスが入る(r4には“AAAA”が入るが、このレジスタは未使用となる)
- pcレジスタの値の通り、system()関数が実行される。System関数はr0レジスタのアドレスの文字列「/bin/sh」を実行する
攻撃コードはこの後に「/bin/sh」が持つ「id」コマンドを発行します。攻撃コードを実行したときのログを以下の図7に示します。今までの説明通りに、実際に脆弱性コードに「id」コマンドを実行させられることが確認できます。
なお、ここでは「id」コマンド発行の例を紹介していますが、攻撃コードの115行目を工夫すれば、別のコマンドも発行できます。例えば、機密情報を不正にメール送信したり、不正なリモートログインしたコマンド実行なども可能です。
「攻撃のスキーム」をふりかえる
今回の攻撃のスキームでは、脆弱性を巧みに利用し、最後には「/bin/sh」を実行させることに成功しました。
- 脆弱性No.1(前編の表1参照、以下、同様)を利用し、サイズ超の文字列を受け付け
- 脆弱性No.2を利用し、終端文字を超えた入力を仕掛ける
- 脆弱性No.3のImproper Null Terminationで後続のcanary値を漏えいさせる
- 脆弱性No.4のFormat String Attackでリターンアドレスを漏えいさせる
- 脆弱性No.5の「rop gadget」命令を起動し「/bin/sh」の不正実行
根本対策はあるのか
以上のように、脆弱性コードが持つさまざまな脆弱性を利用した攻撃が可能である点を示しました。バッファオーバーフロー脆弱性は、ユーザ入力の入り口の脆弱性です。このような脆弱性はぜひとも根本から対策したいところです。
しかしながら、根本対策の対象であるCPUについては、現時点で例えばIntelの第11世代CPUやARMv8.3に限定されています(これらに装備されているControl-Flow Enforcement Technology機能やPointer Authentication機能は、今回のような攻撃からコードを保護します)。これらのCPUはサーバやパソコン、スマートフォンなど、リソースとして比較的リッチな環境で用いられることの多いCPUです。
一方、小型デバイスやIoTデバイスなどの多くのデバイスでは、現在まで根本対策が十分施されていないのが実情です。例えば上記のARMv8.3でサポートされるPointer Authenticationを採用する場合、32ビットのコードは64ビットへのポーティングが必須となり、低価格なIoTデバイスなどへの利用の障壁となります。このような事情もあり、これらのデバイスでソフトウェアを開発するときは、バッファオーバーフロー脆弱性を持たないよう、コードレビューやテストを手厚くしています。
おわりに
本シリーズでは、プログラムの動くしくみから、バッファオーバーフローの脆弱性とその攻撃について解説してきました。バッファオーバーフロー攻撃の要点は以下のとおりです。
- バッファオーバーフロー脆弱性を用いて他の脆弱性を突く
- バッファオーバーフローによって制御情報を書き換えられる
- 制御情報の書き換えにより、コードは攻撃者の意図通りに動作する
- 小型・IoTデバイスの根本対策は現在まで十分に施されていない
最後になりますが、本記事に関連して当社(株式会社ATTC)製品を紹介致します。当社は小型デバイスやIoTデバイス向けに、バッファオーバーフロー攻撃の根本対策として 「ATTC Control Flow Integrity」を開発しました。本記事で実演したバッファオーバーフロー攻撃を防ぎます。製品紹介へのリンクを挙げますので、目を通していただけますと幸いです。
以上で本連載を終了します。今までご覧になっていただき誠にありがとうございました。
著者
株式会社ATTC(エーティーティーシー) 尾關晃充
元々はWebアプリケーションのサーバサイドエンジニアだった。
ATTC入社後はセキュリティ製品開発に携わっている。
現在は「ATTC Control Flow Integrity」の開発担当を務め
セキュリティや脆弱性対策に取り組んでいる。
株式会社ATTC(エーティーティーシー) 飯嶋弘久
暗号をコアとする情報セキュリティの理論と応用、決済セキュリティ、組み込み系IoT等の専門領域でR&D、コンサルティング、テクニカルサポート等に従事。工学博士(電子工学)
「CPUのアーキテクチャから徹底解説 バッファオーバーフローで何が起こるか」の最新話は、メールマガジンにてもご案内致しています。是非JAPANSecuritySummit Updateのメールマガジンにご登録ください。
メールマガジンの登録はこちらからお願いします。