この記事は、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の場合、0x401126
と0x40114f
を出力します。
コンパイラ基盤の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
-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 *drcontext
やinstrlist_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 *drcontext
とinstrlist_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
下記は出力結果の一部です。
TAG
とEND
に囲まれたアセンブリがコードキャッシュに移行された基本ブロックです。
0x401126
と0x40114f
が記録されており、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
参考文献
- Code Manipulation API
- Basic Blocks (GNU Compiler Collection (GCC) Internals)
- Control Flow (GNU Compiler Collection (GCC) Internals)
- Edges (GNU Compiler Collection (GCC) Internals)
- LLVM’s Analysis and Transform Passes
- Tool Event Model and API
- DynamoRIO System Overview
- How to Run
- dr_register_bb_event
- dr_insert_clean_call