[造轮子系列]手动打造一个PE分析工具
本帖最后由 低调的菜鸡 于 2019-12-20 22:09 编辑十分感谢版主大大给我的上一篇帖子加了优秀,这次为了回报坛友,就借着发帖的机会,跟各位小伙伴们分享一下PE结构
## 引言
一提起LoadPE,或者PEID,相信在座的各位不管是大牛还是和我一样新入行的小白都特别熟悉,那么,今天我就手把手的跟大家一起造个轮子,亲手打造一个PE解析工具,同时也和大家分享一下PE文件结构相关的知识。界面仿的LoadPE,全程MFC开发
为什么用MFC呢,因为不想在界面上下功夫,我们主要是学PE的结构,MFC的界面丑是丑了点,但是能看,各位大佬轻喷,接下来我们就进入今天的主题吧
## 一、程序运行效果
## 二、PE结构
**请注意:这个结构特别重要**
1. 为什么要了解PE结构?
* PE文件格式自古以来都是Windows软件安全的一个非常重要的知识点,不管是加壳脱壳,IAT注入,HOOK寻找文件偏移,都离不开这个结构。它描述了程序运行的所有信息,什么入口点啦,导入导出表啦,资源文件啦,几乎你所需要的一切,都能在这个结构里找到。当然,如果熟练掌握了这个结构,以后在Windows逆向的过程中,必然事半功倍
2. PE到底是什么?
* PE文件格式是微软Windows NT内核系列系统和Win32子集中可执行的二进制文件格式。我们加载的用的DLL模块,以及我们能跑起来的EXE程序,都必须遵从这个格式。
* 如果简单说的话,PE其实就是可执行文件中,区段前的一堆二进制代码,存放着可执行文件里面所有的信息。
3. PE结构详解
* 大致描述:这里只是概述一下有哪些东西,我们需要用到什么
* PE文件从文件开始,分别有3大数据块
* MS-DOS头:为了兼容dos,没什么用,现在已经成为PE文件的标配。
* 重要的字段:WORD e_magic; // Magic number 0x5A4D
LONG e_lfanew; // PE头的偏移,用于找到PE头的位置
* PE头:
* 标志位:标志是否为一个PE文件
* PE文件头:包含了区段数量,运行平台,文件类型,文件创建时间等信息
* PE可选头:包含了程序运行入口的RVA,程序默认载入基地址,目录表等信息
* 目录表(**非常重要**):我们的导入、导出表以及IAT表的地址就存放在这里
* 区段表:里面存放着我们加载入内存中的区段信息,比如数据段,代码段等
* 详细解释:这里的结构体代码详细描述了上述结构
* MS-DOS头:
```c
//DOS头
//typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
// WORD e_magic; // Magic number 0x5A4D
// WORD e_cblp; // 文件末页字节数
// WORD e_cp; // 文件页数
// WORD e_crlc; // 重定位项的数目
// WORD e_cparhdr; // 区段中头部大小
// WORD e_minalloc; // 最小内存附加段需求
// WORD e_maxalloc; // 最大内存附加段需求
// WORD e_ss; // 初始SS值
// WORD e_sp; // 初始SP值
// WORD e_csum; // 校验和
// WORD e_ip; // 初始IP值(程序入口点)
// WORD e_cs; // 初始CS值
// WORD e_lfarlc; // 重定位表偏移
// WORD e_ovno; // 代码附加数
// WORD e_res; // Reserved words
// WORD e_oemid; // OEM identifier (for e_oeminfo)
// WORD e_oeminfo; // OEM information; e_oemid specific
// WORD e_res2; // Reserved words
// LONG e_lfanew; // PE头的偏移
//} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
```
* PE头
```c
//PE文件头(PE头)
//typedef struct _IMAGE_FILE_HEADER {
// WORD Machine; //运行平台,一般为IMAGE_FILE_MACHINE_I386
// WORD NumberOfSections; //区段的数量
// DWORD TimeDateStamp; //文件的创建时间
// DWORD PointerToSymbolTable; //符号表指针,一般为0
// DWORD NumberOfSymbols; //符号表中符号的数量
// WORD SizeOfOptionalHeader; //在IMAGE_FILE_HEADER结构后面的扩展头大小,一般为0x00E0
// WORD Characteristics; //文件属性,EXE文件(IMAGE_FILE_EXECUTABLE_IMAGE)为0x010F,DLL(IMAGE_FILE_DLL)为0x210
//} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
```
* PE文件头
```c
//PE文件头(PE头)
//typedef struct _IMAGE_FILE_HEADER {
// WORD Machine; //运行平台,一般为IMAGE_FILE_MACHINE_I386
// WORD NumberOfSections; //区段的数量
// DWORD TimeDateStamp; //文件的创建时间
// DWORD PointerToSymbolTable; //符号表指针,一般为0
// DWORD NumberOfSymbols; //符号表中符号的数量
// WORD SizeOfOptionalHeader; //在IMAGE_FILE_HEADER结构后面的扩展头大小,一般为0x00E0
// WORD Characteristics; //文件属性,EXE文件(IMAGE_FILE_EXECUTABLE_IMAGE)为0x010F,DLL(IMAGE_FILE_DLL)为0x210
//} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
```
* PE可选头
```c
//PE可选头
//typedef struct _IMAGE_OPTIONAL_HEADER {
// //
// // Standard fields.
// //
//
// WORD Magic; //文件类型标识(普通可执行映像0x010B、ROM镜像为0x0170、PE32+为0x020B)
// BYTE MajorLinkerVersion; //链接器的主版本号。
// BYTE MinorLinkerVersion; //链接器的子版本号。
// DWORD SizeOfCode; //所有IMAGE_SCN_CNT_CODE属性的区段总大小,此大小在计算时按照磁盘扇区字节数的整数倍计算。
// DWORD SizeOfInitializedData; //已初始化的数据块大小。
// DWORD SizeOfUninitializedData;//未初始化的数据块大小,装载程序需要在虚拟地址空间中为这些
//数据保留空间,但是这些块在磁盘中并不占用任何空间(在程序
//运行之前没有指定的值),未初始化的数据通常都位于一个名称为.bbs的区段中。
// DWORD AddressOfEntryPoint; //程序执行入口的RAV,一般指向运行时的库代码,然后再调用main
//在DLL文件中,这个入口
//点有可能在进程初始化、进程关闭、线程创建与线程关闭时被调用,
//并且鉴于DLL文件的特殊性,这个入口点在DLL文件中可以被置为0
// DWORD BaseOfCode; //代码段的起始RVA,这个值通常为0x00001000
// DWORD BaseOfData; //数据段的起始RVA,数据段通常位于PE文件头与代码段之后
//
// //
// // NT additional fields.
// //
//
// DWORD ImageBase; //文件在内存中的首选装入,地址加载器将试图在此地址装
//入这个映像文件,如果载入成功,则装载器将跳过应用基
//址重定位的步骤,如果此地址被占用,则加载器会重新在
//正确对齐的合法地址中选择一个作为实际装载地址。
// DWORD SectionAlignment; //映像文件在被装入内存时的区段对齐大小
// DWORD FileAlignment; //映像文件在磁盘上的区段对齐大小
// WORD MajorOperatingSystemVersion;//要求操作系统最低版本的主版本号
// WORD MinorOperatingSystemVersion;//要求操作系统最低版本的子版本号
// WORD MajorImageVersion; //此可执行文件的主版本号,此版本号由程序作者指定
//(它与后一个字段成对使用,可以被置为0,可以通过连接器开关 / VERSION控制)
// WORD MinorImageVersion; //此可执行文件的子版本号,此版本号由程序作者指定
// WORD MajorSubsystemVersion; //要求最低子系统的主版本号(与后一个字段成对使用,一
//般情况下其值为4,可以通过连接器开关 / SUBSYSTEM控制)。
// WORD MinorSubsystemVersion; //要求最低子系统的子版本号
// DWORD Win32VersionValue; //这是一个保留值,且必须为0x00000000
// DWORD SizeOfImage; //映像文件装入内存后的总大小(从Image Base
//到最后一个区段的总大小)
// DWORD SizeOfHeaders; //是MS-DOS头、PE头、区块表的尺寸之和
// DWORD CheckSum; //映像文件的校验和(在内核模式的驱动和系统dll中必须为有效值)
// WORD Subsystem; //可执行文件所期望的子系统值IMAGE_SUBSYSTEM_WINDOWS_GUI
// WORD DllCharacteristics; //DllMain()函数何时被调用,默认为0
// DWORD SizeOfStackReserve; //在EXE文件中,为线程保留的堆栈大小
// DWORD SizeOfStackCommit; //在EXE文件中,栈初始内存大小(默认4KB)
// DWORD SizeOfHeapReserve; //在EXE文件中,为进程默认堆保留的内存(默认1MB)
// DWORD SizeOfHeapCommit; //在EXE文件中,每次指派给堆的内存大小(默认4KB)
// DWORD LoaderFlags; //与调试有关,默认为0
// DWORD NumberOfRvaAndSizes; //数据目录成员的数量,一般为0x00000010(16个)
//数据目录表
// IMAGE_DATA_DIRECTORY DataDirectory;
//} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
```
* 数据目录表
```c
//数据目录表
//typedef struct _IMAGE_DATA_DIRECTORY {
// DWORD VirtualAddress; // 数据起始块的RVA地址
// DWORD Size; // 数据块的长度
//} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
```
* 区段表
```c
//区段表
//typedef struct _IMAGE_SECTION_HEADER {
// BYTE Name; //区段名,一般情况下以.开头
// union {
// DWORD PhysicalAddress; //
// DWORD VirtualSize; //
// } Misc; //实际被使用的区段大小
// DWORD VirtualAddress; //此区段载入内存后的RAV
// DWORD SizeOfRawData; //此区段在磁盘中的体积,这个地址是按照文件页对齐
//的(恒为PE头结构中FileAlignment字段的整数倍)
// DWORD PointerToRawData; //此区段在文件中的偏移
// DWORD PointerToRelocations; //此区段重定位表的偏移地址,它指向IMAGE_RELOCATION结构数组
// DWORD PointerToLinenumbers; //行号表在文件中的偏移
// WORD NumberOfRelocations; //此区段重定位表项的数量
// WORD NumberOfLinenumbers; //行号表项的数量
// DWORD Characteristics; //区段属性,用以描述此区段的读写情况、状态等属性
//IMAGE_SCN_MEM_READ(可读)
//} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
```
* 好啦,我们对PE结构的介绍就到这里啦,如果想深究,大家可以下来细品我的工程代码和我上述的教程这里就不多说啦,有个印象就成,接下来咱们就来获取PE结构吧。
## 三、关键代码
这里我封装了一个C语言的函数,可以直接把PE文件的这些信息存放到内存中
```c
bool CLoadPEDlg::GetFilePEInfo(const TCHAR * pszPathName)
{
// 获取当前文件的所有信息,保存到类的结构体中
//打开文件
FILE *pFile = NULL;
_tfopen_s(&pFile, pszPathName, _T("rb+"));
if (pFile == NULL)
{
MessageBox(_T("文件打开失败!!"));
return false;
}
//读取DOS头的信息
fread(&m_stDosHeader, sizeof(IMAGE_DOS_HEADER), 1, pFile);
//定义NT头结构体
IMAGE_NT_HEADERS stNtHeader;
//获取NT头信息
fseek(pFile, m_stDosHeader.e_lfanew, SEEK_SET);
fread(&stNtHeader, sizeof(IMAGE_NT_HEADERS), 1, pFile);
//如果不是PE文件,则提示重新获取
if (m_stDosHeader.e_magic != IMAGE_DOS_SIGNATURE || stNtHeader.Signature != IMAGE_NT_SIGNATURE)
{
MessageBox(_T("该文件不是可解析的PE文件,请重试"),_T("ERROR"));
fclose(pFile);
return false;
}
//获取文件头信息和可选头信息
m_stFileHeader = stNtHeader.FileHeader;
m_stOptHeader = stNtHeader.OptionalHeader;
//循环获取区段表
m_vecSections.clear();
IMAGE_SECTION_HEADER stSectionHeader;
for (int i = 0; i < m_stFileHeader.NumberOfSections; i++)
{
fread(&stSectionHeader, sizeof(IMAGE_SECTION_HEADER), 1, pFile);
m_vecSections.push_back(stSectionHeader);
}
fclose(pFile);
return true;
}
```
可以看到,我这里是用的文件读写直接做的,其实PE结构也就这样,把文件的相应位读出来,就可以根据上述结构体直接解析啦。
## 四、工程源码及下载
PE工具源码:
编译环境:vs2017 + win10
偷偷告诉大家,这次的源码可是我用上次的清理工具清理过的哟,就这么大,刺激不,哈哈哈
## 五、结语
怕大家理解困难,所以这里并没有进行导入导出表的解析,功能可能会在下面一次帖子加上,主要是做界面太麻烦了,耗费时间,哈哈。看这次的反应吧,如果大家不喜欢,那可能就只有这一个版本了。 不懂这个,但还是支持一下。。。 头像爱了 好帖子,多谢!希望能看到更多精彩内容! 这真是高手所为。 谢谢分享 MFC很多第三方UI库,还可以自绘,但没什么必要,这种工具效率为主。 不明觉厉支持下! 用c画界面会哭 感谢楼主,谢谢分享
页:
[1]
2