鑫郁飞网络技术-郁金香灬老师

 找回密码
 立即注册

QQ登录

只需一步,快速开始

郁金香 外挂开发(实战)郁金香 终身VIP 尊贵特权郁金香 VIP学员办理任鸟飞2015外挂教程
◆招传奇霸业辅助作者◆论坛VIP会员账号郁金香游戏外挂制作 终极教程   ◆招传奇霸业辅助作者◆
查看: 93|回复: 0

QQ电脑管家中的TsFltMgr Hook框架分析 hook CPU多核处理(转)

[复制链接]
发表于 2018-6-6 12:16:30 | 显示全部楼层 |阅读模式
本帖最后由 郁金香灬老师 于 2018-6-6 12:19 编辑

QQ电脑管家中的 Hook 过程分析

作者:Fypher

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

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

我们从这个函数开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 是初始化一些全局变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 点时,依然通过特征码搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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) 调用,接下来就结合注释和代码呈现一下这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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, 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, (CCHAR)i);
                    KeSetImportanceDpc(&g_Dpcs, HighImportance);
                    KeInsertQueueDpc(&g_Dpcs, 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历程只是简单地卡在自旋锁上:

1
2
3
4
5
6
7
8
9
10
11
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也贴一下吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。
郁金香外挂教程,学习中...
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


QQ|小黑屋|手机版|鑫郁飞网络技术-郁金香灬老师 ( 苏ICP备10059359号 )

GMT+8, 2018-6-22 18:51 , Processed in 0.067612 second(s), 17 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表