背景
一些游戏反作弊系统使用了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安全的。