EMACLAB Anticheat Driver, Part 2: Globals

  • File Name: EMAC-Driver-x64.sys
  • TimeDateStamp: 0x67CAFFCE (Friday, 7 March 2025 14:16:46 GMT)
  • Protector: VMProtect 3.8+

Globals

There are a few interesting “globals” which can lead to some assumptions, for example:

img

Some are pretty straightforward, like finding syscall indexes dynamically, getting module information from PEB, addresses for tables/dispatchers often used for integrity check, offsets for dynamic system structs and the list goes.

At initialization, the driver resolves a large set of kernel symbols, tables, and offsets stored as global variables using three resolution methods:

  1. Export lookupEmacFindExportByName walks PsLoadedModuleList and parses PE export tables
  2. Pattern scanningEmacFindFunctionByPattern / EmacResolveKernelTableByPattern scans for byte patterns in kernel modules
  3. Disassembly-based resolution — Uses bddisasm (NdDecodeEx) to analyze function prologues and extract dynamic offsets from instructions

Here’s a summary of the key resolved globals:

Function Address What it resolves
EmacInitializeSyscallTables 0xBCF0DD64 ntoskrnl SSDT and win32k shadow SSDT base addresses
EmacGetNtWow64SyscallTable 0xBCF0DD80 WOW64 syscall translation table
EmacGetWin32kSyscallTable 0xBCF0E378 Win32k SSDT for GUI syscalls
EmacResolveProcessNotifyTable 0xBCF11D2C PspCreateProcessNotifyRoutine array
EmacResolveThreadNotifyTable 0xBCF11A5C PspCreateThreadNotifyRoutine array
EmacCaptureKernelDebuggerBlock 0xBCF12AC4 KdDebuggerDataBlock for debugger detection
EmacResolvePspGetContextThreadInternal 0xBCF11BCC Internal context retrieval function

Let’s take FindNtPsLoadedModuleResource as an example:

tries to obtain address by export img

fallback to famous ‘FindPattern’ search img

Why i don’t like dynamic search

The main thing that leads me to confusion is why the fuck there’s dynamic search for these symbols; like i said it’s a symbol so i wonder why not simply obtain symbol’s and parse its information!? There’s nothing wrong with using FindPattern i guess, but me personally would rather parse symbols, specially because this is a demand driver so obtaining addresses in the driver logic itself is not necessary.

They even take it as far as using a disassembly engine, creating a smart logic to obtain offsets dynamically.

They’re using bitdefender/bddisasm img

Extra

For some reason they have checks for ancient Windows versions like 7, 8 and 8.1. This leave me wondering why this code is still left here, Windows 7 for example is not actively supported on Steam for quite a while, and i am pretty sure Windows 10 is the minimum OS required for Counter-Strike: 2. This is honestly dead code, since it will never be actually used/ran.

img

Version-Dependent Offsets

The driver dynamically resolves KTHREAD structure offsets at runtime based on Windows version, which are used extensively in the thread stack trace verification and other integrity checks:

Function Address Resolves
EmacGetVersionDependentOffset 0xBCF0ED10 Various KTHREAD/EPROCESS offsets by build number
EmacResolveThreadTerminatedOffset 0xBCF0EF24 KTHREAD.Terminated offset
GetOffsetKThreadStackLimit KTHREAD.StackLimit
GetOffsetKThreadStackBase KTHREAD.StackBase
GetOffsetKThreadThreadLock KTHREAD.ThreadLock
GetOffsetKThreadKernelStack KTHREAD.KernelStack
GetOffsetKThreadState KTHREAD.State

ExPreviousMode Manipulation

One interesting technique i found is that the driver modifies the calling thread’s PreviousMode to KernelMode before calling certain Nt* functions, then restores it after. This allows kernel code to call Nt* APIs that normally expect user-mode callers, bypassing ProbeForRead/ProbeForWrite checks:

KPROCESSOR_MODE oldMode = KeGetCurrentThread()->PreviousMode;
KeGetCurrentThread()->PreviousMode = KernelMode;
NtQuerySystemInformation(...);
KeGetCurrentThread()->PreviousMode = oldMode;

The offset of PreviousMode in KTHREAD is also resolved dynamically via EmacGetVersionDependentOffset.

Assumptions

Some of those globals are self-explanatory, for example FindNtWmipSMBiosTableLength surely will be used to extract information about SMBIOS, FindWin32kbase_gDxgkInterface this will get the gDxgkInterface table and will perform integrity check later, FindNtKdpDebugRoutineSelect and FindNtKdpTrap[2,3] will be used to verify global exception hooks, FindNtPiDDBCacheTable and FindNtMmUnloadedDrivers are both very known tables that contain information about unloaded drivers ultimately leading to traces if not cleaned correctly, and the list goes…

Important Global Variables

After deeper analysis, here’s a reference of the most important global variables used throughout the driver:

Address Name Purpose
qword_FFFFF801BCFACC40 XOR IAT Key API pointer obfuscation key
qword_FFFFF801BCFACC38 Opaque Predicate Always-true value for dead code generation
qword_FFFFF801BCFADB28 SSDT Base ntoskrnl syscall table (KeServiceDescriptorTable)
qword_FFFFF801BCFADB30 Shadow SSDT Base win32k shadow syscall table
qword_FFFFF801BCFACC98 Filter Handle Minifilter registration handle
qword_FFFFF801BCFACCA8 Client Port User-mode FltMgr connection port
qword_FFFFF801BCFACDA0 Disable Flag Global hook bypass flag
g_NtoskrnlBase Cached ntoskrnl.exe base address
g_DriverBase EMAC driver’s own base address
g_EmacProcesses Array of protected process IDs
g_InfinityHookEnabled InfinityHook active flag
g_PsLoadedModuleList Cached PsLoadedModuleList pointer
g_MmUnloadedDrivers Cached MmUnloadedDrivers pointer
g_PiDDBCacheTable Cached PiDDBCacheTable pointer

Conclusion

I decided not go so deep into that subject initially, but with further analysis i was able to document most of the globals and their purposes. If you’re curious then take a look at the .IDB