16 KiB
内核内存映射文件之获取SSDT函数索引号
背景
很多时候,内核下的开发和用户层上的程序开发使用到的技术原理都是相同的,所以,我们可以通过类比学习,快速地对内核开发进行理解与熟悉。
正如本文讲解的内存映射文件技术,用户层上有专门的 WIN32 API 提供给我们开发使用。对于内核上,也有相对应的内核函数给我们调用,实现内存映射文件,把磁盘上的文件映射到内核内存空间中来。
本文要实现的就是使用内存映射文件技术,将磁盘上的 ntdll.dll 文件映射到内核内存空间中,并从导出表中获取导出函数地址,然后获取 SSDT 函数索引号。所以,这篇文章除了要对内存映射文件技术比较了解之外,还需要对 PE 结构也有一定得了解,否则很难理解透彻这篇文章。现在,我就把实现过程整理成文档,分享给大家。
函数介绍
ZwOpenFile 函数
打开现有文件,目录,设备或卷。
函数声明
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 函数
创建一个节对象。
函数声明
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 函数
将一个节表的视图映射到内核的虚拟地址空间。
函数声明
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 导出函数总是以下面代码形式为开头:
mov eax, 函数索引号(4字节)
对于 64 位系统,ntdll.dll 导出函数总是以下面代码形式为开头:
mov r10, rcx mov eax, 函数索引号(4字节)
所以,我们对于 32 位系统,只需对导出函数偏移 1 字节处获取 4 字节的数据,那么这 4 字节的数据就是 SSDT 函数索引号;对于 64 位系统,在导出函数的偏移为 4 字节,也是获取 4 字节数据,这 4 字节的数据就是 SSDT 函数索引号。
编码实现
内存映射文件
// 内存映射文件
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函数索引号
// 根据导出表获取导出函数地址, 从而获取 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 位系统下,驱动程序执行正常:
在 Win10 64 位系统下,驱动程序执行正常:
总结
其中,我们要注意 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黑客编程技术详解》一书