tangptr@126.com 发表于 2015-7-25 14:45:52

TP的学习笔记:实现通用且较为稳定安全的Head Inline Hook模块

记得从2011年开始学Win32内核的时候,看到的不少和Inline Hook有关的文章。要么直接写HookXXX完事,搞得只能Hook这个函数,让小白改不了,比如在《VB小子玩转驱动程序》系列帖子的第九章的Hook ObReferenceObjectByHandle的附件;要么就是稳定性太差,Hook某些调用极其频繁的函数(比如ObOpenObjectByPointer)的时候会蓝屏。因此,本人在此发表一个较为安全的Inline Hook模块,以挂钩PsLookupProcessByProcess+PsLookupThreadByThreadId为例,制作进程保护器(进程保护器效果立竿见影,上次的MDL映射方式貌似只支持NT5)。
由于Inline Hook的基本原理是修改函数自身,而非地址替换,故SSDT Hook以及EAT/IAT Hook均不是Inline Hook,但属于地址替换的Call Hook是修改函数中某个call指令的后四个字节,所以Call Hook是Inline Hook。
本文所示范的是Head Inline Hook。Head Inline Hook有很多方式实现,可以是修改成jmp xxxxxxxx的方式,也就是E9 XXXXXXXX,它占五个字节,这是最常见的方式;也可以是修改成push xxxxxxxx ret的方式,即68 XXXXXXXX C3,它占六个字节,一般不这么搞;还有就是修改成jmp dword ptr xxxxxxxx的方式,也就是FF 25 XXXXXXXX,它也占六个字节。由于绝大多数的Win32下的内核函数头五个字节均是8B FF 55 8B EC,因此如果不是去挂钩奇怪的函数,比如KiSwapContext,其头五个字节是83 EC 10 89 5C,并不是完整的两条指令,故Hook之后会导致WinDbg显示异常的情况。由于其头七个字节是完整的两个指令,故要Hook这个函数并使其在WinDbg中显示不异常,可以用jmp dword ptr xxxxxxxx改掉前六个字节,第七个字节填NOP就可以使其显示不异常了。
首先先讲一下我们的Hook过程:首先利用反汇编引擎取得要Patch的字节个数,然后申请MDL并锁定页面,提升中断请求级到DPC级别,在该中断请求级中申请并使用自旋锁防止关闭中断的时候蓝屏,随后关闭中断并暴力修改CR0寄存器的值,使写入保护失效,接着修改函数头五个字节为jmp xxxxxxxx,再改回CR0寄存器的值并开启中断,使得写入保护生效。然后在DPC中断请求级下释放掉自旋锁,并降低中断请求级,最后一步便是解锁页面并释放MDL。
代码如下:
void Hook(PVOID Func,PVOID NewFunc,PVOID ProxyFunc)
{
        PMDL pMdl;
        ULONG PatchSize;
        BYTE g_HookCode={0xE9,0,0,0,0};//Relative Jump
        BYTE Jmp_Orig_Code={0xEA,0,0,0,0,0x08,0x00 }; //Absolute Address Jump
        PatchSize=GetPatchSize(Func,5);//Get The Number Of Bytes To Patch
        RtlCopyMemory((PBYTE)ProxyFunc,(PBYTE)Func,PatchSize);//Set The Head Of Original Function
        *((PULONG)(Jmp_Orig_Code+1))=(ULONG)((PBYTE)Func+PatchSize );//Address Of Original Function + N
        RtlCopyMemory((PBYTE)ProxyFunc+PatchSize,Jmp_Orig_Code,7);//Absolute Jump
        *((ULONG*)(g_HookCode+1))=(ULONG)NewFunc-(ULONG)Func-5;//Calculate The Relative Jump
        pMdl=IoAllocateMdl(Func,5,FALSE,FALSE,NULL);//Allocate MDL
        if(pMdl)
        {
                __try
                {
                        //Lock Page
                        MmProbeAndLockPages(pMdl,KernelMode,IoWriteAccess);
                }
                __except(EXCEPTION_EXECUTE_HANDLER)
                {
                        IoFreeMdl(pMdl);
                }
                //Raise The Interrupt Request Level
                f_oldirql=KeRaiseIrqlToDpcLevel();
                //Initialize And Acquire Spinlock
                KeInitializeSpinLock(&f_spinlock);
                KeAcquireSpinLockAtDpcLevel(&f_spinlock);
                //Close Write-Protection
                _asm
                {
                        cli            
                        mov eax, cr0   
                        and eax, not 10000h
                        mov cr0, eax   
                }
                //Hook Operation
                RtlCopyMemory(Func, g_HookCode, 5);
                //Reopen Write-Protection
                _asm
                {
                        mov eax, cr0   
                        oreax, 10000h            
                        mov cr0, eax            
                        sti                  
                }
                //Release Spinlock
                KeReleaseSpinLockFromDpcLevel(&f_spinlock);
                //Lower Interupt Request Level
                KeLowerIrql(f_oldirql);
                //Unlock Pages
                MmUnlockPages(pMdl);
                //Free MDL
                IoFreeMdl(pMdl);
        }
}
卸载钩子的时候过程相似,也需要锁定页面,提升中断请求级并使用自旋锁,代码如下:
void UnHook(PVOID Func,PVOID ProxyFunc)
{
        PMDL pMdl;
        //Allocate MDL
        pMdl=IoAllocateMdl(Func,5,FALSE,FALSE,NULL);
        if(pMdl)
        {
                __try
                {
                        //Lock Pages
                        MmProbeAndLockPages(pMdl,KernelMode,IoWriteAccess);
                }
                __except(EXCEPTION_EXECUTE_HANDLER)
                {
                        IoFreeMdl(pMdl);
                }
                //Raise Interupt Request Level
                f_oldirql=KeRaiseIrqlToDpcLevel();
                //Initialize And Acquire Spinlock
                KeInitializeSpinLock(&f_spinlock);
                KeAcquireSpinLockAtDpcLevel(&f_spinlock);
                //Close Write-Protection
                _asm
                {
                        cli            
                        mov eax, cr0   
                        and eax, not 10000h
                        mov cr0, eax   
                }
                //Unhook Operation
                RtlCopyMemory(Func,ProxyFunc,5);
                //Reopen Write-Protection
                _asm
                {
                        mov eax, cr0   
                        oreax, 10000h            
                        mov cr0, eax            
                        sti                  
                }
                //Release Spinlock
                KeReleaseSpinLockFromDpcLevel(&f_spinlock);
                //Lower Interupt Request Level
                KeLowerIrql(f_oldirql);
                //Unlock Pages
                MmUnlockPages(pMdl);
                //Free MDL
                IoFreeMdl(pMdl);
        }
}
光有上述代码还不行,我们还要写个StartHook和StopHook函数去调用Hook和UnHook函数
void StartHook()
{
        //deal with the NTKERNELAPI
        UNICODE_STRING uniFuncName1;
        UNICODE_STRING uniFuncName2;
        RtlInitUnicodeString(&uniFuncName1,L"PsLookupProcessByProcessId");
        RtlInitUnicodeString(&uniFuncName2,L"PsLookupThreadByThreadId");
        PsLookupProcessByProcessId=MmGetSystemRoutineAddress(&uniFuncName1);
        PsLookupThreadByThreadId=MmGetSystemRoutineAddress(&uniFuncName2);
        //Initialize Proxy Function
        Old_PsLookupProcessByProcessId=ExAllocatePool(NonPagedPool,20);
        Old_PsLookupThreadByThreadId=ExAllocatePool(NonPagedPool,20);
        //NOP The Proxy Function
        memset(Old_PsLookupProcessByProcessId,0x90,20);
        memset(Old_PsLookupThreadByThreadId,0x90,20);
        //Start To Hook
        Hook(PsLookupProcessByProcessId,fake_PsLookupProcessByProcessId,Old_PsLookupProcessByProcessId);
        Hook(PsLookupThreadByThreadId,fake_PsLookupThreadByThreadId,Old_PsLookupThreadByThreadId);
}
void StopHook()
{
        //Unhook
        UnHook(PsLookupProcessByProcessId,Old_PsLookupProcessByProcessId);
        UnHook(PsLookupThreadByThreadId,Old_PsLookupThreadByThreadId);
        //Release the memory
        ExFreePool(Old_PsLookupProcessByProcessId);
        ExFreePool(Old_PsLookupThreadByThreadId);
}
因此若是想Hook新的函数,只要向里面添加项目即可。
写伪函数就没有什么悬念了,在PsLookupProcessByProcessId的伪函数里过滤掉EPROCESS,在PsLookupThreadByThreadId中过滤掉IoThreadToProcess(ETHREAD)就可以了(当然也可以过滤PID和TID),代码如下:
NTSTATUS fake_PsLookupProcessByProcessId(IN ULONG ProcessId,OUT PEPROCESS *Process)
{
        NTSTATUS st;
        PEPROCESS txps;
        st=Old_PsLookupProcessByProcessId(ProcessId,&txps);
        if(txps==ProtectedProcess)
        {
                st=0xC0000022;
        }
        else
        {
                st=Old_PsLookupProcessByProcessId(ProcessId,Process);
        }
        return st;
}

NTSTATUS fake_PsLookupThreadByThreadId(IN ULONG ThreadId,OUT PETHREAD *Thread)
{
        NTSTATUS st;
        PETHREAD txtd;
        PEPROCESS txps;
        st=Old_PsLookupThreadByThreadId(ThreadId,&txtd);
        txps=IoThreadToProcess(txtd);
        if(txps==ProtectedProcess)
        {
                st=0xC0000022;
        }
        else
        {
                st=Old_PsLookupThreadByThreadId(ThreadId,Thread);
        }
        return st;
}
效果很不错,在Windows XP,Windows Server 2003,Windows 7,Windows 8中均测试通过。如图所示:
Windows XP的测试效果:

Windows Server 2003的测试效果:

Windows 7的测试效果:

Windows 8的测试效果:

我个人认为Hook这两个函数是非常出人意料的,因为在黑客防线2010年中的《模拟实现NT通用PspTerminateProcess》里,利用PsLookupThreadByThreadId+IoThreadToProcess来枚举线程,故该文的代码对本进程保护无效。
另外说八点:
一、本文的附件防不了复制句柄取得句柄的方式杀进程,由于NtDuplicateObject是导出函数,大家可以Hook这个函数。
二、NT6操作系统里有个函数叫ZwGetNextProcess,此函数可以枚举系统内所有进程,并以句柄的形式返回,故这种方式也防不了。要防止这种方式杀进程,可以Hook NtGetNextProcess,也可以Hook ObOpenObjectByPointer,还可以摘除活动进程链表。
三、利用线程消息洪水也可以杀死本文的进程保护。这里需要Hook SSSDT表中的NtUserPostThreadMessage。
四、本文的通用Hook模块,不解决罕见函数头的问题。故用本文的Hook通用模块Hook像KiSwapContext之类的函数虽然有效且不会蓝屏,但WinDbg会显示异常。
五、可以改进《模拟实现NT通用PspTerminateProcess》的代码,绕过PsLookupProcessByProcessId可以用枚举句柄表的方式实现,而绕过PsLookupThreadByThreadId的钩子还是中规中矩的遍历ThreadListHead吧。
六、本文的附件包括了《模拟实现NT通用PspTerminateProcess》原文的代码。
七、附件的描述摘自Hovi.Delphic的帖子
八、TP的学习笔记系列帖子均是安利给大家的,既不设置阅读权限,也不设置水晶币。

Tesla.Angela 发表于 2015-7-25 23:10:06

这可以算是新版的“NT通用进程保护”了。

radarhp 发表于 2015-8-22 14:06:07

下载代码不回帖是一种很欠扁的行为,赤裸裸的威胁阿

tangptr@126.com 发表于 2015-8-22 15:01:03

我也只是口头上说说,真正赤裸裸的威胁是那种“想看隐藏内容请回复”之类的。

376408384 发表于 2023-12-7 00:09:54

x64 可以这样玩吗?
页: [1]
查看完整版本: TP的学习笔记:实现通用且较为稳定安全的Head Inline Hook模块