Windows内核驱动Hook入门

article/2025/10/5 19:01:36

文章目录

  • Hook框架选择
    • 基于微软规范的框架
    • 微软规范以外的框架
      • 简单介绍一下InfinityHook
  • 获取内核中的函数地址
    • 内核中导出的函数
    • 内核未导出的函数
      • 获取 SSDT ShadowSSDT 地址
      • 获取系统服务号
        • 手动获取
          • 获取并判断系统版本
        • 代码自动获取
      • 获取GUI相关的函数地址,还需附加GUI进程
        • 获取进程 PEPROCESS
      • 获取函数地址
  • 替换被Hook的函数 的函数实现
    • 获取函数原型
    • 如果被Hook的函数是一个高频函数,如何准确定位到是自己的程序调用
    • 分析不同情况下的返回值类型,调用的 R3 API 如何进行处理这些返回值
    • 分析驱动如何返回信息到R3,返回值如何修改
  • 驱动与R3通讯
    • 通讯 驱动部分代码
    • 通讯 R3应用程序部分代码
  • 其他
    • 声明参数未引用
    • 中断请求级别
  • 参考

Hook框架选择

基于微软规范的框架

优势:高稳定性、开发难度降低、系统大版本升级兼容
劣势:自由度低、只知其然不知其所以然、框架中未提供功能束手无策

相关框架 例如:
文件系统相关:miniFilter、SFilter
进程、注册表相关:ObRegisterCallbacks注册回调
网络:TDI、WFP

微软规范以外的框架

优势:自由度高、没有做不到只有想不到,更接近底层
劣势:难度高 需要对处理的功能前后都了解、错误或不完善的处理会引发未知的错误

相关框架 例如:
InfinityHook、SSDTHook、IDTHook 等

如果选择微软规范以外的框架,win7 64位系统以后 我们碰到了一只拦路虎——PG (PatchGuard)
我们有两个选择 干掉PG 或者 绕过PG

干掉PG
需要找到所有检测点,每个监测点触发的条件和时间都不定,很难过掉所有的检测
绕过PG
InfinityHook 利用Windows的事件跟踪机制,可以与 PatchGuard 同时稳定运行

简单介绍一下InfinityHook

开启 Event Tracing 后,当用户层程序进行系统调用时,首先会调用PerfInfoLogSysCallEntry函数来对此次系统调用进行记录。然后才调用系统调用函数。
PerfInfoLogSysCallEntry内部又会调用驱动注册的WMI_LOGGER_CONTEXT结构中的GetCpuClock指向的函数。
这样就可以通过修改GetCpuClock指针为自己的代理函数,而且此时系统调用函数被放置在栈上,因此在该函数中能从栈中获取系统调用函数的值并进行修改为自己的hook函数。

获取内核中的函数地址

内核中导出的函数

这里我用 ZwQueryInformationProcess 举例

定义函数类型

typedef NTSTATUS(*PfnZwQueryInformationProcess) (__in HANDLE ProcessHandle,__in PROCESSINFOCLASS ProcessInformationClass,__out_bcount(ProcessInformationLength) PVOID ProcessInformation,__in ULONG ProcessInformationLength,__out_opt PULONG ReturnLength);

声明函数指针

PfnZwQueryInformationProcess ZwQueryInformationProcess;

获取函数地址

UNICODE_STRING UtrZwQueryInformationProcessName =RTL_CONSTANT_STRING(L"ZwQueryInformationProcess");ZwQueryInformationProcess =(PfnZwQueryInformationProcess)MmGetSystemRoutineAddress(&UtrZwQueryInformationProcessName);

内核未导出的函数

请注意! 调用未导出函数,可能会导致驱动不能兼容高版本;
调用导出函数,也可能会导致驱动不能兼容高版本。
例如:

/* 将当前线程附加到Process参数所指向的进程的地址空间 */
void KeAttachProcess(PRKPROCESS Process
);

KeAttachProcess 已过时,Win10.19041 开始不再支持
改用 KeStackAttachProcess

void KeStackAttachProcess(PRKPROCESS   PROCESS,PRKAPC_STATE ApcState
);

获取 SSDT ShadowSSDT 地址

应用程序调用WinAPI,GUI无关的对应到 ntdll.dll 中的 NtXXX函数,GUI相关的对应到 win32u.dll 中的 NtXXX函数
NtXXX函数中 通过 mov eax,0x***将系统服务号放入EAX(win32u.dll 中真实系统服务号为0x*** - 0x1000
服务分发函数 KiSystemService 根据传入的系统服务号,通过SSDT表 找到内核中的地址。

SSDT 系统服务描述符表 (System Services Descriptor Table)
SSDT 对应 ntoskrnl.exe 中的服务函数,这些函数实现了如文件管理、进程管理、设备管理等等相关的功能。
ShadowSSDT 对应 win32k.sys 中的服务函数,重点实现创建窗口、查找窗口、窗口绘图等 Gdi 与用户交互相关的功能。

Win7 32 位系统中,SSDT 在内核 Ntoskrnl.exe 中导出,直接获取导出符号 KeServiceDescriptorTable。

而在 64 位系统中,SSDT 表并没有在内核 Ntoskrnl.exe 中导出,我们不能像 32 位系统中那样直接获取导出符号 KeServiceDescriptorTable。

Win7 x64 与 Win10 64(Win10低版本)中 通过 __readmsr(0xC0000082) 获取内核函数 KiSystemCall64 的地址
KiSystemCall64 中调用了 KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow
IDA查看 KiSystemCall64 的反汇编

	 Win7 x64nt!KiSystemServiceRepeat:fffff800`03e8d772 4c8d15c7202300  lea     r10,[nt!KeServiceDescriptorTable (fffff800`040bf840)]fffff800`03e8d779 4c8d1d00212300  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff800`040bf880)]Win10 x64nt!KiSystemServiceRepeat:00000001401906D4 4C 8D 15 A5 71 28 00		lea     r10, KeServiceDescriptorTable00000001401906DB 4C 8D 1D 1E 00 27 00		lea     r11, KeServiceDescriptorTableShadow

KeServiceDescriptorTable 特征码 4c8d15
KeServiceDescriptorTableShadow 特征码 4c8d1d

win10 高版本中 __readmsr(0xC0000082) 返回 KiSystemCall64Shadow 函数
而 KiSystemCall64Shadow 无法直接搜索到 KeServiceDescriptorTable,IDA查看 KiSystemCall64Shadow 的反汇编

	 000000014034F140 KiSystemCall64Shadow proc near...000000014034F39A	e9631ae9ff	jmp     KiSystemServiceUser(00000001401CDC02) ; Jump000000014034F39F ; ---------------------------------------------------------------------------000000014034F39F   c3          retn						; Return Near from Procedure000000014034F39F KiSystemCall64Shadow endpKiSystemServiceUser 中也有4C 8D 15 A5 71 28 00		lea     r10, KeServiceDescriptorTable4C 8D 1D 1E 00 27 00		lea     r11, KeServiceDescriptorTableShadow

最终获取到的函数地址由 功能代码 变为 jmp跳转
代码从 win32k!NtXXX 跳转到了 win32kfull!NtXXX
win32k.sys 不再直接处理来自用户层的系统服务调用,而真正去处理用户的系统服务调用的函数,实际上是 win32kfull.sys 中的同名函数

这里贴的代码是获取 ShadowSSDT 地址,如果需要获取SSDT 地址,简单修改即可

/* 获取ShadowSSDT表地址 */
PVOID GetShadowTableAddress() {PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082);PUCHAR EndSearchAddress = StartSearchAddress + 0x500;ULONGLONG KeServiceDescriptorTable = 0;KeServiceDescriptorTable = SearchforKeServiceDescriptorTableShadow64(StartSearchAddress, EndSearchAddress);if (KeServiceDescriptorTable)return (PVOID)KeServiceDescriptorTable;/* msr[0xc0000082]变成了KiSystemCall64Shadow函数 */ULONGLONG KiSystemServiceUser = 0;ULONGLONG templong = 0xffffffffffffffff;for (PUCHAR i = StartSearchAddress; i < EndSearchAddress + 0xff; i++){if (*(PUCHAR)i == 0xe9 && *(PUCHAR)(i + 5) == 0xc3){//fffff803`23733383 e9631ae9ff      jmp     nt!KiSystemServiceUser(fffff803`235c4deb)//fffff803`23733388 c3              retRtlCopyMemory(&templong, (PUCHAR)(i + 1), 4);KiSystemServiceUser = templong + 5 + (ULONGLONG)i;//KiSystemServiceUserEndSearchAddress = (PUCHAR)(KiSystemServiceUser + 0x500);KeServiceDescriptorTable = SearchforKeServiceDescriptorTableShadow64((PUCHAR)KiSystemServiceUser, EndSearchAddress);return (PVOID)KeServiceDescriptorTable;}}return 0;
}/* 传入检查的地址的起始与结束位置,根据ShadowSSDT特征查找 */
ULONGLONG SearchforKeServiceDescriptorTableShadow64(PUCHAR StartSearchAddress, PUCHAR EndSearchAddress)
{UCHAR b1 = 0, b2 = 0, b3 = 0;ULONG templong = 0;ULONGLONG KeServiceDescriptorTable = 0;/*地址效验 */if (MmIsAddressValid(StartSearchAddress) == FALSE)return NULL;if (MmIsAddressValid(EndSearchAddress) == FALSE)return NULL;for (PUCHAR i = StartSearchAddress; i < EndSearchAddress; i++){if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2)){b1 = *i;b2 = *(i + 1);b3 = *(i + 2);if (b1 == 0x4c && b2 == 0x8d && b3 == 0x1D)/* 4c8d1D */{memcpy(&templong, i + 3, 4);KeServiceDescriptorTable = (ULONGLONG)i + 7 + (ULONGLONG)templong;/* 当前地址 + 指令长度 + 偏移 */return KeServiceDescriptorTable;}}}return NULL;
}

定义服务描述符表

/* 服务描述符表 */
typedef struct _SERVICE_DESCRIPTOR_TABLE {PULONGLONG   ServiceTable;PVOID  CounterTable;ULONGLONG  TableSize;PVOID  ArgumentTable;
} SERVICE_DESCRIPTOR_TABLE, * PSERVICE_DESCRIPTOR_TABLE;

用法举例

PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorShadowTable;
KeServiceDescriptorShadowTable = (PSERVICE_DESCRIPTOR_TABLE)GetShadowTableAddress();

获取系统服务号

手动获取

GUI无关的 使用IDA 查看 ntdll.dll,找到 NtXXX 函数,查看反汇编
找到 mov eax,0x***0x***就是系统服务号
GUI相关的 使用IDA 查看 win32u.dll,找到 NtXXX 函数,查看反汇编
找到 mov eax,0x***0x*** - 0x1000 就是系统服务号

注意! 有一部分内核函数的系统服务号,会随着系统版本、补丁发生改变,导致驱动不兼容多版本,建议采用代码自动获取的方式。

获取并判断系统版本

手动获取的系统服务号不能兼容多个版本,所以还需要获取并判断系统版本

枚举windows版本

/* 枚举windows版本 */
typedef enum WIN_VER_DETAIL {WINDOWS_VERSION_NONE,/* 0 */WINDOWS_VERSION_7_7600_UP,WINDOWS_VERSION_7_7000,WINDOWS_VERSION_10_15063,/* win10 1703 */WINDOWS_VERSION_10_16299,/* win10 1709 */WINDOWS_VERSION_10_17134,/* win10 1803 */WINDOWS_VERSION_10_17763,/* win10 1809 */WINDOWS_VERSION_10_18362,/* win10 1903 */WINDOWS_VERSION_10_18363,/* win10 1909 */WINDOWS_VERSION_10_19041_UP/* win10 大于等于2004版本 */
} WIN_VER_DETAIL;

获取并判断系统版本

/* 获取系统版本 */
WIN_VER_DETAIL GetWindowsVersion()
{RTL_OSVERSIONINFOEXW	osverinfo;WIN_VER_DETAIL WinVersion = WINDOWS_VERSION_NONE;memset(&osverinfo, 0, sizeof(RTL_OSVERSIONINFOEXW));osverinfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);if (RtlGetVersion((RTL_OSVERSIONINFOW*)&osverinfo) != STATUS_SUCCESS) {return WINDOWS_VERSION_NONE;}else if (osverinfo.dwMajorVersion == 6 && osverinfo.dwMinorVersion == 1 && osverinfo.dwBuildNumber == 7000) {WinVersion = WINDOWS_VERSION_7_7000;}else if (osverinfo.dwMajorVersion == 6 && osverinfo.dwMinorVersion == 1 && osverinfo.dwBuildNumber >= 7600) {WinVersion = WINDOWS_VERSION_7_7600_UP;}else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 15063) {WinVersion = WINDOWS_VERSION_10_15063;}else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 16299) {WinVersion = WINDOWS_VERSION_10_16299;}else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 17134) {WinVersion = WINDOWS_VERSION_10_17134;}else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 17763) {WinVersion = WINDOWS_VERSION_10_17763;}else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 18362) {WinVersion = WINDOWS_VERSION_10_18362;}else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 18363) {WinVersion = WINDOWS_VERSION_10_18363;}else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber >= 19041) {WinVersion = WINDOWS_VERSION_10_19041_UP;}kprintf("WinVersion: dwMajorVersion=%d; dwMinorVersion=%d; dwBuildNumber=%d;\n", osverinfo.dwMajorVersion, osverinfo.dwMinorVersion, osverinfo.dwBuildNumber);return WinVersion;
}

用法举例

if (WinVersion == WINDOWS_VERSION_10_19041_UP) {/* 当前的infinityhook不支持高于win10.19041的版本 *///return STATUS_NDIS_BAD_VERSION;/* 指定版本无效 */return STATUS_INCOMPATIBLE_DRIVER_BLOCKED;/* 由于与该系统不兼容,驱动已被禁止加载。请与软件供应商联系以获取驱动程序的兼容版本 */
}

代码自动获取

代码自动获取系统服务号相当于是用代码模拟了手动获取的方式

  1. 将dll读入内存中
  2. 检查是否位有效的PE文件
  3. 根据PE文件结构 PE头->扩展头->数据目录->导出表
  4. 获取以函数名字导出的函数个数、导出函数名称表,遍历找到我们要获取的函数地址
  5. 根据函数地址,找 mov eax,0x*** 的特征 0xB8
    注意! Rva到文件偏移的转换

下面贴出代码

/* 一下是获取SSDT表函数索引 */
#define PE_ERROR_VALUE (ULONG)-1NTSTATUS InitReadFile(PWCHAR Path);/* 初始 将Path读入内存中 */
int GetExportSsdtIndex(const char* ExportName);/* 获取导出表中 函数索引 */
ULONG GetExportOffset(const unsigned char* FileData, ULONG FileSize, const char* ExportName);/* 获取导出表中 函数地址 */
ULONG RvaToOffset(PIMAGE_NT_HEADERS pnth, ULONG Rva, ULONG FileSize);/* 将Rva转为文件偏移 */
void* RtlAllocateMemory(bool InZeroMemory, SIZE_T InSize);/* 分配内存 */
void RtlFreeMemory(void* InPointer);/* 释放内存 *//* 初始 将Path读入内存中 */
NTSTATUS InitReadFile(PWCHAR Path)
{UNICODE_STRING FileName;OBJECT_ATTRIBUTES ObjectAttributes;RtlInitUnicodeString(&FileName, Path);/* L"\\SystemRoot\\system32\\ntdll.dll" */InitializeObjectAttributes(&ObjectAttributes, &FileName,OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,NULL, NULL);if (KeGetCurrentIrql() != PASSIVE_LEVEL){
#ifdef _DEBUGDbgPrint("[TITANHIDE] KeGetCurrentIrql != PASSIVE_LEVEL!\n");
#endifreturn STATUS_UNSUCCESSFUL;}HANDLE FileHandle;IO_STATUS_BLOCK IoStatusBlock;NTSTATUS NtStatus = ZwCreateFile(&FileHandle,GENERIC_READ,&ObjectAttributes,&IoStatusBlock, NULL,FILE_ATTRIBUTE_NORMAL,FILE_SHARE_READ,FILE_OPEN,FILE_SYNCHRONOUS_IO_NONALERT,NULL, 0);if (NT_SUCCESS(NtStatus)){FILE_STANDARD_INFORMATION StandardInformation = { 0 };NtStatus = ZwQueryInformationFile(FileHandle, &IoStatusBlock, &StandardInformation, sizeof(FILE_STANDARD_INFORMATION), FileStandardInformation);if (NT_SUCCESS(NtStatus)){g_FileSize = StandardInformation.EndOfFile.LowPart;kprintf("[TITANHIDE] FileSize of ntdll.dll is %08X!\r\n", StandardInformation.EndOfFile.LowPart);g_FileData = (unsigned char*)RtlAllocateMemory(true, g_FileSize);LARGE_INTEGER ByteOffset;ByteOffset.LowPart = ByteOffset.HighPart = 0;NtStatus = ZwReadFile(FileHandle,NULL, NULL, NULL,&IoStatusBlock,g_FileData,g_FileSize,&ByteOffset, NULL);if (!NT_SUCCESS(NtStatus)){RtlFreeMemory(g_FileData);kprintf("[TITANHIDE] ZwReadFile failed with status %08X...\r\n", NtStatus);}}elsekprintf("[TITANHIDE] ZwQueryInformationFile failed with status %08X...\r\n", NtStatus);ZwClose(FileHandle);}elsekprintf("[TITANHIDE] ZwCreateFile failed with status %08X...\r\n", NtStatus);return NtStatus;
}/* 获取导出表中  SSDT表 ShadowSSDT表 函数索引 */
int GetExportSsdtIndex(const char* ExportName)
{ULONG_PTR ExportOffset = GetExportOffset(g_FileData, g_FileSize, ExportName);if (ExportOffset == PE_ERROR_VALUE)return -1;int SsdtOffset = -1;unsigned char* ExportData = g_FileData + ExportOffset;for (int i = 0; i < 32 && ExportOffset + i < g_FileSize; i++) {if (ExportData[i] == 0xC2 || ExportData[i] == 0xC3)  //RETbreak;if (ExportData[i] == 0xB8) { //mov eax,X	SsdtOffset = *(int*)(ExportData + i + 1);break;}}if (SsdtOffset == -1) {kprintf("[TITANHIDE] SSDT Offset for %s not found...\r\n", ExportName);}if (SsdtOffset >= 0x1000) {/* ShadowSSDT表 函数索引减0x1000使用 */SsdtOffset -= 0x1000;}return SsdtOffset;
}/* 获取导出表中 函数地址 */
ULONG GetExportOffset(const unsigned char* FileData, ULONG FileSize, const char* ExportName)
{//Verify DOS HeaderPIMAGE_DOS_HEADER pdh = (PIMAGE_DOS_HEADER)FileData;if (pdh->e_magic != IMAGE_DOS_SIGNATURE){kprintf("[TITANHIDE] Invalid IMAGE_DOS_SIGNATURE!\r\n");return PE_ERROR_VALUE;}//Verify PE HeaderPIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(FileData + pdh->e_lfanew);if (pnth->Signature != IMAGE_NT_SIGNATURE){kprintf("[TITANHIDE] Invalid IMAGE_NT_SIGNATURE!\r\n");return PE_ERROR_VALUE;}//Verify Export DirectoryPIMAGE_DATA_DIRECTORY pdd = NULL;if (pnth->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)pdd = ((PIMAGE_NT_HEADERS64)pnth)->OptionalHeader.DataDirectory;elsepdd = ((PIMAGE_NT_HEADERS32)pnth)->OptionalHeader.DataDirectory;ULONG ExportDirRva = pdd[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;ULONG ExportDirSize = pdd[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;ULONG ExportDirOffset = RvaToOffset(pnth, ExportDirRva, FileSize);if (ExportDirOffset == PE_ERROR_VALUE){kprintf("[TITANHIDE] Invalid Export Directory!\r\n");return PE_ERROR_VALUE;}//Read Export DirectoryPIMAGE_EXPORT_DIRECTORY ExportDir = (PIMAGE_EXPORT_DIRECTORY)(FileData + ExportDirOffset);ULONG NumberOfNames = ExportDir->NumberOfNames;ULONG AddressOfFunctionsOffset = RvaToOffset(pnth, ExportDir->AddressOfFunctions, FileSize);ULONG AddressOfNameOrdinalsOffset = RvaToOffset(pnth, ExportDir->AddressOfNameOrdinals, FileSize);ULONG AddressOfNamesOffset = RvaToOffset(pnth, ExportDir->AddressOfNames, FileSize);if (AddressOfFunctionsOffset == PE_ERROR_VALUE ||AddressOfNameOrdinalsOffset == PE_ERROR_VALUE ||AddressOfNamesOffset == PE_ERROR_VALUE){kprintf("[TITANHIDE] Invalid Export Directory Contents!\r\n");return PE_ERROR_VALUE;}ULONG* AddressOfFunctions = (ULONG*)(FileData + AddressOfFunctionsOffset);USHORT* AddressOfNameOrdinals = (USHORT*)(FileData + AddressOfNameOrdinalsOffset);ULONG* AddressOfNames = (ULONG*)(FileData + AddressOfNamesOffset);//Find ExportULONG ExportOffset = PE_ERROR_VALUE;for (ULONG i = 0; i < NumberOfNames; i++){ULONG CurrentNameOffset = RvaToOffset(pnth, AddressOfNames[i], FileSize);if (CurrentNameOffset == PE_ERROR_VALUE)continue;const char* CurrentName = (const char*)(FileData + CurrentNameOffset);ULONG CurrentFunctionRva = AddressOfFunctions[AddressOfNameOrdinals[i]];if (CurrentFunctionRva >= ExportDirRva && CurrentFunctionRva < ExportDirRva + ExportDirSize)continue; //we ignore forwarded exportsif (!strcmp(CurrentName, ExportName))  //compare the export name to the requested export{ExportOffset = RvaToOffset(pnth, CurrentFunctionRva, FileSize);break;}}if (ExportOffset == PE_ERROR_VALUE){kprintf("[TITANHIDE] Export %s not found in export table!\r\n", ExportName);}return ExportOffset;
}/* 将Rva转为文件偏移 */
ULONG RvaToOffset(PIMAGE_NT_HEADERS pnth, ULONG Rva, ULONG FileSize)
{PIMAGE_SECTION_HEADER psh = IMAGE_FIRST_SECTION(pnth);USHORT NumberOfSections = pnth->FileHeader.NumberOfSections;for (int i = 0; i < NumberOfSections; i++){if (psh->VirtualAddress <= Rva){if ((psh->VirtualAddress + psh->Misc.VirtualSize) > Rva){Rva -= psh->VirtualAddress;Rva += psh->PointerToRawData;return Rva < FileSize ? Rva : PE_ERROR_VALUE;}}psh++;}return PE_ERROR_VALUE;
}/* 分配内存 */
void* RtlAllocateMemory(bool InZeroMemory, SIZE_T InSize) {void* Result = ExAllocatePoolWithTag(NonPagedPool, InSize, 'HIDE');if (InZeroMemory && (Result != NULL))RtlZeroMemory(Result, InSize);return Result;
}/* 释放内存 */
void RtlFreeMemory(void* InPointer) {ExFreePool(InPointer);
}

获取GUI相关的函数地址,还需附加GUI进程

只有在有 GUI 的线程当中,win32k.sys 的内存才可以被访问。
调用 KeAttachProcess 将当前线程附加到GUI进程的地址空间
KeAttachProcess 需要用到被附加进程的PEPROCESS

获取进程 PEPROCESS

目前最好用的是,遍历PID 对比进程名
其他方式在高版本可能不再兼容

这个直接贴代码

/* 通过进程名 获取进程EPROCESS */
NTSTATUS LookupProcessByName(IN PCHAR pcProcessName, OUT PEPROCESS* pEprocess) {PEPROCESS	pCurrentEprocess = NULL;ULONG uPid = 4;PCSZ lpcProcessName = NULL;ULONG uLength = 0;			/* 进程名长度 */if (!ARGUMENT_PRESENT(pcProcessName) || !ARGUMENT_PRESENT(pEprocess)) {return STATUS_INVALID_PARAMETER;}if (KeGetCurrentIrql() > PASSIVE_LEVEL) {return STATUS_UNSUCCESSFUL;}uLength = strlen(pcProcessName);while (uPid < 0x186a0) {if (NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)uPid, &pCurrentEprocess))){lpcProcessName = (PCSZ)PsGetProcessImageFileName(pCurrentEprocess);ObDereferenceObject(pCurrentEprocess);if (_strnicmp(pcProcessName, lpcProcessName, uLength) == 0) {*pEprocess = pCurrentEprocess;/*DbgBreakPoint();*/return STATUS_SUCCESS;}			}uPid += 4;}	return STATUS_FLT_INSTANCE_NOT_FOUND;
}

获取函数地址

用法示例

typedef NTSTATUS(*NtGdiDdDDIGetDisplayModeList)(_Inout_ PVOID lpParam
);static unsigned char* g_FileData = 0;
static ULONG g_FileSize = 0;
static NtUserEnumDisplaySettings g_OriginalNtUserEnumDisplaySettings = NULL;PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorShadowTable;
KeServiceDescriptorShadowTable = (PSERVICE_DESCRIPTOR_TABLE)GetShadowTableAddress();NTSTATUS status = STATUS_SUCCESS;
if (KeServiceDescriptorShadowTable) {PEPROCESS eprocess_explorer;/* 我们得到一个gui进程的对象,因为我们切换进程的时候需要用到 */if (LookupProcessByName("explorer.exe", &eprocess_explorer) == STATUS_SUCCESS || LookupProcessByName("csrss.exe", &eprocess_explorer) == STATUS_SUCCESS){KeAttachProcess(eprocess_explorer);/* 附加到目标进程 */ULONG64 shadowSSDTTable = *(ULONG64*)((PCHAR)KeServiceDescriptorShadowTable + 0x20);LONG32* shadowSSDTEntry = (LONG32*)shadowSSDTTable;if (!NT_SUCCESS(InitReadFile(L"\\SystemRoot\\system32\\win32u.dll"))) {kprintf("InitReadFile failed...\n");}else {int nNtUserEnumDisplaySettingsSerialNumber = GetExportSsdtIndex("NtUserEnumDisplaySettings");if ((nNtUserEnumDisplaySettingsSerialNumber == -1)|| (nNtGdiDdDDIGetDisplayModeListSerialNumber == -1)) {status = STATUS_UNSUCCESSFUL;}LONG32 offsetNtUserEnumDisplaySettings = shadowSSDTEntry[nNtUserEnumDisplaySettingsSerialNumber] >> 4;g_OriginalNtUserEnumDisplaySettings = (NtUserEnumDisplaySettings)(offsetNtUserEnumDisplaySettings + shadowSSDTTable);RtlFreeMemory(g_FileData);kprintf("NtUserEnumDisplaySettings=%d;NtGdiDdDDIGetDisplayModeList=%d;\n", nNtUserEnumDisplaySettingsSerialNumber, nNtGdiDdDDIGetDisplayModeListSerialNumber);}KeDetachProcess();/* 解除附加 */}
}
if (g_OriginalNtUserEnumDisplaySettings == NULL)return STATUS_UNSUCCESSFUL;
elsereturn status;

替换被Hook的函数 的函数实现

获取函数原型

如果为内核中导出的函数,MSDN可以查到函数原型
例如:ZwQueryInformationProcess
ZwQueryInformationProcess

如果为内核未导出的函数

  1. 浏览器搜索,可能大佬已经先一步分享了函数原型
  2. IDA查看ntdll.dll、win32u.dll、ntoskrnl.exe、win32k.sys(IDA分析的函数原因很可能不准确,作为参考)
    在这里插入图片描述
    在这里插入图片描述
  3. 自己写一个测试程序,单独调用R3的API,OD调试,跟踪到NtXXX函数,分析参数;如果参数是堆地址等,分析NtXXX返回后 堆填充的内容,如果不需要用到所有的返回信息,不需自己定义一个结构体,参数类型给成 PVOID 即可,记录需要用到的信息所在的字节偏移。

测试代码,测试R3 APIEnumDisplaySettingsW R0 APINtUserEnumDisplaySettings

int main() {DEVMODEW DeviceMode = { 0 };/* SIZE 220 DC */   EnumDisplaySettingsW(NULL, ENUM_CURRENT_SETTINGS,&DeviceMode);return 0;
}

X64dbg 执行NtXXX 函数前
可以看到有四个参数,参数三是 RtlAllocateHeap 申请的堆地址
在这里插入图片描述
X64dbg 执行NtXXX 函数后,分析返回的信息,提取有用的部分
在这里插入图片描述

如果被Hook的函数是一个高频函数,如何准确定位到是自己的程序调用

思路:

  1. 获取当前线程所属进程的 PEPROCESS
  2. 通过 PEPROCESS 获取当前进程句柄
  3. 通过 进程句柄 调用 ZwQueryInformationProcess 获取 进程名

实现代码

/*++
Routine Description:
获取指定进程的完整进程名
Arguments:
pEproc - 指定进程的EPROCESS地址
Return Value:
成功则返回进程名,失败返回NULL
Comments:
该函数返回的进程名,由调用则负责释放(ExFreePool)
--*/
PUNICODE_STRING GetProcNameByEproc(IN PEPROCESS pEproc) {NTSTATUS NtStatus;HANDLE hProc = NULL;PBYTE pBuf = NULL;ULONG ulSize = 32;PAGED_CODE();// 1. pEproc --> handleNtStatus = ObOpenObjectByPointer(pEproc,OBJ_KERNEL_HANDLE,NULL,0,NULL,KernelMode,&hProc);if (!NT_SUCCESS(NtStatus))return NULL;// 2. ZwQueryInformationProcesswhile (TRUE){pBuf = (PBYTE)ExAllocatePoolWithTag(NonPagedPool, ulSize, ALLOC_TAG);if (!pBuf) {ZwClose(hProc);return NULL;}NtStatus = ZwQueryInformationProcess(hProc,ProcessImageFileName,pBuf,ulSize,&ulSize);if (NtStatus != STATUS_INFO_LENGTH_MISMATCH)break;ExFreePool(pBuf);}ZwClose(hProc);if (!NT_SUCCESS(NtStatus)) {ExFreePool(pBuf);return NULL;}// 3. overreturn (PUNICODE_STRING)pBuf;
}

用法示例

/* 调试用 */PUNICODE_STRING CurProcName = GetProcNameByEproc(IoGetCurrentProcess());if ((CurProcName->Length != 0) && (wcsstr(CurProcName->Buffer, L"test.exe"))) {DbgBreakPoint();}ExFreePool(CurProcName);

分析不同情况下的返回值类型,调用的 R3 API 如何进行处理这些返回值

例如:

系统设置是如何获取的显示分辨率列表

系统设置->显示设置->显示分辨率
设置 是通过,调用 EnumDisplaySettingsW,
参数二 从0开始,每次调用+1,每次获取一条显示信息,直到返回值为0,枚举出了所有显示信息;

而内核中的处理是,当参数二为0时,操作系统将初始化并缓存有关显示设备的信息。当您将参数二设置为非零值调用EnumDisplaySettings时,该函数将返回上次在参数二为0的情况下调用该函数时缓存的信息。

内核NtAPI 返回成功 STATUS_SUCCESS 返回值 rax = 0
而当最后一个显示信息被返回后,再次参数二+1调用,内核会返回 STATUS_INVALID_PARAMETER_2 rax = 0xC00000F0L

当系统枚举显示信息时,我们如果在挂钩的函数中,直接返回 STATUS_INVALID_PARAMETER_2,系统将停止枚举分辨率。

分析驱动如何返回信息到R3,返回值如何修改

还是用 NtUserEnumDisplaySettings 举例
NtUserEnumDisplaySettings 使用第三个参数,也是R3 API 申请的堆来保存要返回的显示信息
如果我们要修改R3 EnumDisplaySettingsW 拿到的显示信息,可以在挂钩的内核函数中 直接修改(当前线程与申请堆的线程所属进程一致时,可以直接修改)
第一次调用返回我们设置好的显示信息,第二次调用返回 STATUS_INVALID_PARAMETER_2,系统的设置就只能选择我们设置的分辨率,而没有其他选项。

驱动与R3通讯

驱动与R3通讯,比较简单 驱动中 只需创建设备、将设备对象和符号链接绑定;R3的应用程序打开设备调用DeviceIoControl与驱动通讯。
需要注意的是: 我们只用到了DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL],但是其他的功能函数也需要填充,否则R3的应用程序打开设备时将报错。

通讯 驱动部分代码

static UNICODE_STRING g_devName = RTL_CONSTANT_STRING(L"\\Device\\XXXXXXDevice");/* 设备对象名, 需要以`\Device\`为前缀 */
static UNICODE_STRING g_symLinkName = RTL_CONSTANT_STRING(L"\\DosDevices\\XXXXXXDeviceLink");
/* 创建R3、R0通讯 设备 符号链接 */
NTSTATUS CreateR3IOR0(_In_ PDRIVER_OBJECT DriverObject) {NTSTATUS status = STATUS_SUCCESS;DEVICE_OBJECT* pDev = NULL;/* 创建一个设备对象 用于接收3环的IO请求 */status = IoCreateDevice(DriverObject,		/* 新设备对象所属的驱动对象 */0,&g_devName,			/* 设备对象名 */FILE_DEVICE_UNKNOWN,/* 设备类型,默认为此值 */0,					/* 默认填0 */FALSE,				/* 必须为FALSE */&pDev);				/* 输出参数, 是创建出来的新设备 */if (!NT_SUCCESS(status)) {KdPrint(("创建设备对象失败,错误:%08X\n", status));return status;}/*  指定设备对象使用缓冲区的方式 */pDev->Flags |= DO_BUFFERED_IO;/* DO_DIRECT_IO *//* 将设备对象和符号链接绑定. 没有符号链接,三环无法打开设备 */status = IoCreateSymbolicLink(&g_symLinkName,	/*符号链接名*/&g_devName);		/*设备名*/if (!NT_SUCCESS(status)) {return STATUS_UNSUCCESSFUL;}for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) {DriverObject->MajorFunction[i] = PassThroughDispatch;}DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = OnDeviceControl;return status;
}NTSTATUS OnDeviceControl(DEVICE_OBJECT* device, IRP* irp) {UNREFERENCED_PARAMETER(device);/*DbgBreakPoint();*//* 获取当前IO栈 */IO_STACK_LOCATION* pIoStack = IoGetCurrentIrpStackLocation(irp);/* 获取CTL_CODE */ ULONG ctlCode = pIoStack->Parameters.DeviceIoControl.IoControlCode;enum DeviceCode {Stop_Hook = CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS),recovery_Hook = CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)};switch (ctlCode) {case Stop_Hook: {if (g_StopHook == FALSE) {InterlockedIncrement(&g_StopHook);}break;}	case recovery_Hook: {if (g_StopHook == TRUE) {InterlockedDecrement(&g_StopHook);}break;}		default:break;}/* 需要在派遣函数中, 将IO请求设置为完成 *//* IoStatus - IO完成状态 */irp->IoStatus.Status = STATUS_SUCCESS;/* IO请求完成的状态 *//* irp->IoStatus.Information = 0; *//* IO请求完成的字节数 *//* 将irp(io请求包)设置为完成 */IoCompleteRequest(irp, IO_NO_INCREMENT);return irp->IoStatus.Status;
}NTSTATUS PassThroughDispatch(PDEVICE_OBJECT  DeviceObject, PIRP Irp) {UNREFERENCED_PARAMETER(DeviceObject);Irp->IoStatus.Status = STATUS_SUCCESS;     //LastError()Irp->IoStatus.Information = 0;             //ReturnLengthIoCompleteRequest(Irp, IO_NO_INCREMENT);   //将Irp返回给Io管理器return STATUS_SUCCESS;
}

用法示例 (在 DriverEntry 中调用)

NTSTATUS status = STATUS_SUCCESS;
status = CreateR3IOR0(DriverObject);/* 创建R3、R0通讯 设备 符号链接 */
if (!NT_SUCCESS(status)) {return status;
}

通讯 R3应用程序部分代码

enum DeviceCode {Stop_Hook = CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS),recovery_Hook = CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
};bool OpenDevice(PHANDLE PhDev) {/*通过CreateFile打开设备打开设备的时候,路径以`\??\`为前缀后面跟着的名字就是驱动中创建的符号链接名.*/*PhDev =CreateFile(L"\\??\\DisableResolutionDeviceLink",GENERIC_WRITE | GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);if (*PhDev == INVALID_HANDLE_VALUE) {printf("打开设备错误:%08X\n", GetLastError());return false;}return true;
}

通讯示例

DWORD dwBytesReturned = 0;
if (FALSE == DeviceIoControl(hDev,/*设备对象*/Stop_Hook, nullptr, NULL, nullptr, NULL,&dwBytesReturned,/*实际完成的字节数*/	/* 如果lpOverlapped(最后一个参数)为NULL,则lpBytesReturned不能为NULL。即使当操作不返回任何输出数据并且lpOutBuffer为NULL时,DeviceIoControl 也会使用lpBytesReturned。 */NULL)) {printf("Stop_Hook DeviceIoControl失败: %08X\n", GetLastError());
}

其他

声明参数未引用

UNREFERENCED_PARAMETER(参数);

中断请求级别

高IRQL的代码,可以中断(抢占)低IRQL的代码的执行过程,从而得到执行机会

常见的IRQL:

IRQLd等级介绍
PASSIVE_LEVEL应用层线程以及大部分内核函数处于该IRQL,可以无限制使用所有内核API,可以访问分页及非分页内存
APC_LEVEL异步方法调用(APC)或页错误时处于该IRQL,可以使用大部分内核API,可以访问分页及非分页内存
DISPATCH_LEVEL延迟方法调用(DPC)时处于该IRQL,可以使用特定的内核API,只能访问非分页内存

注意! 在调用任何一个内核API前,必须查看WDK文档,了解这个内核API的中断要求!

获取当前IRQL

NTHALAPI KIRQL KeGetCurrentIrql();

参考

  	[1]: Windows内核编程[2]: https://bbs.pediy.com/thread-253450.htm[3]: https://github.com/mrexodia/TitanHide/tree/3bb2af068278ca64d67f4ca109bb56121d946131[4]: https://github.com/microsoft/Windows-driver-samples/tree/master/filesys/miniFilter[5]: https://github.com/fIappy/InfinityHook[6]: https://www.angelic47.com/archives/85/

http://chatgpt.dhexx.cn/article/lSrfyC42.shtml

相关文章

Windows内核编程(二)-第一个内核程序

第一个内核程序 通过 Visual Studio新建工程 注意事项&#xff1a; 大部分widnows驱动程序都是内核驱动(Kernel Driver)&#xff0c;所以本笔记不分"驱动程序"与"内核编程"&#xff0c;也不区分"内核模块"(Kernel Module)、“驱动程序”(Drive…

Windows内核原理与实现之Windows研究内核(WRK)

Windows并非一个开放源码的操作系统&#xff0c;但正如上一章所提&#xff0c;Microsoft开放了一份以Windows XP x64和Windows Server 2003 SP1为基础的内核源代码&#xff0c;它可以编译和运行&#xff0c;作为教育科研机构的教学实践和研究的平台使用&#xff0c;称为WRK&…

Windows内核结构

Windows内核结构 第一篇博客&#xff0c;随便写下练练手:) Windows内核总共分为三层&#xff1a; 与硬件直接打交道的这一层叫做硬件抽象层简称HAL&#xff0c;这一层的用意就是把所有与硬件相关联的代码逻辑隔离到一个专门的模块中&#xff0c;从而做到尽可能的独立于硬件平…

WINDOWS内核对象及其理解

一&#xff0e;前言 Windows中有很多像进程对象、线程对象、文件对象等等这样的对象&#xff0c;我们称之为Windows内核对象。内核对象是系统地址空间中的一个内存块&#xff0c;由系统创建并维护&#xff0c;这个内存对象是一个数据结构&#xff0c;维护着与对象相关的信息&a…

windows内核——基石

友链 内存布局 用户内存空间和内核内存空间之间的gap是为了避免不经意的越界而导致安全问题 CPU的初始化 系统启动期间&#xff0c;会对所有的处理器进行初始化操作 大部分初始化操作我们都不必了解&#xff0c;因为你了不了解都不影响你的逆向&#xff0c;毕竟你又不是开发…

Windows内核原理与实现--Windows基本结构概述

一、Windows系统结构概述 1、Windows采用双模式来保护操作系统本身,内核模式和用户模式。在Windows中,用户代码和内核代码有各自的运行环境,而且它们可以访问的内存空间也并不相同。在x86中,内核代码可以访问当前进程的4GB虚拟地址空间,而用户代码只能访问底端2GB虚拟地址…

《Windows内核原理与实现笔记》(一)Windows系统结构和基本概念

Windows内核结构 上图是windows内核的组成结构 如图Windows内核分三层&#xff0c;与硬件直接打交道的是硬件抽象层HAL&#xff0c;这一层把所有与硬件相关代码逻辑隔离到一个专门模块中&#xff0c;从而是上层尽可能独立于硬件平台。HAL是一个独立动态链接库&#xff0c;wind…

windows内核基础

windows分层模型 硬件抽象层屏蔽了硬件实现功能的细节。 IRP为内核层重要的数据结构。 物理地址和虚拟地址 x64的cpu仅仅支持64位地址中的前48位。其中若虚拟地址为内核&#xff0c;则前16位为ffff&#xff1b;若虚拟地址为用户模式&#xff0c;则前16位为0000。用户能看到的…

WebService的工作原理

WebService的工作原理如下&#xff1a; 服务提供者WebService2和WebService3通过UDDI协议将服务注册WebService目录中服务消费者WebService1通过UDDI协议从WebService目录中查询服务&#xff0c;并获得服务的WSDL服务描述文件服务消费者WebService1通过WSDL语言远程调用WebSer…

WebService原理

1、WebService实际上就是两个应用程序之间的远程调用&#xff0c;而且这种调用是跨语言的。 2、应用程序调用WebService的接口&#xff0c;实际上就是解析XML语言。也就是说两个应用程序之间的交流实际上就是通过XML来交流的。 3、WebService内部的实现是基于HTTP协议的&…

Web Service 的工作原理

Web Service基本概念 Web Service也叫XML Web Service WebService是一种可以接收从Internet或者Intranet上的其它系统中传递过来的请求&#xff0c;轻量级的独立的通讯技术。是:通过SOAP在Web上提供的软件服务&#xff0c;使用WSDL文件进行说明&#xff0c;并通过UDDI进行注册。…

JavaScript高级程序设计(第三版)pdf版 下载

分享关于JavaScript高级程序设计&#xff08;第3版&#xff09;[美] Nicholas C.Zakas著 李松峰 曹力 译 一书供大家学习&#xff01;&#xff01;&#xff01; 链接: https://pan.baidu.com/s/1RD4EXuQnTqH3kUfHWFaOYw 提取码: vias 复制这段内容后打开百度网盘手机App&#…

JS高级程序设计(14)

DOM 文档对象模型&#xff08;DOM&#xff09;是HTML和XML文档的编程借口。DOM表示由多层节点构成的文档&#xff0c;通过它开发者可以添加、删除和修改页面的各个部分。 文章目录 DOM一、节点层级1.Node类型2.Document类型3.Element类型4.Text类型5.Comment属性6.CDATASectio…

javascript高级程序设计(红宝书)记录

故心故心故心故心小故冲啊 文章目录 第3章.语法基础第4章.变量作用域与内存第5章.基本引用类型第6章.集合引用类型第8章.对象、类与面向对象编程第10章.函数第11章.期约与异步函数第12章.BOM第17章事件第24章.网络请求与远程资源第25章.客户端存储 第3章.语法基础 undefined 值…

JavaScript高级编程

原文地址&#xff1a; http://www.onlamp.com/pub/a/onlamp/2007/07/05/writing-advanced-javascript.html Web应用程序&#xff08;Web Applications&#xff09; 从计算机纪元的黎明刚刚来临开始&#xff0c;不同平台间软件的互用性就一直是关注的焦点。为了尽可能实…

javascript高级程序设计 第三版

网盘地址 提取码&#xff1a;vh81 笔记 第二章 2.1script标签 <script>元素属性&#xff1a;async、charset、defer、language、src、type async和defer只对外部脚本有效&#xff0c;language已废弃&#xff0c;type默认为“text/javascript”defer属性可以让脚本在文…

js高级程序设计(一) —— js简介

学习《JavaScript高级程序设计》的知识总结&#xff0c;以及对部分内容的扩展~ 1、一个完整的js实现的三个部分 核心&#xff08;ECMAScript) 文档对象模型&#xff08;DOM) 浏览器对象模型&#xff08;BOM) 1-1&#xff1a;ECMAScript ECMA-262的近一版是第 5版&#xf…

JS高级程序设计(12)

BOM 浏览器对象模型BOM提供了与网页无关的浏览器功能对象。 文章目录 BOM一、window对象1.Global作用域2.窗口关系3.窗口位置与像素比4.窗口大小5.视口位置6.导航与打开新窗口7.定时器8.系统对话框 二、location对象1.查询字符串2.操作地址 三、navigator对象1.检测插件2.注册…

JavaScript 高级程序设计

理解原型对象 无论什么时候&#xff0c;只要创建一个新函数&#xff0c;就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。 在默认的情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个包含一个指向prototyp…

js高级程序设计(第一章)

1.什么是JavaScript 1.1 简短的历史回顾 出现背景&#xff1a;当时&#xff0c;验证简单的表单&#xff0c;需要大量与服务器的往返通信成为用户的痛点。 出现时间&#xff1a;1997 年&#xff0c;JavaScript 1.1 作为提案被提交给欧洲计算机制造商协会&#xff08;Ecma&…