Mergen是一个用于将汇编代码转换为LLVM中间表示(IR)的工具。 该工具旨在:
要构建并运行该项目,请查看docs/BUILDING.md。
重写工作应保持基准回归门控为绿色。门控会构建精选的PE样本,运行lifter,并验证提升后的IR输出。
scripts\\rewrite\\run.cmd我们对目标进行符号执行(或符号提升),这里的思路不是提升单个指令,而是提升整个函数。我们不期望每条指令或每个基本块每次都表现相同,而是将其视为每次可能且确实服务于不同目的。我们尽量使生成的IR保持简单并尽可能优化。我们还有与通常编译器不同的需求。我们利用分析来评估控制流。我们不能依赖LLVM完成所有分析,因为它们是为不同目标创建的,并且对我们的用例可能不是最优的。

以下是实际示例,说明Mergen如何解决虚拟化程序的问题。
这是我们的目标程序
struct test {
int a;
int b;
int c;
};
int maths(test a, int b, int c) {
return a.a + b - c;
}


VMProtect设置,所有选项均关闭,我们在超(ultra)设置下虚拟化该函数。(测试版本 3.4.0-3.6.0 3.8.1)


这里,我们运行mergen。第一个参数是文件名,第二个参数是函数的地址。看,运行起来多么简单。我们可以编译输出,以便使用我们最喜欢的反编译器进行探索。

; ModuleID = 'my_lifting_module'
source_filename = "my_lifting_module"
; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read)
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %0, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr nocapture readonly %memory) local_unnamed_addr #0 {
entry:
%stackmemory = alloca i128, i128 13758960, align 8
%1 = trunc i64 %r8 to i32
%2 = trunc i64 %rdx to i32
%GEPLoadxd-5369456437- = getelementptr i8, ptr %memory, i64 %rcx
%3 = load i32, ptr %GEPLoadxd-5369456437-, align 4
%adc-temp-5370242400- = sub i32 %2, %1
%realnot-5369532059- = add i32 %adc-temp-5370242400-, %3
%stackmemory10243.sroa.55.1375304.insert.ext10255 = zext i32 %realnot-5369532059- to i64
ret i64 %stackmemory10243.sroa.55.1375304.insert.ext10255
}
attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read) }
编译后:


现在你可能会注意到寄存器有点不对。这是因为我们没有遵循调用约定,如果我们遵循调用约定,函数签名应该是这样的:
define i64 @main(i64 %rcx, i64 %rdx, i64 %rdx, i64 %r8, i64 %r9 ...)
所以,我们只需调整函数签名使其看起来正常。如果你对这部分有更多疑问,建议你研究调用约定和ABI。
那么,假设我们有这段代码。虚拟机将把下面的代码转换为间接跳转,这对逆向工程师来说稍微有点不便。
int maths(int a, int b, int c) {
if (a > b)
return a + b + c;
else
return a - b - c;
}
next_handler = xxx;
if ( a-b > 0 )
next_handler = yyy;
jump next_handler;
我们总是试图分析值并跟踪它们。这使我们能够理解控制流。 对于类似跳转表的分支 优化后的输出是一个简单的
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr nocapture readnone %TEB, ptr nocapture readnone %memory) local_unnamed_addr #0 {
fake_ret:
%0 = lshr i64 %rcx, 62
%common.ret.op = and i64 %0, 2
ret i64 %common.ret.op
}
未优化的输出。(为可读性进行了DCE)
source_filename = "my_lifting_module"
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr %TEB, ptr %memory) {
%lsb = and i64 %rcx, 255
%pf1 = mul i64 %lsb, 72340172838076673
%pf2 = and i64 %pf1, -9205322385119247871
%pf3 = urem i64 %pf2, 511
%pf4 = and i64 %pf3, 1
%pf5 = icmp eq i64 0, %pf4
%0 = zext i1 %pf5 to i64
%createrflag2 = shl i64 %0, 2
%creatingrflag = or i64 2, %createrflag2
%zeroflag = icmp eq i64 %rcx, 0
%1 = zext i1 %zeroflag to i64
%createrflag21 = shl i64 %1, 6
%creatingrflag2 = or i64 %creatingrflag, %createrflag21
%signflag = icmp slt i64 %rcx, 0
%2 = zext i1 %signflag to i64
%createrflag23 = shl i64 %2, 7
%creatingrflag4 = or i64 %creatingrflag2, %createrflag23
%GEPSTORE-5368713221- = getelementptr i8, ptr %memory, i64 1376032
store i64 %creatingrflag4, ptr %GEPSTORE-5368713221-, align 4
%realand-5368713229- = and i64 %creatingrflag4, 128
%shr-lshr-5368713233- = lshr i64 %realand-5368713229-, 7
%3 = mul i64 %shr-lshr-5368713233-, 4
%bvalue_indexvalue = add i64 5368713249, %3
%4 = icmp eq i64 %bvalue_indexvalue, 5368713253
%lolb- = select i1 %4, i64 5368713264, i64 5368713257
%GEPSTORE-5368713248- = getelementptr i8, ptr %memory, i64 1376032
store i64 %lolb-, ptr %GEPSTORE-5368713248-, align 4
br i1 %4, label %real_ret, label %real_ret41
real_ret: ; preds = %fake_ret
%inc-5368713273- = add i64 %shr-lshr-5368713233-, 1
ret i64 %inc-5368713273-
real_ret41: ; preds = %fake_ret
ret i64 %shr-lshr-5368713233-
}
注意这部分
%realand-5368713229- = and i64 %creatingrflag4, 128
%shr-lshr-5368713233- = lshr i64 %realand-5368713229-, 7
我们获取标志位,然后获取第7位,即符号标志(Sign Flag),然后使用符号标志来计算地址。通过分析,我们确定地址可能是两个值之一:5368713257或5368713264,然后将其转换为比较。如果地址是5368713257,走一个分支,否则走另一个。这样做时,用适当的值标记条件也很重要,因为稍后我们可能需要使用相同的值来计算另一个跳转。
尽管我们解决了间接跳转问题,但具有多于2个可能位置的跳转不受支持。这是因为针对它们的分析尚未实现。这使我们能够解决虚拟机风格的分支,但在处理现实中的跳转表时会遇到问题。
我们的目标程序:

Themida设置(我们目前只关心虚拟机):



虚拟化后:

运行Mergen:

输出代码:点击这里 那么,为什么我们的结果不如提升受VMP保护的程序那么成功呢?
Themida会主动写入.themida节区。与栈不同,我们不能忽略这些写入,因为这些值之后可能被其他东西读取。
但是,我们有一个临时解决方案。删除所有对.themida节区的存储操作。由于我们的程序不写入内存,我只是注释掉了所有的存储操作。现在我们剩下的是:
source_filename = "my_lifting_module"
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr writeonly %memory) local_unnamed_addr #0 {
%trunc = trunc i64 %r8 to i32
%trunc1 = trunc i64 %rdx to i32
%trunc2 = trunc i64 %rcx to i32
%realadd-5369771371- = add i32 %trunc1, %trunc2
%realadd-5369582686- = add i32 %realadd-5369771371-, %trunc
%trunc457139 = zext i32 %realadd-5369582686- to i64
ret i64 %trunc457139
}
attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: write) }
加入我们的Mergen Discord服务器交流想法或只是随便聊聊。