(转)SSDT HOOK拦截远线程的创建(上)
http://nokyo.blogbus.com/logs/37787913.html
在ring3的API HOOK中,怎样迫使目标进程调用我们的傀儡DLL是我们非常重视的一个问题。在多数情况下,我们都喜欢使用CreateRemoteThread在目标进程中创建一个远程线程来迫使它加载我们的DLL。
因为CreateRemoteThread的使用方法并不复杂,而且与其他方式相比,它可以称得上是一种相当“优雅”的做法。各种因素的汇集就导致了这种方法的泛滥,致使很多具备主动防御或行为监控的安全软件都加强了对这个函数的照顾。
最近在自己的毕业设计中也要用到这个功能,阻止一些简单地调用CreateRemoteThread注入到我们要保护的进程中,就对这个问题稍微学习了下。
根据我们的思维惯性,很显然我们应该HOOK CreateRemoteProcess函数,这在ring3下非常容易实现,但是如果这样的话我们的保护也太没有强度可言了。正好最近正在学习SSDT HOOK,就使用这个方法吧,算是在实战中磨炼自己,吼吼~~
第一部分:SSDT HOOK的使用方法
关于什么是SSDT等基本概念我就不再赘述了,网上有很多文章,而且黑防以前的杂志上也有不少详细介绍,这里我推荐一篇李马的《城里城外看SSDT》,确实不错。我一开始是修改《Rootkits——Windows内核的安全防护》关于SSDT的示例代码,它给出了一个挂钩ZwQuerySystemInformation来隐藏进程的示例。
很不幸的是,当我照本宣科地把ZwQuerySystemInformation修改成ZwCreateThread(第二部分会介绍,这里只要知道CreateRemoteThread是最终调用ZwCreateThread)后,编译器报错:“error LNK2019: unresolved external symbol __imp__ZwCreateThread@32referenced in function _DriverEntry@8”。这个错误令我诧异了很久,在尝试解决问题的过程中,偶然发现了出现问题的原因,原来ZwCreateThread函数没有被ntoskrnl.exe导出(使用depends看看就知道了)。
与之同时,我们在函数中用到了几个宏,它们的详细定义如下:
#define SYSTEMSERVICE(_Function) \ KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_Function+1)] #define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1) #define HOOK_SYSCALL(_Function, _Hook, _Orig ) \ _Orig = (PVOID) InterlockedExchange( (PLONG) &MappedSystemCallTable\ [SYSCALL_INDEX(_Function)], (LONG) _Hook) #define UNHOOK_SYSCALL(_Function, _Hook, _Orig) InterlockedExchange((PLONG)\ &MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook)
而SYSTEMSERVICE宏是采用由ntoskrnl.exe导出的Zw*函数地址,并返回对应的Nt*函数在SSDT中的地址,SYSCALL_INDEX采用Zw*函数地址并返回它在SSDT中相应的索引号。
由于ZwCreateThread没有被ntoskrnl.exe导出,所以这时我们就无法直接使用上述的宏,只能另想他法。
这时候最常规的方法就是从ntdll中动态获取地址,不过我不想那么麻烦。昨天在群里问了下,iceboy大牛给出了一种猥琐的方法,因为ZwCreateThread在SSDT中的前后两个函数都被导出了,分别是ZwCreateSymbolicLinkObject和ZwCreateTimer。
这时候我们就可以分别获取这两个函数的索引,判断它们的索引之差是否为2,如果是,则它们中间的函数就是ZwCreateThread,代码如下所示:
a = SYSCALL_INDEX(ZwCreateSymbolicLinkObject); b = SYSCALL_INDEX(ZwCreateTimer); c = b - a; KdPrint(("[nokyo] FunctionIndex = %d -> %d", a, c)); if (c != 2) { c = 0; return STATUS_UNSUCCESSFUL; } ...... HOOK_ON(c, New_ZwCreateThread);
当然,这时候开启和关闭HOOK的宏也要进行细微的修改,如下所示:
#define HOOK_ON(_FuncIndex, _New) (PVOID)InterlockedExchange\ ((PLONG)&MappedSystemCallTable[_FuncIndex], (LONG)_New) #define HOOK_OFF(_FuncIndex, _Old) InterlockedExchange\ ((PLONG)&MappedSystemCallTable[_FuncIndex], (LONG)_Old)
不过我用起这个方法还是有问题,主要是不会获取原函数的地址并保存起来,这样虽然能够开启HOOK,但驱动卸载的时候却没法还原(事实上是可以的,不过当时我没有想到什么简单的方法,其实就是直接读取SSDT中的内容,把它当成指针就行了);而且我获取到ZwCreateTimer的索引号总是错误的,很不解。
问题放的时间久了还没解决,心里难免有点急躁,到处找资料(偶可是在硬盘上一个文档一个文档地翻,不能上网真苦啊)总算找到了一点信息。
硬盘的某个角落里静静地躺着adly以前给的一个SSDT HOOK ZwCreateFile的例子,正好符合我的要求,赶紧拿来用用。
然后就是劳力活儿,经过将近一个小时的忙活,总算成功挂钩了ZwCreateThread函数,效果如图1所示:
整体代码如下所示:
#define CREATETHREAD_INDEX 0x35 // 硬编码,不保证在其他地方也相同 // 安装 HOOK // 关闭写保护 __asm { cli ; mov eax, cr0 and eax, ~0x10000 mov cr0, eax } // 保存原始值 (ULONG)Old_ZwCreateThread = *((PULONG)(KeServiceDescriptorTable.\ ServiceTableBase) + (ULONG)CREATETHREAD_INDEX); // 指向新函数 *((PULONG)(KeServiceDescriptorTable.ServiceTableBase) + \ (ULONG)CREATETHREAD_INDEX) = (ULONG)New_ZwCreateThread; // 恢复写保护 __asm { mov eax, cr0 or eax, 0x10000 mov cr0, eax sti ; }
在卸载的时候使用以下的代码恢复,这里限于篇幅我省略了关闭/恢复写保护的代码,在程序中可不能省略,否则会BSOD滴:
// 恢复原函数 *((PULONG)(KeServiceDescriptorTable.ServiceTableBase) + \ (ULONG)CREATETHREAD_INDEX) = (ULONG)Old_ZwCreateThread;
第二部分:挂钩CreateRemoteThread的几个问题
在内核中如何挂钩CreateRemoteThread函数呢,因为我们必须知道它没有与之向对应的ZwCreateRemoteThread函数,我搜遍了硬盘上所有的资料,终于在《w2k native》里面搜到了有用的东西,原来它和CreateThread一样最终都是调用ZwCreateThread实现的。
这样我们的问题就转化成了如何使用SSDT HOOK来挂钩ZwCreateThread函数,这个函数的原型如下所示:
NTSYSAPI NTSTATUS NTAPI ZwCreateThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN HANDLE ProcessHandle,
OUT PCLIENT_ID ClientId,
IN PCONTEXT ThreadContext,
IN PUSER_STACK UserStack,
IN BOOLEAN CreateSuspended);
很明显,ZwCreateThread的第四个参数ProcessHandle指定了线程将要被创建的目标进程(在第三部分我们将介绍如何通过进程句柄获取PID等信息)。
可是又有新问题出现了,虽然我们可以知道线程将要被创建的目标进程,但由于CreateThread和CreateRemoteThread都是最终调用的ZwCreateThread,因此我们还必须知道当前被创建的线程是否为远程线程,换言之,我们需要知道是哪个进程正在调用相关函数才能够进行一些验证和判断。
实际上,驱动程序的不同例程实际是运行在不同的进程空间中的,比如说DriverEntry和Unload例程都是运行在PID为4的system进程中;而其他例程在运行的时候也会被映射到不同的进程空间。
既然驱动程序的不同例程可以运行在不同的进程空间内,那么我们是否可以通过调用GetCurrentProcessId之类的函数来得到当前进程呢?由于我们是在内核模式,所以可以调用函数PsGetCurrentProcessId来测试。
如下所示是我编写的测试函数代码:
VOID GetCurrentProcessInfo(LPCTSTR lpFuncName) { ULONG pid; PEPROCESS EProcess; PUCHAR ImageFilePath; ULONG dwNameOffset; pid = (ULONG)PsGetCurrentProcessId(); dwNameOffset = GetPlantformDependentInfo(); PsLookupProcessByProcessId(pid, &EProcess); ImageFilePath = (PUCHAR)((LPTSTR)EProcess + dwNameOffset); KdPrint(("[nokyo] [%s] %d : %s", lpFuncName, pid, ImageFilePath)); }
上述测试函数的唯一参数是函数名称字符串,它的目的是为了便于区分,比如说我们要在DriverEntry例程中调用该函数就传入参数“DriverEntry”。
我在挂钩函数New_ZwCreateThread中调用该函数并传入参数“New_CreateThread”,然后我专门编写了一个程序调用CreateRemoteThread向所有进程中注入DLL,此时驱动程序记录到的结果如图2所示:
由图2可以看出,此时我们的演示程序“test.exe”大量调用了该函数,目标进程涵盖了系统的全部进程,这证明了我们的思路是可行的。这样,我们就可以分别获得目标进程的PID和当前调用进程的PID,如果它们不相等则说明建立的是远线程。