内核内存映射文件之获取SSDT函数索引号
Go to file
Demon-Gan-123 52c374f461 Initial commit 2021-10-19 16:02:33 +08:00
src Initial commit 2021-10-19 09:21:39 +08:00
LICENSE Initial commit 2021-10-19 01:22:45 +00:00
README.md Initial commit 2021-10-19 16:02:33 +08:00

README.md

内核内存映射文件之获取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_READONLYPAGE_READWRITEPAGE_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 [inout] 指向接收视图基地址的变量的指针。如果此参数的值不为NULL则会从指定的虚拟地址开始分配视图向下舍入到下一个64K字节的地址边界。
  • ZeroBits[in] 指定截面视图基地址中必须为零的高位地址位数。此参数的值必须小于21仅当BaseAddress为NULL时才使用 - 换句话说,当调用者允许系统确定在哪里分配视图时。
  • CommitSize [in] 指定视图初始提交的区域的大小(以字节为单位)。 CommitSize仅对页面文件支持的部分有意义并且四舍五入为PAGE_SIZE的最接近的倍数。 (对于映射文件的部分,数据和图像都将在段创建时提交。)
  • SectionOffset [inoutoptional] 指向变量的指针该变量从字节开始到视图接收以字节为单位的偏移量。如果此指针不为NULL则向左舍入到下一个分配粒度大小边界。
  • ViewSize [inout] 指向SIZE_T变量的指针。如果此变量的初始值为零则ZwMapViewOfSection将在SectionOffset中开始的部分的视图映射到该部分的末尾。否则初始值指定视图的大小以字节为单位。在映射视图之前ZwMapViewOfSection始终将此值舍入到最接近PAGE_SIZE的倍数。 返回时,该值接收视图的实际大小(以字节为单位)。
  • InheritDisposition [in] 指定视图如何与子进程共享。可能的值是: ViewShare 该视图将映射到将来创建的任何子进程。 ViewUnmap 该视图将不会映射到子进程。 驱动程序通常应为此参数指定ViewUnmap。
  • AllocationType[in] 指定一组描述要为指定的页面区域执行的分配类型的标志。有效标志是MEM_LARGE_PAGESMEM_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黑客编程技术详解》一书