深入分析PE结构(二)
字数 1285 2025-08-07 08:22:20
深入分析PE结构(二) - 代码节空白区添加代码技术详解
0x0 前言
本教程详细讲解如何在PE文件的代码节空白区域手动添加自定义代码的技术原理和实现方法。通过这项技术,我们可以实现在程序执行时首先运行我们添加的代码(如弹窗演示),然后再跳转到原始程序入口继续执行。
0x1 技术原理
基本思路
- 修改程序入口点(OEP):修改PE文件可选头中的AddressOfEntryPoint参数,使其指向我们添加的代码
- 添加跳转指令:在我们添加的代码中使用call指令调用目标函数(如MessageBoxA),代码执行完毕后使用jmp指令跳转回原始OEP
关键点
- 添加的代码必须是二进制硬编码形式
- 需要精确计算call和jmp指令的跳转偏移量
- 需要确保代码节有足够的空白空间存放我们的代码
0x2 手动分析实现
1. 获取MessageBoxA函数地址
bp MessageBoxA ; 设置断点
记录下MessageBoxA函数地址:0x77263670
2. 分析call和jmp指令编码
- call指令:
E8+ 4字节偏移量 - jmp指令:
E9+ 4字节偏移量
偏移量计算公式:
真正要跳转的地址 = E8/E9指令的下一行地址 + X
X = 真正要跳转的地址 - E8/E9指令的下一行地址
示例计算:
00401068 E8 98 FF FF FF call @ILT+0(Function) (00401005)
0040106D 68 1C 20 42 00 push offset string "Hello World!\n" (0042201c)
X = 00401005 - 0040106D = FFFFFF98
3. 构造ShellCode
MessageBoxA函数调用需要4个push 0参数:
6A 00 6A 00 6A 00 6A 00 ; 四个push 0
E8 00 00 00 00 ; call指令(偏移量待填充)
E9 00 00 00 00 ; jmp指令(偏移量待填充)
总共18字节的ShellCode。
4. 寻找代码节空白区
使用PE工具分析节表:
.rdata节:
VirtualSize: 0x00000180 ; 内存中大小(对齐前)
SizeOfRawData: 0x00000188 ; 文件中大小(对齐后)
PointerToRawData: 0x00000400 ; 文件中偏移
计算空白区位置:
对齐后地址 = 文件中偏移 + 文件中大小
= 400 + C600 = CA00
5. 计算跳转偏移
call指令偏移计算:
DWORD callAddr = (MESSAGEBOXADDR - (pOptionHeader->ImageBase + ((DWORD)(codeBegin + 0xD) - (DWORD)pImageBuffer)));
jmp指令偏移计算:
DWORD jmpAddr = ((pOptionHeader->ImageBase + pOptionHeader->AddressOfEntryPoint) - (pOptionHeader->ImageBase + ((DWORD)(codeBegin + SHELLCODELENGTH) - (DWORD)pImageBuffer)));
6. 修改OEP
pOptionHeader->AddressOfEntryPoint = (DWORD)codeBegin - (DWORD)pImageBuffer;
0x3 自动化实现代码
核心数据结构
BYTE ShellCode[] = {
0x6A,00,0x6A,00,0x6A,00,0x6A,00, // MessageBox push 0的硬编码
0xE8,00,00,00,00, // call汇编指令E8和后面待填充的硬编码
0xE9,00,00,00,00 // jmp汇编指令E9和后面待填充的硬编码
};
关键函数
- 读取PE文件到内存:
DWORD ReadPEFile(IN LPSTR lpszFile, OUT LPVOID* pFileBuffer) {
// 文件操作和内存分配代码
// ...
}
- FileBuffer转ImageBuffer:
DWORD CopyFileBufferToImageBuffer(IN LPVOID pFileBuffer, OUT LPVOID* pImageBuffer) {
// PE结构解析和内存拉伸代码
// ...
}
- ImageBuffer转NewBuffer:
DWORD CopyImageBufferToNewBuffer(IN LPVOID pImageBuffer, OUT LPVOID* pNewBuffer) {
// 内存到文件格式转换代码
// ...
}
- 内存写入文件:
BOOL MemeryTOFile(IN LPVOID pMemBuffer, IN size_t size, OUT LPSTR lpszFile) {
// 文件写入操作
// ...
}
- 主功能函数:
VOID AddCodeInCodeSec() {
// 完整实现添加ShellCode的流程
// 1. 读取文件
// 2. 转换内存格式
// 3. 检查空白区空间
// 4. 写入ShellCode
// 5. 修正跳转偏移
// 6. 修改OEP
// 7. 写回文件
// ...
}
0x4 实现细节详解
内存转换过程
-
FileBuffer → ImageBuffer:
- 根据SizeOfImage分配内存空间
- 拷贝PE头(包括Dos头、PE头、节表)
- 按节表信息拷贝各节数据,考虑内存对齐
-
ImageBuffer → NewBuffer:
- 计算文件所需空间:最后一个节的文件偏移+节对齐后长度
- 拷贝PE头
- 按节表信息拷贝各节数据,考虑文件对齐
关键计算公式
-
call/jmp偏移计算:
真正要跳转的地址 = E8/E9指令地址 + 5 + X X = 目标地址 - (E8/E9指令地址 + 5) -
空白区定位:
代码节空白区起始 = ImageBase + VirtualAddress + VirtualSize -
新OEP计算:
新OEP = (DWORD)codeBegin - (DWORD)pImageBuffer
0x5 注意事项
-
空间检查:必须确保代码节有足够空白空间(至少18字节)
if (((pSectionHeader->SizeOfRawData) - (pSectionHeader->Misc.VirtualSize)) < SHELLCODELENGTH) { printf("代码区域空闲空间不够\r\n"); // 错误处理 } -
地址计算:所有地址计算必须考虑ImageBase
-
字节序:偏移量必须按小端序存储
-
节属性:确保代码节有可执行权限
0x6 完整流程总结
- 读取PE文件到FileBuffer
- 将FileBuffer转换为ImageBuffer
- 在ImageBuffer中定位代码节空白区
- 将ShellCode复制到空白区
- 修正ShellCode中的call和jmp偏移
- 修改OEP指向我们的ShellCode
- 将ImageBuffer转换回NewBuffer
- 将NewBuffer写入新文件
通过这项技术,我们可以实现PE文件的无损修改,在程序原有功能基础上添加自定义代码逻辑,为软件分析和安全研究提供了有力工具。