freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

WindowsPE文件头详解及其编程 [原创]
xxxTea 2024-06-22 19:40:04 88636

Windows PE and PE文件头编程

基本概念

简介

PE(Portable Executeable File Format, 可移植的执行体文件格式),是一种用于可执行目标文件动态链接库的文件格式,主要是用于windows操作系统,使用该格式的目标是使链接生成的EXE文件能在不同的CPU工作指令下工作。

在Windows中的可执行程序有很多种,例如COM,PIF,SCR,EXE。但这些文件的格式大多继承自PE,其中,EXE是最常见的PE文件,动态链接库(dll)文件也是PE格式。

在Linux中,最常见的文件格式则是ELF文件格式。

地址

在PE文件结构中,一般会涉及四种地址,分别是:

  • 虚拟内存地址(VA)

  • 相对虚拟内存地址(RVA)

  • 文件偏移地址(FOA)

  • 基地址(Imagebase)

虚拟内存地址

当PE文件被操作系统载入内存之后,PE对应的进程所对应的虚拟空间,在这个空间中的地址即为虚拟地址,它是抽象地址,不是真实存在的。

基地址

当PE文件被载入内存后,其相关的动态链接库也会被载入。此时,被载入的文件称为模块(Module),映射文件的起始地址被称为模块句柄(hModule),可以通过模块句柄访问内存中的其他数据结构。这个初始内存地址也称为基地址(ImageBase)

基地址的作用是告诉操作系统应当在哪里开始存储该模块,不同模块的基地址一般是不同的。

相对虚拟内存地址

RVA是相对于基地址的偏移,即RVA是虚拟内存中用来定位某个特定位置的地址,该地址的值是这个特定位置距离某个模块基地址的偏移量,所以说,RVA是针对于某个模块存在的。

其中,VA = Imagebase + RVA 。

注意:RVA是针对于某个模块存在的,因此RVA是有范围的,从模块开始到模块结束,脱离该范围的RVA是无效的,称为越界。

文件偏移地址

FOA与内存无关,它是某个位置距离文件头的偏移量。使用WinHex等十六进制编辑器打开PE文件,看到的就是文件FOA。
image.png

对齐

对齐这个概念在很多文件格式中都有,在PE中,归类了三种对齐方式:数据在内存中的对齐,数据在文件中的对齐,资源文件中资源数据的对齐。

  1. 内存对齐
    由于Windows的内存管理机制(分页机制),内存一般以页为单位,所以PE文件的节在内存中的对齐单位也必须至少是一个页的大小。对于32位的操作系统来说,这个值是4KB(1000h);对于64位的操作系统来说,这个值是8KB(2000h)

  2. 文件对齐
    一般情况下,定义的节在文件中的对齐单位要远小于内存对齐的单位。通常会以512字节(200h)来作为对齐的单位

  3. 资源数据对齐
    在资源文件中,资源字节码部分一般要求以双字(4个字节)的方式对齐。

分页机制是现代操作系统常用的一种内存管理方式。通过将物理内存划分为固定大小的页(Page),并将进程的虚拟地址空间也划分为同样大小的页,使得虚拟地址到物理地址的转换可以通过页表(Page Table)进行映射。
页表是用来存储页的映射关系,即虚拟地址到物理地址的对应关系。页表中的每个条目称为页表项(Page Table Entry,PTE),其中包含了该页在物理内存中的地址和一些控制信息(如页是否在内存中、访问权限等)。

PE结构概述

PE结构简图

image.png

程序员眼中的PE结构。

image.png

如上图所示,一个标准的PE文件一般由四大部分组成:

  • DOS头

  • PE头(IMAGE_NT_HEADERS)

  • 节表(多个IMAGE_SECTION_HEADER结构)

  • 页内容

其中,PE头的数据结构最为复杂,简单来说,PE头包含:

  • 4个字节大小的标识符号(Signature)

  • 20个字节大小的基本头信息(IMAGE_FILE_HEADER)

  • 216个字节大小的扩展头信息(IMAGE_OPTIONAL_HEADER32)

若是按照“头部+身体”的信息组织方式来看:

PE文件头部 = DOS头 + PE头 + 节表

PE文件身体 = 节内容

节内容中会出现各种不同的数据结构,如导入表、导出表、资源表、重定位表等。

PE文件头部

DOS MZ 头

在Windows的PE格式中,DOS MZ头的定义如下:

image.png

主要为现代PE文件可以对早期的DOS文件进行良好兼容存在,其结构体为IMAGE_DOS_HEADER。

大小为64字节,其中2个重要的成员分别是:

  • e_magic:DOS签名(4D5A,MZ)

  • e_lfanew:指示NT头的偏移(文件不同,值不同)

在DOS MZ 头下面的是DOS Stub(DOS存根)。整个DOS Stub是一个字节块,其内容随着链接时使用的链接器不同而不同,PE中并没有与之对应的相关结构。

实例:

image.png

在恶意样本分析中,经常能遇到恶意软件寻找计算机中的PE可执行文件,如下:
image.png

PE头(NT头)

NT头部保存着 Windows 系统加载可执行文件的重要信息。NT头部由IMAGE_NT_HEADERS定义。

从该结构体的定义名称可以看出,IMAGE_NT_HEADERS由多个结构体组合而成,包括IMAGE_NT_SIGNATRUEIMAGE_FILE_HEADERIMAGE_OPTIONAL_HEADER三部分。

NT头部在PE文件中的位置不是固定不变的,NT头部的位置由DOS头部的e_lfanew字段给出。

当执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构的e_lfanew字段里找到NT头的起始偏移量,用其加上基址,得到PE文件头的指针。

定义:

image.png

PE头标识 Signature

PE文件标识,被定义为00004550h,对应于ASCII码的字符串是**“PE\0\0”**。

即上述结构体中的 Signature成员,它紧随在DOS Stub的后面,该标识的位置位于 IMAGE_DOS_HEADER.e_lfanew指向的位置。

如果更改这个文件标识,操作系统就无法把该文件识别成正确的PE文件。

image.png

标准PE头 IMAGE_FILE_HEADER

文件头 IMAGE_FILE_HEADER紧随在PE标识后面,在此位置往后二十个字节的内容为数据结构标准PE头的内容。

该结构在微软的官方文档中被称为 标准通过对象文件格式 (Common Object File Format,COFF)头。它记录了PE的全局属性,如该PE文件运行的平台,PE文件类型(EXE or DLL),文件中存在节的总数等。最重要的是它指出了下一个结构 IMAGE_OPTIONAL_HEADER32的大小。 其详细定义如下:

image.png

以该程序为例:

image.png
重点关注下 Machine成员,NumberOfSections成员以及SizeOfOptionalHeader成员和Characteristics成员。

Machine

+0004h,单字。

每个CPU都拥有的唯一的Machine码,用来指定PE文件的运行平台。这里是 0x8664,对应的就是AMD64CPU。

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_TARGET_HOST       0x0001  // Useful for indicating we want to interact with the host and not a WoW guest.
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2  // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33              0x01d3
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_ARM64             0xAA64  // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE

NumberOfSections

+0006h,单字。

这个成员指出文件中存在的节区数量。

TimeDateStamp

+0008h,双字。

编译器创建此文件的时间戳。

PointToSymbolTable

+000Ch,双字。

COFF符号表的文件偏移。

NumberOfSymbol

+0010h,双字。

符号表中元素的数目。

SizeOfOptionalHeader

+0014h,单字。

指出结构体IMAGE_OPTIONAL_HEADER32(32位系统)的长度。对于32位PE文件,这个域通常是00E0h;对于64位PE32+文件,这个域是00F0h。

注意:用户可以自定义这个值的大小

  • 更改完之后,要手动将IMAGE_OPTIONAL_HEADER32的大小扩充成你指定的值。

  • 扩充完之后,要维持文件中的对齐特性。

Characteristics

+0016h,单字。

标识文件属性,文件是否是可运行形态、是否为DLL等,以bit OR形式进行组合。

image.png

扩展PE头 IMAGE_OPTIONAL_HEADER

IMAGE_OPTIONAL_HEADER 结构有 32位 和 64位 的区别。以

typedef struct _IMAGE_OPTIONAL_HEADER 
{
    //
    // Standard fields.
    //
+18h    WORD    Magic;                   // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
+1Ah    BYTE    MajorLinkerVersion;      // 链接程序的主版本号
+1Bh    BYTE    MinorLinkerVersion;      // 链接程序的次版本号
+1Ch    DWORD   SizeOfCode;              // 所有含代码的节的总大小
+20h    DWORD   SizeOfInitializedData;   // 所有含已初始化数据的节的总大小
+24h    DWORD   SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h    DWORD   AddressOfEntryPoint;     // 程序执行入口RVA
+2Ch    DWORD   BaseOfCode;              // 代码的区块的起始RVA
+30h    DWORD   BaseOfData;              // 数据的区块的起始RVA
    //
    // NT additional fields.    以下是属于NT结构增加的领域。
    //
+34h    DWORD   ImageBase;               // 程序的首选装载地址
+38h    DWORD   SectionAlignment;        // 内存中的区块的对齐大小
+3Ch    DWORD   FileAlignment;           // 文件中的区块的对齐大小
+40h    WORD    MajorOperatingSystemVersion;  // 要求操作系统最低版本号的主版本号
+42h    WORD    MinorOperatingSystemVersion;  // 要求操作系统最低版本号的副版本号
+44h    WORD    MajorImageVersion;       // 可运行于操作系统的主版本号
+46h    WORD    MinorImageVersion;       // 可运行于操作系统的次版本号
+48h    WORD    MajorSubsystemVersion;   // 要求最低子系统版本的主版本号
+4Ah    WORD    MinorSubsystemVersion;   // 要求最低子系统版本的次版本号
+4Ch    DWORD   Win32VersionValue;       // 莫须有字段,不被病毒利用的话一般为0
+50h    DWORD   SizeOfImage;             // 映像装入内存后的总尺寸
+54h    DWORD   SizeOfHeaders;           // 所有头 + 区块表的尺寸大小
+58h    DWORD   CheckSum;                // 映像的校检和
+5Ch    WORD    Subsystem;               // 可执行文件期望的子系统
+5Eh    WORD    DllCharacteristics;      // DllMain()函数何时被调用,默认为 0
+60h    DWORD   SizeOfStackReserve;      // 初始化时的栈大小
+64h    DWORD   SizeOfStackCommit;       // 初始化时实际提交的栈大小
+68h    DWORD   SizeOfHeapReserve;       // 初始化时保留的堆大小
+6Ch    DWORD   SizeOfHeapCommit;        // 初始化时实际提交的堆大小
+70h    DWORD   LoaderFlags;             // 与调试有关,默认为 0 
+74h    DWORD   NumberOfRvaAndSizes;     // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
+78h    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

较为重要的成员:

Magic

魔术字,说明文件的类型,如果是010B,则表示该文件为PE32,若是0107,则表示文件是ROM映像;如果为020B,则表示该文件是PE32+,即64位下的PE文件。

AddressOfEntryPoint

程序执行的入口地址。该地址是一个相对虚拟地址,简称 EP (EntryPoint),这个值指向了程序第一条要执行的代码。程序如果被加壳后会修改该字段的值。在脱壳的过程中找到了加壳前该字段的值,就说明找到了原始入口点,原始入口点被称为OEP

该字段的地址指向的不是 main()函数的地址,也不是WinMain()函数的地址,而是运行库的启动代码的地址。

如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。

ImageBase

该字段指出PE映像的优先装入地址。如果可能的话(该地址没有被占用)那么操作系统会按照这个地址加载机器码到内存中,运行速度会快很多;若是该地址被其他模块占用,载入的文件就需要进行重定位操作

对于EXE文件,默认的装入地址是0x00400000。而对于DLL文件,默认装入地址是0x00100000。

SectionAlignment,FileAlignment

SectionAlignment字段指定了节被转入内存后的对齐单位。FileAlignment字段指定了字在文件中的对齐单位。

SizeOfImage

文件中整个PE文件的映射尺寸。以加载到内存中的HelloWorld.exe为例,HelloWorld.exe的文件头占用了1000h字节,三个字节各占用1000h字节,所以文件在内存中占用的空间总大小为4000h,该值可以比实际的值大,却不能笑,而且必须为SectionAlignment字段值的整数倍。

SizeOfHeaders

是MS-DOS头部、PE文件头、区块表的总尺寸。

Subsystem

一个标明可执行文件所期望的子系统(用户界面类型)的枚举值。

image.png

NumberOfRvaAndSize

数据目录的项数。一般为00000010h,即16个。

DataDirectory

数据目录结构。这是一个结构体数组,由16个相同的IMAGE_DATA_DIRECTORY结构组成,大小为字节,指向输出表、输入表、资源块等数据。详见下。

数据目录项 IMAGE_DATA_DIRECTORY

IMAGE_OPTIONAL_HEADER32(扩展PE头)结构的最后一个字段为DataDirectory。该字段定义了PE文件中出现的所有不同类型的数据的目录信息。

如前面所提过的,应用程序中的数据被按照用途分成很多种类,如导出表,导入表,资源,重定位表等。在内存中,这些数据被操作系统以页为单位组织起来,并赋以不同的访问属性;在文件中,这些数据页同余被组织起来,按照不同类别分别放在文件的指定位置。

该结构就是用来描述这些不同类别的数据在文件(和内存)中的位置及大小的。所以这个字段比较重要。

数据目录中定义的数据类型一共是16种,PE就是使用 IMAGE_DATA_DIRECTORY来定义每种数据的,该结构的定义如下:
image.png

俩个字段依次为VirtualAddress 和 isize ,如图所示,总的数据目录就由16个 IMAGE_DATA_DIRECTORY 连续排列一起组成。

image.pngimage.png

数据目录表项描述:
image.png

image.png

如上图3-11所示,如果想要查询特定类型的数据,就要从该结构开始。比如,想要查看PE中都调用了哪些动态链接库的函数,则需要从数据目录表的第二个元素(数组编号为1,)的IMAGE_DATA_DIRECTORY结构中获取导入表的起始位置和大小,再根据VirtualAddress_1地址指向的位置找到导入表相关的字节码。

节表项 IMAGE_SECTION_HEADER

从先前的PE结构图中可以知道,节表是由多个节表项,每个节表项记录了PE中与某个特定的节有关的信息,如节的属性,包括不同的特性、访问权限等。节表中,节的数量由 IMAGE_FILE_HEADER中的 NumberOfSections决定。

image.png

重要成员有4个:

  • VirtualSize:内存中节区所占大小

  • VirtualAddress:内存中节区起始地址(RVA)

  • SizeOfRawData:磁盘文件中节区所占大小

  • Charateristics:节区属性(bit OR)

PE文件头编程

PE内存映像

PE内存映像指的是PE文件被加载到内存后的组织方式。上面提到过Windows的内存管理机制,每个运行的程序都会有自己独立的运行空间。每个部分按照1000h的大小对齐。因此,PE文件映像会与PE内存映像有一个对应关系。

载入过程:

  • 读取PE头和节表:加载器首先读取PE头和节表,了解文件的结构和各个节的位置。

  • 分配内存:根据PE头中的信息,为程序分配适当的内存空间。

  • 加载各个节:将PE文件中的各个节加载到内存中相应的位置。不同的节可能有不同的加载方式,如代码节(.text)通常是只读的,数据节(.data)是可读写的。

  • 重定位(Relocation):如果PE文件的基地址(Preferred Base Address)不能被满足,加载器会根据重定位表对内存中的地址进行调整。

  • 解析导入表(Import Table):加载器会根据导入表加载所需的动态链接库(DLL),并解析函数和变量的地址。

image.png

如上图,从文件到内存,“头+节表”部分的数据没有做任何更改,多出的部分也只是以0补足的数据而已。对于各个节,其对齐的方式则是由数据结构中的字段IMAGEOPTIONAL HEADER32.FileAlignment和IMAGE OPTIONAL HEADER32.SectionAlignment分别定义的。

RVA和FOA的转换

RVA是相对虚拟地址,FOA是文件偏移,在学习文件头编程之前,要先知道如何进行二者的转换。

方法:

  • 判断指定的RVA落在哪个节内。

  • 求出该节的起始RVA(sectionStartRVA)。

  • 求出偏移量(offsetWithinSection)。

  • 求出该RVA相对于磁盘文件头的偏移(fileOffset)

代码实现:

#include <windows.h>

DWORD RVAToOffset(PBYTE lpFileHead, DWORD dwRVA) {
    PIMAGE_DOS_HEADER dosHeader;
    PIMAGE_NT_HEADERS ntHeaders;
    PIMAGE_SECTION_HEADER sectionHeader;
    DWORD fileOffset = (DWORD)-1;

    dosHeader = (PIMAGE_DOS_HEADER)lpFileHead;
    ntHeaders = (PIMAGE_NT_HEADERS)(lpFileHead + dosHeader->e_lfanew);
    sectionHeader = (PIMAGE_SECTION_HEADER)((BYTE*)ntHeaders + sizeof(IMAGE_NT_HEADERS));

    for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i) {
        DWORD sectionStartRVA = sectionHeader[i].VirtualAddress;
        DWORD sectionEndRVA = sectionStartRVA + sectionHeader[i].SizeOfRawData;

        if (dwRVA >= sectionStartRVA && dwRVA < sectionEndRVA) {
            DWORD offsetWithinSection = dwRVA - sectionStartRVA;
            fileOffset = sectionHeader[i].PointerToRawData + offsetWithinSection;
            break;
        }
    }

    return fileOffset;
}

数据定位

在处理PE文件时,定位步骤是进行任何数据访问和操作的前提条件。

  • PE头定位:通过DOS头的 e_lfanew 字段找到PE头的位置。

  • 数据目录表项定位:通过可选头中的 DataDirectory 数组定位特定的数据目录表项。

  • 节表项定位:遍历节表,查找包含特定RVA的节。

这些定位步骤既适用于处理磁盘上的PE文件,也适用于处理内存中的PE文件映像。

PE头定位

PE头定位是指找到PE文件的核心结构——PE头。PE头包含了关于文件和运行时映像的基本信息。

#include <windows.h>

// 定义常量用于dwFlag1和dwFlag2的值
#define PE_IMAGE_HEADER 0
#define PE_MEMORY_MAPPED 1
#define RETURN_RVA_MODULE_BASE 0
#define RETURN_FOA_FILE_BASE 1
#define RETURN_RVA 2
#define RETURN_FOA 3

DWORD rPE(PBYTE lpHeader, DWORD dwFlag1, DWORD dwFlag2) {
    DWORD ret = 0;
    DWORD imageBase = 0;

    // 头指针转换
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)lpHeader;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(lpHeader + dosHeader->e_lfanew);

    // 获取程序的建议装载地址
    imageBase = ntHeaders->OptionalHeader.ImageBase;

    if (dwFlag1 == PE_IMAGE_HEADER) { // _lpHeader是PE映像头
        if (dwFlag2 == RETURN_RVA_MODULE_BASE) { // 返回RVA+模块基地址
            ret = (DWORD)ntHeaders;
        } else if (dwFlag2 == RETURN_FOA_FILE_BASE) { // 返回FOA
            ret = (DWORD)((BYTE*)ntHeaders - lpHeader);
        } else if (dwFlag2 == RETURN_RVA) { // 返回RVA
            ret = (DWORD)((BYTE*)ntHeaders - lpHeader);
        } else if (dwFlag2 == RETURN_FOA) { // 返回FOA
            ret = (DWORD)((BYTE*)ntHeaders - lpHeader);
        }
    } else if (dwFlag1 == PE_MEMORY_MAPPED) { // _lpHeader是内存映射文件头
        if (dwFlag2 == RETURN_RVA_MODULE_BASE) { // 返回RVA+模块基地址
            ret = (DWORD)((BYTE*)ntHeaders - lpHeader + imageBase);
        } else if (dwFlag2 == RETURN_FOA_FILE_BASE) { // 返回FOA+文件基地址
            ret = (DWORD)ntHeaders;
        } else if (dwFlag2 == RETURN_RVA) { // 返回RVA
            ret = (DWORD)((BYTE*)ntHeaders - lpHeader);
        } else if (dwFlag2 == RETURN_FOA) { // 返回FOA
            ret = (DWORD)((BYTE*)ntHeaders - lpHeader);
        }
    }

    return ret;
}

根据dwFlag1的值判断,如果dwFlag1 为 PE_IMAGE_HEADER,表示 lpHeader 是PE映像头;反之则是内存映像头。
之后根据dwFlag2:

  • 如果 dwFlag2 为 RETURN_RVA_MODULE_BASE,返回RVA + 模块基地址。

  • 如果 dwFlag2 为 RETURN_FOA_FILE_BASE,返回FOA + 文件基地址。

  • 如果 dwFlag2 为 RETURN_RVA,返回RVA。

  • 如果 dwFlag2 为 RETURN_FOA,返回FOA。

有一点,如果是PE映像头的话, dwFlag2 为 RETURN_FOA_FILE_BASE,返回FOA,因此此时文件基地址没有意义。

数据目录表项定位

数据目录表项是PE文件中可选头(Optional Header)的一部分,指向各种数据结构,例如导入表、导出表、资源表等。

代码:

#include <windows.h>

// 定义常量用于dwFlag1和dwFlag2的值
#define PE_IMAGE_HEADER 0
#define PE_MEMORY_MAPPED 1
#define RETURN_RVA_MODULE_BASE 0
#define RETURN_FOA_FILE_BASE 1
#define RETURN_RVA 2
#define RETURN_FOA 3

// 辅助函数:将RVA转换为文件偏移(FOA)
DWORD RVAToOffset(PBYTE lpFileHead, DWORD dwRVA) {
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)lpFileHead;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(lpFileHead + dosHeader->e_lfanew);
    PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);

    for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i, ++sectionHeader) {
        DWORD sectionStartRVA = sectionHeader->VirtualAddress;
        DWORD sectionEndRVA = sectionStartRVA + sectionHeader->SizeOfRawData;

        if (dwRVA >= sectionStartRVA && dwRVA < sectionEndRVA) {
            return sectionHeader->PointerToRawData + (dwRVA - sectionStartRVA);
        }
    }
    return -1;  // 返回无效偏移
}

DWORD rDDEntry(PBYTE lpHeader, DWORD index, DWORD dwFlag1, DWORD dwFlag2) {
    DWORD ret = 0, ret1 = 0, ret2 = 0;
    DWORD imageBase = 0;

    // 头指针转换
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)lpHeader;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(lpHeader + dosHeader->e_lfanew);

    // 获取程序的建议装载地址
    imageBase = ntHeaders->OptionalHeader.ImageBase;

    // 指向DataDirectory
    PIMAGE_DATA_DIRECTORY dataDirectory = &ntHeaders->OptionalHeader.DataDirectory[index];

    // 取出指定索引数据目录项的位置, 是RVA
    ret1 = dataDirectory->VirtualAddress;

    if (dwFlag1 == PE_IMAGE_HEADER) { // _lpHeader是PE映像头
        if (dwFlag2 == RETURN_RVA_MODULE_BASE) { // 返回RVA+模块基地址
            ret = ret1 + (DWORD)lpHeader;
        } else if (dwFlag2 == RETURN_FOA_FILE_BASE) { // 无意义,返回FOA
            ret = RVAToOffset(lpHeader, ret1);
        } else if (dwFlag2 == RETURN_RVA) { // 返回RVA
            ret = ret1;
        } else if (dwFlag2 == RETURN_FOA) { // 返回FOA
            ret = RVAToOffset(lpHeader, ret1);
        }
    } else if (dwFlag1 == PE_MEMORY_MAPPED) { // _lpHeader是内存映射文件头
        if (dwFlag2 == RETURN_RVA_MODULE_BASE) { // 返回RVA+模块基地址
            ret = ret1 + imageBase;
        } else if (dwFlag2 == RETURN_FOA_FILE_BASE) { // FOA+文件基地址
            ret2 = RVAToOffset(lpHeader, ret1);
            ret = ret2 + (DWORD)lpHeader;
        } else if (dwFlag2 == RETURN_RVA) { // 返回RVA
            ret = ret1;
        } else if (dwFlag2 == RETURN_FOA) { // 返回FOA
            ret = RVAToOffset(lpHeader, ret1);
        }
    }

    return ret;
}

节表项定位

节表项包含PE文件的各个节(Section)的信息,每个节都描述了一段代码或数据。

#include <windows.h>

// 定义常量用于dwFlag1和dwFlag2的值
#define PE_IMAGE_HEADER 0
#define PE_MEMORY_MAPPED 1
#define RETURN_RVA_MODULE_BASE 0
#define RETURN_FOA_FILE_BASE 1
#define RETURN_RVA 2
#define RETURN_FOA 3

DWORD rSection(PBYTE lpHeader, DWORD index, DWORD dwFlag1, DWORD dwFlag2) {
    DWORD ret = 0;
    DWORD imageBase = 0;

    // 头指针转换
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)lpHeader;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(lpHeader + dosHeader->e_lfanew);

    // 获取程序的建议装载地址
    imageBase = ntHeaders->OptionalHeader.ImageBase;

    // 获取节表的起始地址
    PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);

    // 指向指定索引的节表项
    sectionHeader += index;

    if (dwFlag1 == PE_IMAGE_HEADER) { // _lpHeader是PE映像头
        if (dwFlag2 == RETURN_RVA_MODULE_BASE) { // 返回RVA+模块基地址
            ret = (DWORD)sectionHeader;
        } else { // 返回相对偏移(RVA或FOA)
            ret = (DWORD)sectionHeader - (DWORD)lpHeader;
        }
    } else if (dwFlag1 == PE_MEMORY_MAPPED) { // _lpHeader是内存映射文件头
        if (dwFlag2 == RETURN_RVA_MODULE_BASE) { // 返回RVA+模块基地址
            ret = (DWORD)sectionHeader - (DWORD)lpHeader + imageBase;
        } else if (dwFlag2 == RETURN_FOA_FILE_BASE) { // 返回FOA+文件基地址
            ret = (DWORD)sectionHeader;
        } else { // 返回相对偏移(RVA或FOA)
            ret = (DWORD)sectionHeader - (DWORD)lpHeader;
        }
    }

    return ret;
}

通过遍历节表,将给定的RVA与节表中的每个节的地址范围进行比对,如果RVA落在该节表地址范围内,则返回该节的名称字符串地址。

#include <windows.h>
#include <stdio.h>

const char* getRVASectionName(PBYTE lpFileHead, DWORD dwRVA) {
    // 定义一个返回值,用于保存找到的节名称
    const char* szNotFound = "Not Found";
    const char* sectionName = szNotFound;

    // 获取DOS头和NT头
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)lpFileHead;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(lpFileHead + dosHeader->e_lfanew);

    // 获取节表的起始地址
    PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
    WORD numberOfSections = ntHeaders->FileHeader.NumberOfSections;

    // 遍历节表
    for (int i = 0; i < numberOfSections; i++, sectionHeader++) {
        DWORD sectionStartRVA = sectionHeader->VirtualAddress;
        DWORD sectionEndRVA = sectionStartRVA + sectionHeader->SizeOfRawData;

        // 判断RVA是否在当前节范围内
        if (dwRVA >= sectionStartRVA && dwRVA < sectionEndRVA) {
            sectionName = (const char*)sectionHeader->Name;
            break;
        }
    }

    return sectionName;
}

int main() {
    // 示例调用,假设lpFileHead指向PE文件的内存映像,dwRVA是要查询的RVA
    PBYTE lpFileHead = ...; // 替换为实际PE文件头的地址
    DWORD dwRVA = ...; // 替换为实际的RVA

    const char* sectionName = getRVASectionName(lpFileHead, dwRVA);
    printf("Section Name: %s\n", sectionName);

    return 0;
}

PE校验和

校验和是一个 WORD值。它是通过对一段数据进行一定的算法进行计算以后生成的值,通常作为判断这段数据是否被非法修改的依据。
PE 文件头部的校验和的算法很简单,共分三步:

  • 将文件头部的字段IMAGE OPTIONAL HEADER32.CheckSum 清0。

  • 以 WORD 为单位对数据块进行带进位的累加,大于WORD 部分自动溢出。

  • 将累加和加上文件的长度。

#include <windows.h>
#include <stdio.h>

DWORD CalculatePEChecksumAPI(const char* lpExeFile) {
    DWORD cSum = 0, hSum = 0;

    // 调用 MapFileAndCheckSum API 计算校验和
    DWORD result = MapFileAndCheckSumA(lpExeFile, &hSum, &cSum);
    if (result == CHECKSUM_SUCCESS) {
        return cSum;
    } else {
        // 处理错误情况,例如文件不存在或无法访问
        return 0;
    }
}

DWORD CalculatePEChecksumManual(const char* lpExeFile) {
    HANDLE hFile = INVALID_HANDLE_VALUE;
    DWORD dwSize = 0, bytesRead = 0;
    DWORD ret = 0;
    PBYTE hBase = NULL;

    // 打开文件
    hFile = CreateFileA(lpExeFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        return 0;
    }

    // 获取文件大小
    dwSize = GetFileSize(hFile, NULL);
    if (dwSize == INVALID_FILE_SIZE) {
        CloseHandle(hFile);
        return 0;
    }

    // 分配内存并读入文件
    hBase = (PBYTE)VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
    if (hBase == NULL) {
        CloseHandle(hFile);
        return 0;
    }

    if (!ReadFile(hFile, hBase, dwSize, &bytesRead, NULL) || bytesRead != dwSize) {
        VirtualFree(hBase, 0, MEM_RELEASE);
        CloseHandle(hFile);
        return 0;
    }

    // 关闭文件句柄
    CloseHandle(hFile);

    // 将 CheckSum 清零
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hBase;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(hBase + dosHeader->e_lfanew);
    ntHeaders->OptionalHeader.CheckSum = 0;

    // 按字进位加,溢出忽略
    DWORD cSum = 0;
    DWORD wordCount = (dwSize + 1) / 2;  // 按 WORD 计算字数
    WORD* pWord = (WORD*)hBase;
    
    for (DWORD i = 0; i < wordCount; i++) {
        cSum += *pWord++;
        cSum = (cSum >> 16) + (cSum & 0xFFFF);  // 处理进位
    }

    // 释放内存
    VirtualFree(hBase, 0, MEM_RELEASE);

    // 加上文件长度
    cSum += dwSize;
    ret = cSum;

    return ret;
}

int main() {
    const char* filePath = "your_pe_file.exe";  // 替换为实际PE文件路径

    DWORD apiChecksum = CalculatePEChecksumAPI(filePath);
    printf("API Checksum: %08X\n", apiChecksum);

    DWORD manualChecksum = CalculatePEChecksumManual(filePath);
    printf("Manual Checksum: %08X\n", manualChecksum);

    return 0;
}

# 系统安全
本文为 xxxTea 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
xxxTea LV.3
这家伙太懒了,还未填写个人描述!
  • 12 文章数
  • 5 关注者
结合ppid欺骗和远线程注入实现dll挖空
2024-12-08
渗透测试 | 入门必备的 JavaScript 基础知识
2024-11-29
Web安全 | 防范浏览器Web控制台攻击
2024-11-28
文章目录