DIYDynamoRIO
动态二进制插桩框架DynamoRIO通过将程序代码进行反复插桩(Instrumentation)执行构建了源程序代码与操纵代码之间的桥梁,使DynamoRIO的客户端编写者能够在更高的层面上驾驭原有的程序代码.虽然程序的载体还是被编译成原生的汇编指令集执行,但是不管是原生代码还是程序行为逻辑DynamoRIO为我们提供丰富的API已经把这些封装成了足够友好操作方式暴露给客户端编写者使用,用户可以透明的修改原有的程序代码(HotPatch),执行追踪,Hook,调试,模拟等高级运行时操纵(Runtime Code Manipulation )技术.本文主要分析DynamoRIO插桩的主要流程和实现原理,深入源码片段中几个有意思的小节体现作者构思的巧妙之处,并附加相关demo分析让读者加深对DynamoRIO的认识.
Install / Use
/learn @cbwang505/DIYDynamoRIOREADME
引用
这篇文章的目的是对动态二进制插桩框架DynamoRIO的原理和基本实现过程进行全面的介绍.
[toc]
简介
动态二进制插桩框架DynamoRIO通过将程序代码进行反复插桩(Instrumentation)执行构建了源程序代码与操纵代码之间的桥梁,使DynamoRIO的客户端编写者能够在更高的层面上驾驭原有的程序代码.虽然程序的载体还是被编译成原生的汇编指令集执行,但是不管是原生代码还是程序行为逻辑DynamoRIO为我们提供丰富的API已经把这些封装成了足够友好操作方式暴露给客户端编写者使用,用户可以透明的修改原有的程序代码(HotPatch),执行追踪,Hook,调试,模拟等高级运行时操纵(Runtime Code Manipulation )技术.本文主要分析DynamoRIO插桩的主要流程和实现原理,深入源码片段中几个有意思的小节体现作者构思的巧妙之处,并附加相关demo分析让读者加深对DynamoRIO的认识.
DynamoRIO源码流程
入口点分析
drrun由于执行流控制的原因无法通过对现有进程注入的方式实现插桩,只能通过drrun启动进程挂起获取进程启动的控制权后在入口点实现注入client的dll模块. drrun调用CreateProcess后调用inject_into_thread加载相关dll,方法还是普通的nt_write_virtual_memory,nt_remote_protect_virtual_memory,nt_get_context,nt_set_context将load_dynamo汇编函数代码注入目标进程,通过之前CONTEXT获取 CXT_XIP等字段即程序的entrypiont写入之后priv_mcontext_t结构中后, 将其作为栈REG_XSP指针作为参数传给auto_setup函数,其中初始化了DynamoRIO中的一个重要结构dcontext_t也就是DynamoRIO全局上下文变量,其中 (dcontext_t*)dcontext->next_tag 来源于之前 (priv_mcontext*)mcontext->pc,,最后调用call_switch_stack不断调用d_r_dispatch这个回调插桩函数实现整个程序的循环.
/* void call_switch_stack(void *func_arg, // 1*ARG_SZ+XAX
* byte *stack, // 2*ARG_SZ+XAX
* void (*func)(void *arg), // 3*ARG_SZ+XAX
* void *mutex_to_free, // 4*ARG_SZ+XAX
* bool return_on_return) // 5*ARG_SZ+XAX
/* get all args with same offset(xax) regardless of plaform */
/* Stack alignment doesn't matter b/c we're swapping. */
mov REG_XAX, REG_XSP
/* we need a callee-saved reg across our call so save it onto stack */
push REG_XBX
mov REG_XBX, REG_XAX
/* alignment doesn't matter: swapping stacks */
push IF_X64_ELSE(r12, REG_XDI) /* xdi is used for func param in X64 */
mov IF_X64_ELSE(r12, REG_XDI), REG_XSP
/* set up for call */
mov REG_XDX, [3*ARG_SZ + REG_XAX] /* func */
mov REG_XCX, [1*ARG_SZ + REG_XAX] /* func_arg */
mov REG_XSP, [2*ARG_SZ + REG_XAX] /* stack */
cmp PTRSZ [4*ARG_SZ + REG_XAX], 0 /* mutex_to_free */
je call_dispatch_alt_stack_no_free
mov REG_XAX, [4*ARG_SZ + REG_XAX]
mov DWORD [REG_XAX], 0
call_dispatch_alt_stack_no_free:
//这里就是循环调用d_r_dispatch
CALLC1(REG_XDX, REG_XCX)
mov REG_XSP, IF_X64_ELSE(r12, REG_XDI)
mov REG_XAX, REG_XBX
cmp BYTE [5*ARG_SZ + REG_XAX], 0 /* return_on_return */
je GLOBAL_REF(unexpected_return)
pop IF_X64_ELSE(r12, REG_XDI)
pop REG_XBX
mov REG_XSP, REG_XAX
ret
插桩循环分析
d_r_dispatch中的主要逻辑是将当前被插桩程序的当前指令位置xip也就是bb->cur_pc解码decode成一个一个basicblock(基本块),将其中每个指令对应一个指令instruction结构放入instrlist_t容器中,处理每个块对应的分支结构也就是cti(其中包括call(程序集内部调用),indirectcall(通过寄存器计算出的地址进行间接调用),cbr(条件跳转),ubr(非条件跳转),mbr(通过寄存器等的间接跳转或调用分支)),将这个容器中的指令encode后生成一个fragment,链接其中的incoming_stubs对应结构linkstub_t中cti_offset构成了所有块之间的调用顺序,调用dcontext_t中fcache_enter方法跳转到encode到程序运行空间vmarea中的start_pc中执行插桩循环,最后又回到d_r_dispatch,这是DynamoRIO主干流程.
void d_r_dispatch(dcontext_t *dcontext){
while(true){
...
//先检测是否生成trace缓存块
fragment_t* targetf = monitor_cache_enter(dcontext, targetf);
if(targetf==NULL)
{
//直到循环构建fragment_t完成
targetf = build_basic_block_fragment(
dcontext, dcontext->next_tag, 0, true /*link*/,
true /*visible*/
_IF_CLIENT(false /*!for_trace*/) _IF_CLIENT(NULL));
}
if (targetf != NULL) {
//执行当前构建好的fragment_t,见下文路由逻辑分析
dispatch_enter_fcache(dcontext, targetf);
}
fragment_t * build_basic_block_fragment(dcontext_t *dcontext, app_pc start, uint initial_flags,
bool link,
bool visible _IF_CLIENT(bool for_trace)
_IF_CLIENT(instrlist_t **unmangled_ilist))
{
build_bb_ilist(dcontext, &bb);
return f = emit_fragment_ex(dcontext, start, bb.ilist, bb.flags, bb.vmlist, link, visible);{
return emit_fragment_common(dcontext, tag, ilist, flags, vmlist, link, visible,
NULL /* not replacing */);
}
}
build_basic_block_fragment内部调用了build_bb_ilist用于decode解码当前指令,DynamoRIO内部实现了一套编码与解码对相对于汇编语言的操作库用于将每一条汇编指令翻译成instr_t 结构,其中包含多个源操作数和目的操作数opnd_t结构和当前指令的实际解析地址app_pc translation和操作码opcode等字段,DynamoRIO的api提供了对于instr可以调用opnd_create_xxx创建操作数后将其和操作码opcode作为参数调用instr_create_xxx系列函数创建instr_t 结构,常用基于寄存器类操作数通常可表示为base_reg + index_reg*scale + disp类型通用处理. build_bb_ilist中循环每个decode后的instr直到确定当前instr存在分支指令,根据分支类型计算出direct_stubs和indirect_stubs的数量,根据这些分支数量在fragment尾部申请若干个linkstub(用于保存分支类型和偏移量),完成fragment的构建初始化.在set_linkstub_fields把linkstub对应分支相对于当前fragment起始地址的偏移量字段赋值,将其中需要退出插桩的linkstub设置出口目标为对应ibl_routine表中的回调项,最后调用默认编码函数instr_encode_arch序列化出程序实际运行的汇编代码和linkstub的分支路由代码完成fragment的构建.
static void build_bb_ilist(dcontext_t *dcontext, build_bb_t *bb){
//最终构建好的块起始地址start_pc就是解码出来的第一条指令
bb->cur_pc = bb->start_pc;
//在这里使用2中模式解码
if (bb->full_decode) {
//默认解码函数decode_with_ldstex
bb->cur_pc = IF_AARCH64_ELSE(decode_with_ldstex,
decode)(dcontext, bb->cur_pc, bb->instr);
if (bb->record_translation)
instr_set_translation(bb->instr, bb->instr_start);
} else {
//分支结构的解码函数decode_cti_with_ldstex
instr_reset(dcontext, bb->instr);
bb->cur_pc = IF_AARCH64_ELSE(decode_cti_with_ldstex, decode_cti)(dcontext, bb->cur_pc, bb->instr);
}
//笔者简化了分支生成代码方便读者理解
if (instr_is_cti(bb->instr))
{
//根据最后一个instr的类型确定当前块的分支类型
bb->ibl_branch_type= instr_branch_type(bb->instr)
bb->exit_type |= instr_branch_type(bb->instr);
//处理当前分支结束后需要继续处理的路由,这个由会见对应的fragment加入不同的ibl表中
bb->exit_target = get_ibl_routine(dcontext, get_ibl_entry_type(bb->exit_type), DEFAULT_IBL_BB(), bb->ibl_branch_type);
}
//处理当前块解析对应留给DynamoRIO客户端的回调
if (!client_process_bb(dcontext, bb)) {
//里面使用这个宏触发回调bb_callbacks,详见插桩API回调分析小节
call_all_ret(ret, |=, , bb_callbacks, int (*)(void *, void *, instrlist_t *, bool, bool), (void *)dcontext,(void *)tag, bb, for_trace, translating);
}}
static fragment_t * emit_fragment_common(dcontext_t *dcontext, app_pc tag, instrlist_t *ilist, uint flags,void *vmlist, bool link_fragment, bool add_to_htable,fragment_t *replace_fragment)
{
//根据当前块分支数量在fragment尾部申请若干个linkstub
fragment_t * f = fragment_create(dcontext, tag, offset + extra_jmp_padding_body, num_direct_stubs,
num_indirect_stubs, stub_size_total + extra_jmp_padding_stubs,
flags);
//编码fragment,链接所有direct_stubs和indirect_stubs
cache_pc pc = set_linkstub_fields(dcontext, f, ilist, num_direct_stubs, num_indirect_stubs,
true /*encode each instr*/);
for (l = FRAGMENT_EXIT_STUBS(f); l; l = LINKSTUB_NEXT_EXIT(l)) {
//内联模式直接复制对应ibl_routine表中的回调项代码
if (indirect && can_inline) {
ibl_code_t *ibl_code = get_ibl_routine_code(dcontext, extract_branchtype(l->flags), f->flags);
memcpy(pc, ibl_code->inline_ibl_stub_template, ibl_code->inline_stub_length);
}else{
//非内联模式设置跳转
cache_pc exit_target=get_ibl_routine_ex(dcontext, get_unlinked_type(ibl_type.link_state),
ibl_type.source_fragment_type,
ibl_type.branch_type _IF_X86_64(mode));
pc = insert_relative_jump(pc, exit_target, NOT_HOT_PATCHABLE);
}
}
return f;
}
*/
cache_pc
set_linkstub_fields(dcontext_t *dcontext, fragment_t *f, instrlist_t *ilist,
uint num_direct_stubs, uint num_indirect_stubs, bool emit)
{
cache_pc pc=f->start_pc;
linkstub_t* l = FRAGMENT_EXIT_STUBS(f);
for (inst = instrlist_first(ilist); inst; inst = instr_get_next(inst)) {
if (instr_is_exit_cti(inst)) {
//
if (LINKSTUB_CBR_FALLTHROUGH(l->flags)) {
/* target is indicated via cti_offset */
ASSERT_TRUNCATE(l->cti_offset, short, target - f->tag);
l->cti_offset = (ushort) /* really a short */ (target - f->tag);
} else {
ASSERT_TRUNCATE(l->cti_offset, ushort, pc - f->start_pc);
l->cti_offset = (ushort)(pc - f->start_pc);
}
//对于indirect类型分支,里面实际上是调用get_ibl_routine_ex获取ibl表中emit_indirect_branch_lookup,其他类型同理
if (!EXIT_HAS_STUB(l->flags, f->flags)) {
instr_set_branch_target_pc(inst, get_unlinked_entry(dcontext, target));
} else {
//暂时先设为入口pc,会在以后的链接fragment的函数里面再次计算
instr_set_branch_target_pc(inst, pc);
}
}
pc = instr_encode_to_copy(dcontext, inst, vmcode_get_writable_addr(pc),
pc);
{
//调用默认编码函数instr_encode_arch序列化出程序实际运行的汇编代码
instr_encode_arch(dcontext, instr, copy_pc, final_pc, true,NULL _IF_DEBUG(true));
}
}
}
插桩API回调分析
DynamoRIO提供了drmgr_register_bb_instrumentation_ex_event等api实现了hook每个插桩过程的回调,里面又包装了4种回调类型,这些回调的真正入口由上文的call_all_ret宏将参数传给由dr_register_bb_event注册的bb_callbacks这个回调数组分发回调.初学者可能对这些不同的回调类型不知道如何使用,DynamoRIO的几个demo中由例子可以参考,借助简化后还原代码我们来分析下这几种回调分别对应什么功能
DR_EXPORT bool drmgr_register_bb_instrumentation_ex_event(
//用于在初次回调时设置user data供后续回调使用
drmgr_app2app_ex_cb_t app2app_func,
//用于进入回调时获取所有instrlist_t触发设置user data
drmgr_ilist_ex_cb_t analysis_func,
//处理每个回调,可以获取调最后一个instr
drmgr_insertion_cb_t insertion_func,
//用于最后过滤回调
drmgr_ilist_ex_cb_t instru2instru_func,
//选项参数用于排序所注册的回调执行的顺序和优先级
drmgr_priority_t *priority)
{
...
if (app2app_func != NULL) {
ok = drmgr_bb_cb_add(&cblist_app2app, NULL, NULL, NULL, app2app_func, NULL,
NULL, NULL, priority, NULL /* no user data */) &&
}
if (analysis_func != NULL) {
ok = drmgr_bb_cb_add(&cblist_instrumentation, NULL, NULL, ins
Related Skills
node-connect
347.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
108.0kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
347.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
347.2kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
