指令虚拟化
虚拟机(VM)其实就是用软件来模拟硬件。我们可以仿照 x86 指令集,自己定义一套指令,在程序(解释器)中有一套函数和结构来解析自定义的指令并执行相应的功能。
虚拟化是一种基于虚拟机的代码保护技术。他将硬件支持的机器码转化为字节码指令系统,来达到不被轻易篡改和逆向的目的。
简单来说就是出题人通过实现一个小型的虚拟机,自定义一些操作码(opcode),然后在程序执行时通过解释操作码,执行对应的函数,从而实现程序原有的功能。
下图是常见的虚拟机结构:
虚拟机的主程序其实就是一个循环,这个循环不断的去读取指令(伪机器码 opcode),然后执行指令opcode 所对应的一些函数,这样下来就可以与真实的程序执行相差无几。
正向实现
想要对抗虚拟化,首先要搞清楚用于保护的虚拟机是如何实现的要想实现虚拟机,需要完成两个目标:
定义一套指令集
实现对应的解释器
结构体定义
真实赛题中的 VM 通常会实现一个类似如下的结构体,用于保存虚拟机状态:
1 2 3 4 5 6 7 8
| typedef struct { unsigned int r1; unsigned int r2; unsigned int eip; unsigned char mem[256]; unsigned char code[1024]; } VM;
|
opcode 定义
接着自定义一些指令,需要决定该指令集是定长的还是变长的。
这里以变长指令集为例,先列出一个表来:
书写机器码
假定希望实现的语义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <stdio.h> #include <string.h> int main() { char input[6]; char target[] = "Hello"; scanf("%5s", input); for (int i = 0; i < 5; i++) { input[i] ^= 0x21; } if (!memcmp(input, target, 5)) printf("Yes\n"); else printf("No\n"); }
|
根据我们定义的指令对其进行拆分和重构,可以得到如下机器码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| unsigned char code[] = { 0x20, 0x10, 0x48, 0x20, 0x11, 0x65, 0x20, 0x12, 0x6c, 0x20, 0x13, 0x6c, 0x20, 0x14, 0x6f, 0x40, 0x11, 0x21, 0x10, 0x0, 0x30, 0x10, 0x1, 0x30, 0x10, 0x2, 0x30, 0x10, 0x3, 0x30, 0x10, 0x4, 0x30, 0x50, 0x10, 0x5 };
|
初始化虚拟机
在实际运行虚拟机之前,需要先对 VM 结构体进行初始化:
1 2 3 4 5 6
| VM* vm_new() { VM* vm = (VM*)malloc(sizeof(VM)); memset(vm, 0, sizeof(VM)); memcpy(vm->code, code, sizeof(code)); return vm; }
|
解释器编写
现在就可以来实现每条指令的 handle 以及 dispatcher 了
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
| int vm_run(VM* vm) { char opcode; char operand_1, operand_2; while (1) { opcode = vm->code[vm->eip]; switch (opcode) { case 0x10: operand_1 = vm->code[vm->eip + 1]; vm->r1 = operand_1; vm->eip += 2; break; case 0x11: operand_1 = vm->code[vm->eip + 1]; vm->r2 = operand_1; vm->eip += 2; break; case 0x20: operand_1 = vm->code[vm->eip + 1]; operand_2 = vm->code[vm->eip + 2]; vm->mem[operand_1] = operand_2; vm->eip += 3; break; case 0x30: vm->mem[vm->r1] ^= vm->r2; vm->eip += 1; break; case 0x40: scanf("%5s", &vm->mem[0]); vm->eip += 1; break; case 0x50: operand_1 = vm->code[vm->eip + 1]; operand_2 = vm->code[vm->eip + 2]; return memcmp(&vm->mem[0], &vm->mem[operand_1], operand_2); } } }
|
启动
于是 main 函数可以这样写:
1 2 3 4 5 6 7
| int main() { VM* vm = vm_new(); if (!vm_run(vm)) printf("Yes\n"); else printf("No\n"); }
|
解题步骤
遇到 VM 类的赛题,我们一般按照如下的步骤来解题:
分析 VM 结构
分析指令集
- 指令长度是否可变
- 每种指令的构成
- 每种指令的含义(伪汇编)
- VM 的退出条件
编写 Python 版解释器,输出伪汇编代码
阅读伪代码,分析程序流程,写出去虚拟化的原始代码
书写解题脚本
本例中的 Python 版解释器如下:
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
| code = [0x20, 0x10, 0x48, 0x20, 0x11, 0x65, 0x20, 0x12, 0x6c, 0x20, 0x13, 0x6c, 0x20, 0x14, 0x6f, 0x40, 0x11, 0x21, 0x10, 0x0, 0x30, 0x10, 0x1, 0x30, 0x10, 0x2, 0x30, 0x10, 0x3, 0x30, 0x10, 0x4, 0x30, 0x50, 0x10, 0x5] ip = 0 r1 = r2 = 0 mem = [0] * 256 while True: opcode = code[ip] if opcode == 0x10 : r1 = code[ip + 1] print(f"mov r1, {hex(code[ip + 1])}") ip += 2 elif opcode == 0x11 : r2 = code[ip + 1] print(f"mov r2, {hex(code[ip + 1])}") ip += 2 elif opcode == 0x20 : op1, op2 = code[ip + 1], code[ip + 2] mem[op1] = op2 print(f"mov [{hex(op1)}], {hex(op2)}") ip += 3 elif opcode == 0x30 : mem[r1] ^= r2 print(f"xor [{hex(r1)}], {hex(r2)}") ip += 1 elif opcode == 0x40 : # flag = input().encode() flag = b"iDMMN" for i in range(len(flag)) : mem[i] = flag[i] ip += 1 elif opcode == 0x50 : op1, op2 = code[ip + 1], code[ip + 2] print(mem[0:op2] == mem[op1:op1 + op2]) break else: raise ValueError("unknown opcode")
|