4

[原创]QQ电脑管家中的 Hook 过程分析

Fypher 2012-2-10 15:29 36570
QQ电脑管家中的 Hook 过程分析

作者:Fypher

最近对QQ电脑管家中的TsFltMgr.sys做了些分析,发现不少有用的东西,这里跟大家分享一下 TsFltMgr 对 KiFastCallEntry 的 Hook 过程。

虽然整个过程中并没有新的技术,但毕竟是面向市场的产品,从兼容性、安全性出发,工作过程中需要把问题考虑全面一些、处理问题时尽量细致,这些都是值得学习的地方。

我们从这个函数开始:

BOOLEAN StartWork()
{
    ULONG ulOsVersion;
    
    if (InitSafeBootMode)
        return FALSE;

    ulOsVersion = GetOsVersion();
    if (ulOsVersion !=  OS_VERSION_ERROR) 
    {
        ULONG ulKiFastCallEntry_Detour;

        if (!InitGlobalVars())
            return FALSE;

        if (!InitFakeSysCallTable())
            return FASLE;
    
        if ( ulOsVersion >= OS_VERSION_VISTA )
            ulKiFastCallEntry_Detour = (ULONG)KiFastCallEntry_Detour_AfterVista;
        else
            ulKiFastCallEntry_Detour = (ULONG)KiFastCallEntry_Detour_BeforeVista;

        return Hook(g_ulHookPoint, ulKiFastCallEntry_Detour);
    }

    return FALSE;
}


说明一下,在这篇文章中,我贴出的代码剔除了真实的 TsFltMgr 中跟 Hook 过程关系不紧密的部分,为了方便阅读,我还会重新组织了一些函数调用关系。但我会保持与 Hook 相关的流程同 TsFltMgr 一致。

在 StartWork 中,先判断系统是否运行在安全模式中(为了抢占先机,TsFltMgr 以boot方式启动),是的话就不 Hook,再根据系统的版本号选择 Detour 函数(GetOsVersion 通过 BuildNumber 来判断版本)。为什么要选择Detour函数?因为在 Vista 前和 Vista 后,KiFastCallEntry 的流程有点小区别(ebx 和 edx 的问题,自己去看看就明白了)。

InitFakeSysCallTable 是初始化一张 FakeSyscallTable 表,想知道这个表是干啥的可以看看我的上一篇文章《QQ电脑管家中的TsFltMgr Hook框架分析》:http://bbs.pediy.com/showthread.php?t=146156

InitGlobalVars 是初始化一些全局变量:

BOOLEAN InitGlobalVars() 
{
    ……
    // InitRegKeys();

    pSysMods = (BYTE *)GetSystemModules();    // 这个函数貌似有点小 bug
    pModInfo = (PSYSTEM_MODULE_INFORMATION)(pSysMods + 4);
    
    g_KernelBase = pModInfo->Base;
    g_KernelSize = pModInfo->Size;
    
    ExFreePool(pSysMods);
    
    RtlInitUnicodeString(&usRoutineName, L"KeServiceDescriptorTable");
    g_KeServiceDescriptorTable = (ULONG)MmGetSystemRoutineAddress(&usRoutineName);
    g_KiServiceTable = *(PULONG)g_KeServiceDescriptorTable;
    g_ServiceNumber = *(PULONG)(g_KeServiceDescriptorTable + 8);
    
    RtlInitUnicodeString(&usRoutineName, L"MmUserProbeAddress");
    g_MmUserProbeAddress = (ULONG)MmGetSystemRoutineAddress(&usRoutineName);
    
    ……

    // 从 KeAddSystemServiceTable 函数到开始做特征码搜索
    GetSSDTShadow(&g_ShadowServiceTable, &g_ShadowServiceNumber);

    g_ulHookPoint = FindHookPoint();          // 找 Hook 点
    g_JmpBack = g_ulHookPoint + 8;

    // 为什么要这样?看看 Detour 就明白了
    g_MmUserProbeAddress = *(PULONG)g_MmUserProbeAddress;
    
    ……
}


以上代码中,GetSystemModules 的实现如下:

PBYTE GetSystemModules() {
    PBYTE pSysMods = NULL;
    ULONG ulSize = 0;
 
    ZwQuerySystemInformation(SystemModuleInformation, &ulSize, 0, &ulSize);

    pSysMods = (PULONG)ExAllocatePoolWithTag(PagedPool, ulSize, 'tPyF');
    
    if (pModInfo) 
    {
        NTSTATUS = ZwQuerySystemInformation(SystemModuleInformation, pSysMods, ulSize, NULL);
        if (!NT_SUCCESS( status ))
        {
            ExFreePool(pSysMods);
            pSysMods = NULL;
        }
    }
    return pSysMods;
}


这个函数可能有点小bug,因为在两次 ZwQuerySystemInformation 调用之间 ulSize 可能会发生变化,不过这种 bug 的诱发概率很小。

回到正题, FindHookPoint 查找 Hook 点时,依然通过特征码搜索:

ULONG FindHookPoint()  {
    ……
    ulKiSystemService = GetAddr_KiSystemService();
    if ( ulKiSystemService < g_KernelBase || ulKiSystemService > g_KernelBase + g_KernelSize )
        return 0;    
    
    for (ulAddr = ulKiSystemService; ulAddr < ulKiSystemService + 1024; ++ulAddr) {
        if (!ulAddr || !MmIsAddressValid((PVOID)ulAddr))
            break;
        if ( RtlCompareMemory((PVOID)ulAddr, &g_Signature, sizeof(g_Signature)) == sizeof(g_Signature) )
            return ulAddr;
    }
    return 0;
}


搜索的起始地址是 KiSystemService,GetAddr_KiSystemService 通过查询IDT中 0x2e 中断的处理函数取得。从兼容性上考虑,比 rdmsr 的方式要好。

现在到了关键的 Hook(g_ulHookPoint, ulKiFastCallEntry_Detour) 调用,接下来就结合注释和代码呈现一下这个过程:

BOOLEAN Hook (ULONG ulHookPoint, ULONG ulDetourAddr)
{
    PMDL pMdl;
    ULONG ulNewVirtualAddr;
    ULONG i;
    KAFFINITY CpuAffinity;
    ULONG ulNumberOfActiveCpu;
    KIRQL OldIrql;
    BOOLEAN bRet = FALSE;

    ULONG ulCurrentCpu;

    // MDL 法去掉写保护,比去掉 CR0 写保护位要好,因为后者更依赖硬件特性
    pMdl = MakeAddrWritable(ulHookPoint, 16, &ulNewVirtualAddr);
    if (!pMdl)
        return FALSE;


    // 对单核和多核的情况分别处理 
    CpuAffinity = KeQueryActiveProcessors();
    ulNumberOfActiveCpu = 0;
   
    for (i = 0; i < 32; ++i) {
        if ( (CpuAffinity >> i) & 1 )
            ++ulNumberOfActiveCpu;
    }
    
    if ( ulNumberOfActiveCpu == 1 ) 
    {
        //
        // 单核,直接 Hook
        //

        // 通过提升 IRQL 来保证线程不被抢占,与cli相比,减少了对硬件特性的依赖
        OldIrql = KeRaiseIrqlToDpcLevel();

        HookInternal(ulNewVirtualAddr, 0xe9909090, ulDetourAddr - ulHookPoint - 8);

        KeLowerIrql(OldIrql);

        bRet = TRUE;
    }
    else  
    {
        //
        // 多核处理,插DPC,把其它CPU全挂在一个自旋锁上,然后再 Hook
        //
        KeInitializeSpinLock(&g_SpinLock);
        for (i = 0; i < sizeof(g_Dpcs) / sizeof(KDPC); ++i) {
            KeInitializeDpc(&g_Dpcs[i], DpcRoutine, NULL);
        }
        
        g_ulNumberOfRaisedCpu = 0;
        KeAcquireSpinLock(&g_SpinLock, &OldIrql);

        ulCurrentCpu = KeGetCurrentProcessorNumber();

        // 重新获取一次 ulNumberOfActiveCpu
        ulNumberOfActiveCpu = 0;    

        for (i = 0; i < 32; ++i) {
            if ((CpuAffinity >> i) & 1) {
                ++ulNumberOfActiveCpu;    
                if (i != ulCurrentCpu) {
                    KeSetTargetProcessorDpc(&g_Dpcs[i], (CCHAR)i);
                    KeSetImportanceDpc(&g_Dpcs[i], HighImportance);
                    KeInsertQueueDpc(&g_Dpcs[i], NULL, NULL);
                }
            }
        }


        // 在有限的时间里无法完成 Hook 就放弃,可能是为了避免卡死系统
        for (i = 0; i < 16; i ++) {
            ULONG ulTmp = 1000000;
            while (ulTmp)
                ulTmp--;

            if ( g_ulNumberOfRaisedCpu == ulNumberOfActiveCpu - 1 ) {
                HookInternal(ulNewVirtualAddr, 0xe9909090, ulDetourAddr - ulHookPoint - 8);
                bRet = TRUE;
                break;
            }
        }

        KeReleaseSpinLock(&g_SpinLock, OldIrql);    
    }
        
    MmUnlockPages(pMdl);
    IoFreeMdl(pMdl);
    return bRet;
}


DPC历程只是简单地卡在自旋锁上:

VOID DpcRoutine(PKDPC pDpc, PVOID DeferredContext, PVOID SystemArgument1, PVOID SystemArgument2)
{
    KIRQL OldIrql;

    OldIrql = KeRaiseIrqlToDpcLevel();
    InterlockedIncrement(&g_ulNumberOfRaisedCpu);

    KeAcquireSpinLockAtDpcLevel(&g_SpinLock);
    KeReleaseSpinLockFromDpcLevel(&g_SpinLock);
    KeLowerIrql(OldIrql);
}


插DPC解决多核的同步问题我最初是在 《RootKits》一书上看到,不过相比书里的方法(DPC历程死循环)我觉得这里处理得更有技巧。

MakeAddrWritable也贴一下吧:

PMDL MakeAddrWritable (ULONG ulOldAddress, ULONG ulSize, ULONG * pulNewAddress) {
    PMDL pMdl = IoAllocateMdl((PVOID)ulOldAddress, ulSize, FALSE, TRUE, NULL);
    if ( pMdl ) 
    {
        PVOID pNewAddr;
        MmProbeAndLockPages(pMdl, KernelMode, IoWriteAccess);

        if ( pMdl->MdlFlags & (MDL_MAPPED_TO_SYSTEM_VA | MDL_SOURCE_IS_NONPAGED_POOL ))
            pNewAddr = pMdl->MappedSystemVa;
        else
            pNewAddr = MmMapLockedPagesSpecifyCache(pMdl, KernelMode, MmCached, NULL, FALSE, NormalPagePriority);

        if ( !pNewAddr ) {
            MmUnlockPages(pMdl);
            IoFreeMdl(pMdl);
            pMdl = 0;
        }

        if ( pulNewAddress )
            *pulNewAddress = (ULONG)pNewAddr;
    }
    return pMdl;
}


MakeAddrWritable 中的 MmMapLockedPagesSpecifyCache 比暴力改标志位要好一些。

最后还剩下一个 HookInternal 做实际性的 Hook 工作:

VOID HookInternal(ULONG ulHookPoint, ULONG ulE9909090, ULONG ulJmpOffSet) {
    __asm {
        mov edi, ulHookPoint;
        
        mov eax, [edi];                 // orig ins
        mov edx, [edi + 4];             // orig ins

        mov ebx, ulE9909090;
        mov ecx, ulJmpOffSet;

        // Compare EDX:EAX with m64. If equal, set ZF and load ECX:EBX into m64.
        // Else, clear ZF and load m64 into EDX:EAX.
        lock cmpxchg8b qword ptr [edi];
    }
}


为了保证 Hook 操作的原子性,使用了lock cmpxchg8b指令(其实到这里,其它线程已经不调度了,不保证原子性也不会出什么问题)。HookInternal 调用之后,ulHookPoint 处的指令就被替换成了三个 nop 加一个 jmp。

以上就是对 Hook 过程的分析,其实我觉得 Hook 过程不会有绝对的安全,比如此时有一个线程正在执行指令N,结果Hook操作导致指令N和指令N + 1被替换掉了。虽然Hook过程中可以保证该线程不去抢占调度,但该线程恢复时同样会造成BSOD。

虽然做不到绝对安全,但我们可以做到尽量谨慎。

快讯:[看雪招聘]十八年来,看雪平台输出了大量安全人才,影响三代安全人才!

最新回复 (39)
Fido 2012-2-10 16:05
2
看起来有点意思啊....学习了...
我是了了 2012-2-10 16:15
3
我学习下,看着有点头大了!
Sysnap 2012-2-10 16:22
4
你说的“其实我觉得 Hook 过程不会有绝对的安全,比如此时有一个线程正在执行指令N,结果Hook操作导致指令N和指令N + 1被替换掉了。虽然Hook过程中可以保证该线程不去抢占调度,但该线程恢复时同样会造成BSOD。”

其实其实HOOK点就3个NOP比较有意思。跟HOOK安全相关,即使不提升IRQL直接HOOK也没问题,也能保证在千万级别的机器上HOOK安全
Sysnap 2012-2-10 16:29
5
框架其实没什么技术。主要是考虑了兼容,HOOK稳定,可调试,接口统一,扩展性,安全反注册回调,先捕获调用。
longloo 2012-2-10 16:41
6
重新获取一次 ulNumberOfActiveCpu
是因为msdn上已经说了“Callers must also be aware that the value returned by KeQueryActiveProcessors can change during runtime on versions of Windows that support hot-add CPU functionality.”
但是我奇怪,为什么不是再调用一次KeQueryActiveProcessors 重新获取下CpuAffinity?
loongzyd 2012-2-10 16:50
7
谢谢楼主的分享,支持一下!
Fypher 2012-2-10 16:57
8
原来如此
莫灰灰 2012-2-10 17:06
9
在DPC例程里面,IRQL本来就是DISPATCH_LEVEL,这里的OldIrql = KeRaiseIrqlToDpcLevel();是不是可以省略呢?
KiDebug 2012-2-10 17:16
10


上传的附件:
Sysnap 2012-2-10 17:27
11
[QUOTE=KiDebug;1043968]

[/QUOTE]

檫,下个版本去掉。之前记得把TAG神马的都去了。
莫灰灰 2012-2-10 17:29
12

这个让我想起了以前360的某个版本里面的 88 xxx
Sysnap 2012-2-10 17:30
13
素啊,影响不好,学生时间写代码BY XXX的坏习惯得改改
Fypher 2012-2-10 17:31
14
我觉得是可以去掉的……
Fypher 2012-2-10 17:34
15
我也是看到调试信息发现是你写的
Naylon 2012-2-10 19:02
16
路过学习大牛。。好像在qqtalk上见过楼主?
熊猫正正 2012-2-10 19:10
17
最近研究QQ管家的怎么这么多呢?
dayang 2012-2-10 20:41
18
能把完整工程发上来就好了
jerrynpc 2012-2-11 07:20
19
sysnap主刀啊,世界清静了。
XPoy 2012-2-11 11:29
20
幡然醒悟, 真聪明,是这样的7字节吗?
PUSH  EBP;
MOV   EBP, ESP;
SUB    ESP, 0x20;
PUSH  EBX;
mumaren 2012-2-11 17:17
21
谢谢楼主的分享,支持一下!
yinning 2012-2-11 18:57
22
伸手党mark一下
jbwang 2012-2-13 01:06
23
楼主分析的很详细,如看源码,多谢分享!
顶sysnap的技术处理。
linyangcan 2012-2-13 14:02
24
我学习下,看着有点头大了!
promsied 2012-2-14 12:58
25
回帖学习~~
zhych 2012-2-14 19:14
26
360那一套东西,先后江民、金山都用了,现在管家也用上了。。。

尼玛管家一堆驱动,有SSDT hook,有KiFastCallEntry hook,显然开发驱动的都是各写各的,没有沟通与交流。。。
Fypher 2012-2-14 19:29
27
同感!哈哈~管家的驱动确实是一大堆
ytyay 2012-2-15 09:45
28
拜读牛人牛帖  谢谢分享
wybx 2012-2-19 17:32
29
一般淫才 讨论的问题看着就给力
hack一生 2012-2-21 01:21
30
学习
guxinyi 2012-3-1 23:50
31
。。。。。。。。。。
丫丫journey 2012-9-16 20:57
32
学习了 学习了
mccoysc 2012-9-16 22:47
33
[QUOTE=Fypher;1043918]插DPC解决多核的同步问题我最初是在 《RootKits》一书上看到,不过相比书里的方法(DPC历程死循环)我觉得这里处理得更有技巧。[QUOTE]

这个。。。貌似是从金山抄来的。。。。不过无所谓了。。。。大家都这么搞,说不上谁抄谁了
OSystem 2013-2-25 11:25
34
希望以后可以看懂 mark
闲云轻烟 2013-2-26 17:20
35
学习学习,谢谢分享!!!!
raigeki 2013-8-2 09:31
36
mark !一下~
kman 2013-12-13 16:17
37
可惜啊,今年微软某个补丁后你那个什么3个NOP就完蛋了~千万级hook安全性也没了,悲催,看起来是代笔改代码应付事导致的。

更有意思的是,过了两年,金山和百度都照抄这个三个Nop了
金山是搞跳转搞出蓝屏门用户全卸光了只好抄了腾讯,百度是生下来全是照抄的,自己写的代码完全不能看。

最有意思的是,百度不知道出于什么心理,抄的时候反着抄,先写jmp再写nop,完全没搞懂这三个nop是干啥的啊~
mccoysc 2013-12-13 16:30
38
处理方式带有明显的金山系风格,几乎完全一样。

TX做了金山的二股东后,确实从KS挖了不少人。
天高 2013-12-13 16:33
39
mark
cqzj70 2014-5-30 10:51
40
mark
返回