(Last modified: )

DynamoRIOでシンプルな基本ブロックトレーサーを実装してみる

この記事は、2021年09月30日2021年11月29日に旧ブログに投稿したものです。

プロセスが実行した基本ブロックをトレースするツールをDynamoRIOを用いて開発します。

DynamoRIOとは

実行中のプログラムコードをランタイムで書き換えることができるフレームワークです。 類似フレームワークに、Intel Pinなどがあります。

基本ブロックとは

基本ブロックとは、GCCのドキュメントによると1つのエントリポイントと1つの出口を持つコードの塊です。 ある基本ブロックの終端は、多くの場合別の基本ブロックの先頭へ到達します。 これを有向グラフとして表したものに制御フローグラフ(CFG)があります。

次のworld!のみを表示するプログラム(hello-world.c)のCFGを見てみましょう。

 1#include <stdio.h>
 2
 3int main(void)
 4{
 5    int a = 1;
 6    if (a == 0)
 7    {
 8        printf("Hello,");
 9    }
10    printf("world!");
11
12    return 0;
13}

プログラムとELFのベースアドレスを同一にするため、PIEは無効にしてビルドします。 RVA(仮想アドレスからベースアドレスを引いたアドレス)を一々計算するのは面倒なので……。

1gcc ./hello-world.c -o ./hello-world -no-pie

バイナリをCFGとして出力できるソフトウェアにIDAやRadare2、Ghidraなどがあります。ここではRadare2を用いて、mainシンボルが指す関数のCFGを生成します。

1$ r2 ./hello-world
2[0x00401040]> aa
3[x] Analyze all flags starting with sym. and entry0 (aa)
4[0x00401040]> s main
5[0x00401126]> agfd > hello-world.dot

dot形式で出力されるので、svg形式に変換します。

1dot -Tsvg -Gbgcolor="transparent" -Nstyle="filled" -o hello-world.svg hello-world.dot

次のCFGが出力されます。 ソースコードと比較すると、CFGの0x40113bがソースコードの8行目に対応し、到達しないことが感覚的に分かるのではないでしょうか。

図1: world!を表示するバイナリのCFG
図1: world!を表示するバイナリのCFG

今回作成するツールは、プログラムが実行した基本ブロックを記録するツールです。 図1の場合、0x4011260x40114fを出力します。

コンパイラ基盤のGCCやLLVMでもCFGを出力する機能があります。

clangでLLVM IRに変換し、optコマンドでCFGを出力します。

1clang -S -emit-llvm hello-world.c
2opt -dot-cfg-only hello-world.ll -enable-new-pm=0
3dot -Tsvg .main.dot -o main.svg
検証プログラムの制御フローグラフ
図2: world!を表示するプログラムのCFG

-dot-cfg-onlyの代わりに-dot-cfgオプションを付加すると、各基本ブロックの中身が中間表現(LLVM IR)で表示されます。

基本ブロックイベント

DynamoRIO上でプログラムを実行すると、様々なイベントが発火します。 その1つに、初めてプログラムのコードが実行される前に発火する基本ブロックイベントがあります。

DynamoRIOは、アプリケーションのコードを中間表現に変換し、コードキャッシュへ移します。 そして、コードキャッシュ内の中間表現をマシンコードに変換して実行します。 コードキャッシュ内のコードは、DynamoRIOのAPIを通して変更可能です。これにより、プログラムの動的な変更が可能となります。

基本ブロックイベントは、コードキャッシュへ移す際に発火するイベントです。 コードキャッシュのコードが実行される時ではないことに注意してください。 このイベントにハンドラを追加するには、dr_register_bb_event関数を使います。

 1#include "dr_api.h"
 2
 3static dr_emit_flags_t do_nothing(void *drcontext, void *tag, instrlist_t *bb, bool for_trace, bool translating)
 4{
 5    return DR_EMIT_DEFAULT;
 6}
 7
 8DR_EXPORT void dr_client_main(client_id_t id, int argc, const char* argv[])
 9{
10    dr_register_bb_event(do_nothing);
11}

コールバック引数

基本ブロックイベントのコールバック関数には、5つの引数が渡されます。

1dr_emit_flags_t(*)(void *drcontext, void *tag, instrlist_t *bb, bool for_trace, bool translating) func

void *drcontextinstrlist_t *bbが、コードキャッシュへ移行する基本ブロックのデータです。 void *tagは基本ブロックの識別子であり、dr_fragment_app_pc関数で基本ブロックのアドレスに変換できます。

instrlist_tは、要素がinstr_tの線形リストです。基本ブロックイベントから取得した場合、instr_tは基本ブロックに含まれる命令です。 instrlist_disassemble関数でアセンブリを出力できます。

 1#include "dr_api.h"
 2
 3static dr_emit_flags_t show_arg_as_assembly(void *drcontext, void *tag, instrlist_t *bb, bool for_trace, bool translating)
 4{
 5    app_pc pc = dr_fragment_app_pc(tag);
 6    instrlist_disassemble(drcontext, pc, bb, STDERR);
 7    return DR_EMIT_DEFAULT;
 8}
 9
10DR_EXPORT void dr_client_main(client_id_t id, int argc, const char *argv[])
11{
12    dr_register_bb_event(show_arg_as_assembly);
13}

このコードをビルドしてhello-worldを実行した結果の一部を次に示します。 図1で示したプログラムの基本ブロックと一致しています。

 1TAG  0x0000000000401126
 2 +0    L3 @0x00007f45b22ca930  55                   push   %rbp %rsp -> %rsp 0xfffffff8(%rsp)[8byte]
 3 +1    L3 @0x00007f45b22f7438  48 89 e5             mov    %rsp -> %rbp
 4 +4    L3 @0x00007f45b22f7300  48 83 ec 10          sub    $0x0000000000000010 %rsp -> %rsp
 5 +8    L3 @0x00007f45b22cbe58  c7 45 fc 01 00 00 00 mov    $0x00000001 -> 0xfffffffc(%rbp)[4byte]
 6 +15   L3 @0x00007f45b22cb9d8  83 7d fc 00          cmp    0xfffffffc(%rbp)[4byte] $0x00000000
 7 +19   L3 @0x00007f45b22cc9d8  75 14                jnz    $0x000000000040114f
 8END 0x0000000000401126
 9
10TAG  0x000000000040114f
11 +0    L3 @0x00007f45b22c7cf0  48 8d 05 b5 0e 00 00 lea    <rel> 0x000000000040200b -> %rax
12 +7    L3 @0x00007f45b22cc9d8  48 89 c7             mov    %rax -> %rdi
13 +10   L3 @0x00007f45b22cb9d8  b8 00 00 00 00       mov    $0x00000000 -> %eax
14 +15   L3 @0x00007f45b22cbe58  e8 cd fe ff ff       call   $0x0000000000401030 %rsp -> %rsp 0xfffffff8(%rsp)[8byte]
15END 0x000000000040114f

一見、このコードで基本ブロックのトレースはできているように見えます。 しかし、基本ブロックを書き換えていないため、基本ブロックイベントによって呼ばれた時しか出力できません。 したがって、ループで同じ基本ブロックを何回も呼ぶコードであっても、1回しか基本ブロックは出力しません。

基本ブロックの編集

基本ブロックを編集するには、基本ブロックイベントのinstrlist_tを編集します。 dr_insert_clean_callは、instrlist_tの任意の位置に自身が作成したサブルーチンを追加する関数です。

1void dr_insert_clean_call(void *drcontext, instrlist_t *ilist, instr_t *where, void *callee,
2                     bool save_fpstate, uint num_args, ...);

void *drcontextinstrlist_t *ilistに、サブルーチンを追加したい基本ブロックを指定します。 追加する位置はinstr_t *whereで指定し、追加するサブルーチンはvoid *calleeに指定します。 サブルーチンの引数は、uint num_argsでその数を指定し、可変長引数に渡します。

実装

引数に渡されたアドレスを出力する関数を作り、それを基本ブロックに追加します。 基本ブロックのアドレスは、基本ブロックイベントのコールバック引数であるvoid *tagから取得できます。 したがって、基本ブロックのアドレスをその関数に渡せば、基本ブロックのトレースが実装できます。

1static void print_executed_bb(app_pc pc)
2{
3    fprintf(stderr, "trace: " PFX "\n", pc);
4}

この関数を挿入する位置はどこでも良いですが、今回は基本ブロックの最初に追加します。 最初の位置は、instrlist_first_app関数で取得できます。

 1static dr_emit_flags_t insert_bbaddr_func(void *drcontext, void *tag, instrlist_t *bb, bool for_trace, bool translating)
 2{
 3    app_pc pc = dr_fragment_app_pc(tag);
 4    if (dr_module_contains_addr(app, pc))
 5    {
 6        instr_t *first_instr = instrlist_first_app(bb);
 7        dr_insert_clean_call(drcontext, bb, first_instr, (void *)print_executed_bb, false, 1, OPND_CREATE_INTPTR(pc));
 8    }
 9    return DR_EMIT_DEFAULT;
10}

メインモジュール

DynamoRIOはプロセスの仮想アドレス空間にマッピングされたモジュールに対してイベントを発火します。 したがって、現状のコードでは外部ライブラリにまで基本ブロックイベントが発火されるため、大量の情報が出力されます。

そこで、アプリケーション内のコード(メインモジュール)のみに絞り込みます。 dr_get_main_module関数でメインモジュールのアドレス範囲を導出し、dr_module_contains_addr関数でフィルタリングします。

dr_module_contains_addr関数を使わなくても、次のようにメインモジュールの開始アドレスと終了アドレスで条件判定できそうです。

1app_pc pc = dr_fragment_app_pc(tag);
2if (pc > app->start && pc < app->end)
3{
4    instrlist_disassemble(drcontext, pc, bb, STDERR);
5}

しかし、モジュールが必ずしも開始アドレスと終了アドレスの範囲すべてにマッピングされているとは限りません。dr_module_contains_addr関数は、そのような場合も上手に処理します。

最終的なコード

 1#include "dr_api.h"
 2
 3static module_data_t *app;
 4
 5static void print_executed_bb(app_pc pc)
 6{
 7    fprintf(stderr, "trace: " PFX "\n", pc);
 8}
 9
10static dr_emit_flags_t insert_bbaddr_func(void *drcontext, void *tag, instrlist_t *bb, bool for_trace, bool translating)
11{
12    app_pc pc = dr_fragment_app_pc(tag);
13    if (dr_module_contains_addr(app, pc))
14    {
15        instr_t *first_instr = instrlist_first_app(bb);
16        dr_insert_clean_call(drcontext, bb, first_instr, (void *)print_executed_bb, false, 1, OPND_CREATE_INTPTR(pc));
17    }
18    return DR_EMIT_DEFAULT;
19}
20
21static void exit_event(void)
22{
23    dr_free_module_data(app);
24}
25
26DR_EXPORT void dr_client_main(client_id_t id, int argc, const char *argv[])
27{
28    app = dr_get_main_module();
29    dr_register_bb_event(insert_bbaddr_func);
30    dr_register_exit_event(exit_event);
31}

ビルド

上記のコードをmain.cとして保存し、次のCMakeLists.txtを作成します。

 1project(show_bb)
 2
 3if (NOT DynamoRIO_DIR)
 4    set(DynamoRIO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/DynamoRIO-Linux-8.0.0-1/cmake")
 5endif ()
 6
 7add_library(show_bb SHARED
 8main.c
 9)
10
11find_package(DynamoRIO)
12configure_DynamoRIO_client(show_bb)

プロジェクトファイルを生成します。

1mkdir build
2cd ./build
3cmake -G "Ninja" ..
4cmake --build .

libshow_bb.soが生成されます。これを用いて、hello-world.cの基本ブロックをトレースしてみます。

1drrun -syntax_intel -c ./libshow_bb.so -- ./hello-world

下記は出力結果の一部です。 TAGENDに囲まれたアセンブリがコードキャッシュに移行された基本ブロックです。 0x4011260x40114f が記録されており、0x40113b は記録されていないことが確認できます。

 1TAG  0x0000000000401126
 2 +0    L3 @0x00007f45b22ca930  55                   push   %rbp %rsp -> %rsp 0xfffffff8(%rsp)[8byte]
 3 +1    L3 @0x00007f45b22f7438  48 89 e5             mov    %rsp -> %rbp
 4 +4    L3 @0x00007f45b22f7300  48 83 ec 10          sub    $0x0000000000000010 %rsp -> %rsp
 5 +8    L3 @0x00007f45b22cbe58  c7 45 fc 01 00 00 00 mov    $0x00000001 -> 0xfffffffc(%rbp)[4byte]
 6 +15   L3 @0x00007f45b22cb9d8  83 7d fc 00          cmp    0xfffffffc(%rbp)[4byte] $0x00000000
 7 +19   L3 @0x00007f45b22cc9d8  75 14                jnz    $0x000000000040114f
 8END 0x0000000000401126
 9
10TAG  0x000000000040114f
11 +0    L3 @0x00007f45b22c7cf0  48 8d 05 b5 0e 00 00 lea    <rel> 0x000000000040200b -> %rax
12 +7    L3 @0x00007f45b22cc9d8  48 89 c7             mov    %rax -> %rdi
13 +10   L3 @0x00007f45b22cb9d8  b8 00 00 00 00       mov    $0x00000000 -> %eax
14 +15   L3 @0x00007f45b22cbe58  e8 cd fe ff ff       call   $0x0000000000401030 %rsp -> %rsp 0xfffffff8(%rsp)[8byte]
15END 0x000000000040114f

参考文献

Powered by Hugo & Kiss.