滴水逆向三期实践2:PE节表解析

书接上回

节表,就是在NT_HEADER 之后紧接着的那块区域,有多个相同结构的节表,整个区域就是个结构体数组,具体有多少个这样相同的节表结构 在COFF头(FILE_HEADER)的 NumberOfSections 字段。

所以要遍历节表,只需 for 循环 NumberOfSections 次即可

节表各字段说明如下,后续有更详细的解析:

1、Name                              8字节 一般以"\0"结尾的ASCII码字符串来标识的名称,内容可自定义
注意:该名称并不遵守必须以"\0"结尾的规律,如果不是以"\0"结尾,系统会截取8个字节的长度进行处理。在自写程序中读取时需注意这点                                         
2、Misc                                双字 是该节在没有对齐前的真实尺寸,该值可以不准确。                    
3、VirtualAddress                节区在内存中的偏移地址,加上ImageBase才是在内存中的真正地址.  
4、SizeOfRawData              节在文件中对齐后的尺寸.                             
5、PointerToRawData          节区在文件中的偏移.                                  
6、PointerToRelocations      在obj文件中使用 对exe无意义                    
7、PointerToLinenumbers    行号表的位置 调试的时候使用                                 
8、NumberOfRelocations     在obj文件中使用  对exe无意义                                  
9、NumberOfLinenumbers   行号表中行号的数量 调试的时候使用                             
10、Characteristics               节的属性                    
                    
更直观的节表存储结构如下,一行16字节 

节表其实就相当于书本的大章节目录,记录书本内从多少页开始,有多少页的"长度"结束。下一章节又从第几页开始,从节起始位置开始数多少页结束这一章节。目录结束后就是正文了,后面的内容就是第一个节的起始位置了。当然中间会有一段空隙

换到程序中,就是 PointerToRawData 是PE文件躺在硬盘的时候(作为文件时)的该节开始位置,注意这个起始位置是相对于文件开头的偏移,但文件在硬盘上MZ头(DOS头的magic)的位置一定是0,所以其实也是文件中的绝对位置了。

而 SizeOfRawData  就是节在PE文件中的长度,但这个长度是根据文件对齐后的长度。这个文件对齐的值在可选头 OPTIONAL_HEADER 中的 FileAlignment 字段,比如FileAlignment值为200,实际该节在文件中的长度是366,那么SizeOfRawData值为400。总之这个长度必须为FileAlignment的整数倍,实际该节在文件中的长度一定是小于或等于SizeOfRawData的。

VirtualAddress就是当文件加载进内存后(比如双击运行该文件)的节的起始地址,而且也是相对于文件MZ头起始地址的偏移,即是一个相对位置。在同一文件同一个节中在硬盘上文件的节起始位置 和 被加载进内存中 的节起始位置不一样 (即使他们都是相对于MZ头的偏移)是不是很奇怪? 

而且节在文件中有节长度,那么内存中也有节长度才对,这个内存中的节长度就是Misc,也就是union形式(C语言关键字union)的Misc的其中一种形态VirtualSize。该值本意是记录放入内存后节中的真实长度(无对齐)。但VirtualSize这个值可以不准确,即瞎填都不影响加载。

这时注意到SectionAlignment内存对齐和FileAlignment文件对齐,是两个不一定相同的值(也有可能相同)那么两种对齐值都不一样,也就不难理解为什么要分为SizeOfRawData 和 VirtualAddress 了。

涉及到操作系统加载文件时的拉伸问题。即文件躺在硬盘上以一种长度对齐,但到了内存中会变为以更长的长度对齐(内存对齐的值通常 ≥ 文件对齐)所以由短变长,就是个拉伸的过程,具体见下图,至于文件从硬盘到内存是怎么变长的,会有个很直观的理解

 PE加载的过程:    
    
1、根据SizeOfImage的大小,开辟一块缓冲区 ImageBuffer(文件在内存中的形式).    
     
2、根据SizeOfHeader的大小,将头信息从FileBuffer(文件在硬盘上的形式)拷贝到 ImageBuffer    
    
3、根据节表中的信息循环进FileBuffer中的节拷贝到 ImageBuffer中.    

图中浅绿色的部分,就是因对齐牺牲的空间,通常是无用空间,通常会填充全零

Characteristics节的属性是按位确定属性的,具体属性位置分布可到 PETool 等工具中查看。

下面就是读取节表的代码了,自己实现一遍或读懂代码可以更深刻理解节表

代码中用到的exe 可自行找一个小点的独立的exe,比如网上搜些逆向用的crackme  exe程序检测自写代码用

#include "stdio.h"
#include "windows.h"
inline LPVOID ReadPEFile(LPSTR lpszFile)  //LPSTR 即  CHAR *
{
	FILE *pFile = NULL;
	DWORD fileSize = 0;
	LPVOID pFileBuffer = NULL;    //LPVOID 即  void *

								  //打开文件	
	pFile = fopen(lpszFile, "rb");
	if (!pFile)
	{
		printf(" 无法打开 EXE 文件! ");
		return NULL;
	}

	//读取文件大小		
	fseek(pFile, 0, SEEK_END);
	fileSize = ftell(pFile);
	fseek(pFile, 0, SEEK_SET);

	//分配缓冲区	
	pFileBuffer = malloc(fileSize);  //pFileBuffer指向一段 分配的exe文件这么大的内存

	if (!pFileBuffer)
	{
		printf(" 分配空间失败! ");
		fclose(pFile);
		return NULL;
	}

	//将文件数据读取到缓冲区	
	size_t n = fread(pFileBuffer, fileSize, 1, pFile);   //size_t 即 unsigned int
	if (!n)
	{
		printf(" 读取数据失败! ");
		free(pFileBuffer);
		fclose(pFile);
		return NULL;
	}

	//关闭文件	
	fclose(pFile);
	return pFileBuffer;		//最后返回指向 已填充exe文件内容 的新开辟空间 的指针
}

VOID h313()
{
	char FilePath[] = "CRACKME.EXE";
	LPVOID pFileBuffer = NULL;
	PIMAGE_DOS_HEADER pDosHeader = NULL;
	PIMAGE_NT_HEADERS pNTHeader = NULL;
	PIMAGE_FILE_HEADER pFileHeader = NULL;
	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
	PIMAGE_SECTION_HEADER pSectionHeader = NULL;

	pFileBuffer = ReadPEFile(FilePath);	//pFileBuffer即指向已装载到内存中的exe首部
	if (!pFileBuffer)
	{
		printf("文件读取失败\n");
		return;
	}

	//判断是否是有效的MZ标志	
	if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE)	//pFileBuffer强转 WORD* 后 取指针指向的内容
	{
		printf("不是有效的MZ标志\n");
		free(pFileBuffer);
		return;
	}
	pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;	// 强转 DOS_HEADER 结构体指针
													//打印DOS头	
	printf("********************DOS头********************\n");
	printf("MZ标志:%x\n", pDosHeader->e_magic);
	printf("PE偏移:%x\n", pDosHeader->e_lfanew);

	//判断是否是有效的PE标志	
	if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)	//基址pFileBuffer + lfanew 为 NTHeader首址
	{
		printf("不是有效的PE标志\n");
		free(pFileBuffer);
		return;
	}
	pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);

	//打印NT头	
	printf("*****************NT HEADERS*****************\n");
	printf("NT:%x\n", pNTHeader->Signature);
	pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);	//NT头地址 + 4 为 FileHeader 首址
	printf("****************FILE HEADER****************\n");
	printf("PE:%x\n", pFileHeader->Machine);
	printf("节的数量:%x\n", pFileHeader->NumberOfSections);
	printf("SizeOfOptionalHeader:%x\n", pFileHeader->SizeOfOptionalHeader);

	//可选PE头	
	pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER为固定值且不存在于PE文件字段中
	printf("**************OPTIOINAL HEADER*************\n");
	printf("Magic:%x\n", pOptionalHeader->Magic);
	

	//首个节表
	pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
	char Name[IMAGE_SIZEOF_SHORT_NAME + 1];    //用于存放节表名,因为节表名可能被写满八个字节,所以这里+1多一个存放 '\0'

	//循环遍历节表
	for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++)	//注意这里i从1开始 i < NumberOfSections
	{
		memset(Name, 0, sizeof(Name));
		printf("*************** Section Headers %d **************\n",i);
		memcpy(Name, pSectionHeader->Name, IMAGE_SIZEOF_SHORT_NAME);
		printf("Name:%s\n", Name);
		printf("Misc:%x\n", pSectionHeader->Misc);
		printf("VirtualAddress:%x\n", pSectionHeader->VirtualAddress);
		printf("SizeOfRawDat:%x\n", pSectionHeader->SizeOfRawData);
		printf("PointerToRawData:%x\n", pSectionHeader->PointerToRawData);
		printf("Characteristics:%x\n", pSectionHeader->Characteristics);
	}	//出循环后pSectionHeader指向最后一个节表

	//释放内存	
	free(pFileBuffer);
}

运行程序打印内容如下:

********************DOS头********************
MZ标志:5a4d
PE偏移:100
*****************NT HEADERS*****************
NT:4550
****************FILE HEADER****************
PE:14c
节的数量:6
SizeOfOptionalHeader:e0
**************OPTIOINAL HEADER*************
Magic:10b
*************** Section Headers 1 **************
Name:CODE
Misc:1000
VirtualAddress:1000
SizeOfRawDat:600
PointerToRawData:600
Characteristics:60000020
*************** Section Headers 2 **************
Name:DATA
Misc:1000
VirtualAddress:2000
SizeOfRawDat:200
PointerToRawData:c00
Characteristics:c0000040
*************** Section Headers 3 **************
Name:.idata
Misc:1000
VirtualAddress:3000
SizeOfRawDat:800
PointerToRawData:e00
Characteristics:c0000040
*************** Section Headers 4 **************
Name:.edata
Misc:1000
VirtualAddress:4000
SizeOfRawDat:200
PointerToRawData:1600
Characteristics:40000040
*************** Section Headers 5 **************
Name:.reloc
Misc:1000
VirtualAddress:5000
SizeOfRawDat:200
PointerToRawData:1800
Characteristics:50000040

可使用 PETool  或 LoadPE 等小软件查看相应PE文件内容。对照打印内容检测自写的程序是否正确。

这里使用 PETool 查看的上述显示值 如下

 

 

 

 

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值