デジタルペンテスト部の木田です。今回は開発者よりのお話です。
はじめに
プログラム開発を行うときにメモリの境界を越えてR/Wしてしまう事は時折存在します。例えば、入力されるデータの検証が不十分な状態で予期せぬ長さの文字列が入力され、用意されたメモリ領域に収まらない長さのデータが書き込まれてしまうと、重要なデータの上書きにつながります。この種の脆弱性はバッファオーバーフローと呼ばれており、「メモリ破壊」に分類される脆弱性です。
ここでは主にIoT機器を開発に従事する方々をターゲットとしてお話しますが、そうでない方々でも問題ないようにお話をしたいと考えています。
メモリ破壊系の脆弱性の問題
最も幸いなパターンはプログラムの異常終了です。エンジニアはプログラムが異常終了した箇所を特定して修正すれば事は済みます。とはいえ、これが大きなシステムの中の1プログラムだとするとエンジニアはバグチケットを握って回りに迷惑かけまいと必死にバグを探し出して修正するでしょう。
いわゆるウェブサービスに関連する機能や外部との通信結果から動作するようなプログラムで異常終了しなかった場合に何が起きるでしょうか。恐らく最悪の場合を想定するとリモートコードの実行(RCE)が行われてしまうかもしれません。Miraiのようなマルウエアの動作環境となってしまいます。
この点については脆弱性に繋がるセキュリティリスクという観点から、SEI CERT C Coding Standard/Rules for Developing Safe, Reliable, and Secure Systems/2016 Edition/2016, Carnegy Mellon University中ではDCL39-C Avoid information leakage when passing a structure across a trust boundaryを初めとしてメモリ操作を扱う各章においてコーディングルールの提示とアセスメントをせよという提言がなされています。
メモリ破壊系の脆弱性の早期発見方法
調査対象によって最適な方法は様々ですが、静的解析ツールの使用は有効な手段の一つです。IoT機器のようにネイティブな環境でしか動かないようなコードの場合には、これが最も早い発見方法といえます。
富士通株式会社のPG-Reliefや日本シノプシス合同会社のCoverityといった製品が著名であり企業内で使われているケースを多く見受けます。これら以外にも商用、オープンソースソフトウェアが多数存在します。
静的解析ツールは、ルールの設定さえ問題なければ明らかにメモリリークを起こしている箇所にはエラーを、メモリリークを引き起こすかもしれない箇所にはワーニングを出します。しかし、それでもネストした関数の中にある標準関数を利用した場合については見落とされる事もあります。
メモリ破壊系の脆弱性の特定
これで見つける事が出来ると良いのですが、一つ課題があります。IoT機器開発の場合に機器固有の設計コードがあるためにクロス開発環境上で動かない事があります。このため、先のSEI CERT C Coding Standardでは、コードをwrapperにより抽象化せよとしています。これを利用することで抽象化した箇所だけダミー関数をリンクするようにすればクロス開発環境上のネイティブコードとして実行可能となるでしょう。モジュール単位でGoogleTestで実行できるようにすれば、モジュールの入力パラーメータに対して全ての動作をテストする事ができます。
それでは、例を用いて、メモリ破壊系の脆弱性の検出手順を解説します。本記事ではvalgrindを使用します。valgrindは2002年にリリースされた、メモリ破壊系の脆弱性の検出に特化したツールであり、現在も継続的にメンテナンスされています。この記事を書いている段階では3.19.0が最新バージョンとなります。gdbとの連携、マルチプロセッサ環境上での並列処理のデバックなど様々な機能が追加されていますが、今回はバッファオーバーフローの脆弱性の検出に焦点を当てます。
valgrindはソースコードレベルで公式ホームページ上(https://valgrind.org)で公開されています。Linux上で動作するパッケージも用意されているのでディストリビューションのパッケージを利用するのも手取早くて良いでしょう。
valgrindはコマンドライン上で動作します。簡単なプログラムで試してみましょう。
まずは、C言語でのプログラムです。mallocでlocal_heap変数に領域を割り当てますが、わざとLOCAL_HEAP_SIZE-1分しか割り当てないようにしてます。この状態でmemset()を実行した場合、確保されたメモリ領域から1バイトだけデータが溢れてしまうため、バッファオーバーフローが発生します(余談ですが、1バイトだけバッファがオーバーフローしてしまう場合は特別に「Off-By-One」と呼ばれています)。この脆弱性を、valgrindで検出してみましょう。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 #define SUCCESS 0 6 #define FAILURE 1 7 8 #define LOCAL_HEAP_SIZE 64 9 10 void main(void) 11 { 12 int pg_status; 13 char *local_heap; 14 int heap_index; 15 16 pg_status = SUCCESS; 17 local_heap = (char *)malloc(LOCAL_HEAP_SIZE - 1); 18 if (local_heap == NULL) { 19 pg_status = FAILURE; 20 } 21 22 if (pg_status == SUCCESS) { 23 memset ( local_heap, 0, LOCAL_HEAP_SIZE ); 24 } 25 26 if (pg_status == SUCCESS) { 27 free( local_heap ); 28 } 29 30 }
valgrindの検知結果は以下のとおりとなります
==3985== Memcheck, a memory error detector ==3985== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. ==3985== Using Valgrind-3.20.0.GIT and LibVEX; rerun with -h for copyright info ==3985== Command: ./a.out ==3985== ==3985== Invalid write of size 8 ==3985== at 0x485217F: memset (vg_replace_strmem.c:1374) ==3985== by 0x1091D3: main (a.c:23) ==3985== Address 0x4a95078 is 56 bytes inside a block of size 63 alloc'd ==3985== at 0x484884F: malloc (vg_replace_malloc.c:393) ==3985== by 0x1091A5: main (a.c:17) ==3985== ==3985== ==3985== HEAP SUMMARY: ==3985== in use at exit: 0 bytes in 0 blocks ==3985== total heap usage: 1 allocs, 1 frees, 63 bytes allocated ==3985== ==3985== All heap blocks were freed -- no leaks are possible ==3985== ==3985== For lists of detected and suppressed errors, rerun with: -s ==3985== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
memset()を実行しようとしている箇所が、メモリ破壊系の脆弱性につながる可能性があると報告されています。そして、その理由としてメモリ確保を行ったmalloc()関数にて割り当てた領域がmemset()で書き込もうとしたメモリの大きさと一致しない事が指摘されています。
もう一つ、メモリ破壊系の脆弱性の検出例を示します。次に示すコードでは、newでメモリ領域を確保しています。createArray()というメモリ領域を確保して、確保したメモリ領域に値を書き込む関数を定義しています。main()で10個のメモリ領域を確保していますが、最後のstd::coutで11番目のメモリ領域(配列のインデックスは0から始まるため10番目の要素はnum[9])を参照しているため、本来であればアクセスできないはずのメモリ領域のデータが読み取られます。この脆弱性は境界外メモリ読み取り(Out-Of-Bound Read)の脆弱性と呼ばれるもので、メモリ破壊に分類される脆弱性の一つです。それでは、この脆弱性をvalgrindで検出してみましょう。
1 #include <iostream> 2 3 int *createArray(int size, int n = 0) 4 { 5 int *arr = new int[size]; 6 7 for (int i = 0; i < size; i++) { 8 arr[i] = n; 9 } 10 11 return arr; 12 } 13 14 int main(void) 15 { 16 int *num = createArray(10,1); 17 18 std::cout << num[0] << std::endl; 19 std::cout << num[5] << std::endl; 20 std::cout << num[10] << std::endl; 21 22 delete[] num; 23 24 return 0; 25 }
valgrindの検知結果は以下のとおりとなります。
==3987== Memcheck, a memory error detector ==3987== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. ==3987== Using Valgrind-3.20.0.GIT and LibVEX; rerun with -h for copyright info ==3987== Command: ./b.out ==3987== 1 1 ==3987== Invalid read of size 4 ==3987== at 0x1092F5: main (b.cpp:20) ==3987== Address 0x4ddbca8 is 0 bytes after a block of size 40 alloc'd ==3987== at 0x484A20F: operator new[](unsigned long) (vg_replace_malloc.c:652) ==3987== by 0x10923A: createArray(int, int) (b.cpp:5) ==3987== by 0x109292: main (b.cpp:16) ==3987== 0 ==3987== ==3987== HEAP SUMMARY: ==3987== in use at exit: 0 bytes in 0 blocks ==3987== total heap usage: 3 allocs, 3 frees, 73,768 bytes allocated ==3987== ==3987== All heap blocks were freed -- no leaks are possible ==3987== ==3987== For lists of detected and suppressed errors, rerun with: -s ==3987== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
main()中のcoutで表示しようとした要素がnewで確保したサイズより外れた場所を参照しているメモリ破壊系の脆弱性につながる可能性があると報告されています。その原因としてcreateArray()でnewしているサイズがcoutで表示しようとしているインデックスより小さい事が指摘されています。
valgrindって便利?
筆者は、数年前に大規模開発に従事した際にメモリリークによる機器上のプログラムの意図せぬ終了という場面に出会った事があります。その機器は組み込みLinuxを利用していたのでLinux上に吐き出されたcoreファイルからメモリリークとなるR/Wの箇所で止まっている事が判明し、その機器内にメモリ破壊系の脆弱性が存在するかの調査をした経験があります。そのときもvalgrindを利用しました。そのときは幸い、その箇所以外で問題はありませんでした。
valgrindのメリットが目立つ記事でしたが、以下のメリット/デメリットを踏まえて利用すると開発の役に立つと思います。
検知できるもの
・C言語またはC++言語で記述されたプログラム。
・POSIX互換のOS上で動かすことが出来るプログラム。
検知できないもの
・C言語またはC++言語以外のプログラミング言語を利用したもの。
・C言語とC++言語のモジュール間でインターフェース呼び出しを行い、Strings型とchar*型を混合させているもの。
・POSIX非互換のOSで動作しているプログラム。
ただし、POSIX非互換であったとしても、テストしたいモジュールやユニットがGoogleTestでテストできるように作成されていれば、POSIX互換OSでの実行が可能であるため、メモリ破壊系の脆弱性の有無をテストできます。。
OSS中にメモリ破壊系の脆弱性が存在する場合
無いとは言いません。ここ数年、ルーターなどの機器で見つかっている脆弱性にはOSS中のメモリリークが原因となっているものがいくつか存在しているのを見受けます。これらは動的解析で見つけるのは困難です。よって、VDoo社(JFrogs社)のVDoo Visionやシノプシス社のBlack Duckを使ってOSSに含まれる脆弱性を予め調べてから、どのバージョンを採用するか、そしてどのようにインターフェースを利用するかを検討した上で使うのが良いと考えます。
さいごに
ここまで書きましたが、ひとつだけお願いです。この記事が嫌いになってもC/C++を嫌いにならないで下さい。
参考文献
[1] https://valgrind.org
[2] SEI CERT C Coding Standard/Rules for Developing Safe, Reliable, and Secure Systems. 2016 Edition., Carnegie Mellon University., https://resources.sei.cmu.edu/forms/secure-coding-form.cfm
[3] Googletest, https://github.com/google/googletest