Official site anti-cheat Ultra Core Protector

Home Download F.A.Q. Addons Monitor Forum Support Advertise English version site UCP Anti-Cheat    Russian version site UCP Anti-Cheat
Ultra Core Protector - is the client-server anti-cheat freeware, for server protection from unscrupulous players.

Abilities Supported games  
Half-Life
Condition Zero
Counter-Strike 1.6
Day of Defeat
Adrenaline Gamer
Team Fortress Classic
Counter-Strike Source
MU Online
Ragnarok Online
Half-Life 2 Deathmatch
Adrenaline Gamer 2
Team Fortress 2
Bypass process, thread and image load notify routines

Since somebody mentioned this method here and I haven’t had use for this for a long time, I decided to share what I have.

The reason why BattlEye gives you a corrupted memory error if you try to create a thread inside the game is PsSetCreateThreadNotifyRoutine. Then again BattlEye (and EAC) uses PsSetLoadImageNotifyRoutine to block loading of blacklisted drivers. PsSetLoadImageNotifyRoutine is not being used to block DLL injection.
However, we can bypass both of these notifications by modifying 1 kernel variable: PspNotifyEnableMask. Why?

Well, just open ntoskrnl.exe in IDA and take a look at PspCallThreadNotifyRoutines for example. That function is responsible for iterating the installed notification routines and calling them:

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) != 0i64;
LODWORD(v6) = PspNotifyEnableMask;
if ( a2 )
{
if ( a3 )
{
if ( PspNotifyEnableMask & 0x10 )
{
v17 = &PspCreateThreadNotifyRoutine;
v18 = 64i64;
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 = 64i64;
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 = 64i64;
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), 0i64);
}
LODWORD(v6) = ExDereferenceCallBackBlock(v13, v15);
}
v13 = (char *)v13 + 8;
--v14;
}
while ( v14 );
}
return v6;
}

As you can see, it seems we can simply write PspNotifyEnableMask to 0 and skip calling of any notification routines. While this kinda works, it’s not clean way to do it and will also block process creation routines which is not a good thing (any process you start when PsSetCreateProcessNotifyRoutine is bypassed will not have ability to connect to internet).
So let’s take a look at PsSetCreateThreadNotifyRoutine itself:

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 = 0i64;
while ( !(unsigned __int8)ExCompareExchangeCallBack((char *)&PspCreateThreadNotifyRoutine + 8 * v5, v4, 0i64) )
{
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, 4u);
}
else
{
_InterlockedIncrement((volatile signed __int32 *)&PspCreateThreadNotifyRoutineCount);
if ( !(PspNotifyEnableMask & 8) )
_interlockedbittestandset((volatile signed __int32 *)&PspNotifyEnableMask, 3u);
}
result = 0i64;
}
else
{
LABEL_11:
result = 3221225626i64;
}
return result;
}

Pay attention to these calls:

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

According to MSDN documentation, _interlockedbittestandset simply sets the bit at given position to 1. So PspNotifyEnableMask is an unsigned integer and by setting bits 3 and 4 to 0 Windows thinks there are no thread creation notify routines installed and simply skips them. Now take a look at PsSetLoadImageNotifyRoutine and you will see this:

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

By setting the bit 0 we can disable loadimage notifications. If you want to disable createprocess notifications, modify bits 1 & 2.
0 = LoadImage
1-2 = CreateProcess
3-4 = CreateThread

Implementation

PspNotifyEnableMask is not exported variable, so we need to find its address manually by doing a simple “sigscan”. I know the code is shit but at least it works.

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; }

And to set the correct bits:

#define SETBIT(X,Y) X|=(1ULL<<(Y)) #define UNSETBIT(X,Y) X&=(~(1ULL<<(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; } }

Conclusion

You can now load blacklisted drivers (WIN64AST for example) and create threads inside the game (as long as you have a handle). However please remember drivers and threads can be enumerated afterwards so simply getting rid of the notifications is not very useful. This method is patchguard safe on Windows 10 version 1703 (build 15063).


 



Home | Download | F.A.Q. | Addons | Forum | Banners | Sitemap | Directory | Support
Copyright © 2008-2015 UCP. All rights reserved. Privacy Policy. Siter.