VMP是一种软件加固方法
Virtual Machine Protect. 虚拟机保护 ,可以将汇编指令转化为自定义指令集,虚拟指令涉及上百万条汇编指令,极大增强pj难度。
由win版本的和linux,安卓版本的。他们的软件实现方法和厂家都不一样,但是原理相同。
win具体的软件由pmvrotect2.x 3.x 3.5。vmp加密时只是对之前的so进行新增加密节区,不修改节区。
pmvrotect2软件加密的手段有很多最著名的是vmp技术。包含 指令虚拟化,导入表加密,反调试,反dump,指令和数据压缩
vmpdumper可以脱出汇编代码但是不能修复vmp节区和导出表,加密字符串。需要手动修复。导入表(IAT)不能修复
代码压缩 (Packing)
原理:压缩代码段
原始PE结构: ├─ .text (500KB) - 可执行代码 ├─ .data (100KB) - 数据 └─ .rsrc (200KB) - 资源 ↓ 压缩后 ↓ ├─ .vmp0 (150KB) - 压缩的代码+解压stub ├─ .vmp1 (80KB) - 压缩的数据 └─ .rsrc (200KB) 运行时: 1. 解压.vmp0到内存 2. 修复重定位 3. 跳转执行
压缩算法:LZMA/自定义
强度:★★
主要用途:减小体积(附带混淆效果)
代码虚拟化 (Virtualization) ⭐最强
原理:将x86指令转换为自定义虚拟机指令
转换示例:
; 原始代码 mov eax, 5 add eax, 3 ret ; ↓ 虚拟化后 ↓ push offset vm_bytecode ; VM字节码 call vm_interpreter ; 调用VM解释器 ret ; vm_bytecode (自定义指令): ; 0x45 0x05 0x00 0x00 0x00 ; VM_MOV eax, 5 ; 0x12 0x03 0x00 0x00 0x00 ; VM_ADD eax, 3 ; 0xFF ; VM_RET
C代码示例:
// 原始 int add(int a, int b) { return a + b; } // SDK标记 #include "VMProtectSDK.h" int add(int a, int b) { VMProtectBeginVirtualization("Add"); return a + b; VMProtectEnd(); }
强度:★★★★★
性能损耗:5-20倍
代码变异 (Mutation/Obfuscation)
原理:用等价但复杂的指令替换原指令
转换示例:
; 原始 mov eax, 5 ; ↓ 变异后 ↓ push 2 push 3 pop ecx pop edx lea eax, [ecx+edx] ; 实际还是5 ; 或者 xor eax, eax add eax, 3 inc eax inc eax ; 仍是5,但复杂化
典型变异技术:
; 1. 指令替换 mov eax, 0 → xor eax, eax ; 2. 花指令插入 nop jmp $+1 db 0xE8 ; 无效字节 add eax, ebx ; 3. 寄存器替换 mov eax, 5 → mov ebx, 5 mov eax, ebx ; 4. 常量加密 mov eax, 100 → mov eax, 0x12345678 xor eax, 0x123456DC ; = 100
强度:★★★★
性能损耗:1.5-3倍
导入表加密
导入表保护 (Import Protection)
原理:隐藏真实的API调用
转换示例:
; 原始导入表 IMPORT TABLE: kernel32.dll - CreateFileA - ReadFile ; ↓ 保护后 ↓ IMPORT TABLE: kernel32.dll - LoadLibraryA ; 只保留最基础的 ; 运行时动态获取 char* encrypted = "x3Fx12x7A..."; // 加密的 "CreateFileA" decrypt(encrypted); pCreateFile = GetProcAddress(LoadLibrary("kernel32"), decrypted); pCreateFile(...); // 调用
代码对比:
// 原始 MessageBoxA(NULL, "Hello", "Test", 0); // 保护后(伪代码) typedef int (WINAPI *pMsgBox)(HWND, LPCSTR, LPCSTR, UINT); pMsgBox MyMsgBox = vmp_get_api(0x1A3F); // 内部索引 MyMsgBox(NULL, vmp_str(0x2B4C), vmp_str(0x3D5E), 0);
强度:★★★★
绕过难度:中等(可API Hook监控)
call后面是加密的iat表

字符串加密 (String Encryption)
原理:加密所有字符串常量
示例:
// 原始 const char* key = "MySecretKey123"; if (strcmp(input, key) == 0) { ... } // ↓ 加密后 ↓ // 数据段存储 .data encrypted_str db 0xA3,0x7F,0x2B,0x9C,... ; 加密的字符串 // 代码中 char* key = vmp_decrypt_string(0x004050A0); if (strcmp(input, key) == 0) { vmp_free_string(key); // 用完立即清除 }
反调试 (Anti-Debug)
技术清单:
// 1. IsDebuggerPresent if (IsDebuggerPresent()) { ExitProcess(0); } // 2. PEB检测 bool CheckDebug() { __asm { mov eax, fs:[0x30] // PEB movzx eax, byte ptr [eax+2] // BeingDebugged test eax, eax } } // 3. NtQueryInformationProcess BOOL isDebug = FALSE; NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &isDebug, sizeof(BOOL), NULL); // 4. 时间检测 DWORD t1 = GetTickCount(); // 一些代码 DWORD t2 = GetTickCount(); if (t2 - t1 > 1000) { /* 被调试 */ } // 5. 硬件断点检测 CONTEXT ctx = {0}; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; GetThreadContext(GetCurrentThread(), &ctx); if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) { /* 检测到硬件断点 */ } // 6. INT 3检测 __try { __asm int 3 } __except(EXCEPTION_EXECUTE_HANDLER) { // 正常程序会走这里 } // 7. 检测调试器窗口 if (FindWindow("OLLYDBG", NULL) || FindWindow("WinDbgFrameClass", NULL)) { ExitProcess(0); }
反虚拟机 (Anti-VM)
检测技术:
// 1. CPUID检测 void CheckVM() { int cpuInfo[4]; __cpuid(cpuInfo, 1); if (cpuInfo[2] & (1 << 31)) { // Hypervisor bit set ExitProcess(0); } } // 2. 特征文件检测 if (PathFileExists("C:\Windows\System32\drivers\vmmouse.sys") || PathFileExists("C:\Windows\System32\drivers\vmhgfs.sys")) { // VMware检测 } // 3. 注册表检测 HKEY hKey; if (RegOpenKey(HKEY_LOCAL_MACHINE, "SOFTWARE\VMware, Inc.\VMware Tools", &hKey) == ERROR_SUCCESS) { // VMware } // 4. MAC地址检测 // VMware前缀: 00:05:69, 00:0C:29, 00:50:56 // VirtualBox前缀: 08:00:27 // 5. 指令时间检测 DWORD t1 = __rdtsc(); // 执行一些指令 DWORD t2 = __rdtsc(); if (t2 - t1 > threshold) { /* VM环境 */ }
强度:★★★
绕过:修改VM配置
内存保护 (Memory Protection)
原理:检测内存完整性
// 1. CRC校验 DWORD original_crc = 0x12345678; DWORD current_crc = calc_crc32(code_section, size); if (current_crc != original_crc) { ExitProcess(0); // 代码被修改 } // 2. 定期校验 void __stdcall CheckThread(LPVOID param) { while (true) { Sleep(1000); if (!verify_memory()) { TerminateProcess(GetCurrentProcess(), 0); } } } // 3. 页面保护 VirtualProtect(code_section, size, PAGE_EXECUTE_READ, &old); // 任何写入会触发异常
资源加密 (Resource Encryption)
原理:加密PE资源段
// 原始 HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(101), RT_BITMAP); HGLOBAL hData = LoadResource(NULL, hRes); void* pData = LockResource(hData); // ↓ 保护后 ↓ // 资源段全部加密 .rsrc section: [encrypted data] // 运行时 HRSRC hRes = FindResource(...); // VMProtect拦截LoadResource HGLOBAL hData = LoadResource(...); // 内部解密 void* pData = LockResource(hData); // 返回解密数据
强度:★★★
效果:防止资源提取工具直接读取
应用VMProtect后(简化示意)
// 编译后的保护代码(逆向视角) .text:00401000 ; VMP Entry .text:00401000 push ebp .text:00401001 call vmp_init_2FA3C .text:00401006 jmp vm_dispatcher_1 // 原始的main函数代码已经不存在 // 被转换为: .vmp0:00501000 db 0x4A, 0x8F, 0x23, ... ; VM字节码 .vmp0:00501100 db 0x7C, 0x12, 0xAB, ... .vmp1:00502000 ; VM解释器 .vmp1:00502000 vm_handler_0: .vmp1:00502000 mov al, [esi] .vmp1:00502002 inc esi .vmp1:00502003 movzx eax, al .vmp1:00502006 jmp [vm_table + eax*4] // 字符串也被加密 .data:00601000 encrypted_str1 db 0xA4,0x7F,0x9C,... .data:00601020 encrypted_str2 db 0x3E,0x12,0x88,... // 导入表被隐藏 .idata:00701000 ; 只剩下 .idata:00701000 dd offset LoadLibraryA .idata:00701004 dd offset GetProcAddress
保护强度对比表
| 方法 | 反静态分析 | 反动态分析 | 性能影响 | 推荐场景 |
|---|---|---|---|---|
| Virtualization | ★★★★★ | ★★★★★ | -90% | 核心算法 |
| Mutation | ★★★★ | ★★★ | -30% | 一般函数 |
| Import Protection | ★★★★ | ★★ | -5% | 全局开启 |
| String Encryption | ★★★ | ★ | -5% | 敏感字符串 |
| Anti-Debug | ★ | ★★★★ | -1% | 全局开启 |
| Anti-VM | ★ | ★★★ | -1% | 可选 |
| Memory Protection | ★★ | ★★★★ | -10% | 全局开启 |
| Packing | ★★ | ★ | +5% | 减小体积 |
虚拟指令、虚拟栈、虚拟寄存器。

msdn。开发文档
<windows程序设计>
<windows核心编程>
<win32汇编语言程序设计>
PE 文件结构图例
+-----------------------------------+ <-- 文件开始 (Offset 0) | IMAGE_DOS_HEADER | |-----------------------------------| | - e_magic: "MZ" (0x5A4D) | <- DOS 可执行文件签名 | - e_lfanew: 指向 NT 头部的偏移量 | <- 这是通往 PE 头的关键指针! +-----------------------------------+ | DOS Stub Program | <- 一小段 DOS 程序,通常显示 "This program cannot be run in DOS mode." +-----------------------------------+ <-- e_lfanew 指向的位置 | IMAGE_NT_HEADERS | | +-----------------------------+ | | | Signature | | | | - "PE " (0x00004550) | | <- PE 文件签名 | +-----------------------------+ | | | IMAGE_FILE_HEADER | | | |-----------------------------| | | | - Machine: (e.g., 0x8664) | | <- 目标 CPU 架构 (如 x64) | | - NumberOfSections: | | <- 后面跟着的节区数量 | | - TimeDateStamp: | | <- 链接时间戳 | | - SizeOfOptionalHeader: | | <- 下一个头的大小 | | - Characteristics: | | <- 文件属性 (如 DLL, Executable) | +-----------------------------+ | | | IMAGE_OPTIONAL_HEADER | | <- 对于操作系统加载器至关重要 | |-----------------------------| | | | - Magic: (e.g., 0x20B) | | <- 标识 PE32(0x10B) 或 PE32+(0x20B) | | - AddressOfEntryPoint: | | <- 程序执行入口 RVA | | - ImageBase: | | <- 映像的首选加载地址 | | - SectionAlignment: | | <- 内存中节区的对齐方式 | | - FileAlignment: | | <- 文件中节区的对齐方式 | | - SizeOfImage: | | <- 内存中整个映像的大小 | | - SizeOfHeaders: | | <- 所有头部的总大小 | | - Subsystem: (e.g., GUI/CUI) | | <- 要求子系统 (如 Windows GUI) | | - NumberOfRvaAndSizes: | | <- 数据目录项的数量 | |-----------------------------| | | | IMAGE_DATA_DIRECTORY | | <- 一个结构体数组 | | [0] Export Directory | | | | [1] Import Directory | | <- 指向导入函数信息 | | [2] Resource Directory | | <- 指向资源 (图标、字符串等) | | [3] Exception Directory | | | | ... (共 16 个) ... | | | +-----------------------------+ | +-----------------------------------+ | IMAGE_SECTION_HEADER[0] | <- 第一个节区头 (如 .text) | - Name: ".text " | <- 节区名称 | - VirtualAddress: | <- 内存中的 RVA | - SizeOfRawData: | <- 在文件中的大小 | - PointerToRawData: | <- 在文件中的偏移 | - Characteristics: | <- 节区属性 (如 可执行、可读) +-----------------------------------+ | IMAGE_SECTION_HEADER[1] | <- 第二个节区头 (如 .data) | - Name: ".data " | | - ... | +-----------------------------------+ | ... | <- 其他节区头 (共 NumberOfSections 个) +-----------------------------------+ <-- SizeOfHeaders 标记的头部结束位置 | 节区数据开始 | | | | .text 节区原始数据 | <- 文件偏移由 PointerToRawData 指定 | (存放代码) | | | | .data 节区原始数据 | <- 文件偏移由 PointerToRawData 指定 | (存放全局变量) | | | | .rsrc 节区原始数据 | | (存放资源) | | | | ... | +-----------------------------------+ <-- 文件结束
各组成部分的简明解释
-
IMAGE_DOS_HEADER (DOS 头)
-
目的:为了保持与古老 DOS 系统的兼容性。
-
关键成员:
e_lfanew字段,它包含了指向真正的 PE 头 (IMAGE_NT_HEADERS) 的文件偏移量。
-
-
IMAGE_NT_HEADERS (NT 头)
-
目的:PE 文件的正式入口和核心描述符。
-
包含三部分:
-
Signature:一个 "PE " 的签名,标识这是一个 PE 文件。
-
IMAGE_FILE_HEADER (文件头):描述了文件的全局属性,如目标机器类型、节区数量、创建时间等。
-
IMAGE_OPTIONAL_HEADER (可选头):虽然叫"可选",但对于可执行文件是必需的。它包含了程序加载和运行所需的关键信息。
-
-
-
IMAGE_OPTIONAL_HEADER (可选头)
-
目的:为操作系统加载器提供如何准备和执行程序的信息。
-
关键成员:入口点地址、映像基址、内存/文件对齐值、子系统等。
-
它末尾的 IMAGE_DATA_DIRECTORY (数据目录) 是一个非常重要的表格,它指出了其他重要数据结构(如导入表、导出表、资源表)在文件中的位置和大小。
-
-
IMAGE_DATA_DIRECTORY (数据目录)
-
目的:作为指向其他重要数据的“目录”或“索引”。
-
结构:一个由16个相同结构组成的数组。每个结构包含一个 RVA(相对虚拟地址) 和 Size。
-
例如:第二个条目(索引1)是 Import Directory,加载器通过它找到所有需要从其他DLL导入的函数列表。
-
-
IMAGE_SECTION_HEADER (节区头)
-
目的:描述文件中的各个“节区”。节区是实际存储代码、数据、资源等内容的部分。
-
数量:由
IMAGE_FILE_HEADER中的NumberOfSections指定。 -
关键成员:
-
Name:节区名称(如.text,.data,.rdata)。 -
VirtualAddress:该节区加载到内存后的 RVA。 -
PointerToRawData:该节区在磁盘文件中的原始数据偏移。 -
Characteristics:节区属性(如可读、可写、可执行)。
-
-
总结与流程
操作系统加载一个 PE 文件的简化流程如下:
-
读取
IMAGE_DOS_HEADER,找到e_lfanew。 -
跳到
e_lfanew位置,验证 "PE" 签名,读取IMAGE_NT_HEADERS。 -
从
IMAGE_FILE_HEADER知道有多少个节区。 -
从
IMAGE_OPTIONAL_HEADER获取关键信息(如入口点、映像大小、数据目录)。 -
遍历
IMAGE_SECTION_HEADER数组,了解每个节区在文件和内存中的映射关系。 -
根据节区头的信息,将文件的各个节区(代码、数据等)映射到内存的相应位置。
-
通过
IMAGE_DATA_DIRECTORY找到导入表,解析并填充所有需要的外部函数地址。 -
最后,跳转到
AddressOfEntryPoint指向的地址,程序开始执行。
这个结构确保了 PE 文件既能在磁盘上高效存储,又能在内存中正确加载和执行。
PE文件头结构图解 + 白话文秒懂
📊 完整结构总览
┌─────────────────────────────────────────────────────────┐ │ DOS Header (64字节) │ ← 开头的"MZ"标记 │ "这是个老式DOS程序" 的伪装外壳 │ ├─────────────────────────────────────────────────────────┤ │ DOS Stub (可变) │ │ "This program cannot be run in DOS mode" │ ← 在DOS下运行会看到的提示 ├─────────────────────────────────────────────────────────┤ │ PE Signature (4字节) │ │ "PE " │ ← 真正的PE文件标记 ├─────────────────────────────────────────────────────────┤ │ File Header (20字节) │ │ 记录机器类型、节数量、时间戳等基本信息 │ ├─────────────────────────────────────────────────────────┤ │ Optional Header (224/240字节) │ │ 记录程序入口点、内存布局、导入导出等关键信息 │ ├─────────────────────────────────────────────────────────┤ │ Section Table (每节40字节) │ │ .text .data .rdata .rsrc 等节的"目录" │ ├─────────────────────────────────────────────────────────┤ │ │ │ Section 1 (.text) │ ← 代码区 │ 你写的代码在这里 │ │ │ ├─────────────────────────────────────────────────────────┤ │ Section 2 (.data) │ ← 数据区 │ 全局变量在这里 │ ├─────────────────────────────────────────────────────────┤ │ Section 3 (.rsrc) │ ← 资源区 │ 图标、对话框、字符串在这里 │ └─────────────────────────────────────────────────────────┘
🔍 详细结构拆解
1️⃣ DOS Header (IMAGE_DOS_HEADER)
偏移 大小 字段名 白话文解释 +0x00 2字节 e_magic "MZ" 标记(0x5A4D)—— 所有PE文件必须以这两个字母开头 +0x02 58字节 [其他DOS字段] 基本没用,为了兼容古董DOS系统 +0x3C 4字节 e_lfanew **超重要!** 指向真正的PE头在哪里
白话:
这是个"假门面",为了让Windows程序能在老DOS系统上显示错误提示,而不是直接崩溃。
最重要的是最后那个e_lfanew,它告诉系统:"真正的PE头在文件偏移XXX处"。
2️⃣ PE Signature (4字节)
+0x00 4字节 Signature "PE " (0x50450000)
白话:
就像盖了个"认证章",证明"我是正宗的Windows程序"。
3️⃣ File Header (IMAGE_FILE_HEADER - 20字节)
偏移 大小 字段名 白话文解释 +0x00 2字节 Machine CPU类型(0x14C=x86, 0x8664=x64) +0x02 2字节 NumberOfSections 这个程序有几个"节"(通常3-6个) +0x04 4字节 TimeDateStamp 程序编译的时间戳 +0x08 4字节 PointerToSymbolTable 调试符号表位置(发布版通常是0) +0x0C 4字节 NumberOfSymbols 符号数量 +0x10 2字节 SizeOfOptionalHeader 下一个头的大小(32位=224, 64位=240) +0x12 2字节 Characteristics 文件属性标志
白话:
这是"身份证"部分:
- 告诉系统这是32位还是64位程序
- 有几个代码/数据分区
- 什么时候编译的
- 是个EXE还是DLL(Characteristics字段)
Characteristics 常见标志:
0x0002 IMAGE_FILE_EXECUTABLE_IMAGE 可执行文件(不是obj) 0x0100 IMAGE_FILE_32BIT_MACHINE 32位程序 0x2000 IMAGE_FILE_DLL 这是个DLL文件
4️⃣ Optional Header (IMAGE_OPTIONAL_HEADER - 最重要!)
标准字段部分
偏移 大小 字段名 白话文解释 +0x00 2字节 Magic 0x10B(32位) / 0x20B(64位) +0x02 1字节 MajorLinkerVersion 编译器版本 +0x03 1字节 MinorLinkerVersion +0x04 4字节 SizeOfCode 代码段总大小 +0x08 4字节 SizeOfInitializedData 已初始化数据大小 +0x0C 4字节 SizeOfUninitializedData未初始化数据大小 +0x10 4字节 AddressOfEntryPoint **入口点!程序从这里开始执行** +0x14 4字节 BaseOfCode 代码段起始地址 +0x18 4字节 BaseOfData 数据段起始地址(仅32位)
Windows专用字段部分
偏移 大小 字段名 白话文解释 +0x1C 4/8字节 ImageBase 程序希望加载到内存的哪个地址 +0x20 4字节 SectionAlignment 节在内存中的对齐单位(通常0x1000=4KB) +0x24 4字节 FileAlignment 节在文件中的对齐单位(通常0x200=512字节) +0x28 8字节 [操作系统版本号] +0x30 8字节 [程序版本号] +0x38 8字节 [子系统版本号] +0x40 4字节 Win32VersionValue 保留(总是0) +0x44 4字节 SizeOfImage 程序加载到内存后的总大小 +0x48 4字节 SizeOfHeaders 所有头的总大小 +0x4C 4字节 CheckSum 校验和(驱动必须正确,普通程序可为0) +0x50 2字节 Subsystem 子系统类型 +0x52 2字节 DllCharacteristics DLL特性标志 +0x54 16字节 [栈/堆大小设置] +0x64 4字节 NumberOfRvaAndSizes 数据目录数量(通常是16)
Subsystem 子系统:
1 = Native(驱动程序) 2 = GUI(窗口程序) 3 = CUI(控制台程序,黑框框)
DllCharacteristics 重要标志:
0x0040 DYNAMIC_BASE 支持ASLR(地址随机化) 0x0100 NX_COMPAT 支持DEP(数据执行保护) 0x0400 NO_SEH 不使用SEH异常处理 0x8000 TERMINAL_SERVER_AWARE 终端服务器感知
5️⃣ 数据目录表 (Data Directory - 16个条目)
索引 名称 作用 [0] Export Table 导出表(DLL导出的函数列表) [1] Import Table 导入表(程序要用哪些DLL的哪些函数) [2] Resource Table 资源表(图标、字符串、对话框) [3] Exception Table 异常处理表 [4] Certificate Table 数字签名 [5] Base Relocation Table 重定位表(修正地址用) [6] Debug 调试信息 [7] Architecture 架构特定数据 [8] Global Ptr 全局指针寄存器 [9] TLS Table 线程局部存储 [10] Load Config Table 加载配置 [11] Bound Import 绑定导入 [12] IAT 导入地址表(最常被破解者关注!) [13] Delay Import Descriptor 延迟导入 [14] CLR Runtime Header .NET程序专用 [15] Reserved 保留
每个条目结构:
+0x00 4字节 VirtualAddress 数据在内存中的RVA +0x04 4字节 Size 数据的大小
6️⃣ 节表 (Section Table - 每节40字节)
偏移 大小 字段名 白话文解释 +0x00 8字节 Name 节名称(如".text"、".data") +0x08 4字节 VirtualSize 在内存中的实际大小 +0x0C 4字节 VirtualAddress 在内存中的起始地址(RVA) +0x10 4字节 SizeOfRawData 在文件中的大小 +0x14 4字节 PointerToRawData 在文件中的偏移 +0x18 12字节 [重定位/行号信息] 通常为0 +0x24 4字节 Characteristics 节的属性(可读/可写/可执行)
常见节名称:
.text 代码段(你的程序逻辑) 可读+可执行 .data 已初始化数据(全局变量) 可读+可写 .rdata 只读数据(常量字符串) 只读 .bss 未初始化数据 可读+可写 .rsrc 资源(图标、菜单、字符串) 只读 .reloc 重定位信息 只读 .idata 导入表(需要的DLL函数) 可读+可写 .edata 导出表(DLL导出的函数) 只读
Characteristics 节属性:
0x00000020 CODE 包含代码 0x00000040 INITIALIZED_DATA 包含已初始化数据 0x00000080 UNINITIALIZED_DATA 包含未初始化数据 0x20000000 EXECUTE 可执行 0x40000000 READ 可读 0x80000000 WRITE 可写
🎯 关键概念白话解释
RVA (Relative Virtual Address) - 相对虚拟地址
假设程序被加载到内存地址 0x00400000 某个函数的RVA是 0x1000 那么这个函数的实际内存地址 = 0x00400000 + 0x1000 = 0x00401000
文件偏移 vs 内存地址
文件偏移:在硬盘上的.exe文件中的位置 内存地址:程序运行时在内存中的位置 需要通过节表来转换!
对齐 (Alignment)
FileAlignment = 0x200 (512字节) → 文件中每个节的起始位置必须是512的倍数 SectionAlignment = 0x1000 (4096字节) → 内存中每个节的起始位置必须是4KB的倍数
🛠️ 实战:用十六进制编辑器看PE头
偏移 十六进制 解释 00000000: 4D 5A 90 00 03 00 00 00... MZ = DOS头开始 0000003C: E0 00 00 00 e_lfanew = 0xE0 000000E0: 50 45 00 00 PE签名 000000E4: 4C 01 06 00 Machine=0x014C(x86), Sections=6 000000F8: 0B 01 Magic=0x10B(32位) 00000100: 00 10 00 00 AddressOfEntryPoint=0x1000
📝 总结:PE头三句话秒懂
- DOS头:"我伪装成DOS程序,但真正的内容在后面"
- PE头:"我是Windows程序,32位/64位,从地址XXX开始执行"
- 节表:"我的代码在.text节,数据在.data节,资源在.rsrc节"
🔥 最关心的地方
✅ AddressOfEntryPoint → 程序从哪里开始跑 ✅ Import Table (IAT) → 调用了哪些关键API(MessageBox? RegCreateKey?) ✅ .text 节 → 注册验证代码藏在这里 ✅ Characteristics → 能不能改这个节的数据 ✅ Resource Table → 修改图标、字符串、对话框
希望这个图解能帮你快速理解PE结构!如果要深入学习,推荐工具:
- CFF Explorer (查看PE结构最直观)
- PE-bear
- 010 Editor (配合PE模板)
- windowspe 权威指南
了解所有编译器和反汇编特征
了解vmp入口,区段,反汇编特征
看特征的方法主要有两个,一个是入口代码的在节区中的位置,入口代码中包含的指令和调用的外部接口
vc6特征和易语言








64-ia-32-architectures-software-developer-manualintel开发手册
<加密与解密>
揭秘><C++反汇编与逆向分析技术

Stud PE
exeinfoExeinfo PE是一款专业的程序查壳工具
LordPE
PE Tools 1.9
WinHex
UltraEdit
Beyond Compare数据比较工具。
iat修复工具
ImportREC
Scylla
Universal lmport Fixer
Imports Fixer
vmp指令转化器软件
将汇编转vmp自定义指令,在转化为真实指令集
好的,我们来详细说明如何尝试使用 VMPDump 来处理一个被 VMProtect 2 加密的程序。
但在开始之前,我必须先强调一个至关重要的事实,这能为你节省大量的时间和精力:
🛑 第一部分:必读的现实情况与重要提醒
-
VMPDump 并非万能神药:VMPDump 是一个比较早期的、针对特定旧版本 VMProtect 的自动化分析工具。它无法真正地“还原所有代码”。它的主要功能是尝试自动完成“寻找OEP”和“Dump内存”这两个初级阶段的工作。
-
它不能“去虚拟化”:VMPDump 绝对无法将 VMProtect 的核心——虚拟化字节码(Bytecode)——翻译回原始的 x86/x64 汇编代码。被虚拟化的关键代码(如注册算法)在 VMPDump 处理后,依然是虚拟化的,你用IDA打开看仍然是一团乱麻。
-
成功率极低:对于现代版本的 VMProtect 2 (例如 2.13 及以后) 或者即使是旧版本但开启了高强度保护选项的程序,VMPDump 的成功率趋近于零。它很可能会失败、卡死、或者生成一个完全无法运行的垃圾文件。
-
它的真正作用:在极少数情况下(例如面对非常古老的、保护设置很弱的VMP版本),它可能可以帮你快速脱掉外层的壳,省去你手动寻找OEP和Dump的几分钟时间。请把它看作一个“快速尝试的彩票”,而不是一个“解决方案”。
结论: 不要期望 VMPDump 能帮你“一键还原”所有代码。它只是一个辅助性的、成功率很低的入门级工具。
第二部分:准备工作
在你开始之前,请确保你已经准备好以下环境和工具:
- 虚拟机 (VM):强烈建议在 VMware 或 VirtualBox 等虚拟机中进行所有操作。这可以防止目标程序或工具损坏你的物理机系统。
- 目标程序:你想要分析的那个被VMP2加密的
.exe文件。 - VMPDump 工具:你需要从一些逆向工程论坛或网站下载这个工具。请注意,这些工具可能被杀毒软件报毒,这是因为它们的行为(如注入进程、读取内存)与恶意软件相似。请在虚拟机中谨慎使用。
- 调试器:x64dbg (强烈推荐)。VMPDump 经常需要附加到一个正在运行的进程上,所以你需要先运行目标程序。
- PE 编辑器:CFF Explorer 或 010 Editor。用于后续检查生成的文件是否结构正常。
第三部分:使用 VMPDump 的详细步骤
假设你已经找到了一个可以运行的 VMPDump 版本,并且你的目标程序是一个32位的EXE(VMPDump主要支持32位)。
步骤 1:运行目标程序
首先,双击运行你要分析的那个 .exe 文件。让它正常运行起来,比如显示出主窗口或对话框。
步骤 2:以管理员身份运行 VMPDump
右键点击 VMPDump.exe,选择 “以管理员身份运行”。这能确保它有足够的权限去读取其他进程的内存。
步骤 3:附加到目标进程
- 在 VMPDump 的主界面上,你会看到一个 “...” 按钮,旁边是 "Process" 或 "PID" 字段。
- 点击这个 “...” 按钮,会弹出一个当前正在运行的进程列表。
- 在列表中找到你的目标程序进程(例如
target.exe),选中它,然后点击 “OK” 或 “Select”。
步骤 4:智能扫描 (Smart Scan)
这是 VMPDump 尝试分析虚拟机的关键一步。
- 在界面上找到一个名为 “Smart Scan of Handlers” 的复选框,勾选它。
- 这一步会让 VMPDump 尝试在程序的内存空间中搜索它认为是 VMProtect 虚拟机处理器(VM Handlers)的代码块。这是它最容易失败的地方。
步骤 5:设置 OEP (Original Entry Point)
VMPDump 会尝试自动检测 OEP。
- 自动检测:通常你不需要手动设置 OEP 字段。VMPDump 会利用一些内置的特征码去定位。
- 手动设置:如果自动检测失败,你需要自己通过其他方法(如 x64dbg 的 ESP 定律)找到 OEP 的相对虚拟地址(RVA),然后手动填入 OEP 字段。对于初学者来说,如果 VMPDump 无法自动找到 OEP,基本可以宣告失败了。
步骤 6:执行 Dump(转储)
- 在 VMPDump 界面上,点击 “Unpack” 或 “Dump” 按钮。
- VMPDump 会开始它的工作流程:
- 寻找 OEP。
- 扫描 VM Handlers。
- 转储内存中的所有节区(Sections)。
- 尝试重建导入地址表(IAT)。
- 如果一切顺利(概率很小),它会提示你保存文件。通常会默认保存为
[原文件名]_unpacked.exe。
步骤 7:验证生成的文件
这是最重要的一步,用来判断 VMPDump 是否真的起作用了。
-
尝试运行:双击运行生成的
_unpacked.exe文件。- 直接崩溃? -> 失败。很可能是 IAT 修复失败或代码段Dump不完整。
- 可以运行但功能异常? -> 部分失败。可能某些保护代码没有被移除。
- 可以正常运行? -> 恭喜,你完成了最基础的“脱壳”。但这不代表代码被还原了。
-
使用 PE 工具检查:用 CFF Explorer 打开
_unpacked.exe文件。- 检查 “Import Directory”(导入目录),看看函数列表是否看起来正常,有没有大量的无效或乱码条目。
- 检查节表(Section Headers),看看
.text,.data等节的大小和权限是否合理。
-
使用 IDA Pro 分析:这是最终的检验。用 IDA Pro 打开
_unpacked.exe文件。- 找到你关心的关键功能(比如注册按钮的点击事件)。
- 按 F5 尝试反编译。
- 结果是什么?
- 如果依然是无法反编译、跳转关系混乱、充满了奇怪计算的代码 -> 这就证明了 VMPDump 没有去虚拟化。它只是把内存里的东西原封不动地Dump了出来。关键逻辑依然被VM保护着。
- 如果能看到清晰的C代码逻辑 -> 这种情况几乎不可能发生,除非你的目标程序用的不是VMP的虚拟化保护。
第四部分:当 VMPDump 失败时(99% 的情况)
你很可能会遇到以下情况,这都意味着 VMPDump 对你的目标无效:
- VMPDump 无法找到 OEP 或 Handlers:直接报错,提示分析失败。
- VMPDump 在扫描过程中卡死或崩溃:说明 VMP 的反分析机制触发了。
- 生成的 Dump 文件无法运行:最常见的结果,说明 Dump 或修复过程出错了。
- 生成的 Dump 文件虽然能运行,但关键功能(如注册)依然被保护:证明了核心逻辑仍是虚拟化的。
当 VMPDump 失败后,你必须回到正统的、手动的逆向工程流程上来:
- 手动寻找 OEP:使用 x64dbg 和 ESP 定律。
- 手动 Dump:使用 Scylla 插件,在 OEP 处进行内存转储。
- 手动修复 IAT:使用 Scylla 的 IAT 搜索和修复功能。
- 手动去虚拟化:这才是真正的挑战。在 IDA 和 x64dbg 中,花费数天、数周甚至数月的时间去:
- 分析 VM Entry 和 Dispatcher。
- 逐个逆向 VM Handlers 的功能。
- 编写脚本翻译字节码。
- 找到关键逻辑并进行 Patch。
总结
使用 VMPDump 是一个“死马当活马医”的尝试。你可以按照上述步骤操作一遍,体验一下自动化工具的工作流程。但这趟旅程的终点,大概率是让你明白:没有捷径可走,对抗高强度的软件保护,唯有扎实的逆向工程基础和巨大的耐心。
好的,我们来非常详细地拆解和说明 VMProtect 2 的加密功能和脱壳流程。这是一个极其复杂但非常有趣的技术话题。
⚠️ 郑重声明:本内容仅用于安全研究、技术学习和防御策略探讨。对商业软件进行逆向工程、破解和传播是违法行为,可能导致严重的法律后果。请在法律允许的范围内使用这些知识,并尊重软件开发者的劳动成果。
Part 1: VMProtect 2 的核心加密功能(它做了什么?)
VMProtect (VMP) 不是一个简单的“壳”,它是一个深度代码混淆和虚拟化系统。其核心思想是让你写的代码,在你的电脑上都看不懂。
1. 虚拟化保护 (Virtualization) - VMP的王牌
这是VMP最强大、最核心的功能。它能将你指定的代码片段从标准的 x86/x64 汇编指令,转换成一种自定义的、每次编译都不一样的字节码(Bytecode)。
工作原理图解
你的原始代码 (x86汇编): ┌──────────────────────────────┐ │ MOV EAX, [EBP+8] ; 取一个参数 │ │ ADD EAX, 10 ; 加上 10 │ │ CMP EAX, 1234 ; 和 1234 比较 │ │ JNE loc_failed ; 不相等就跳走 │ │ CALL CheckLicense ; 相等就调用授权 │ └──────────────────────────────┘ ↓ 经过 VMProtect 处理后... ┌──────────────────────────────────────────────────────┐ │ VM Entry(虚拟机入口) │ │ 功能: 保存当前CPU的真实寄存器状态 (PUSHAD/PUSHFD), │ │ 初始化虚拟机的环境(虚拟寄存器、虚拟栈指针等)。 │ ├──────────────────────────────────────────────────────┤ │ Bytecode(自定义的字节码) │ │ 内容: 0x47 0x12 0x89 0xAA 0x3F 0x91 0x05 ... │ │ 特点: 这串字节码不是x86指令,只有VMP自己生成的虚拟机能看懂。│ ├──────────────────────────────────────────────────────┤ │ VM Dispatcher(调度器 / 指令分发器) │ │ 功能: 这是一个循环,不断地读取下一个字节码,然后根据这个 │ │ 字节码的值,跳转到对应的处理器(Handler)去执行。 │ ├──────────────────────────────────────────────────────┤ │ VM Handlers(处理器集合) │ │ 功能: 每一个Handler都是一小段真实的x86代码,负责实现 │ │ 一个字节码的功能。例如: │ │ - Handler_0x47: 实现“虚拟加法”功能。 │ │ - Handler_0x12: 实现“虚拟数据加载”功能。 │ │ - Handler_0x89: 实现“虚拟比较”功能。 │ │ 特点: 这些Handler之间互相穿插,充满了垃圾指令,让你难以分析。│ ├──────────────────────────────────────────────────────┤ │ VM Exit(虚拟机退出) │ │ 功能: 执行完虚拟化代码后,从虚拟机环境退出,恢复之前保存的│ │ 真实CPU寄存器状态,然后返回到正常的x86代码继续执行。│ └──────────────────────────────────────────────────────┘
为什么这让破解变得极其困难?
- 独一无二的指令集:每个被VMP保护的程序,其内部的虚拟机、字节码、Handler都是随机生成的。你在A程序上分析得到的经验,完全无法用于B程序。
- 分析成本极高:你无法再用IDA Pro的F5功能直接看到C代码。你必须先逆向分析出这套虚拟机的几十甚至上百个Handler的功能,才能像翻译密码一样,一点点地把字节码“翻译”回原始逻辑。这个过程可能需要数周甚至数月。
- 高度混淆:一个简单的
ADD EAX, 10指令,在VM里可能被分解成V_PUSH 10->V_PUSH EAX->V_ADD->V_POP EAX等多条虚拟指令,而每个虚拟指令的Handler本身又是高度混淆的真实x86代码。
2. 变异混淆 (Mutation)
即使是不被虚拟化的代码,VMP也会对其进行“变异”,让代码变得臃肿、难以阅读。
- 等价指令替换:
ADD EAX, 1会被替换成SUB EAX, -1或LEA EAX, [EAX+1]。 - 指令膨胀:
MOV EAX, EBX会变成PUSH EBX; POP EAX。 - 插入垃圾代码:在有效指令之间插入大量无用的、迷惑性的指令,这些指令不影响程序执行结果,但会严重干扰静态分析。
- 控制流平坦化:将正常的
if-else或switch-case结构,变成一个巨大的while循环和状态机,每次都通过一个调度器来决定下一步执行哪个代码块,打乱原始的逻辑顺序。
3. 加壳与压缩 (Packing)
这是VMP的基础功能。它会将程序的原始代码段(.text)、数据段(.data)等进行加密和压缩,然后包裹在一个“加载器”(Loader)外壳里。
- 运行时解密:程序启动时,首先执行的是VMP的Loader。它会在内存中解密、解压原始的代码和数据。
- 入口点修改:程序的入口点(OEP, Original Entry Point)被隐藏起来,指向了VMP的Loader。
- 导入表保护:程序调用的Windows API(如
MessageBoxA)地址不再静态存储在导入地址表(IAT)中,而是在运行时动态获取,这使得分析者很难直接看到程序调用了哪些关键函数。
4. 反调试与反分析 (Anti-Debug)
VMP内置了海量的反调试技术,像地雷一样遍布在代码中,一旦发现自己被调试器(如x64dbg, IDA Pro)附加,就会改变行为。
- 调试器检测:通过
IsDebuggerPresent,CheckRemoteDebuggerPresent等API检测。 - 时间检测:使用
RDTSC指令或GetTickCountAPI,通过计算两段代码执行的时间差来判断是否在单步调试(单步执行会花费更长的时间)。 - 硬件断点检测:检查
DR0-DR7调试寄存器。 - 异常处理技巧:故意触发一个异常,然后用
SEH(结构化异常处理)来捕获并执行正常代码。如果调试器干预了异常处理流程,程序就会崩溃。 - 完整性检查:在运行时计算自身代码的校验和(CRC),如果发现代码被修改(比如被打了补丁),就会退出。
Part 2: VMProtect 2 的脱壳流程(如何应对?)
“脱壳”对于VMP来说是一个非常宽泛的概念。完整的流程极其复杂,通常分为以下几个阶段。
阶段一:准备工作与环境配置
这是所有工作的基础。你需要一个“干净”且“武装到牙齿”的分析环境。
-
工具清单:
- 调试器: x64dbg (主流选择,插件丰富)
- 反汇编器: IDA Pro 7.x (静态分析的王者)
- 反反调试插件: ScyllaHide (x64dbg插件,用于对抗各种反调试技术)
- Dump工具: Scylla (集成在ScyllaHide中,用于dump内存和修复IAT)
- PE工具: CFF Explorer 或 010 Editor (用于查看和编辑PE文件结构)
-
环境配置:
- 虚拟机:强烈建议在VMware或VirtualBox中进行分析,防止搞垮物理机。
- 配置ScyllaHide:在x64dbg中加载ScyllaHide插件,并尽可能多地勾选对抗选项,如
NtQueryInformationProcess,GetTickCount等。
阶段二:寻找OEP (Original Entry Point - 真实入口点)
由于VMP加了壳,第一步就是找到VMP的Loader执行完毕,即将跳转到程序原始代码的那个点(OEP)。
常用方法:ESP定律
- 用x64dbg加载目标程序,程序会停在系统断点。
- 在x64dbg的命令行输入
bp GetModuleHandleA,然后按F9运行。程序会在加载系统DLL时断下。 - 找到ESP寄存器的值,右键 -> 在内存窗口中转到。
- 在内存窗口中,选中ESP指向的前4个字节,右键 -> 断点 -> 硬件, 访问 (Dword)。
- 按F9继续运行。程序会在VMP的壳代码即将
RET或JMP到OEP之前,访问这个栈地址时断下。 - 此时,单步执行(F7/F8),很大概率就会跳转到OEP。
如何判断找到了OEP?
- 代码看起来像是正常的函数开头(如
PUSH EBP; MOV EBP, ESP)。 - 可以看到清晰的API调用。
- IDA Pro能够很好地分析这部分代码。
阶段三:Dump内存镜像
在OEP处,程序的代码和数据已经在内存中解密,此时需要将它们从内存中“倒”出来,存成一个新的EXE文件。
- 在x64dbg中,停在OEP处。
- 打开插件菜单 -> Scylla。
- 在Scylla窗口中,确保进程已附加,OEP地址已自动填好(如果不对,手动修改为OEP的RVA)。
- 点击 "IAT Autosearch" 按钮,Scylla会自动寻找并定位导入地址表。
- 点击 "Get Imports" 获取所有导入函数。
- 点击 "Dump" 将内存转储到文件(例如
dumped.exe)。 - 点击 "Fix Dump",选择刚才的
dumped.exe,Scylla会尝试修复导入表,并生成一个新文件(例如dumped_SCY.exe)。
阶段四:去虚拟化 (De-virtualization) - 最核心、最艰难的一步
仅仅Dump出文件是远远不够的,因为关键逻辑(如注册码验证)仍然是被虚拟化的字节码。去虚拟化的目标就是理解虚拟机的设计并还原原始逻辑。
-
识别VM Entry:在dump出的文件中,用IDA Pro打开。找到调用虚拟化代码的地方。通常这个地方会有一系列
PUSH指令(保存环境),然后是一个JMP到一个非常复杂混乱的区域,这个区域就是VM Entry。 -
分析VM架构:这是纯粹的逆向工程体力活。
- 定位Dispatcher:VM Entry之后,你会找到一个核心的指令分发器。它通常是一个循环,根据字节码的值跳转到不同的Handler。
- 分析Handlers:以Dispatcher为中心,逐个分析它跳转到的每一个Handler。
- 在x64dbg中对某个Handler下断点,观察它执行前后对虚拟环境(通常在栈上分配的一块内存)做了什么修改。
- 例如,你发现某个Handler总是把虚拟栈顶的两个值相加,那么你就可以把它标记为
V_ADD。 - 另一个Handler可能是从某个地方加载一个常量,你可以标记为
V_LOAD_CONST。
- 重复此过程:你需要分析出尽可能多的Handler的功能,为它们命名。这个过程极其枯燥,可能需要分析几十到上百个Handlers。
-
提取并反编译字节码:
- 当你大致搞清楚了虚拟机的指令集后,就可以在x64dbg中dump出被虚拟化的那段字节码。
- 编写一个简单的脚本(如Python),根据你分析出的Handler功能,将字节码“翻译”成可读的伪代码。
Python# 伪代码示例 bytecode = [0x10, 0x04, 0x12, 0x34, 0x56, 0x78, 0x25, 0x30, 0x01] # 翻译后可能得到 # V_LOAD_CONST 0x12345678 (指令0x10) # V_CMP_WITH_INPUT (指令0x25) # V_JNE_FAIL (指令0x30) -
还原逻辑并Patch:
- 从翻译出的伪代码中,你就能看懂原始的验证逻辑了。
- 暴力破解:找到关键的跳转,例如
V_JNE_FAIL,然后在实现这个虚拟跳转的真实x64代码处,将其修改(如JNE改成JMP或NOP),从而绕过验证。这通常是最高效的方法。 - 算法还原:如果你的目标是写注册机,那就需要完全逆向整个算法,这难度极高。
阶段五:修复与清理
- 修复其他VMP“陷阱”:如完整性检查(CRC Check)。你需要找到计算CRC的地方,然后将其NOP掉,或者在Patch后重新计算正确的CRC值并填回去。
- 测试:运行你最终patch过的文件,确保所有功能正常,并且破解有效。
总结
| 步骤 | 目标 | 难度 | 常用工具 | 关键技巧 |
|---|---|---|---|---|
| 阶段一: 准备 | 搭建稳定的分析环境 | ★☆☆☆☆ | VMware, x64dbg, ScyllaHide | 反反调试配置 |
| 阶段二: 寻OEP | 找到壳执行完后的真实入口 | ★★★☆☆ | x64dbg | ESP定律、硬件断点 |
| 阶段三: Dump | 将内存中的解密代码存盘 | ★★☆☆☆ | Scylla, PE-sieve | 修复IAT |
| 阶段四: 去虚拟化 | 分析VM,还原原始逻辑 | ★★★★★ | IDA Pro, x64dbg | 耐心、毅力、逆向思维 |
| 阶段五: 修复 | 移除其他保护,完成破解 | ★★★☆☆ | x64dbg, 010 Editor | 定位CRC校验 |
结论:对VMP2进行所谓的“暴力破解”几乎是不可能的。真正的“破解”是一个极其漫长且复杂的逆向工程过程,其核心是对 VMProtect的虚拟机进行“去虚拟化”分析。这需要深厚的汇编、操作系统底层知识,以及超乎常人的耐心和毅力。对于大多数情况,分析者会选择最简单的路径,即只分析到能绕过验证的关键跳转,然后进行Patch,而不会尝试去完整地还原所有被虚拟化的代码。
VMProtect 2 加密功能与脱壳流程详解
⚠️ 法律声明:以下内容仅供安全研究和学习用途。未经授权对软件进行逆向工程可能违反法律和软件许可协议。
📌 Part 1: VMProtect 2 的核心加密功能
1.1 三大核心保护技术
┌─────────────────────────────────────────────────────┐ │ VMProtect 2 保护技术架构图 │ ├─────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌───────────┐ │ │ │ Virtualization│ │ Mutation │ │ Packing │ │ │ │ 虚拟化 │ │ 变异混淆 │ │ 加壳 │ │ │ └─────────────┘ └──────────────┘ └───────────┘ │ │ ↓ ↓ ↓ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Anti-Debug & Anti-Dump │ │ │ │ 反调试 & 反转储 │ │ │ └─────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘
1.2 虚拟化保护 (Virtualization) - 最强大的功能
原理图解
原始代码(x86汇编): ┌──────────────────────────────┐ │ MOV EAX, [EBP+8] │ │ ADD EAX, 10 │ │ CMP EAX, 64 │ │ JNE loc_failed │ │ CALL CheckLicense │ └──────────────────────────────┘ ↓ VMProtect 处理 ┌──────────────────────────────────────────────────────┐ │ VM Entry(虚拟机入口) │ │ • 保存真实寄存器状态 │ │ • 初始化虚拟寄存器 │ │ • 加载字节码指针 │ ├──────────────────────────────────────────────────────┤ │ Bytecode(自定义字节码) │ │ 0x47 0x12 0x89 0xAA 0x3F 0x91 0x05 ... │ │ (每次编译都不同!) │ ├──────────────────────────────────────────────────────┤ │ VM Dispatcher(调度器) │ │ while (true) { │ │ opcode = *bytecode_ptr++; │ │ jump handlers[opcode]; // 跳转到处理器 │ │ } │ ├──────────────────────────────────────────────────────┤ │ VM Handlers(处理器集合) │ │ Handler_0x47: // 可能是 PUSH │ │ vm_stack[++sp] = vm_regs[...]; │ │ 混淆代码... │ │ return to dispatcher; │ │ │ │ Handler_0x12: // 可能是 ADD │ │ vm_regs[a] = vm_regs[b] + decrypt(...); │ │ 垃圾代码... │ │ return to dispatcher; │ │ ...(可能有 50-200+ 个 handlers) │ ├──────────────────────────────────────────────────────┤ │ VM Exit(虚拟机退出) │ │ • 恢复真实寄存器 │ │ • 返回到未保护代码 │ └──────────────────────────────────────────────────────┘
虚拟化的"恐怖"之处
// 原始代码:1条指令 if (serial == 0x12345678) return true; // 虚拟化后:可能变成 100+ 条等价操作 vm_reg[0] = decrypt_constant(bytecode[0]); // 获取 serial vm_reg[1] = 0x12; vm_reg[1] = (vm_reg[1] << 8) | 0x34; vm_reg[1] = (vm_reg[1] << 8) | 0x56; vm_reg[1] = (vm_reg[1] << 8) | 0x78; vm_reg[2] = vm_reg[0] ^ vm_reg[1]; vm_reg[3] = ~vm_reg[2]; vm_reg[3] = vm_reg[3] + 1; if (vm_reg[3] == 0) vm_flag = 1; // ... 还有几十行垃圾指令
1.3 变异混淆 (Mutation)
代码膨胀技术
原始代码: ADD EAX, 5 变异后(可能变成以下任意一种): 方式1: 等价替换 SUB EAX, -5 方式2: 多步骤分解 ADD EAX, 3 ADD EAX, 2 方式3: 加入垃圾指令 PUSH EBX MOV EBX, 5 ADD EAX, EBX POP EBX 方式4: 使用复杂运算 LEA EAX, [EAX + 5] 方式5: 插入无效代码 JMP $+5 DB 0xE8, 0xFF, 0xFF // 看起来像CALL,但跳过了 ADD EAX, 5
控制流平坦化 (Control Flow Flattening)
原始代码: A → B → C → D 平坦化后: ┌→ Dispatcher ←┐ │ ↓ │ │ switch(state) { │ case 1: A; state=3; break; │ case 2: C; state=4; break; │ case 3: B; state=2; break; │ case 4: D; state=0; break; │ } │ └─────────────┘
1.4 加壳与压缩 (Packing)
原始PE文件结构: ┌────────────────┐ │ PE Headers │ ├────────────────┤ │ .text (100KB) │ ← 代码段 ├────────────────┤ │ .data (50KB) │ ← 数据段 ├────────────────┤ │ .rsrc (200KB) │ ← 资源 └────────────────┘ ↓ VMP 加壳处理 加壳后的文件: ┌──────────────────────┐ │ Original PE Headers │ ← 保留原始头(部分修改) ├──────────────────────┤ │ VMP Loader │ ← 解密解压代码 ├──────────────────────┤ │ Encrypted .text │ ← 加密的代码段 │ (压缩 + 加密) │ ├──────────────────────┤ │ Encrypted .data │ ├──────────────────────┤ │ VM Core Engine │ ← 虚拟机引擎代码 ├──────────────────────┤ │ Encrypted Bytecode │ ← 虚拟化后的字节码 ├──────────────────────┤ │ Anti-Debug Code │ ← 反调试模块 ├──────────────────────┤ │ Import Protection │ ← 导入表保护 └──────────────────────┘
1.5 反调试与反分析
多层反调试技术清单
╔══════════════════════════════════════════════════════════╗ ║ VMProtect 2 反调试技术大全 ║ ╠══════════════════════════════════════════════════════════╣ ║ [硬件层] ║ ║ ✓ 硬件断点检测 (DR0-DR7寄存器) ║ ║ ✓ 单步检测 (Trap Flag) ║ ║ ✓ 性能计数器检测 ║ ╠══════════════════════════════════════════════════════════╣ ║ [API层] ║ ║ ✓ IsDebuggerPresent ║ ║ ✓ CheckRemoteDebuggerPresent ║ ║ ✓ NtQueryInformationProcess (ProcessDebugPort) ║ ║ ✓ NtSetInformationThread (HideFromDebugger) ║ ║ ✓ OutputDebugString 技巧 ║ ╠══════════════════════════════════════════════════════════╣ ║ [时间检测] ║ ║ ✓ RDTSC 指令(检测时间差) ║ ║ ✓ GetTickCount / QueryPerformanceCounter ║ ║ ✓ 两次执行间隔异常 → 判定为单步调试 ║ ╠══════════════════════════════════════════════════════════╣ ║ [进程/线程检测] ║ ║ ✓ 检测调试器进程名(OllyDbg.exe, x64dbg.exe等) ║ ║ ✓ 检测调试器窗口类名 ║ ║ ✓ 父进程检测(是否被调试器启动) ║ ╠══════════════════════════════════════════════════════════╣ ║ [内存检测] ║ ║ ✓ PEB.BeingDebugged 标志检测 ║ ║ ✓ PEB.NtGlobalFlag 检测 ║ ║ ✓ Heap Flags 检测 ║ ║ ✓ 校验和检测(代码被修改) ║ ╠══════════════════════════════════════════════════════════╣ ║ [异常处理] ║ ║ ✓ SEH 反调试(异常处理链) ║ ║ ✓ VEH 反调试(向量化异常) ║ ║ ✓ INT 3 / INT 2D 检测 ║ ║ ✓ 故意触发异常,检查处理流程 ║ ╠══════════════════════════════════════════════════════════╣ ║ [虚拟机检测] ║ ║ ✓ VMware / VirtualBox 检测 ║ ║ ✓ CPUID 指令检测 ║ ║ ✓ 虚拟化环境特征检测 ║ ╚══════════════════════════════════════════════════════════╝
📌 Part 2: VMProtect 2 脱壳流程详解
2.1 脱壳总体流程图
┌─────────────────────────────────────────────────────────────┐ │ VMProtect 2 脱壳流程 │ └─────────────────────────────────────────────────────────────┘ 阶段一:环境准备 ├─ 安装必要工具 ├─ 配置反反调试环境 └─ 样本初步分析 ↓ 阶段二:定位 OEP (Original Entry Point) ├─ 方法1: ESP定律 ├─ 方法2: 内存断点 ├─ 方法3: 最后一次异常 └─ 方法4: SFX自解压特征 ↓ 阶段三:Dump内存镜像 ├─ 使用 Scylla / OllyDumpEx ├─ 完整dump进程内存 └─ 修正 ImageSize ↓ 阶段四:修复导入表 (IAT) ├─ 自动搜索IAT ├─ 修复Thunk └─ 重建导入表 ↓ 阶段五:去虚拟化 (最难!) ├─ 识别VM Entry ├─ 分析VM架构 ├─ 提取Handler ├─ 反编译字节码 └─ 重建原始逻辑 ↓ 阶段六:修复与测试 ├─ 修复重定位表 ├─ 修复资源 ├─ 删除反调试代码 └─ 测试运行
2.2 阶段一:环境准备与工具配置
必备工具清单
┌──────────────────────────────────────────────────────────┐ │ 工具类型 工具名称 用途 │ ├──────────────────────────────────────────────────────────┤ │ 调试器 x64dbg 主力调试 │ │ OllyDbg 2.01 32位程序备用 │ │ WinDbg 内核态调试 │ ├──────────────────────────────────────────────────────────┤ │ 反汇编器 IDA Pro 7.x 静态分析 │ │ Ghidra 开源替代品 │ ├──────────────────────────────────────────────────────────┤ │ 反反调试 ScyllaHide 对抗反调试 │ │ TitanHide 驱动级隐藏 │ │ HideOD 隐藏OllyDbg │ ├──────────────────────────────────────────────────────────┤ │ Dump工具 Scylla dump+IAT修复 │ │ OllyDumpEx Olly插件 │ │ PE-sieve 内存扫描 │ ├──────────────────────────────────────────────────────────┤ │ PE工具 CFF Explorer PE结构查看 │ │ LordPE PE编辑 │ │ StudPE PE分析 │ ├──────────────────────────────────────────────────────────┤ │ 去虚拟化 VMPDump (部分有效) 自动化工具 │ │ Code Virtualizer Tools 辅助分析 │ │ 自写脚本 IDAPython等 │ ├──────────────────────────────────────────────────────────┤ │ 监控工具 Process Monitor 文件/注册表监控 │ │ API Monitor API调用监控 │ │ WinAPIOverride API Hook │ └──────────────────────────────────────────────────────────┘
x64dbg 配置示例
1. 安装 ScyllaHide 插件 - 下载 ScyllaHide.zip - 解压到 x64dbgplugins - 重启 x64dbg - 插件 → ScyllaHide → Options - 勾选所有反调试对抗选项: ✓ NtQueryInformationProcess ✓ NtQuerySystemInformation ✓ NtSetInformationThread ✓ NtClose ✓ OutputDebugString ✓ GetTickCount ✓ ... 2. 配置选项 - 选项 → 首选项 → 异常 ✓ 忽略所有异常(第一轮) - 选项 → 首选项 → 引擎 ✓ 禁用调试特权 ✓ 保存数据库 3. 安装 Scylla - 插件 → Scylla → Hide from PEB
2.3 阶段二:定位 OEP (真实入口点)
方法1: ESP 定律(最常用)
原理: VMP的壳代码执行完后,会用类似 RETN 的指令跳转到OEP 此时ESP指向的栈位置存放着OEP地址 步骤: 1. x64dbg 加载程序(暂停在系统断点) 2. 命令行执行: bp GetProcAddress // 或者 bp LoadLibraryA F9 运行 3. 断下后,在命令行: hr esp // 对ESP指向的内存地址下硬件访问断点 4. F9 继续运行 5. 程序会在访问这个栈地址时断下 观察此时的指令,通常是: PUSH xxxxxxxx RETN ← 断在这里 6. 单步执行 (F7),会跳转到一个新地址 此时查看代码,如果看到: - 正常的函数序言(PUSH EBP; MOV EBP,ESP) - 或者开始调用API - 代码段名称变成 .text → 这就是 OEP! 7. 记录当前地址,例如:00401000
方法2: 内存断点法
步骤: 1. 运行程序,在 x64dbg 中打开内存映射 (Alt+M) 2. 找到 .text 节(代码段) - 名称:.text - 权限:ER-(可执行,可读) - 大小:通常最大的可执行段 3. 右键 → 在反汇编中转到 → 地址 记录起始地址,例如:00401000 4. 在命令行设置内存访问断点: bpm 00401000, 1, x // 在00401000地址,1字节,执行时中断 5. F9 运行,当第一次执行 .text 段代码时会断下 → 这通常就是 OEP 附近 注意:VMP可能会在OEP前多次访问.text段进行完整性检查
方法3: 异常断点法
原理:VMP使用大量异常作为混淆手段,最后一个异常通常在OEP附近 步骤: 1. x64dbg → 选项 → 首选项 → 异常 - 取消"忽略所有异常" - 只保留"忽略INT 3" 2. F9 运行,每次异常断下时: - 查看当前位置是否在 .text 段 - 如果不是,按 Shift+F9 传递异常 3. 重复多次后,最终会停在接近OEP的地方 4. 配合反汇编判断是否为OEP
如何确认找到的是真正的 OEP?
特征检查清单: ✓ 代码段名称是 .text 或 CODE ✓ 可以看到清晰的函数调用(CALL API) ✓ 有正常的函数序言: PUSH EBP MOV EBP, ESP SUB ESP, xxx ✓ 或者是 WinMain 的标准开头: PUSH EBX PUSH ESI PUSH EDI ✓ 附近有字符串引用 ✓ IDA Pro F5能正常反编译 ✓ 地址接近PE头中的 AddressOfEntryPoint(但不完全相同)
2.4 阶段三:Dump 内存镜像
使用 Scylla 进行 Dump
步骤: 1. 在 OEP 处暂停程序(上一步已找到) 2. x64dbg → 插件 → Scylla 3. Scylla 窗口设置: [*] Attach to: [选择目标进程] [*] OEP: 00001000 ← 输入相对于ImageBase的偏移 [*] IAT AutoSearch ← 自动搜索导入表 4. 点击 "IAT Autosearch" - 如果成功,会显示: Found IAT: 00402000 (Size: 0x400) 5. 点击 "Get Imports" - Scylla会分析IAT,列出所有导入函数 - 检查是否有 "invalid" 标记的项 6. 如果有invalid项: - 右键 → Cut Thunk - 或手动修复 7. 点击 "Dump" - 选择保存位置,例如:dumped.exe 8. 点击 "Fix Dump" - 选择刚才保存的 dumped.exe - Scylla会修复IAT并生成 dumped_SCY.exe
手动 Dump (OllyDumpEx)
在 OllyDbg 中: 1. 插件 → OllyDumpEx 2. 设置参数: Base Address: 00400000 ← 从内存窗口查看 Entry Point: 00001000 ← OEP的RVA Size: 00050000 ← 从PE头SizeOfImage获取 3. 勾选选项: ✓ Rebuild Import ✓ Fix Dump 4. 点击 "Dump" 保存 注意:手动dump可能需要后续修复重定位表
2.5 阶段四:修复导入表 (IAT Fix)
IAT修复原理
正常程序的IAT: ┌──────────────────────────┐ │ .idata 节 │ ├──────────────────────────┤ │ Import Directory │ │ DLL: kernel32.dll │ │ Thunk Array: │ │ → CreateFileA │ │ → ReadFile │ │ → CloseHandle │ ├──────────────────────────┤ │ IAT (导入地址表) │ │ 0x402000: 76A81234 ← CreateFileA的真实地址 │ 0x402004: 76A85678 ← ReadFile的真实地址 │ 0x402008: 76A89ABC ← CloseHandle的真实地址 └──────────────────────────┘ VMP加密后: ┌──────────────────────────┐ │ Import Directory 被破坏 │ │ 或指向假数据 │ ├──────────────────────────┤ │ IAT 被加密/动态生成 │ │ 0x402000: 00000000 │ │ 运行时才填充真实地址 │ └──────────────────────────┘ 修复目标: 重建 Import Directory 和 IAT,使dump出的文件能独立运行
Scylla 自动修复
1. "IAT Autosearch" 原理: - 在OEP附近扫描内存 - 查找连续的、指向DLL代码段的指针 - 识别为IAT 2. "Get Imports" 原理: - 读取IAT中的地址 - 通过地址找到对应的DLL模块 - 在DLL的导出表中查找函数名 - 重建导入表 3. 常见问题及解决: 问题1: "Found invalid imports" 原因:某些地址不是真正的API地址 解决:右键 → Cut Thunk (删除) 问题2: "IAT not found" 原因:IAT被严重混淆 解决:手动搜索 - 在OEP处下断点 - 运行程序,观察哪些API被调用 - 在内存中搜索这些API的地址 - 找到IAT的大致范围后,手动指定
手动修复 IAT(高级)
工具:IDA Pro + IDAPython 脚本示例: import idaapi import idc def rebuild_iat(start, end): """重建IAT""" addr = start imports = [] while addr < end: # 读取指针 ptr = idc.get_qword(addr) if idaapi.get_inf_structure().is_64bit() else idc.get_wide_dword(addr) # 检查是否指向DLL代码 seg = idaapi.getseg(ptr) if seg and seg.type == idaapi.SEG_XTRN: # 获取函数名 name = idc.get_name(ptr) if name: imports.append((addr, name)) print(f"{hex(addr)}: {name}") addr += 4 if not idaapi.get_inf_structure().is_64bit() else 8 return imports # 使用 iat_start = 0x00402000 # 手动确定的IAT起始 iat_end = 0x00402400 # 手动确定的IAT结束 rebuild_iat(iat_start, iat_end)
2.6 阶段五:去虚拟化 (De-virtualization) - 核心难点
5.1 识别 VM Entry
特征码搜索法: 在IDA Pro中搜索以下特征: 特征1: 大量 PUSHFD / PUSHAD (保存寄存器) 60 PUSHAD 9C PUSHFD ... 大量MOV/PUSH操作 ... 特征2: 跳转到动态计算的地址 MOV EAX, [EBP+var_XX] JMP [EAX*4+table_base] ← VM Dispatcher 特征3: 大量的RET链 RETN RETN RETN ← 每个Handler结束都用RETN返回Dispatcher 手动识别: 1. 从OEP开始F5查看 2. 如果看到极其复杂的代码,无法理解的嵌套 3. 大量单字节变量操作 4. 频繁的函数调用但没有明显逻辑 → 这些都是VM化的代码
5.2 分析 VM 架构
目标:理解这个虚拟机的"指令集" ┌─────────────────────────────────────────┐ │ VM 架构分析清单 │ ├─────────────────────────────────────────┤ │ 1. 虚拟寄存器 (Virtual Registers) │ │ - 在哪里存储?(栈上/全局内存) │ │ - 有多少个?(通常8-16个) │ │ - 如何访问? │ ├─────────────────────────────────────────┤ │ 2. 虚拟栈 (Virtual Stack) │ │ - Stack Pointer 在哪? │ │ - PUSH/POP 如何实现? │ ├─────────────────────────────────────────┤ │ 3. 字节码 (Bytecode) │ │ - 存储位置 │ │ - 读取方式 │ │ - 格式:定长?变长? │ ├─────────────────────────────────────────┤ │ 4. Dispatcher (调度器) │ │ - 入口地址 │ │ - 解码逻辑 │ │ - 跳转表结构 │ ├─────────────────────────────────────────┤ │ 5. Handlers (处理器) │ │ - 数量统计 │ │ - 功能分类 │ │ - 参数传递方式 │ └─────────────────────────────────────────┘
5.3 提取 VM Handlers
方法:动态跟踪 + 静态分析结合 x64dbg 脚本示例: -------------------- // 在 Dispatcher 下断点 bp 00401234 // Dispatcher地址 // 记录每个Handler var handler_list log "Opcode, Handler Address" loop: run mov eax, [esp] // 假设栈顶是返回地址 mov ebx, [ebp+opcode] // 假设EBP+offset存放opcode log "{ebx}, {eax}" esti // 执行直到返回 jmp loop -------------------- 输出示例: Opcode, Handler Address 0x01, 0x00401500 ← Handler_01 0x02, 0x00401650 ← Handler_02 0x03, 0x004017A0 ← Handler_03 ... 然后在IDA中逐个分析这些地址的代码
5.4 反编译字节码
步骤一:提取字节码 ----------------- x64dbg内存dump: 1. 运行到VM Entry后断下 2. 在dump窗口,找到字节码起始地址(通常从某个寄存器得到) 例如:EBP+0x100 指向字节码 3. 右键 → Dump Memory to File 4. 保存为 bytecode.bin 步骤二:编写反编译器 ------------------- Python示例: class VMDecompiler: def __init__(self, bytecode_file): with open(bytecode_file, 'rb') as f: self.bytecode = f.read() self.pc = 0 # Program Counter # Handler映射表(需手动填充) self.handlers = { 0x01: self.vm_push, 0x02: self.vm_pop, 0x03: self.vm_add, 0x04: self.vm_sub, 0x05: self.vm_mov, 0x10: self.vm_cmp, 0x11: self.vm_jne, # ... 更多 } def read_byte(self): val = self.bytecode[self.pc] self.pc += 1 return val def read_dword(self): val = int.from_bytes(self.bytecode[self.pc:self.pc+4], 'little') self.pc += 4 return val def vm_push(self): val = self.read_dword() print(f"PUSH {hex(val)}") def vm_add(self): print("ADD vreg0, vreg1") def vm_jne(self): offset = self.read_dword() print(f"JNE {hex(offset)}") # ... 实现所有handler def decompile(self): while self.pc < len(self.bytecode): opcode = self.read_byte() if opcode in self.handlers: self.handlers[opcode]() else: print(f"Unknown opcode: {hex(opcode)}") break # 使用 dec = VMDecompiler('bytecode.bin') dec.decompile()
5.5 重建原始逻辑(最终目标)
输入:反编译的字节码 PUSH 0x12345678 PUSH [ebp+8] CALL vm_hash CMP vreg0, vreg1 JNE fail_label PUSH 1 RET 输出:还原的C代码 int check_serial(char* input) { int hashed = vm_hash(input); if (hashed == 0x12345678) { return 1; } return 0; } 然后可以: 1. 用nop掉验证逻辑 2. 或者写注册机(基于还原的算法) 3. 或者直接patch跳转
2.7 阶段六:修复与测试
6.1 删除反调试代码(暴力法)
在IDA Pro中搜索并NOP以下代码: 1. IsDebuggerPresent 搜索:CALL IsDebuggerPresent 替换:NOP x 5 2. 时间检测 搜索:RDTSC / CPUID 替换:XOR EAX,EAX (清零结果) 3. 检查BeingDebugged 搜索:MOV AL, [FS:0x30] + 0x02 替换:MOV AL, 0 4. 异常反调试 搜索:INT 3 / INT 2D 替换:NOP
6.2 修复重定位表
使用 CFF Explorer: 1. 打开 dump_fixed.exe 2. Relocation Editor 3. 如果显示 "No relocations": - 可能需要手动重建 - 或者将ImageBase固定为原始值 手动fix (如果必要): 使用 LordPE: 1. PE Editor 2. Section Headers → .reloc 3. 如果损坏,从原始文件复制.reloc节
6.3 测试流程
1. 静态测试 - CFF Explorer 打开,检查PE结构完整性 - ImportREC 验证导入表 2. 沙箱测试 - 虚拟机中运行 - 检查是否崩溃 - Process Monitor 监控行为 3. 功能测试 - 主要功能是否正常 - 是否还有反调试触发 - 是否有完整性检查 4. 对比测试 - 与原始程序对比API调用序列 - 对比关键算法输出
📌 Part 3: 实战案例分析
案例:破解一个简单的VMP2保护程序
目标程序:CrackMe_VMP2.exe 保护方式:VMProtect 2.08 目标:绕过序列号验证 ======================================= 步骤记录: ======================================= 1. 查壳 ► PEID: VMProtect 2.0x ► DIE: VMProtect 2.08 2. 初步运行 ► 弹出对话框:"Enter Serial" ► 输入错误显示:"Wrong Serial!" ► 推测:验证逻辑被VM保护 3. x64dbg加载 + ScyllaHide ► 所有反调试选项全开 ► F9运行,程序正常弹窗 4. 定位验证函数 方法:API断点 ► bp GetDlgItemTextA // 获取输入的API ► F9运行,在输入框输入"test",点确定 ► 断下! 5. 回溯查找验证逻辑 ► 单步返回(Ctrl+F9)到调用者 ► 发现代码段: CALL GetDlgItemTextA LEA EAX, [EBP-0x100] PUSH EAX CALL 00401234 ← 可疑的验证函数 TEST EAX, EAX JZ fail_label ► 00401234 就是验证函数入口 6. 分析验证函数 ► 跟入 CALL 00401234 ► 发现大量混淆代码: PUSH ECX PUSH EDX MOV EAX, [ESP+8] JMP 00405678 ← 跳转到VM Entry! 7. 识别VM Entry 地址:00405678 特征: PUSHAD PUSHFD MOV EBP, ESP SUB ESP, 0x200 MOV ESI, [大量初始化] ... JMP [EAX*4+0x00406000] ← Dispatcher! 8. 简化破解法:爆破跳转 ► 不去虚拟化了,直接改跳转 ► 在第5步的代码处: TEST EAX, EAX JZ fail_label ← 改成 JNZ ► 使用x64dbg修改: 选中 JZ 指令 空格 → 编辑 改成:JNZ fail_label 右键 → Patch → Patch File 保存为 CrackMe_VMP2_cracked.exe 9. 测试 ► 运行 CrackMe_VMP2_cracked.exe ► 输入任意序列号 ► 成功弹出:"Correct!" ======================================= 总结: 对于简单目标,不需要完全去虚拟化 找到关键跳转直接patch即可 但对于复杂的License系统,可能需要完整分析 =======================================
📌 Part 4: 进阶技术
4.1 自动化去虚拟化工具
工具1: VMPAttack - GitHub开源项目 - 支持部分VMP版本 - 能自动识别Handler 使用: python vmpattack.py target.exe --trace --devirt 工具2: Kryptonite - IDA Pro插件 - 符号执行 + 污点分析 - 半自动去虚拟化 工具3: 自己用Unicorn引擎写模拟器 from unicorn import * from unicorn.x86_const import * # 模拟VM执行 uc = Uc(UC_ARCH_X86, UC_MODE_32) uc.mem_map(0x00400000, 2 * 1024 * 1024) uc.mem_write(0x00400000, code) uc.emu_start(0x00401000, 0x00401234)
4.2 对抗完整性检查
VMP的CRC校验: 1. 在运行时计算代码段的校验和 2. 与内置值比较 3. 不匹配则退出 对策: 方法1:Hook校验函数 bp [校验函数地址] 修改返回值为"正确" 方法2:修复校验值 计算patch后的新CRC 在内存中搜索旧CRC值 替换为新值 方法3:NOP掉校验代码 找到CRC计算和比较代码 全部NOP
总结
VMProtect 2 的脱壳是逆向工程中的终极挑战之一:
难度评级: ⭐⭐⭐⭐⭐ (5/5)
时间成本:
- 简单patch:1-3小时
- 完整dump:1-2天
- 去虚拟化:1周-数月
成功率:
- 基本脱壳:90%
- IAT修复:70%
- 完全去虚拟化:<20%
关键建议:
- 从简单目标开始练习
- 理论+实践结合
- 善用自动化工具
- 专注于关键逻辑,不要试图还原全部
- 考虑替代方案(如内存patch而不是文件patch)
最后强调:将知识用于合法的安全研究和学习,尊重软件版权!
这是一个非常好的问题,也是无数逆向工程爱好者和安全研究人员梦寐以求的“圣杯”。
答案非常明确且残酷:不存在,也几乎不可能存在一个能够自动化、通用、完美修复VMProtect加密程序的“万能工具”。
下面我将详细解释为什么这在技术上是一个近乎无解的难题,以及为什么你看到的所谓“工具”都只是辅助性的。
第一部分:为什么“万能工具”是一个幻想?
要理解这一点,我们必须区分**“加壳器 (Packer)”** 和 “虚拟化保护器 (Virtualizer)” 的根本区别。
1. 简单的加壳器 (如 UPX)
- 工作原理:像一个
zip压缩程序。它有一套固定的算法来压缩原始代码,然后在程序入口点放一小段固定的解压代码。 - 脱壳原理:因为解压算法是固定的,所以可以写一个固定的“解压”工具(脱壳机)。
UPX -d就是这样一个万能脱壳工具,因为它完全了解UPX的压缩算法。
2. VMProtect (虚拟化保护器)
- 工作原理:它不是一个
zip程序,而是一个**“语言发明工厂”**。- 当你用VMProtect保护你的程序时,VMP会为你程序的关键代码发明一种全新的、独一无二的、随机的编程语言(字节码)。
- 同时,它还会为你**生成一个唯一的解释器(虚拟机VM)**来执行这种新语言。
- 核心特性:无限变种 (Infinite Variation)
- 不同的程序,不同的VM:用VMP保护
A.exe和B.exe,会得到两个完全不同的虚拟机。A.exe里的VM不认识B.exe的字节码,反之亦然。 - 同一程序,每次编译都不同:你用完全相同的设置,两次保护同一个
A.exe,得到的A_v1.exe和A_v2.exe,它们内部的虚拟机和字节码也是不一样的。
- 不同的程序,不同的VM:用VMP保护
把这个概念具象化:
想象一下,你想写一个“万能翻译器”。
- 对于
UPX,这很简单,因为你只需要翻译一种公开的语言(比如英语翻译成中文),算法是固定的。- 对于
VMProtect,这相当于要求你的“万能翻译器”能够自动翻译地球上从未出现过的、由外星人刚刚随机发明的、且每一个外星人说的都不一样的神秘语言。你会发现,你根本无法制造这样一个“万能翻译器”。你只能逐个去接触每一个外星人,花费大量时间学习并破解他那套独有的语言体系。
第二部分:三大技术壁垒,让“万能工具”无法实现
1. 虚拟机架构的无限随机性
一个自动化工具需要依赖固定的模式(Pattern)来识别和处理代码。但VMP的设计哲学就是消灭一切固定模式。
每次保护,以下所有元素都是随机生成的:
- 虚拟指令集 (Bytecode):
0x01在A程序里可能是“加法”,在B程序里可能是“跳转”。 - 虚拟机处理器 (VM Handlers):执行“加法”的真实x86代码,在A和B程序里完全不同,充满了各种垃圾指令和混淆。
- 虚拟寄存器:虚拟机的寄存器数量、存储位置(在栈上还是在特定内存区域)都是随机的。
- 调度器 (Dispatcher):分发指令的核心逻辑也是千变万化。
一个工具无法通过静态分析找到任何可依赖的“签名”或“特征码”来自动化还原这个过程。
2. 代码变异与深度混淆 (Mutation & Obfuscation)
即使是虚拟机本身的代码(那些VM Handlers),也是经过VMP深度混淆的。一个简单的 ADD EAX, EBX 会被变成几十条甚至上百条等价但难以理解的指令。自动化工具无法分辨哪些是有效代码,哪些是垃圾代码,更无法将其还原成最简形式。
3. 无处不在的反分析陷阱 (Anti-Analysis Traps)
VMP会在代码中埋下无数“地雷”,专门用来对抗自动化分析工具。
- 完整性检查 (CRC Check):自动化工具在分析和修改代码时,会改变文件的校验和。VMP的自校验代码一旦发现文件被修改,就会导致程序崩溃或行为异常。一个“万能工具”除非能智能地定位并修复所有校验点,否则生成的程序就是个废品。
- 反调试和时序攻击:自动化工具的某些分析技术(如符号执行)可能会被VMP的时钟检测、API行为检测等机制发现,从而触发对抗。
第三部分:那市面上的“VMP工具”是做什么的?
你可能听过或用过一些名字里带“VMP”的工具,比如VMPDump、VMP Unpacker by VT,或者一些开源脚本。它们都不是“万能工具”,而是辅助分析的脚本或小程序,作用非常有限。
-
自动化Dump工具 (如VMPDump)
- 能做什么:尝试自动完成“寻找OEP”和“内存转储”这两个最最基础的步骤。
- 不能做什么:完全不能去虚拟化。它Dump出来的文件,关键代码依然是VMP的字节码。
- 成功率:对非常古老、保护选项弱的VMP版本可能有效。对现代VMP版本基本100%失败。
-
分析辅助脚本 (如IDAPython脚本)
- 能做什么:帮助人类分析师提高效率。例如,一个脚本可以自动地:
- 追踪VM Dispatcher,并列出所有VM Handler的地址。
- 为每个Handler打上标签,方便你逐个分析。
- 尝试对字节码进行初步的染色或分组。
- 不能做什么:它不能告诉你某个Handler是“加法”还是“减法”。这个最关键的、需要智力判断的工作,仍然需要你手动完成。
- 能做什么:帮助人类分析师提高效率。例如,一个脚本可以自动地:
-
学术研究型工具 (基于符号执行、污点分析)
- 能做什么:这是最前沿的尝试。通过模拟执行每一条指令,并追踪数据的流动(污点分析),理论上有可能推断出部分代码的原始逻辑。
- 不能做什么:
- 极度缓慢:分析一小段VMP代码可能需要几个小时甚至几天。
- 路径爆炸:遇到复杂的分支,需要探索的路径呈指数级增长,很快就会耗尽所有计算资源。
- 使用复杂:需要博士级别的专业知识来配置和使用,绝非“一键运行”。
结论
放弃寻找“自动化脱壳VMP的万能工具”的想法。
对抗VMProtect的唯一途径,是将其视为一场人与机器的智力对抗。真正的“工具”是:
- 你的大脑:你的逆向工程知识、逻辑推理能力和分析经验。
- 强大的调试器和反汇编器:如
x64dbg和IDA Pro,它们是你的眼睛和手术刀。 - 无尽的耐心:去虚拟化是一个极其枯燥和漫长的过程,以周或月为单位计算。
如果你真的想攻克它,正确的道路是学习扎实的逆向工程基础,而不是在网上寻找那个不存在的“魔法按钮”。
加密程序的执行流程
解密流程
1.脱壳流程
进入程序的真实入口
dump内存文件
修复pe数据、修复iat
重定位及其他检查

这里壳的作用就是解压和解密代码段和数据段和资源段。入口点变为了虚假的壳入口,而不是代码段的mian函数。
什么是修复
目标是使ida可以正常工作,可以静态分析。
保证pe结构关键数据的完整及正确。
保证对数据的引用正确,全局变量涉及重定位。
保证跳转指令的跳转地址正确,cal、jmp指令涉及重定位。
保证api的调用正确,涉及iat的修复


到OEP如何
特征法:根据原程序入口特征查找些编译器编译程序的入口特征

同时我们知道OEP的位置在dump中的ida程序中是没有建立函数识别的,因为没有任何一段函数会引用这段函数,基本可以定位到这个OEP位置
壳分析法:根据壳代码查找这里需要我们熟悉壳代码的流程,知道壳代码将控制权转交给OEP的位置。
API方法;


我识别是什么程序我们可以dump内存获取特征
这里我用
https://www.unpac.me/results/556bdbc0-32f7-467f-ad28-42d5cf9112be
上传样本自动分析流程发现特折


vmp检测软件和硬件断点就无法设置oep处的断点


破解VMProtect v.1.6x – 1.7壳
1.附加进程
2.输入要跟随的表达式:VirtualProtect
3.找到Call VirtualProtectEx下端点
4.运行,至堆栈窗口显示ReadOnly。(即代码释放完毕)
5.打开内存窗口,在第一个代码段设置内存访问断点
6.运行后停下来,右键断点–删除内存短点
7.找到寄存器的ESP,数据窗口中跟随
8.从最低找,找到第一个“返回Kernel32…”,在其上一行设置硬件写入断点(Byte)
9.运行,打开内存窗口,第一个代码段设置内存访问断点。
10.继续运行,到达OEP。
11.可以使用od自带插件,也可用LordPE脱壳,再用ImportREC修复转存。