2021-06-19

控制流平坦+VMP定制使用与还原实战分析

作者:好中文的样子 所属分类 - 安全 - 黑科技

在Windows、iOS到现在的Android平台上,使用一些手段保护自己的源代码,防止被别有用心的人逆向分析是非常重要的。防止自己的算法被第三者分析利用,可以有效防止TCP双端互非对称加密通信中间过程的内容泄露。我们今天就以一个简单的源代码为基础,实现控制流平坦化以及VMP,并且尝试将其还原。

控制流平坦化

控制流平坦化的定制与使用

我们首先自己实现控制流平坦化,目前适合学习与练习的编译器有CLANG、TCC、GCC、MSVC等等,考虑到便利性与可定制修改性,我们使用TCC进行练习。

Tiny C Compiler没有所谓的IR,也是特别原始简单的编译器,我们学习起来比较轻松,只需要掌握微机原理以及龙书即可。我们重点关注的是tccgen以及x86_64-gen的部分,这里涉及了机器码生成以及块处理部分。

tccgen里面有个很重要的概念,叫做block,这个跟gen_function以及我们的控制流平坦密切相关,控制流平坦化需要关注的重点部分如下:

static void gen_function(Sym *sym)
{
    nocode_wanted = 0;
    ind = cur_text_section->data_offset;
    /* NOTE: we patch the symbol size later */
    put_extern_sym(sym, cur_text_section, ind, 0);
    funcname = get_tok_str(sym->v, NULL);
    func_ind = ind;
    /* Initialize VLA state */
    vla_sp_loc = -1;
    vla_sp_root_loc = -1;
    /* put debug symbol */
    tcc_debug_funcstart(tcc_state, sym);
    /* push a dummy symbol to enable local sym storage */
    sym_push2(&local_stack, SYM_FIELD, 0, 0);
    local_scope = 1; /* for function parameters */
    gfunc_prolog(&sym->type);
    local_scope = 0;
    rsym = 0;
    block(NULL, NULL, 0);
    nocode_wanted = 0;
    gsym(rsym);
    gfunc_epilog();
    cur_text_section->data_offset = ind;
    label_pop(&global_label_stack, NULL, 0);
    /* reset local stack */
    local_scope = 0;
    sym_pop(&local_stack, NULL, 0);
    /* end of function */
    /* patch symbol size */
    elfsym(sym)->st_size = ind - func_ind;
    tcc_debug_funcend(tcc_state, ind - func_ind);
    /* It's better to crash than to generate wrong code */
    cur_text_section = NULL;
    funcname = ""; /* for safety */
    func_vt.t = VT_VOID; /* for safety */
    func_var = 0; /* for safety */
    ind = 0; /* for safety */
    nocode_wanted = 0x80000000;
    check_vstack();
}

最重点的是

static void block(int *bsym, int *csym, int is_expr)

控制流平坦化,大概就是在这里插入dispatcher,也就是中文里面的分发块,然后迭代生成block,注意这里跟具有IR的LLVM不同,我们跟GCC类似,跳过IR而是生成中间函数。具体的源码有点多,我们无需贴出全部源码,思路如下:

创建一个新的函数,名为"原函数_dispatcher",将原函数的块,在gen以后获取的机器码,例如jmp、je、jne等等各种指令作为一个新的函数,名为"原函数_blockxxx"等等,更改gen_function函数,里面的block改为我们的blockEx,并且在我们的block里面创建跳转代码以及新的block块。机器码我们无需关心,我们只需要知道token是什么,因为机器码是x86_64-gen完成的,而token可以使用gen来生成汇编码,这里不会生成IR,方便简单看见我们的效果。

TCC典型控制流平坦化

我们编写一段根据args以及argv的内容输出hello或者hi xxx的C语言代码,并且通过在一个函数创建大量block来实现控制流平坦化。我们创建的block需要额外创建switch块来执行,也就是dispatcher里面编写的switch函数。并且我们的blockEx还可以继续通过迭代,创建多个父dispatcher来继续dispatch我们的每一个子块,效果如图。

总结就是通过分解原来函数的部分,作为一个一个的block,通过dispatcher来执行,并且将retn等返回块作为end block,也就是典型的将start -> code block -> return转化为start -> dispatcher -> real code block -> dispatcher -> return block。其中start里面需要插入为我们dispatcher准备的预留栈空间。

控制流平坦化的还原

通过x64dbg这个强力工具软件以及内置的雪人反编译引擎,我们可以很容易找到dispatcher以及real block。x64dbg可以编写插件,直接从内存抓取相关代码段,使用我们的插件还原。插件思路大致与deflat相似,但是deflat基于angr框架,我们这里使用的是x64dbg的接口作为框架。

x64dbg可以使用插件还原回去,如下:

x64dbg插件还原控制流平坦化

有人会使用一些工具将目标机器码转化回去IR,再利用LLVM的后端优化转换为正常代码,这种也是可行的,但是有些控制流扁平化,特别是使用了C++ STL、函数模板、匿名函数、C++类的控制流平坦化,单纯使用后端优化难以还原。

由于能力有限,对IR以及编译后端理解有限,目前还是机器码块查找、跳转指令分析,去除无用块,找到真实块,将分发器删除并重新构建函数。我们所做的编译器,目前没有插入虚假块,因此去除无用块可以跳过,只需要找到每一个真实块,根据跳转指令拼接,删除跳转回dispatcher的内容,将其与下一个应该执行的block联系起来即可。

由于我们定制的控制流平坦化比较简单,少了一些基础的算法更改,例如加减乘除一类的算法更改,因此算法更改优化这部分就可以省略了。

控制流平坦化后加虚拟机壳

此VMP与大家熟知的VMP就是一个意思,注意虚拟机壳与控制流平坦化是两个概念,一个是对机器码进行再编译,另一个是对代码进行逻辑更改混淆。

VMP生成的部分包括gen vm (包括VM虚拟指令的生成以及VM Handler部分的生成) -> take origin opcode -> convert to vm byte code -> emplace to new function area -> take next origin opcode -> ......

然后 function jump -> vm jump -> gen vm jump native byte code。

这个是生成的过程,运行的过程也跟控制流平坦化有一些相似,大致如下:

entry point -> get vm handler -> analyze each vm byte code -> excute -> analyze next vm byte code -> excute,类似我们熟知的解释器,但是VM里面有虚拟寄存器、虚拟栈、虚拟堆,会映射到物理机的内存上。我们可以使用的开源项目有QEMU,利用LLVM生成类似MIPS的机器码,再从QEMU源码扒出来解析执行的部分,进行解释执行。

VMProtect X64

经过整体vmp,原来的程序已经膨胀了不少,并且函数符号一类的都被隐藏了。通过控制流平坦化+VMP,我们的程序几乎难以更改,因为VMP最重要的功能是防止破解者更改膨胀后的vm byte code,因为这样会造成堆栈失衡,并且因为我们原来的机器码被转化成了虚拟机的机器码,所以直接更改转化后的机器码,会造成堆栈失衡而无法继续执行。

还原VMP(未实战)

破解VMP程序,要么分析出来虚拟指令对应的机器码,再更改这部分机器码,例如vmpop、vmadd、vmmov、vmjmp等等的虚拟指令对应的真实硬编码,要么整体还原VMP,更改真实机器码。

目前常用的仍然是还原回去编译器的中间表达式(IR),再通过编译后端优化压缩还原。人工分析整个VM是非常困难的事情,因为函数以及对应的汇编码都被转化成VM的字节码了,必须分析出来每一块对应是虚拟寄存器的操作、中间分发器、虚拟堆栈还是什么别的东西。

已有的还原包括Win32 VMProtect的基于编译器优化还原,这种相比熟知的控制流扁平化还原更困难,成本更高,但是经过VMP的程序一般运行都十分缓慢。

思考与完善

在我们定制的Tiny C Compiler with fla当中,生成多个block并且创建链接,可能跟现有的函数冲突,因此我们是不是应该采用别的方法而不是创建一个symbol来实现块跳转呢?

VMP之前的程序是否需要进行控制流扁平化处理?处理是否对逆向有效?是否会降低过多执行效率?

麦科技原创,转载请说明出处。