驱动开发:内核解析PE结构节表

打印 上一主题 下一主题

主题 913|帖子 913|积分 2739

在笔者上一篇文章《驱动开发:内核解析PE结构导出表》介绍了如何解析内存导出表结构,本章将继续延申实现解析PE结构的PE头,PE节表等数据,总体而言内核中解析PE结构与应用层没什么不同,在上一篇文章中LyShark封装实现了KernelMapFile()内存映射函数,在之后的章节中这个函数会被多次用到,为了减少代码冗余,后期文章只列出重要部分,读者可以自行去前面的文章中寻找特定的片段。
Windows NT 系统中可执行文件使用微软设计的新的文件格式,也就是至今还在使用的PE格式,PE文件的基本结构如下图所示:

在PE文件中,代码,已初始化的数据,资源和重定位信息等数据被按照属性分类放到不同的Section(节区/或简称为节)中,而每个节区的属性和位置等信息用一个IMAGE_SECTION_HEADER结构来描述,所有的IMAGE_SECTION_HEADER结构组成了一个节表(Section Table),节表数据在PE文件中被放在所有节数据的前面.
上面PE结构图中可知PE文件的开头部分包括了一个标准的DOS可执行文件结构,这看上去有些奇怪,但是这对于可执行程序的向下兼容性来说却是不可缺少的,当然现在已经基本不会出现纯DOS程序了,现在来说这个IMAGE_DOS_HEADER结构纯粹是历史遗留问题。
DOS头结构解析: PE文件中的DOS部分由MZ格式的文件头和可执行代码部分组成,可执行代码被称为DOS块(DOS stub),MZ格式的文件头由IMAGE_DOS_HEADER结构定义,在C语言头文件winnt.h中有对这个DOS结构详细定义,如下所示:
  1. typedef struct _IMAGE_DOS_HEADER {
  2.     WORD   e_magic;                     // DOS的头部
  3.     WORD   e_cblp;                      // Bytes on last page of file
  4.     WORD   e_cp;                        // Pages in file
  5.     WORD   e_crlc;                      // Relocations
  6.     WORD   e_cparhdr;                   // Size of header in paragraphs
  7.     WORD   e_minalloc;                  // Minimum extra paragraphs needed
  8.     WORD   e_maxalloc;                  // Maximum extra paragraphs needed
  9.     WORD   e_ss;                        // Initial (relative) SS value
  10.     WORD   e_sp;                        // Initial SP value
  11.     WORD   e_csum;                      // Checksum
  12.     WORD   e_ip;                        // Initial IP value
  13.     WORD   e_cs;                        // Initial (relative) CS value
  14.     WORD   e_lfarlc;                    // File address of relocation table
  15.     WORD   e_ovno;                      // Overlay number
  16.     WORD   e_res[4];                    // Reserved words
  17.     WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
  18.     WORD   e_oeminfo;                   // OEM information; e_oemid specific
  19.     WORD   e_res2[10];                  // Reserved words
  20.     LONG   e_lfanew;                    // 指向了PE文件的开头(重要)
  21.   } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
复制代码
在DOS文件头中,第一个字段e_magic被定义为MZ,标志着DOS文件的开头部分,最后一个字段e_lfanew则指明了PE文件的开头位置,现在来说除了第一个字段和最后一个字段有些用处,其他字段几乎已经废弃了,这里附上读取DOS头的代码。
  1. void DisplayDOSHeadInfo(HANDLE ImageBase)
  2. {
  3.     PIMAGE_DOS_HEADER pDosHead = NULL;
  4.     pDosHead = (PIMAGE_DOS_HEADER)ImageBase;
  5.     printf("DOS头:        %x\n", pDosHead->e_magic);
  6.     printf("文件地址:     %x\n", pDosHead->e_lfarlc);
  7.     printf("PE结构偏移:   %x\n", pDosHead->e_lfanew);
  8. }
复制代码
PE头结构解析: 从DOS文件头的e_lfanew字段向下偏移003CH的位置,就是真正的PE文件头的位置,该文件头是由IMAGE_NT_HEADERS结构定义的,定义结构如下:
  1. typedef struct _IMAGE_NT_HEADERS {
  2.     DWORD Signature;                   // PE文件标识字符
  3.     IMAGE_FILE_HEADER FileHeader;
  4.     IMAGE_OPTIONAL_HEADER32 OptionalHeader;
  5. } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
复制代码
如上PE文件头的第一个DWORD是一个标志,默认情况下它被定义为00004550h也就是P,E两个字符另外加上两个零,而大部分的文件属性由标志后面的IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER32结构来定义,我们继续跟进IMAGE_FILE_HEADER这个结构:
  1. typedef struct _IMAGE_FILE_HEADER {
  2.     WORD    Machine;                  // 运行平台
  3.     WORD    NumberOfSections;         // 文件的节数目
  4.     DWORD   TimeDateStamp;            // 文件创建日期和时间
  5.     DWORD   PointerToSymbolTable;     // 指向符号表(用于调试)
  6.     DWORD   NumberOfSymbols;          // 符号表中的符号数量
  7.     WORD    SizeOfOptionalHeader;     // IMAGE_OPTIONAL_HANDLER32结构的长度
  8.     WORD    Characteristics;          // 文件的属性 exe=010fh dll=210eh
  9. } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
复制代码
继续跟进 IMAGE_OPTIONAL_HEADER32 结构,该结构体中的数据就丰富了,重要的结构说明经备注好了:
  1. typedef struct _IMAGE_OPTIONAL_HEADER {
  2.     WORD    Magic;
  3.     BYTE    MajorLinkerVersion;           // 连接器版本
  4.     BYTE    MinorLinkerVersion;
  5.     DWORD   SizeOfCode;                   // 所有包含代码节的总大小
  6.     DWORD   SizeOfInitializedData;        // 所有已初始化数据的节总大小
  7.     DWORD   SizeOfUninitializedData;      // 所有未初始化数据的节总大小
  8.     DWORD   AddressOfEntryPoint;          // 程序执行入口RVA
  9.     DWORD   BaseOfCode;                   // 代码节的起始RVA
  10.     DWORD   BaseOfData;                   // 数据节的起始RVA
  11.     DWORD   ImageBase;                    // 程序镜像基地址
  12.     DWORD   SectionAlignment;             // 内存中节的对其粒度
  13.     DWORD   FileAlignment;                // 文件中节的对其粒度
  14.     WORD    MajorOperatingSystemVersion;  // 操作系统主版本号
  15.     WORD    MinorOperatingSystemVersion;  // 操作系统副版本号
  16.     WORD    MajorImageVersion;            // 可运行于操作系统的最小版本号
  17.     WORD    MinorImageVersion;
  18.     WORD    MajorSubsystemVersion;        // 可运行于操作系统的最小子版本号
  19.     WORD    MinorSubsystemVersion;
  20.     DWORD   Win32VersionValue;
  21.     DWORD   SizeOfImage;                  // 内存中整个PE映像尺寸
  22.     DWORD   SizeOfHeaders;                // 所有头加节表的大小
  23.     DWORD   CheckSum;
  24.     WORD    Subsystem;
  25.     WORD    DllCharacteristics;
  26.     DWORD   SizeOfStackReserve;           // 初始化时堆栈大小
  27.     DWORD   SizeOfStackCommit;
  28.     DWORD   SizeOfHeapReserve;
  29.     DWORD   SizeOfHeapCommit;
  30.     DWORD   LoaderFlags;
  31.     DWORD   NumberOfRvaAndSizes;          // 数据目录的结构数量
  32.     IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
  33. } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
复制代码
IMAGE_DATA_DIRECTORY数据目录列表,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,这16个数据目录结构定义很简单仅仅指出了某种数据的位置和长度,定义如下:
  1. typedef struct _IMAGE_DATA_DIRECTORY {
  2.     DWORD   VirtualAddress;      // 数据起始RVA
  3.     DWORD   Size;                // 数据块的长度
  4. } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
复制代码
上方的结构就是PE文件的重要结构,接下来将通过编程读取出PE文件的开头相关数据,读取这些结构也非常简单代码如下所示。
  1. // 署名权
  2. // right to sign one's name on a piece of work
  3. // PowerBy: LyShark
  4. // Email: me@lyshark.com
  5. NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
  6. {
  7.         DbgPrint("hello lyshark.com \n");
  8.         NTSTATUS status = STATUS_SUCCESS;
  9.         HANDLE hFile = NULL;
  10.         HANDLE hSection = NULL;
  11.         PVOID pBaseAddress = NULL;
  12.         UNICODE_STRING FileName = { 0 };
  13.         // 初始化字符串
  14.         RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
  15.         // 内存映射文件
  16.         status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
  17.         if (!NT_SUCCESS(status))
  18.         {
  19.                 return 0;
  20.         }
  21.         // 获取PE头数据集
  22.         PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
  23.         PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
  24.         PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;
  25.         DbgPrint("运行平台:     %x\n", pFileHeader->Machine);
  26.         DbgPrint("节区数目:     %x\n", pFileHeader->NumberOfSections);
  27.         DbgPrint("时间标记:     %x\n", pFileHeader->TimeDateStamp);
  28.         DbgPrint("可选头大小    %x\n", pFileHeader->SizeOfOptionalHeader);
  29.         DbgPrint("文件特性:     %x\n", pFileHeader->Characteristics);
  30.         DbgPrint("入口点:        %p\n", pNtHeaders->OptionalHeader.AddressOfEntryPoint);
  31.         DbgPrint("镜像基址:      %p\n", pNtHeaders->OptionalHeader.ImageBase);
  32.         DbgPrint("镜像大小:      %p\n", pNtHeaders->OptionalHeader.SizeOfImage);
  33.         DbgPrint("代码基址:      %p\n", pNtHeaders->OptionalHeader.BaseOfCode);
  34.         DbgPrint("区块对齐:      %p\n", pNtHeaders->OptionalHeader.SectionAlignment);
  35.         DbgPrint("文件块对齐:    %p\n", pNtHeaders->OptionalHeader.FileAlignment);
  36.         DbgPrint("子系统:        %x\n", pNtHeaders->OptionalHeader.Subsystem);
  37.         DbgPrint("区段数目:      %d\n", pNtHeaders->FileHeader.NumberOfSections);
  38.         DbgPrint("时间日期标志:  %x\n", pNtHeaders->FileHeader.TimeDateStamp);
  39.         DbgPrint("首部大小:      %x\n", pNtHeaders->OptionalHeader.SizeOfHeaders);
  40.         DbgPrint("特征值:        %x\n", pNtHeaders->FileHeader.Characteristics);
  41.         DbgPrint("校验和:        %x\n", pNtHeaders->OptionalHeader.CheckSum);
  42.         DbgPrint("可选头部大小:  %x\n", pNtHeaders->FileHeader.SizeOfOptionalHeader);
  43.         DbgPrint("RVA 数及大小:  %x\n", pNtHeaders->OptionalHeader.NumberOfRvaAndSizes);
  44.         ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
  45.         ZwClose(hSection);
  46.         ZwClose(hFile);
  47.         Driver->DriverUnload = UnDriver;
  48.         return STATUS_SUCCESS;
  49. }
复制代码
运行如上这段代码,即可解析出ntdll.dll模块的核心内容,如下图所示;

接着来实现解析节表,PE文件中的所有节的属性定义都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构邮过来描述一个节,节表总被存放在紧接在PE文件头的地方,也即是从PE文件头开始偏移为00f8h的位置处,如下是节表头部的定义。
  1. typedef struct _IMAGE_SECTION_HEADER {
  2.     BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
  3.     union {
  4.             DWORD   PhysicalAddress;
  5.             DWORD   VirtualSize;           // 节区尺寸
  6.     } Misc;
  7.     DWORD   VirtualAddress;                // 节区RVA
  8.     DWORD   SizeOfRawData;                 // 在文件中对齐后的尺寸
  9.     DWORD   PointerToRawData;              // 在文件中的偏移
  10.     DWORD   PointerToRelocations;          // 在OBJ文件中使用
  11.     DWORD   PointerToLinenumbers;
  12.     WORD    NumberOfRelocations;
  13.     WORD    NumberOfLinenumbers;
  14.     DWORD   Characteristics;               // 节区属性字段
  15. } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
复制代码
解析节表也很容易实现,首先通过pFileHeader->NumberOfSections获取到节数量,然后循环解析直到所有节输出完成,这段代码实现如下所示。
  1. // 署名权
  2. // right to sign one's name on a piece of work
  3. // PowerBy: LyShark
  4. // Email: me@lyshark.com
  5. NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
  6. {
  7.         DbgPrint("hello lyshark.com \n");
  8.         NTSTATUS status = STATUS_SUCCESS;
  9.         HANDLE hFile = NULL;
  10.         HANDLE hSection = NULL;
  11.         PVOID pBaseAddress = NULL;
  12.         UNICODE_STRING FileName = { 0 };
  13.         // 初始化字符串
  14.         RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
  15.         // 内存映射文件
  16.         status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
  17.         if (!NT_SUCCESS(status))
  18.         {
  19.                 return 0;
  20.         }
  21.         // 获取PE头数据集
  22.         PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
  23.         PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
  24.         PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
  25.         PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;
  26.         DWORD NumberOfSectinsCount = 0;
  27.         // 获取区块数量
  28.         NumberOfSectinsCount = pFileHeader->NumberOfSections;
  29.         DWORD64 *difA = NULL;   // 虚拟地址开头
  30.         DWORD64 *difS = NULL;   // 相对偏移(用于遍历)
  31.         difA = ExAllocatePool(NonPagedPool, NumberOfSectinsCount*sizeof(DWORD64));
  32.         difS = ExAllocatePool(NonPagedPool, NumberOfSectinsCount*sizeof(DWORD64));
  33.         DbgPrint("节区名称 相对偏移\t虚拟大小\tRaw数据指针\tRaw数据大小\t节区属性\n");
  34.         for (DWORD temp = 0; temp<NumberOfSectinsCount; temp++, pSection++)
  35.         {
  36.                 DbgPrint("%10s\t 0x%x \t 0x%x \t 0x%x \t 0x%x \t 0x%x \n",
  37.                         pSection->Name, pSection->VirtualAddress, pSection->Misc.VirtualSize,
  38.                         pSection->PointerToRawData, pSection->SizeOfRawData, pSection->Characteristics);
  39.                 difA[temp] = pSection->VirtualAddress;
  40.                 difS[temp] = pSection->VirtualAddress - pSection->PointerToRawData;
  41.         }
  42.         ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
  43.         ZwClose(hSection);
  44.         ZwClose(hFile);
  45.         Driver->DriverUnload = UnDriver;
  46.         return STATUS_SUCCESS;
  47. }
复制代码
运行驱动程序,即可输出ntdll.dll模块的节表信息,如下图;


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立山

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表