9.2 Windows驱动开发:内核解析PE结构导出表
在笔者的上一篇文章《内核特征码扫描PE代码段》
中LyShark
带大家通过封装好的LySharkToolsUtilKernelBase
函数实现了动态获取内核模块基址,并通过ntimage.h
头文件中提供的系列函数解析了指定内核模块的PE节表
参数,本章将继续延申这个话题,实现对PE文件导出表的解析任务,导出表无法动态获取,解析导出表则必须读入内核模块到内存才可继续解析,所以我们需要分两步走,首先读入内核磁盘文件到内存,然后再通过ntimage.h
中的系列函数解析即可。
PE结构(Portable Executable Structure)是Windows操作系统用于执行可执行文件和动态链接库(DLL)的标准格式。导出表(Export Table)是PE结构中的一个部分,它记录了一个DLL中所有可供外部调用的函数和变量。
导出表通常位于PE结构的数据目录中。它包含两个重要的表格:导出名称表格和导出地址表格。导出名称表格列出了DLL中所有导出函数和变量的名称,而导出地址表格列出了这些函数和变量的内存地址。
当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。导出函数在DLL文件中,导出信息被保存在导出表,导出表就是记载着动态链接库的一些导出信息。通过导出表,DLL文件可以向系统提供导出函数的名称、序号和入口地址等信息,以便Windows装载器能够通过这些信息来完成动态链接的整个过程。
导出函数存储在PE文件的导出表里,导出表的位置存放在PE文件头中的数据目录表中,与导出表对应的项目是数据目录中的首个IMAGE_DATA_DIRECTORY
结构,从这个结构的VirtualAddress
字段得到的就是导出表的RVA值,导出表同样可以使用函数名或序号这两种方法导出函数。
导出表的起始位置有一个IMAGE_EXPORT_DIRECTORY
结构,与导入表中有多个IMAGE_IMPORT_DESCRIPTOR
结构不同,导出表只有一个IMAGE_EXPORT_DIRECTORY
结构,该结构定义如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp; // 文件的产生时刻
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // 指向文件名的RVA
DWORD Base; // 导出函数的起始序号
DWORD NumberOfFunctions; // 导出函数总数
DWORD NumberOfNames; // 以名称导出函数的总数
DWORD AddressOfFunctions; // 导出函数地址表的RVA
DWORD AddressOfNames; // 函数名称地址表的RVA
DWORD AddressOfNameOrdinals; // 函数名序号表的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
其中,Name字段指向了该DLL的名称字符串,Base字段为该DLL的加载基地址,NumberOfFunctions
和NumberOfNames
分别表示导出函数和变量的数量,AddressOfFunctions、AddressOfNames
和AddressOfNameOrdinals
则是三个表格的地址。
总的来说,导出表是DLL中非常重要的一个部分,它提供了一种方便的方法,使其他程序可以调用DLL中的函数和变量。
上面的_IMAGE_EXPORT_DIRECTORY
结构如果总结成一张图,如下所示:
在上图中最左侧AddressOfNames
结构成员指向了一个数组,数组里保存着一组RVA,每个RVA指向一个字符串即导出的函数名,与这个函数名对应的是AddressOfNameOrdinals
中的结构成员,该对应项存储的正是函数的唯一编号并与AddressOfFunctions
结构成员相关联,形成了一个导出链式结构体。
获取导出函数地址时,先在AddressOfNames
中找到对应的名字MyFunc1
,该函数在AddressOfNames
中是第1项,然后从AddressOfNameOrdinals
中取出第1项的值这里是1,然后就可以通过导出函数的序号AddressOfFunctions[1]
取出函数的入口RVA,然后通过RVA加上模块基址便是第一个导出函数的地址,向后每次相加导出函数偏移即可依次遍历出所有的导出函数地址。
其解析过程与应用层基本保持一致,如果不懂应用层如何解析也可以去看我以前写过的《PE格式:手写PE结构解析工具》
里面具体详细的分析了解析流程。
首先使用InitializeObjectAttributes()
打开文件,打开后可获取到该文件的句柄,InitializeObjectAttributes
宏初始化一个OBJECT_ATTRIBUTES
结构体, 当一个例程打开对象时由此结构体指定目标对象的属性,此函数的微软定义如下;
VOID InitializeObjectAttributes(
[out] POBJECT_ATTRIBUTES p, // 权限
[in] PUNICODE_STRING n, // 文件名
[in] ULONG a, // 输出文件
[in] HANDLE r, // 权限
[in, optional] PSECURITY_DESCRIPTOR s // 0
);
当权限句柄被初始化后则即调用ZwOpenFile()
打开一个文件使用权限FILE_SHARE_READ
打开,打开文件函数微软定义如下;
NTSYSAPI NTSTATUS ZwOpenFile(
[out] PHANDLE FileHandle, // 返回打开文件的句柄
[in] ACCESS_MASK DesiredAccess, // 打开的权限,一般设为GENERIC_ALL。
[in] POBJECT_ATTRIBUTES ObjectAttributes, // OBJECT_ATTRIBUTES结构
[out] PIO_STATUS_BLOCK IoStatusBlock, // 指向一个结构体的指针。该结构体指明打开文件的状态。
[in] ULONG ShareAccess, // 共享的权限。可以是FILE_SHARE_READ 或者 FILE_SHARE_WRITE。
[in] ULONG OpenOptions // 打开选项,一般设为 FILE_SYNCHRONOUS_IO_NONALERT。
);
接着文件被打开后,我们还需要调用ZwCreateSection()
该函数的作用是创建一个Section
节对象,并以PE结构中的SectionALignment
大小对齐映射文件,其微软定义如下;
NTSYSAPI NTSTATUS ZwCreateSection(
[out] PHANDLE SectionHandle, // 指向 HANDLE 变量的指针,该变量接收 section 对象的句柄。
[in] ACCESS_MASK DesiredAccess, // 指定一个 ACCESS_MASK 值,该值确定对 对象的请求访问权限。
[in, optional] POBJECT_ATTRIBUTES ObjectAttributes, // 指向 OBJECT_ATTRIBUTES 结构的指针,该结构指定对象名称和其他属性。
[in, optional] PLARGE_INTEGER MaximumSize, // 指定节的最大大小(以字节为单位)。
[in] ULONG SectionPageProtection, // 指定要在 节中的每个页面上放置的保护。
[in] ULONG AllocationAttributes, // 指定确定节的分配属性的SEC_XXX 标志的位掩码。
[in, optional] HANDLE FileHandle // (可选)指定打开的文件对象的句柄。
);
最后读取导出表就要将一个磁盘中的文件映射到内存中,内存映射核心文件时ZwMapViewOfSection()
该系列函数在应用层名叫MapViewOfSection()
只是一个是内核层一个应用层,这两个函数参数传递基本一致,以ZwMapViewOfSection
为例,其微软定义如下;
NTSYSAPI NTSTATUS ZwMapViewOfSection(
[in] HANDLE SectionHandle, // 接收一个节对象
[in] HANDLE ProcessHandle, // 进程句柄,此处使用NtCurrentProcess()获取自身句柄
[in, out] PVOID *BaseAddress, // 指定填充地址
[in] ULONG_PTR ZeroBits, // 0
[in] SIZE_T CommitSize, // 每次提交大小 1024
[in, out, optional] PLARGE_INTEGER SectionOffset, // 0
[in, out] PSIZE_T ViewSize, // 浏览大小
[in] SECTION_INHERIT InheritDisposition, // ViewShare
[in] ULONG AllocationType, // 分配类型 MEM_TOP_DOWN
[in] ULONG Win32Protect // 权限 PAGE_READWRITE(读写)
);
将如上函数研究明白那么代码就变得很容易了,首先InitializeObjectAttributes
设置文件权限与属性,然后调用ZwOpenFile
打开文件,接着调用ZwCreateSection
创建节对象,最后调用ZwMapViewOfSection
将磁盘文件映射到内存,这段代码实现起来很简单,完整案例如下所示;
#include <ntifs.h>
#include <ntimage.h>
#include <ntstrsafe.h>
// 内存映射文件
NTSTATUS KernelMapFile(UNICODE_STRING FileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress)
{
NTSTATUS status = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
OBJECT_ATTRIBUTES objectAttr = { 0 };
IO_STATUS_BLOCK iosb = { 0 };
PVOID pBaseAddress = NULL;
SIZE_T viewSize = 0;
// 设置文件权限
InitializeObjectAttributes(&objectAttr, &FileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// 打开文件
status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttr, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
if (!NT_SUCCESS(status))
{
return status;
}
// 创建节对象
status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile);
if (!NT_SUCCESS(status))
{
ZwClose(hFile);
return status;
}
// 映射到内存
status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddress, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE);
if (!NT_SUCCESS(status))
{
ZwClose(hSection);
ZwClose(hFile);
return status;
}
// 返回数据
*phFile = hFile;
*phSection = hSection;
*ppBaseAddress = pBaseAddress;
return status;
}
VOID UnDriver(PDRIVER_OBJECT driver)
{
DbgPrint("驱动卸载 \n");
}
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\\ntoskrnl.exe");
// 内存映射文件
status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
if (NT_SUCCESS(status))
{
DbgPrint("读取内存地址 = %p \n", pBaseAddress);
}
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
运行这段程序,即可读取到ntoskrnl.exe
磁盘所在文件的内存映像基地址,效果如下所示;
如上代码读入了ntoskrnl.exe
文件,接下来就是解析导出表,首先将pBaseAddress
解析为PIMAGE_DOS_HEADER
获取DOS头,并在DOS头中寻找PIMAGE_NT_HEADERS
头,接着在NTHeader
头中得到数据目录表,此处指向的就是导出表PIMAGE_EXPORT_DIRECTORY
通过pExportTable->NumberOfNames
可得到导出表的数量,通过(PUCHAR)pDosHeader + pExportTable->AddressOfNames
得到导出表的地址,依次循环读取即可得到完整的导出表。
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 };
LONG FunctionIndex = 0;
// 初始化字符串
RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntoskrnl.exe");
// 内存映射文件
status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
if (NT_SUCCESS(status))
{
DbgPrint("[LyShark] 读取内存地址 = %p \n", pBaseAddress);
}
// Dos 头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
// NT 头
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
// 导出表
PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
// 有名称的导出函数个数
ULONG ulNumberOfNames = pExportTable->NumberOfNames;
DbgPrint("[lyshark] 导出函数个数: %d \n\n", ulNumberOfNames);
// 导出函数名称地址表
PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
PCHAR lpName = NULL;
// 开始遍历导出表(输出ulNumberOfNames导出函数)
for (ULONG i = 0; i < ulNumberOfNames; i++)
{
lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
// 获取导出函数地址
USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);
// 获取SSDT函数Index
FunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4);
DbgPrint("序号: [ %d ] | Hint: %d | 地址: %p | 函数名: %s \n", i, uHint, lpFuncAddr, lpName);
}
// 释放指针
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
代码运行后即可获取到当前ntoskrnl.exe
程序中的所有导出函数,输出效果如下所示;
- SSDT表通常会解析
\\??\\C:\\Windows\\System32\\ntoskrnl.exe
- SSSDT表通常会解析
\\??\\C:\\Windows\\System32\\win32k.sys
根据上方的函数流程将其封装为GetAddressFromFunction()
用户传入DllFileName
指定的PE文件,以及需要读取的pszFunctionName
函数名,即可输出该函数的导出地址。
// 寻找指定函数得到内存地址
ULONG64 GetAddressFromFunction(UNICODE_STRING DllFileName, PCHAR pszFunctionName)
{
NTSTATUS status = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
PVOID pBaseAddress = NULL;
// 内存映射文件
status = KernelMapFile(DllFileName, &hFile, &hSection, &pBaseAddress);
if (!NT_SUCCESS(status))
{
return 0;
}
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
ULONG ulNumberOfNames = pExportTable->NumberOfNames;
PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
PCHAR lpName = NULL;
for (ULONG i = 0; i < ulNumberOfNames; i++)
{
lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);
if (_strnicmp(pszFunctionName, lpName, strlen(pszFunctionName)) == 0)
{
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
return (ULONG64)lpFuncAddr;
}
}
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
return 0;
}
VOID UnDriver(PDRIVER_OBJECT driver)
{
DbgPrint("驱动卸载 \n");
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
DbgPrint("hello lyshark \n");
UNICODE_STRING FileName = { 0 };
ULONG64 FunctionAddress = 0;
// 初始化字符串
RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
// 取函数内存地址
FunctionAddress = GetAddressFromFunction(FileName, "ZwQueryVirtualMemory");
DbgPrint("ZwQueryVirtualMemory内存地址 = %p \n", FunctionAddress);
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
如上程序所示,当运行后即可获取到ntdll.dll
模块内ZwQueryVirtualMemory
的导出地址,输出效果如下所示;
9.2 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控制,已保持数据的完整性. 我 ...
随机推荐
- 使用 DPO 微调 Llama 2
简介 基于人类反馈的强化学习 (Reinforcement Learning from Human Feedback,RLHF) 事实上已成为 GPT-4 或 Claude 等 LLM 训练的最后一步 ...
- sort与uniq
1 sort 将文本文件内容加以排序 语法: sort [-bcdfimMnr][-o<输出文件>][-t<分隔字符>][+<起始栏位>-<结束栏位>] ...
- ASP.NET Web API Demo OwinSelfHost 自宿主 Swagger Swashbuckle 在线文档
新建Web API工程 选Empty,勾选Web API,不要选择Web API,那样会把MVC勾上,这里不需要MVC Web API工程属性 XML文件用于生成在线文档 新建Windows服务作为W ...
- 库函数 | C++17 std::filesystem文件系统 用法指北
本文将针对常用的场景,对 std::filesystem 的使用逐一进行验证: 判断文件夹是否存在 创建单层目录 逐级创建多层目录 创建多级目录 当前文件路径 创建文件"from.dat&q ...
- IDEA | 使用Maven创建Web项目并配置Tomcat
学习这种方式的原因是以后Tomcat中运行的绝大多数都是Web项目,而使用Maven工具能更加简单快捷的把Web项目给创建出来,所以Maven的Web项目具体如何来构建呢? 在真正创建Maven We ...
- Codeforces Round #722 (Div. 2) A~D题解
补题链接:Here 1529A. Eshag Loves Big Arrays [题意描述] 给定一个长度为 \(n\) 的正整数数组 \(a\) ,现在可执行若干次操作(可为 \(0\)) 具体操作 ...
- MySQL 的 crash-safe 原理解析
本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/5i9wmJs4_Er7RaYfNnETyA作者:xieweipeng MySQL作为当下最流行 ...
- 3D编程模式:介绍设计原则
大家好~本文介绍6个设计原则的定义 系列文章详见: 3D编程模式:开篇 目录 单一职责原则(SRP) 依赖倒置原则(DIP) 接口隔离原则(ISP) 迪米特法则(LoD) 合成复用原则(CARP) 开 ...
- Vue3使用vue-video-player组件
1.安装依赖(亲测5.0.1版本可用,最新版本会找不到'vue-video-player/src/custom-theme.css'这个样式) yarn add vue-video-player@5 ...
- php开发之文件下载的实现
前言 php是网络安全学习里必不可少的一环,简单理解php的开发环节能更好的帮助我们去学习php以及其他语言的web漏洞原理 正文 在正常的开发中,文件下载的功能是必不可少,比如我们在论坛看到好看图片 ...