-
-
[原创]DynamoRIO源码分析(二)--基本块(Basic Blocks)和跟踪 (trace)
-
2023-4-17 17:59 16015
-
前提回顾
上篇我们分析到劫持了目标程序,进行了一系列初始化并且注册了收集覆盖率信息的回调函数,最后以一个干净的堆栈调用d_r_dispatch。现在我们还没有运行目标程序的代码,本章将讲述DynamoRIO如何运行目标程序代码。
概述
下图演示了DynamoRIO的高级设计。DynamoRIO通过将应用程序代码复制到代码缓存中来执行目标应用程序,每次复制一个基本块。代码缓存是通过从DynamoRIO的调度状态到应用程序的调度状态的上下文切换进入的。
d_r_dispatch
现在我们开始,由于我在上篇分析到d_r_dispatch的时候打了快照,现在恢复快照:
请注意dcontext->next_tag为目标进程主线程的EIP(RtlUserThreadStart)
现在我们来分析d_r_dispatch,同样的,我们只保留关键的操作,简化后的函数如下:
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 | void d_r_dispatch(dcontext_t * dcontext) { fragment_t * targetf; fragment_t coarse_f; dispatch_enter_dynamorio(dcontext); targetf = fragment_lookup_fine_and_coarse(dcontext, dcontext - >next_tag, &coarse_f, dcontext - >last_exit); do { if (targetf ! = NULL) { targetf = monitor_cache_enter(dcontext, targetf); } if (targetf ! = NULL) break ; SHARED_BB_LOCK(); if (USE_BB_BUILDING_LOCK() || targetf = = NULL) { targetf = fragment_lookup_fine_and_coarse(dcontext, dcontext - >next_tag, &coarse_f, dcontext - >last_exit); } if (targetf = = NULL) { targetf = build_basic_block_fragment(dcontext, dcontext - >next_tag, 0 , true / * link * / , true / * visible * / , false / * !for_trace * / , NULL); } if (targetf = = NULL) break ; } while (true); if (targetf ! = NULL) { if (dispatch_enter_fcache(dcontext, targetf)) { / * won't reach here: will re - enter d_r_dispatch() with a clean stack * / ASSERT_NOT_REACHED(); } else targetf = NULL; / * targetf was flushed * / } } |
现在得到简化后的d_r_dispatch函数,我们以动态跟踪详细分析各个函数的作用。
在执行的过程中发现前面的函数都没有为targetf赋值,直到build_basic_block_fragment函数:
因此我们首先分析build_basic_block_fragment,之后再分析前面的函数做了哪些操作。
build_basic_block_fragment
从名字上就能看出此函数创建了基本块。基本块是从入口点开始,直到到达控制转移(控制转移说白了 就是如跳转,call,ret等,这种不按程序的语句流程执行的指令)。下图便是一个基本块:
首先笔者先带大家了解常见的控制转移指令的缩写:cti(Control Transfer Instructions 控制转移指令),ubr(Unconditional Branch Instruction 无条件跳转指令如jmp),cbr(Conditional Branch Instruction 条件分支指令),mbr(通过寄存器等的间接分支)
现在我们开始分析build_basic_block_fragment此函数,简化后如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | fragment_t * build_basic_block_fragment(dcontext_t * dcontext, app_pc start, uint initial_flags, bool link, bool visible, bool for_trace, instrlist_t * * unmangled_ilist) { fragment_t * f; build_bb_t bb; dr_where_am_i_t wherewasi = dcontext - >whereami; / * 初始化bb * / init_interp_build_bb(dcontext, &bb, start, initial_flags, for_trace, unmangled_ilist); build_bb_ilist(dcontext, &bb); f = emit_fragment_ex(dcontext, start, bb.ilist, bb.flags, bb.vmlist, link, visible); exit_interp_build_bb(dcontext, &bb); return f; } |
让我们来逐个分析各个函数的作用
init_interp_build_bb
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 | static inline void init_interp_build_bb(dcontext_t * dcontext, build_bb_t * bb, app_pc start, uint initial_flags, bool for_trace, instrlist_t * * unmangled_ilist) { dcontext - >bb_build_info = (void * )bb; init_build_bb( bb, start, true / * real interp * / , true / * for cache * / , true / * mangle * / , false / * translation: set below for clients * / , INVALID_FILE, initial_flags | (INTERNAL_OPTION(store_translations) ? FRAG_HAS_TRANSLATION_INFO : 0 ), NULL / * no overlap * / ); if (!TEST(FRAG_TEMP_PRIVATE, initial_flags)) bb - >has_bb_building_lock = true; ..... } static void init_build_bb(build_bb_t * bb, app_pc start_pc, bool app_interp, bool for_cache, bool mangle_ilist, bool record_translation, file_t outf, uint known_flags, overlap_info_t * overlap_info) { memset(bb, 0 , sizeof( * bb)); bb - >check_vm_area = true; bb - >start_pc = start_pc; bb - >app_interp = app_interp; bb - >for_cache = for_cache; if (bb - >for_cache) bb - >record_vmlist = true; bb - >mangle_ilist = mangle_ilist; bb - >record_translation = record_translation; bb - >outf = outf; bb - >overlap_info = overlap_info; bb - >follow_direct = !TEST(FRAG_SELFMOD_SANDBOXED, known_flags); bb - >flags = known_flags; bb - >ibl_branch_type = IBL_GENERIC; / * initialization only * / } |
可以看到init_interp_build_bb此函数为bb分配了空间,并对bb进行了初始化操作。执行此函数后bb的结构如下:
build_bb_ilist
此函数十分庞大,因为它维持着一个基本块的解码操作,我们将之简化后如下:
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | DISABLE_NULL_SANITIZER static void build_bb_ilist(dcontext_t * dcontext, build_bb_t * bb) { dcontext_t * my_dcontext = get_thread_private_dcontext(); bb - >cur_pc = bb - >start_pc; bb - >instr_start = bb - >cur_pc; / * 为bb - >ilist分配空间 并对其初始化 * / bb - >ilist = instrlist_create(dcontext); bb - >instr = NULL; / * 循环为每条指令解码 * / while (true) { / * 为bb - >instr分配空间并初始化 * / bb - >instr = instr_create(dcontext); non_cti_start_pc = bb - >cur_pc; bb - >instr_start = bb - >cur_pc; / * 如果是 64 位执行状态调用decode_with_ldstex * / / * 如果是 32 位执行状态调用decode,这里我们使用decode * / / * decode函数会解析一条指令填充到bb - >instr 并将下一条指令基址给bb - >cur_pc * / bb - >cur_pc = IF_AARCH64_ELSE(decode_with_ldstex, decode)(dcontext, bb - >cur_pc, bb - >instr); total_instrs + + ; / * 如果当前指令为非条件跳转 * / if (instr_is_near_ubr(bb - >instr)) { / * 一般返回false * / / * 此函数会将instr追加到ilist中 * / / * 同时会设置 exit_target为跳转的地址 * / if (bb_process_ubr(dcontext, bb)) continue ; else { / * 一般会进入这里 * / if (bb - >instr ! = NULL) / * else , bb_process_ubr() set exit_type * / bb - >exit_type | = instr_branch_type(bb - >instr); break ; } } / * 如果不是,将当前指令加入到bb - >ilist中 * / else instrlist_append(bb - >ilist, bb - >instr); / * 如果是直接调用 * / if (instr_is_near_call_direct(bb - >instr)) { if (!bb_process_call_direct(dcontext, bb)) { if (bb - >instr ! = NULL) bb - >exit_type | = instr_branch_type(bb - >instr); break ; } } / * 之后都是判断是什么控制转移指令 * / ...... / * 如果是条件跳转 * / else if (instr_is_cti(bb - >instr) && (!instr_is_call(bb - >instr) || instr_is_cbr(bb - >instr))) { total_branches + + ; if (total_branches > = BRANCH_LIMIT) { / * set type of 1st exit cti for cbr (bb - >exit_type is for fall - through) * / / * 设置instr - >flags * / instr_exit_branch_set_type(bb - >instr, instr_branch_type(bb - >instr)); break ; } } / * 但是条件跳转并没有设置exit_target * / } / * while (true)的结尾 * / bb - >end_pc = bb - >cur_pc; / * 此函数执行会调用我们注册的回调 * / / * 我们后面再分析他 * / client_process_bb(dcontext, bb); 如果没有设置exit_target,则会将下一条指令地址给exit_target。 if (bb - >exit_target = = NULL){ bb - >exit_target = (cache_pc)bb - >cur_pc; } 下面过程笔者将通过实例呈现 if (bb - >mangle_ilist && (bb - >instr = = NULL || !instr_opcode_valid(bb - >instr) || !instr_is_near_ubr(bb - >instr) || instr_is_meta(bb - >instr))) { / * 此宏会创建一个jmp指令,参数二为跳转的目标地址 * / instr_t * exit_instr = XINST_CREATE_jump(dcontext, opnd_create_pc(bb - >exit_target)); / * 将此jmp语句加入到ilist中 * / instrlist_append(bb - >ilist, exit_instr); } / * bb - >instr清空 * / bb - >instr = NULL; / * 此函数会将ilist处理成我们想要的状态 * / / * 比如 如果是以直接调用指令结束的基本块 * / / * 此函数会将call 转化成 push eip和 jmp * / mangle_bb_ilist(dcontext, bb); } |
终于我们完成了对此函数的大致解读,为了让我们更加清晰,笔者将通过动态调试分析此过程
首先让我们执行到while (true) ,此时cur_pc为772641e0:
运行decode函数,此时应解码成cmp指令,将此指令填充到bb->instr结构:
此时opcode为0xe,
随后发现其不是cti指令将此cmp指令加入到bb->ilist:
接着解码下一条指令,发现其为条件跳转指令,将其加入ilist跳出while循环,将下一条指令地址给bb->exit_target:
创建ilist的过程还没有结束,因为此条件跳转指令带来了两个分支,为了确定采用了哪个分支,DynamoRIO在条件跳转指令后再添加一个jmp指令,jmp的跳转地址为bb->exit_target(也就是条件跳转指令的下一条指令)。将此jmp语句加入到ilist中。
还没有结束因为还有一个函数我们没有分析,client_process_bb这个函数是在while循环结束后调用的。还记得上篇中我们注册的收集覆盖率信息的回调吗。client_process_bb将调用我们注册的回调。
client_process_bb
回调的调用过程如下:
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 | static bool client_process_bb(dcontext_t * dcontext, build_bb_t * bb) { instrument_basic_block(dcontext, / * DrMem #1735: pass app pc, not selfmod copy pc */ (bb - >pretend_pc = = NULL ? bb - >start_pc : bb - >pretend_pc), bb - >ilist, bb - >for_trace, !bb - >app_interp, &emitflags) } bool instrument_basic_block(dcontext_t * dcontext, app_pc tag, instrlist_t * bb, bool for_trace, bool translating, dr_emit_flags_t * emitflags) { call_all_ret(ret, | = , , bb_callbacks, int ( * )(void * , void * , instrlist_t * , bool , bool ), (void * )dcontext, (void * )tag, bb, for_trace, translating); } #define call_all_ret(ret, retop, postop, vec, type, ...) \ do { \ size_t idx, num; \ / * we will be called even if no callbacks (i.e., (vec).num = = 0 ) * / \ / * we guarantee we're in DR state at all callbacks and clean calls * / \ / * XXX: add CLIENT_ASSERT here * / \ d_r_read_lock(&callback_registration_lock); \ num = (vec).num; \ if (num = = 0 ) { \ d_r_read_unlock(&callback_registration_lock); \ } else if (num < = FAST_COPY_SIZE) { \ callback_t tmp[FAST_COPY_SIZE]; / * 这里将(vec).callbacks赋值给tmp * / memcpy(tmp, (vec).callbacks, num * sizeof(callback_t)); \ d_r_read_unlock(&callback_registration_lock); \ for (idx = 0 ; idx < num; idx + + ) { / * 这里调用tmp * / \ ret retop((( type )tmp[num - idx - 1 ])(__VA_ARGS__)) postop; \ } \ } else { \ callback_t * tmp = HEAP_ARRAY_ALLOC(GLOBAL_DCONTEXT, callback_t, num, \ ACCT_OTHER, UNPROTECTED); \ memcpy(tmp, (vec).callbacks, num * sizeof(callback_t)); \ d_r_read_unlock(&callback_registration_lock); \ for (idx = 0 ; idx < num; idx + + ) { \ ret retop((( type )tmp[num - idx - 1 ])(__VA_ARGS__)) postop; \ } \ HEAP_ARRAY_FREE(GLOBAL_DCONTEXT, tmp, callback_t, num, ACCT_OTHER, \ UNPROTECTED); \ } \ } while ( 0 ) |
从上面可知最终是由call_all_ret宏调用,call_all_ret调用了bb_callbacks.callbacks。我们来看看bb_callbacks是什么结构:
可以看到调用的是drmgr!drmgr_bb_event,我们跟进drmgr_bb_event后发现它又调用了drmgr_bb_event_do_instrum_phases。
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 | static dr_emit_flags_t drmgr_bb_event_do_instrum_phases(void * drcontext, void * tag, instrlist_t * bb, bool for_trace, bool translating, per_thread_t * pt, local_cb_info_t * local_info, void * * pair_data, void * * quintet_data) { uint i; cb_entry_t * e; dr_emit_flags_t res = DR_EMIT_DEFAULT; instr_t * inst, * next_inst; uint pair_idx, quintet_idx; int opcode; / * denotes whether opcode instrumentation is applicable for this bb * / bool is_opcode_instrum_applicable = false; hashtable_t local_opcode_instrum_table; / * Pass 1 : app2app * / pt - >cur_phase = DRMGR_PHASE_APP2APP; for (quintet_idx = 0 , i = 0 ; i < local_info - >iter_app2app.num_def; i + + ) { e = &local_info - >iter_app2app.cbs.bb[i]; if (!e - >pri.valid) continue ; if (e - >has_quintet) { res | = ( * e - >cb.app2app_ex_cb)(drcontext, tag, bb, for_trace, translating, &quintet_data[quintet_idx]); quintet_idx + + ; } else res | = ( * e - >cb.xform_cb)(drcontext, tag, bb, for_trace, translating); } / * Pass 2 : analysis * / pt - >cur_phase = DRMGR_PHASE_ANALYSIS; for (quintet_idx = 0 , pair_idx = 0 , i = 0 ; i < local_info - >iter_insert.num_def; i + + ) { e = &local_info - >iter_insert.cbs.bb[i]; if (!e - >pri.valid) continue ; if (e - >has_quintet) { res | = ( * e - >cb.pair_ex.analysis_ex_cb)( drcontext, tag, bb, for_trace, translating, quintet_data[quintet_idx]); quintet_idx + + ; } else { ASSERT(e - >has_pair, "internal pair-vs-quintet state is wrong" ); if (e - >cb.pair.analysis_cb = = NULL) { pair_data[pair_idx] = NULL; } else { / * 调用回调 * / res | = ( * e - >cb.pair.analysis_cb)(drcontext, tag, bb, for_trace, translating, &pair_data[pair_idx]); } pair_idx + + ; } / * XXX: add checks that cb followed the rules * / } / * Pass 3 : instru, per instr * / ... / * Pass 4 : instru optimizations * / ... / * Pass 5 : meta - instrumentation (final) * / ... } |
我们注册的是analysis回调类型,关于回调类型描述可以在官网查询。
之后通过 e->cb.pair.analysis_cb调用我们注册的回调:
关于回调是如何收集覆盖率信息的,将在下篇分析。
总结
我们现在总结一下build_bb_ilist执行过程。首先会解码出一个原始基本块,之后会调用注册的回调,最后记录一些参数并将基本块变成想要的模样。
emit_fragment_ex
函数代码如下 :
1 2 3 4 5 6 7 | fragment_t * emit_fragment_ex(dcontext_t * dcontext, app_pc tag, instrlist_t * ilist, uint flags, void * vmlist, bool link, bool visible) { return emit_fragment_common(dcontext, tag, ilist, flags, vmlist, link, visible, NULL / * not replacing * / ); } |
此函数是一个封装,核心代码在emit_fragment_common中。
emit_fragment_common
简化后如下:
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 | 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_t f结构 * / / * 并将f添加到代码缓存中 * / / * 并且在fragment_t后面创建linkstubs * / f = fragment_create(dcontext, tag, offset + extra_jmp_padding_body, num_direct_stubs, num_indirect_stubs, stub_size_total + extra_jmp_padding_stubs, flags); / * 为ilist中相应的出口cti适当地设置每个linkstub_t * / / * 将ilist中的每个instr编码到f的代码缓存中 * / / * 会将inst中分支的目标pc改为自己指令的pc * / pc = set_linkstub_fields(dcontext, f, ilist, num_direct_stubs, num_indirect_stubs, true / * encode each instr * / ); / * 遍历linkstub_t * / for (l = FRAGMENT_EXIT_STUBS(f); l; l = LINKSTUB_NEXT_EXIT(l)) { / * (direct_linkstub_t * )l l - >stub_pc赋值 此值为exit stub * / separate_stub_create(dcontext, f, l); / * 将代码缓存中cti指令的目标地址设置为exit stub * / patch_branch(FRAG_ISA_MODE(f - >flags), EXIT_CTI_PC(f, l), EXIT_STUB_PC(dcontext, f, l), false); } / * 将此f的地址添加到f - >prev_vmarea * / vm_area_add_fragment(dcontext, f, vmlist); / * 此函数执行链接操作 具体在后面讲解 * / link_new_fragment(dcontext, f); return f; } |
我们知道了emit_fragment_common的大致流程,但仍感觉一头雾水。没关系,接下来通过实践来将此函数的功能呈现。
调用fragment_create创建f,f的结构如下:
请注意这两个数值
tag:为基本块在原始代码中的基址。
start_pc:此基本块在代码缓存中的基址,此时还没有将基本块写入到代码缓存中。
同时,我们注意到f的地址为0x20cc520c。你肯定有疑问为什么start_pc的值为0x20d01004,而不是从0x20d01000开始的。我们查看0x20d01000里的值发现里面存放了f的地址:
此外还为在f后面为每个cti指令创建linkstubs结构,但此时还没有赋值:
接下来执行set_linkstub_fields,此函数将ilist中的每个instr编码到f的代码缓存中,但会将inst中分支目标pc改为自己指令的pc:
并且会为linkstubs赋值:
现在我们终于复制了一份基本块并将它加载到了代码缓存中,但是有一个问题,当切换上下文并执行这个基本块后该怎么切换上下文回到DynamoRIO以复制下一个基本块。这就需要用到exit stub。
接着执行遍历linkstub_t,为l->stub_pc赋值此值为exit stub,将代码缓存中cti指令的目标地址设置为exit stub:
接下来再执行就是关于链接的操作了,由于现在我们内存中只有一个基本块,还无法做到链接。链接操作将在之后分析,现在主要关注这个基本块是怎么被执行的。
总结:
此函数的核心就是将ilist加入到代码缓存中。代码缓存结构如下表示:
exit_interp_build_bb
1 2 3 4 5 6 7 8 9 10 | static inline void exit_interp_build_bb(dcontext_t * dcontext, build_bb_t * bb) { ASSERT(dcontext - >bb_build_info = = (void * )bb); / * Caller's responsibility to clean up since bb.for_cache * / dcontext - >bb_build_info = NULL; / * free the instrlist_t elements * / instrlist_clear_and_destroy(dcontext, bb - >ilist); } |
此函数最后做了清理操作。
到此build_basic_block_fragment大致分析完毕,就差一个链接部分之后分析。
回看d_r_dispatch函数,此时我们拿到了targetf,下一个执行的函数理应是monitor_cache_enter(dcontext, targetf);但此函数控制着trace的创建,此过程在之后分析。
targetf != NULL 跳出循环,之后执行dispatch_enter_fcache。
dispatch_enter_fcache
简化后如下:
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 | static bool dispatch_enter_fcache(dcontext_t * dcontext, fragment_t * targetf) { fcache_enter = get_fcache_enter_shared_routine(dcontext); enter_fcache( dcontext, (fcache_enter_func_t) fcache_enter, targetf - >start_pc ); } static void enter_fcache(dcontext_t * dcontext, fcache_enter_func_t entry, cache_pc pc) { set_fcache_target(dcontext, pc); ( * entry)(dcontext); } void set_fcache_target(dcontext_t * dcontext, cache_pc value) { / * 将targetf - >start_pc赋值给dcontext - >next_tag * / dcontext - >next_tag = value; / * set eip as well to complete mcontext state * / get_mcontext(dcontext) - >pc = value; } |
从上面可以看到此函数核心就是(*entry)(dcontext);我们跟踪看看entry是什么:
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 | 1 : 003 > u 0x20cfaa00 l50 20cfaa00 8b7c2404 mov edi,dword ptr [esp + 4 ] ;保存dcontext 20cfaa04 8b879c020000 mov eax,dword ptr [edi + 29Ch ] ;取dcontext - >next_tag(此值为代码缓存中的基本块基址) 20cfaa0a 64a3ec0e0000 mov dword ptr fs:[ 00000EECh ],eax ;将值存在fs:[ 00000EECh ]中 20cfaa10 b80030c100 mov eax, 0C13000h ;之后保存一些参数,并进行上下文切换,切换到目标进程的上下文。 20cfaa15 64a330000000 mov dword ptr fs:[ 00000030h ],eax 20cfaa1b 8b87d4020000 mov eax,dword ptr [edi + 2D4h ] 20cfaa21 64a304000000 mov dword ptr fs:[ 00000004h ],eax 20cfaa27 8b87ac020000 mov eax,dword ptr [edi + 2ACh ] 20cfaa2d 64a334000000 mov dword ptr fs:[ 00000034h ],eax 20cfaa33 64a1b40f0000 mov eax,dword ptr fs:[ 00000FB4h ] 20cfaa39 8987b4020000 mov dword ptr [edi + 2B4h ],eax 20cfaa3f 8b87b0020000 mov eax,dword ptr [edi + 2B0h ] 20cfaa45 64a3b40f0000 mov dword ptr fs:[ 00000FB4h ],eax 20cfaa4b 64a11c0f0000 mov eax,dword ptr fs:[ 00000F1Ch ] 20cfaa51 8987bc020000 mov dword ptr [edi + 2BCh ],eax 20cfaa57 8b87b8020000 mov eax,dword ptr [edi + 2B8h ] 20cfaa5d 64a31c0f0000 mov dword ptr fs:[ 00000F1Ch ],eax 20cfaa63 64a1a00f0000 mov eax,dword ptr fs:[ 00000FA0h ] 20cfaa69 8987c4020000 mov dword ptr [edi + 2C4h ],eax 20cfaa6f 8b87c0020000 mov eax,dword ptr [edi + 2C0h ] 20cfaa75 64a3a00f0000 mov dword ptr fs:[ 00000FA0h ],eax 20cfaa7b c5fd6f4740 vmovdqa ymm0,ymmword ptr [edi + 40h ] 20cfaa80 c5fd6f8f80000000 vmovdqa ymm1,ymmword ptr [edi + 80h ] 20cfaa88 c5fd6f97c0000000 vmovdqa ymm2,ymmword ptr [edi + 0C0h ] 20cfaa90 c5fd6f9f00010000 vmovdqa ymm3,ymmword ptr [edi + 100h ] 20cfaa98 c5fd6fa740010000 vmovdqa ymm4,ymmword ptr [edi + 140h ] 20cfaaa0 c5fd6faf80010000 vmovdqa ymm5,ymmword ptr [edi + 180h ] 20cfaaa8 c5fd6fb7c0010000 vmovdqa ymm6,ymmword ptr [edi + 1C0h ] 20cfaab0 c5fd6fbf00020000 vmovdqa ymm7,ymmword ptr [edi + 200h ] 20cfaab8 8b4720 mov eax,dword ptr [edi + 20h ] 20cfaabb 50 push eax 20cfaabc 9d popfd 20cfaabd 8b471c mov eax,dword ptr [edi + 1Ch ] 20cfaac0 8b5f10 mov ebx,dword ptr [edi + 10h ] 20cfaac3 8b4f18 mov ecx,dword ptr [edi + 18h ] 20cfaac6 8b5714 mov edx,dword ptr [edi + 14h ] 20cfaac9 8b7704 mov esi,dword ptr [edi + 4 ] 20cfaacc 8b6f08 mov ebp,dword ptr [edi + 8 ] 20cfaacf 8b670c mov esp,dword ptr [edi + 0Ch ] 20cfaad2 8b3f mov edi,dword ptr [edi] 20cfaad4 64ff25ec0e0000 jmp dword ptr fs:[ 0EECh ] ;最后跳转到代码缓存中执行基本块 |
经过上下文切换,终于执行到了代码缓存中的基本块,真是一个复杂的过程!
执行完cmp指令之后执行je指令跳转到exit stub:
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 | 20d2101c 6764a3e40e mov dword ptr fs:[ 00000EE4h ],eax 20d21021 b82c52cc20 mov eax, 20CC522Ch 20d21026 e9d59afdff jmp 20cfab00 1 : 003 > u 20cfab00 l50 20cfab00 64893df00e0000 mov dword ptr fs:[ 0EF0h ],edi ;保存目标进程上下文 之后进行上下文切换回DynamoRIO 20cfab07 648b3df40e0000 mov edi,dword ptr fs:[ 0EF4h ] 20cfab0e 895f10 mov dword ptr [edi + 10h ],ebx 20cfab11 648b1de40e0000 mov ebx,dword ptr fs:[ 0EE4h ] 20cfab18 895f1c mov dword ptr [edi + 1Ch ],ebx 20cfab1b 648b1df00e0000 mov ebx,dword ptr fs:[ 0EF0h ] 20cfab22 891f mov dword ptr [edi],ebx 20cfab24 894f18 mov dword ptr [edi + 18h ],ecx 20cfab27 895714 mov dword ptr [edi + 14h ],edx 20cfab2a 897704 mov dword ptr [edi + 4 ],esi 20cfab2d 896f08 mov dword ptr [edi + 8 ],ebp 20cfab30 89670c mov dword ptr [edi + 0Ch ],esp 20cfab33 8ba7a4020000 mov esp,dword ptr [edi + 2A4h ] 20cfab39 9c pushfd 20cfab3a 5b pop ebx 20cfab3b 895f20 mov dword ptr [edi + 20h ],ebx 20cfab3e 6a00 push 0 20cfab40 9d popfd 20cfab41 c5fd7f4740 vmovdqa ymmword ptr [edi + 40h ],ymm0 20cfab46 c5fd7f8f80000000 vmovdqa ymmword ptr [edi + 80h ],ymm1 20cfab4e c5fd7f97c0000000 vmovdqa ymmword ptr [edi + 0C0h ],ymm2 20cfab56 c5fd7f9f00010000 vmovdqa ymmword ptr [edi + 100h ],ymm3 20cfab5e c5fd7fa740010000 vmovdqa ymmword ptr [edi + 140h ],ymm4 20cfab66 c5fd7faf80010000 vmovdqa ymmword ptr [edi + 180h ],ymm5 20cfab6e c5fd7fb7c0010000 vmovdqa ymmword ptr [edi + 1C0h ],ymm6 20cfab76 c5fd7fbf00020000 vmovdqa ymmword ptr [edi + 200h ],ymm7 20cfab7e 8987a0020000 mov dword ptr [edi + 2A0h ],eax 20cfab84 b8f4a0cb20 mov eax, 20CBA0F4h 20cfab89 64a330000000 mov dword ptr fs:[ 00000030h ],eax 20cfab8f 64a104000000 mov eax,dword ptr fs:[ 00000004h ] 20cfab95 8987d4020000 mov dword ptr [edi + 2D4h ],eax 20cfab9b 8b87a4020000 mov eax,dword ptr [edi + 2A4h ] 20cfaba1 64a304000000 mov dword ptr fs:[ 00000004h ],eax 20cfaba7 64a134000000 mov eax,dword ptr fs:[ 00000034h ] 20cfabad 8987ac020000 mov dword ptr [edi + 2ACh ],eax 20cfabb3 64a1b40f0000 mov eax,dword ptr fs:[ 00000FB4h ] 20cfabb9 8987b0020000 mov dword ptr [edi + 2B0h ],eax 20cfabbf 8b87b4020000 mov eax,dword ptr [edi + 2B4h ] 20cfabc5 64a3b40f0000 mov dword ptr fs:[ 00000FB4h ],eax 20cfabcb 64a11c0f0000 mov eax,dword ptr fs:[ 00000F1Ch ] 20cfabd1 8987b8020000 mov dword ptr [edi + 2B8h ],eax 20cfabd7 8b87bc020000 mov eax,dword ptr [edi + 2BCh ] 20cfabdd 64a31c0f0000 mov dword ptr fs:[ 00000F1Ch ],eax 20cfabe3 64a1a00f0000 mov eax,dword ptr fs:[ 00000FA0h ] 20cfabe9 8987c0020000 mov dword ptr [edi + 2C0h ],eax 20cfabef 8b87c4020000 mov eax,dword ptr [edi + 2C4h ] 20cfabf5 64a3a00f0000 mov dword ptr fs:[ 00000FA0h ],eax 20cfabfb 57 push edi ;dcontext 20cfabfc e8bfef014f call dynamorio!d_r_dispatch ( 6fd19bc0 ) 20cfac01 8d642404 lea esp,[esp + 4 ] 20cfac05 e9a68a0b4f jmp dynamorio!unexpected_return ( 6fdb36b0 ) |
可以看到将目标进程上下文保存之后再切换回DynamoRIO的上下文,之后再调用d_r_dispatch。但此时有一个问题,DynamoRIO将怎么知道我们到底执行了哪个分支,我们将要复制的下一个基本块的地址又是什么?
d_r_dispatch的dispatch_enter_dynamorio将处理以上问题
dispatch_enter_dynamorio
1 2 3 4 5 6 7 8 9 10 11 12 | static void dispatch_enter_dynamorio(dcontext_t * dcontext) { / * dcontext - >last_fragment赋值为上个fragment_t * / dcontext - >last_fragment = linkstub_fragment(dcontext, dcontext - >last_exit); / * 根据eflags选择到底选择哪个linkstub_t 之后赋值给dcontext - >last_exit * / dcontext - >last_exit = linkstub_cbr_disambiguate( dcontext, dcontext - >last_fragment, dcontext - >last_exit, nxt); / * dcontext - >next_tag赋值为下一个将要复制的基本块 * / dispatch_exit_fcache_stats(dcontext); } |
首先会将dcontext->last_fragment赋值为上个fragment_t
根据eflags选择到底选择哪个linkstub_t 之后赋值给dcontext->last_exit,我们是运行的是je指令因此last_exit 应该为0x20cc522c:
同时dcontext->next_tag也执行了目标进程中下一个基本块。到此我们跟踪了一个基本块从创建到执行的过程。总算对DynamoRIO有了更深入的了解。但是又有一个问题,我们不可能每次复制一个基本块后上下文切换执行基本块再次上下文切换回DynamoRIO再复制。这样会造成大量的资源浪费在切换上下文。如果是一个直接分支指令将两个基本块相连,我们可不可以在代码缓存中将这两个基本块链接起来。
链接
为了更好理解这个过程我们将程序运行到test.exe的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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 1 : 003 > u test!main l50 Test!main [D:\c + + pro\Vulnerability\Test\main.cpp @ 6 ]: 004b1600 55 push ebp 004b1601 8bec mov ebp,esp 004b1603 83ec4c sub esp, 4Ch 004b1606 53 push ebx 004b1607 56 push esi 004b1608 57 push edi 004b1609 c745fc00000000 mov dword ptr [ebp - 4 ], 0 ;count = 0 004b1610 c745f800000000 mov dword ptr [ebp - 8 ], 0 ;j = 0 004b1617 eb09 jmp Test!main + 0x22 ( 004b1622 ) 004b1619 8b45f8 mov eax,dword ptr [ebp - 8 ] 004b161c 83c001 add eax, 1 004b161f 8945f8 mov dword ptr [ebp - 8 ],eax ;j + + 004b1622 837df802 cmp dword ptr [ebp - 8 ], 2 004b1626 7d36 jge Test!main + 0x5e ( 004b165e ) ;j< 2 004b1628 c745f400000000 mov dword ptr [ebp - 0Ch ], 0 004b162f eb09 jmp Test!main + 0x3a ( 004b163a ) ;i = 0 004b1631 8b45f4 mov eax,dword ptr [ebp - 0Ch ] 004b1634 83c001 add eax, 1 004b1637 8945f4 mov dword ptr [ebp - 0Ch ],eax ;i + + 004b163a 837df43c cmp dword ptr [ebp - 0Ch ], 3Ch 004b163e 7d1c jge Test!main + 0x5c ( 004b165c ) ;i< 60 004b1640 837df41e cmp dword ptr [ebp - 0Ch ], 1Eh 004b1644 7d0b jge Test!main + 0x51 ( 004b1651 ) ; if (i> = 30 ) 004b1646 8b45fc mov eax,dword ptr [ebp - 4 ] 004b1649 83c001 add eax, 1 004b164c 8945fc mov dword ptr [ebp - 4 ],eax 004b164f eb09 jmp Test!main + 0x5a ( 004b165a ) ;count + + 004b1651 8b45fc mov eax,dword ptr [ebp - 4 ] 004b1654 83e801 sub eax, 1 004b1657 8945fc mov dword ptr [ebp - 4 ],eax ;count - - 004b165a ebd5 jmp Test!main + 0x31 ( 004b1631 ) 004b165c ebbb jmp Test!main + 0x19 ( 004b1619 ) 004b165e 8b45fc mov eax,dword ptr [ebp - 4 ] 004b1661 50 push eax 004b1662 6850be5400 push offset Test!`string' ( 0054be50 ) 004b1667 e8c1a3ffff call Test!ILT + 2600 (_printf) ( 004aba2d ) 004b166c 83c408 add esp, 8 004b166f 33c0 xor eax,eax 004b1671 5f pop edi 004b1672 5e pop esi 004b1673 5b pop ebx 004b1674 8be5 mov esp,ebp 004b1676 5d pop ebp 004b1677 c3 ret |
也就是让dcontext->next_tag为004b1600:
执行一次build_basic_block_fragment之后将地址0x004b1600到0x004b1617的基本块复制到代码缓存中,再次执行build_basic_block_fragment,其中emit_fragment_common的link_new_fragment将执行链接操作,他会将两个存在于代码缓存中,且由直接分支定位的基本块,将这两个基本块链接起来(跟踪头除外):
昂贵的上下文切换被简单的跳转代替。
此外link_new_fragment还会进行标记跟踪头的操作。
跟踪
什么是跟踪,这里我们使用官方原话:
为了提高间接分支的效率,并实现更好的代码布局,经常按顺序执行的基本块被缝合到一个称为跟踪的单元中。卓越的代码布局和跟踪中的块间分支消除提供了显著的性能提升,跟踪的最大好处之一是通过将间接分支的流行目标内联到跟踪中来避免间接分支查找。
简单来说就是将循环执行的代码(比如for,while)看成一个整体复制到代码缓存中,同时内联间接分支。
跟踪的实现过程如下:
DynamoRIO的跟踪基于 Next Executing Tail (NET)方案,NET通过将计数器与每个跟踪头关联来进行操作。跟踪头要么是向后分支(目标循环)的目标,要么是现有跟踪的出口(称为辅助跟踪头)。计数器在每次执行跟踪头时递增。一旦计数器超过一个阈值(通常是一个很小的数字,比如50),就会进入跟踪创建模式。这意味着在之后执行的下一个基本块序列将连接在一起成为一个新的跟踪。当跟踪到达向后分支或另一个跟踪或跟踪头时,跟踪将终止。DynamoRIO修改了NET,使其不将向后间接分支目标视为跟踪头。这样做的好处是将更多的间接分支内联到跟踪中。
简单来说就是如果一个循环执行了50次,第51次执行的时候将他创建成跟踪,同时内联间接分支。
现在我们大致知道了什么是跟踪。回看我们的程序,我们猜测由i引导的for循环应该被创建成trace。让我们验证此过程:
地址0x004b165a处的jmp语句会跳转到0x004b1631,发现其为后向分支的目标,于是由link_new_fragment将0x004b1631处的基本块标记为跟踪头。之后一直进行循环,有一点要注意有没有发现循环执行的代码已经存在于代码缓存中了,在这种情况下fragment_lookup_fine_and_coarse会查找代码缓存,一旦发现其已经存在于代码缓存中,就将targetf赋值,这样就不需要再次调用build_basic_block_fragmen创建基本块了。
最后让我们查看monitor_cache_enter的实现过程
monitor_cache_enter
此函数比较复杂,同样我们将之简化:
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 | fragment_t * monitor_cache_enter(dcontext_t * dcontext, fragment_t * f) { monitor_data_t * md = (monitor_data_t * )dcontext - >monitor_field; if (md - >trace_tag ! = NULL) { / * 这里进行创建跟踪 * / } / * 如果不是跟踪头 * / if (!TEST(FRAG_IS_TRACE_HEAD, f - >flags)) { / * 这里什么也不做 * / } / * 找到跟踪头 * / ctr = thcounter_lookup(dcontext, f - >tag); / * 计数器 + 1 * / ctr - >counter + + ; / * 如果大于等于阈值 此值为 50 * / if (ctr - >counter > = INTERNAL_OPTION(trace_threshold)){ f - >flags | = FRAG_TRACE_BUILDING; start_trace = true; } if (start_trace && (TEST(FRAG_COARSE_GRAIN, f - >flags) || TEST(FRAG_SHARED, f - >flags) || md - >pass_to_client)){ / * 重新为f创建基本块 值给 md - >last_fragment 和 md - >last_copy * / create_private_copy(dcontext, f); / * operate on new f from here on * / f = md - >last_fragment; } if (start_trace){ md - >trace_tag = f - >tag; md - >trace_flags = trace_flags_from_trace_head_flags(f - >flags); md - >emitted_size = fragment_prefix_size(md - >trace_flags); } } |
创建跟踪后如下:
可以看到进行了优化,将循环创建成为一个trace。
结语
终于我们完成了对d_r_dispatch这个核心控制函数的解读,但其实还有很多细节我们没有讲解,比如间接分支,他是如何通过哈希查找的,trace是如何内联间接分支的,感兴趣的可以自行去研究。
现在回顾整个过程,真的能感受到DynamoRIO作者为了能监控控制整个程序所进行的疯狂操作。这种大胆且细致的行为,真是让人着迷!
下一章我们将研究到底什么是覆盖率信息,如果我们创建了一个新线程,DynamoRIO应该怎么拿到此线程的控制权。
个人能力有限,最后如果有什么分析错误的地方,请一定指点斧正。
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。