tangptr@126.com 发表于 2015-7-28 13:13:22

TP的学习笔记:稳定安全的Call Hook组件以及对二次跳转的学习

记得上次我发了个帖子叫《Call Hook KeInsertQueueApc总蓝屏》的帖子,地址http://www.m5home.com/bbs/thread-8517-1-1.html
这段时间重写了一遍黑客防线上胡文亮的文章《函数CALL地址替换实现深度钩子》,发现这里面有一些小BUG的存在。和Head Inline Hook一样,我认为Call Hook也需要有锁定页面,提升中断请求级并使用自旋锁。由于Call Hook是替换四个字节的Inline Hook,所以最好使用InterlockedExchange函数来替换值,而不是用memcpy或者*(PULONG)(Address)=0xXXXXXXXX的方式暴力修改。
本文以Hook NtOpenProcess和NtOpenThread为例,其替换的地址是PsLookupProcessByProcessId和PsLookupThreadByThreadId,主要是因为XueTr(现在的PCHunter)的自我保护是替换了ObOpenObjectByPointer,我们替换这个地址没意思。
首先先讲一下经过我修改之后的Call Hook的过程:
在这个函数里搜索每一个call指令,若call指令后面跟着的相对地址是我们要替换的地址,那么就开始HOOK操作。
Hook操作的流程是先用MDL锁定页面,然后提升中断请求级到DPC级别,在这个级别下申请并使用自旋锁,然后计算call指令后的相对地址,并用InterlockedExchange函数替换地址,替换完成后,释放自旋锁,再降低中断请求级,解锁页面,释放MDL就好了。
代码如下:
BOOL CallAddrHook(PVOID StartAddr,PVOID OldAddr,PVOID NewAddr)
{
        PUCHAR cPtr, pOpcode;
        ULONG Length,Tmp;
        PMDL pMdl;
        for (cPtr=StartAddr;(ULONG)cPtr<(ULONG)StartAddr+0x1000;cPtr += Length)
        {
                Length = SizeOfCode(cPtr, &pOpcode);//计算当前指令长度
                if (!Length) break;
                if (Length ==5 && *cPtr==0xE8)// 当前长度5 且第一字节为E8
                {//因为CALL用的是相对偏移所以我们还需要进行计算相对偏移
                        if ( (ULONG)OldAddr-(ULONG)cPtr-5 == *(PULONG)(cPtr+1)) //判断当前是否为OldAddr的CALL相对地址
                        {
                                Tmp=(ULONG)NewAddr-(ULONG)cPtr-5;//我们的CALL地址相对偏移
                                pMdl=IoAllocateMdl((PVOID)(cPtr+1),4,FALSE,FALSE,NULL);
                                if(pMdl)
                                {
                                        __try
                                        {
                                                MmProbeAndLockPages(pMdl,KernelMode,IoWriteAccess);
                                        }
                                        __except(EXCEPTION_EXECUTE_HANDLER)
                                        {
                                                IoFreeMdl(pMdl);
                                        }
                                        //进行HOOK操作
                                        f_oldirql=KeRaiseIrqlToDpcLevel();
                                        KeInitializeSpinLock(&f_spinlock);
                                        KeAcquireSpinLockAtDpcLevel(&f_spinlock);
                                        __asm
                                        {   
                                                cli
                                                moveax,cr0
                                                andeax,not 10000h
                                                movcr0,eax
                                        }
                                        InterlockedExchange((PULONG)(cPtr+1),Tmp);//直接替换为我们的FAKE函数地址   
                                        __asm
                                        {   
                                                moveax,cr0
                                                or   eax,10000h
                                                movcr0,eax
                                                sti
                                        }
                                        KeReleaseSpinLockFromDpcLevel(&f_spinlock);
                                        KeLowerIrql(f_oldirql);
                                        IoFreeMdl(pMdl);
                                        return TRUE;
                                }
                        }
                }
        }
        return FALSE;
}

而UnHook的时候,把原本OldAddr改成NewAddr就可以了。
接下来,是演示Hook和Unhook的过程,这里用到了二次跳转:
void StartHook()
{
        //deal with NTKERNELAPI
        UNICODE_STRING uniFuncName1;
        UNICODE_STRING uniFuncName2;
        UNICODE_STRING uniFuncName3;
        UNICODE_STRING uniFuncName4;
        RtlInitUnicodeString(&uniFuncName1,L"PsLookupProcessByProcessId");
        RtlInitUnicodeString(&uniFuncName2,L"PsLookupThreadByThreadId");
        RtlInitUnicodeString(&uniFuncName3,L"ZwOpenProcess");
        RtlInitUnicodeString(&uniFuncName4,L"ZwOpenThread");
        PsLookupProcessByProcessId=MmGetSystemRoutineAddress(&uniFuncName1);
        PsLookupThreadByThreadId=MmGetSystemRoutineAddress(&uniFuncName2);
        ZwOpenProcess=MmGetSystemRoutineAddress(&uniFuncName3);
        ZwOpenThread=MmGetSystemRoutineAddress(&uniFuncName4);
        //deal with SSDT Function
        RtlCopyMemory(&IndexOf_NtOpenProcess,(PVOID)((ULONG)ZwOpenProcess+1),4);
        RtlCopyMemory(&IndexOf_NtOpenThread,(PVOID)((ULONG)ZwOpenThread+1),4);
        pNtOpenProcess=(PVOID)GetSSDTCurAddr(IndexOf_NtOpenProcess);
        pNtOpenThread=(PVOID)GetSSDTCurAddr(IndexOf_NtOpenThread);
        //deal with second jump
        *(PULONG)((ULONG)&SjBytes+1)=(ULONG)fake_PsLookupProcessByProcessId;
        Sj1=ExAllocatePool(NonPagedPool,6);
        RtlCopyMemory(Sj1,&SjBytes,6);
        *(PULONG)((ULONG)&SjBytes+1)=(ULONG)fake_PsLookupThreadByThreadId;
        Sj2=ExAllocatePool(NonPagedPool,6);
        RtlCopyMemory(Sj2,&SjBytes,6);
        //Start to Hook
        CallAddrHook(pNtOpenProcess,PsLookupProcessByProcessId,Sj1);
        CallAddrHook(pNtOpenThread,PsLookupThreadByThreadId,Sj2);
}

void StopHook()
{
        CallAddrHook(pNtOpenProcess,Sj1,PsLookupProcessByProcessId);
        CallAddrHook(pNtOpenThread,Sj2,PsLookupThreadByThreadId);
        //deal with second jump
        ExFreePool(Sj1);
        ExFreePool(Sj2);
}
我刚看到二次跳转这玩意的时候觉得好牛逼。。。现在觉得其实也就那么一回事,这里的二次跳转原理很简单,申请一段六字节的NonPagedPool,然后在第一个字节写上0x68,第六个字节写上0xC3,第二到第五个字节写上伪函数的绝对地址,它就是一个push xxxxxxxx ret方式的跳转!当然也可以在第一和第二个字节写上0xFF和0x25,第三到第六个字节写上绝对地址,也就是一个jmp dword ptr方式跳转。
伪函数和上次的TP学习笔记的差不多,代码如下:
NTSTATUS fake_PsLookupProcessByProcessId(IN ULONG ProcessId,OUT PEPROCESS *Process)
{
        NTSTATUS st;
        PEPROCESS txps;
        st=PsLookupProcessByProcessId(ProcessId,&txps);
        if(txps==ProtectedProcess)
        {
                st=0xC0000022;
        }
        else
        {
                st=PsLookupProcessByProcessId(ProcessId,Process);
        }
        return st;
}

NTSTATUS fake_PsLookupThreadByThreadId(IN ULONG ThreadId,OUT PETHREAD *Thread)
{
        NTSTATUS st;
        PETHREAD txtd;
        PEPROCESS txps;
        st=PsLookupThreadByThreadId(ThreadId,&txtd);
        txps=IoThreadToProcess(txtd);
        if(txps==ProtectedProcess)
        {
                st=0xC0000022;
        }
        else
        {
                st=PsLookupThreadByThreadId(ThreadId,Thread);
        }
        return st;
}
效果很不错,在Windows XP和Windows Server 2003上均测试通过,若要在Windows 7等NT6的操作系统上使用,请Call Hook PsOpenProcess和PsOpenThread的PsLookupProcessByProcessId和PsLookupThreadByThreadId。
测试效果如图所示:
XP的测试效果:

2003的测试效果:

PCHunter还是不错的,连跳的第二个地址都检测到了。。。
p.s:本文所带的附件描述摘自Hovi.Delphic的帖子,由于本文是安利给大家的,不设权限,不设水晶币{:soso_e113:}

Tesla.Angela 发表于 2015-7-28 15:25:46

过PCH的二次跳转检测不难。第二次跳转别直接跳,弄点无意义代码类似于NOP、MOV EDI,EDI之类的再跳。

tangptr@126.com 发表于 2015-7-28 17:03:45

没有什么卵用啊,该检测出来的还是检测出来了,我都挂了100个NOP了。。。

Tesla.Angela 发表于 2015-8-3 08:47:57

光NOP没用,要弄点垃圾代码。比如mov edi,edi之类的。
页: [1]
查看完整版本: TP的学习笔记:稳定安全的Call Hook组件以及对二次跳转的学习