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

这篇具有很好参考价值的文章主要介绍了驱动开发:内核解析PE结构导出表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

当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;

上面的_IMAGE_EXPORT_DIRECTORY 结构如果总结成一张图,如下所示:

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

在上图中最左侧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将磁盘文件映射到内存,这段代码实现起来很简单,完整案例如下所示;

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

#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.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\\ntoskrnl.exe");

	// 内存映射文件
	status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
	if (NT_SUCCESS(status))
	{
		DbgPrint("读取内存地址 = %p \n", pBaseAddress);
	}

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

运行这段程序,即可读取到ntoskrnl.exe磁盘所在文件的内存映像基地址,效果如下所示;

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

如上代码读入了ntoskrnl.exe文件,接下来就是解析导出表,首先将pBaseAddress解析为PIMAGE_DOS_HEADER获取DOS头,并在DOS头中寻找PIMAGE_NT_HEADERS头,接着在NTHeader头中得到数据目录表,此处指向的就是导出表PIMAGE_EXPORT_DIRECTORY通过pExportTable->NumberOfNames可得到导出表的数量,通过(PUCHAR)pDosHeader + pExportTable->AddressOfNames得到导出表的地址,依次循环读取即可得到完整的导出表。

// 署名权
// 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 };
	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.com] 导出函数个数: %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

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

根据上方的函数流程将其封装为GetAddressFromFunction()用户传入DllFileName指定的PE文件,以及需要读取的pszFunctionName函数名,即可输出该函数的导出地址。

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

// 寻找指定函数得到内存地址
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.com \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的导出地址,输出效果如下所示;

驱动开发:内核解析PE结构导出表文章来源地址https://www.toymoban.com/news/detail-465432.html

到了这里,关于驱动开发:内核解析PE结构导出表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

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

    当今操作系统普遍采用64位架构,CPU最大寻址能力虽然达到了64位,但其实仅仅只是用到了48位进行寻址,其内存管理采用了 9-9-9-9-12 的分页模式, 9-9-9-9-12 分页表示物理地址拥有四级页表,微软将这四级依次命名为PXE、PPE、PDE、PTE这四项。 关于内存管理和分页模式,不同的操

    2024年02月06日
    浏览(51)
  • 2.4 PE结构:节表详细解析

    节表(Section Table)是Windows PE/COFF格式的可执行文件中一个非常重要的数据结构,它记录了各个代码段、数据段、资源段、重定向表等在文件中的位置和大小信息,是操作系统加载文件时根据节表来进行各个段的映射和初始化的重要依据。节表中的每个记录则被称为 IMAGE_SECT

    2024年02月10日
    浏览(36)
  • 2.8 PE结构:资源表详细解析

    在Windows PE中,资源是指可执行文件中存放的一些固定不变的数据集合,例如图标、对话框、字符串、位图、版本信息等。PE文件中每个资源都会被分配对应的唯一资源ID,以便在运行时能够方便地查找和调用它们。PE文件中的资源都被组织成一个树形结构,其中最顶层为根节点

    2024年02月09日
    浏览(27)
  • 2.2 PE结构:文件头详细解析

    PE结构是 Windows 系统下最常用的可执行文件格式,理解PE文件格式不仅可以理解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,DOS头是PE文件开头的一个固定长度的结构体,这个结构体的大小为64字节(0x40)。DOS头包含了很多有用的信息,该信

    2024年02月10日
    浏览(30)
  • 2.7 PE结构:重定位表详细解析

    重定位表(Relocation Table)是Windows PE可执行文件中的一部分,主要记录了与地址相关的信息,它在程序加载和运行时被用来修改程序代码中的地址的值,因为程序在不同的内存地址中加载时,程序中使用到的地址也会受到影响,因此需要重定位表这个数据结构来完成这些地址

    2024年02月09日
    浏览(31)
  • 【免杀前置课——PE文件结构】十八、数据目录表及其内容详解——数据目录表(导出表、导入表、IAT表、TLS表)详解;如何在程序在被调试之前反击?TLS反调试(附代码)

    数据目录表:可选PE头最后一个成员,就是数据目录.一共有16个 分别是:导出表、导入表、资源表、异常信息表、安全证书表、重定位表、调试信息表、版权所以表、全局指针表 TLS表、加载配置表、绑定导入表、IAT表、延迟导入表、COM信息表 最后一个保留未使用,默认为0。

    2024年01月15日
    浏览(39)
  • Linux驱动开发——内核模块

    目录 内核模块的由来 第一个内核模块程序  内核模块工具  将多个源文件编译生成一个内核模块  内核模块参数 内核模块依赖 关于内核模块的进一步讨论  习题 最近一直在玩那些其它的技术,眼看快暑假了,我决定夯实一下我的驱动方面的技能,迎接我的实习,找了一本

    2024年02月04日
    浏览(75)
  • Linux内核驱动开发(一)

    linux操作系统历史 开发模式 git 分布式管理 git clone 获取 git push 提交 git pull 更新 邮件组 mailing list patch 内核代码组成 Makfile arch 体系系统架构相关 block 块设备 crypto 加密算法 drivers 驱动(85%) atm 通信 bluetooth 蓝牙 firmware:外设 fs 文件系统 include 头文件 init 启动代码 ipc 进程通

    2023年04月11日
    浏览(40)
  • 【usb】linux内核USB键盘驱动解析--普通键值上报及转化

    建议阅读前置文章【usb】linux内核USB键盘驱动解析–特殊键值上报及转化 以Linux5.10内核中USB键盘驱动为例进行解析:https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.tar.gz 文件路径:linux-5.10/drivers/hid/usbhid/usbkbd.c 本次我们主要分析第120~139行的这个for循环。 for循环变量i范围是

    2023年04月08日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包