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

打印 上一主题 下一主题

主题 871|帖子 871|积分 2613

在笔者的上一篇文章《驱动开发:内核特征码扫描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结构,该结构定义如下:
  1. typedef struct _IMAGE_EXPORT_DIRECTORY {
  2.     DWORD   Characteristics;
  3.     DWORD   TimeDateStamp;        // 文件的产生时刻
  4.     WORD    MajorVersion;
  5.     WORD    MinorVersion;
  6.     DWORD   Name;                  // 指向文件名的RVA
  7.     DWORD   Base;                  // 导出函数的起始序号
  8.     DWORD   NumberOfFunctions;     // 导出函数总数
  9.     DWORD   NumberOfNames;         // 以名称导出函数的总数
  10.     DWORD   AddressOfFunctions;    // 导出函数地址表的RVA
  11.     DWORD   AddressOfNames;        // 函数名称地址表的RVA
  12.     DWORD   AddressOfNameOrdinals; // 函数名序号表的RVA
  13. } 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结构体, 当一个例程打开对象时由此结构体指定目标对象的属性,此函数的微软定义如下;
  1. VOID InitializeObjectAttributes(
  2.   [out]          POBJECT_ATTRIBUTES   p,      // 权限
  3.   [in]           PUNICODE_STRING      n,      // 文件名
  4.   [in]           ULONG                a,      // 输出文件
  5.   [in]           HANDLE               r,      // 权限
  6.   [in, optional] PSECURITY_DESCRIPTOR s       // 0
  7. );
复制代码
当权限句柄被初始化后则即调用ZwOpenFile()打开一个文件使用权限FILE_SHARE_READ打开,打开文件函数微软定义如下;
  1. NTSYSAPI NTSTATUS ZwOpenFile(
  2.   [out] PHANDLE            FileHandle,         // 返回打开文件的句柄
  3.   [in]  ACCESS_MASK        DesiredAccess,      // 打开的权限,一般设为GENERIC_ALL。
  4.   [in]  POBJECT_ATTRIBUTES ObjectAttributes,   // OBJECT_ATTRIBUTES结构
  5.   [out] PIO_STATUS_BLOCK   IoStatusBlock,      // 指向一个结构体的指针。该结构体指明打开文件的状态。
  6.   [in]  ULONG              ShareAccess,        // 共享的权限。可以是FILE_SHARE_READ 或者 FILE_SHARE_WRITE。
  7.   [in]  ULONG              OpenOptions         // 打开选项,一般设为 FILE_SYNCHRONOUS_IO_NONALERT。
  8. );
复制代码
接着文件被打开后,我们还需要调用ZwCreateSection()该函数的作用是创建一个Section节对象,并以PE结构中的SectionALignment大小对齐映射文件,其微软定义如下;
  1. NTSYSAPI NTSTATUS ZwCreateSection(
  2.   [out]          PHANDLE            SectionHandle,            // 指向 HANDLE 变量的指针,该变量接收 section 对象的句柄。
  3.   [in]           ACCESS_MASK        DesiredAccess,            // 指定一个 ACCESS_MASK 值,该值确定对 对象的请求访问权限。
  4.   [in, optional] POBJECT_ATTRIBUTES ObjectAttributes,         // 指向 OBJECT_ATTRIBUTES 结构的指针,该结构指定对象名称和其他属性。
  5.   [in, optional] PLARGE_INTEGER     MaximumSize,              // 指定节的最大大小(以字节为单位)。
  6.   [in]           ULONG              SectionPageProtection,    // 指定要在 节中的每个页面上放置的保护。
  7.   [in]           ULONG              AllocationAttributes,     // 指定确定节的分配属性的SEC_XXX 标志的位掩码。
  8.   [in, optional] HANDLE             FileHandle                // (可选)指定打开的文件对象的句柄。
  9. );
复制代码
最后读取导出表就要将一个磁盘中的文件映射到内存中,内存映射核心文件时ZwMapViewOfSection()该系列函数在应用层名叫MapViewOfSection()只是一个是内核层一个应用层,这两个函数参数传递基本一致,以ZwMapViewOfSection为例,其微软定义如下;
  1. NTSYSAPI NTSTATUS ZwMapViewOfSection(
  2.   [in]                HANDLE          SectionHandle,          // 接收一个节对象
  3.   [in]                HANDLE          ProcessHandle,          // 进程句柄,此处使用NtCurrentProcess()获取自身句柄
  4.   [in, out]           PVOID           *BaseAddress,           // 指定填充地址
  5.   [in]                ULONG_PTR       ZeroBits,               // 0
  6.   [in]                SIZE_T          CommitSize,             // 每次提交大小 1024
  7.   [in, out, optional] PLARGE_INTEGER  SectionOffset,          // 0
  8.   [in, out]           PSIZE_T         ViewSize,               // 浏览大小
  9.   [in]                SECTION_INHERIT InheritDisposition,     // ViewShare
  10.   [in]                ULONG           AllocationType,         // 分配类型 MEM_TOP_DOWN
  11.   [in]                ULONG           Win32Protect            // 权限 PAGE_READWRITE(读写)
  12. );
复制代码
将如上函数研究明白那么代码就变得很容易了,首先InitializeObjectAttributes设置文件权限与属性,然后调用ZwOpenFile打开文件,接着调用ZwCreateSection创建节对象,最后调用ZwMapViewOfSection将磁盘文件映射到内存,这段代码实现起来很简单,完整案例如下所示;
  1. // 署名权
  2. // right to sign one's name on a piece of work
  3. // PowerBy: LyShark
  4. // Email: me@lyshark.com
  5. #include <ntifs.h>
  6. #include <ntimage.h>
  7. #include <ntstrsafe.h>
  8. // 内存映射文件
  9. NTSTATUS KernelMapFile(UNICODE_STRING FileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress)
  10. {
  11.         NTSTATUS status = STATUS_SUCCESS;
  12.         HANDLE hFile = NULL;
  13.         HANDLE hSection = NULL;
  14.         OBJECT_ATTRIBUTES objectAttr = { 0 };
  15.         IO_STATUS_BLOCK iosb = { 0 };
  16.         PVOID pBaseAddress = NULL;
  17.         SIZE_T viewSize = 0;
  18.         // 设置文件权限
  19.         InitializeObjectAttributes(&objectAttr, &FileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
  20.         // 打开文件
  21.         status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttr, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
  22.         if (!NT_SUCCESS(status))
  23.         {
  24.                 return status;
  25.         }
  26.         // 创建节对象
  27.         status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile);
  28.         if (!NT_SUCCESS(status))
  29.         {
  30.                 ZwClose(hFile);
  31.                 return status;
  32.         }
  33.         // 映射到内存
  34.         status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddress, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE);
  35.         if (!NT_SUCCESS(status))
  36.         {
  37.                 ZwClose(hSection);
  38.                 ZwClose(hFile);
  39.                 return status;
  40.         }
  41.         // 返回数据
  42.         *phFile = hFile;
  43.         *phSection = hSection;
  44.         *ppBaseAddress = pBaseAddress;
  45.         return status;
  46. }
  47. VOID UnDriver(PDRIVER_OBJECT driver)
  48. {
  49.         DbgPrint("驱动卸载 \n");
  50. }
  51. NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
  52. {
  53.         DbgPrint("hello lyshark.com \n");
  54.         NTSTATUS status = STATUS_SUCCESS;
  55.         HANDLE hFile = NULL;
  56.         HANDLE hSection = NULL;
  57.         PVOID pBaseAddress = NULL;
  58.         UNICODE_STRING FileName = {0};
  59.         // 初始化字符串
  60.         RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntoskrnl.exe");
  61.         // 内存映射文件
  62.         status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
  63.         if (NT_SUCCESS(status))
  64.         {
  65.                 DbgPrint("读取内存地址 = %p \n", pBaseAddress);
  66.         }
  67.         Driver->DriverUnload = UnDriver;
  68.         return STATUS_SUCCESS;
  69. }
复制代码
运行这段程序,即可读取到ntoskrnl.exe磁盘所在文件的内存映像基地址,效果如下所示;

如上代码读入了ntoskrnl.exe文件,接下来就是解析导出表,首先将pBaseAddress解析为PIMAGE_DOS_HEADER获取DOS头,并在DOS头中寻找PIMAGE_NT_HEADERS头,接着在NTHeader头中得到数据目录表,此处指向的就是导出表PIMAGE_EXPORT_DIRECTORY通过pExportTable->NumberOfNames可得到导出表的数量,通过(PUCHAR)pDosHeader + pExportTable->AddressOfNames得到导出表的地址,依次循环读取即可得到完整的导出表。
  1. // 署名权
  2. // right to sign one's name on a piece of work
  3. // PowerBy: LyShark
  4. // Email: me@lyshark.com
  5. NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
  6. {
  7.         DbgPrint("hello lyshark.com \n");
  8.         NTSTATUS status = STATUS_SUCCESS;
  9.         HANDLE hFile = NULL;
  10.         HANDLE hSection = NULL;
  11.         PVOID pBaseAddress = NULL;
  12.         UNICODE_STRING FileName = { 0 };
  13.         LONG FunctionIndex = 0;
  14.         // 初始化字符串
  15.         RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntoskrnl.exe");
  16.         // 内存映射文件
  17.         status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
  18.         if (NT_SUCCESS(status))
  19.         {
  20.                 DbgPrint("[LyShark] 读取内存地址 = %p \n", pBaseAddress);
  21.         }
  22.         // Dos 头
  23.         PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
  24.         // NT 头
  25.         PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
  26.         // 导出表
  27.         PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
  28.         // 有名称的导出函数个数
  29.         ULONG ulNumberOfNames = pExportTable->NumberOfNames;
  30.         DbgPrint("[LyShark.com] 导出函数个数: %d \n\n", ulNumberOfNames);
  31.         // 导出函数名称地址表
  32.         PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
  33.         PCHAR lpName = NULL;
  34.         // 开始遍历导出表(输出ulNumberOfNames导出函数)
  35.         for (ULONG i = 0; i < ulNumberOfNames; i++)
  36.         {
  37.                 lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
  38.                 // 获取导出函数地址
  39.                 USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
  40.                 ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
  41.                 PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);
  42.                 // 获取SSDT函数Index
  43.                 FunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4);
  44.                 DbgPrint("序号: [ %d ] | Hint: %d | 地址: %p | 函数名: %s \n", i, uHint, lpFuncAddr, lpName);
  45.         }
  46.         // 释放指针
  47.         ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
  48.         ZwClose(hSection);
  49.         ZwClose(hFile);
  50.         Driver->DriverUnload = UnDriver;
  51.         return STATUS_SUCCESS;
  52. }
复制代码
代码运行后即可获取到当前ntoskrnl.exe程序中的所有导出函数,输出效果如下所示;

  • SSDT表通常会解析\\??\\C:\\Windows\\System32\\ntoskrnl.exe
  • SSSDT表通常会解析\\??\\C:\\Windows\\System32\\win32k.sys

根据上方的函数流程将其封装为GetAddressFromFunction()用户传入DllFileName指定的PE文件,以及需要读取的pszFunctionName函数名,即可输出该函数的导出地址。
  1. // 署名权
  2. // right to sign one's name on a piece of work
  3. // PowerBy: LyShark
  4. // Email: me@lyshark.com
  5. // 寻找指定函数得到内存地址
  6. ULONG64 GetAddressFromFunction(UNICODE_STRING DllFileName, PCHAR pszFunctionName)
  7. {
  8.         NTSTATUS status = STATUS_SUCCESS;
  9.         HANDLE hFile = NULL;
  10.         HANDLE hSection = NULL;
  11.         PVOID pBaseAddress = NULL;
  12.         // 内存映射文件
  13.         status = KernelMapFile(DllFileName, &hFile, &hSection, &pBaseAddress);
  14.         if (!NT_SUCCESS(status))
  15.         {
  16.                 return 0;
  17.         }
  18.         PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
  19.         PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
  20.         PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
  21.         ULONG ulNumberOfNames = pExportTable->NumberOfNames;
  22.         PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
  23.         PCHAR lpName = NULL;
  24.         for (ULONG i = 0; i < ulNumberOfNames; i++)
  25.         {
  26.                 lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
  27.                 USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
  28.                 ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
  29.                 PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);
  30.                 if (_strnicmp(pszFunctionName, lpName, strlen(pszFunctionName)) == 0)
  31.                 {
  32.                         ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
  33.                         ZwClose(hSection);
  34.                         ZwClose(hFile);
  35.                         return (ULONG64)lpFuncAddr;
  36.                 }
  37.         }
  38.         ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
  39.         ZwClose(hSection);
  40.         ZwClose(hFile);
  41.         return 0;
  42. }
  43. VOID UnDriver(PDRIVER_OBJECT driver)
  44. {
  45.         DbgPrint("驱动卸载 \n");
  46. }
  47. NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
  48. {
  49.         DbgPrint("hello lyshark.com \n");
  50.         UNICODE_STRING FileName = { 0 };
  51.         ULONG64 FunctionAddress = 0;
  52.         // 初始化字符串
  53.         RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
  54.         // 取函数内存地址
  55.         FunctionAddress = GetAddressFromFunction(FileName, "ZwQueryVirtualMemory");
  56.         DbgPrint("ZwQueryVirtualMemory内存地址 = %p \n", FunctionAddress);
  57.         Driver->DriverUnload = UnDriver;
  58.         return STATUS_SUCCESS;
  59. }
复制代码
如上程序所示,当运行后即可获取到ntdll.dll模块内ZwQueryVirtualMemory的导出地址,输出效果如下所示;


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

温锦文欧普厨电及净水器总代理

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表