一、前言

本系列將以官網資料為基礎主要通過動態跟蹤來解析DynamoRIO的源代碼。因為如果不結合實例只是將各函數的作用寫出來,實在無法很好的說明問題,我們將以代碼覆蓋工具drcov為例,分析DynamoRIO的執行流程。

本系列主要的參考資料是《Efficient, Transparent, and Comprehensive Runtime Code Manipulation》(一定要看這篇論文),本章主要講述DynamoRIO如何劫持控制目標進程。

二、概述

DynamoRIO是一個運行時代碼操作系統,支持在程序執行時對其任何部分進行代碼轉換。DynamoRIO輸出了一個接口,用于建立動態工具,用途廣泛:程序分析和理解、剖析、儀表、優化、翻譯等。與許多動態工具系統不同,DynamoRIO并不局限于插入調用/中斷,通過強大的IA-32/AMD64/ARM/AArch64指令操作庫,允許對應用程序指令進行任意修改。

DynamoRIO提供了高效、透明和全面的操作,可以對運行在庫存操作系統(Windows、Linux或Android,試驗性支持Mac)和商品IA-32、AMD64、ARM和AArch64硬件上的未修改的應用程序進行操作。

我們希望有一個全面的工具平臺,它能夠系統地將自己置于運行中的應用程序執行的每條指令和底層硬件之間,如圖所示:

我們的目標是構建一個靈活的軟件層,它將自己完全地插入到正在運行的應用程序和底層平臺之間。該層充當運行時控制點,允許自定義工具嵌入其中。

三、準備工作

首先需要準備一個程序,因為我們主要以此程序為例分析DynamoRIO,所以程序不應太過復雜。我們使用的簡單程序如下:

#include 
#include 
int main(int argc, char* argv[])
{
 int count = 0;
 for (int j = 0; j < 2; j++)
 {
 for (int i = 0; i < 60; i++)
 {
 if (i < 30)
 {
 count++;
 }
 else
 {
 count--;
 }
 }
 }
 printf("count:%d", count);
 return 0;
}

在命令行輸入:

"E:\windbg\Debuggers\x86\windbg.exe" E:\dynamorio\build32\bin32\drrun.exe -t drcov --E:\test\Test.exe

開啟子進程調試,下斷并運行到主函數中,接下來我們開始。

int_tmain主函數

主函數在drdeploy.c中 我將把主函數簡化保留關鍵的部分。主函數如下:

int_tmain(int argc, TCHAR* targv[])
{
 ...
 /* 開頭主要進行讀取參數 驗證參數,初始化等操作 */
 /* 比如會找drcov.dll和dynamorio.dll的絕對路徑等 */
 dr_inject_process_create(app_name, app_argv, &inject_data);
 ...
 /* 在C:\Users\Lenovo\dynamorio下創建并寫入配置文件 */
 dr_inject_process_inject(inject_data, force_injection, drlib_path)
 dr_inject_process_run(inject_data)
}

可以看到主函數的關鍵就在于這三個函數,我們接下來逐個分析。

dr_inject_process_create

首先我們看看官網對此函數是如何進行解釋的:為指定的可執行文件和命令行創建一個新進程。進程中的初始線程被暫停。

參數

[in]  app_name   目標可執行文件的路徑

[in]  app_cmdline   一個以NULL結尾的字符串數組,代表應用程序的命令行

[out]  data       一個不透明的指針,它應該被傳遞給后續的drinject*例程以引用這個進程。

簡化后dr_inject_process_create主要執行流程如下:

int dr_inject_process_create(const char* app_name, const char** argv, void** data OUT)
{
 dr_inject_info_t* info = HeapAlloc(GetProcessHeap(), 0, sizeof(*info));
 ...
 /* 進行格式化參數,填充info等操作 */
 res = CreateProcess(wapp_name, wapp_cmdline, NULL, NULL, TRUE,
 CREATE_SUSPENDED |
 ((debug_stop_function && info->using_debugger_injection)
 ? DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS
 : 0),
 NULL, NULL, &si, &info->pi);
 *data = (void*)info;
}

可以看到此函數主要是以暫停線程的方式創建了目標進程。我們跟蹤進去看看info的結構,發現info主要保存一些進程信息。

在執行dr_inject_process_inject之前會在C:\Users\Lenovo\dynamorio目錄下創建以進程名和pid命名的配置文件,我們可以查看此配置文件:

之后調用dr_inject_process_inject

dr_inject_process_inject

將DynamoRIO注入由dr_inject_process_create()創建的進程中。

參數

[in] data 由dr_inject_process_create()返回的指針。

[in] force_injection 即使進程被配置為不在DynamoRIO下運行,也會要求注入。

[in] library_path 要使用的DynamoRIO庫的路徑。如果為空,將使用目標進程所配置的庫。

dr_inject_process_inject的主要執行流程如下:

bool dr_inject_process_inject(void* data, bool force_injection, const char* library_path)
{
 dr_inject_info_t* info = (dr_inject_info_t*)data;
 ...
 /* 如果library_path=NULL 就會從配置文件中讀取dynamorio.dll的路徑賦值給library_path */
 res = inject_into_new_process(info->pi.hProcess, info->pi.hThread,
 (char*)library_path, true /*map*/,
 INJECT_LOCATION_ThreadStart, NULL);
}

可以看到此函數是一個封裝,主要實現在inject_into_new_process中

inject_into_new_process

簡化后inject_into_new_process的主要實現流程如下:

#define EARLY_INJECT_HOOK_SIZE 14
bool
inject_into_new_process(HANDLE phandle, HANDLE thandle, char *dynamo_path, bool map,
 uint inject_location, void *inject_address)
{
 uint64 image_entry = 0;
 uint64 hook_target = 0;
 byte hook_buf[EARLY_INJECT_HOOK_SIZE];
 uint old_prot;
 switch (inject_location) 
 {
 case INJECT_LOCATION_ThreadStart:
 /* 此函數讀取目標進程PE文件 最終將EntryPoint給image_entry */
 image_entry = get_remote_process_entry(phandle, &x86_code);
 if (thandle != NULL) {
 if (IF_X64(!) is_32bit_process(phandle)) {
 cxt.cxt.ContextFlags = CONTEXT_CONTROL;
 /*通過NTGetContextThread函數獲取目標進程主線程上下文*/
 if (NT_SUCCESS(nt_get_context(thandle, &cxt.cxt)))
 /*將目標進程主線程EIP賦值給hook_location */
 hook_location = cxt.cxt.CXT_XIP;
 }
 }
 /* 判斷hook_location=0 所以下面一般不會執行 */ 
 if (hook_location == 0) {
 bool target_64 = !x86_code IF_X64(|| DYNAMO_OPTION(inject_x64));
 uint64 ntdll_base = find_remote_dll_base(phandle, target_64, "ntdll.dll");
 uint64 thread_start =
 get_remote_proc_address(phandle, ntdll_base, "RtlUserThreadStart");
 if (thread_start != 0)
 hook_location = thread_start;
 }
 /* hook_location仍為0的情況下使用image_entry */
 if (hook_location == 0) {
 hook_location = image_entry;
 }
 }
 /* 將hook_location 14字節的數據讀到hook_buf */
 /* 14字節是因為最大字節數是x64下 jmp (6 bytes) + target (8 bytes). */
 read_remote_memory_maybe64(phandle, hook_location, hook_buf, sizeof(hook_buf),
 &num_bytes_out)
 /* 利用NtProtectVirtualMemory將hook_location 14個字節保護更改為可讀可寫可執行 */
 remote_protect_virtual_memory_maybe64(phandle, hook_location, sizeof(hook_buf),
 PAGE_EXECUTE_READWRITE, &old_prot)
 /* 此函數將在后面詳細分析 */
 hook_target = inject_gencode_mapped(phandle, dynamo_path, hook_location, hook_buf,
 NULL, x86_code, late_injection, old_prot);
 /* 將hook_target給主線程eip */
 if (inject_location == INJECT_LOCATION_ThreadStart && hook_location != image_entry &&
 thandle != NULL) {
 if (IF_X64_ELSE(true, is_32bit_process(phandle))) {
 cxt.cxt.ContextFlags = CONTEXT_CONTROL;
 if (NT_SUCCESS(nt_get_context(thandle, &cxt.cxt))) {
 cxt.cxt.CXT_XIP = (ptr_uint_t)hook_target;
 if (NT_SUCCESS(nt_set_context(thandle, &cxt.cxt)))
 skip_hook = true;
 }
 }
 }
 /* 恢復hook_location 但我感覺沒有必要 因為沒有改變過hook_location */
 write_remote_memory_maybe64(phandle, hook_location, hook_buf, sizeof(hook_buf),
 &num_bytes_out) 
}

我們知道了此函數的大致流程,接下來我們來深入研究其各個參數和各函數的實現過程。

我們首先驗證一下image_entry的值:

執行get_remote_process_entry后image_entry為0x1ab118,

可以看到實際上image_entry存放著EntryPoint:

接下來我們查看nt_get_context的實現過程:

實際上nt_get_context的實現是通過變參宏調用NTGetContextThread函數。

查看hook_location

可以看到目標進程主線程的eip為RtlUserThreadStart賦值給hook_location。

分析inject_gencode_mapped

inject_gencode_mapped主要流程如下:

static uint64
inject_gencode_mapped(HANDLE phandle, char *dynamo_path, uint64 hook_location,
 byte hook_buf[EARLY_INJECT_HOOK_SIZE], void *must_reach,
 bool x86_code, bool late_injection, uint old_hook_prot)
{
 bool success = false;
 NTSTATUS res;
 HANDLE file = INVALID_HANDLE_VALUE;
 HANDLE section = INVALID_HANDLE_VALUE;
 byte *map = NULL;
 size_t view_size = 0;
 wchar_t dllpath[MAX_PATH];
 uint64 ret = 0;
 if (!convert_to_NT_file_path(dllpath, dynamo_path, BUFFER_SIZE_ELEMENTS(dllpath)))
 goto done;
 NULL_TERMINATE_BUFFER(dllpath);
 res = nt_create_module_file(&file, dllpath, NULL, FILE_EXECUTE | FILE_READ_DATA,
 FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, 0);
 if (!NT_SUCCESS(res))
 goto done;
 res = nt_create_section(§ion, SECTION_ALL_ACCESS, NULL, /* full file size */
 PAGE_EXECUTE_WRITECOPY, SEC_IMAGE, file,
 /* XXX: do we need security options to put in other process?*/
 NULL /* unnamed */, 0, NULL, NULL);
 if (!NT_SUCCESS(res))
 goto done;
 res = nt_raw_MapViewOfSection(section, phandle, &map, 0, 0 /* not page-file-backed */,
 NULL, (PSIZE_T)&view_size, ViewUnmap,
 0 /* no special top-down or anything */,
 PAGE_EXECUTE_WRITECOPY);
 if (!NT_SUCCESS(res))
 goto done;
 ret =
 inject_gencode_mapped_helper(phandle, dynamo_path, hook_location, hook_buf, map,
 must_reach, x86_code, late_injection, old_hook_prot);
done:
 if (ret == 0) {
 close_handle(file);
 close_handle(section);
 }
 return ret;
}

此函數利用NtCreateSection和NtMapViewOfSection將dynamorio.dll注入到目標進程中:

之后調用inject_gencode_mapped_helper函數。

inject_gencode_mapped_helper

inject_gencode_mapped_helper主要執行流程如下:

typedef struct {
 uint64 app_xax;
 uint64 dr_base;
 uint64 ntdll_base;
 uint64 tofree_base;
 uint64 hook_location;
 uint hook_prot;
 bool late_injection;
 char dynamorio_lib_path[MAX_PATH];
} earliest_args_t;
static uint64
inject_gencode_mapped_helper(HANDLE phandle, char *dynamo_path, uint64 hook_location,
 byte hook_buf[EARLY_INJECT_HOOK_SIZE], byte *map,
 void *must_reach, bool x86_code, bool late_injection,
 uint old_hook_prot)
{
 const size_t remote_alloc_sz = 2 * PAGE_SIZE; 
 const size_t code_alloc_sz = PAGE_SIZE;
 earliest_args_t args;
 /* 使用NTAllocateVirtualMemory在目標進程的虛擬空間中申請2 page內存*/
 remote_code_buf =
 (uint64)allocate_remote_code_buffer(phandle, remote_alloc_sz, must_reach);
 /* 在本進程申請1 page內存 */
 local_code_buf = allocate_remote_code_buffer(NT_CURRENT_PROCESS, code_alloc_sz, NULL);
 /* hook_code_buf和remote_code_buf都指向目標進程第一個page */
 hook_code_buf = remote_code_buf;
 /* remote_data此時指向目標進程第二個page */
 remote_data = remote_code_buf + code_alloc_sz;
 args.dr_base = (uint64)map;
 args.ntdll_base = find_remote_dll_base(phandle, target_64, "ntdll.dll");
 if (args.ntdll_base == 0)
 goto error;
 args.tofree_base = remote_code_buf;
 args.hook_location = hook_location;
 args.hook_prot = old_hook_prot;
 args.late_injection = late_injection;
 strncpy(args.dynamorio_lib_path, dynamo_path,
 BUFFER_SIZE_ELEMENTS(args.dynamorio_lib_path));
 /* 將args寫入到目標進程的第二個page也就是remote_data中 */
 write_remote_memory_maybe64(phandle, remote_data, &args, sizeof(args),
 &num_bytes_out)
 ...
 /* 這里會在local_code_buf 構造的代碼 */ 
 構造的代碼如下:
 mov dword ptr ds:[remote_data],eax ;也就是將eax存在args.app_xax
 mov eax,offset ntdll!RtlUserThreadStart (772641e0);hook_location(目標進程主線程eip)
 mov dword ptr [eax],0E9683D83h;恢復hook
 mov dword ptr [eax+4],74007730h
 mov dword ptr [eax+8],680D8B0Eh
 mov byte ptr [eax+0Ch],0E9h
 mov byte ptr [eax+0Dh],30h
 mov eax,remote_data
 push offset ntdll!RtlUserThreadStart (772641e0);hook_location
 jmp dynamorio.dll!dynamorio_earliest_init_takeover
 /* 將上面在local_code_buf 構造的代碼 寫入到remote_code_buf(hook_code_buf) */
 write_remote_memory_maybe64(phandle, hook_code_buf, local_code_buf,
 cur_local_pos - local_code_buf, &num_bytes_out) 
 return hook_code_buf;
}

我們同樣跟蹤驗證此函數。讓大家對此過程更清晰。

內存分配后各個參數

remote_code_buf = 0xb60000

remote_data = 0xb61000

local_code_buf = 0x010d0000

查看agrs:

args寫入到目標進程的第二個page:

查看構造的代碼:

寫入remote_code_buf:

我們現在已經分析完inject_gencode_mapped_helper,現在我們回到inject_into_new_process,hook_target就是我們的remote_code_buf:

執行后如下:

總結:可以看到我們劫持目標進程是通過修改目標進程主線程的eip到remote_code_buf來實現的,我們在整個過程中都沒有修改hook_location里的值,所以我認為那些恢復hook的操作是沒有必要的。

dr_inject_process_run

恢復由dr_inject_process_create()創建的進程中的暫停線程。

參數

[in] data 由dr_inject_process_create()返回的指針。

bool
dr_inject_process_run(void* data)
{
 dr_inject_info_t* info = (dr_inject_info_t*)data;
 if (info->attached == true) {
 /* detach the debugger */
 DebugActiveProcessStop(info->pi.dwProcessId);
 }
 /* resume the suspended app process so its main thread can run */
 /* 恢復主線程*/
 ResumeThread(info->pi.hThread);
 close_handle(info->pi.hThread);
 return true;
}

執行ResumeThread后,我們切換到子進程,記住此時的寄存器信息:

00b60000 89050010b600 mov dword ptr ds:[0B61000h],eax
00b60006 b8e0412677 mov eax,772641E0h
00b6000b c74000833d68e9 mov dword ptr [eax],0E9683D83h
00b60012 c7400430770074 mov dword ptr [eax+4],74007730h
00b60019 c740080e8b0d68 mov dword ptr [eax+8],680D8B0Eh
00b60020 c6400ce9 mov byte ptr [eax+0Ch],0E9h
00b60024 c6400d30 mov byte ptr [eax+0Dh],30h
00b60028 b80010b600 mov eax,0B61000h ;remote_data
00b6002d 68e0412677 push 772641E0h ;RtlUserThreadStart
00b60032 e9a634256f jmp dynamorio!dynamorio_earliest_init_takeover (6fdb34dd)

主函數已經分析完了,我們來總結一下這個過程,首先使用暫停線程的方式打開目標進程,將dynamorio.dll注入到目標進程空間中,在目標進程申請空間并寫入跳轉到dynamorio!dynamorio_earliest_init_takeover的代碼,修改目標進程線程EIP到這段代碼中。這樣之后當恢復線程運行的時候,就會運行到dynamorio_earliest_init_takeover中。

dynamorio_earliest_init_takeover

dynamorio_earliest_init_takeover函數 是在x86.asm_core.s中用匯編寫的,如下:

1:003> u 6fdb34dd l20
dynamorio!dynamorio_earliest_init_takeover [E:\dynamorio\build32\core\x86.asm_core.s @ 4381]:
6fdb34dd 50 push eax 
6fdb34de 8d6424fc lea esp,[esp-4] 
6fdb34e2 8da424a8fdffff lea esp,[esp-258h] ;提高棧頂
6fdb34e9 ffb42460020000 push dword ptr [esp+260h] 
6fdb34f0 9c pushfd ;構建priv_mcontext_t結構
6fdb34f1 60 pushad
6fdb34f2 8d0424 lea eax,[esp] 
6fdb34f5 50 push eax 
6fdb34f6 e825fdfcff call dynamorio!get_simd_vals (6fd83220)
6fdb34fb 8d642404 lea esp,[esp+4]
6fdb34ff 8d842480020000 lea eax,[esp+280h]
6fdb3506 8944240c mov dword ptr [esp+0Ch],eax
6fdb350a 8d1424 lea edx,[esp]
6fdb350d 8b44240c mov eax,dword ptr [esp+0Ch]
6fdb3511 8d400c lea eax,[eax+0Ch]
6fdb3514 8944240c mov dword ptr [esp+0Ch],eax
6fdb3518 8b842484020000 mov eax,dword ptr [esp+284h]
6fdb351f 8b08 mov ecx,dword ptr [eax]
6fdb3521 894c241c mov dword ptr [esp+1Ch],ecx
6fdb3525 52 push edx ;mc
6fdb3526 50 push eax ;arg_ptr
6fdb3527 e82428f5ff call dynamorio!dynamorio_earliest_init_takeover_C (6fd05d50)
6fdb352c 8d642408 lea esp,[esp+8]
6fdb3530 61 popad
6fdb3531 9d popfd
6fdb3532 8da4245c020000 lea esp,[esp+25Ch]
6fdb3539 8d642408 lea esp,[esp+8]
6fdb353d c3 ret

此函數主要調用dynamorio!dynamorio_earliest_init_takeover_C,這之前的操作主要為了構建priv_mcontext_t結構。

dynamorio_earliest_init_takeover_C

priv_mcontext_t結構保存了目標進程的上下文。對比剛切換子進程時的上下文,可以看到此時EIP為RtlUserThreadStart:

arg_ptr為remote_data

void
dynamorio_earliest_init_takeover_C(byte *arg_ptr, priv_mcontext_t *mc)
{
 int res;
 bool earliest_inject;
 /* Windows-specific code for the most part */
 earliest_inject = earliest_inject_init(arg_ptr);
 /* Initialize now that DR dll imports are hooked up */
 if (earliest_inject) {
 dr_earliest_injected = true;
 dr_earliest_inject_args = arg_ptr;
 } else
 dr_early_injected = true;
 res = dynamorio_app_init();
 ASSERT(res == SUCCESS);
 ASSERT(dynamo_initialized && !dynamo_exited);
 LOG(GLOBAL, LOG_TOP, 1, "taking over via earliest injection in %s", __FUNCTION__);
 /* earliest_inject_cleanup() is called within dynamorio_app_init() to avoid
 * confusing the exec areas scan
 */
 dynamorio_app_take_over_helper(mc);
}

此函數的關鍵在于dynamorio_app_init和dynamorio_app_take_over_helper。

dynamorio_app_init

由于初始化操作很多,當以后用到的時候我們再回頭分析。

DYNAMORIO_EXPORT int
dynamorio_app_init(void)
{
 dynamorio_app_init_part_one_options();
 return dynamorio_app_init_part_two_finalize();
}
void
dynamorio_app_init_part_one_options(void)
{
 此函數主要讀取配置文件
}
int
dynamorio_app_init_part_two_finalize(void)
{
 ...
 /* 加載client 這里我們使用的是drcov.dll */
 instrument_load_client_libs()
 /* 此函數很重要 之后會用到 */
 callback_interception_init_start()
 /* 加載與client相關的模塊*/
 loader_init_prologue(); 
 /* 此函數初始化dcontext 和dstack*/
 dynamo_thread_init(NULL, NULL, NULL, false);
 /* 這個函數主要執行client的主函數*/
 instrument_init();
}

此函數執行了很多初始化操作,我先貼出對現在有用的操作,當在之后用到的時候再回頭分析。現在instrument_init是關鍵,我們進入查看其實現。

void
instrument_init(void)
{
 for (i = 0; i < num_client_libs; i++) {
 /* 將init賦值成drcov!dr_client_main */ 
 void (*init)(client_id_t, int, const char **) =
 (void (*)(client_id_t, int, const char **))(
 lookup_library_routine(client_libs[i].lib, INSTRUMENT_INIT_NAME));
 if (init != NULL)
 /* 調用dr_client_main */
 (*init)(client_libs[i].id, client_libs[i].argc, client_libs[i].argv);
 }
}

可以看到此函數調用了drcov!dr_client_main,讓我們接著深入看看覆蓋率信息是怎么收集的。

dr_client_main

DR_EXPORT void
dr_client_main(client_id_t id, int argc, const char *argv[])
{
 drcovlib_options_t ops = {
 sizeof(ops),
 };
 dr_set_client_name("DrCov", "http://dynamorio.org/issues");
 client_id = id;
 /* 驗證參數 */
 options_init(id, argc, argv, &ops);
 /* 一旦調用此例程,drcovlib 的操作就會生效并開始收集覆蓋率信息。*/
 if (drcovlib_init(&ops) != DRCOVLIB_SUCCESS) {
 NOTIFY(0, "fatal error: drcovlib failed to initialize");
 dr_abort();
 }
 if (!dr_using_all_private_caches()) {
 const char *logname;
 if (drcovlib_logfile(NULL, &logname) == DRCOVLIB_SUCCESS)
 NOTIFY(1, "", logname);
 }
 if (nudge_kills) {
 drx_register_soft_kills(event_soft_kill);
 dr_register_nudge_event(event_nudge, id);
 }
 /* 為進程退出事件注冊一個回調函數。*/
 dr_register_exit_event(event_exit);
}
/* event_exit回調如下 當進程退出的時候會調用drcovlib_exit */
static void
event_exit(void)
{
 drcovlib_exit();
}

options_init

此函數的作用是驗證參數,比如-logdir可以指定覆蓋率文件路徑。由于我們沒有指定參數,所以此函數ops輸出如下:

之后調用drcovlib_init。

drcovlib_init

官方解釋如下:初始化drcovlib擴展。必須在任何其他例程之前調用。可以多次調用(通常由不同的組件調用),但每次調用必須與相應的drcovlib_exit()的調用配對。

一旦這個例程被調用,drcovlib的操作就會生效,并開始收集覆蓋信息。

參數

[in] ops 指定控制drcovlib操作方式的可選參數。

typedef struct _per_thread_t {
 void *bb_table;
 file_t log;
 char logname[MAXIMUM_PATH];
} per_thread_t;
static per_thread_t *global_data;
drcovlib_status_t
drcovlib_init(drcovlib_options_t *ops)
{
 ...
 /* 讀取ops的值 */
 drmgr_init();
 drx_init();
 /* 注冊創建線程回調 */ 
 drmgr_register_thread_init_event(event_thread_init);
 /* 注冊了線程退出事件的回調函數。*/
 drmgr_register_thread_exit_event(event_thread_exit);
 /* 此函數是收集覆蓋率信息的關鍵 */
 /* event_basic_block_analysis此回調函數是如何收集覆蓋率信息的,我們將在后續的章節中介紹 */
 /* 我們主要查看drmgr_register_bb_instrumentation_event這個回調注冊函數 */ 
 drmgr_register_bb_instrumentation_event(event_basic_block_analysis, NULL, NULL);
 dr_register_filter_syscall_event(event_filter_syscall);
 drmgr_register_pre_syscall_event(event_pre_syscall);
 /* 此函數創建覆蓋率文件,并創建global_data結構 */
 return event_init();
}

drmgr_register_bb_instrumentation_event

這個回調注冊函數到底將回調注冊到了哪里,到底在什么時候會調用我們的回調函數,我們接著深入研究。

typedef struct _cb_list_t {
 union { /* array of callbacks */
 cb_entry_t *bb;
 generic_event_entry_t *generic;
 byte *array;
 } cbs;
 size_t entry_sz; /* size of each entry */
 size_t num_def; /* defined (may not all be valid) entries in array */
 size_t num_valid; /* valid entries in array */
 size_t capacity; /* allocated entries in array */
 /* We support only keeping events when a user has requested them.
 * This helps with things like DR's assert about a filter event (i#2991).
 */
 void (*lazy_register)(void);
 void (*lazy_unregister)(void);
} cb_list_t;
static cb_list_t cblist_instrumentation;
DR_EXPORT
bool
drmgr_register_bb_instrumentation_event(drmgr_analysis_cb_t analysis_func,
 drmgr_insertion_cb_t insertion_func,
 drmgr_priority_t *priority)
{
 if (analysis_func == NULL && insertion_func == NULL)
 return false; /* invalid params */
 return drmgr_bb_cb_add(&cblist_instrumentation, (void *)analysis_func,
 (void *)insertion_func, priority, NULL /* no user data */,
 cb_entry_set_fields_instrumentation);
}

drmgr_bb_cb_add

static bool
drmgr_bb_cb_add(cb_list_t *list, void *func1, void *func2, drmgr_priority_t *priority,
 void *user_data, /*passed at registration */
 void (*set_cb_fields)(cb_entry_t *e, void *f1, void *f2))
{
 cb_entry_t *new_e = &list->cbs.bb[idx];
 set_cb_fields(new_e, func1, func2);
}

此函數調用了set_cb_fields,set_cb_fields是一個函數指針參數,實際調用的是cb_entry_set_fields_instrumentation

static void
cb_entry_set_fields_instrumentation(cb_entry_t *new_e, void *func1, void *func2)
{
 new_e->has_pair = true;
 new_e->cb.pair.analysis_cb = (drmgr_analysis_cb_t)func1;
 new_e->cb.pair.insertion_cb = (drmgr_insertion_cb_t)func2;
}

我們現在知道注冊drmgr_analysis_cb_t回調就是將回調函數地址賦值給new_e->cb.pair.analysis_cb。但是什么時候調用它將在之后的章節中分析。

總結

drcov首先創建log文件,之后注冊收集覆蓋率信息的回調函數和進程退出回調,在進程退出的時候會將覆蓋率信息寫入log文件中。此過程會在之后的章節中分析。

dynamorio_app_init分析完畢,接下來分析dynamorio_app_take_over_helper。

dynamorio_app_take_over_helper

void
dynamorio_app_take_over_helper(priv_mcontext_t *mc)
{
 /* 主要調用dynamo_start */
 dynamo_start(mc);
}

dynamo_start

void
dynamo_start(priv_mcontext_t *mc)
{
 priv_mcontext_t *mcontext;
 /* 得到dcontext結構 此結構已在上面初始化函數中完成初始化 */
 dcontext_t *dcontext = get_thread_private_dcontext();
 /* 這里很關鍵將dcontext->next_tag賦值 */
 dcontext->next_tag = mc->pc;
 /* 將目標進程上下文結構賦值給dcontext中的mcontext */
 mcontext = get_mcontext(dcontext);
 *mcontext = *mc;
 /* 清零pc */
 mcontext->pc = 0;
 /* dcontext->dstack也在上面初始化函數中完成初始化 */
 call_switch_stack(dcontext, dcontext->dstack, (void (*)(void *))d_r_dispatch,
 NULL /*not on d_r_initstack*/, true /*return on error*/);
}
static INLINE_FORCED priv_mcontext_t *
get_mcontext(dcontext_t *dcontext)
{
 return &(dcontext->upcontext.upcontext.mcontext);
}

調用call_switch_stack

 call_switch_stack PROC
 mov eax, esp ;用eax找參數
 push ebx 
 mov ebx, eax 
 push edi ;保存此時的edi
 mov edi, esp ;將esp保存在edi中
 mov edx, [3*4 + eax] ;d_r_dispatch函數賦值給edx
 mov ecx, [1*4 + eax] ;dcontext賦值給ecx
 mov esp, [2*4 + eax] ;切換成dstack的堆棧
 cmp dword ptr [4*4 + eax], 0 ;參數4為0
 je call_dispatch_alt_stack_no_free ;跳轉
 mov eax, [4*4 + eax]
 mov dword ptr [eax], 0
call_dispatch_alt_stack_no_free:
 push ecx 
 call edx ;調用d_r_dispatch 參數為dcontext
 lea esp, [4*1 + esp]
 mov esp, edi
 mov eax, ebx
 cmp byte ptr [5*4 + eax], 0 
 je unexpected_return
 pop edi
 pop ebx
 mov esp, eax
 ret
 call_switch_stack ENDP

可以看到此函數將堆棧切換到dstack,之后調用d_r_dispatch。

四、總結

我們成功劫持控制了目標程序,并且注冊了收集覆蓋率信息的回調函數,最后以一個干凈的堆棧調用d_r_dispatch。

d_r_dispatch是DynamoRIO控制管理的中心,下一章我們將分析d_r_dispatch。請記住一點DynamoRIO永遠不會運行目標程序代碼,而是讓目標程序代碼以一個基本塊復制到代碼緩存中,然后在本地執行緩存的代碼。此過程將在下一章詳細分析。