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 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);
}
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_HEADER
和IMAGE_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文件的开头相关数据,读取这些结构也非常简单代码如下所示。
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; // 节区RVA
DWORD 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结构节表的更多相关文章
- Windows驱动开发-内核常用内存函数
搞内存常用函数 C语言 内核 malloc ExAllocatePool memset RtlFillMemory memcpy RtlMoveMemory free ExFreePool
- [Windows驱动开发](一)序言
笔者学习驱动编程是从两本书入门的.它们分别是<寒江独钓——内核安全编程>和<Windows驱动开发技术详解>.两本书分别从不同的角度介绍了驱动程序的制作方法. 在我理解,驱动程 ...
- windows驱动开发推荐书籍
[作者] 猪头三 个人网站 :http://www.x86asm.com/ [序言] 很多人都对驱动开发有兴趣,但往往找不到正确的学习方式.当然这跟驱动开发的本土化资料少有关系.大多学的驱动开发资料都 ...
- C++第三十八篇 -- 研究一下Windows驱动开发(二)--WDM式驱动的加载
基于Windows驱动开发技术详解这本书 一.简单的INF文件剖析 INF文件是一个文本文件,由若干个节(Section)组成.每个节的名称用一个方括号指示,紧接着方括号后面的就是节内容.每一行就是一 ...
- Windows驱动开发(中间层)
Windows驱动开发 一.前言 依据<Windows内核安全与驱动开发>及MSDN等网络质料进行学习开发. 二.初步环境 1.下载安装WDK7.1.0(WinDDK\7600.16385 ...
- windows 驱动开发入门——驱动中的数据结构
最近在学习驱动编程方面的内容,在这将自己的一些心得分享出来,供大家参考,与大家共同进步,本人学习驱动主要是通过两本书--<独钓寒江 windows安全编程> 和 <windows驱动 ...
- Windows驱动——读书笔记《Windows驱动开发技术详解》
=================================版权声明================================= 版权声明:原创文章 谢绝转载 请通过右侧公告中的“联系邮 ...
- Windows驱动开发-IRP的完成例程
<Windows驱动开发技术详解 >331页, 在将IRP发送给底层驱动或其他驱动之前,可以对IRP设置一个完成例程,一旦底层驱动将IRP完成后,IRP完成例程立刻被处罚,通过设置完成例程 ...
- C++第三十三篇 -- 研究一下Windows驱动开发(一)内部构造介绍
因为工作原因,需要做一些与网卡有关的测试,其中涉及到了驱动这一块的知识,虽然程序可以运行,但是不搞清楚,心里总是不安,觉得没理解清楚.因此想看一下驱动开发.查了很多资料,看到有人推荐Windows驱动 ...
- Windows 驱动开发 - 5
上篇<Windows 驱动开发 - 4>我们已经完毕了硬件准备. 可是我们还没有详细的数据操作,比如接收读写操作. 在WDF中进行此类操作前须要进行设备的IO控制,已保持数据的完整性. 我 ...
随机推荐
- 聊聊时下火热的 AIGC 与 Web3
近期,AI 已经开始影响到了音乐行业,在 B 站搜索"AI 孙燕姿",从流行歌曲到摇滚,从周杰伦到王力宏,没有 AI 孙燕姿驾驭不了的歌曲. 有用户评论:"我感觉 AI ...
- Go--发起HTTP请求
一.HTTP请求 根据 HTTP 标准,HTTP 请求可以使用多种请求方法.在日常开发中大多数会用到 5 种请求方法: GET.POST.PUT.PATCH 和 DELETE 方法 描述 GET 请求 ...
- C++ lambda 内 std::move 失效问题的思考
最近在学习 C++ Move 时,有看到这样一个代码需求:在 lambda 中,将一个捕获参数 move 给另外一个变量. 看似一个很简单常规的操作,然而这个 move 动作却没有生效. 具体代码如下 ...
- token原理分析
- Docker 魔法解密:探索 UnionFS 与 OverlayFS
本文主要介绍了 Docker 的另一个核心技术:Union File System.主要包括对 overlayfs 的演示,以及分析 docker 是如何借助 ufs 实现容器 rootfs 的. 如 ...
- P1765
和那道题一样,这次用的getchar,结果对了可是洛谷评测WA了,换成scanf单个字符,结果还是WA了,换成直接getline读入整个字符串就对了. 可见读入单个字符的方式有可能出现各种小错,尤其是 ...
- freeswitch的mod_xml_curl模块动态获取configuration
概述 freeswitch是一款简单好用的VOIP开源软交换平台. mod_xml_curl模块支持从web服务获取xml配置,本文介绍如何动态获取acl配置. 环境 centos:CentOS r ...
- docker 安装 ElasticSearch 和 Kibana 及ik 中文分词器
本文为博主原创,未经允许不得转载: 1. 使用 docker 下载 elasticsearch 7.6.1 docker pull elasticsearch:7.6.1 2. 启动 elastic ...
- Jstack 查看线程状态及定位占用 cpu 较高的 java 线程
本文为博主原创,未经允许不得转载: 1. Jstack 用来查看 java 指定进程所包含的 java 线程状态: "arthas-NettyHttpTelnetBootstrap-3 ...
- Laravel : 模糊查询 where orWhere
Banner::where('title', 'like', "%{$keyword}%")->orWhere('introduce', 'like', "%{$k ...