低调的菜鸡 发表于 2019-12-20 22:04

[造轮子系列]手动打造一个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
      偷偷告诉大家,这次的源码可是我用上次的清理工具清理过的哟,就这么大,刺激不,哈哈哈
## 五、结语
      怕大家理解困难,所以这里并没有进行导入导出表的解析,功能可能会在下面一次帖子加上,主要是做界面太麻烦了,耗费时间,哈哈。看这次的反应吧,如果大家不喜欢,那可能就只有这一个版本了。

冥月影 发表于 2019-12-20 22:20

不懂这个,但还是支持一下。。。

518 发表于 2019-12-20 22:50

头像爱了

erh 发表于 2019-12-20 22:59

好帖子,多谢!希望能看到更多精彩内容!

diyikuai 发表于 2019-12-20 23:11

这真是高手所为。

moranyuyan 发表于 2019-12-20 23:13

谢谢分享

SherryMefum 发表于 2019-12-20 23:26

MFC很多第三方UI库,还可以自绘,但没什么必要,这种工具效率为主。

蒂兰圣雪 发表于 2019-12-21 00:18

不明觉厉支持下!

syrmb 发表于 2019-12-21 00:19

用c画界面会哭

growing88 发表于 2019-12-21 00:57

感谢楼主,谢谢分享
页: [1] 2
查看完整版本: [造轮子系列]手动打造一个PE分析工具