本系列将以官网资料为基础主要通过动态跟踪来解析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,所以程序不应太过复杂。我们使用的简单程序如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
在命令行输入
1 |
|
开启子进程调试,下断并运行到主函数中,接下来我们开始。
主函数在drdeploy.c中 我将把主函数简化保留关键的部分。主函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
可以看到主函数的关键就在于这三个函数,我们接下来逐个分析。
首先我们看看官网对此函数是如何进行解释的:
为指定的可执行文件和命令行创建一个新进程。进程中的初始线程被暂停。
参数
[in] app_name 目标可执行文件的路径
[in] app_cmdline 一个以NULL结尾的字符串数组,代表应用程序的命令行
[out] data 一个不透明的指针,它应该被传递给后续的drinject*例程以引用这个进程。
简化后dr_inject_process_create主要执行流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
可以看到此函数主要是以暂停线程的方式创建了目标进程。我们跟踪进去看看info的结构,发现info主要保存一些进程信息
在执行dr_inject_process_inject之前会在C:\Users\Lenovo\dynamorio目录下创建以进程名和pid命名的配置文件,我们可以查看此配置文件:
之后调用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的主要执行流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
可以看到此函数是一个封装,主要实现在inject_into_new_process中
简化后inject_into_new_process的主要实现流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
我们知道了此函数的大致流程,接下来我们来深入研究其各个参数和各函数的实现过程。
我们首先验证一下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主要流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
此函数利用NtCreateSection和NtMapViewOfSection将dynamorio.dll注入到目标进程中:
之后调用inject_gencode_mapped_helper函数
inject_gencode_mapped_helper
inject_gencode_mapped_helper主要执行流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
我们同样跟踪验证此函数。让大家对此过程更清晰。
内存分配后各个参数
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_create()创建的进程中的暂停线程。
参数
[in] data 由dr_inject_process_create()返回的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
执行ResumeThread后,我们切换到子进程,记住此时的寄存器信息:
1 2 3 4 5 6 7 8 9 10 |
|
主函数已经分析完了,我们来总结一下这个过程,首先使用暂停线程的方式打开目标进程,将dynamorio.dll注入到目标进程空间中,在目标进程申请空间并写入跳转到dynamorio!dynamorio_earliest_init_takeover的代码,修改目标进程线程EIP到这段代码中。这样之后当恢复线程运行的时候,就会运行到dynamorio_earliest_init_takeover中。
dynamorio_earliest_init_takeover函数 是在x86.asm_core.s中用汇编写的,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
此函数主要调用dynamorio!dynamorio_earliest_init_takeover_C,这之前的操作主要为了构建priv_mcontext_t结构。
priv_mcontext_t结构保存了目标进程的上下文。对比刚切换子进程时的上下文,可以看到此时EIP为RtlUserThreadStart:
arg_ptr为remote_data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
此函数的关键在于dynamorio_app_init和dynamorio_app_take_over_helper。
由于初始化操作很多,当以后用到的时候我们再回头分析。
1 2 3 4 5 6 |
|
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
此函数执行了很多初始化操作,我先贴出对现在有用的操作,当在之后用到的时候再回头分析。现在instrument_init是关键。我们进入查看其实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
可以看到此函数调用了drcov!dr_client_main,让我们接着深入看看覆盖率信息是怎么收集的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
options_init
此函数的作用是验证参数,比如-logdir可以指定覆盖率文件路径。由于我们没有指定参数,所以此函数ops输出如下:
之后调用drcovlib_init。
drcovlib_init
官方解释如下:
初始化drcovlib扩展。必须在任何其他例程之前调用。可以多次调用(通常由不同的组件调用),但每次调用必须与相应的drcovlib_exit()的调用配对。
一旦这个例程被调用,drcovlib的操作就会生效,并开始收集覆盖信息。
参数
[in] ops 指定控制drcovlib操作方式的可选参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
drmgr_register_bb_instrumentation_event
这个回调注册函数到底将回调注册到了哪里,到底在什么时候会调用我们的回调函数,我们接着深入研究。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
drmgr_bb_cb_add
1 2 3 4 5 6 7 8 |
|
此函数调用了set_cb_fields,set_cb_fields是一个函数指针参数,实际调用的是cb_entry_set_fields_instrumentation
1 2 3 4 5 6 7 |
|
我们现在知道注册drmgr_analysis_cb_t回调就是将回调函数地址赋值给new_e->cb.pair.analysis_cb。但是什么时候调用它将在之后的章节中分析。
总结
drcov首先创建log文件,之后注册收集覆盖率信息的回调函数和进程退出回调,在进程退出的时候会将覆盖率信息写入log文件中。此过程会在之后的章节中分析。
dynamorio_app_init分析完毕,接下来分析dynamorio_app_take_over_helper
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
调用call_switch_stack
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
可以看到此函数将堆栈切换到dstack,之后调用d_r_dispatch。
我们成功劫持控制了目标程序,并且注册了收集覆盖率信息的回调函数,最后以一个干净的堆栈调用d_r_dispatch。
d_r_dispatch是DynamoRIO控制管理的中心,下一章我们将分析d_r_dispatch。请记住一点DynamoRIO永远不会运行目标程序代码,而是让目标程序代码以一个基本块复制到代码缓存中,然后在本地执行缓存的代码。此过程将在下一章详细分析。
[1] https://dynamorio.org/
[2] 《Efficient, Transparent, and Comprehensive Runtime Code Manipulation》
[3] https://bbs.kanxue.com/thread-263357.htm