はじめに
こんにちは。基礎技術研究室リサーチエンジニアの中川です。前回に引き続き、Black Hat EU 2020 の参加報告記事となります。
今回は私が聴講して気になった、興味深い発表について紹介します。
C/C++ で未定義の動作となるソースコードの箇所を特定するためのツール開発
Finding Bugs Compiler Knows but Doesn't Tell You: Dissecting Undefined Behavior Optimizations in LLVMは C/C++ で未定義の動作 (undefined behavior; 以下 UB) の箇所を特定するツール開発、及びそのツールを使って実アプリケーションをスキャンし、脆弱性を発見したという発表でした。
C/C++ で UB を引き起こすプログラムは、処理系がいかなる動作をしても規格には抵触しません。これについては、「鼻から悪魔」という冗談があります。要するに、UB を引き起こした際に「鼻から悪魔を出す」処理系だとしても、そうした挙動が起こってしまうのはプログラム作成者の責任ということです。
発表中では UB を引き起こすソースコード例として次のようなものが挙げられています。
class A { public: void read(int x) { int *addr = internalRead(x); printf("0x%x\n", *addr); } private: int* internalRead(int x) { if (x < 0 || x >= 100){ return nullptr;} return array+x; } int flag = 0xdeadbeef; int array[100] = {0}; }; int main() { A a; a.read(-1); }
一見、以下のような流れでセグメンテーション違反を起こしてプログラムが終了するだけに思われます。
read
関数の引数として -1 が入っているのでinternalRead
関数はnullptr
を返す*addr
でnullptr
にアクセスするため、セグメンテーション違反が起こり、プログラムが終了する
しかしながら、これを clang++ で最適化レベルを変化させコンパイルし、実行すると以下のようになります。
$ clang++ --version clang version 10.0.0-4ubuntu1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin $ clang++ -std=c++11 ub.cpp $ ./a.out Segmentation fault $ clang++ -O3 -std=c++11 ub.cpp $ ./a.out 0xdeadbeef # セグメンテーション違反が発生せず、なぜか deadbeef と表示される
最適化レベルがデフォルトの場合は "Segmentation fault" と表示され、プログラムが終了します。しかし、最適化レベルが -O3
の場合、0xdeadbeef
と表示されます。
これは一体どういうことでしょうか。
注目すべきは以下の箇所です。
void read(int x) { int *addr = internalRead(x); printf("0x%x\n", *addr); // <-- null pointer dereference }
addr
に nullptr
が入っている場合、上に矢印で示した箇所において Null Pointer Dereference が発生します。これは C/C++ において UB となります。
そのため、UB を引き起こす internalRead
関数内で境界チェックを行う処理が最適化の過程で落とされ、internalRead
は単に return array+x;
を実行するだけとなります。
境界値チェックがなく、x
が -1 の場合、array
の直前にある flag
変数のアドレスが internalRead
の戻り値として返ります。よって、read
関数の printf
では 0xdeafbeef
と表示されたというわけです。
これだけでも驚くべき結果なのですが、この手の UB が発生している箇所はコンパイラの警告レベルを最大にしても指摘されないことが更に驚きです。-Wall -Wextra
などで警告レベルを最大限引き上げたとしても、この手の問題は一切指摘されません。
コンパイラによる UB の指摘は、例えば clang++ であれば -fsanitize=undefined
を指定することで行えます。
$ clang++ -O3 -std=c++11 -fsanitize=undefined ub.cpp $ ./a.out ub.cpp:7:20: runtime error: load of null pointer of type 'int' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ub.cpp:7:20 in Illegal instruction
しかし、このサニタイザーは実際に UB を踏まない限りその問題を指摘しません。UB を含む領域が実行されなかった場合、上記の様な指摘は行われません。
この発表では LLVM に手を入れることで、UB の発生箇所を特定するためのツールを作成しています。 例えば先の Null Pointer Dereference に対しては、下図に示すようなフックを導入し、ユーザーに UB の存在を通知します。
このような形で、その他 Integer Overflow などの UB を検知できるようにし、実アプリケーションの脆弱性を発見したという発表でした。
一方、現時点でツールが公開されていないところが残念です。C/C++ でプロダクトを開発している方にとって、需要は高いはずですので、公開されることを期待したいですね。
静的アプリケーション解析ツール評価用のベンチマーク
FPs are Cheap. Show me the CVEs!では Static Application Security Testing (SAST) ツール評価用のベンチマークデータセット OpenSSF CVE Benchmark について発表が行われました。発表者の 1 人は著名なコード解析エンジン CodeQL のプロダクトマネージャーで、元 Semmle (現在は GitHub に買収) 所属です。
この発表では既存の SAST ツール評価用ベンチマークの問題点についての紹介を通じ、なぜ OpenSSF CVE Benchmark を作成・提案したのかが説明されました。
SAST ツール評価用のベンチマークとしては、例えば Juliet Test Suite があります。
test suite には上に示すようなフォルダ構成で CWE ごとに C/C++ のソースコードが含まれています。例えば、CWE-190: Integer Overflow or Wraparound の例を見てみましょう。
void CWE190_Integer_Overflow__int_fgets_multiply_01_bad() { int data; /* Initialize data */ data = 0; { char inputBuffer[CHAR_ARRAY_SIZE] = ""; /* POTENTIAL FLAW: Read data from the console using fgets() */ if (fgets(inputBuffer, CHAR_ARRAY_SIZE, stdin) != NULL) { /* Convert to int */ data = atoi(inputBuffer); } else { printLine("fgets() failed."); } } if(data > 0) /* ensure we won't have an underflow */ { /* POTENTIAL FLAW: if (data*2) > INT_MAX, this will overflow */ int result = data * 2; printIntLine(result); } }
fgets
関数で文字列を受け取り、atoi
関数で int 型の変数に変換した後、2 倍した値を出力するコードとなっています。
コメントで "POTENTIAL FLAW" と記載されていますが、fgets
で受け取る入力値によっては INT_MAX
を超え、オーバーフローが発生するコードとなっています。
このように CWE ごとにテストケースとなる C/C++ のソースコードが用意され、これらの問題点を正しく検出できるかを確認し、SAST ツールの評価に利用できます。
しかし、ご覧の通りこうしたベンチマークに含まれる脆弱性は人為的に作り込まれたもので、実際に OSS や製品で見られる脆弱性につながる問題のあるコードとはかけ離れています。 そのため、ベンチマークに含まれるコードをすべて検出できる SAST ツールにしてしまうと、OSS や製品コードに SAST ツールを適用した際、FP が多く発生するとのことでした。
更に従来のベンチマークには、脆弱性を修正しパッチを当てた後のコードが含まれていないことも問題点として挙げられていました。パッチが提供されていない場合、パッチ適用後に指摘されなくなることを確認できません。
そこで、「実際に報告された脆弱性・その脆弱性に対するパッチをセットにしたデータセットを提供すればよいのでは」という発想に発表者は至り、そのようなデータセットとして OpenSSF CVE Benchmark を提案しています。
現在は JavaScript と TypeScript のみが対象となっていますが、今後データセットを拡張していく予定とのことです。 また、複数の SAST ツールの性能比較用に CLI tool も提供しており、ツール間の性能比較はコマンドを叩けば一発でできるようになっています。
発表の最後ではフィードバックを求めているということが強調され、将来的な他の言語のサポートについても言及されました。 データセットの拡張についてのプルリクエストも歓迎とのことです。
先程紹介した UB 検出ツールの発表とも関係しますが、C/C++ の UB で生じた脆弱性をデータセットとしてまとめ、既存の SAST ツールでどの程度検出できるのかを調べると面白いかもしれません。
静的解析による UEFI ファームウェアに存在する脆弱性の自動検出
efiXplorer: Hunting for UEFI Firmware Vulnerabilities at Scale with Automated Static Analysisでは、UEFI ファームウェアの静的解析と脆弱性を持つ可能性があるコードパターンの自動検出を支援する IDA Plugin である efiXplorer について発表が行われました。 ちなみに、この IDA Plugin は今年の IDA Plug-In Contest で 3 位を受賞しています。
UEFI ファームウェアの脆弱性は攻撃者に非常に強い CPU 実行モードでのコード実行を可能とする場合があります。その 1 つに SMM コールアウト脆弱性があります。 この発表の紹介の前に、まず SMM コールアウト脆弱性について説明します。
SMM とは Intel System Management Mode の略称で、ファームウェアにおける利用を想定している x86/x64 プロセッサの動作モードです。 主にシステムの電源の管理とハードウェアの制御を行います。 このモードは x86/x64 プロセッサにおいて事実上最も強力なモードとなっており、物理メモリへのフルアクセスが可能となります。
SMM でのコード実行がなぜ攻撃者にとって魅力的であるかといえば、このモードはユーザーアプリケーション・OS から透過的に動作するためです。 また、SMM で実行されるコードは SMRAM という隔離された領域に格納されることが仕様で定められており、この領域にはプロテクトモードからはアクセスできません。 そのため、SMM を悪用したルートキットは非常にステルス性が高いものとなります。
SMM でのコード実行はシステム管理割り込みにより行われ、この割り込み時に実行されるコードは SMI ハンドラーと呼ばれます。
SMM コールアウト脆弱性は SMI ハンドラーが SMRAM 以外のメモリ領域に存在するデータへアクセスし、例えばそこに含まれる関数ポインタを介して関数 call することにより起きる脆弱性です。 この関数ポインタがプロテクトモードで改竄可能な領域に存在する場合、攻撃者はプロテクトモードから SMM に権限昇格した上でコードを実行できます。
efiXplorer はこうした SMM コールアウト脆弱性といった UEFI ファームウェアの脆弱性を持つ可能性があるコードパターンを自動的に検出します。 具体的には以下の 2 つのステップを通じて行います。
- UEFI ファームウェア全体から UEFI のプロトコル・SMI ハンドラーのアドレスを特定
- UEFI ファームウェアイメージに存在する脆弱性を持つ可能性のあるコードパターンの検索
- 例えば、SMM コールアウト脆弱性の場合であれば、SMI ハンドラー内で BootServices 構造体などの SMRAM 以外の領域にアクセスしている命令列を検索するなど
これまで UEFI ファームウェアイメージ全体から UEFI プロトコル・SMI ハンドラーの特定は手動で行われていました。 そのため、SMI ハンドラーの特定から SMM コールアウト脆弱性を検出まで自動的に行うことは困難でした。 efiXplorer はこの問題を解消し、自動的に行えるようにした点が画期的と言えます。
efiXplorer による SMI ハンドラーを特定する例を以下に示します。
下図は SMI ハンドラーを登録する EFI_SMM_SW_DISPATCH2_PROTOCOL.Register
関数呼び出しの付近のデコンパイル結果です。efiXplorer を有効にした状態でデコンパイルしています。
EFI_SMM_SW_DISPATCH2_PROTOCOL.Register
関数引数として渡される関数ポインタから SMI ハンドラーが特定され、SwSmiHandler_90F480
というラベルがついていることがわかります。
その他、SMM コールアウト脆弱性以外についても、EFI 変数を取得する関数 GetVariable のスタックバッファオーバーフロー脆弱性の自動検出する例も発表では紹介されていました。
今後の展望としては、UEFI ファームウェアに典型的な脆弱性のコードパターンの検索を命令列に対してではなく、中間表現に対して行うことが挙げられていました。 IDA Pro であれば、microcode と呼ばれる中間表現が逆アセンブルエンジンに使われており、これを用いたデータフロー解析が可能となっています。 こうした中間表現での検索により、標準的な呼び出し規約に則らないコードであっても、脆弱性を持つ可能性のあるコードパターンの検索が可能となります。 更に、アーキテクチャに依存しない形の検索が行えるため、対象を x86/x64 以外のアーキテクチャ向けのファームウェアイメージにも広げられるというメリットもあります。
ちなみに、Ghidra にも IDA Pro の microcode に相当する中間表現として p-code があり、これを用いて同様にデータフロー解析ができます。 この p-code を用いたデータフロー解析も強力で、簡単な例は Alexei 氏による記事 "WORKING WITH GHIDRA'S P-CODE TO IDENTIFY VULNERABLE FUNCTION CALLS" で紹介されていますが、十分に活用された事例はまだまだ少ないという印象です。 Ghidra の p-code を活用して同様のことができるのかは気になるところです。
おわりに
Black Hat EU 2020 に参加して聴講し気になった発表を紹介しました。 今回紹介できませんでしたが、その他にも興味深い最先端の研究が発表されておりました。 スライド・アブストラクトは公式 HPから閲覧が可能ですので、今回参加できなかった方も確認可能です。
次に Black Hat 参加する際は、ぜひともロンドン (もしくはラスベガスかシンガポール) へ行き、登壇・発表した後、ほかの発表を聴講しスピーカーと対面で議論がしたいですね。
エンジニア募集
FFRIセキュリティではサイバーセキュリティに関する研究開発を希望するエンジニアを募集しています。採用に関してはこちらをご覧ください。