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:

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:
- Export lookup —
EmacFindExportByNamewalksPsLoadedModuleListand parses PE export tables - Pattern scanning —
EmacFindFunctionByPattern/EmacResolveKernelTableByPatternscans for byte patterns in kernel modules - 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 
fallback to famous ‘FindPattern’ search 
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 
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.

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