未混淆编译
编写一个最简单的测试代码, 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 指令流程
从函数入口开始分析:
__text:0000000100003D80 _test_add_VM proc near ; CODE XREF: _test_add+4↑p
__text:0000000100003D80
__text:0000000100003D80 var_440 = dword ptr -440h
__text:0000000100003D80 var_43C = dword ptr -43Ch
__text:0000000100003D80 var_438 = qword ptr -438h
__text:0000000100003D80 var_430 = byte ptr -430h
__text:0000000100003D80 var_42C = qword ptr -42Ch
__text:0000000100003D80 var_424 = byte ptr -424h
__text:0000000100003D80 var_420 = qword ptr -420h
__text:0000000100003D80 var_418 = byte ptr -418h
__text:0000000100003D80 var_414 = qword ptr -414h
__text:0000000100003D80 var_400 = dword ptr -400h
__text:0000000100003D80
__text:0000000100003D80 sub rsp, 3C0h 分配栈空间
__text:0000000100003D87 lea rax, [rsp+3C0h+var_430] 取地址
__text:0000000100003D8C mov [rsp+3C0h+var_42C], rax 保存地址
__text:0000000100003D91 lea rax, [rsp+3C0h+var_424] 取地址
__text:0000000100003D96 mov [rsp+3C0h+var_420], rax 保存地址
__text:0000000100003D9B lea rax, [rsp+3C0h+var_418] 取地址
__text:0000000100003DA0 mov [rsp+3C0h+var_414], rax 保存地址
__text:0000000100003DA5 mov dword ptr [rsp+3C0h+var_438], edi 保存参数1
__text:0000000100003DA9 mov dword ptr [rsp+3C0h+var_438+4], esi 保存参数2
__text:0000000100003DAD mov [rsp+3C0h+var_43C], 0 初始化字节码下标
__text:0000000100003DB5 mov [rsp+3C0h+var_440], 0 初始化栈下标
__text:0000000100003DBD lea rax, unk_100004000 字节码首地址
__text:0000000100003DC4 lea rcx, off_100003F74 handler 表
__text:0000000100003DCB jmp short loc_100003E00 ; 跳转 dispatch
这段入口基本块的主要作用就是初始化,首先 sub rsp, 3C0h 指令为函数执行分配栈空间,然后把 3 个局部变量的地址,保存到另外 3 个变量中。edi 和 esi 是传入的两个参数值(x86_64调用约定)保存到 rsp+3C0h+var_438处,可以认为 rsp+3C0h+var_438 地址是虚拟内存的首地址,是一个栈中的局部变量。 初始化后 rsp+3C0h+var_438 和 rsp+3C0h+var_438+4 分别保存了两个参数。
接着把 rsp+3C0h+var_43C 和 rsp+3C0h+var_440 初始化为 0,用来表示当前字节码下标和当前虚拟栈下标。最后将虚拟字节码地址保存到 rax寄存器,将 handler 地址保存到 rcx寄存器。然后跳转到地址 loc_100003E00处执行。
__text:0000000100003E00 loc_100003E00: ; CODE XREF: _test_add_VM+4B↑j
__text:0000000100003E00 ; _test_add_VM+94↓j ...
__text:0000000100003E00 movsxd rsi, [rsp+3C0h+var_43C] ; jumptable 0000000100003E1D default case
__text:0000000100003E05 movzx edx, byte ptr [rsi+rax]
__text:0000000100003E09 inc esi
__text:0000000100003E0B mov [rsp+3C0h+var_43C], esi
__text:0000000100003E0F dec edx
__text:0000000100003E11 cmp edx, 8 ; switch 9 cases
__text:0000000100003E14 ja short loc_100003E00 ; jumptable 0000000100003E1D default case
__text:0000000100003E16 movsxd rdx, dword ptr [rcx+rdx*4]
__text:0000000100003E1A add rdx, rcx
__text:0000000100003E1D jmp rdx ; switch jump
这个基本块的作用是 dispatch,取出一个字节码,然后跳转到对应 handler 继续执行。首先从rsp+3C0h+var_43C 处取出当前字节码下标保存到 rsi寄存器。 [rsi+rax] 即当前字节码地址取出一个值,这里是取出一个 byte值保存到 edx中。然后 inc esi将当前字节码下标+1,并保存到rsp+3C0h+var_43C地址处,即将当前字节码下标移动了一个字节。然后将 dex减去 1,如果 edx的值大于8,则跳转到 dispatch,正常情况下字节码的值应该不会大于9。然后取 rcx+rdx*4 处的值保存到 rdx中,rcx 存储的是 handler 地址表,rdx是字节码-1 后的值,实际上这个字节码就是一个下标,在 handler 表中找到具体是哪个handler。取出 handler 保存到 rdx后,这个 handler 的地址实际上是一个偏移,相对于 handler表的偏移,加上 rcx后,得到实际的 handler 地址保存到 rdx中,最后 jump rdx,无条件跳转到对应 handler 执行。
我们取出内存中的字节码:
data:0000000100004000 unk_100004000 db 5 ; DATA XREF: _test_add_VM+3D↑o
__data:0000000100004001 db 0
__data:0000000100004002 db 0
__data:0000000100004003 db 0
__data:0000000100004004 db 0
__data:0000000100004005 db 5
__data:0000000100004006 db 0Ch
__data:0000000100004007 db 0
__data:0000000100004008 db 0
__data:0000000100004009 db 0
__data:000000010000400A db 1
__data:000000010000400B db 5
__data:000000010000400C db 4
__data:000000010000400D db 0
__data:000000010000400E db 0
__data:000000010000400F db 0
__data:0000000100004010 db 5
__data:0000000100004011 db 18h
__data:0000000100004012 db 0
__data:0000000100004013 db 0
__data:0000000100004014 db 0
__data:0000000100004015 db 1
__data:0000000100004016 db 5
__data:0000000100004017 db 0Ch
__data:0000000100004018 db 0
__data:0000000100004019 db 0
__data:000000010000401A db 0
__data:000000010000401B db 2
__data:000000010000401C db 2Ch ; ,
__data:000000010000401D db 0
__data:000000010000401E db 0
__data:000000010000401F db 0
__data:0000000100004020 db 5
__data:0000000100004021 db 18h
__data:0000000100004022 db 0
__data:0000000100004023 db 0
__data:0000000100004024 db 0
__data:0000000100004025 db 2
__data:0000000100004026 db 30h ; 0
__data:0000000100004027 db 0
__data:0000000100004028 db 0
__data:0000000100004029 db 0
__data:000000010000402A db 5
__data:000000010000402B db 2Ch ; ,
__data:000000010000402C db 0
__data:000000010000402D db 0
__data:000000010000402E db 0
__data:000000010000402F db 5
__data:0000000100004030 db 30h ; 0
__data:0000000100004031 db 0
__data:0000000100004032 db 0
__data:0000000100004033 db 0
__data:0000000100004034 db 3
__data:0000000100004035 db 34h ; 4
__data:0000000100004036 db 0
__data:0000000100004037 db 0
__data:0000000100004038 db 0
__data:0000000100004039 db 5
__data:000000010000403A db 34h ; 4
__data:000000010000403B db 0
__data:000000010000403C db 0
__data:000000010000403D db 0
__data:000000010000403E db 5
__data:000000010000403F db 24h ; $
__data:0000000100004040 db 0
__data:0000000100004041 db 0
__data:0000000100004042 db 0
__data:0000000100004043 db 1
__data:0000000100004044 db 5
__data:0000000100004045 db 24h ; $
__data:0000000100004046 db 0
__data:0000000100004047 db 0
__data:0000000100004048 db 0
__data:0000000100004049 db 2
__data:000000010000404A db 2Ch ; ,
__data:000000010000404B db 0
__data:000000010000404C db 0
__data:000000010000404D db 0
__data:000000010000404E db 5
__data:000000010000404F db 2Ch ; ,
__data:0000000100004050 db 0
__data:0000000100004051 db 0
__data:0000000100004052 db 0
__data:0000000100004053 db 4
__data:0000000100004053 __data ends
第一个字节码是 0x5,即 handler 是handler 表中第 5 个,下标 4 处。
__text:0000000100003E8A loc_100003E8A: ; CODE XREF: _test_add_VM+9D↑j
__text:0000000100003E8A ; DATA XREF: __text:off_100003F74↓o
__text:0000000100003E8A movsxd rdx, [rsp+3C0h+var_43C] ; jumptable 0000000100003E1D case 4
__text:0000000100003E8F movsxd rsi, [rsp+3C0h+var_440]
__text:0000000100003E94 mov edi, [rdx+rax]
__text:0000000100003E97 mov [rsp+rsi*4+3C0h+var_400], edi
__text:0000000100003E9B inc esi
__text:0000000100003E9D mov [rsp+3C0h+var_440], esi
__text:0000000100003EA1 jmp short loc_100003F1B
这个代码块先从 rsp+3C0h+var_43C 取值保存到 rdx中,即当前字节码下标。从 rsp+3C0h+var_440取值保存到 rsi中,即当前栈顶下标。执行第一个 handler 的时候,字节码下标是 1,栈顶下标是 0。然后将 rdx+rax 处的值保存到 edi,即从当前字节码处取出了 4 字节(edi是rdi的一半,即32 位寄存器 )。然后将edi的值保存到 rsp+rsi*4+3C0h+var_400,rsp+3C0h+var_400应该就是虚拟栈的地址 rsi*4 是当前栈顶偏移,所以这个操作就是从当前字节码处取出一个 4 字节的地址,然后保存到虚拟栈中。后面两个指令将当前栈顶下标+1后更新到了局部变量中。然后跳转到地址 loc_100003F1B处。
__text:0000000100003F1B loc_100003F1B: ; CODE XREF: _test_add_VM+121↑j
__text:0000000100003F1B add edx, 4
__text:0000000100003F1E mov [rsp+3C0h+var_43C], edx
__text:0000000100003F22 jmp loc_100003E00
这里将 edx寄存器值加 4,因为前面从当前字节码处取出了一个 4 字节地址,所以需要将当前字节码的下标后移 4 个字节,然后 mov [rsp+3C0h+var_43C], edx 更新当前字节码下标变量,最后继续跳转到 loc_100003E00 即 dispatch 处继续执行下一个字节码。
所以字节码 0x5 对应的虚拟指令是 vpush,将字节码保存的一个4 字节常量地址保存到虚拟栈,并更新字节码下标和栈顶下标。这里具体是 vpush 0
下一个字节码同样是 0x5,对应的虚拟指令是 vpush 12。
下一个字节码地址是 000000010000400A,对应的字节码是 0x1,即 handler 表第一个:
__text:0000000100003E1F loc_100003E1F: ; CODE XREF: _test_add_VM+9D↑j
__text:0000000100003E1F ; DATA XREF: __text:off_100003F74↓o
__text:0000000100003E1F mov edx, [rsp+3C0h+var_440] ; jumptable 0000000100003E1D case 0
__text:0000000100003E23 lea esi, [rdx-2]
__text:0000000100003E26 movsxd rsi, esi
__text:0000000100003E29 movsxd rdi, [rsp+rsi*4+3C0h+var_400]
__text:0000000100003E2E mov edi, dword ptr [rsp+rdi+3C0h+var_438]
__text:0000000100003E32 dec edx
__text:0000000100003E34 movsxd rdx, edx
__text:0000000100003E37 movsxd rdx, [rsp+rdx*4+3C0h+var_400]
__text:0000000100003E3C mov rdx, [rsp+rdx+3C0h+var_438]
__text:0000000100003E41 mov [rdx], edi
__text:0000000100003E43 mov [rsp+3C0h+var_440], esi
__text:0000000100003E47 jmp short loc_100003E00 ;
这个handler 第一个指令是取当前栈顶下标保存到 edx寄存器,然后把下标-2 后赋值给 rsi,即栈顶后两个元素,前面两个 vpush操作正好在栈中保存了两个元素。
movsxd rdi, [rsp+rsi*4+3C0h+var_400]指令将栈中的值赋值到 rdi,这个值实际上是虚拟内存的地址下标,mov edi, dword ptr [rsp+rdi+3C0h+var_438]将对应的虚拟地址处的值赋值给 edi。此时完成了从栈中取出一个地址,并根据这个地址在虚拟内存中取值,保存到了 edi寄存器。
dec edx是栈顶下标 - 1 处,转给 rdx,movsxd rdx, [rsp+rdx*4+3C0h+var_400]将虚拟栈中的值保存到了 rdx寄存器,这个值同样是一个地址下标,mov rdx, [rsp+rdx+3C0h+var_438]将对应虚拟内存中的值取出,这个值依然是一个地址,mov [rdx], edi 即将前面取出的值保存在了这个地址处。所以这个 handler 的逻辑是,从栈顶取出两个元素,这是两个地址下标,根据第一个地址从虚拟内存取出一个值,根据第二个地址在虚拟内存取出一个地址,将第一个取的值,保存在第二个取的地址中。对应的虚拟指令是 vstore,没有操作数。
下一个字节码地址是000000010000400B,对应虚拟指令是 vpush 4
下一个字节码地址是 0000000100004010,对应虚拟指令是 vpush 24
下一个字节码地址是 0000000100004015,对应虚拟指令是 vstore
下一个字节码地址是 0000000100004016,对应虚拟指令是 vpush 12
下一个字节码地址是 000000010000401B,对应字节码是 0x2:
__text:0000000100003F27 loc_100003F27: ; CODE XREF: _test_add_VM+9D↑j
__text:0000000100003F27 ; DATA XREF: __text:off_100003F74↓o
__text:0000000100003F27 movsxd rdx, [rsp+3C0h+var_43C] ; jumptable 0000000100003E1D case 1
__text:0000000100003F2C mov esi, [rsp+3C0h+var_440]
__text:0000000100003F30 dec esi
__text:0000000100003F32 movsxd rsi, esi
__text:0000000100003F35 movsxd rdi, [rsp+rsi*4+3C0h+var_400]
__text:0000000100003F3A mov rdi, [rsp+rdi+3C0h+var_438]
__text:0000000100003F3F mov edi, [rdi]
__text:0000000100003F41 movsxd r8, dword ptr [rdx+rax]
__text:0000000100003F45 mov dword ptr [rsp+r8+3C0h+var_438], edi
__text:0000000100003F4A add edx, 4
__text:0000000100003F4D mov [rsp+3C0h+var_43C], edx
__text:0000000100003F51 mov [rsp+3C0h+var_440], esi
__text:0000000100003F55 jmp loc_100003E00
上述基本块,首先取出字节码下标保存到 rdx,取出栈顶下标保存到 esi,dec esi后赋值给 rsi,即上一个指令 push到栈顶的元素,movsxd rdi, [rsp+rsi*4+3C0h+var_400]指令取出这个值保存到 rdi寄存器,这个值是一个虚拟内存的地址下标,mov rdi, [rsp+rdi+3C0h+var_438]从虚拟内存取值保存到 rdi,这个 rdi同样是一个地址,mov edi, [rdi]指令从 rdi指向的地址处取值保存到 edi。
接着指令movsxd r8, dword ptr [rdx+rax]从当前字节码处取出 4 字节保存到 r8寄存器,这是指令的操作数,同时也是一个地址下标,mov dword ptr [rsp+r8+3C0h+var_438], edi指令将上述保存在 edi中的值保存在 r8指向的虚拟内存中。
上述的分析流程可以理解成是一个 vstore 指令,这个虚拟指令的流程是从取出栈顶元素取出虚拟地址下标,根据这个下标从虚拟内存处取值,取出的值同样是一个地址,再次从虚拟内存中取值,最后将这个值保存在操作数指向的地址中,具体虚拟指令是 vload 44
下一个字节码地址是 0000000100004020,对应虚拟指令是 vpush 24
下一个字节码地址是 0000000100004025,对应虚拟指令是 vload 48
下一个字节码地址是 000000010000402A,对应虚拟指令是 vpush 44
下一个字节码地址是 000000010000402F,对应虚拟指令地址是 vpush 48
下一个字节码地址是 0000000100004034,对应字节码是 0x3:
__text:0000000100003E49 loc_100003E49: ; CODE XREF: _test_add_VM+9D↑j
__text:0000000100003E49 ; DATA XREF: __text:off_100003F74↓o
__text:0000000100003E49 movsxd rdx, [rsp+3C0h+var_43C] ; jumptable 0000000100003E1D case 2
__text:0000000100003E4E mov esi, [rsp+3C0h+var_440]
__text:0000000100003E52 lea edi, [rsi-2]
__text:0000000100003E55 movsxd rdi, edi
__text:0000000100003E58 movsxd r8, [rsp+rdi*4+3C0h+var_400]
__text:0000000100003E5D mov r8d, dword ptr [rsp+r8+3C0h+var_438]
__text:0000000100003E62 dec esi
__text:0000000100003E64 movsxd rsi, esi
__text:0000000100003E67 movsxd rsi, [rsp+rsi*4+3C0h+var_400]
__text:0000000100003E6C add r8d, dword ptr [rsp+rsi+3C0h+var_438]
__text:0000000100003E71 movsxd rsi, dword ptr [rdx+rax]
__text:0000000100003E75 mov dword ptr [rsp+rsi+3C0h+var_438], r8d
__text:0000000100003E7A add edx, 4
__text:0000000100003E7D mov [rsp+3C0h+var_43C], edx
__text:0000000100003E81 mov [rsp+3C0h+var_440], edi
__text:0000000100003E85 jmp loc_100003E00 ;
这个指令从栈顶取出了两个元素,是两个指向虚拟内存的地址,然后根据这两个地址取出两个 dword类型的值,相加后保存到 r8d 寄存器,然后讲结果保存到操作数指向的地址处,具体对应的虚拟指令是 vadd 52
下一个字节码地址是 0000000100004039,对应虚拟指令是 vpush 52
下一个字节码地址是 000000010000403E,对应虚拟指令是 vpush 36
下一个字节码地址是 0000000100004043,对应虚拟指令是 vstore
下一个字节码地址是 0000000100004044,对应虚拟指令是 vpush 36
下一个字节码地址是 0000000100004049,对应虚拟指令是 vload 44
下一个字节码地址是 000000010000404E,对饮虚拟指令是 vpush 44
最后一个字节码地址是 0000000100004053,对应字节码是 0x4:
__text:0000000100003F5A loc_100003F5A: ; CODE XREF: _test_add_VM+9D↑j
__text:0000000100003F5A ; DATA XREF: __text:off_100003F74↓o
__text:0000000100003F5A mov eax, [rsp+3C0h+var_440] ; jumptable 0000000100003E1D case 3
__text:0000000100003F5E dec eax
__text:0000000100003F60 cdqe
__text:0000000100003F62 movsxd rax, [rsp+rax*4+3C0h+var_400]
__text:0000000100003F67 mov eax, dword ptr [rsp+rax+3C0h+var_438]
__text:0000000100003F6B add rsp, 3C0h
__text:0000000100003F72 retn
这个基本块的第一个指令 mov eax, [rsp+3C0h+var_440] 取出栈顶下标赋值给 eax寄存器,dec eax 减 1,指向实际的栈顶元素。cdqe是一个扩展指令,将 eax双倍扩展成了 rax 8字节。接下来movsxd rax, [rsp+rax*4+3C0h+var_400]指令从虚拟栈中取出了这个值保存到 rax寄存器,这个值是一个虚拟内存的下标地址,然后 mov eax, dword ptr [rsp+rax+3C0h+var_438] 从虚拟内存中取出了这个值保存在了 eax寄存器中,最后恢复栈大小后函数返回,根据x86_64的调用约定,函数的返回值保存在 eax寄存器中。
根据上述分析,这是一个 vret虚拟指令,操作逻辑是从栈顶取出地址,根据地址从虚拟内存取出一个值,并返回这个值。
到这里字节码执行完成,执行的字节码序列和分析 IR代码时完全一致,根据这个字节码序列可以知道,这个字节码序列的功能就是返回两个 int整数的和。
IDA脚本自动提取虚拟指令
通过上面对实际反汇编代码的分析,我们知道了每个字节码的表示和 handler 的具体逻辑,那么可以写一个脚本对 opcode 序列进行处理,提取出具体的虚拟指令序列:
TODO