首页
社区
课程
招聘
[原创]DynamoRIO源码分析(二)--基本块(Basic Blocks)和跟踪 (trace)
2023-4-17 17:59 16015

[原创]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漏洞挖掘与利用;代码审计。

最后于 2023-4-17 18:07 被DriverUnload编辑 ,原因:
收藏
点赞6
打赏
分享
最新回复 (1)
雪    币: 12744
活跃值: (16282)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
有毒 10 2023-4-18 09:37
2
0
期待师傅下一篇!
游客
登录 | 注册 方可回帖
返回