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

驱动,开发,内核,解析,pe,结构 · 浏览次数 : 148

小编点评

**代码解析** **1. 获取文件头** * `pDosHeader` 是 `PIMAGE_DOS_HEADER` 的指针 * `pNtHeaders` 是 `PIMAGE_NT_HEADERS` 的指针 * `pSection` 是 `PIMAGE_SECTION_HEADER` 的指针 **2. 获取区块数量** * `NumberOfSectinsCount` 是 `pSection` 的 `NumberOfRelocations` 的值 **3. 获取虚拟地址和数据大小** * 虚拟地址 `difA` 的第一个元素是 `pSection->VirtualAddress` * 数据大小 `difS` 的第一个元素是 `pSection->SizeOfRawData` **4. 获取节表信息** * `pSection->Characteristics` 存储节区属性 * `pSection->PointerToLinenumbers` 存储线性地址 * `pSection->PointerToRelocations` 存储相对地址 * `pSection->PointerToLinenumbers` 存储线性地址 **5. 输出节表信息** * 使用 `printf` 输出节区名称、虚拟地址、数据大小、属性等信息 **6. 生成节表** * 创建 `PIMAGE_FILE_HEADER` 的指针 * 设置 `FileHeader->NumberOfSections` 的值 * 初始化 `FileHeader->FileData` 的指针 * 在每个字节中写入节区名称、虚拟地址、数据大小、属性等信息 * 最后将 `FileData` 的指针写入 `PIMAGE_FILE_HEADER` 的 `FileData` 中 **7. 导出节表** * 创建 `PIMAGE_NT_HEADERS` 的指针 * 设置 `FileHeader->NumberOfSections` 的值 * 初始化 `FileHeader->FileData` 的指针 * 在每个字节中写入节区名称、虚拟地址、数据大小、属性等信息 * 最后将 `FileData` 的指针写入 `PIMAGE_NT_HEADERS` 的 `FileData` 中

正文

在笔者上一篇文章《驱动开发:内核解析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结构详细定义,如下所示:

typedef struct _IMAGE_DOS_HEADER { 
    WORD   e_magic;                     // DOS的头部
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   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);
}

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;          // 程序执行入口RVA
    DWORD   BaseOfCode;                   // 代码节的起始RVA
    DWORD   BaseOfData;                   // 数据节的起始RVA
    DWORD   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;      // 数据起始RVA
    DWORD   Size;                // 数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

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

// 署名权
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: me@lyshark.com

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
	DbgPrint("hello lyshark.com \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;                // 节区RVA
    DWORD   SizeOfRawData;                 // 在文件中对齐后的尺寸
    DWORD   PointerToRawData;              // 在文件中的偏移
    DWORD   PointerToRelocations;          // 在OBJ文件中使用
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;               // 节区属性字段
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

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

// 署名权
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: me@lyshark.com

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
	DbgPrint("hello lyshark.com \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模块的节表信息,如下图;

与驱动开发:内核解析PE结构节表相似的内容:

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

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

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

在笔者的上一篇文章`《驱动开发:内核特征码扫描PE代码段》`中`LyShark`带大家通过封装好的`LySharkToolsUtilKernelBase`函数实现了动态获取内核模块基址,并通过`ntimage.h`头文件中提供的系列函数解析了指定内核模块的`PE节表`参数,本章将继续延申这个话题,实现对PE文件导出表的解析任务,导出表无法动态获取,解析导出表则必须读入内核模块到内存才可继续解析,所

驱动开发:内核实现SSDT挂钩与摘钩

在前面的文章`《驱动开发:内核解析PE结构导出表》`中我们封装了两个函数`KernelMapFile()`函数可用来读取内核文件,`GetAddressFromFunction()`函数可用来在导出表中寻找指定函数的导出地址,本章将以此为基础实现对特定`SSDT`函数的`Hook`挂钩操作,与`《驱动开发:内核层InlineHook挂钩函数》`所使用的挂钩技术基本一致,不同点是前者使用了`CR3`

驱动开发:内核PE结构VA与FOA转换

本章将继续探索内核中解析PE文件的相关内容,PE文件中FOA与VA,RVA之间的转换也是很重要的,所谓的FOA是文件中的地址,VA则是内存装入后的虚拟地址,RVA是内存基址与当前地址的相对偏移,本章还是需要用到`《驱动开发:内核解析PE结构导出表》`中所封装的`KernelMapFile()`映射函数,在映射后对其PE格式进行相应的解析,并实现转换函数。

详解驱动开发中内核PE结构VA与FOA转换

摘要:本文将探索内核中解析PE文件的相关内容。 本文分享自华为云社区《驱动开发:内核PE结构VA与FOA转换》,作者: LyShark 。 本章将探索内核中解析PE文件的相关内容,PE文件中FOA与VA、RVA之间的转换也是很重要的,所谓的FOA是文件中的地址,VA则是内存装入后的虚拟地址,RVA是

驱动开发:PE导出函数与RVA转换

在笔者上篇文章`《驱动开发:内核扫描SSDT挂钩状态》`中简单介绍了如何扫描被挂钩的SSDT函数,并简单介绍了如何解析导出表,本章将继续延申PE导出表的解析,实现一系列灵活的解析如通过传入函数名解析出函数的RVA偏移,ID索引,Index下标等参数,并将其封装为可直接使用的函数,以在后期需要时可以被直接引用,同样为了节约篇幅本章中的`LoadKernelFile()`内存映射函数如需要使用请去前一

驱动开发:内核解析内存四级页表

关于内存管理和分页模式,不同的操作系统和体系结构可能会有略微不同的实现方式。9-9-9-9-12的分页模式是一种常见的分页方案,其中物理地址被分成四级页表:PXE(Page Directory Pointer Table Entry)、PPE(Page Directory Entry)、PDE(Page Table Entry)和PTE(Page Table Entry)。这种分页模式可以支持大量

驱动开发:内核枚举Registry注册表回调

在笔者上一篇文章`《驱动开发:内核枚举LoadImage映像回调》`中`LyShark`教大家实现了枚举系统回调中的`LoadImage`通知消息,本章将实现对`Registry`注册表通知消息的枚举,与`LoadImage`消息不同`Registry`消息不需要解密只要找到`CallbackListHead`消息回调链表头并解析为`_CM_NOTIFY_ENTRY`结构即可实现枚举。

驱动开发:内核解锁与强删文件

在某些时候我们的系统中会出现一些无法被正常删除的文件,如果想要强制删除则需要在驱动层面对其进行解锁后才可删掉,而所谓的解锁其实就是释放掉文件描述符(句柄表)占用,文件解锁的核心原理是通过调用`ObSetHandleAttributes`函数将特定句柄设置为可关闭状态,然后在调用`ZwClose`将其文件关闭,强制删除则是通过`ObReferenceObjectByHandle`在对象上提供相应的权

驱动开发:内核枚举LoadImage映像回调

在笔者之前的文章`《驱动开发:内核特征码搜索函数封装》`中我们封装实现了特征码定位功能,本章将继续使用该功能,本次我们需要枚举内核`LoadImage`映像回调,在Win64环境下我们可以设置一个`LoadImage`映像加载通告回调,当有新驱动或者DLL被加载时,回调函数就会被调用从而执行我们自己的回调例程,映像回调也存储在数组里,枚举时从数组中读取值之后,需要进行位运算解密得到地址。