MingwとGDBでヒープ破壊を検出する
はじめに
題の通り、Mingwでヒープ破壊(ヒープコラプション、Heap Corruption)を検出する方法とそのためのライブラリを作ったので公開します。必要な人は限定的ですが、役に立つ場合は役に立ちます(笑)。
先に書いておくと、近代の一般的な開発環境では不要と思われます。なぜなら、VC++もLinuxのGCCも、ヒープ破壊を検出する機構を備えているからです。ところが、Windows用のGCC環境であるMingwにはいくら探しても公式のメモリ保護機構やヒープ破壊検出機構がありません。ですが、MingwはGCCをWindowsに移植し続けるという難行を常にこなしているためか、こういった先進ライブラリの実装は遅れがちです。とはいえ、Mingwは軽く、USBメモリーに開発環境を全部入れて、出先でプログラムのコンパイルができるなどのメリットもあります。そして何より無料なのであり、やはり僕としてはMingwだけでヒープコラプションを検出することには大きな動機があったわけで、同じ状況の人も世界中探せば数人はいると思います。
ヒープコラプションとは何か
C言語プログラマならご存じと思いますが、一応概要だけ。知ってたら読み飛ばしてください。
ヒープコラプション、意味としては「ヒープメモリーの管理領域汚染」ということになります。C言語には必要に応じてメモリを確保する機構としてヒープという仕組みが使われており、プログラムの中で最も大きなメモリ領域を扱えます。一方で、ヒープメモリはデータと管理領域が連続的に存在し、いわゆるバッファーオーバーフロー、バッファーアンダーフローで簡単にメモリ管理のための領域が壊れます。これ自体は良いことです。この手の破壊は要はプログラマのミス(バグ)であり、ミスによって管理領域が壊れるとほぼ確実にプログラムが落ちます。時々うまくいくようなバグよりはっきりしていてとても良いバグ(?)です。
一方で難しいところもあります。それは、バッファ破壊が発見されたタイミングと、バッファ破壊が起きたタイミングと、バッファを操作したタイミングが必ずしも一致しないということです。破壊が起きてからずいぶん経ってから、最悪の場合はプログラム終了直前に発覚することもあります。これは、破壊箇所の特定を著しく難しくします。
そこで、「破壊のタイミングをピンポイントで見つける」ために本ライブラリが作られました。
仕組み
本ライブラリ名を「Canary_watch(カナリア・ウォッチ)」といいます。カナリアとは鳥の名前ですが、IT用語としては別の意味があり、メモリ破壊を検出する囮領域の名称でもあります。これはかつて、炭鉱のガス検出に本種が使われていたことに由来します。
ライブラリの要は
・ウォッチポイント機能で監視する
・グローバル変数にポインタ登録することで監視しやすくする
・mallocをラッピングすることでバッファ両端にカナリア値の自動挿入を行う
という感じです。
悪名高いグローバル変数を使う理由は、
・変数名やアドレス指定などをしなくてもいい
・どの関数位置にいても必ずアクセスでき(スコープが広い)、寿命が長い
・メモリ配置が、ヒープと明確に異なるデータセグメントであるため、ヒープ破壊に巻き込まれにくい
・ヒープの次に大きなメモリ確保ができる
という理由です。
使い方
見本データ
見本データを置いときます。この中にライブラリのソースコードと、バグ入りのソースコード、およびmakeファイルとリソースファイルが入っています。
GDB用にプログラムをビルド
コンパイル、ビルドにはマクロ「CANARY_WATCH」を指定します。「CANARY_WATCH_AUTO_MAIN_DEBUG」を追加で指定すると、main関数をラッピングして全てのヒープをチェックするチェック関数を自動で走らせることもできます。
ヒープ破壊の位置特定
ヒープ破壊位置がわからない場合は、main関数終了直前にawp()を呼び出して全てのヒープのチェックを行います。ヒープ破壊があった場合はその旨が表示され、また「実行ファイル名+_CanaryWatch.log」というネーミングのログファイルが出力されます。この例では「Canary_Test.exe_CanaryWatch.log」に出力されます。
ヒープ破壊(Heap corruption)があった場合は、*CR* という目印がつき、そのバッファを監視するために適切なブレークポイントが表示されます。今回は
「Canary_Test2.c:87」つまりCanary_Test2.cというソースの87行目にブレークポイントを仕掛けるといいわけです。
ウォッチンポイントの設定
破壊されたバッファの位置がわかったので、再度GDBを起動、指定位置「Canary_Test2.c:87」にブレークポイントを仕掛け、その位置でウォッチポイント候補一覧表示関数cwp()を呼び出します。ymd3が確保した20バイトのバッファで、オーバーフロー側のカナリア値をウォッチすればいいので、
watch *(GW32[5])
として実行を続けます。
カナリア値が変更されて実行がストップ。btしてみると、Canary_Test2.cの47行目で_tcsncat(date, _T("01・"), len);がバッファを破壊したことがわかりました。
ヒープ破壊に困っていたら、試してみてください。
2023/09/01 アップロードした見本にreadmeファイルが入っていなかったのでアップロードファイルを置き換え。