読者です 読者をやめる 読者になる 読者になる

UnicornでWindowsAPIトレーサーみたいなものを作った

とは言っても技術的に何か特殊なことをしているとかいうわけではなくて,単純にPEローダーを頑張って実装したみたいな話.

作ったもの github.com

Unicorn

QEMUのラッパみたいなもので,CPUエミュレータフレームワークとして手軽に扱えるUnicornというライブラリがある.
つい先月v1.0がリリースされて,Go, Python, .NET, Javaの他にMSVC, VB6, Ruby, Haskellバインディングが追加された.

www.unicorn-engine.org

他に特筆すべき点としては,GDTR, IDTR, LDTRあたりのサポートが追加されて適切にセグメントレジスタを設定できるようになった(!)ことや,コンテキスト周りのサポートが入ったことが挙げられる.前者の方が結構重要で,WindowsではFSレジスタがTIBのアドレスを指しているのでマルウェアだとよく使われたりする.

PyAnaとDutas

UnicornでPEエミュレータってあるんじゃね?と思って探したら,案の定あった.

github.com

github.com

どちらもメインの機能としてWindowsAPIのトレーサーがついていて,サンプルコードを実行すると胡散臭い命令がジャンジャン呼ばれている様子を観測できる.

しかしこれらはUnicorn v0.9での動作を前提に作られていて,セグメント周りのサポートが入っていなかったためFSレジスタにメモリのアドレスを直接書き込むという荒業を使っている.

        mu.reg_write(UC_X86_REG_FS, TIB)

これをv1.0で動かすと当然クラッシュする.

また,どちらもLDR周りの整備を無理矢理やっていて,例えば別のdllを追加するのに結構労力を割かれたりする.解析する検体側でLoadLibraryが呼ばれてたりするとメモリにマッピングしてくれるのだけれど,PEが事前に別のdllを必要としていると全く動かない.

WindowsAPIフック

このように制約条件はありながらも,WindowsAPIフック機能はかなり良い感じに動作する.どうやってAPIフックを実現しているかというと,

  1. DLLに含まれる関数の先頭を\xc3 (ret) に書き換える
  2. 当該関数の先頭アドレスをフック対象に追加
  3. 関数が呼ばれるとUnicorn上で代わりにその処理を行い,スタックや戻り値などの辻褄を合わせる

という非常にシンプルなものである.
フック対象を管理しているリスト (実際はdict) では関数の先頭アドレスとWindowsAPIの名前をペアとして持っておく.代理で呼ぶ関数には予めhook_というprefixをつけておき,globals()から取得してeip, esp, uc (Unicornインスタンス)を渡して呼び出す.例えばエミュレータ上でGetProcAddressが呼ばれるとhook_GetProcAddress(eip, esp, uc)が実行される,という具合.
hook_*という関数が大量に実装されているが,もちろんされてないもののほうが多い.最初はPyAnaないしDutasにちまちまとフック関数を実装して頑張っていたのだが,結構な数になってくると見通しが悪い.あとlstrcatみたいな絶対に動作が変わらないものの処理が検体毎に複製されていくのがなんだか気持ち悪いので,ライブラリ化しておきたいという気持ちになった.

pefile

PyAnaもDutasもDLLがエクスポートした関数を列挙するのにpefileというライブラリを使っている.

github.com

これはPythonでPEを扱うなら鉄板ライブラリといえるほど動作が安定していて,実績も多い.例えばangrはPEを解析するときに内部でpefileを使っている.

しかしこのライブラリ,非常に遅い.kernel32.dll (約1.1MB) を読み込むのに6秒くらいかかる.DLL一つだけならまだ良いが,3個以上のDLLを予め読み込んだり,LoadLibraryが途中で呼ばれて追加のDLLが読み込まれたりすると,正直待ってられない.DLLが足りなくて途中で落ちたらまた20秒くらい待たされることになる. と思っていたのだが,単純にpipで入るバージョンがクソ古いだけだった.githubから最新版を落としてくるともうちょっと早く動く.それでも2秒くらいはかかる.

PythonでPEパーサーといえばもう一つ,pype32というライブラリがある.readpe.pyが有名.前は普通にサクサク使えてた印象があったのだけど,いつの間にか遅くなっていた.というかパースが終わらない.バグか?これも勘違い.正しくは exeのパースは早いがdllはクソ遅い だった.kernel32.dllで試したのだけれど,2分48秒かかった. 測り直したら1分5秒でした.論外.

PEパーサー

pefileもpype32もまともに使えない(pefileはまあ使える)ことがわかったので,自分で実装することにした.参考にしたサイトを貼っておく.

PE(Portable Executable)ファイルフォーマットの概要

Portable Executable カテゴリーの記事一覧 - 鷲ノ巣

DOSヘッダからセクションヘッダまでなら前者,IMAGE_DATA_DIRECTORY以降の話なら後者がわかりやすい.
これらを参考にしつつ,MSDNのサイトPeering Inside the PE: A Tour of the Win32 Portable Executable File Formatを見ながらctypesでごりごりとパーサーを書いた.

今回必要な機能は

  • EXEがインポートしているDLL, 関数名/RVA列挙
  • DLLがエクスポートしている関数名/RVA列挙
  • PEがメモリ上へ展開された後のイメージ
  • PEの各種情報 (ImageBase, EntryPointなど)

で,IMAGE_DIRECTORYのうちサポートするインポートはIMAGE_DIRECTORY_ENTRY_IMPORTのみ.
この位まで機能を絞ると案外実装しやすくて,速度も出た.

github.com

A Tour of the Win32 Portable Executable File Formatとあるように,32bitまでしか対応してないので現状64bitは未対応.気が向いたらやる.

TIB周り

WindowsのプロセスにはTIB (Thread Information Block) というのがあって,プロセス・スレッド自身の情報を保持している.プロセス起動時にFSレジスタがこの構造体の先頭アドレスを指すようになっており,これを辿ることで各情報にアクセスできる.
特に対解析機能として難読化をかけてるコードやパッキングしているようなコードは,元のバイナリが使用しているWindowsAPIのアドレスを動的に解決する必要があるため,PEBのLDRからモジュールのリストを辿ってLoadLibrary,GetProcAddress (kernel32.dll) のアドレスを取得し,そこから必要な関数をもってくる,といったことをよくする.

そのためTIB周りをちゃんとセットアップしてやらないと動かない検体が結構あって,実装することにした.PyAnaもDutasも見かけ上はLDRまでやってるけど,オフセットも決め打ちで,用意するLDRのリンクも真面目に繋いでないので,ちょっと別経路を辿ると動かなかったりする.具体的に言うと,ロードされたモジュールはLoadOrder, MemOrder, InitOrderの3種類の順番で辿ることができるが,LoadOrderの部分しか繋いでないとMemOrderを辿る検体が動かない,という具合.LDR_MODULEについては Understanding the PEB_LDR_DATA Structure がわかりやすい.
ちなみにTIB周りの構造体は Welcome to WinAppDbg 1.5! — WinAppDbg 1.5 documentation を拝借して流用している.素のままだとUnix環境で動かないので適当に手を加えた.

セグメントレジスタ周り

UnicornはあくまでもCPUエミュレータなので,アーキテクチャの設計に対して忠実に処理を書かなければならない.最初ここを理解してなくて,FSレジスタを設定するだけでsegv起こしたり,mapしたはずのスタック領域がunmapped扱いになったりした.
ざっくり説明すると,セグメントレジスタを設定するときはGDT (Global Discriptor Table) をあらかじめ設定しておく必要があって,その中のインデックス番号,メモリ上の範囲,各種フラグを含んだ値 (selectorという) をセグメントレジスタに書き込む.selectorを書き込む段階でGDTのセットアップが終わってないとsegvする.
GDTについては Global Descriptor Table - OSDev Wikiプロテクトモードのセグメント機構 を参照した.

他にハマりどころとして,Windows (x86環境) ではGS, ES, DS, SSが全て同じ値 0x002B (index:5, range:0x00000000-0xffffffff, flags: gr=1, sz=1, pr=1, privl=3, ex=0, dc=0, rw=1, ac=1, rpl=3) に設定されているのだけれど,Unicornではこの値だとスタックがうまく扱えなくて,正しくはSSに設定するフラグを gr=1, sz=1, pr=1, privl=0, ex=0, dc=1, rw=1, ac=1, rpl=0 にしてあげる必要がある.違いはgdt entryの特権レベルとselectorの要求特権レベルが0 (kernel) になっているのと,dc (Direction bit/Conforming bit) が1 (segment grows down) になっている点.dc bitは全然気づかなかった.
セグメント周りの設定はこのへんでやってる.indexが露骨に調整してあるのはWindowsをマネしたため.

フック

フックの仕組みはPyAnaやDutasと同じく,関数の先頭をretに書き換えてUnicorn側で代わりの処理を呼ぶ.トレーサーとフック処理の接点をUnitracerインスタンスだけに抑えるため,hookを呼ぶ時にselfを渡すというちょっと変なことをやっている.WindowsAPIが行う処理はアーキテクチャに依らず同じ結果を返してほしいので,フック処理ではなるべくUnicornの都合を意識しなくて済むような設計にしたかったのだが,現状はできてない.多分呼び出し規約をWindowsクラス側で一意に扱えるようにしないと実現できない気がする.
実際にUnitracerを使うときは絶対に挙動が変わらない処理はlib/windows/hooks以下に書いておいて,挙動を変えたい時に[Unitracerインスタンス].hooksに直接関数を追加して動かすみたいな使い方を想定している.これでとりあえず当初の目的であったフック処理のライブラリ化に関しては達成できたとは思う.

現状

ここまで長々と書いてしまったけど,とりあえずAPIトレーサとしては十分動くと思う.
まだTIB周りが弱くて,サンプル (samples/Wincalc.sc) が動かない!w [17/03/11]PEパーサーのバグだった.現在は修正済み. あとフックを全然実装してないのでちょくちょく追加していく予定.