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

2023-11-23 02:12

本文主要是介绍9.3 Windows驱动开发:内核解析PE结构节表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在笔者上一篇文章《内核解析PE结构导出表》介绍了如何解析内存导出表结构,本章将继续延申实现解析PE结构的PE头,PE节表等数据,总体而言内核中解析PE结构与应用层没什么不同,在上一篇文章中LyShark封装实现了KernelMapFile()内存映射函数,在之后的章节中这个函数会被多次用到,为了减少代码冗余,后期文章只列出重要部分,读者可以自行去前面的文章中寻找特定的片段。

PE结构(Portable Executable Structure)是Windows操作系统用于执行可执行文件和动态链接库(DLL)的标准格式。节表(Section Table)是PE结构中的一个部分,它记录了可执行文件或DLL中每个区域的详细信息,例如代码、数据、资源等。

Windows NT 系统中可执行文件使用微软设计的新的文件格式,PE文件的基本结构如下图所示:

在PE文件中,代码,已初始化的数据,资源和重定位信息等数据被按照属性分类放到不同的Section(节区/或简称为节)中,而每个节区的属性和位置等信息用一个IMAGE_SECTION_HEADER结构来描述,所有的IMAGE_SECTION_HEADER结构组成了一个节表(Section Table),节表数据在PE文件中被放在所有节数据的前面.

上面PE结构图中可知PE文件的开头部分包括了一个标准的DOS可执行文件结构,这看上去有些奇怪,但是这对于可执行程序的向下兼容性来说却是不可缺少的,当然现在已经基本不会出现纯DOS程序了,现在来说这个IMAGE_DOS_HEADER结构纯粹是历史遗留问题。

9.1.1 DOS头结构解析

PE文件中的DOS部分由MZ格式的文件头和可执行代码部分组成,可执行代码被称为DOS块(DOS stub),MZ格式的文件头由IMAGE_DOS_HEADER结构定义,在C语言头文件winnt.h中有对这个DOS结构详细定义,如下所示:

typedef struct _IMAGE_DOS_HEADER { WORD   e_magic;                     // DOS的头部WORD   e_cblp;                      // Bytes on last page of fileWORD   e_cp;                        // Pages in fileWORD   e_crlc;                      // RelocationsWORD   e_cparhdr;                   // Size of header in paragraphsWORD   e_minalloc;                  // Minimum extra paragraphs neededWORD   e_maxalloc;                  // Maximum extra paragraphs neededWORD   e_ss;                        // Initial (relative) SS valueWORD   e_sp;                        // Initial SP valueWORD   e_csum;                      // ChecksumWORD   e_ip;                        // Initial IP valueWORD   e_cs;                        // Initial (relative) CS valueWORD   e_lfarlc;                    // File address of relocation tableWORD   e_ovno;                      // Overlay numberWORD   e_res[4];                    // Reserved wordsWORD   e_oemid;                     // OEM identifier (for e_oeminfo)WORD   e_oeminfo;                   // OEM information; e_oemid specificWORD   e_res2[10];                  // Reserved wordsLONG   e_lfanew;                    // 指向了PE文件的开头(重要)} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

在DOS文件头中,第一个字段e_magic被定义为MZ,标志着DOS文件的开头部分,最后一个字段e_lfanew则指明了PE文件的开头位置,现在来说除了第一个字段和最后一个字段有些用处,其他字段几乎已经废弃了,这里附上读取DOS头的代码。

void DisplayDOSHeadInfo(HANDLE ImageBase)
{PIMAGE_DOS_HEADER pDosHead = NULL;pDosHead = (PIMAGE_DOS_HEADER)ImageBase;printf("DOS头:        %x\n", pDosHead->e_magic);printf("文件地址:     %x\n", pDosHead->e_lfarlc);printf("PE结构偏移:   %x\n", pDosHead->e_lfanew);
}

9.1.2 PE头结构解析

从DOS文件头的e_lfanew字段向下偏移003CH的位置,就是真正的PE文件头的位置,该文件头是由IMAGE_NT_HEADERS结构定义的,定义结构如下:

typedef struct _IMAGE_NT_HEADERS {DWORD Signature;                   // PE文件标识字符IMAGE_FILE_HEADER FileHeader;IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

如上PE文件头的第一个DWORD是一个标志,默认情况下它被定义为00004550h也就是P,E两个字符另外加上两个零,而大部分的文件属性由标志后面的IMAGE_FILE_HEADERIMAGE_OPTIONAL_HEADER32结构来定义,我们继续跟进IMAGE_FILE_HEADER这个结构:

typedef struct _IMAGE_FILE_HEADER {WORD    Machine;                  // 运行平台WORD    NumberOfSections;         // 文件的节数目DWORD   TimeDateStamp;            // 文件创建日期和时间DWORD   PointerToSymbolTable;     // 指向符号表(用于调试)DWORD   NumberOfSymbols;          // 符号表中的符号数量WORD    SizeOfOptionalHeader;     // IMAGE_OPTIONAL_HANDLER32结构的长度WORD    Characteristics;          // 文件的属性 exe=010fh dll=210eh
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

继续跟进 IMAGE_OPTIONAL_HEADER32 结构,该结构体中的数据就丰富了,重要的结构说明经备注好了:

typedef struct _IMAGE_OPTIONAL_HEADER {WORD    Magic;BYTE    MajorLinkerVersion;           // 连接器版本BYTE    MinorLinkerVersion;DWORD   SizeOfCode;                   // 所有包含代码节的总大小DWORD   SizeOfInitializedData;        // 所有已初始化数据的节总大小DWORD   SizeOfUninitializedData;      // 所有未初始化数据的节总大小DWORD   AddressOfEntryPoint;          // 程序执行入口RVADWORD   BaseOfCode;                   // 代码节的起始RVADWORD   BaseOfData;                   // 数据节的起始RVADWORD   ImageBase;                    // 程序镜像基地址DWORD   SectionAlignment;             // 内存中节的对其粒度DWORD   FileAlignment;                // 文件中节的对其粒度WORD    MajorOperatingSystemVersion;  // 操作系统主版本号WORD    MinorOperatingSystemVersion;  // 操作系统副版本号WORD    MajorImageVersion;            // 可运行于操作系统的最小版本号WORD    MinorImageVersion;WORD    MajorSubsystemVersion;        // 可运行于操作系统的最小子版本号WORD    MinorSubsystemVersion;DWORD   Win32VersionValue;DWORD   SizeOfImage;                  // 内存中整个PE映像尺寸DWORD   SizeOfHeaders;                // 所有头加节表的大小DWORD   CheckSum;WORD    Subsystem;WORD    DllCharacteristics;DWORD   SizeOfStackReserve;           // 初始化时堆栈大小DWORD   SizeOfStackCommit;DWORD   SizeOfHeapReserve;DWORD   SizeOfHeapCommit;DWORD   LoaderFlags;DWORD   NumberOfRvaAndSizes;          // 数据目录的结构数量IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

IMAGE_DATA_DIRECTORY数据目录列表,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,这16个数据目录结构定义很简单仅仅指出了某种数据的位置和长度,定义如下:

typedef struct _IMAGE_DATA_DIRECTORY {DWORD   VirtualAddress;      // 数据起始RVADWORD   Size;                // 数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

上方的结构就是PE文件的重要结构,接下来将通过编程读取出PE文件的开头相关数据,读取这些结构也非常简单代码如下所示。

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint("hello lyshark \n");NTSTATUS status = STATUS_SUCCESS;HANDLE hFile = NULL;HANDLE hSection = NULL;PVOID pBaseAddress = NULL;UNICODE_STRING FileName = { 0 };// 初始化字符串RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");// 内存映射文件status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);if (!NT_SUCCESS(status)){return 0;}// 获取PE头数据集PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;DbgPrint("运行平台:     %x\n", pFileHeader->Machine);DbgPrint("节区数目:     %x\n", pFileHeader->NumberOfSections);DbgPrint("时间标记:     %x\n", pFileHeader->TimeDateStamp);DbgPrint("可选头大小    %x\n", pFileHeader->SizeOfOptionalHeader);DbgPrint("文件特性:     %x\n", pFileHeader->Characteristics);DbgPrint("入口点:        %p\n", pNtHeaders->OptionalHeader.AddressOfEntryPoint);DbgPrint("镜像基址:      %p\n", pNtHeaders->OptionalHeader.ImageBase);DbgPrint("镜像大小:      %p\n", pNtHeaders->OptionalHeader.SizeOfImage);DbgPrint("代码基址:      %p\n", pNtHeaders->OptionalHeader.BaseOfCode);DbgPrint("区块对齐:      %p\n", pNtHeaders->OptionalHeader.SectionAlignment);DbgPrint("文件块对齐:    %p\n", pNtHeaders->OptionalHeader.FileAlignment);DbgPrint("子系统:        %x\n", pNtHeaders->OptionalHeader.Subsystem);DbgPrint("区段数目:      %d\n", pNtHeaders->FileHeader.NumberOfSections);DbgPrint("时间日期标志:  %x\n", pNtHeaders->FileHeader.TimeDateStamp);DbgPrint("首部大小:      %x\n", pNtHeaders->OptionalHeader.SizeOfHeaders);DbgPrint("特征值:        %x\n", pNtHeaders->FileHeader.Characteristics);DbgPrint("校验和:        %x\n", pNtHeaders->OptionalHeader.CheckSum);DbgPrint("可选头部大小:  %x\n", pNtHeaders->FileHeader.SizeOfOptionalHeader);DbgPrint("RVA 数及大小:  %x\n", pNtHeaders->OptionalHeader.NumberOfRvaAndSizes);ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);ZwClose(hSection);ZwClose(hFile);Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}

运行如上这段代码,即可解析出ntdll.dll模块的核心内容,如下图所示;

接着来实现解析节表,PE文件中的所有节的属性定义都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构邮过来描述一个节,节表总被存放在紧接在PE文件头的地方,也即是从PE文件头开始偏移为00f8h的位置处,如下是节表头部的定义。

typedef struct _IMAGE_SECTION_HEADER {BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];union {DWORD   PhysicalAddress;DWORD   VirtualSize;           // 节区尺寸} Misc;DWORD   VirtualAddress;                // 节区RVADWORD   SizeOfRawData;                 // 在文件中对齐后的尺寸DWORD   PointerToRawData;              // 在文件中的偏移DWORD   PointerToRelocations;          // 在OBJ文件中使用DWORD   PointerToLinenumbers;WORD    NumberOfRelocations;WORD    NumberOfLinenumbers;DWORD   Characteristics;               // 节区属性字段
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

其中,Name是该节的名称,VirtualAddress是该节在内存中的虚拟地址,SizeOfRawData是该节在文件中的大小,PointerToRawData是该节在文件中的偏移地址,Characteristics描述了该节的属性,例如是否可读、可写、可执行等。

节表通常位于PE结构的文件头后面,它包含了多个节表项,每个节表项描述了一个节的信息,包括:

  • 节名称:每个节都有一个名称,例如代码节的名称为.text,数据节的名称为.data等;
  • 节大小:该节的大小,以字节为单位;
  • 节的虚拟地址:该节在内存中的虚拟地址;
  • 节的物理地址:该节在文件中的偏移地址;
  • 节的属性:例如该节是否可读、可写、可执行等。

总的来说,节表记录了PE文件中每个区域的详细信息,这些信息对于可执行文件或DLL的加载和运行都非常重要。

解析节表也很容易实现,首先通过pFileHeader->NumberOfSections获取到节数量,然后循环解析直到所有节输出完成,这段代码实现如下所示。

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint("hello lyshark \n");NTSTATUS status = STATUS_SUCCESS;HANDLE hFile = NULL;HANDLE hSection = NULL;PVOID pBaseAddress = NULL;UNICODE_STRING FileName = { 0 };// 初始化字符串RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");// 内存映射文件status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);if (!NT_SUCCESS(status)){return 0;}// 获取PE头数据集PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;DWORD NumberOfSectinsCount = 0;// 获取区块数量NumberOfSectinsCount = pFileHeader->NumberOfSections;DWORD64 *difA = NULL;   // 虚拟地址开头DWORD64 *difS = NULL;   // 相对偏移(用于遍历)difA = ExAllocatePool(NonPagedPool, NumberOfSectinsCount*sizeof(DWORD64));difS = ExAllocatePool(NonPagedPool, NumberOfSectinsCount*sizeof(DWORD64));DbgPrint("节区名称 相对偏移\t虚拟大小\tRaw数据指针\tRaw数据大小\t节区属性\n");for (DWORD temp = 0; temp<NumberOfSectinsCount; temp++, pSection++){DbgPrint("%10s\t 0x%x \t 0x%x \t 0x%x \t 0x%x \t 0x%x \n",pSection->Name, pSection->VirtualAddress, pSection->Misc.VirtualSize,pSection->PointerToRawData, pSection->SizeOfRawData, pSection->Characteristics);difA[temp] = pSection->VirtualAddress;difS[temp] = pSection->VirtualAddress - pSection->PointerToRawData;}ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);ZwClose(hSection);ZwClose(hFile);Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}

运行驱动程序,即可输出ntdll.dll模块的节表信息,如下图;

这篇关于9.3 Windows驱动开发:内核解析PE结构节表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/414585

相关文章

python常见环境管理工具超全解析

《python常见环境管理工具超全解析》在Python开发中,管理多个项目及其依赖项通常是一个挑战,下面:本文主要介绍python常见环境管理工具的相关资料,文中通过代码介绍的非常详细,需要的朋友... 目录1. conda2. pip3. uvuv 工具自动创建和管理环境的特点4. setup.py5.

全面解析HTML5中Checkbox标签

《全面解析HTML5中Checkbox标签》Checkbox是HTML5中非常重要的表单元素之一,通过合理使用其属性和样式自定义方法,可以为用户提供丰富多样的交互体验,这篇文章给大家介绍HTML5中C... 在html5中,Checkbox(复选框)是一种常用的表单元素,允许用户在一组选项中选择多个项目。本

Python实例题之pygame开发打飞机游戏实例代码

《Python实例题之pygame开发打飞机游戏实例代码》对于python的学习者,能够写出一个飞机大战的程序代码,是不是感觉到非常的开心,:本文主要介绍Python实例题之pygame开发打飞机... 目录题目pygame-aircraft-game使用 Pygame 开发的打飞机游戏脚本代码解释初始化部

基于Python实现一个Windows Tree命令工具

《基于Python实现一个WindowsTree命令工具》今天想要在Windows平台的CMD命令终端窗口中使用像Linux下的tree命令,打印一下目录结构层级树,然而还真有tree命令,但是发现... 目录引言实现代码使用说明可用选项示例用法功能特点添加到环境变量方法一:创建批处理文件并添加到PATH1

Python包管理工具核心指令uvx举例详细解析

《Python包管理工具核心指令uvx举例详细解析》:本文主要介绍Python包管理工具核心指令uvx的相关资料,uvx是uv工具链中用于临时运行Python命令行工具的高效执行器,依托Rust实... 目录一、uvx 的定位与核心功能二、uvx 的典型应用场景三、uvx 与传统工具对比四、uvx 的技术实

使用Python开发一个现代化屏幕取色器

《使用Python开发一个现代化屏幕取色器》在UI设计、网页开发等场景中,颜色拾取是高频需求,:本文主要介绍如何使用Python开发一个现代化屏幕取色器,有需要的小伙伴可以参考一下... 目录一、项目概述二、核心功能解析2.1 实时颜色追踪2.2 智能颜色显示三、效果展示四、实现步骤详解4.1 环境配置4.

SpringBoot排查和解决JSON解析错误(400 Bad Request)的方法

《SpringBoot排查和解决JSON解析错误(400BadRequest)的方法》在开发SpringBootRESTfulAPI时,客户端与服务端的数据交互通常使用JSON格式,然而,JSON... 目录问题背景1. 问题描述2. 错误分析解决方案1. 手动重新输入jsON2. 使用工具清理JSON3.

Windows的CMD窗口如何查看并杀死nginx进程

《Windows的CMD窗口如何查看并杀死nginx进程》:本文主要介绍Windows的CMD窗口如何查看并杀死nginx进程问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录Windows的CMD窗口查看并杀死nginx进程开启nginx查看nginx进程停止nginx服务

Redis过期删除机制与内存淘汰策略的解析指南

《Redis过期删除机制与内存淘汰策略的解析指南》在使用Redis构建缓存系统时,很多开发者只设置了EXPIRE但却忽略了背后Redis的过期删除机制与内存淘汰策略,下面小编就来和大家详细介绍一下... 目录1、简述2、Redis http://www.chinasem.cn的过期删除策略(Key Expir

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和