LLVM IR指令VM混淆分析
未混淆编译
编写一个最简单的测试代码,对 test_add函数用于对两个数相加:
int __attribute((__annotate__("vm"))) test_add(int a, int b)
{
int c = a + b;
return c;
}
int main(void) {
int c = test_add(1, 2);
return c;
}
编译成中间代码:
未加入混淆时编译的中间代码如下:
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @test_add(i32 %0, i32 %1) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i32, align 4
store i32 %0, i32* %3, align 4
store i32 %1, i32* %4, align 4
%6 = load i32, i32* %3, align 4
%7 = load i32, i32* %4, align 4
%8 = add nsw i32 %6, %7
store i32 %8, i32* %5, align 4
%9 = load i32, i32* %5, align 4
ret i32 %9
}
首先分配 3 个局部变量,参数 1 保存在 %3所在地址,参数 2 保存在 %4所在地址,接着 %3 地址取值到 %6,%4 地址取值到 %7,然后 %6 和 %7 相加保存到%8,%8 保存到 地址%5,再从 %5 地址取值到 %9,返回 %9。表达式简化后如下:
*%3 = %0
*%4 = %1
%6 = *%3
%7 = *%4
%8 = %6 + %7
*%5 = %8
%9 = *%5
return %9
实际上 %9 = %0 + %1
虚拟化混淆编译
接着加载虚拟化混淆 PASS生成处理后的 IR中间代码:
@opcodes = private global [84 x i8] c"\05\00\00\00\00\05\0C\00\00\00\01\05\04\00\00\00\05\18\00\00\00\01\05\0C\00\00\00\02,\00\00\00\05\18\00\00\00\020\00\00\00\05,\00\00\00\050\00\00\00\034\00\00\00\054\00\00\00\05$\00\00\00\01\05$\00\00\00\02,\00\00\00\05,\00\00\00\04"
define i32 @test_add(i32 %0, i32 %1) {
entry:
%2 = alloca i32
%3 = alloca [56 x i8]
%4 = alloca [256 x i32]
%5 = alloca i32
%6 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 8
%7 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 12
%8 = bitcast i8* %7 to i8**
store i8* %6, i8** %8
%9 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 20
%10 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 24
%11 = bitcast i8* %10 to i8**
store i8* %9, i8** %11
%12 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 32
%13 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 36
%14 = bitcast i8* %13 to i8**
store i8* %12, i8** %14
%15 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 0
%16 = bitcast i8* %15 to i32*
store i32 %0, i32* %16
%17 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 4
%18 = bitcast i8* %17 to i32*
store i32 %1, i32* %18
store i32 0, i32* %2
store i32 0, i32* %5
br label %dispatch
dispatch: ; preds = %loopend, %entry
%19 = load i32, i32* %2
%20 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %19
%21 = load i8, i8* %20
%22 = load i32, i32* %2
%23 = add i32 %22, 1
store i32 %23, i32* %2
switch i8 %21, label %loopend [
i8 1, label %handler_store
i8 2, label %handler_load
i8 3, label %handler_add
i8 4, label %handler_ret
i8 5, label %push_addr
i8 6, label %store_imm1
i8 7, label %store_imm2
i8 8, label %store_imm4
i8 9, label %store_imm8
]
handler_store: ; preds = %dispatch
%24 = load i32, i32* %2
%25 = load i32, i32* %5
%26 = sub i32 %25, 2
%27 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %26
%28 = load i32, i32* %27
%29 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %28
%30 = bitcast i8* %29 to i32*
%31 = load i32, i32* %30
%32 = sub i32 %25, 1
%33 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %32
%34 = load i32, i32* %33
%35 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %34
%36 = bitcast i8* %35 to i32**
%37 = load i32*, i32** %36
store i32 %31, i32* %37, align 4
%38 = sub i32 %25, 2
store i32 %38, i32* %5
br label %loopend
handler_load: ; preds = %dispatch
%39 = load i32, i32* %2
%40 = load i32, i32* %5
%41 = sub i32 %40, 1
%42 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %41
%43 = load i32, i32* %42
%44 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %43
%45 = bitcast i8* %44 to i32**
%46 = load i32*, i32** %45
%47 = load i32, i32* %46, align 4
%48 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %39
%49 = bitcast i8* %48 to i32*
%50 = load i32, i32* %49
%51 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %50
%52 = bitcast i8* %51 to i32*
store i32 %47, i32* %52
%53 = add i32 %39, 4
store i32 %53, i32* %2
%54 = sub i32 %40, 1
store i32 %54, i32* %5
br label %loopend
handler_add: ; preds = %dispatch
%55 = load i32, i32* %2
%56 = load i32, i32* %5
%57 = sub i32 %56, 2
%58 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %57
%59 = load i32, i32* %58
%60 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %59
%61 = bitcast i8* %60 to i32*
%62 = load i32, i32* %61
%63 = sub i32 %56, 1
%64 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %63
%65 = load i32, i32* %64
%66 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %65
%67 = bitcast i8* %66 to i32*
%68 = load i32, i32* %67
%69 = add nsw i32 %62, %68
%70 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %55
%71 = bitcast i8* %70 to i32*
%72 = load i32, i32* %71
%73 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %72
%74 = bitcast i8* %73 to i32*
store i32 %69, i32* %74
%75 = add i32 %55, 4
store i32 %75, i32* %2
%76 = sub i32 %56, 2
store i32 %76, i32* %5
br label %loopend
handler_ret: ; preds = %dispatch
%77 = load i32, i32* %2
%78 = load i32, i32* %5
%79 = sub i32 %78, 1
%80 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %79
%81 = load i32, i32* %80
%82 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %81
%83 = bitcast i8* %82 to i32*
%84 = load i32, i32* %83
ret i32 %84
push_addr: ; preds = %dispatch
%85 = load i32, i32* %2
%86 = load i32, i32* %5
%87 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %85
%88 = bitcast i8* %87 to i32*
%89 = load i32, i32* %88
%90 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %86
store i32 %89, i32* %90
%91 = add i32 %86, 1
store i32 %91, i32* %5
%92 = add i32 %85, 4
store i32 %92, i32* %2
br label %loopend
store_imm1: ; preds = %dispatch
%93 = load i32, i32* %2
%94 = load i32, i32* %5
%95 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %93
%96 = load i8, i8* %95
%97 = sub i32 %94, 1
%98 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %97
%99 = load i32, i32* %98
%100 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %99
store i8 %96, i8* %100
%101 = sub i32 %94, 1
store i32 %101, i32* %5
%102 = add i32 %93, 1
store i32 %102, i32* %2
br label %loopend
store_imm2: ; preds = %dispatch
%103 = load i32, i32* %2
%104 = load i32, i32* %5
%105 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %103
%106 = bitcast i8* %105 to i16*
%107 = load i16, i16* %106
%108 = sub i32 %104, 1
%109 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %108
%110 = load i32, i32* %109
%111 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %110
%112 = bitcast i8* %111 to i16*
store i16 %107, i16* %112
%113 = sub i32 %104, 1
store i32 %113, i32* %5
%114 = add i32 %103, 2
store i32 %114, i32* %2
br label %loopend
store_imm4: ; preds = %dispatch
%115 = load i32, i32* %2
%116 = load i32, i32* %5
%117 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %115
%118 = bitcast i8* %117 to i32*
%119 = load i32, i32* %118
%120 = sub i32 %116, 1
%121 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %120
%122 = load i32, i32* %121
%123 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %122
%124 = bitcast i8* %123 to i32*
store i32 %119, i32* %124
%125 = sub i32 %116, 1
store i32 %125, i32* %5
%126 = add i32 %115, 4
store i32 %126, i32* %2
br label %loopend
store_imm8: ; preds = %dispatch
%127 = load i32, i32* %2
%128 = load i32, i32* %5
%129 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %127
%130 = bitcast i8* %129 to i64*
%131 = load i64, i64* %130
%132 = sub i32 %128, 1
%133 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %132
%134 = load i32, i32* %133
%135 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %134
%136 = bitcast i8* %135 to i64*
store i64 %131, i64* %136
%137 = sub i32 %128, 1
store i32 %137, i32* %5
%138 = add i32 %127, 8
store i32 %138, i32* %2
br label %loopend
loopend: ; preds = %store_imm8, %dispatch, %store_imm4, %store_imm2, %store_imm1, %push_addr, %handler_add, %handler_load, %handler_store
br label %dispatch
}
就一个加法运算,搞成这样至于吗😩
虚拟化混淆 IR层面分析
接下来分析字节码是如何一步一步运算的:
首先分析函数的入口代码块:
entry:
%2 = alloca i32 分配一个变量表示当前执行的字节码下标
%3 = alloca [56 x i8] 分配虚拟内存
%4 = alloca [256 x i32] 分配虚拟栈
%5 = alloca i32 分配一个变量表示虚拟栈下标
%6 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 8
%7 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 12
%8 = bitcast i8* %7 to i8**
store i8* %6, i8** %8
%9 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 20
%10 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 24
%11 = bitcast i8* %10 to i8**
store i8* %9, i8** %11
%12 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 32
%13 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 36
%14 = bitcast i8* %13 to i8**
store i8* %12, i8** %14
%15 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 0 取虚拟内存首地址
%16 = bitcast i8* %15 to i32* 地址从 int8* 类型转换成 int32类型
store i32 %0, i32* %16 参数 1 保存到虚拟内存首地址
%17 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 4
%18 = bitcast i8* %17 to i32*
store i32 %1, i32* %18 参数 2 保存到虚拟内存第二个 4 字节处
store i32 0, i32* %2 初始化当前字节码下标为 0
store i32 0, i32* %5 初始化当前虚拟栈下标为 0
br label %dispatch 跳转到 dispatch 执行
看注解,entry 块主要做了一些初始化的操作,分配虚拟内存空间和虚拟栈空间,保存参数到虚拟内存中,初始化字节码当前下标 和 虚拟栈下标为 0
接着看 dispatch 块是怎么处理的:
dispatch: ; preds = %loopend, %entry
%19 = load i32, i32* %2 取字节码下标值
%20 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %19 取字节码地址
%21 = load i8, i8* %20 加载一个字节码
%22 = load i32, i32* %2 取字节码下标值
%23 = add i32 %22, 1 字节码下标值加 1
store i32 %23, i32* %2 保存字节码下标值
switch i8 %21, label %loopend [ 根据字节码值进行跳转
i8 1, label %handler_store
i8 2, label %handler_load
i8 3, label %handler_add
i8 4, label %handler_ret
i8 5, label %push_addr
i8 6, label %store_imm1
i8 7, label %store_imm2
i8 8, label %store_imm4
i8 9, label %store_imm8
]
dispatch 是调度器,根据opcode 执行对应的 handler 解释器,dispatch 执行取字节码,并把当前字节码指针+1,根据字节码跳转到对应的解释器执行。
接下来从字节码序列分析执行过程,字节码内容如下:
@opcodes = private global [84 x i8] c"\05\00\00\00\00\05\0C\00\00\00\01\05\04\00\00\00\05\18\00\00\00\01\05\0C\00\00\00\02,\00\00\00\05\18\00\00\00\020\00\00\00\05,\00\00\00\050\00\00\00\034\00\00\00\054\00\00\00\05$\00\00\00\01\05$\00\00\00\02,\00\00\00\05,\00\00\00\04"
字节码 opcodes是长 84 的 i8类型数组,第一个字节码是 05,根据上面的跳转是到 push_addr继续执行:
push_addr: ; preds = %dispatch
%85 = load i32, i32* %2 取当前字节码下标
%86 = load i32, i32* %5 取当前虚拟栈下标
%87 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %85 取当前字节码地址
%88 = bitcast i8* %87 to i32* 当前字节码地址转成 int32*
%89 = load i32, i32* %88. 从当前字节码地址取 4 个字节
%90 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %86 取当前栈顶地址
store i32 %89, i32* %90 将当前字节码取出的 4 个字节保存到栈顶
%91 = add i32 %86, 1 虚拟栈下标+1
store i32 %91, i32* %5 更新虚拟栈下标
%92 = add i32 %85, 4 字节码下标+4
store i32 %92, i32* %2 更新当前字节码下标
br label %loopend 跳转到 loopend
看注解,这个基本块的主要作用是从当前字节码处取一个 int32值,保存到虚拟栈顶,并把虚拟栈顶+1,把当前字节码下标+4,对应的虚拟指令就是 vpush 0 . 即 字节码 0x5执行的操作是将取出字节码后面的 4 字节转为 int32,这是一个指向虚拟内存的地址,将该地址push到虚拟栈中。
那么字节码 \05\00\00\00\00\05\0C\00\00\00 对应的操作就是 vpush 0 和 vpush 12
接下来取字节码是 0x1,对应的处理 handler 是 handler_store:
handler_store: ; preds = %dispatch
%24 = load i32, i32* %2 取当前字节码地址下标
%25 = load i32, i32* %5 取虚拟栈当前下标
%26 = sub i32 %25, 2 虚拟栈下标 - 2
%27 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %26 取出栈地址
%28 = load i32, i32* %27 从栈地址取出一个int32值
%29 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %28 值对应的虚拟内存地址
%30 = bitcast i8* %29 to i32* 虚拟内存地址从 int8* 转 int32*
%31 = load i32, i32* %30. 从虚拟地址取出这个 int32值
%32 = sub i32 %25, 1 虚拟栈下标 - 1 ,后面重复前面的步骤
%33 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %32 虚拟栈对应的地址
%34 = load i32, i32* %33 从虚拟栈对应的地址取值,这个值是虚拟内存的下标
%35 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %34 取虚拟内存地址
%36 = bitcast i8* %35 to i32** 虚拟内存地址从 int8* 转成 int32**
%37 = load i32*, i32** %36. 从虚拟内存处取出的值是一个 int32*地址
store i32 %31, i32* %37, align 4 将前面第一次取出的值存放在这个地址处
%38 = sub i32 %25, 2 当前虚拟栈顶指针 - 2
store i32 %38, i32* %5 更新当前栈顶指针
br label %loopend
看注解,这个虚拟指令 vstore 的逻辑是从当前栈顶取两个元素,这两个元素都是指向虚拟内存的地址,实际上是虚拟内存的下标,第一个元素指向虚拟内存中的一个 int32值,第二个元素指向虚拟内存中的一个 int32*地址值,然后把第一个元素取出的 int32值,保存到第二个元素取出的int32*地址处,然后把栈顶下标移动2 个。即 vstore 将虚拟内存 0 处的值,保存到虚拟内存 12 处值指向的地址处。
接下来又是两个 vpush和一个 vstore,分别是 vpush 4, vpush 24 和 vstore。
函数的 entry 处是做一些初始化的操作,就包括把参数 1 存储在虚拟地址下标 0 处,把参数 2 存储在虚拟地址下标 4 处。这几步的虚拟指令:
vpush 0
vpush 12
vStore
vpush 4
vpush 24
vStore
可以理解成就是把参数 1 和参数 2 分别存放到了虚拟内存 12 和 24 处的值指向的地址处。 根据函数 entry 处的分析,虚拟内存下标 12 和 24 处分别存放的是下标 8 和 20 处的地址。即最终把参数 1 和 参数 2 存放到了虚拟内存下标 8 和 20 处。
接下来是一个虚拟vpush指令,具体操作是 vpush 12.
然后后面是字节码 0x2,对应的 handler 是 handler_load:
handler_load: ; preds = %dispatch
%39 = load i32, i32* %2 取当前字节码下标
%40 = load i32, i32* %5 取当前虚拟栈下标
%41 = sub i32 %40, 1 当前虚拟栈下标 - 1
%42 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %41 对应栈中的地址
%43 = load i32, i32* %42 从栈中取出这个值,也上个虚拟指令vpush的操作数
%44 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %43 这个值内存虚拟内存下标处
%45 = bitcast i8* %44 to i32** 将地址从 i8* 转成 i32**
%46 = load i32*, i32** %45 从这个虚拟内存取出的值是一个地址
%47 = load i32, i32* %46, align 4 从这个地址处取值
%48 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %39 当前字节码地址
%49 = bitcast i8* %48 to i32* 转 i32*
%50 = load i32, i32* %49. 从当前字节码处取4 字节
%51 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %50 对应虚拟内存地址
%52 = bitcast i8* %51 to i32* 转成 i32*
store i32 %47, i32* %52. 将上面取的值,存放在这个虚拟地址处
%53 = add i32 %39, 4 当前字节码下标 - 4
store i32 %53, i32* %2 更新当前字节码下标
%54 = sub i32 %40, 1 当前虚拟栈顶 - 1
store i32 %54, i32* %5 并更新当前栈顶
br label %loopend
看注解,vload 指令有一,4 字节操作数,这里具体是 vload 44, 操作逻辑是将栈顶元素对应的虚拟内存下标处,取出一个值,这个值也是一个地址,然后用这个地址在虚拟内存处取值,将这个值保存在虚拟指令操作数对应的虚拟地址下标处。上一条指令是 vpush 12, 对应这条指令的栈顶元素就是 12,虚拟内存 12 处,存放的是地址 8,地址 8 处保存了参数 1。因此这条指令执行后将参数 1 保存到了虚拟内存 44 处。
下两条指令分别是 vpush 24 和 vload 48,同上,即将参数 2 保存在虚拟地址 48 处。
接着是 vpush 44 和 vpush 48,即将两个参数再次保存到栈中。
接着是字节码 0x3,对应 handler 是 push_addr:
handler_add: ; preds = %dispatch
%55 = load i32, i32* %2 取当前字节码下标
%56 = load i32, i32* %5 取当前栈下标
%57 = sub i32 %56, 2 栈下标 - 2
%58 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %57 取栈地址
%59 = load i32, i32* %58 栈地址取值,得到虚拟内存地址
%60 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %59 取最内存地址
%61 = bitcast i8* %60 to i32* 虚拟内存地址转成 i32*
%62 = load i32, i32* %61. 从虚拟内存地址处取值
%63 = sub i32 %56, 1 栈下标 - 1, 重复上述步骤
%64 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %63
%65 = load i32, i32* %64
%66 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %65
%67 = bitcast i8* %66 to i32*
%68 = load i32, i32* %67 取出了第二个值
%69 = add nsw i32 %62, %68 两个值相加
%70 = getelementptr [84 x i8], [84 x i8]* @opcodes, i32 0, i32 %55 字节码当前地址
%71 = bitcast i8* %70 to i32* 地址转成 i32*
%72 = load i32, i32* %71. 字节码当前地址处取出 4 字节,是一个下标地址
%73 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %72 虚拟内存处这个地址
%74 = bitcast i8* %73 to i32* 虚拟内存处地址转 i32*
store i32 %69, i32* %74. 前面计算的和保存在这个地址处
%75 = add i32 %55, 4 当前字节码下标 + 4
store i32 %75, i32* %2 更新当前字节码下标
%76 = sub i32 %56, 2 虚拟栈下标 - 2
store i32 %76, i32* %5 更新虚拟栈下标
br label %loopend
看注解,vadd指令有一个操作数,操作逻辑是将前两个 vpush指令压入栈中的地址,取出来并从虚拟内存中取出值,相加后保存在指令操作数指向的地址中。这里具体是 vadd 52。即相加的结果保存在了虚拟内存下标 52 处。
接下来的虚拟指令分别是:
vpush 52
vpush 36
vstore
vpush 36
vload 44
vpush 44
最后一个字节码是 0x4,对应该的 handler 是 handler_ret:
handler_ret: ; preds = %dispatch
%77 = load i32, i32* %2 取当前字节码下标
%78 = load i32, i32* %5 当前栈下标
%79 = sub i32 %78, 1 栈下标 - 1
%80 = getelementptr [256 x i32], [256 x i32]* %4, i32 0, i32 %79 栈地址
%81 = load i32, i32* %80 取出栈中的值
%82 = getelementptr [56 x i8], [56 x i8]* %3, i32 0, i32 %81 对应虚拟内存地址
%83 = bitcast i8* %82 to i32* 转 i32*
%84 = load i32, i32* %83. 取出这个值
ret i32 %84 返回这个值
看注解,vret指令的逻辑是从栈顶取出地址,并用该地址在虚拟内存中取出值,返回这个值。
综上分析得到字节码指令序列:
vpush 0
vpush 12
vStore
vpush 4
vpush 24
vStore
vpush 12
vload 44
vpush 24
vload 48
vpush 44
vpush 48
vadd 52
vpush 52
vpush 36
vstore
vpush 36
vload 44
vpush 44
vret
上述字节码的逻辑就是对输入的两个参数做转换赋值,结果相加后,再进行转换保存,最后返回。功能上与混淆前的函数等价。
上述虚拟化的过程实际上是对每个 IR指令的模拟,比如使用 vpush vpush vstore 来模拟一个store 指令,使用 vpush 和 vload 摸你 load 指令,并借助一个虚拟栈和虚拟内存。来模拟全部 IR指令的过程。
反编译
混淆前的反编译代码:
控制流图:
就一个基本块,没有控制流
混淆后的反编译代码:
__int64 __fastcall test_add(unsigned int a1, unsigned int a2)
{
int v2; // edx
int v3; // edx
int v5; // [rsp+0h] [rbp-440h]
int v6; // [rsp+4h] [rbp-43Ch]
unsigned __int64 v7; // [rsp+8h] [rbp-438h]
char v8; // [rsp+10h] [rbp-430h]
char *v9; // [rsp+14h] [rbp-42Ch]
char v10; // [rsp+1Ch] [rbp-424h]
char *v11; // [rsp+20h] [rbp-420h]
char v12; // [rsp+28h] [rbp-418h]
char *v13; // [rsp+2Ch] [rbp-414h]
int v14[256]; // [rsp+40h] [rbp-400h]
v9 = &v8;
v11 = &v10;
v13 = &v12;
v7 = __PAIR__(a2, a1);
v6 = 0;
v5 = 0;
while ( 1 )
{
v2 = byte_100004000[v6++];
switch ( v2 )
{
case 1:
**(_DWORD **)((char *)&v7 + v14[v5 - 1]) = *(_DWORD *)((char *)&v7 + v14[v5 - 2]);
v5 -= 2;
continue;
case 2:
*(_DWORD *)((char *)&v7 + *(signed int *)&byte_100004000[v6]) = **(_DWORD **)((char *)&v7 + v14[v5 - 1]);
v6 += 4;
--v5;
continue;
case 3:
*(_DWORD *)((char *)&v7 + *(signed int *)&byte_100004000[v6]) = *(_DWORD *)((char *)&v7 + v14[v5 - 1])
+ *(_DWORD *)((char *)&v7 + v14[v5 - 2]);
v6 += 4;
v5 -= 2;
continue;
case 4:
return *(unsigned int *)((char *)&v7 + v14[v5 - 1]);
case 5:
v3 = v6;
v14[v5++] = *(_DWORD *)&byte_100004000[v6];
goto LABEL_10;
case 6:
*((_BYTE *)&v7 + v14[v5-- - 1]) = byte_100004000[v6++];
break;
case 7:
*(_WORD *)((char *)&v7 + v14[v5-- - 1]) = *(_WORD *)&byte_100004000[v6];
v6 += 2;
break;
case 8:
v3 = v6;
*(_DWORD *)((char *)&v7 + v14[v5-- - 1]) = *(_DWORD *)&byte_100004000[v6];
LABEL_10:
v6 = v3 + 4;
break;
case 9:
*(unsigned __int64 *)((char *)&v7 + v14[v5-- - 1]) = *(_QWORD *)&byte_100004000[v6];
v6 += 8;
break;
default:
continue;
}
}
}
控制流图:
汇编层面分析虚拟opcode 指令流程
TODO