GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- 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 = "\x3F\x12\x7A...";  // 加密的 "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表

image

 

 

字符串加密 (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% 减小体积

 




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

  

image

 

 

 

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\0\0" (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\0\0\0"           |  <- 节区名称
|   - VirtualAddress:               |  <- 内存中的 RVA
|   - SizeOfRawData:                |  <- 在文件中的大小
|   - PointerToRawData:             |  <- 在文件中的偏移
|   - Characteristics:              |  <- 节区属性 (如 可执行、可读)
+-----------------------------------+
|     IMAGE_SECTION_HEADER[1]       |  <- 第二个节区头 (如 .data)
|   - Name: ".data\0\0\0"           |
|   - ...                           |
+-----------------------------------+
|             ...                   |  <- 其他节区头 (共 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\0\0" 的签名,标识这是一个 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\0\0"                             │  ← 真正的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\0\0" (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特征和易语言

image

 

image

 

 

image

 

image

 

 

image

 

image

 

image

 

image

 

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

 

image

 

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
   - 解压到 x64dbg\plugins\
   - 重启 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

重定位及其他检查

image

 

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

什么是修复

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

image

 

 

image

 

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

image

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

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

API方法;

image

 

 

image

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

这里我用

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

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

 

image

 

 

image

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

 

 

image

 

 

image

 

 1.执行后第一次会自动断点到系统库nt停止,点击执行后第二次第三次,执行是gdi库,第四此就进入到了软件的vmp段,也就是正式进入了壳中。

image

 2.ctrl+g 输入 vs2017的入口函数特征 GetSystemTimeAsFileTime,点击定位后双击进入这个api,

image

 双击进入这个api,在第二个汇编双击下断点。因为vmp会检测第一句代码。 然后运行

3.继续运行,我们在堆栈返回函数地址,检查段区域,发现在vmp中,说明vmp中也用了这个api,我们跳过这个

4.继续运行,我们在堆栈返回函数地址,检查段区域,发现在text段中,我们右键在反汇编展示这个段汇编定位到返回地址,也就是说调用这个api的入口函数就在这里附近。

image

 我们在真实入口OEP下断即可,并记录oep地址,取消api断点,重启这个程序并重新执行到这个断点处,也就是text的用f4执行到GetSystemTimeAsFileTime的下一条指令f4。我们也就完成了vmp段的执行将text段的代码解压也就完成了。这里为什么我们要下断后重启程序,因为我们下断的oep位置已经执行完毕了代码,数据已经发生了变化,为了保持原始数据,我们必须在oep处下断重启执行。

image

 我们查看上一个栈的返回值,我们右键同步反汇编试图发现这里就是真正的oep位置

因此我们清楚了调用流程是

vmp到 txt的oep 再到 txt的api,我们现在在api下。

为了验证猜想,我们下断回退代码。

我们在txt的api下一个返回地址下端,执行,单步执行,发现返回到了oep,同样的操作就返回的了壳子的调用oep的位置。这里就不演示了。

 

dump文件整数据保存在文件中。相当于模块内存的将加载到内存中的模块的完一个拷贝。
查看dump文件需要16进制编辑器,如winhex

 因为加了壳的程序,原代码和数据是以压缩或者加密的形式保存在文件中。只有程序运行到OEP时,内存中才会有完整的原代码和原数据。所以我们需要在OEP处dump模块。

 

思考一个问题:是不是OEP以后的位置都可以dump?
答案:不是的,需要在处理数据之前,我们还要保证数据段是初始的状态。

 当EIP在EP时原代码段是没有数据的,所以这里dump它,是没有意义的。

 当EIP在OEP时,其实OEP就在text段说明代码段已经有了数据。

 我们已经找到了OEP的位置我们直接到这里,通过比较框架代码,OEP的指令是"call,jmp我们通过栈回溯到OEPMVIO OA/S

 

 第二部修复ida

 

image

 因为vmp保护的程序,它的导入表是壳的导入表,不是原程序的导入表。我们dump下来后,代码和数据有了,但是代码中的API调用是错误的。脱壳后,壳的导入表就失去作用,可以去掉它,

image

 

image

 

 

 

修复vmcall,就是修复代码中的call调用

xx vm iat x32插件发,主要用于修复vmp保护的API调用。

我们在od里操作因为插件在x64dbg有问题,打开软件运行目标程序。

ctrl+g 输入api名称GetSystemTimeAsFileTime,下一个汇编处下断点运行

发现在vmp段里,再次运行发现在text段内

在栈返回地址右键同步汇编代码,在汇编试图右键取消分析显示汇编代码,我们在8eb处下断点

开始修复call

image

 

image

 

image

 在按钮区域点击I,查看日志,显示成功

image

 我们点击窗口标题后完成所有修复

image

 

 

dump内存

https://down.52pojie.cn/Tools/PEtools/?amp%3BO=A

Scylla.v.0.9.8.rar

这有两种方法,一个插件,一个可执行,

我们用右键管理员执行可执行的32或64位exe,具体位数取决于你的目标程序位数,打开选择这个程序,并填入oep虚拟地址地址,点击dump

image

 

修复pe

image

 这里的模块基地址和内存的一致。因为重定位要固定模块地址,不在修复模块偏移。

 

 

 

image

 将40改为00,去除dll属性重定位。

 

image

 我们看到DlCharacteristics是0x8140,有模块重定位标志0x40,我们去掉它,改成详情我们在第6节0x8100,讲解。

 

修复IAT

uifUniversal lmport Fixer,在内存中重建导入地址表if,

image

 修复完成后不要关闭

 

 

 

 

修复IT
lmports Fixer,重建dump文件的导入表

image

 

image

注意上面是oep是偏移  

点击修复dump,选这上次的dump出的文件,修复即可生成if.dump文件

 

 

image

 

image

 

image

 

image

 

image

 我们上一节中,修复完iat,程序就已经可以运行了,看起来不用修复重定位了,其实我们是使用了比较简便的办法。我们回想一下ageBase是0x00400000,并且我我们修复PE数据时,看到Im的模块重定位标志,dump文件时们去掉了DlCharacteristics中内存中模块地址是0x00400000,而dump文件的寻址指令中的立000对应的,所以我们只要让dump即数都是和模块地址0x00400文件加载到0x00400000这个也址,所有的寻址指令就都对上了,就可以正常运行了。

 指令中API的调用我们也是因为固定了模块地址。

 

壳代码隐藏了原程序的重定位数据,我们看不到重定位表
想要找到原程序的重定位表会非常困难,学会了虚拟代码还原之后我们就可以做到。
对于exe,我们可以使用固定模块地址的方法,d则不可以。
重定表的结构是一样的。前面我vmp修改了的重定位的数据定义,们看到正常的重定位数据是16位,高4位为标志,低12位为页偏移vmp自己的重定位数据,高12位为页偏移,低4位为标志

 

资源检查
例子中没有使用vmp资源加密。关于资源的方面的知识点,可以查看相关文档资料进行学习。

 

image

 

 

image

 

 

 

 

image

 

 

image

 

image

 

image

 

image

 

image

 

image

 

image

 

image

 

 

image

 

image

 

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

posted on 2025-10-25 18:41  GKLBB  阅读(43)  评论(0)    收藏  举报