今天这一章,我们就完成HookSSDT中NtCreateThread的代码,在原来的DynamicHook.asm文件的HookNtCreateThread中加入了如下的代码:
HookNtCreateThread proc
local dwNtCreateThreadIndex
local dwNtCreateThreadAddress
local status:NTSTATUS
mov status,STATUS_UNSUCCESSFUL
;获取NtCreateThread服务序号
invoke GetSysCallIndex,addr g_usNtCreateThread
.if eax
mov dwNtCreateThreadIndex,eax
;获取NtCreteThread在内核中的地址
invoke GetSSDTOrigValue,dwNtCreateThreadIndex
.if eax
mov dwNtCreateThreadAddress,eax
invoke DbgPrint,$CTA0("NtCreateThreadindex:%08X,address:%08X"),dwNtCreateThreadIndex,dwNtCreateThreadAddress
invoke HookSSDT,dwNtCreateThreadIndex,offsetHook_NtCreateThread,addr g_lpOldNtCreateThread
.if eax==STATUS_SUCCESS
push dwNtCreateThreadIndex
pop g_dwNtCreateThreadIndex
invoke DbgPrint,$CTA0("Hook NtCreateThreadsuccess")
mov status,STATUS_SUCCESS
.else
mov g_lpOldNtCreateThread,0
.endif
.endif
.endif
mov eax,status
ret
HookNtCreateThread endp
来看一下HookSSDT这个函数的内容:
HookSSDT proc uses edi esi edx dwServiceIndex:DWORD,lpFunc:DWORD,pOldValue:PDWORD
local status:NTSTATUS
local pMdlSystemCall:PMDL
local dwOldData:DWORD
mov status,STATUS_UNSUCCESSFUL
invoke KeGetCurrentIrql
.if eax!=PASSIVE_LEVEL
jmp @F
.endif
mov eax,dwServiceIndex
mov edi,dwordptr [KeServiceDescriptorTable]
assume edi:ptrServiceDescriptorEntry
.if (eax>=[edi].ulNumberOfServices)
jmp @F
.endif
mov edi,[edi].pvSSDTBase
assume edi:nothing
rol eax,2
add edi,eax
M2M dwOldData,dword ptr [edi]
mov edx,dwOldData
.if lpFunc!=edx
invoke IoAllocateMdl,edi,4,0,0,NULL
.if eax
mov esi,pOldValue
.if esi
M2M dword ptr[esi],dwOldData
.endif
mov pMdlSystemCall,eax
invoke MmBuildMdlForNonPagedPool,eax
invoke MmMapLockedPages,pMdlSystemCall,KernelMode
push eax
fastcall InterlockedExchange,eax,lpFunc
pop eax
invoke MmUnmapLockedPages,eax,pMdlSystemCall
invoke IoFreeMdl,pMdlSystemCall
mov status,STATUS_SUCCESS
.endif
.endif
@@:
mov eax,status
ret
HookSSDT endp
为了实现这个函数,添加了许多新的代码和ASM文件,这里我就不再一一举出来,我已经把我们这篇文章到今天为止的代码上传,你可以在这里下载:
http://download.csdn.net/source/3432817
使用时你要保证你安装了VS2003、MASM32和KmdKit。
HookSSDT函数的参数dwServiceIndex:SSDT服务序号,对NtCreateThread来说就是0x35,上一章我们已经获取到了;lpFunc:钩子函数的地址;pOldValue:一个指针,用来返回原来SSDT函数的地址,这个地址我们必须保存,因为我们的钩子函数在完成处理后必须回到原函数,而且在我们的驱动卸载时必须用这个地址去恢复原来的SSDT:
DriverUnload proc pDriverObject:PDRIVER_OBJECT
.if g_dwNtCreateThreadIndex&& g_lpOldNtCreateThread
invoke UnHookSSDT,g_dwNtCreateThreadIndex,g_lpOldNtCreateThread
invoke DbgPrint,$CTA0("UnhookNtCreateThread")
.endif
invoke DbgPrint,$CTA0("Driverunload")
invoke IoDeleteSymbolicLink,addrg_usSymbolicLinkName
mov eax,pDriverObject
invoke IoDeleteDevice,(DRIVER_OBJECTptr [eax]).DeviceObject
ret
DriverUnload endp
来具体看下HookSSDT中的代码,原理也很简单:在SSDT中用我们钩子函数的地址去替换原来表中第dwServiceIndex项的内容,并将这一项中原来的值回传给pOldValue,这里我们使用了InterlockedExchange这个函数,用来保证在替换这个内容时是原子操作。
这里我就来解释一下为什么我极不推荐大家对SSDT使用Inline Hook。绝大部分通常的Inline Hook都在函数首使用一条Jmp指令跳到钩子函数。第一点麻烦的是Jmp指令覆盖了原函数5字节,那么你必须在钩子函数的最后实现大于等于5字节的最小字节的指令(不晓得我这样的表述清不清楚)并跳到原函数正确的位置去继续执行。当然你也可以简化一点写硬编码,但这也是我不推荐的。那么你至少就要用代码实现计算opcode的长度。
这还不是最重要的,重要的是这些操作是在内核中,内核代码与用户代码不同,用户代码的线程有限,而且线程切换并不频繁,你甚至可以在做Hook的时候先挂起所有线程,在内核中因为有中断等因素线程切换得非常频繁,况且中断又不能挂起。你用memcpy向目标地址拷贝5字节代码时,你并不能保证在完成拷贝前没有任何一个线程切换去执行目标函数。若有,必然立即蓝屏。不幸的是,根据经验,这样的几率是非常大的。当然,你又可以用:
mov eax,cr0
and eax,7fffffffh
mov cr0,eax
这样的代码来关闭分页。如此一来,情况好一些了(关闭内存分页本身会对依赖于分页机制的Windows内核造成影响),但如果是多处理器或多处理器呢?上面的代码仅关闭了当前处理器的内存分页,如果有线程此时此刻在其它处理器来调用目标函数,又立即蓝屏。
KeSetAffinityThread这个函数可以设置线程跟CPU的亲和性,但根据观察来看,并不能立即将线程切换到到指定CPU。
后来我在与某网络牛人讨论这个问题时,他提出可以通过Hook IDT表接管某中断(例如INT 1),在需要Inline Hook的函数起始位置直接用原子操作写入INT 1代码即可。但问题又回到前面,你必须保证接管所有CPU的INT 1中断。
好了,又扯远了。在内核中,可以用PsSetCreateProcessNotifyRoutine这个函数来监视进程的创建,但这不是我们要的,因为通过这个函数注册回调来监视进程,我们已经失去了在进程中做手脚的机会。看下我们的钩子函数:
Hook_NtCreateThread proc ThreadHandle:PHANDLE,DesiredAccess:DWORD,ObjectAttributes:POBJECT_ATTRIBUTES,ProcessHandle:HANDLE,ClientId:PCLIENT_ID,ThreadContext:PCONTEXT,InitialTeb:PVOID,CreateSuspended:DWORD
pushad
pushfd
.if ThreadContext&&CreateSuspended&&ProcessHandle&&ProcessHandle!=-1
invoke DbgPrint,$CTA0("Newprocess")
.endif
popfd
popad
invoke g_lpOldNtCreateThread,ThreadHandle,DesiredAccess,ObjectAttributes,ProcessHandle,ClientId,ThreadContext,InitialTeb,CreateSuspended
ret
Hook_NtCreateThread endp
同样也可以监视到进程创建,同时,在这一时刻,我们有该进程的第一个线程的一些重要信息,并且,在我们钩子函数处理完之前,进程是无法启动的,因此,我们可以在这里做很多事情。需要注意的是,在钩子函数中,不应过多的做处理,特别是字符串之类的操作,内核中很多字符串操作对IRQL是有要求的。比较好的做法是在调用函数前先用KeGetCurrentIrql检查一下(其它的代码因为自己知道自己在什么IRQL级别下,所以都不用检查)。同时我们Hook了NtCreateThread,如果你的钩子代码中有什么地方的调用深入下去又调到NtCreateThread,那又死。所以还是得非常注意。
好了,今天就写到这里吧。测试一下今天的成果:
下一章我会讲如何将我们的代码注入到新进程中并最先执行。