4月1日当天晚上,我在瑞星论坛版主区以及相应Q群上联系瑞星人员告知该问题。
4月7日瑞星人员通过论坛短消息联系我,并通过邮箱发送正在测试中的新版HookHelp.sys驱动给我测试。该驱动版本号25.0.0.11,数字签名时间戳2011年4月2日 15:14:14,从这个时间上看瑞星算是挺重视的了,我4月1日告知的内容第二天就进行了修补。
换上这个驱动之后终于可以正常使用了,于是我又分析了一下,不过因为当时此驱动处于测试阶段,详情就没有往这里发。
直到4月14日瑞星终于通过更新推送了这个驱动(同时还更新了HookTdi.sys)。
新版HookHelp.sys驱动的具体做法是在hookhelp.sys注册的CreateProcessNotifyRoutine中,修改了获取子进程映像文件名的方法。
原函数调用位置(hookhelp.sys版本号25.0.0.9):
.text:00011FF6 push 208h ; Size
.text:00011FFB lea ecx, [ebp+ProcessFileNameBuffer]
.text:00012001 push ecx ; Buffer
.text:00012002 mov edx, [ebp+ProcessId]
.text:00012005 push edx ; ProcessId
.text:00012006 mov ecx, ds:FastMutex
.text:0001200C call GetProcessFileName
现函数调用位置:
.text:00011CF6 push 208h ; Size
.text:00011CFB lea ecx, [ebp+ProcessFileNameBuffer]
.text:00011D01 push ecx ; Buffer
.text:00011D02 push 0 ; ImagePathOffsetModify
.text:00011D04 mov edx, [ebp+ProcessId]
.text:00011D07 push edx ; ProcessId
.text:00011D08 mov ecx, ds:FastMutex
.text:00011D0E call GetProcessFileName
可以看到多了一个参数,这个参数在函数中用来修正_RTL_USER_PROCESS_PARAMETERS结构中ImagePathName的偏移,应该是针对另一个版本的系统预留的。
原函数出问题的地方,根源在于它调用NtReadVirtualMemory来读取子进程用户态空间内容,而这个函数在nt内核中其实是未导出给内核态使用的,而只是在SSDT上给用户态使用的,它要求读取内容后写入的目标缓冲区也是用户态缓冲区,因此原函数必须调用ZwAllocateVirtualMemory先为自身进程获取一块位于用户态空间的缓冲区,才能调用NtReadVirtualMemory。而正是对ZwAllocateVirtualMemory的调用内容将获取自身进程的AddressCreationLock,这个动作加上hookhelp.sys的全局性Fast Mutex,就可能导致与另一线程的LoadImageNotifyRoutine之间造成死锁。
为了避免这个死锁,应该放弃使用ZwAllocateVirtualMemory,从而也放弃使用NtReadVirtualMemory这个本来就不是为内核态调用准备的函数,直接使用KeStackAttachProcess切换到目标进程的上下文直接读取。
修改之后的hookhelp.sys,采用了如下方法:
1. 仍然使用ZwOpenProcess获取进程句柄,再使用ZwQueryInformationProcess(ProcessBasicInformation)获取进程信息,从而得到进程PEB地址。这一步与原来是一样的。
2. 第1步的句柄关闭后,再次使用ZwOpenProcess获取进程句柄,这一次的目的是使用ObReferenceObjectByHandle通过这个句柄得到EPROCESS结构指针。
3. 当目标进程不是自身进程时,采用KeStackAttachProcess切换到目标进程的上下文,然后直接读取其中的内容得到ImagePathName
4. 最后KeUnstackDetachProcess切换回来后ObfDereferenceObjece和ZwClose进行扫尾。
总的来说,确实实现了避免原先的ZwAllocateVirtualMemory造成的死锁,初步看起来应该是解决了我提到的问题,至少不会完全卡死了。
不过第1、2步仍稍显累赘,特别是有了ProcessId的情况下,还要先获取Handle再用ObReferenceObjectByHandle来得到EPROCESS,是没有必要的,完全可以用PsLookupProcessByProcessId一步到位(实际上ZwOpenProcess内部也是PsLookupProcessByProcessId再ObOpenObjectByPointer得到Handle)
另外,原函数得到ImagePathName之后还有个格式转换并转大写的操作,现函数没有。