fridaでお手軽Windows APIトレース

プロセスが呼び出している特定のWindows APIのログを取りたい場合、fridaというツールが便利。 fridaは次のような特徴がある。

  • Windowsだけでなく、Linux/Macにも対応している
  • トレース対象の関数の引数や返り値をJavascriptで制御できる
    • 引数や返り値を書き換えることも可能

このように、fridaを覚えるとLinux/Macにも応用できる上に、スクリプトで引数・返り値の制御ができるので自由度が高い。 たとえば、他にWindows API専用のAPI Monitorというツールもあるが、書き換えまではできないし引数のマルチバイト文字が化けていたりと困る点があった。

トレース対象のプログラム

MessageBoxA を呼び出すだけのプログラムをトレース対象とする。 以下のコードをVisual Studioでビルドした MessageBoxTest.exe を作った。

#include <Windows.h>

int main()
{
    MessageBoxA(nullptr, "あいうえお", "", MB_OK);
    return 0;
}

fridaのインストール

Pythonのパッケージマネージャーpipを使ってインストールする。

Javascriptで制御するのにPython...?と最初思ったが、 frida自体はPythonで作られており、対象のプロセス内でトレースを制御するためのスクリプトがJavascriptで動作する ということらしい。

$ pip3 install frida-tools
$ frida --version

12.11.10

fridaによるトレース開始

次のように frida-trace コマンドを使うと、特定のAPIをトレースするためのスクリプトを自動生成してくれる。

$ frida-trace -i MessageBoxA .\MessageBoxTest.exe

Instrumenting...
MessageBoxA: Loaded handler at "C:\\Users\\castaneai\\Documents\\MessageBoxTest\\Release\\__handlers__\\USER32.dll\\MessageBoxA.js"
Started tracing 1 function. Press Ctrl+C to stop.
           /* TID 0x4074 */
   215 ms  MessageBoxA()
Process terminated

実行するとメッセージボックス「あいうえお」が表示され 215 ms MessageBoxA() とトレースのログが一緒に表示される。

スクリプトの編集

__handlers__\\USER32.dll\\MessageBoxA.js にトレース用のスクリプトが自動生成されている。 中には2つの関数が用意されていて、それぞれ次の役割になる。

  • onEnter : 関数に入るときに呼び出される(引数がわかる)
  • onLeave : 関数から抜けるときに呼び出される(返り値がわかる)

デフォルトでは呼び出されたときに関数の名前を表示するだけになっている。

  /**
   * Called synchronously when about to call MessageBoxA.
   *
   * @this {object} - Object allowing you to store state for use in onLeave.
   * @param {function} log - Call this function with a string to be presented to the user.
   * @param {array} args - Function arguments represented as an array of NativePointer objects.
   * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8.
   * It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
   * @param {object} state - Object allowing you to keep state across function calls.
   * Only one JavaScript function will execute at a time, so do not worry about race-conditions.
   * However, do not use this to store function arguments across onEnter/onLeave, but instead
   * use "this" which is an object for keeping state local to an invocation.
   */
  onEnter: function (log, args, state) {
    log('MessageBoxA()');
  },

  /**
   * Called synchronously when about to return from MessageBoxA.
   *
   * See onEnter for details.
   *
   * @this {object} - Object allowing you to access state stored in onEnter.
   * @param {function} log - Call this function with a string to be presented to the user.
   * @param {NativePointer} retval - Return value represented as a NativePointer object.
   * @param {object} state - Object allowing you to keep state across function calls.
   */
  onLeave: function (log, retval, state) {
  }

引数も表示する

MessageBoxAに渡された引数も表示したいので、onEnterを次のように書き換える。

自動生成されたコメントにある通り、args[0] といったふうに引数をNativePointer型 として取得できるので、MessageBoxAの定義を参考にしながらそれぞれの型に変換して表示する。

  onEnter: function (log, args, state) {
    const hWnd = args[0];
    const lpText = args[1].readAnsiString();
    const lpCaption = args[2].readAnsiString();
    const uType = args[3];
    log('MessageBoxA(', hWnd, '"'+lpText+'"', '"'+lpCaption+'"', uType, ')');
  },
$ frida-trace -i MessageBoxA .\MessageBoxTest.exe

Instrumenting...
MessageBoxA: Loaded handler at "C:\\Users\\castaneai\\Documents\\MessageBoxTest\\Release\\__handlers__\\USER32.dll\\MessageBoxA.js"
Started tracing 1 function. Press Ctrl+C to stop.
           /* TID 0x4194 */
   219 ms  MessageBoxA( 0x0 "あいうえお" "" 0x0 )
Process terminated

引数を書き換える

fridaは次のように args[n] の値を直接差し替えることで引数を書き換えることもできる。

今回はMessageBoxの表示テキストを書き換えたが、ここで this. を使っているのは確保した文字列のメモリの生存期間を伸ばすための小技で、fridaドキュメントのBest Practicesで紹介されている

  onEnter: function (log, args, state) {
    this.newLpText = Memory.allocAnsiString("こんにちはfridaだよ");
    args[1] = this.newLpText;
    const hWnd = args[0];
    const lpText = args[1].readAnsiString();
    const lpCaption = args[2].readAnsiString();
    const uType = args[3];
    log('MessageBoxA(', hWnd, '"'+lpText+'"', '"'+lpCaption+'"', uType, ')');
  },

これでもう一度 frida-trace を実行すると、メッセージボックスの文字列が変化した。

f:id:castaneai:20200823144122p:plain

$ frida-trace -i MessageBoxA .\MessageBoxTest.exe

Instrumenting...
MessageBoxA: Loaded handler at "C:\\Users\\castaneai\\Documents\\MessageBoxTest\\Release\\__handlers__\\USER32.dll\\MessageBoxA.js"
Started tracing 1 function. Press Ctrl+C to stop.
           /* TID 0x25cc */
   107 ms  MessageBoxA( 0x0 "こんにちはfridaだよ" "" 0x0 )
Process terminated