Asprotect 中的 X86 虚拟机代码分析
作 者: blackeyes
1. 起因:
最近跟踪一 Asprotect 保护的程序, 发现 stolen code 都是在 Asprotect 自己的虚拟机中执行,
非常不利于跟踪与分析, 于是把 Asprotect 的虚拟机代码进行了分析.
2. 代码处理概述
还是用例子来说明吧, 原始的一段 CODE 如下:
00D6FC1C 55 PUSH EBP
00D6FC1D 8BEC MOV EBP,ESP
00D6FC1F 83C4 E0 ADD ESP,-20
...
00D6FD48 8BE5 MOV ESP,EBP
00D6FD4A 5D POP EBP
00D6FD4B C2 0C00 RETN 0C
Asprotect 将上面的每一行机器代码分析处理, 然后每一行保存到一个固定大小的结构中,
运行的时候这段代码就只需要下面四行:
00D6FC1C 68 00000000 PUSH 0
00D6FC21 68 1CFCD600 PUSH 0D6FC1C
00D6FC26 68 B432E600 PUSH 0E632B4
00D6FC2B E8 18960000 CALL 00D79248
其中:
00D6FC1C ----- 代码起始地址
00E632B4 ----- 一结构起始地址, 包含处理后的代码信息
00D79248 ----- X86 虚拟机 Function 地址
3. 机器代码分析
每一行机器代码被分析处理后, 会分解成 10 项 保存到结构中, 如下:
1 - 第 1 个 机器码的内存起始地址;
2 - 机器码的第 1 个 BYTE, 如果不是前缀机器码, 就是真正的机器码的第 1 个 BYTE;
3 - 机器码的第 2 个 BYTE, 并且前面是前缀机器码, 它是真正的机器码的第 1 个 BYTE;
4 - 机器码中的立即数是否要调整, 相当于重定位, 例如;
00400000 68 34124000 PUSH 00401234
如果希望这行代码在 00500000 是这样工作的:
00500000 68 34125000 PUSH 00501234
即表示机器码中的立即数是随段起始地址而调整的.
5 - 机器码中的 第 1 个 立即数;
6 - 机器码中的 第 2 个 立即数;
7 - 机器码中的算术/逻辑操作;
0:ADD, 1:OR, 2:ADC, 3:SBB, 4:TEST, 5:SUB, 6:XOR, 7:CMP
8 - 机器码中的 ModRM 操作码;
9 - 机器码中的 SIM 操作码;
10 - 机器码中的 Displacement 操作码;
每一项都由一 Function 读出, 其中一些还要做一些变换.
并不是每一项都存在于每一行机器码.
4. 机器代码数据结构
每一行机器代码对应的结构如下:
typedef struct {
BYTE FirstOpcode_0;
BYTE Unknown1;
BYTE SecondImmediateData;
BYTE Unknown2[5];
BYTE SIMOpcode;
BYTE Unknown3[2];
DWORD DisplacementOpcode;
BYTE Unknown4;
BYTE FirstOpcode_1; // if FirstOpCode is a prefix
BYTE Unknown5[3];
DWORD ImmediateDataOpcode;
DWORD Unknown6;
BOOL bAdjustValueFlag;
BYTE Unknown7;
DWORD EncryptedEIPAddress; //+1E , EncryptedEIPAddress + baseAddress + randxx ==>EIPAddress
BYTE Unknown8[2];
BYTE MathType; // Mathtype or an additional opcode
BYTE Unknown9[3];
BYTE ModRMOpcode;
DWORD Unknown10;
} ENC_LINE;
每一段代码由 n 行代码构成, 对应如下的结构:
typedef struct {
DWORD Unknown;
DWORD pFirstItem;
DWORD ItemNum;
BYTE FuncIndex[0x0A]; // 00E632C0 01 06 05 00 08 04 03 07 02 09
BYTE FuncIndex2[0x0A];
DWORD Funcs[0x0A];
/*
00E632D4 011F0000 // return __0014 Func3
00E632D8 011E0000 // return __10 Func0
00E632DC 01200000 // return __1C Func8
00E632E0 011C0000 // return __08 Func6
00E632E4 01230000 // return __28 Func5
00E632E8 01220000 // return __24 Func2
00E632EC 011A0000 // return __00 Func1
00E632F0 011D0000 // return __000B Func7
00E632F4 011B0000 // return __02 Func4
00E632F8 01210000 // return __001E Func9
*/
DWORD ItemSize;
DWORD BaseAddr;
DWORD RandomXX; // +50
DWORD procID;
DWORD Size;
ENC_LINE Lines[0];
} ENC_INFO;
其中 FuncIndex[], FuncIndex2[], Funcs[], 每次运行都会随机重新排序, 但是
for(i=0;i<0x0A;i++) {
j = FuncIndex[i];
Funcxx = Funcs[j]; // Funxx 跟 i 是一一对应的
}
这是Funcxx的返回值 与 i 的 对应图
00E63310 A1 88 00 4A 7B A2 B0 2F 00 0F 18 00 00 00 00 C9 1?4?????6??7777?
00E63320 00 54 F7 21 00 00 00 00 7D 05 54 89 00 9C 68 FD 0???3333????8?99
00E63330 EC A1 7B BC 00 25 45 BF 00 61 08 4A F5 99??2???5????
5. 虚拟机数据结构
typedef struct {
DWORD Unknown1[8]; // Drx[8]??
#define EAX REGs[0]
#define ECX REGs[1]
#define EDX REGs[2]
#define EBX REGs[3]
#define ESP REGs[4]
#define EBP REGs[5]
#define ESI REGs[6]
#define EDI REGs[7]
DWORD REGs[8];
DWORD Unknown2;
DWORD EIP;
DWORD EFlags;
DWORD Unknown3;
DWORD CS_base, SS_base, DS_base, ES_base, FS_base, GS_base; // +60
WORD CS, SS, DS, ES, FS, GS; // +78
BYTE Unknown4[0x0C]; //+84
DWORD LowOpNum; //+90
DWORD HiOpNum; //+94
DWORD CurSegbase; //+98
BOOL bAdjustValueFlag; //+9C
DWORD seh; // +9D
BYTE bPrefixFlag; //+A1
OpDataInfo Src; // +A2
OpDataInfo Dst; //+A7
DWORD OperandSize; // +AC: 1 - byte ptr, 2 - word ptr, 4 - dword ptr
#define X86_FLAGS_LOCK_PREFIX 0x0001
#define X86_FLAGS_REPNE_PREFIX 0x0002
#define X86_FLAGS_REP_PREFIX 0x0004
#define X86_FLAGS_CS_PREFIX 0x0008
#define X86_FLAGS_SS_PREFIX 0x0010
#define X86_FLAGS_DS_PREFIX 0x0020
#define X86_FLAGS_ES_PREFIX 0x0040
#define X86_FLAGS_FS_PREFIX 0x0080
#define X86_FLAGS_GS_PREFIX 0x0100
#define X86_FLAGS_OPERAND_PREFIX 0x0200
#define X86_FLAGS_ADDRESS_PREFIX 0x0400
DWORD BitFlags; // +B0
BYTE Opcode;
DWORD curCodeItem;
DWORD nextCodeItem;
} X86_INFO;
其中
typedef struct {
union {
DWORD RegIndex;
DWORD Address;
} u;
BYTE RegMode; // 1: RegMode, 0: MemAddrMode, 2:ImmediateData, 4:MemAddrMode, dispx[Regx]?
} OpDataInfo;
当进入到 虚拟机 Function 地址后, 在栈上开出一片空间, 为X86_INFO, 代表了虚拟机的状态, 主要就是
CPU 的各个寄存器, 及一些标志.
6. 转储(DUMP) ENC_INFO 和 ENC_LINE[] 结构
用 OD 附加在运行的程序上, 然后在 ASPR 所在的内存段, SEARCH 下面的代码
addr_xx:
68 00000000 PUSH 0
68 xxxxxxxx PUSH addr_xx
68 yyyyyyyy PUSH addr_yy
E8 zzzzzzzz CALL func_zz
其中 addr_yy 就是结构 ENC_INFO 的地址, 根据其中的 ItemNum 和 ItemSize 可以确定需要DUMP 的大小.
附件中的 aspr_x86_dump_info.txt 可用来帮助DUMP
7. 还原机器码
刚开始以为, 可以在各个 Funcxx 中 设 断点来DUMP 再还原机器码, 也不理想, 理由如下:
A.) 各个 Funcxx, 有的对应机器码, 有的不对应机器码;
B.) 有些即使对应机器码, 还要进行变换;
C.) 有些 Funcxx , 可能对应 1 BYTE, 1WORD, 或者 1DWORD, 与参数有关;
D.) 如果有循环, 就会有重复;
E.) 如果有跳转, 未运行到的CODE 又没法 DUMP.
所以还是把它的X86处理CODE分析清楚, 再写 CODE 还原;
好在它是按标准的Intel的X86指令集来处理的, 参考 Intel 的 "Instruction Set Reference", 好多CODE 很容易明白,
刚开始还想把每一行机器码的反汇编弄出来, 试了以下, 有点累, 算了.
附件中的 aspr_x86.c 和 aspr_x86.h 可用来处理 DUMP 出来的二进制数据, 还原机器码
8. Reference
A. ) 24319102.PDF, "Intel Architecture Software Developer's Manual Volume2: Instruction Set Reference";
B. ) 被分析的程序: "Registry Defragmentation 8.2.6.11"
8. 后记
分析完ASPR的这段CODE后, 对 Intel 的X86 指令集又熟悉了一些, 也算是有所收获.
以下是附件中的各个文件:
A.) aspr_x86_code.txt ASPR_X86 CODE 跟踪分析笔记
B.) aspr_x86_dump_info.txt ODbgScript 脚本, DUMP X86_INFO
C.) aspr_x86.c & aspr_x86.h C 代码, 处理DUMP 的 X86_INFO
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)