PE32格式学习之三——一个简单的PE壳
字数 2165 2025-08-22 22:47:30
PE32格式学习之三:一个简单的PE壳
1. 加壳技术概述
加壳是指通过程序改变被加壳程序,增加逆向分析和破解难度的技术。主要技术包括:
- 加密
- 压缩
- 混淆
- 反调试
本文重点介绍两种简单的PE加壳方法:
- 仅加密.code和.data节的壳
- 能加密导入表的壳
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)
解密代码需要:
- 解密.code和.data节
- 跳转回原始入口点
汇编实现:
; 解密.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 关键修改步骤
- 部署unpack code:写入.code节末尾(FOA=0x3D9)
- 修改入口点:EntryPoint RVA = 0x3D9 - 0x200 + 0x1000 = 0x11D9
- 修改节属性:.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节
- 修改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代码实现
; 解密.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 关键修改步骤
- 部署unpack code:写入.unpack节(FOA=0x500, RVA=0x2100)
- 修改入口点:AddressOfEntryPoint=0x2100
- 设置导入表:Import Table RVA=0x203C
4. 关键知识点总结
- PE节加密:通常加密代码节和数据节,需保留导入表
- 解密代码:
- 需在内存中解密加密内容
- 需处理导入表重建(如需)
- 最后跳转原始入口点
- 节属性修改:解密代码所在节需有可写权限
- 导入表处理:
- 简单壳可保留原始导入表结构
- 复杂壳需重建导入表,通常需要LoadLibraryA和GetProcAddress
- RVA/FOA转换:关键地址需正确转换
5. 扩展思考
- 如何实现压缩壳?
- 如何添加反调试功能?
- 如何实现多阶段解密?
- 如何对抗动态分析?
通过这个简单的PE壳实现,可以深入理解PE文件格式和加壳技术基本原理,为进一步研究更复杂的保护技术打下基础。