PE32格式学习之三——一个简单的PE壳
字数 2165 2025-08-22 22:47:30

PE32格式学习之三:一个简单的PE壳

1. 加壳技术概述

加壳是指通过程序改变被加壳程序,增加逆向分析和破解难度的技术。主要技术包括:

  • 加密
  • 压缩
  • 混淆
  • 反调试

本文重点介绍两种简单的PE加壳方法:

  1. 仅加密.code和.data节的壳
  2. 能加密导入表的壳

2. 仅加密.code和.data节的壳实现

2.1 加密过程

使用异或0x7F加密.code和.data节:

key = 0x7F
with open("PE.exe", 'rb') as f:
    pe = bytearray(f.read())
    
# .code节加密 FOA 0x200, size 0x200
for i in range(0x200, 0x400):
    pe[i] = pe[i] ^ key
    
# .data节加密 FOA 0x400, size 0x200
for i in range(0x400, 0x600):
    pe[i] = pe[i] ^ key
    
with open('packed_PE.exe', 'wb') as f:
    f.write(pe)

2.2 解密代码(unpack code)

解密代码需要:

  1. 解密.code和.data节
  2. 跳转回原始入口点

汇编实现:

; 解密.code节
mov eax, 0x401000    ; .code节RVA
mov ecx, 0x1D9       ; 解密大小(需预留unpack code空间)
start1:
    xor byte ptr [eax], 0x7F
    inc eax
    loop start1

; 解密.data节
mov eax, 0x402000    ; .data节RVA
mov ecx, 0x200       ; .data节大小
start2:
    xor byte ptr [eax], 0x7F
    inc eax
    loop start2

; 跳转原始入口点
mov eax, 0x401000
jmp eax

二进制形式:
{0xB8,0x00,0x10,0x40,0x00,0xB9,0xD9,0x01,0x00,0x00,0x80,0x30,0x7F,0x40,0xE2,0xFA,0xB8,0x00,0x20,0x40,0x00,0xB9,0x00,0x02,0x00,0x00,0x80,0x30,0x7F,0x40,0xE2,0xFA,0xB8,0x00,0x10,0x40,0x00,0xFF,0xE0}

2.3 关键修改步骤

  1. 部署unpack code:写入.code节末尾(FOA=0x3D9)
  2. 修改入口点:EntryPoint RVA = 0x3D9 - 0x200 + 0x1000 = 0x11D9
  3. 修改节属性:.code节Characteristics改为0xE0000060(可执行、可读、可写)

3. 能加密导入表的壳实现

3.1 加密过程

加密三合一节(.mixed节):

key = 0x7F
with open("min_PE1.1.exe", 'rb') as f:
    pe = bytearray(f.read())
    
# .mixed节加密 FOA 0x200, size 0x200
for i in range(0x200, 0x400):
    pe[i] = pe[i] ^ key
    
with open('packed_PE1.1.exe', 'wb') as f:
    f.write(pe)

3.2 增加.unpack节

  1. 修改NumberOfSections为2
  2. 添加Section Header:
    • Name: ".unpack"
    • VirtualSize: 0x1000
    • VirtualAddress: 0x2000
    • SizeOfRawData: 0x200
    • PointerToRawData: 0x400
    • Characteristics: 0xE0000060
  3. 调整文件大小:增加0x200字节
  4. 修改SizeOfImage: 0x2000→0x3000

3.3 构建unpack导入表

FOA RVA 大小 内容 说明
0x400 0x2000 13 "kernel32.dll\0" DLL名称
0x40E 0x200E 2 0x00 Hint
0x410 0x2010 13 "LoadLibraryA\0\0" 函数名
0x41E 0x201E 2 0x00 Hint
0x420 0x2020 15 "GetProcAddress\0" 函数名
0x430 0x2030 4 0x200E LoadLibrary的Hint/Name Entry
0x434 0x2034 4 0x201E GetProcAddress的Hint/Name Entry
0x438 0x2038 4 0x00 ILT结束标志
0x43C 0x203C 4 0x2030 ILT RVA
0x440 0x2040 4 0x00 Time/Date Stamp
0x444 0x2044 4 0x00 Forwarder Chain
0x448 0x2048 4 0x2000 Name RVA
0x44C 0x204C 4 0x2030 IAT RVA
0x450 0x2050 20 0x00 结束标志

3.4 unpack代码实现

; 解密.mixed节
mov eax, 0x401000
mov ecx, 0x200
start:
    xor byte ptr [eax], 0x7F
    inc eax
    loop start

; 加载user32.dll
push 0x401150    ; "user32.dll"字符串地址
call DWORD ptr ds:0x402030  ; LoadLibraryA

; 获取MessageBoxA地址
push 0x40115D    ; "MessageBoxA"字符串地址
push eax         ; LoadLibraryA返回值
call DWORD ptr ds:0x402034  ; GetProcAddress

; 更新原始IAT
mov dword ptr [0x401148], eax

; 跳转原始入口点
mov eax, 0x401000
jmp eax

二进制形式:
{0xB8,0x00,0x10,0x40,0x00,0xB9,0x00,0x02,0x00,0x00,0x80,0x30,0x7F,0x40,0xE2,0xFA,0x68,0x50,0x11,0x40,0x00,0xFF,0x15,0x30,0x20,0x40,0x00,0x68,0x5D,0x11,0x40,0x00,0x50,0xFF,0x15,0x34,0x20,0x40,0x00,0xA3,0x48,0x11,0x40,0x00,0xB8,0x00,0x10,0x40,0x00,0xFF,0xE0}

3.5 关键修改步骤

  1. 部署unpack code:写入.unpack节(FOA=0x500, RVA=0x2100)
  2. 修改入口点:AddressOfEntryPoint=0x2100
  3. 设置导入表:Import Table RVA=0x203C

4. 关键知识点总结

  1. PE节加密:通常加密代码节和数据节,需保留导入表
  2. 解密代码
    • 需在内存中解密加密内容
    • 需处理导入表重建(如需)
    • 最后跳转原始入口点
  3. 节属性修改:解密代码所在节需有可写权限
  4. 导入表处理
    • 简单壳可保留原始导入表结构
    • 复杂壳需重建导入表,通常需要LoadLibraryA和GetProcAddress
  5. RVA/FOA转换:关键地址需正确转换

5. 扩展思考

  1. 如何实现压缩壳?
  2. 如何添加反调试功能?
  3. 如何实现多阶段解密?
  4. 如何对抗动态分析?

通过这个简单的PE壳实现,可以深入理解PE文件格式和加壳技术基本原理,为进一步研究更复杂的保护技术打下基础。

PE32格式学习之三:一个简单的PE壳 1. 加壳技术概述 加壳是指通过程序改变被加壳程序,增加逆向分析和破解难度的技术。主要技术包括: 加密 压缩 混淆 反调试 本文重点介绍两种简单的PE加壳方法: 仅加密.code和.data节的壳 能加密导入表的壳 2. 仅加密.code和.data节的壳实现 2.1 加密过程 使用异或0x7F加密.code和.data节: 2.2 解密代码(unpack code) 解密代码需要: 解密.code和.data节 跳转回原始入口点 汇编实现: 二进制形式: {0xB8,0x00,0x10,0x40,0x00,0xB9,0xD9,0x01,0x00,0x00,0x80,0x30,0x7F,0x40,0xE2,0xFA,0xB8,0x00,0x20,0x40,0x00,0xB9,0x00,0x02,0x00,0x00,0x80,0x30,0x7F,0x40,0xE2,0xFA,0xB8,0x00,0x10,0x40,0x00,0xFF,0xE0} 2.3 关键修改步骤 部署unpack code :写入.code节末尾(FOA=0x3D9) 修改入口点 :EntryPoint RVA = 0x3D9 - 0x200 + 0x1000 = 0x11D9 修改节属性 :.code节Characteristics改为0xE0000060(可执行、可读、可写) 3. 能加密导入表的壳实现 3.1 加密过程 加密三合一节(.mixed节): 3.2 增加.unpack节 修改NumberOfSections为2 添加Section Header: Name: ".unpack" VirtualSize: 0x1000 VirtualAddress: 0x2000 SizeOfRawData: 0x200 PointerToRawData: 0x400 Characteristics: 0xE0000060 调整文件大小:增加0x200字节 修改SizeOfImage: 0x2000→0x3000 3.3 构建unpack导入表 | FOA | RVA | 大小 | 内容 | 说明 | |-----|------|-----|------|------| |0x400|0x2000|13|"kernel32.dll\0"|DLL名称| |0x40E|0x200E|2|0x00|Hint| |0x410|0x2010|13|"LoadLibraryA\0\0"|函数名| |0x41E|0x201E|2|0x00|Hint| |0x420|0x2020|15|"GetProcAddress\0"|函数名| |0x430|0x2030|4|0x200E|LoadLibrary的Hint/Name Entry| |0x434|0x2034|4|0x201E|GetProcAddress的Hint/Name Entry| |0x438|0x2038|4|0x00|ILT结束标志| |0x43C|0x203C|4|0x2030|ILT RVA| |0x440|0x2040|4|0x00|Time/Date Stamp| |0x444|0x2044|4|0x00|Forwarder Chain| |0x448|0x2048|4|0x2000|Name RVA| |0x44C|0x204C|4|0x2030|IAT RVA| |0x450|0x2050|20|0x00|结束标志| 3.4 unpack代码实现 二进制形式: {0xB8,0x00,0x10,0x40,0x00,0xB9,0x00,0x02,0x00,0x00,0x80,0x30,0x7F,0x40,0xE2,0xFA,0x68,0x50,0x11,0x40,0x00,0xFF,0x15,0x30,0x20,0x40,0x00,0x68,0x5D,0x11,0x40,0x00,0x50,0xFF,0x15,0x34,0x20,0x40,0x00,0xA3,0x48,0x11,0x40,0x00,0xB8,0x00,0x10,0x40,0x00,0xFF,0xE0} 3.5 关键修改步骤 部署unpack code :写入.unpack节(FOA=0x500, RVA=0x2100) 修改入口点 :AddressOfEntryPoint=0x2100 设置导入表 :Import Table RVA=0x203C 4. 关键知识点总结 PE节加密 :通常加密代码节和数据节,需保留导入表 解密代码 : 需在内存中解密加密内容 需处理导入表重建(如需) 最后跳转原始入口点 节属性修改 :解密代码所在节需有可写权限 导入表处理 : 简单壳可保留原始导入表结构 复杂壳需重建导入表,通常需要LoadLibraryA和GetProcAddress RVA/FOA转换 :关键地址需正确转换 5. 扩展思考 如何实现压缩壳? 如何添加反调试功能? 如何实现多阶段解密? 如何对抗动态分析? 通过这个简单的PE壳实现,可以深入理解PE文件格式和加壳技术基本原理,为进一步研究更复杂的保护技术打下基础。