深入分析PE结构(二)
字数 1285 2025-08-07 08:22:20

深入分析PE结构(二) - 代码节空白区添加代码技术详解

0x0 前言

本教程详细讲解如何在PE文件的代码节空白区域手动添加自定义代码的技术原理和实现方法。通过这项技术,我们可以实现在程序执行时首先运行我们添加的代码(如弹窗演示),然后再跳转到原始程序入口继续执行。

0x1 技术原理

基本思路

  1. 修改程序入口点(OEP):修改PE文件可选头中的AddressOfEntryPoint参数,使其指向我们添加的代码
  2. 添加跳转指令:在我们添加的代码中使用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和后面待填充的硬编码
};

关键函数

  1. 读取PE文件到内存
DWORD ReadPEFile(IN LPSTR lpszFile, OUT LPVOID* pFileBuffer) {
    // 文件操作和内存分配代码
    // ...
}
  1. FileBuffer转ImageBuffer
DWORD CopyFileBufferToImageBuffer(IN LPVOID pFileBuffer, OUT LPVOID* pImageBuffer) {
    // PE结构解析和内存拉伸代码
    // ...
}
  1. ImageBuffer转NewBuffer
DWORD CopyImageBufferToNewBuffer(IN LPVOID pImageBuffer, OUT LPVOID* pNewBuffer) {
    // 内存到文件格式转换代码
    // ...
}
  1. 内存写入文件
BOOL MemeryTOFile(IN LPVOID pMemBuffer, IN size_t size, OUT LPSTR lpszFile) {
    // 文件写入操作
    // ...
}
  1. 主功能函数
VOID AddCodeInCodeSec() {
    // 完整实现添加ShellCode的流程
    // 1. 读取文件
    // 2. 转换内存格式
    // 3. 检查空白区空间
    // 4. 写入ShellCode
    // 5. 修正跳转偏移
    // 6. 修改OEP
    // 7. 写回文件
    // ...
}

0x4 实现细节详解

内存转换过程

  1. FileBuffer → ImageBuffer

    • 根据SizeOfImage分配内存空间
    • 拷贝PE头(包括Dos头、PE头、节表)
    • 按节表信息拷贝各节数据,考虑内存对齐
  2. ImageBuffer → NewBuffer

    • 计算文件所需空间:最后一个节的文件偏移+节对齐后长度
    • 拷贝PE头
    • 按节表信息拷贝各节数据,考虑文件对齐

关键计算公式

  1. call/jmp偏移计算

    真正要跳转的地址 = E8/E9指令地址 + 5 + X
    X = 目标地址 - (E8/E9指令地址 + 5)
    
  2. 空白区定位

    代码节空白区起始 = ImageBase + VirtualAddress + VirtualSize
    
  3. 新OEP计算

    新OEP = (DWORD)codeBegin - (DWORD)pImageBuffer
    

0x5 注意事项

  1. 空间检查:必须确保代码节有足够空白空间(至少18字节)

    if (((pSectionHeader->SizeOfRawData) - (pSectionHeader->Misc.VirtualSize)) < SHELLCODELENGTH) {
        printf("代码区域空闲空间不够\r\n");
        // 错误处理
    }
    
  2. 地址计算:所有地址计算必须考虑ImageBase

  3. 字节序:偏移量必须按小端序存储

  4. 节属性:确保代码节有可执行权限

0x6 完整流程总结

  1. 读取PE文件到FileBuffer
  2. 将FileBuffer转换为ImageBuffer
  3. 在ImageBuffer中定位代码节空白区
  4. 将ShellCode复制到空白区
  5. 修正ShellCode中的call和jmp偏移
  6. 修改OEP指向我们的ShellCode
  7. 将ImageBuffer转换回NewBuffer
  8. 将NewBuffer写入新文件

通过这项技术,我们可以实现PE文件的无损修改,在程序原有功能基础上添加自定义代码逻辑,为软件分析和安全研究提供了有力工具。

深入分析PE结构(二) - 代码节空白区添加代码技术详解 0x0 前言 本教程详细讲解如何在PE文件的代码节空白区域手动添加自定义代码的技术原理和实现方法。通过这项技术,我们可以实现在程序执行时首先运行我们添加的代码(如弹窗演示),然后再跳转到原始程序入口继续执行。 0x1 技术原理 基本思路 修改程序入口点(OEP) :修改PE文件可选头中的AddressOfEntryPoint参数,使其指向我们添加的代码 添加跳转指令 :在我们添加的代码中使用call指令调用目标函数(如MessageBoxA),代码执行完毕后使用jmp指令跳转回原始OEP 关键点 添加的代码必须是二进制硬编码形式 需要精确计算call和jmp指令的跳转偏移量 需要确保代码节有足够的空白空间存放我们的代码 0x2 手动分析实现 1. 获取MessageBoxA函数地址 记录下MessageBoxA函数地址: 0x77263670 2. 分析call和jmp指令编码 call指令 : E8 + 4字节偏移量 jmp指令 : E9 + 4字节偏移量 偏移量计算公式 : 示例计算: 3. 构造ShellCode MessageBoxA函数调用需要4个push 0参数: 总共18字节的ShellCode。 4. 寻找代码节空白区 使用PE工具分析节表: 计算空白区位置: 5. 计算跳转偏移 call指令偏移计算 : jmp指令偏移计算 : 6. 修改OEP 0x3 自动化实现代码 核心数据结构 关键函数 读取PE文件到内存 : FileBuffer转ImageBuffer : ImageBuffer转NewBuffer : 内存写入文件 : 主功能函数 : 0x4 实现细节详解 内存转换过程 FileBuffer → ImageBuffer : 根据SizeOfImage分配内存空间 拷贝PE头(包括Dos头、PE头、节表) 按节表信息拷贝各节数据,考虑内存对齐 ImageBuffer → NewBuffer : 计算文件所需空间:最后一个节的文件偏移+节对齐后长度 拷贝PE头 按节表信息拷贝各节数据,考虑文件对齐 关键计算公式 call/jmp偏移计算 : 空白区定位 : 新OEP计算 : 0x5 注意事项 空间检查 :必须确保代码节有足够空白空间(至少18字节) 地址计算 :所有地址计算必须考虑ImageBase 字节序 :偏移量必须按小端序存储 节属性 :确保代码节有可执行权限 0x6 完整流程总结 读取PE文件到FileBuffer 将FileBuffer转换为ImageBuffer 在ImageBuffer中定位代码节空白区 将ShellCode复制到空白区 修正ShellCode中的call和jmp偏移 修改OEP指向我们的ShellCode 将ImageBuffer转换回NewBuffer 将NewBuffer写入新文件 通过这项技术,我们可以实现PE文件的无损修改,在程序原有功能基础上添加自定义代码逻辑,为软件分析和安全研究提供了有力工具。