精华内容
下载资源
问答
  • PE文件解析

    千次阅读 2019-05-28 14:53:48
    标题:PE文件解析 作者:流浪的野指针 1 IMAGE_DOS_HEADER MS-DOS 头部存在于每个 PE 文件中,它的存在完全是出于兼容性的考虑,MS-DOS 头部紧接的是 DOS-STUB,这两部分构成了可执行文件的基本要素,当该程序运行...

    标题:PE文件解析
    作者:猫猫、有点乖

    1 IMAGE_DOS_HEADER

    MS-DOS 头部存在于每个 PE 文件中,它的存在完全是出于兼容性的考虑,MS-DOS 头部紧接的是 DOS-STUB,这两部分构成了可执行文件的基本要素,当该程序运行在 MS-DOS 系统中,并不会出现不可预料的错误,因为它会执行 DOS-STUB 中的代码,如果不行它也仅仅是提示你 This program cannot run in DOS mode,大致的意思是告诉用户这个文件并不能在当前环境下运行。
    在这里插入图片描述
    MS-DOS 头部结构 IMAGE_DOS_HEADER 结构体,这个结构体中包含 19 个成员,其中有两个 WORD 型数组,共占 64 个字节,前 18 个字段中占 60 个字节,而 60 相当于十六进制当中的 0x3C,也就是说最后一个字段 e_lfanew 的偏移为 3CH 处,而这个字段是最重要的字段,它指的是 PE 头部的开始,这个值也可通过 IMAGE_DOS_HEADER 结构体中第 13 个字段 e_lfarlc 重定位表偏移获取,这个值一般为 0x40,通过这个值减去 4 个字节也可得到 PE 头部的真正偏移,也就是 0x3C:
    在这里插入图片描述
    除了这两个字段外,还有一个也比较重要,那就是第 1 个字段 e_magic,这个字段被称为魔数,如果这个文件是一个有效的 PE 文件的话,那么该字段的值为 0x00005A4D,宏定义为 IMAGE_DOS_SIGNATURE(0x00005A4D)
    在这里插入图片描述
    **总结:**在 DOS 头中,你只需要知道以上列出的三个字段就可以了,关于其它字段,无需过多关心。

    2 IMAGE_NT_HEADERS

    PE 文件头是 Windows NT 内核下判断可执行文件的唯一有效结构,位于 DOS-STUB 块后,由 MS-DOS 头中 e_lfanew 字段指向的结构,如下:
    在这里插入图片描述
    此结构由三个字段组成,分别为 DWORD 型的 Signature,它是用来判断是否为有效的 PE 文件,它的值为 0x00004550,ANSI 码为 PE\0\0,该值用一个宏来表示,为 IMAGE_NT_SIGNTURE,剩下的两个字段分别为 IMAGE_FILE_HEADERIMAGE_OPTIONAL_HEADER32,前者表示的是文件头,后者表示的扩展头。
    在这里插入图片描述
    这个 Signature 字段和 DOS 头中的 e_magic 字段可以用来判断一个文件是否为一个有效的 PE 文件,相关代码如下:

    /*********************************************************
     * 说明:判断一个文件是否为一个有效的 PE 文件
     * 关键字段:
     *  - IMAGE_DOS_HEADER 中的 e_magic
     *  - IMAGE_NT_HEADERS 中的 Signature
     * 两个字段的值分别要为 0x00005A4D 和 0x00004550
     * 相应的宏为 IMAGE_DOS_SIGNATURE 和 IMAGE_NT_SIGNATURE
     *********************************************************/
    #include <windows.h>
    #include <stdio.h>
    
    int main(void)
    {
    	// 1.首先须打开一个文件
    	HANDLE hFile = CreateFile(
    		TEXT("x86.exe"),
    		GENERIC_ALL,
    		NULL,
    		NULL,
    		OPEN_EXISTING,
    		NULL,
    		NULL
    	);
    	// 2.判断文件句柄是否有效,若无效则提示打开文件失败并退出
    	if (hFile == INVALID_HANDLE_VALUE)
    	{
    		printf("打开文件失败!\n");
    		CloseHandle(hFile);
    		exit(EXIT_SUCCESS);
    	}
    	// 3.若打开文件成功,则获取文件的大小
    	DWORD dwFileSize = GetFileSize(hFile, NULL);
    	// 4.申请内存空间,用于存放文件数据
    	BYTE * FileBuffer = new BYTE[dwFileSize];
    	// 5.读取文件内容
    	DWORD dwReadFile = 0;
    	ReadFile(hFile, FileBuffer, dwFileSize, &dwReadFile, NULL);
    	// 6.判断这个文件是不是一个有效的PE文件
    	//    6.1 先检查DOS头中的MZ标记,判断e_magic字段是否为0x5A4D,或者是IMAGE_DOS_SIGNATURE
    	DWORD dwFileAddr = (DWORD)FileBuffer;
    	auto DosHeader = (PIMAGE_DOS_HEADER)dwFileAddr;
    	if (DosHeader->e_magic != IMAGE_DOS_SIGNATURE)
    	{
    		// 如果不是则提示用户,并立即结束
    		MessageBox(NULL, TEXT("这不是一个有效PE文件"), TEXT("提示"), MB_OK);
    		delete FileBuffer;
    		CloseHandle(hFile);
    		exit(EXIT_SUCCESS);
    	}
    	//    6.2 若都通过的话再获取NT头所在的位置,并判断e_lfanew字段是否为0x00004550,
        //        或者是IMAGE_NT_SIGNATURE
    	auto NtHeader = (PIMAGE_NT_HEADERS)(dwFileAddr + DosHeader->e_lfanew);
    	if (NtHeader->Signature != IMAGE_NT_SIGNATURE)
    	{
    		// 如果不是则提示用户,并立即结束
    		MessageBox(NULL, TEXT("这不是一个有效PE文件"), TEXT("提示"), MB_OK);
    		delete FileBuffer;
    		CloseHandle(hFile);
    		exit(EXIT_SUCCESS);
    	}
    	// 7.若上述都通过,则为一个有效的PE文件
    	MessageBox(NULL, TEXT("这是一个有效PE文件"), TEXT("提示"), MB_OK);
    	delete FileBuffer;
    	CloseHandle(hFile);
    	// 8.结束程序
    	return 0;
    }
    

    2.1 IMAGE_FILE_HEADER

    IMAGE_FILE_HEADER 这个结构体中有 7 个字段,如下:
    在这里插入图片描述
    4个 WORD 和 3 个 DWORD,共占 20 字节,20 相当于十六进制中的 0x14,并有一宏来表示,为 IMAGE_SIZEOF_FILE_HEADER,如下:
    在这里插入图片描述
    其中有 3 个重要的字段,分别为区段的数目(NumberOfSections)、扩展头的大小(SizeOfOptionalHeader)以及文件的属性(Characteristics)。

    区段的数目表示的是这个可执行文件中存在多少个区段,从上图可以看出,该文件中有 9 个区段,如下:
    在这里插入图片描述
    扩展头大小一般为固定值,32 位下 0xE0,64 位下为 0xF0;文件的属性用来说明文件的特征,比如说这个文件是 exe 文件还是 dll 文件。除了这 3 个重要字段外,另外 Machine 字段表明该文件能在目标什么平台下运行,比如常见的 0x14C,代表的是 Intel386 平台。还有一个字段是 TimeDateStamp,这个字段表明这个文件的创建时间,可以通过 tm 相关函数来转换,相关代码如下:

    /*********************************************************
     * 说明:用 tm 相关函数对时间进行转换
     *********************************************************/
    void TimeTranslate()
    {
    	struct tm FileCreateTime;
    	errno_t err = gmtime_s(&FileCreateTime, (time_t*)&g_pFileHeader->TimeDateStamp);
    	if (err)
    	{
    		printf("无效的参数\n");
    	}
    	else
    	{
    		printf("文件创建日期和时间:%d-%d-%d %d:%d:%d\n",
    			FileCreateTime.tm_year + 1900,
    			FileCreateTime.tm_mon + 1,
    			FileCreateTime.tm_mday,
    			FileCreateTime.tm_hour + 8,
    			FileCreateTime.tm_min,
    			FileCreateTime.tm_sec
    		);
    	}
    }
    

    上述代码需用到 time.h 头文件。

    2.2 IMAGE_OPTIONAL_HEADER32

    这个结构体比较大,共 31 个字段,32 位下占 0xE0 大小,也就是 224 个字节,这个结构体分为两部分,分别为标准域部分和扩展域部分,标准域部分有 9 个字段,在这 9 个字段中,比较重要的是第 7 个字段(AddressOfEntryPoint),它表示的是程序执行入口 RVA,接下来的是它后面 2 个字段,分别为代码段起始 RVA(BaseOfCode) 和 数据段起始 RVA(BaseOfData),标准域中不是特别重要的字段为 Magic 和 SizeOfCode,标志位(普通可执行映像为 0x010B,64 位下为 0x020B)和所有代I码段的总大小(这个代码段指的是具有 IMAGE_SCN_CNT_CODE 属性的区段)。
    在这里插入图片描述
    扩展域中有 22 个字段,22 个字段中比较重要的是:

    • 程序默认装入地址( ImageBase):这个地址一般为 0x4000000;

    • 内存对齐粒度(SectionAlignment):这个字段的值一般为 0x1000,即 4KB 大小;

    • 文件对齐粒度(FileAlignment):这个字段的值一般为 0x200;

    • 内存中映像总尺寸(SizeOfImage):从 ImageBase 开始到最后一个区段的总大小;

    • DLL 标志位(DllCharacteristic):随机基址就与这个标志位有关;

    • 数据目录表项(NumberOfRvaAndSize):数据目录表的项数,为 0x10;

    • 数据目录表结构(IMAGE_DATA_DIRECTROY):该结构中只有两个成员,分别为起始 RVA 和 相应大小;
      在这里插入图片描述
      在这里插入图片描述
      数据目录表一共有 16 个,分别为:

    • 导出表:用于导出此映像中的函数示例符号,以便于其他应用程序可通过些导出符号调用此映像的函数示例;

    • 导入表:用于导入此映像;

    • 资源表:用于保存各种资源(包括图标、对话框等);

    • 异常表:用于保存可执行文件中异常处理的相关数据;

    • 安全表:一般情况下用于保存数字签名或安全证书;

    • 重定位表:保存着需要执行重定位的代码偏移信息;

    • 调试表:用于保存符号名与调试相关信息;

    • 版权表:保留字段,必须为 0;

    • 全局指针偏移表:保存着全局指针寄存器的 RVA 地址;

    • TLS 表:线程局部存储器目录表,其本质上属于一个局部变量,单独存在于每个线程中;

    • 载入配置表:用来描述一些太大或是太复杂而不适合在 PE 头或选项头中描述的特征;

    • 绑定导入表:存储可以减少程序加载时间的一些 API 绑定信息;

    • 导入地址表:保存导入函数的真正地址;

    • 延迟加载表:通过指定可以延迟加载的 DLL 列表,减少程序启动之初加载 DLL 的数量,进而提高程序启动速度;

    • COM:COM 运行时描述符目录;

    • 保留:保留;

    相关代码如下:

    /*********************************************************
     * 说明:读取扩展头信息
     *********************************************************/
    void ReadOptionalHeaderInfo()
    {
    	g_pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)&g_pNtHeader->OptionalHeader;
    	printf("Magic:0x%08x\n", g_pOptionalHeader->Magic);
    	printf("SizeOfCode:0x%08x\n", g_pOptionalHeader->SizeOfCode);
    	printf("AddressOfEntryPoint:0x%08x\n", g_pOptionalHeader->AddressOfEntryPoint);
    	printf("BaseOfCode:0x%08x\n", g_pOptionalHeader->BaseOfCode);
    	printf("BaseOfData:0x%08x\n", g_pOptionalHeader->BaseOfData);
    	printf("ImageBase:0x%08x\n", g_pOptionalHeader->ImageBase);
    	printf("SectionAlignment:0x%08x\n", g_pOptionalHeader->SectionAlignment);
    	printf("FileAlignment:0x%08x\n", g_pOptionalHeader->FileAlignment);
    	printf("SizeOfImage:0x%08x\n", g_pOptionalHeader->SizeOfImage);
    	printf("DllCharacteristics:0x%08x\n", g_pOptionalHeader->DllCharacteristics);
    	printf("**************************************\n");
    	printf("目录表\n");
    	printf("RVA\t\t大小\n");
    	for (int i = 0; i < IMAGE_NUMBEROF_DIRECTORY_ENTRIES; ++i)
    	{
    		printf("0x%08x\t0x%08x\n", 
                   g_pOptionalHeader->DataDirectory[i].VirtualAddress,
                   g_pOptionalHeader->DataDirectory[i].Size
    		);
    	}
    }
    

    3 IMAGE_SECTION_HEADER

    PE 文件头的数据目录表后便是区段表了,区段表用来描述位于其后各个区段的各种属性(区段名、长度、属性等内容)。PE 文件最少要有一个区段才能被加载运行。

    区段表是由数个首尾相连的 IMAGE_SECTION_HEADER 结构体数组构成的,可以使用 IMAGE_FIRST_SECTION32(NtHeader) 这个宏找到第一个区段表所在的位置:
    在这里插入图片描述
    示例代码如下:

    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION32(NtHeader);
    

    在这里插入图片描述
    结构体成员相关解释如下:

    typedef struct _IMAGE_SECTION_HEADER 
    {
        BYTE Name[0x8];							// 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;
    

    在这里插入图片描述
    在实际当中对应关系如下:
    在这里插入图片描述

    4 RVA 与 FOA 之间关系及转换

    要计算出某个 RVA 的 FOA,首先需要计算出这个 RVA 落在哪个区段中,然后用这个 RVA 减去这个区段中的 RVA,得到的值为相对于这个区段的偏移,最后用这个得到的偏移加上该区段的 FOA,就能算出该 RVA 所对应的 FOA。

    DWORD RVAtoFOA(DWORD Buffer, DWORD Rva)
    {
    	// 通过NT头获取到第一个区段
    	auto DosHeader = (PIMAGE_DOS_HEADER)Buffer;
    	auto NtHeader = (PIMAGE_NT_HEADERS)(Buffer + DosHeader->e_lfanew);
    
    	// 获取第一个区段
    	auto Section = IMAGE_FIRST_SECTION(NtHeader);
    
    	// 获取到区段的数量
    	DWORD Number = NtHeader->FileHeader.NumberOfSections;
    
    	// 遍历区段并找到需要转换的地址所在的区段
    	for (int i = 0; i < Number; ++i)
    	{
    		// 判断是否在有效区间内,如果过满足条件就是找到了
    		// Section[i].VirtualAddress 区段的起始位置
    		// Section[i].VirtualAddress+Section[i].SizeOfRawData: 区段的结束位置
    		if (Rva >= Section[i].VirtualAddress && Rva < 
    			(Section[i].VirtualAddress+Section[i].SizeOfRawData))
    		{
    			// Rva - Section[i].VirtualAddress 求出来的是距离所在区段偏移
    			DWORD Foa = Rva - Section[i].VirtualAddress
    				// 偏移 + 所在区段的的FOA就是转换后的FOA了
                        + Section[i].PointerToRawData;
    			return Foa;
    		}
    	}
    
    	return -1;
    }
    

    以上就是相关转换的代码。

    5 IMAGE_EXPORT_DIRECTORY

    导出表是 PE 文件为其他应用程序提供的 API 的一种函数示例导出方式。Windows 下存在导出表的可执行文件以指定自身的一些变量、函数以及类,并将其导出,以便提供给其他第三方程序使用。

    /*******************************************
     * 说明:导出表结构
     *******************************************/
    typedef struct _IMAGE_EXPORT_DIRECTORY
    {
        DWORD Characteristics;				// 1 保留,恒为 0x00000000
        DWORD TimeDateStamp;				// 2 时间戳
        WORD MajorVersion;					// 3 主版本号,一般不赋值
        WORD MinorVersion;					// 4 子版本号,一般不赋值
        DWORD Name;							// 5 模块名称
        DWORD Base;							// 6 索引基数
        DWORD NumberOfFunctions;			// 7 导出地址表中的成员个数(EAT)
        DWORD NumberOfName;					// 8 导出名称表中的成员个数(ENT)
        DWORD AddressOfFunctions;			// 9 导出地址表(EAT)的 RVA
        DWORD AddressOfNames;				// 10 导出名称表(ENT)的 RVA
        DWORD AddressOfNameOrdinals;		// 11 导出序列号数组的 RVA
    } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
    

    在上面所展示的结构体中:

    • Name 字段为指向模块名的 ASCII 字符的 RVA;
    • Base 字段为导出 API 函数索引值的基数(函数索引值 = 导出函数索引值 - 基数),一般情况下为 1;
    • NumberOfFunctions 字段为 EAT 的条目数量;
    • NumberOfNames 字段为 ENT 的条目数量;
    • AddressOfFunctions 字段为 EAT 的 RVA;
    • AddressOfNames 字段为 ENT 的 RVA;

    ENT:导出名称表

    EAT:导出地址表

    导出表中有三张表,分别为 名称表函数表序号表,其中函数表与序号表是必须有的,而名称表则是可选的。这三张表关系也是非常简单,序号表与名称表的作用就是索引,引导调用者找到真正需要的函数表,而函数表中保存的就是这个被导出的函数地址信息。

    导出表中的序号并不代表其本身在内存或 PE 文件中的排列顺序,而仅仅是作为一个标识,其真正的存储顺序是完全被打乱的,导出表在内存中的存储顺序是按照导出名称来确定的。

    另外,文件中保存的序号也并不是我们平时调用函数时使用的,这一点一定要分清。我们平时调用函数使用的序号减去 Base(序号基数)的值才能得到文件中保存的序号(这就是文件中保存的序号总是以 0 开始,而我们调用的序号大多以 1 开始的原因)。我们也将文件中保存的序号称为原始序号,而将调用函数时使用的序号称为调用序号。

    // 读取导出表的数据
    void ReadExportTable()
    {
    	// 找到数据目录表下标为0的一项[导出表]的RVA
    	DWORD ExportTableRva = g_NtHeader->OptionalHeader.
    		DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    
    	// 找到导出表的结构体
    	auto ExportTable = (PIMAGE_EXPORT_DIRECTORY)
    		(RVAtoFOA(ExportTableRva) + g_FileBaseAddr);
    
    	// 解析出模块的名称
    	printf("Name: %s\n", (char*)(g_FileBaseAddr +
    		RVAtoFOA(ExportTable->Name)));
    
    	// 分别获取 名称表(RVA)、地址表(RVA)、序号表(RVA)[WORD]
    	DWORD* AddrTable = (DWORD*)(RVAtoFOA(ExportTable->AddressOfFunctions) + g_FileBaseAddr);
    	DWORD* NameTable = (DWORD*)(RVAtoFOA(ExportTable->AddressOfNames) + g_FileBaseAddr);
    	WORD* OrdTable = (WORD*)(RVAtoFOA(ExportTable->AddressOfNameOrdinals) + g_FileBaseAddr);
    
    	// 遍历地址表,函数地址表单的元素个数就是函数的个数
    	for (DWORD i = 0; i < ExportTable->NumberOfFunctions; ++i)
    	{
    		// 判断当前函数地址是否有效
    		if (AddrTable[i] == NULL)
            {
                continue;
            }
    		BOOL HavaName = FALSE;
    		// 查看当前遍历到的函数有没有名字[遍历序号表]
    		for (int j = 0; j < ExportTable->NumberOfNames; ++j)
    		{
    			// 如果序号表中保存了当前地址的下标,就说明是名称导出
    			// i 表示当前地址的下标, j是序号表和名称表的下标
    			if (i == OrdTable[j])
    			{
    				HavaName = TRUE;
    				// 函数的序号 = 地址表的下标 + Base
    				printf("序号: %04X\t地址: 0x%08X\t名称:%s\n",
    					i + ExportTable->Base, AddrTable[i],
    					(char*)(RVAtoFOA(NameTable[j]) + g_FileBaseAddr));
    			}
    		}
    
    		// 否则,说明这是一个序号导出的函数
    		if (HavaName == FALSE)
    		{
    			printf("序号: %04X\t地址: 0x%08X\t名称:%s\n",
    				i + ExportTable->Base, AddrTable[i], "[None]");
    		}
    	}
    }
    

    6 IMAGE_IMPORT_DESCRIPTOR

    导入表机制是 PE 文件从其他第三方程序中导入 API,以供本程序调用的机制。

    /*******************************************
     * 说明:导入表结构
     *******************************************/
    typedef struct _IMAGE_IMPORT_DESCRIPTOR
    {
        union
        {
            DWORD Characteristics;
            DWORD OriginalFirstThunk;	// 1 指向导入名称表(INT)的 RVA
        };
        DWORD TimeDateStamp;			// 2 时间标识
        DWORD ForwarderChain;			// 3 转发链,如果不转发则此值为 0
        DWORD Name;						// 4 指向导入映像文件的名字 RVA
        DWORD FirstThunk;				// 5 指向导入地址表(IAT)的 RVA
    } IMAGE_IMPORT_DESCRIPTOR;
    

    IMAGE_IMPORT_DESCRIPTOR 结构的个数是由导入映像文件的个数决定的,需要从多少个映像文件中导入函数,就要有多少个 IMAGE_IMPORT_DESCRIPTOR 结构,最后会以一个空的 IMAGE_IMPORT_DESCRIPTOR 结构结束。在上述这个结构体中,我们只需关心两个字段,分别为 OriginalFirstThunkFirstThunk 。这两个字段分别指向了保存导出名称与导出地址的 IMAGE_THUNK_DATA 结构数组,且这个数组以一个空的 IMAGE_THUNK_DATA 结构结尾,其结构如下:

    /*******************************************
     * 说明:IMAGE_THUNK_DATA32 结构
     *******************************************/
    typedef struct _IMAGE_THUNK_DATA32
    {
        union
        {
            PBYTE ForwarderString;						// 1 转发字符串的 RVA
            PDWORD Function;							// 2 被导入函数的地址
            DWORD Oridinal;								// 3 被导入函数的序号
            PIMAGE_IMPORT_BY_NAME AddressOfData;		// 4 指向导入名称表
        } u1;
    } IMAGE_THUNK_DATA32;
    typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
    

    在这里插入图片描述

    /*******************************************
     * 说明:IMAGE_IMPORT_BY_NAME 结构
     *******************************************/
    typedef struct _IMAGE_IMPORT_BY_NAME
    {
        WORD Hint;										// 1 需导入的函数序号
        BYTE Name[1];									// 2 需导入的函数名称
    } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
    

    以下为导入表相关代码:

    // 读取导入表的数据
    void ReadImportTable()
    {
    	// 首先找到数据目录表的下标为[1]的地方(导入表)的RVA
    	DWORD ImportTableRva = g_NtHeader->OptionalHeader.
    		DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    
    	// 找到导入表结构体数组的首地址[导入表可以有多个,以一组全0的结构结尾]
    	auto ImportTables = (PIMAGE_IMPORT_DESCRIPTOR)(RVAtoFOA(ImportTableRva) + g_FileBaseAddr);
    
    	// 遍历出所有的导入DLL的名称
    	while (ImportTables->Name != NULL)
    	{
    		// 输出被导入模块的名称
    		printf("Name: %s\n", (char*)(RVAtoFOA(ImportTables->Name) + g_FileBaseAddr));
    
    		// INT: 不管在内存中还是在文件中,通常保存的都是函数的名称 
    		// IAT: 在文件中通常保存的是函数的名称(INT),在内存中会被修复成函数的地址
    		auto IntTable = (IMAGE_THUNK_DATA*)(g_FileBaseAddr
    			+ RVAtoFOA(ImportTables->OriginalFirstThunk));
    
    		// INT 表以一个全0的字段结尾
    		while (IntTable->u1.Function)
    		{
    			// 由于函数有两种导出方式: 序号、名称,需要判断函数是否有名字
    			// 根据判断的结果解析字段
    			//if (IntTable->u1.Ordinal & 0x80000000)
    			if (IMAGE_SNAP_BY_ORDINAL(IntTable->u1.Ordinal))
    			{
    				// 当最高为为1的时候,说明这是一个序号导出的函数
    				// 那么这个字段的低2字节保存的就是当前函数的序号
    				printf("序号: %04X\t名称:[None]\n", IntTable->u1.Ordinal & 0xFFFF);
    			}
    			else
    			{
    				// 当这个函数是用名称导入的时候,当前字段保存的是一个RVA
    				// 指向了 IMAGE_IMPORT_BY_NAME 结构体,这个结构体的前两
    				// 个字节(第一个字段)保存是导入函数的序号,后面(第二个字段
    				// )保存的是一个不定长的字符串
    				auto Name = (PIMAGE_IMPORT_BY_NAME)(g_FileBaseAddr
    					+ RVAtoFOA(IntTable->u1.AddressOfData));
    
    				// 输出函数的名称以及序号
    				printf("序号: %04X\t名称:%s\n", Name->Hint, Name->Name);
    			}
    
    			// 开始遍历下一个函数
    			IntTable++;
    		}
    
    		// 转到数组的下一个元素
    		ImportTables++;
    	}
    }
    

    7 重定位表

    在这里插入图片描述

    struct
    {
        WORD Offset:12;			// 大小为 12 位的重定位偏移
        WORD Type:4;			// 大小为 4 位的重定位信息的类型值
    } TypeOffset;				// 此结构在 Windows SDK 中无定义
    
    /*******************************************
     * 说明:读取重定位表信息
     *******************************************/
    void GetRelocTable()
    {
    	struct TYPEOFFSET
    	{
    		// 12位能表示的最大值是 0xFFF,足以表示一个分页中的所有偏移
    		WORD Offset : 12;
    		// 这个字段通常保存的是3,也只需要关注为3的字段
    		WORD Type : 4;
    	};
    
    	// 获取数据目录表中下标为[5]的位置(重定位表)的RVA
    	DWORD RelocTableRva = g_NtHeader->OptionalHeader.DataDirectory
    		[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
    
    	// 获取重定位块的起始位置
    	auto RelocTable = (PIMAGE_BASE_RELOCATION)
    		(RVAtoFOA(RelocTableRva) + g_FileBaseAddr);
    
    	// 因为重定位块里面有一个字段是 SizeofBlock 保存的是重定位块的大小
    	// 如果这个字段为0,说明重定位快遍历结束了
    	while (RelocTable->SizeOfBlock)
    	{
    		// 重定位块[重定位结构体[8] + TypeOffsetArray]
    		// 重定位块1 + 重定位块2 + 重定位块3 + 00000000
    
    		// 输出当前是哪一个分页需要进行重定位
    		printf("PageRva = 0x%08X\n", RelocTable->VirtualAddress);
    
    		// 获取 TYPEOFFSET 结构体数组
    		TYPEOFFSET* TypeOffset = (TYPEOFFSET*)(RelocTable + 1);
    
    		// 获取重定位项(TypeOffset)的个数
    		// 原因是 SizeOfBlock 是一个重定位块的大小,包含了结构体以及数组
    		// - sizeof(RelocTable) 减去的是结构体的大小,/2原因是数组元素的
    		// 大小为 2
    		DWORD Counts = (RelocTable->SizeOfBlock - sizeof(RelocTable)) / 2;
    
    		// 遍历出每一个重定位项
    		for (DWORD i = 0; i < Counts; ++i)
    		{
    			// 判断类型是否为 3
    			if (TypeOffset[i].Type == 3)
    			{
    				// 算出需要重定位的数据所在的RVA
    				DWORD Rva = TypeOffset[i].Offset + RelocTable->VirtualAddress;
    				DWORD Foa = RVAtoFOA(Rva);
    
    				// 输出重定位的信息
    				printf("    RVA: %08X - FOA: %08X - Addr: %08X\n", 
    					Rva, Foa, *(DWORD*)(Foa + g_FileBaseAddr));
    			}
    		}
    
    		// 获取下一个重定位块
    		RelocTable = (PIMAGE_BASE_RELOCATION)
    			((DWORD)RelocTable + RelocTable->SizeOfBlock);
    	}
    }
    

    重定位的个数公式:

    重定位的个数 = (SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2
    

    重定位后的地址计算公式:

    重定位后的地址 = (加载地址 - ImageBase) + 重定位前的地址(需要重定位的地址)
    重定位后的地址 = (加载地址 - ImageBase) + (VirtualAddress + TypeOffset.Offset - 1)
    
    展开全文
  • IMAGE_NT_HEADERS32->IMAGE_OPTIONAL_HEADER32 DWORD SectionAlignment; //内存对齐 DWORD FileAlignment; //文件对齐
  • 同时该类也可以让想创建自己的PE文件解析软件的朋可以轻松在此基础上实现。 最后,错误在所难免,如果大家发现有错误,欢迎大家指正。 具体参看:http://blog.csdn.net/paschen/article/details/50640421
  • PE 文件解析

    2008-03-29 17:39:13
    自己写的一个逐步分析PE文件的VC例子,比较简单易懂,适合想进阶PE加密方向的同行。
  • PE文件解析

    2017-08-20 10:56:14
    简易的基于文件操作的PE文件解析器源码
  • [网络安全自学篇] 六十二.PE文件逆向之PE文件解析、PE编辑工具使用和PE结构修改(三).pdf

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,008
精华内容 5,203
关键字:

pe文件解析