应用安全 — win安全 之 VMP初体验

VMP是一种软件加固方法

Virtual Machine Protect. 虚拟机保护 ,可以将汇编指令转化为自定义指令集,虚拟指令涉及上百万条汇编指令,极大增强pj难度。

 

由win版本的和linux,安卓版本的。他们的软件实现方法和厂家都不一样,但是原理相同。

win具体的软件由pmvrotect2.x 3.x 3.5。vmp加密时只是对之前的so进行新增加密节区,不修改节区。

pmvrotect2软件加密的手段有很多最著名的是vmp技术。包含 指令虚拟化,导入表加密,反调试,反dump,指令和数据压缩

 vmpdumper可以脱出汇编代码但是不能修复vmp节区和导出表,加密字符串。需要手动修复。导入表(IAT)不能修复

 

代码压缩 (Packing)

原理:压缩代码段

text

原始PE结构: ├─ .text  (500KB) - 可执行代码 ├─ .data  (100KB) - 数据 └─ .rsrc  (200KB) - 资源  ↓ 压缩后 ↓  ├─ .vmp0  (150KB) - 压缩的代码+解压stub ├─ .vmp1  (80KB)  - 压缩的数据   └─ .rsrc  (200KB)  运行时: 1. 解压.vmp0到内存 2. 修复重定位 3. 跳转执行

压缩算法:LZMA/自定义

强度:★★
主要用途:减小体积(附带混淆效果)

 

 

代码虚拟化 (Virtualization) ⭐最强

原理:将x86指令转换为自定义虚拟机指令

转换示例:

asm

; 原始代码 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代码示例:

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)

原理:用等价但复杂的指令替换原指令

转换示例:

asm

; 原始 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,但复杂化

典型变异技术:

asm

; 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调用

转换示例:

C++

; 原始导入表 IMPORT TABLE:   kernel32.dll     - CreateFileA     - ReadFile  ; ↓ 保护后 ↓  IMPORT TABLE:   kernel32.dll     - LoadLibraryA  ; 只保留最基础的  ; 运行时动态获取 char* encrypted = "x3Fx12x7A...";  // 加密的 "CreateFileA" decrypt(encrypted); pCreateFile = GetProcAddress(LoadLibrary("kernel32"), decrypted); pCreateFile(...);  // 调用

代码对比:

C++

// 原始 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表

应用安全 --- win安全 之 VMP初体验

 

 

字符串加密 (String Encryption)

原理:加密所有字符串常量

示例:

C++

// 原始 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)

技术清单:

C++

// 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)

检测技术:

C++

// 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)

原理:检测内存完整性

C++

// 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资源段

C++

// 原始 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后(简化示意)

C++

// 编译后的保护代码(逆向视角)  .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% 减小体积

 




虚拟指令、虚拟栈、虚拟寄存器。

  

应用安全 --- win安全 之 VMP初体验

 

 

 

msdn。开发文档
<windows程序设计>
<windows核心编程>
<win32汇编语言程序设计>

 

 

 

PE 文件结构图例

text

+-----------------------------------+  <-- 文件开始 (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 节区原始数据          | |        (存放资源)                  | |                                   | |             ...                   | +-----------------------------------+  <-- 文件结束


各组成部分的简明解释

  1. IMAGE_DOS_HEADER (DOS 头)

    • 目的:为了保持与古老 DOS 系统的兼容性。

    • 关键成员:e_lfanew 字段,它包含了指向真正的 PE 头 (IMAGE_NT_HEADERS) 的文件偏移量。

  2. IMAGE_NT_HEADERS (NT 头)

    • 目的:PE 文件的正式入口和核心描述符。

    • 包含三部分:

      • Signature:一个 "PE" 的签名,标识这是一个 PE 文件。

      • IMAGE_FILE_HEADER (文件头):描述了文件的全局属性,如目标机器类型、节区数量、创建时间等。

      • IMAGE_OPTIONAL_HEADER (可选头):虽然叫"可选",但对于可执行文件是必需的。它包含了程序加载和运行所需的关键信息。

  3. IMAGE_OPTIONAL_HEADER (可选头)

    • 目的:为操作系统加载器提供如何准备和执行程序的信息。

    • 关键成员:入口点地址、映像基址、内存/文件对齐值、子系统等。

    • 它末尾的 IMAGE_DATA_DIRECTORY (数据目录) 是一个非常重要的表格,它指出了其他重要数据结构(如导入表、导出表、资源表)在文件中的位置和大小。

  4. IMAGE_DATA_DIRECTORY (数据目录)

    • 目的:作为指向其他重要数据的“目录”或“索引”。

    • 结构:一个由16个相同结构组成的数组。每个结构包含一个 RVA(相对虚拟地址) 和 Size。

    • 例如:第二个条目(索引1)是 Import Directory,加载器通过它找到所有需要从其他DLL导入的函数列表。

  5. IMAGE_SECTION_HEADER (节区头)

    • 目的:描述文件中的各个“节区”。节区是实际存储代码、数据、资源等内容的部分。

    • 数量:由 IMAGE_FILE_HEADER 中的 NumberOfSections 指定。

    • 关键成员:

      • Name:节区名称(如 .text.data.rdata)。

      • VirtualAddress:该节区加载到内存后的 RVA。

      • PointerToRawData:该节区在磁盘文件中的原始数据偏移。

      • Characteristics:节区属性(如可读、可写、可执行)。

总结与流程

操作系统加载一个 PE 文件的简化流程如下:

  1. 读取 IMAGE_DOS_HEADER,找到 e_lfanew

  2. 跳到 e_lfanew 位置,验证 "PE" 签名,读取 IMAGE_NT_HEADERS

  3. 从 IMAGE_FILE_HEADER 知道有多少个节区。

  4. 从 IMAGE_OPTIONAL_HEADER 获取关键信息(如入口点、映像大小、数据目录)。

  5. 遍历 IMAGE_SECTION_HEADER 数组,了解每个节区在文件和内存中的映射关系。

  6. 根据节区头的信息,将文件的各个节区(代码、数据等)映射到内存的相应位置。

  7. 通过 IMAGE_DATA_DIRECTORY 找到导入表,解析并填充所有需要的外部函数地址。

  8. 最后,跳转到 AddressOfEntryPoint 指向的地址,程序开始执行。

这个结构确保了 PE 文件既能在磁盘上高效存储,又能在内存中正确加载和执行。

 

 

 

PE文件头结构图解 + 白话文秒懂

📊 完整结构总览

text

┌─────────────────────────────────────────────────────────┐ │                    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)

text

偏移    大小    字段名              白话文解释 +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字节)

text

+0x00   4字节   Signature          "PE" (0x50450000)

白话:

就像盖了个"认证章",证明"我是正宗的Windows程序"。


3️⃣ File Header (IMAGE_FILE_HEADER - 20字节)

text

偏移    大小    字段名                  白话文解释 +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 常见标志:

text

0x0002  IMAGE_FILE_EXECUTABLE_IMAGE     可执行文件(不是obj) 0x0100  IMAGE_FILE_32BIT_MACHINE        32位程序 0x2000  IMAGE_FILE_DLL                  这是个DLL文件


4️⃣ Optional Header (IMAGE_OPTIONAL_HEADER - 最重要!)

标准字段部分

text

偏移    大小    字段名                  白话文解释 +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专用字段部分

text

偏移    大小    字段名                  白话文解释 +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 子系统:

text

1 = Native(驱动程序) 2 = GUI(窗口程序) 3 = CUI(控制台程序,黑框框)

DllCharacteristics 重要标志:

text

0x0040  DYNAMIC_BASE           支持ASLR(地址随机化) 0x0100  NX_COMPAT              支持DEP(数据执行保护) 0x0400  NO_SEH                 不使用SEH异常处理 0x8000  TERMINAL_SERVER_AWARE  终端服务器感知


5️⃣ 数据目录表 (Data Directory - 16个条目)

text

索引  名称                        作用 [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                  保留

每个条目结构:

text

+0x00  4字节  VirtualAddress    数据在内存中的RVA +0x04  4字节  Size              数据的大小


6️⃣ 节表 (Section Table - 每节40字节)

text

偏移    大小    字段名              白话文解释 +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

.text   代码段(你的程序逻辑)          可读+可执行 .data   已初始化数据(全局变量)        可读+可写 .rdata  只读数据(常量字符串)          只读 .bss    未初始化数据                   可读+可写 .rsrc   资源(图标、菜单、字符串)      只读 .reloc  重定位信息                     只读 .idata  导入表(需要的DLL函数)        可读+可写 .edata  导出表(DLL导出的函数)        只读

Characteristics 节属性:

text

0x00000020  CODE                 包含代码 0x00000040  INITIALIZED_DATA     包含已初始化数据 0x00000080  UNINITIALIZED_DATA   包含未初始化数据 0x20000000  EXECUTE              可执行 0x40000000  READ                 可读 0x80000000  WRITE                可写


🎯 关键概念白话解释

RVA (Relative Virtual Address) - 相对虚拟地址

text

假设程序被加载到内存地址 0x00400000 某个函数的RVA是 0x1000 那么这个函数的实际内存地址 = 0x00400000 + 0x1000 = 0x00401000

文件偏移 vs 内存地址

text

文件偏移:在硬盘上的.exe文件中的位置 内存地址:程序运行时在内存中的位置 需要通过节表来转换!

对齐 (Alignment)

text

FileAlignment = 0x200 (512字节) → 文件中每个节的起始位置必须是512的倍数  SectionAlignment = 0x1000 (4096字节) → 内存中每个节的起始位置必须是4KB的倍数


🛠️ 实战:用十六进制编辑器看PE头

text

偏移      十六进制                            解释 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头三句话秒懂

  1. DOS头:"我伪装成DOS程序,但真正的内容在后面"
  2. PE头:"我是Windows程序,32位/64位,从地址XXX开始执行"
  3. 节表:"我的代码在.text节,数据在.data节,资源在.rsrc节"

🔥 最关心的地方

text

✅ AddressOfEntryPoint    → 程序从哪里开始跑 ✅ Import Table (IAT)     → 调用了哪些关键API(MessageBox? RegCreateKey?) ✅ .text 节               → 注册验证代码藏在这里 ✅ Characteristics        → 能不能改这个节的数据 ✅ Resource Table         → 修改图标、字符串、对话框

希望这个图解能帮你快速理解PE结构!如果要深入学习,推荐工具:

  • CFF Explorer (查看PE结构最直观)
  • PE-bear
  • 010 Editor (配合PE模板)
  • windowspe 权威指南

 

了解所有编译器和反汇编特征

了解vmp入口,区段,反汇编特征

 

看特征的方法主要有两个,一个是入口代码的在节区中的位置,入口代码中包含的指令和调用的外部接口

vc6特征和易语言

应用安全 --- win安全 之 VMP初体验

 

应用安全 --- win安全 之 VMP初体验

 

 

应用安全 --- win安全 之 VMP初体验

 

应用安全 --- win安全 之 VMP初体验

 

 

应用安全 --- win安全 之 VMP初体验

 

应用安全 --- win安全 之 VMP初体验

 

应用安全 --- win安全 之 VMP初体验

 

应用安全 --- win安全 之 VMP初体验

 

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

 

应用安全 --- win安全 之 VMP初体验

 

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 加密的程序。

但在开始之前,我必须先强调一个至关重要的事实,这能为你节省大量的时间和精力:

🛑 第一部分:必读的现实情况与重要提醒

  1. VMPDump 并非万能神药:VMPDump 是一个比较早期的、针对特定旧版本 VMProtect 的自动化分析工具。它无法真正地“还原所有代码”。它的主要功能是尝试自动完成“寻找OEP”和“Dump内存”这两个初级阶段的工作。

  2. 它不能“去虚拟化”:VMPDump 绝对无法将 VMProtect 的核心——虚拟化字节码(Bytecode)——翻译回原始的 x86/x64 汇编代码。被虚拟化的关键代码(如注册算法)在 VMPDump 处理后,依然是虚拟化的,你用IDA打开看仍然是一团乱麻。

  3. 成功率极低:对于现代版本的 VMProtect 2 (例如 2.13 及以后) 或者即使是旧版本但开启了高强度保护选项的程序,VMPDump 的成功率趋近于零。它很可能会失败、卡死、或者生成一个完全无法运行的垃圾文件。

  4. 它的真正作用:在极少数情况下(例如面对非常古老的、保护设置很弱的VMP版本),它可能可以帮你快速脱掉外层的壳,省去你手动寻找OEP和Dump的几分钟时间。请把它看作一个“快速尝试的彩票”,而不是一个“解决方案”。

结论: 不要期望 VMPDump 能帮你“一键还原”所有代码。它只是一个辅助性的、成功率很低的入门级工具。


第二部分:准备工作

在你开始之前,请确保你已经准备好以下环境和工具:

  1. 虚拟机 (VM):强烈建议在 VMware 或 VirtualBox 等虚拟机中进行所有操作。这可以防止目标程序或工具损坏你的物理机系统。
  2. 目标程序:你想要分析的那个被VMP2加密的 .exe 文件。
  3. VMPDump 工具:你需要从一些逆向工程论坛或网站下载这个工具。请注意,这些工具可能被杀毒软件报毒,这是因为它们的行为(如注入进程、读取内存)与恶意软件相似。请在虚拟机中谨慎使用。
  4. 调试器:x64dbg (强烈推荐)。VMPDump 经常需要附加到一个正在运行的进程上,所以你需要先运行目标程序。
  5. PE 编辑器:CFF Explorer 或 010 Editor。用于后续检查生成的文件是否结构正常。

第三部分:使用 VMPDump 的详细步骤

假设你已经找到了一个可以运行的 VMPDump 版本,并且你的目标程序是一个32位的EXE(VMPDump主要支持32位)。

步骤 1:运行目标程序

首先,双击运行你要分析的那个 .exe 文件。让它正常运行起来,比如显示出主窗口或对话框。

步骤 2:以管理员身份运行 VMPDump

右键点击 VMPDump.exe,选择 “以管理员身份运行”。这能确保它有足够的权限去读取其他进程的内存。

步骤 3:附加到目标进程

  1. 在 VMPDump 的主界面上,你会看到一个 “...” 按钮,旁边是 "Process" 或 "PID" 字段。
  2. 点击这个 “...” 按钮,会弹出一个当前正在运行的进程列表。
  3. 在列表中找到你的目标程序进程(例如 target.exe),选中它,然后点击 “OK” 或 “Select”。

步骤 4:智能扫描 (Smart Scan)

这是 VMPDump 尝试分析虚拟机的关键一步。

  1. 在界面上找到一个名为 “Smart Scan of Handlers” 的复选框,勾选它。
  2. 这一步会让 VMPDump 尝试在程序的内存空间中搜索它认为是 VMProtect 虚拟机处理器(VM Handlers)的代码块。这是它最容易失败的地方。

步骤 5:设置 OEP (Original Entry Point)

VMPDump 会尝试自动检测 OEP。

  • 自动检测:通常你不需要手动设置 OEP 字段。VMPDump 会利用一些内置的特征码去定位。
  • 手动设置:如果自动检测失败,你需要自己通过其他方法(如 x64dbg 的 ESP 定律)找到 OEP 的相对虚拟地址(RVA),然后手动填入 OEP 字段。对于初学者来说,如果 VMPDump 无法自动找到 OEP,基本可以宣告失败了。

步骤 6:执行 Dump(转储)

  1. 在 VMPDump 界面上,点击 “Unpack” 或 “Dump” 按钮。
  2. VMPDump 会开始它的工作流程:
    • 寻找 OEP。
    • 扫描 VM Handlers。
    • 转储内存中的所有节区(Sections)。
    • 尝试重建导入地址表(IAT)。
  3. 如果一切顺利(概率很小),它会提示你保存文件。通常会默认保存为 [原文件名]_unpacked.exe

步骤 7:验证生成的文件

这是最重要的一步,用来判断 VMPDump 是否真的起作用了。

  1. 尝试运行:双击运行生成的 _unpacked.exe 文件。

    • 直接崩溃? -> 失败。很可能是 IAT 修复失败或代码段Dump不完整。
    • 可以运行但功能异常? -> 部分失败。可能某些保护代码没有被移除。
    • 可以正常运行? -> 恭喜,你完成了最基础的“脱壳”。但这不代表代码被还原了。
  2. 使用 PE 工具检查:用 CFF Explorer 打开 _unpacked.exe 文件。

    • 检查 “Import Directory”(导入目录),看看函数列表是否看起来正常,有没有大量的无效或乱码条目。
    • 检查节表(Section Headers),看看 .text.data 等节的大小和权限是否合理。
  3. 使用 IDA Pro 分析:这是最终的检验。用 IDA Pro 打开 _unpacked.exe 文件。

    • 找到你关心的关键功能(比如注册按钮的点击事件)。
    • 按 F5 尝试反编译。
    • 结果是什么?
      • 如果依然是无法反编译、跳转关系混乱、充满了奇怪计算的代码 -> 这就证明了 VMPDump 没有去虚拟化。它只是把内存里的东西原封不动地Dump了出来。关键逻辑依然被VM保护着。
      • 如果能看到清晰的C代码逻辑 -> 这种情况几乎不可能发生,除非你的目标程序用的不是VMP的虚拟化保护。

第四部分:当 VMPDump 失败时(99% 的情况)

你很可能会遇到以下情况,这都意味着 VMPDump 对你的目标无效:

  • VMPDump 无法找到 OEP 或 Handlers:直接报错,提示分析失败。
  • VMPDump 在扫描过程中卡死或崩溃:说明 VMP 的反分析机制触发了。
  • 生成的 Dump 文件无法运行:最常见的结果,说明 Dump 或修复过程出错了。
  • 生成的 Dump 文件虽然能运行,但关键功能(如注册)依然被保护:证明了核心逻辑仍是虚拟化的。

当 VMPDump 失败后,你必须回到正统的、手动的逆向工程流程上来:

  1. 手动寻找 OEP:使用 x64dbg 和 ESP 定律。
  2. 手动 Dump:使用 Scylla 插件,在 OEP 处进行内存转储。
  3. 手动修复 IAT:使用 Scylla 的 IAT 搜索和修复功能。
  4. 手动去虚拟化:这才是真正的挑战。在 IDA 和 x64dbg 中,花费数天、数周甚至数月的时间去:
    • 分析 VM Entry 和 Dispatcher。
    • 逐个逆向 VM Handlers 的功能。
    • 编写脚本翻译字节码。
    • 找到关键逻辑并进行 Patch。

总结

使用 VMPDump 是一个“死马当活马医”的尝试。你可以按照上述步骤操作一遍,体验一下自动化工具的工作流程。但这趟旅程的终点,大概率是让你明白:没有捷径可走,对抗高强度的软件保护,唯有扎实的逆向工程基础和巨大的耐心。

 

 

 

好的,我们来非常详细地拆解和说明 VMProtect 2 的加密功能和脱壳流程。这是一个极其复杂但非常有趣的技术话题。

⚠️ 郑重声明:本内容仅用于安全研究、技术学习和防御策略探讨。对商业软件进行逆向工程、破解和传播是违法行为,可能导致严重的法律后果。请在法律允许的范围内使用这些知识,并尊重软件开发者的劳动成果。


Part 1: VMProtect 2 的核心加密功能(它做了什么?)

VMProtect (VMP) 不是一个简单的“壳”,它是一个深度代码混淆和虚拟化系统。其核心思想是让你写的代码,在你的电脑上都看不懂。

1. 虚拟化保护 (Virtualization) - VMP的王牌

这是VMP最强大、最核心的功能。它能将你指定的代码片段从标准的 x86/x64 汇编指令,转换成一种自定义的、每次编译都不一样的字节码(Bytecode)。

工作原理图解

text

你的原始代码 (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代码继续执行。│ └──────────────────────────────────────────────────────┘

为什么这让破解变得极其困难?

  1. 独一无二的指令集:每个被VMP保护的程序,其内部的虚拟机、字节码、Handler都是随机生成的。你在A程序上分析得到的经验,完全无法用于B程序。
  2. 分析成本极高:你无法再用IDA Pro的F5功能直接看到C代码。你必须先逆向分析出这套虚拟机的几十甚至上百个Handler的功能,才能像翻译密码一样,一点点地把字节码“翻译”回原始逻辑。这个过程可能需要数周甚至数月。
  3. 高度混淆:一个简单的 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)附加,就会改变行为。

  • 调试器检测:通过 IsDebuggerPresentCheckRemoteDebuggerPresent 等API检测。
  • 时间检测:使用 RDTSC 指令或 GetTickCount API,通过计算两段代码执行的时间差来判断是否在单步调试(单步执行会花费更长的时间)。
  • 硬件断点检测:检查 DR0-DR7 调试寄存器。
  • 异常处理技巧:故意触发一个异常,然后用 SEH(结构化异常处理)来捕获并执行正常代码。如果调试器干预了异常处理流程,程序就会崩溃。
  • 完整性检查:在运行时计算自身代码的校验和(CRC),如果发现代码被修改(比如被打了补丁),就会退出。

Part 2: VMProtect 2 的脱壳流程(如何应对?)

“脱壳”对于VMP来说是一个非常宽泛的概念。完整的流程极其复杂,通常分为以下几个阶段。

阶段一:准备工作与环境配置

这是所有工作的基础。你需要一个“干净”且“武装到牙齿”的分析环境。

  1. 工具清单:

    • 调试器: x64dbg (主流选择,插件丰富)
    • 反汇编器: IDA Pro 7.x (静态分析的王者)
    • 反反调试插件: ScyllaHide (x64dbg插件,用于对抗各种反调试技术)
    • Dump工具: Scylla (集成在ScyllaHide中,用于dump内存和修复IAT)
    • PE工具: CFF Explorer 或 010 Editor (用于查看和编辑PE文件结构)
  2. 环境配置:

    • 虚拟机:强烈建议在VMware或VirtualBox中进行分析,防止搞垮物理机。
    • 配置ScyllaHide:在x64dbg中加载ScyllaHide插件,并尽可能多地勾选对抗选项,如 NtQueryInformationProcessGetTickCount 等。

阶段二:寻找OEP (Original Entry Point - 真实入口点)

由于VMP加了壳,第一步就是找到VMP的Loader执行完毕,即将跳转到程序原始代码的那个点(OEP)。

常用方法:ESP定律

  1. 用x64dbg加载目标程序,程序会停在系统断点。
  2. 在x64dbg的命令行输入 bp GetModuleHandleA,然后按F9运行。程序会在加载系统DLL时断下。
  3. 找到ESP寄存器的值,右键 -> 在内存窗口中转到。
  4. 在内存窗口中,选中ESP指向的前4个字节,右键 -> 断点 -> 硬件, 访问 (Dword)。
  5. 按F9继续运行。程序会在VMP的壳代码即将RETJMP到OEP之前,访问这个栈地址时断下。
  6. 此时,单步执行(F7/F8),很大概率就会跳转到OEP。

如何判断找到了OEP?

  • 代码看起来像是正常的函数开头(如 PUSH EBP; MOV EBP, ESP)。
  • 可以看到清晰的API调用。
  • IDA Pro能够很好地分析这部分代码。

阶段三:Dump内存镜像

在OEP处,程序的代码和数据已经在内存中解密,此时需要将它们从内存中“倒”出来,存成一个新的EXE文件。

  1. 在x64dbg中,停在OEP处。
  2. 打开插件菜单 -> Scylla。
  3. 在Scylla窗口中,确保进程已附加,OEP地址已自动填好(如果不对,手动修改为OEP的RVA)。
  4. 点击 "IAT Autosearch" 按钮,Scylla会自动寻找并定位导入地址表。
  5. 点击 "Get Imports" 获取所有导入函数。
  6. 点击 "Dump" 将内存转储到文件(例如 dumped.exe)。
  7. 点击 "Fix Dump",选择刚才的 dumped.exe,Scylla会尝试修复导入表,并生成一个新文件(例如 dumped_SCY.exe)。

阶段四:去虚拟化 (De-virtualization) - 最核心、最艰难的一步

仅仅Dump出文件是远远不够的,因为关键逻辑(如注册码验证)仍然是被虚拟化的字节码。去虚拟化的目标就是理解虚拟机的设计并还原原始逻辑。

  1. 识别VM Entry:在dump出的文件中,用IDA Pro打开。找到调用虚拟化代码的地方。通常这个地方会有一系列PUSH指令(保存环境),然后是一个JMP到一个非常复杂混乱的区域,这个区域就是VM Entry。

  2. 分析VM架构:这是纯粹的逆向工程体力活。

    • 定位Dispatcher:VM Entry之后,你会找到一个核心的指令分发器。它通常是一个循环,根据字节码的值跳转到不同的Handler。
    • 分析Handlers:以Dispatcher为中心,逐个分析它跳转到的每一个Handler。
      • 在x64dbg中对某个Handler下断点,观察它执行前后对虚拟环境(通常在栈上分配的一块内存)做了什么修改。
      • 例如,你发现某个Handler总是把虚拟栈顶的两个值相加,那么你就可以把它标记为 V_ADD
      • 另一个Handler可能是从某个地方加载一个常量,你可以标记为 V_LOAD_CONST
    • 重复此过程:你需要分析出尽可能多的Handler的功能,为它们命名。这个过程极其枯燥,可能需要分析几十到上百个Handlers。
  3. 提取并反编译字节码:

    • 当你大致搞清楚了虚拟机的指令集后,就可以在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)

  4. 还原逻辑并Patch:

    • 从翻译出的伪代码中,你就能看懂原始的验证逻辑了。
    • 暴力破解:找到关键的跳转,例如 V_JNE_FAIL,然后在实现这个虚拟跳转的真实x64代码处,将其修改(如 JNE 改成 JMP 或 NOP),从而绕过验证。这通常是最高效的方法。
    • 算法还原:如果你的目标是写注册机,那就需要完全逆向整个算法,这难度极高。

阶段五:修复与清理

  1. 修复其他VMP“陷阱”:如完整性检查(CRC Check)。你需要找到计算CRC的地方,然后将其NOP掉,或者在Patch后重新计算正确的CRC值并填回去。
  2. 测试:运行你最终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 三大核心保护技术

text

┌─────────────────────────────────────────────────────┐ │          VMProtect 2 保护技术架构图                  │ ├─────────────────────────────────────────────────────┤ │                                                     │ │  ┌─────────────┐  ┌──────────────┐  ┌───────────┐ │ │  │ Virtualization│  │  Mutation    │  │  Packing  │ │ │  │   虚拟化      │  │   变异混淆    │  │   加壳    │ │ │  └─────────────┘  └──────────────┘  └───────────┘ │ │         ↓                ↓                 ↓        │ │  ┌─────────────────────────────────────────────┐   │ │  │         Anti-Debug & Anti-Dump              │   │ │  │         反调试 & 反转储                      │   │ │  └─────────────────────────────────────────────┘   │ │                                                     │ └─────────────────────────────────────────────────────┘


1.2 虚拟化保护 (Virtualization) - 最强大的功能

原理图解

text

原始代码(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(虚拟机退出)                      │ │  • 恢复真实寄存器                                     │ │  • 返回到未保护代码                                   │ └──────────────────────────────────────────────────────┘

虚拟化的"恐怖"之处

C

// 原始代码: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)

代码膨胀技术

asm

原始代码:     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)

text

原始代码:     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)

text

原始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 反调试与反分析

多层反调试技术清单

text

╔══════════════════════════════════════════════════════════╗ ║              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 脱壳总体流程图

text

┌─────────────────────────────────────────────────────────────┐ │                    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 阶段一:环境准备与工具配置

必备工具清单

text

┌──────────────────────────────────────────────────────────┐ │ 工具类型          工具名称                  用途           │ ├──────────────────────────────────────────────────────────┤ │ 调试器            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 配置示例

text

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 定律(最常用)

text

原理: 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: 内存断点法

text

步骤: 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: 异常断点法

text

原理:VMP使用大量异常作为混淆手段,最后一个异常通常在OEP附近  步骤: 1. x64dbg → 选项 → 首选项 → 异常    - 取消"忽略所有异常"    - 只保留"忽略INT 3"  2. F9 运行,每次异常断下时:    - 查看当前位置是否在 .text 段    - 如果不是,按 Shift+F9 传递异常     3. 重复多次后,最终会停在接近OEP的地方  4. 配合反汇编判断是否为OEP

如何确认找到的是真正的 OEP?

text

特征检查清单: ✓ 代码段名称是 .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

text

步骤: 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)

text

在 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修复原理

text

正常程序的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 自动修复

text

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(高级)

text

工具: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

text

特征码搜索法: 在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 架构

text

目标:理解这个虚拟机的"指令集"  ┌─────────────────────────────────────────┐ │          VM 架构分析清单                 │ ├─────────────────────────────────────────┤ │ 1. 虚拟寄存器 (Virtual Registers)        │ │    - 在哪里存储?(栈上/全局内存)       │ │    - 有多少个?(通常8-16个)            │ │    - 如何访问?                          │ ├─────────────────────────────────────────┤ │ 2. 虚拟栈 (Virtual Stack)                │ │    - Stack Pointer 在哪?                │ │    - PUSH/POP 如何实现?                 │ ├─────────────────────────────────────────┤ │ 3. 字节码 (Bytecode)                     │ │    - 存储位置                            │ │    - 读取方式                            │ │    - 格式:定长?变长?                  │ ├─────────────────────────────────────────┤ │ 4. Dispatcher (调度器)                   │ │    - 入口地址                            │ │    - 解码逻辑                            │ │    - 跳转表结构                          │ ├─────────────────────────────────────────┤ │ 5. Handlers (处理器)                     │ │    - 数量统计                            │ │    - 功能分类                            │ │    - 参数传递方式                        │ └─────────────────────────────────────────┘

5.3 提取 VM Handlers

text

方法:动态跟踪 + 静态分析结合  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 反编译字节码

text

步骤一:提取字节码 ----------------- 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 重建原始逻辑(最终目标)

text

输入:反编译的字节码  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 删除反调试代码(暴力法)

text

在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 修复重定位表

text

使用 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 测试流程

text

1. 静态测试    - CFF Explorer 打开,检查PE结构完整性    - ImportREC 验证导入表     2. 沙箱测试    - 虚拟机中运行    - 检查是否崩溃    - Process Monitor 监控行为     3. 功能测试    - 主要功能是否正常    - 是否还有反调试触发    - 是否有完整性检查  4. 对比测试    - 与原始程序对比API调用序列    - 对比关键算法输出


📌 Part 3: 实战案例分析

案例:破解一个简单的VMP2保护程序

text

目标程序: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 自动化去虚拟化工具

text

工具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 对抗完整性检查

text

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%

关键建议:

  1. 从简单目标开始练习
  2. 理论+实践结合
  3. 善用自动化工具
  4. 专注于关键逻辑,不要试图还原全部
  5. 考虑替代方案(如内存patch而不是文件patch)

最后强调:将知识用于合法的安全研究和学习,尊重软件版权!

 

 

 

这是一个非常好的问题,也是无数逆向工程爱好者和安全研究人员梦寐以求的“圣杯”。

答案非常明确且残酷:不存在,也几乎不可能存在一个能够自动化、通用、完美修复VMProtect加密程序的“万能工具”。

下面我将详细解释为什么这在技术上是一个近乎无解的难题,以及为什么你看到的所谓“工具”都只是辅助性的。


第一部分:为什么“万能工具”是一个幻想?

要理解这一点,我们必须区分**“加壳器 (Packer)”** 和 “虚拟化保护器 (Virtualizer)” 的根本区别。

1. 简单的加壳器 (如 UPX)

  • 工作原理:像一个zip压缩程序。它有一套固定的算法来压缩原始代码,然后在程序入口点放一小段固定的解压代码。
  • 脱壳原理:因为解压算法是固定的,所以可以写一个固定的“解压”工具(脱壳机)。UPX -d 就是这样一个万能脱壳工具,因为它完全了解UPX的压缩算法。

2. VMProtect (虚拟化保护器)

  • 工作原理:它不是一个zip程序,而是一个**“语言发明工厂”**。
    • 当你用VMProtect保护你的程序时,VMP会为你程序的关键代码发明一种全新的、独一无二的、随机的编程语言(字节码)。
    • 同时,它还会为你**生成一个唯一的解释器(虚拟机VM)**来执行这种新语言。
  • 核心特性:无限变种 (Infinite Variation)
    • 不同的程序,不同的VM:用VMP保护A.exeB.exe,会得到两个完全不同的虚拟机。A.exe里的VM不认识B.exe的字节码,反之亦然。
    • 同一程序,每次编译都不同:你用完全相同的设置,两次保护同一个A.exe,得到的A_v1.exeA_v2.exe,它们内部的虚拟机和字节码也是不一样的。

把这个概念具象化:

想象一下,你想写一个“万能翻译器”。

  • 对于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”的工具,比如VMPDumpVMP Unpacker by VT,或者一些开源脚本。它们都不是“万能工具”,而是辅助分析的脚本或小程序,作用非常有限。

  1. 自动化Dump工具 (如VMPDump)

    • 能做什么:尝试自动完成“寻找OEP”和“内存转储”这两个最最基础的步骤。
    • 不能做什么:完全不能去虚拟化。它Dump出来的文件,关键代码依然是VMP的字节码。
    • 成功率:对非常古老、保护选项弱的VMP版本可能有效。对现代VMP版本基本100%失败。
  2. 分析辅助脚本 (如IDAPython脚本)

    • 能做什么:帮助人类分析师提高效率。例如,一个脚本可以自动地:
      • 追踪VM Dispatcher,并列出所有VM Handler的地址。
      • 为每个Handler打上标签,方便你逐个分析。
      • 尝试对字节码进行初步的染色或分组。
    • 不能做什么:它不能告诉你某个Handler是“加法”还是“减法”。这个最关键的、需要智力判断的工作,仍然需要你手动完成。
  3. 学术研究型工具 (基于符号执行、污点分析)

    • 能做什么:这是最前沿的尝试。通过模拟执行每一条指令,并追踪数据的流动(污点分析),理论上有可能推断出部分代码的原始逻辑。
    • 不能做什么:
      • 极度缓慢:分析一小段VMP代码可能需要几个小时甚至几天。
      • 路径爆炸:遇到复杂的分支,需要探索的路径呈指数级增长,很快就会耗尽所有计算资源。
      • 使用复杂:需要博士级别的专业知识来配置和使用,绝非“一键运行”。

结论

放弃寻找“自动化脱壳VMP的万能工具”的想法。

对抗VMProtect的唯一途径,是将其视为一场人与机器的智力对抗。真正的“工具”是:

  1. 你的大脑:你的逆向工程知识、逻辑推理能力和分析经验。
  2. 强大的调试器和反汇编器:如 x64dbg 和 IDA Pro,它们是你的眼睛和手术刀。
  3. 无尽的耐心:去虚拟化是一个极其枯燥和漫长的过程,以周或月为单位计算。

如果你真的想攻克它,正确的道路是学习扎实的逆向工程基础,而不是在网上寻找那个不存在的“魔法按钮”。

 

加密程序的执行流程

解密流程

1.脱壳流程

进入程序的真实入口

dump内存文件

修复pe数据、修复iat

重定位及其他检查

应用安全 --- win安全 之 VMP初体验

 

这里壳的作用就是解压和解密代码段和数据段和资源段。入口点变为了虚假的壳入口,而不是代码段的mian函数。

什么是修复

目标是使ida可以正常工作,可以静态分析。
保证pe结构关键数据的完整及正确。
保证对数据的引用正确,全局变量涉及重定位。
保证跳转指令的跳转地址正确,cal、jmp指令涉及重定位。
保证api的调用正确,涉及iat的修复

应用安全 --- win安全 之 VMP初体验

 

 

应用安全 --- win安全 之 VMP初体验

 

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

应用安全 --- win安全 之 VMP初体验

 同时我们知道OEP的位置在dump中的ida程序中是没有建立函数识别的,因为没有任何一段函数会引用这段函数,基本可以定位到这个OEP位置

壳分析法:根据壳代码查找这里需要我们熟悉壳代码的流程,知道壳代码将控制权转交给OEP的位置。

API方法;

应用安全 --- win安全 之 VMP初体验

 

 

应用安全 --- win安全 之 VMP初体验

 我识别是什么程序我们可以dump内存获取特征

这里我用

https://www.unpac.me/results/556bdbc0-32f7-467f-ad28-42d5cf9112be

上传样本自动分析流程发现特折

 

应用安全 --- win安全 之 VMP初体验

 

 

应用安全 --- win安全 之 VMP初体验

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

 

 

应用安全 --- win安全 之 VMP初体验

 

 

应用安全 --- win安全 之 VMP初体验

 

 

 

破解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修复转存。

发表评论

评论已关闭。

相关文章