# 内核内存映射文件之获取SSDT函数索引号 # 背景 很多时候,内核下的开发和用户层上的程序开发使用到的技术原理都是相同的,所以,我们可以通过类比学习,快速地对内核开发进行理解与熟悉。 正如本文讲解的内存映射文件技术,用户层上有专门的 WIN32 API 提供给我们开发使用。对于内核上,也有相对应的内核函数给我们调用,实现内存映射文件,把磁盘上的文件映射到内核内存空间中来。 本文要实现的就是使用内存映射文件技术,将磁盘上的 ntdll.dll 文件映射到内核内存空间中,并从导出表中获取导出函数地址,然后获取 SSDT 函数索引号。所以,这篇文章除了要对内存映射文件技术比较了解之外,还需要对 PE 结构也有一定得了解,否则很难理解透彻这篇文章。现在,我就把实现过程整理成文档,分享给大家。 # 函数介绍 ## ZwOpenFile 函数 > 打开现有文件,目录,设备或卷。 > > 函数声明 > > ```c++ > NTSTATUS ZwOpenFile( > _Out_ PHANDLE FileHandle, > _In_ ACCESS_MASK DesiredAccess, > _In_ POBJECT_ATTRIBUTES ObjectAttributes, > _Out_ PIO_STATUS_BLOCK IoStatusBlock, > _In_ ULONG ShareAccess, > _In_ ULONG OpenOptions > ); > ``` > > 参数 > > - FileHandle [out] > 指向接收文件句柄的HANDLE变量的指针。 > - DesiredAccess [in] > 指定一个ACCESS_MASK值,用于确定请求的对象访问。有关详细信息,请参阅ZwCreateFile的DesiredAccess参数。其中,GENERIC_READ 包括权限有 STANDARD_RIGHTS_READ、FILE_READ_DATA、FILE_READ_ATTRIBUTES、FILE_READ_EA、以及 SYNCHRONIZE。 > - ObjectAttributes [in] > 指向OBJECT_ATTRIBUTES结构的指针,指定对象名称和其他属性。使用InitializeObjectAttributes初始化此结构。如果调用者未在系统线程上下文中运行,则调用InitializeObjectAttributes时必须设置OBJ_KERNEL_HANDLE属性。 > - IoStatusBlock [out] > 指向接收最终完成状态的IO_STATUS_BLOCK结构的指针以及有关所请求操作的信息。 > - ShareAccess [in] > 指定文件的共享访问类型。有关详细信息,请参阅ZwCreateFile的ShareAccess参数。其中,FILE_SHARE_READ 表示允许其它线程读此文件;FILE_SHARE_WRITE 表示允许其它线程写此文件。 > - OpenOptions [in] > 指定打开文件时要应用的选项。有关更多信息,请参阅ZwCreateFile的CreateOptions参数。其中,FILE_SYNCHRONOUS_IO_ALERT 表示文件中的所有操作都是同步执行的。 代表呼叫者的任何等待都会从提醒中提前终止。 该标志还使I / O系统保持文件位置指针。 如果设置此标志,则必须在DesiredAccess参数中设置SYNCHRONIZE标志。 > > 返回值 > > - 成功,则返回 STATUS_SUCCESS;否则,返回其它 NTSTATUS 错误码。 ## ZwCreateSection 函数 > 创建一个节对象。 > > 函数声明 > > ```c++ > NTSTATUS ZwCreateSection( > _Out_ PHANDLE SectionHandle, > _In_ ACCESS_MASK DesiredAccess, > _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes, > _In_opt_ PLARGE_INTEGER MaximumSize, > _In_ ULONG SectionPageProtection, > _In_ ULONG AllocationAttributes, > _In_opt_ HANDLE FileHandle > ); > ``` > > 参数 > > - SectionHandle [out] > 指向接收段对象的句柄的HANDLE变量的指针。 > > - DesiredAccess [in] > 指定一个ACCESS_MASK值,用于确定请求的对象访问。除了为所有类型的对象定义的访问权限(请参阅ACCESS_MASK)之外,调用者可以指定以下任何访问权限,这些访问权限特定于部分对象: > DesiredAccess标志允许调用者执行此操作 > SECTION_EXTEND_SIZE:动态扩展部分的大小。 > SECTION_MAP_EXECUTE:执行该部分的视图。 > SECTION_MAP_READ:读取该部分的视图。 > SECTION_MAP_WRITE:编写该部分的视图。 > SECTION_QUERY:查询节对象有关该部分的信息。驱动应该设置这个标志。 > SECTION_ALL_ACCESS:包括上面所有的标志之外,还包括STANDARD_RIGHTS_REQUIRED。 > > - ObjectAttributes [in,可选] > 指向OBJECT_ATTRIBUTES结构的指针,指定对象名称和其他属性。使用InitializeObjectAttributes初始化此结构。如果调用者未在系统线程上下文中运行,则调用InitializeObjectAttributes时必须设置OBJ_KERNEL_HANDLE属性。 > > - MaximumSize [in,可选] > 指定部分的最大大小(以字节为单位)。 ZwCreateSection将此值转换为PAGE_SIZE的最接近的倍数。如果该部分由分页文件支持,则MaximumSize将指定该部分的实际大小。如果该部分由普通文件支持,则MaximumSize指定文件可以扩展或映射到的最大大小。 > > - SectionPageProtection [in] > 指定在该部分的每个页面上放置的保护。使用以下四个值之一:PAGE_READONLY,PAGE_READWRITE,PAGE_EXECUTE或PAGE_WRITECOPY。有关这些值的说明,请参阅CreateFileMapping。其中,PAGE_READWRITE 表示允许将视图映射为只读、写时复制、读/写访问。必须使用 GENERIC_READ 和 GENERIC_WRITE 访问权限创建 hFile 参数指定的文件句柄。 > > - AllocationAttributes[in] > 指定SEC_XXX标志的位掩码,以确定该部分的分配属性。有关这些标志的描述,请参阅CreateFileMapping。其中, > SEC_COMMIT 是以 PE 结构中的 FileAlignment 大小对齐映射文件。 > SEC_IMAGE 是以 PE 结构中的 SectionALignment 大小对齐映射文件。 > > - FileHandle [in,可选] > 可选地指定打开的文件对象的句柄。如果FileHandle的值为NULL,则该段由分页文件支持。否则,该部分由指定的文件支持。 > > 返回值 > > - 成功,则返回 STATUS_SUCCESS;否则,返回其它 NTSTATUS 错误码。 ## ZwMapViewOfSection 函数 > 将一个节表的视图映射到内核的虚拟地址空间。 > > 函数声明 > > ```c++ > NTSTATUS ZwMapViewOfSection( > _In_ HANDLE SectionHandle, > _In_ HANDLE ProcessHandle, > _Inout_ PVOID *BaseAddress, > _In_ ULONG_PTR ZeroBits, > _In_ SIZE_T CommitSize, > _Inout_opt_ PLARGE_INTEGER SectionOffset, > _Inout_ PSIZE_T ViewSize, > _In_ SECTION_INHERIT InheritDisposition, > _In_ ULONG AllocationType, > _In_ ULONG Win32Protect > ); > ``` > > 参数 > > - SectionHandle [in] > 节对象的句柄。该句柄是通过成功调用ZwCreateSection或ZwOpenSection创建的。 > - ProcessHandle [in] > 处理表示视图应该映射到进程的对象。使用ZwCurrentProcess宏来指定当前进程。必须使用PROCESS_VM_OPERATION访问(在Microsoft Windows SDK文档中描述)打开句柄。 > - BaseAddress [in,out] > 指向接收视图基地址的变量的指针。如果此参数的值不为NULL,则会从指定的虚拟地址开始分配视图,向下舍入到下一个64K字节的地址边界。 > - ZeroBits[in] > 指定截面视图基地址中必须为零的高位地址位数。此参数的值必须小于21,仅当BaseAddress为NULL时才使用 - 换句话说,当调用者允许系统确定在哪里分配视图时。 > - CommitSize [in] > 指定视图初始提交的区域的大小(以字节为单位)。 CommitSize仅对页面文件支持的部分有意义,并且四舍五入为PAGE_SIZE的最接近的倍数。 (对于映射文件的部分,数据和图像都将在段创建时提交。) > - SectionOffset [in,out,optional] > 指向变量的指针,该变量从字节开始到视图接收以字节为单位的偏移量。如果此指针不为NULL,则向左舍入到下一个分配粒度大小边界。 > - ViewSize [in,out] > 指向SIZE_T变量的指针。如果此变量的初始值为零,则ZwMapViewOfSection将在SectionOffset中开始的部分的视图映射到该部分的末尾。否则,初始值指定视图的大小(以字节为单位)。在映射视图之前,ZwMapViewOfSection始终将此值舍入到最接近PAGE_SIZE的倍数。 > 返回时,该值接收视图的实际大小(以字节为单位)。 > - InheritDisposition [in] > 指定视图如何与子进程共享。可能的值是: > ViewShare > 该视图将映射到将来创建的任何子进程。 > ViewUnmap > 该视图将不会映射到子进程。 > 驱动程序通常应为此参数指定ViewUnmap。 > - AllocationType[in] > 指定一组描述要为指定的页面区域执行的分配类型的标志。有效标志是MEM_LARGE_PAGES,MEM_RESERVE和MEM_TOP_DOWN。虽然不允许MEM_COMMIT,但是除非指定了MEM_RESERVE,否则是默认的。有关MEM_XXX标志的更多信息,请参阅VirtualAlloc例程的说明。MEM_TOP_DOWN 表示在尽可能高的地址分配内存。 > - Win32Protect [in] > 指定最初提交的页面区域的保护类型。设备和中间驱动程序应将此值设置为PAGE_READWRITE。 > > 返回值 > > - 成功,则返回 STATUS_SUCCESS;否则,返回其它 NTSTATUS 错误码。 # 实现原理 使用WIN32 API来实现内存映射文件,实现步骤如下: - 调用 CreatFile 打开想要映射的文件,获得句柄hFile。 - 调用 CreatFileMapping 函数生成一个建立在CreatFile函数创建的文件对象基础上的内存映射对象,得到句柄hFileMap。 - 调用 MapViewOfFile 函数把整个文件的一个区域或者整个区域映射到内存中,得到指向映射到内存的第一个字节的指针 lpMemory。 - 用该指针来读写文件。 - 调用 UnmapViewOfFile 来解除文件映射,传入参数为 lpMemory。 - 调用 CloseHandle 来关闭内存映射文件,传入参数为 hFileMap。 - 调用 CloseHandle 来关闭文件,传入函数为 hFile。 那么,在内核下的内存映射文件的实现步骤和用户层的也相同,只是使用的函数不相同而已: - 调用 ZwOpenFile 打开想要映射的文件,获得句柄 hFile。 - 调用 ZwCreatSection 函数生成一个建立在 ZwOpenFile 函数创建的文件对象基础上的内存映射对象,得到句柄 hSection。 - 调用 ZwMapViewOfSection 函数把整个文件的一个区域或者整个区域映射到内存中,得到指向映射到内存的第一个字节的指针 lpMemory。 - 用该指针来读写文件。 - 调用 ZwUnmapViewOfSection 来解除文件映射,传入参数为进程句柄以及 lpMemory。 - 调用 ZwClose 来关闭内存映射文件,传入参数为 hSection。 - 调用 ZwClose 来关闭文件,传入函数为 hFile。 其中,我们从导入表获取指定导出函数的导出地址的实现流程是: - 首先,将文件映射到内核内存后,我们便可以获取文件的映射基址。我们根据 PE 文结构体 IMAGE_DOS_HEADER 和 IMAGE_NT_HEADERS 计算出 OptionahlHeader,接着获取 DataDirectory 中的导出表 RVA 地址。这样,我们可以计算出导出表在内存中的地址。 - 然后,我们根据导出表结构 IMAGE_EXPORT_DIRECTORY 获取导出函数名称的个数以及导出函数名称的地址,以此遍历匹配是否是要查找的函数名称。若是,则从 AddressOfNamesOrdinal 中获取导出函数名称对应的导出函数索引值。有了这个导出函数索引值,我们直接就可以在 AddressOfFunctions 导出函数地址表中获取导出函数的地址。 - 最后,我们就可以根据 ntdll.dll 导出函数的地址,来获取 SSDT 函数索引号。 其中,对于 32 位系统,ntdll.dll 导出函数总是以下面代码形式为开头: ```c++ mov eax, 函数索引号(4字节) ``` 对于 64 位系统,ntdll.dll 导出函数总是以下面代码形式为开头: ```c++ mov r10, rcx mov eax, 函数索引号(4字节) ``` 所以,我们对于 32 位系统,只需对导出函数偏移 1 字节处获取 4 字节的数据,那么这 4 字节的数据就是 SSDT 函数索引号;对于 64 位系统,在导出函数的偏移为 4 字节,也是获取 4 字节数据,这 4 字节的数据就是 SSDT 函数索引号。 # 编码实现 ## 内存映射文件 ```c++ // 内存映射文件 NTSTATUS DllFileMap(UNICODE_STRING ustrDllFileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress) { NTSTATUS status = STATUS_SUCCESS; HANDLE hFile = NULL; HANDLE hSection = NULL; OBJECT_ATTRIBUTES objectAttributes = { 0 }; IO_STATUS_BLOCK iosb = { 0 }; PVOID pBaseAddress = NULL; SIZE_T viewSize = 0; // 打开 DLL 文件, 并获取文件句柄 InitializeObjectAttributes(&objectAttributes, &ustrDllFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttributes, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT); if (!NT_SUCCESS(status)) { KdPrint(("ZwOpenFile Error! [error code: 0x%X]", status)); return status; } // 创建一个节对象, 以 PE 结构中的 SectionALignment 大小对齐映射文件 status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile); if (!NT_SUCCESS(status)) { ZwClose(hFile); KdPrint(("ZwCreateSection Error! [error code: 0x%X]", status)); 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); KdPrint(("ZwMapViewOfSection Error! [error code: 0x%X]", status)); return status; } // 返回数据 *phFile = hFile; *phSection = hSection; *ppBaseAddress = pBaseAddress; return status; } ``` ## 根据导出表获取导出函数地址及获取SSDT函数索引号 ```c++ // 根据导出表获取导出函数地址, 从而获取 SSDT 函数索引号 ULONG GetIndexFromExportTable(PVOID pBaseAddress, PCHAR pszFunctionName) { ULONG ulFunctionIndex = 0; // Dos Header PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress; // NT Header PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew); // Export Table 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]); // 判断是否查找的函数 if (0 == _strnicmp(pszFunctionName, lpName, strlen(pszFunctionName))) { // 获取导出函数地址 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 # ifdef _WIN64 ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4); # else ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 1); # endif break; } } return ulFunctionIndex; } ``` # 程序测试 在 Win7 32 位系统下,驱动程序执行正常: ![](http://www.writebug.com/myres/static/uploads/2021/10/19/16a4b1d5fb35885f4e7b7c2bb9ee876f.writebug) 在 Win10 64 位系统下,驱动程序执行正常: ![](http://www.writebug.com/myres/static/uploads/2021/10/19/9b672566cc4661b4f9afb5c29d62c309.writebug) # 总结 其中,我们要注意 3 个地方: 一是,在调用 ZwCreateSection 函数的时候,第 6 个参数我们要设置为SEC_IMAGE,表示以 PE 结构中的 SectionALignment 大小对齐映射文件。这样映射到内存后,我们就可以直接从导出表中较为方便地获取 SSDT 函数索引值。 二是,SSDT 函数索引号在 32 位系统下和 64 位系统下,在 ntdll.dll 的导出函数中的偏移是不同的。32 位系统中,SSDT 函数索引号在 ntdll.dll 导出函数偏移 1 字节处;64 位系统中,SSDT 函数索引号在 ntdll.dll 导出函数偏移 4 字节处。 三是,内核下表示的文件或者目录路径要在路径前面加上 \\??\\,例如表示 C 盘下的 ntdll.dll文件路径:\\??\\C:\\Windows\\System32\\ntdll.dll。 # 参考 参考自《[Windows黑客编程技术详解](https://www.write-bug.com/article/1811.html "Windows黑客编程技术详解")》一书