zer0daysec 发表于 2024-3-18 14:30:07

[原创] PE 文件结构剖析之区段目录

本帖最后由 zer0daysec 于 2024-3-18 14:32 编辑

在前两节中,我们讨论了 DOS 头和 NT 头。在这一节当中将讨论区段部分 (位于数据目录表后),这部分可看作区段的一个汇总目录,描述了各个区段的属性:



PE 文件最少要一个区段才能被加载运行,区段表由数个首尾相连的 IMAGEE_SECTION_HEADER 结构体数组组成,可以使用 IMAGE_FIRST_SECTION(NtHeader) 这个宏找到第一个区段所在的位置,winnt.h 中对此定义



此宏只需提供一个 NT 头结构参数即可,定义宏的上面有一条注释,意思为该宏不需要 32 或 64 位版本,我记得早期此结构有 32 位版,注释也说明了原因,无论 32 位的还是 64 位的,文件头都是相同的,区段结构体如下:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name;               // 1) 区段名
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;                         // 2) 区段大小
    DWORD   VirtualAddress;         // 3) 区段的 RVA 地址
    DWORD   SizeOfRawData;          // 4) 文件中的区段对齐大小
    DWORD   PointerToRawData;       // 5) 区段在文件中的偏移
    DWORD   PointerToRelocations;   // 6) 重定位的偏移(用于 OBJ 文件)
    DWORD   PointerToLinenumbers;   // 7) 行号表的偏移(用于调试)
    WORD    NumberOfRelocations;    // 8) 重定位表项数量(用于 OBJ 文件)
    WORD    NumberOfLinenumbers;    // 9) 行号表项数量
    DWORD   Characteristics;      // 10) 区段的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
FIELD_OFFSET 宏定义



根据注释可知该宏的功能是计算一个结构体中某个字段的偏移。

对于区段结构体也只说重要字段,Name 字段是一个为 8 字节的 ASCII 字符串数组,一般情况下以 "." 开始,但这并不是必须的 (以 $ 开头的同名区段会被合并),区段名称是可以自定义的,但微软对区段名称还是有一个约定俗成命名标准:



区段名一般是由编译器在自动编译链接时生成的,不过可以使用以下代码来自定义数据区段名:

// #pragma data_seg("secton_name");

// for example
#pragma data_seg("zer0day")
int z = 1;


VirtualSize 字段为实际使用的段大小,需要注意的是此字段未做对齐处理,在 obj 文件中为 0;

VirtualAddress 区段载入到内存后的 RVA,这个地址是做了对齐处理的 (内存对齐粒度);

SizeOfRawData 字段表示此区段在文件中大小,也是做了对齐处理 (文件对齐粒度),它与 VirtualSize 可能不同,发生这种情况原因有多种,当该区段加载到内存时,它不会遵循文件对齐方式,仅占用该区段的实际大小,在这种情况下,SizeOfRawData 将大于 VirtualSize,相反的情况也可能发生,如果该区段包含未初始化数据,这些数据将不会被记录在磁盘上,但是当该区段被映射到内存中时,该区段将扩展以预留内存空间以便稍后初始化和使用未初始化的数据,这说明了磁盘上的区段占用空间将小于内存中区段占用空间,在这种情况下 VirtualSize 将大于 SizeOfRawData。

PointerToRawData 字段表示区段在文件中的偏移;

PointerToRelocations 字段表示重定位表的偏移地址,指向 IMAGE_RELOCATION 结构体数组;

Characteristics 字段表示区段的属性,描述此区段的读写情况、状态等属性



属性值可以用 "|" 进行合并,例如 0xE0000020 就是 "0x20000000 | 0x40000000 | 0x80000000 | 0x00000020" 几种属性合并后的结果,表示的是这是一个可读、可写、可执行的区段。以下为代码解析

// 10.区段信息
std::cout << "\nSection Information" << std::endl;
std::cout << "Name\tVirtualSize\tVirtualAddress\tSizeOfRawData\tPointerRawData\tPointerToRelocations\tCharacteristics" << std::endl;
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
DWORD dwSectionNumber = pNtHeader->FileHeader.NumberOfSections;
for (DWORD dwIndex = 0; dwIndex < dwSectionNumber; ++dwIndex)
{
        std::cout << pSectionHeader->Name << "\t";
        std::cout << "0x" << std::hex << std::setfill('0') << std::setw(8) << pSectionHeader->Misc.VirtualSize << "\t";
        std::cout << "0x" << std::hex << std::setfill('0') << std::setw(8) << pSectionHeader->VirtualAddress << "\t";
        std::cout << "0x" << std::hex << std::setfill('0') << std::setw(8) << pSectionHeader->SizeOfRawData << "\t";
        std::cout << "0x" << std::hex << std::setfill('0') << std::setw(8) << pSectionHeader->PointerToRawData << "\t";
        std::cout << "0x" << std::hex << std::setfill('0') << std::setw(8) << pSectionHeader->PointerToRelocations << "\t\t";
        std::cout << "0x" << std::hex << std::setfill('0') << std::setw(8) << pSectionHeader->Characteristics << std::endl;
        pSectionHeader++;
}
运行结果与 DIE 检测结果作比较



区段为什么要对齐
因为这是微软规定的东西,每个区段与结构的起始位置都要遵循页对齐机制,在 32 位平台下,一个分页的大小为 4KB,所以无论是在内存中还是在文件中对齐,一般为 4KB 的整数倍 (但 PE 文件对此并无强制性规定,只要是 2 的整数倍即可),剩余有空多出的空间全部以 0 填充。

RVA 到 FOA 的转换
RVA (相对虚拟地址) 到 FOA (file offset address,文件偏移地址) 的转换,这个东西挺重要的,需要记住。

计算公式:


[*]FOA = VA - ImageBase - (所在区段的 RVA - 所在区段的 FOA)
[*]FOA = RVA - 所在区段的 RVA + 所在区段的 FOA

// RVA 转 FOA
DWORD Rva2Foa(PIMAGE_NT_HEADERS pNtHeader, DWORD dwRva)
{
    // 获取区段表的数量
    DWORD dwSectionNumber = pNtHeader->FileHeader.NumberOfSections;

    // 获取区段表数组的首元素
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);

    // 遍历所有的区段表找到符合要求的区段
    for (DWORD dwIndex = 0; dwIndex < dwSectionNumber; ++dwIndex)
    {
      // 要求:RVA >= 区段的首地址并且 RVA < 区段的结尾的地址
      if (dwRva >= pSectionHeader->VirtualAddress &&
            dwRva < (pSectionHeader->VirtualAddress + pSectionHeader->SizeOfRawData))
      {
            // FOA = VA - ImageBase - (所在区段的 RVA - 所在区段的 FOA)
            // FOA = RVA - 所在区段的 RVA + 所在区段的 FOA
            return dwRva - pSectionHeader->VirtualAddress + pSectionHeader->PointerToRawData;
      }
      pSectionHeader++;
    }

    // 如果找不到就返回 FALSE
    return FALSE;
}

// 11.测试
// 大家可以自行测试,有问题的留言
DWORD dwR2FRet = Rva2Foa(pNtHeader, 0x1000);
if (!dwR2FRet)
{
        std::cout << "[!] invalid address." << std::endl;
}
else
{
        std::cout << dwR2FRet << std::endl;
}
页: [1]
查看完整版本: [原创] PE 文件结构剖析之区段目录