在笔者的上一篇文章《驱动开发:内核特征码扫描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 结构如果总结成一张图,如下所示:

在上图中最左侧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磁盘所在文件的内存映像基地址,效果如下所示;

如上代码读入了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

根据上方的函数流程将其封装为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结构导出表的更多相关文章

  1. 【驱动】网卡驱动·linux内核网络分层结构

    Preface   Linux内核对网络驱动程序使用统一的接口,并且对于网络设备采用面向对象的思想设计. Linux内核采用分层结构处理网络数据包.分层结构与网络协议的结构匹配,既能简化数据包处理流程 ...

  2. Windows驱动开发-内核常用内存函数

    搞内存常用函数 C语言 内核 malloc ExAllocatePool memset RtlFillMemory memcpy RtlMoveMemory free ExFreePool

  3. [PE结构]导出表结构浅析

    导出函数的总数-->以导出函数序号最大的减最小的+1,但导出函数序号是可自定义的,所以NumbersOfFunctions是不准确的 1.根据函数名称找,函数名称表->对应索引函数序号表中 ...

  4. 【PE结构】由浅入深PE基础学习-菜鸟手动查询导出表、相对虚拟地址(RVA)与文件偏移地址转换(FOA)

    0 前言 此篇文章想写如何通过工具手查导出表.PE文件代码编程过程中的原理.文笔不是很好,内容也是查阅了很多的资料后整合出来的.希望借此加深对PE文件格式的理解,也希望可以对看雪论坛有所贡献.因为了解 ...

  5. 修改记事本PE结构弹计算器Shellcode

    目录 修改记事本PE结构弹计算器Shellcode 0x00 前言 0x01 添加新节 修改节数量 节表位置 添加新节表信息 0x02 添加弹计算器Shellcode 修改代码 0x03 修改入口点 ...

  6. 驱动开发:内核特征码扫描PE代码段

    在笔者上一篇文章<驱动开发:内核特征码搜索函数封装>中为了定位特征的方便我们封装实现了一个可以传入数组实现的SearchSpecialCode定位函数,该定位函数其实还不能算的上简单,本章 ...

  7. 驱动开发:内核遍历进程VAD结构体

    在上一篇文章<驱动开发:内核中实现Dump进程转储>中我们实现了ARK工具的转存功能,本篇文章继续以内存为出发点介绍VAD结构,该结构的全程是Virtual Address Descrip ...

  8. Linux 网络设备驱动开发(一) —— linux内核网络分层结构

    Preface Linux内核对网络驱动程序使用统一的接口,并且对于网络设备采用面向对象的思想设计. Linux内核采用分层结构处理网络数据包.分层结构与网络协议的结构匹配,既能简化数据包处理流程,又 ...

  9. Linux驱动开发必看详解神秘内核(完全转载)

    Linux驱动开发必看详解神秘内核 完全转载-链接:http://blog.chinaunix.net/uid-21356596-id-1827434.html   IT168 技术文档]在开始步入L ...

  10. xenomai内核解析之xenomai的组成结构

    @ 目录 一.xenomai 3 二.xenomai3 结构 这是第二篇笔记. 一.xenomai 3 从xenomai3开始支持两种方式构建linux实时系统,分别是cobalt 和 mercury ...

随机推荐

  1. CF1037H Security题解

    根据字典序的定义,位置大的大于长度长的,长度长的大于长度短的. 所以我们贪心,先追求长度长的,再追求后面的位置大的,再追求前面的位置大的. 我们要一个能遍历子串的结构,就选 SAM 得了. 还有个限制 ...

  2. Linux基础知识归纳

    1.Linux:Linux is not Unix.主要用于企业的服务器端.Windows不开源(系统价格大概2000左右,安装软件也特别贵,例如Offers就6000左右等).基于内核的操作系统(r ...

  3. 如何单机部署多个 MySQL 8.0 实例 ?

    在服务器资源有限的情况下,可利用该方案快速搭建各类 mysql 架构方案.各 MySQL 实例共享一个 mysqld 主程序,但各实例数据目录是独立的,存放在不同的文件夹中:好了.废话不多说,直接上干 ...

  4. flutter系列之:在flutter中使用媒体播放器

    目录 简介 使用前的准备工作 在flutter中使用video_player 总结 简介 现在的app功能越来越强大,除了基本的图文之外,还需要各种各样的其他的功能,比如视频,和直播. 直播可能会比较 ...

  5. go微服务框架kratos学习笔记二(kratos demo 结构)

    目录 api cmd configs dao di model server service 上篇文章go微服务框架kratos学习笔记一(kratos demo)跑了kratos demo 本章来看 ...

  6. Python 3.11.官方文档

    索引 模块 | Python » English Spanish French Japanese Korean Brazilian Portuguese Simplified Chinese Trad ...

  7. LLaMA:开放和高效的基础语言模型

    LLaMA:开放和高效的基础语言模型 论文:https://arxiv.org/pdf/2302.13971.pdf 代码:https://github.com/facebookresearch/ll ...

  8. 四月十五号java基础知识

    1.今天下午做了一个题感受很深,自己做题没有思路或者有点思路死磕也没有搞清楚,看起来很简单的问题,在我手里很难 做咯许久还是室友帮忙解决的,后面重新打一遍还是出问题,找他解决的,问了问他我自己的问题, ...

  9. Java:如何加密或解密PDF文档?

    在工作中,我们会将重要的文档进行加密,并且设置用户的访问权限,其他外部人员均无法打开,只有获取该权限的用户才有资格打开文档.此外,限制用户的使用权限,极大程度上阻止了那些有意要篡改.拷贝其中内容的人, ...

  10. Ubuntu20.04 Docker搭建远程xfce桌面以及ssh教程

    简介:本文主要介绍ubuntu20.04容器中搭建xfce远程桌面.C++.Go环境.容器内docker操作配置. 一.创建容器1.创建容器 docker pull ubuntu:20.04docke ...