原文

背景

一些游戏反作弊系统使用了PsSetCreateThreadNotifyRoutine来阻止作弊软件在Ring3层下从游戏进程创建新线程。
为了阻止加载黑名单驱动程序,还使用了PsSetLoadImageNotifyRoutine(此方法不用来防止DLL注入)
通过修改内核变量: PspNotifyEnableMask 可以绕过此方法

分析

从IDA中打开 ntoskrnl.exe 并检查 PspCallThreadNotifyRoutines 反汇编代码可以看到:

显示
int __fastcall PspCallThreadNotifyRoutines(__int64 a1, unsigned __int8 a2, char a3) {
    unsigned __int8 v3; // r14@1
    __int64 v4; // r15@1
    bool v5; // r12@1
    __int64 v6; // rax@1
    void * v7; // rbx@4
    signed __int64 v8; // rsi@4
    __int64 v9; // rbp@5
    char v10; // al@8
    __int64 v11; // rcx@8
    __int64 v12; // rdi@10
    void * v13; // rbx@16
    signed __int64 v14; // rsi@16
    __int64 v15; // rbp@17
    __int64 v16; // rdi@21
    void * v17; // rbx@13
    signed __int64 v18; // rsi@13
    __int64 v19; // rbp@25
    __int64 v20; // rcx@26
    __int64 v21; // rdi@27

    v3 = a2;
    v4 = a1;
    v5 = * (_QWORD * )(a1 + 1944) != 0 i64;
    LODWORD(v6) = PspNotifyEnableMask;
    if (a2) {
        if (a3) {
            if (PspNotifyEnableMask & 0x10) {
                v17 = & PspCreateThreadNotifyRoutine;
                v18 = 64 i64;
                do {
                    LODWORD(v6) = ExReferenceCallBackBlock(v17);
                    v19 = v6;
                    if (v6) {
                        if (ExGetCallBackBlockContext(v6) & 1) {
                            v21 = * (_QWORD * )(v4 + 544);
                            ExGetCallBackBlockRoutine(v20);
                            guard_dispatch_icall( * (_QWORD * )(v21 + 736), *(_QWORD * )(v4 + 1600), v3);
                        }
                        LODWORD(v6) = ExDereferenceCallBackBlock(v17, v19);
                    }
                    v17 = (char * ) v17 + 8;
                    --v18;
                }
                while (v18);
            }
        } else if (PspNotifyEnableMask & 8) {
            v7 = & PspCreateThreadNotifyRoutine;
            v8 = 64 i64;
            do {
                LODWORD(v6) = ExReferenceCallBackBlock(v7);
                v9 = v6;
                if (v6) {
                    v10 = ExGetCallBackBlockContext(v6);
                    if (!(v10 & 1) && (!v5 || v10 & 2)) {
                        v12 = * (_QWORD * )(v4 + 544);
                        ExGetCallBackBlockRoutine(v11);
                        guard_dispatch_icall( * (_QWORD * )(v12 + 736), *(_QWORD * )(v4 + 1600), v3);
                    }
                    LODWORD(v6) = ExDereferenceCallBackBlock(v7, v9);
                }
                v7 = (char * ) v7 + 8;
                --v8;
            }
            while (v8);
        }
    } else if (PspNotifyEnableMask & 0x10 || (LODWORD(v6) = PspNotifyEnableMask, PspNotifyEnableMask & 8)) {
        v13 = & PspCreateThreadNotifyRoutine;
        v14 = 64 i64;
        do {
            LODWORD(v6) = ExReferenceCallBackBlock(v13);
            v15 = v6;
            if (v6) {
                if (!v5 || ExGetCallBackBlockContext(v6) & 2) {
                    v16 = * (_QWORD * )(v4 + 544);
                    ExGetCallBackBlockRoutine(v15);
                    guard_dispatch_icall( * (_QWORD * )(v16 + 736), *(_QWORD * )(v4 + 1600), 0 i64);
                }
                LODWORD(v6) = ExDereferenceCallBackBlock(v13, v15);
            }
            v13 = (char * ) v13 + 8;
            --v14;
        }
        while (v14);
    }
    return v6;
}

显然可以简单地将PspNotifyEnableMask设为0来跳过任何例程,但这将阻止进程创建,因此检查 PsSetCreateThreadNotifyRoutine

显示
signed __int64 __fastcall PspSetCreateThreadNotifyRoutine(__int64 a1, unsigned int a2) {
    char v2; // si@1
    void * v3; // rax@1
    void * v4; // rdi@1
    __int64 v5; // rbx@2
    signed __int64 result; // rax@7

    v2 = a2;
    LODWORD(v3) = ExAllocateCallBack(a1, a2);
    v4 = v3;
    if (v3) {
        v5 = 0 i64;
        while (!(unsigned __int8) ExCompareExchangeCallBack((char * ) & PspCreateThreadNotifyRoutine + 8 * v5, v4, 0 i64)) {
            v5 = (unsigned int)(v5 + 1);
            if ((unsigned int) v5 >= 0x40) {
                ExFreePoolWithTag(v4, 0);
                goto LABEL_11;
            }
        }
        if (v2 & 1) {
            _InterlockedIncrement((volatile signed __int32 * ) & PspCreateThreadNotifyRoutineNonSystemCount);
            if (!(PspNotifyEnableMask & 0x10))
                _interlockedbittestandset((volatile signed __int32 * ) & PspNotifyEnableMask, 4 u);
        } else {
            _InterlockedIncrement((volatile signed __int32 * ) & PspCreateThreadNotifyRoutineCount);
            if (!(PspNotifyEnableMask & 8))
                _interlockedbittestandset((volatile signed __int32 * ) & PspNotifyEnableMask, 3 u);
        }
        result = 0 i64;
    } else {
        LABEL_11: result = 3221225626 i64;
    }
    return result;
}

注意到其中有:

 _interlockedbittestandset((volatile signed __int32 *)&PspNotifyEnableMask, 4u);
 _interlockedbittestandset((volatile signed __int32 *)&PspNotifyEnableMask, 3u);

通过 MSDN文档 可知,此函数将对应地址的指定位设置为1,所以PspNotifyEnableMask是无符号数,并且将位3和4设置为0将使Windows认为没有线程创建通知例程被安装并跳过它们。
并且如果查看PsSetLoadImageNotifyRoutine还注意到有:

_interlockedbittestandset((volatile signed __int32 *)&PspNotifyEnableMask, 0);

将其位0设为0将跳过模块加载通知,想要跳过进程创建通知,则设置位1和2:
0 = LoadImage
1-2 = CreateProcess
3-4 = CreateThread

实现

由于PspNotifyEnableMask是未公开变量,因此需要使用特征扫描方式来搜索它:

ULONG64 GetNotifyVarAddress() {
    ULONG64 i = 0;
    PULONG64 pAddrOfFnc = 0;
    UNICODE_STRING fncName;
    RtlInitUnicodeString( & fncName, L "PsSetLoadImageNotifyRoutine");
    ULONG64 fncAddr = (ULONG64) MmGetSystemRoutineAddress( & fncName);
    if (fncAddr) {
        fncAddr += 0x50;
        for (i = fncAddr; i < fncAddr + 0x15; i++) {
            if ( * (UCHAR * ) i == 0x8B && * (UCHAR * )(i + 1) == 0x05) {
                LONG OffsetAddr = 0;
                memcpy( & OffsetAddr, (UCHAR * )(i + 2), 4);

                pAddrOfFnc = (ULONG64 * )(OffsetAddr + i + 0x6);
                break;
            }
        }
        return (ULONG64) pAddrOfFnc;
    }
    return 0;
}

然后设置其位:

#define SETBIT(X, Y) X |= (1 ULL << (Y))
#define UNSETBIT(X, Y) X &= (~(1 ULL << (Y)))

VOID CHANGE_NOTIFY_MASK(BOOLEAN enableThread, BOOLEAN enableImage) {
    ULONG64 varaddress = GetNotifyVarAddress();
    if (varaddress) {
        ULONG val = * (ULONG * )(varaddress);
        if (!enableThread) {
            UNSETBIT(val, 3);
            UNSETBIT(val, 4);
        } else {
            SETBIT(val, 3);
            SETBIT(val, 4);
        }

        if (!enableImage) {
            UNSETBIT(val, 0);
        } else {
            SETBIT(val, 0);
        }

        *(ULONG * )(varaddress) = val;
    }
}

结尾

现在可以在游戏内创建线程以及加载黑名单驱动程序了,但由于可以枚举模块以及线程,因此仅使用此方式不是完全有效。
此方法在 Windows 10 version 1703(build 15063)上是PatchGuard安全的。