查看原文
其他

高版本64位Win10(RS2或更高)下枚举消息钩子的一种思路

hhkqqs 看雪学苑 2022-07-01

本文为看雪论坛优秀文章

看雪论坛作者ID:hhkqqs



先是参考文章:

https://bbs.pediy.com/thread-14600.htm (作者:一块三毛钱)
https://bbs.pediy.com/thread-55453.htm (作者:炉子)

这里先简单回顾一下前人的枚举思路:一块三毛钱采用的是枚举所有线程并取ETHREAD的THREADINFO,同时枚举16种钩子类型iHookType,调用Win32k.sys中未导出函数PVOID PhkFirstValid(PTHREADINFO pti, int iHookType)获得保存钩子信息的结构体tagHOOK的首地址,随后循环调用PVOID PhkNextValid(tagHOOK*)以枚举所有的消息钩子。

炉子的思路是获取user32.dll中与Win32k.sys的共享变量gSharedInfo,遍历gSharedInfo->aheList的数组成员_HANDLEENTRY,总数为gSharedInfo->psi->cHandleEntries,若成员_HANDLEENTRY的bType为5,则其为消息钩子,_HANDLEENTRY的phead成员即为钩子对应的内核tagHOOK结构体首地址。

鉴于炉子的方法相对简便,本文的研究思路以炉子的方法作为基础展开。

首先介绍枚举方案中涉及的关键结构体:tagHOOK和_HANDLEENTRY。

tagHOOK结构体成员在64位Win10下没有变化,其简要结构为:

typedef struct tagHOOK{ PVOID Handle; PVOID cLockObj; PW32THREAD Win32Thread; ULONGLONG Patch1; tagHOOK* pSelf; //指向结构体的首地址 tagHOOK* pNext; //指向下一个结构体 ULONG hType; //钩子的类型 ULONG Patch2; PVOID offPfn; //ihmod为-1时,表示钩子函数绝对地址,否则为函数相对所在模块的偏移 ULONG flags; //取值1或3为全局钩子 int ihmod; //钩子所在模块的索引 PTHREADINFO ptiHooked; //被hook线程的THREADINFO} HOOK, *PHOOK;

在Win10 RS1(14393)及之前的64位Windows系统,_HANDLEENTRY结构为:

typedef struct _HANDLEENTRY{ PVOID phead; PVOID pOwner; UCHAR bType; UCHAR bFlags; USHORT wUniq;} HANDLEENTRY, *PHANDLEENTRY;

按照炉子的方法,只要取出其中的phead成员即为内核tagHOOK结构体的首地址,然而Win10 RS2(15063)中,微软为了修复GDI Object Leak,将_HANDLEENTRY中的phead和pOwner成员移除了,以下分别是14393和15063下gSharedInfo->aheList的_HANDLEENTRY数组:


    
新的HANDLEENTRY结构也没有写进符号文件,笔者水平有限,只能得到新的HANDLEENTRY大概长这样:

typedef struct _HANDLEENTRY_RS2{ PVOID Unknown1; PVOID UniqueThreadId; PVOID Unknown2; UCHAR bType; UCHAR bFlags; USHORT wUniq;} HANDLEENTRY, *PHANDLEENTRY;

第一眼看过去,似乎新版HANDLEENTRY没有提供多少查找tagHOOK结构体的线索。笔者决定跟一下gSharedInfo在内核的变动,值得一提的是,Windows 10下gSharedInfo从Win32k.sys移到了Win32kbase.sys中。

经过一番查找,笔者发现Win32kbase!HMValidateHandle引用了gSharedInfo->aheList的成员,传入的句柄参数取低16位即为aheList数组的索引,同时该索引也参与到取内核结构体地址的运算。

在Win10 RS2中伪代码如下:

enum HANDLE_TYPE { TYPE_FREE = 0, TYPE_WINDOW = 1, TYPE_MENU = 2, TYPE_CURSOR = 3, TYPE_SETWINDOWPOS = 4, TYPE_HOOK = 5, TYPE_CLIPDATA = 6, TYPE_CALLPROC = 7, TYPE_ACCELTABLE = 8, TYPE_DDEACCESS = 9, TYPE_DDECONV = 10, TYPE_DDEXACT = 11, TYPE_MONITOR = 12, TYPE_KBDLAYOUT = 13, TYPE_KBDFILE = 14, TYPE_WINEVENTHOOK = 15, TYPE_TIMER = 16, TYPE_INPUTCONTEXT = 17, TYPE_HIDDATA = 18, TYPE_DEVICEINFO = 19, TYPE_TOUCHINPUT = 20, TYPE_GESTUREINFO = 21, TYPE_CTYPES = 22, TYPE_GENERIC = 255};
PVOID HMValidateHandle(HANDLE Handle, HANDLE_TYPE hType){ PVOID Object = 0; if ((USHORT)Handle < gSharedInfo->psi->cHandleEntries) { PHANDLEENTRY hEntry = &gSharedInfo->aheList[(USHORT)Handle]; PVOID *pObject = &gpKernelHandleTable[2 * (USHORT)Handle]; USHORT Handle_HIWORD = (USHORT)(Handle >> 0x10); if ((Handle_HIWORD == hEntry->wUniq || Handle_HIWORD == -1 || !Handle_HIWORD && PsGetCurrentProcessWow64Process()) && !(hEntry->bFlags & 1) && hEntry->bType == hType ) { Object = *pObject; } } if (W32GetThreadWin32Thread(PsGetCurrentThread())->TIF_flags & 0x20000000) { if (!ValidateHandleSecure(Handle, 3)) Object = 0; } else { if (!ValidateHandleSecure(Handle, 2)) Object = 0; } if (!Object) { DWORD dwErrorCode; switch (hType) { case TYPE_WINDOW: dwErrorCode = 1400; break; case TYPE_MENU: dwErrorCode = 1401; break; case TYPE_CURSOR: dwErrorCode = 1402; break; case TYPE_SETWINDOWPOS: dwErrorCode = 1405; break; case TYPE_HOOK: dwErrorCode = 1404; break; case TYPE_ACCELTABLE: dwErrorCode = 1403; break; default: dwErrorCode = 6; break; } UserSetLastError(dwErrorCode); } return Object;}

从伪代码来看,似乎只需要遍历gSharedInfo->aheList并取出HANDLEENTRY.bType为TYPE_HOOK的索引,即可从gpKernelHandleTable中找到我们想要的tagHOOK。

但是,在15063以上的版本的Win10中,gpKernelHandleTable检索方法发生了变化,如:

15063下tagHOOKObject=gpKernelHandleTable[2 * Index];

18362下则变成了tagHOOKObject=gpKernelHandleTable[3 * Index]。

出于兼容性的考虑,笔者决定通过wUniq来生成钩子的句柄hHOOK = Index | (gSharedInfo->aheList[Index].wUniq << 0x10),并调用HmValidateHandle来获取tagHOOK。

虽然Win32kbase.sys下HmValidateHandle没有导出且没有导出函数调用它,但Win32kfull.sys里也有与其功能相同的HmValidateHandle。

而且笔者发现user32!UnhookWindowsHookEx的底层调用Win32kfull!NtUserUnhookWindowsHookEx中出现了对HmValidateHandle的调用,如图所示:


至此,枚举tagHOOK结构体的思路已经完善,伪代码如下:

tagSHAREDINFO* gSharedInfo = 0;typedef PVOID(*HMValidateHandleFn)(HANDLE Handle, HANDLE_TYPE hType);HMValidateHandleFn HMValidateHandle = 0;void GetWinHook_15063orHigher(PKPROCESS GUIProcess, PHOOK OutputBuffer){ _KAPC_STATE ApcState; KeStackAttachProcess(GUIProcess, &ApcState); if (!gSharedInfo) gSharedInfo = (tagSHAREDINFO*)GetKernelProcAddress(GetKernelModuleHandle(L"win32kbase.sys"), "gSharedInfo"); if (!HMValidateHandle) { ULONGLONG p = GetKernelProcAddress(GetKernelModuleHandle(L"win32kfull.sys"), "NtUserUnhookWindowsHookEx"); //Pattern B2 05 ?? ?? ?? E8 ?? ?? ?? ?? while ((*(PULONGLONG)p & 0xFF000000FFFF) != 0xE800000005B2) p++; HmValidateHandle = (HMValidateHandleFn)((p & 0xFFFFFFFF00000000) + (DWORD)(p + *(PDWORD)(p + 6) + 10)); } PHOOK Hook = OutputBuffer; ULONG HandleEntryCount = (ULONG)gSharedInfo->psi->cHandleEntries; for (ULONG i = 0; i < HandleEntryCount; i++) { if (gSharedInfo->aheList[i].bType == TYPE_HOOK) { HANDLE ObjectHandle = (HANDLE)(i | (gSharedInfo->aheList[i].wUniq << 0x10)); PHOOK HookObject = HmValidateHandle(ObjectHandle, TYPE_HOOK); if (HookObject) { memcpy(Hook, HookObject, sizeof(HANDLEENTRY)); Hook++; } } } KeUnstackDetachProcess(&ApcState);}

作为题外话,这里再谈一谈tagHOOK中ihmod的用法,按一块三毛钱的说法,若tagHOOK.ihmod!=-1,直接取tagHOOK.ptiHooked->ppi->ahmodLibLoaded [tagHOOK.ihmod]即可获取钩子函数所在模块的基地址,其中ppi是指向_PROCESSINFO的指针。

但是从Win10开始,微软把THREADINFO和PROCESSINFO这两结构体从符号文件移除了,目前可行的检索模块信息的方法是使用Win32kfull!aatomSysLoaded。

这是一个ATOM类型的数组,ATOM所记录的名称即为模块路径,遗憾的是该数组没有导出,只能通过dbghelp.dll定位符号或特征码定位。

获取钩子所在模块的文件路径的伪代码如下:

typedef USHORT ATOM;typedef NTSTATUS(*RtlQueryAtomInAtomTableFn)(PVOID AtomTable, ATOM Atom, PULONG RefCount, PULONG PinCount, PWSTR AtomName, PULONG NameLength);ATOM* aatomSysLoaded = 0;PVOID UserAtomTableHandle = 0;RtlQueryAtomInAtomTableFn RtlQueryAtomInAtomTable = 0;//使用前先挂靠GUI进程NTSTATUS QueryModuleImageFileNameFromHookObject(IN PHOOK HookObject, OUT LPWSTR NameBuffer){ if (!aatomSysLoaded) aatomSysLoaded = (ATOM*)DBGLocateSymbol(L"win32kfull.sys", "aatomSysLoaded"); if (!UserAtomTableHandle) UserAtomTableHandle = (PVOID)GetKernelProcAddress(GetKernelModuleHandle(L"win32kbase.sys"), "UserAtomTableHandle"); if (!RtlQueryAtomInAtomTable) RtlQueryAtomInAtomTable = (RtlQueryAtomInAtomTableFn)GetKernelProcAddress(GetKernelModuleHandle(L"ntoskrnl.exe"), "RtlQueryAtomInAtomTable"); ULONG MaxLength = sizeof(WCHAR) * MAX_PATH; return RtlQueryAtomInAtomTable(UserAtomTableHandle, aatomSysLoaded[HookObject->ihmod], 0, 0, NameBuffer, &MaxLength);}

获取模块文件路径后,可以通过遍历VAD树或PEB->Ldr来获取模块基址,加上tagHOOK.offPfn即为钩子函数的地址,这里以非Wow64进程为例贴出查询钩子函数地址的伪代码:

PVOID GetWinHookFunctionAddress(IN PHOOK HookObject, IN LPWSTR szDllPath){ if (HookObject->ihmod == -1) return HookObject->offPfn; PVOID HookFunctionAddress = 0; _KAPC_STATE ApcState; PKPROCESS Process = IoThreadToProcess(HookObject->ptiHooked->Win32Thread.pEThread); PPEB Peb = (PPEB)PsGetProcessPeb(Process); KeStackAttachProcess(Process, &ApcState); PLIST_ENTRY pFirstLoadOrderFlink = &Peb->Ldr->InLoadOrderModuleList; for (PLIST_ENTRY pListEntry = pFirstLoadOrderFlink->Flink; pListEntry != pFirstLoadOrderFlink; pListEntry = pListEntry->Flink) { LPWSTR FullDllNameBuffer = ((PLDR_DATA_TABLE_ENTRY)pListEntry)->FullDllName.Buffer; if (FullDllNameBuffer && !wcsicmp(FullDllNameBuffer, szDllPath)) { HookFunctionAddress = (PVOID)((ULONGLONG)((PLDR_DATA_TABLE_ENTRY)pListEntry)->DllBase + (ULONGLONG)HookObject->offPfn); break; } } KeUnstackDetachProcess(&ApcState); return HookFunctionAddress;}

本人水平有限,代码写的很粗糙,如有不妥之处欢迎大家批评指正。



- End -



看雪ID:hhkqqs

https://bbs.pediy.com/user-home-805252.htm

  *本文由看雪论坛 hhkqqs 原创,转载请注明来自看雪社区。




推荐文章++++

* Linux Kernel Pwn 学习笔记 (UAF)

* Linux-5.6.6 内核引导

* 崩溃回溯分析

加密壳之ACProtect系列通杀

CVE-2016-0165 Win32k漏洞分析笔记







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



求分享

求点赞

求在看


“阅读原文”一起来充电吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存