# cat ./posts/warmane-anti-cheat.md
[Warmane] Anti Cheat
Disclaimer
This document is provided for educational and research purposes only. The author is not responsible for any misuse of the information contained herein. Any actions taken by individuals using this information are solely their own responsibility. Use of this information for malicious purposes, unauthorized access, or any illegal activities is strictly prohibited and may violate local, state, federal, or international laws.
Executive Summary
Warmane is a World of Warcraft: Wrath of the Lich King private server founded by Kaer in November 2009. Originally launched under the name Molten-WoW, it was later rebranded as Warmane and has since grown into one of the largest and most established private server communities dedicated to the 3.3.5a era of the game.
Notably, Warmane's anti-cheat enforcement is built on top of a Remote Code Execution (RCE) exploit against its own client, allowing the server to run arbitrary code inside the player's game process for detection and inspection purposes.
Anti-Cheat Initialization
When a player logs in-game, Warden activates the injected RCE payload, which in turn loads Warmane's anti-cheat runtime and dispatches its initialization routines. During this handshake, four core functions are executed to establish the anti-cheat's presence within the client process. Dispatcher
Dispatcher The Dispatcher is the central entry point for all incoming anti-cheat checks. Each check arrives as an obfuscated packet, is decrypted in place, executed within the client's address space, and then wiped from memory to eliminate forensic traces. An acknowledgement packet is subsequently re-encrypted and returned to the server, completing the round trip.
int __usercall Dispatcher@<eax>(int a1@<eax>, int a2, int a3, int a4, int a5)
{
int i; // ebx - inbound decrypt offset
int payloadLen; // esi - packet length / outbound XOR key
int codeLen; // eax - shellcode length
unsigned int j; // ebx - pad counter
int k; // ebx - outbound encrypt offset
_BYTE outPkt[4]; // [esp+Ch] - outbound CDataStore header
int outBuf; // [esp+10h] - outbound buffer pointer
unsigned int outSize; // [esp+1Ch] - outbound buffer size
int outTail; // [esp+20h] - outbound tail marker
int ctx; // [esp+28h] - saved self pointer
ctx = a1;
// Decrypt inbound payload: rotating-XOR over 4-byte words.
for (i = 2; *(_DWORD *)(a5 + 16) != i; i += 4)
{
*(_DWORD *)(i + *(_DWORD *)(a5 + 4)) ^= a2;
a2 = __ROR4__(a2, 1);
}
// Register handler for opcode 1051 (0x41B).
payloadLen = ((int (__fastcall *)(int))((char *)&loc_40116C + 4))(a5);
ClientServices::SetMessageHandler(1051, ctx, payloadLen);
// Build outbound ACK packet.
CDataStore::InitPacket2(outPkt);
CDataStore::PutInt32(outPkt, 1051);
CDataStore::PutInt32(outPkt, -792828975); // 0xD0BE6D31 handshake magic
// Copy shellcode to scratch buffer, execute it, then wipe.
codeLen = ((int (__fastcall *)(int))((char *)&loc_40116C + 4))(a5);
CDataStore::GetBytes(a5, ctx + 340, codeLen, codeLen);
((void (__cdecl *)(int))(ctx + 340))(ctx + 340);
memset(ctx + 340, 0);
// Pad outbound buffer to 4-byte boundary.
if (outSize % 4)
{
for (j = outSize % 4; j != 4; ++j)
CDataStore::PutInt8(outPkt, 0);
}
// Re-encrypt outbound payload with rotating-XOR.
for (k = 4; outSize != k; k += 4)
{
*(_DWORD *)(k + outBuf) ^= payloadLen;
payloadLen = __ROR4__(payloadLen, 1);
}
outTail = 0;
ClientServices::SendPacket(outPkt);
return CDataStore::ReleasePacket(outPkt);
}
In practice, the Dispatcher orchestrates the full lifecycle of an anti-cheat check: inbound decryption, handler registration under opcode 1051 (0x41B), execution of the delivered shellcode, memory sanitization, and finally the construction and re-encryption of the response packet before it is handed off to ClientServices::SendPacket.
HWID Collection
Once the Dispatcher receives and executes the appropriate anti-cheat check, one of the routines it invokes is Hwid — the hardware fingerprinting stage. This function aggregates a broad set of system identifiers from the client machine (total physical memory, CPU core count, machine and user names, GPU description, Windows install date, CPU brand string, disk capacity, and the local hardware profile GUID) and serializes them into a single outbound packet. Together these values form a stable per-machine signature used to correlate accounts, detect ban evasion, and enforce hardware-level restrictions.
int __usercall Hwid@<eax>(int a1@<edi>)
{
__int64 memStatus; // rax - GlobalMemoryStatusEx result
int cpuCount; // eax - processor count
int scratch; // ebx - multi-purpose scratch buffer
HMODULE hAdvapi; // esi - advapi32.dll handle
void (__stdcall *pHwProf)(int); // eax - GetCurrentHwProfileA pointer
_DWORD *cpuidBuf; // esi - CPUID brand string buffer
HKEY hKey; // - registry key handle
unsigned int _EAX; // eax
int _EDX; // edx
int _ECX; // ecx
int _EBX; // ebx
int bufSize; // [esp+4h] - buffer size for Win32 calls
// --- System memory ---
memStatus = GlobalMemoryStatusEx();
CDataStore::PutInt64(a1, memStatus, HIDWORD(memStatus));
// --- Processor count ---
cpuCount = OsGetNumberOfProcessors();
CDataStore::PutInt32(a1, cpuCount);
// --- Allocate 424-byte scratch buffer for Win32 string queries ---
bufSize = 424;
scratch = (int)malloc(424);
// --- Computer name ---
GetComputerNameA((LPSTR)scratch, (LPDWORD)&bufSize);
CDataStore::PutCString(a1, scratch);
// --- User name ---
GetUserNameA((LPSTR)scratch, (LPDWORD)&bufSize);
CDataStore::PutCString(a1, scratch);
// --- Primary display device description ---
EnumDisplayDevicesA((LPCSTR)scratch, 4);
CDataStore::PutCString(a1, scratch + 36);
// --- Hardware profile GUID via advapi32!GetCurrentHwProfileA ---
*(_DWORD *)scratch = 0;
*(_DWORD *)(scratch + 4) = 0;
hAdvapi = LoadLibraryA("advapi32.dll");
if (hAdvapi)
{
strcpy((char *)(scratch + 16), "GetCurrentHwProfileA");
*(_BYTE *)(scratch + 37) = 0;
*(_WORD *)(scratch + 38) = 0;
pHwProf = (void (__stdcall *)(int))GetProcAddress(hAdvapi, (LPCSTR)(scratch + 16));
if (pHwProf)
pHwProf(scratch);
FreeLibrary(hAdvapi);
}
CDataStore::PutCString(a1, scratch + 4);
// --- Windows install date from HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\InstallDate ---
*(_DWORD *)scratch = 0;
strcpy((char *)(scratch + 16), "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\");
*(_WORD *)(scratch + 62) = 0;
strcpy((char *)(scratch + 64), "InstallDate");
if (!RegOpenKeyExA(HKEY_LOCAL_MACHINE,
(LPCSTR)(scratch + 16),
0,
KEY_READ,
(PHKEY)(scratch + 12)))
{
*(_DWORD *)(scratch + 8) = 4;
RegQueryValueExA(*(HKEY *)(scratch + 12),
(LPCSTR)(scratch + 64),
NULL,
(LPDWORD)(scratch + 8),
(LPBYTE)scratch,
(LPDWORD)(scratch + 8));
RegCloseKey(*(HKEY *)(scratch + 12));
}
CDataStore::PutInt32(a1, *(_DWORD *)scratch);
// --- CPU brand string via CPUID leaves 0x80000002-0x80000004 ---
cpuidBuf = (_DWORD *)scratch;
*(_DWORD *)scratch = 0;
*(_DWORD *)(scratch + 48) = 0;
_EAX = 0x80000000;
__asm { cpuid }
if ((_EAX & 0xFFFF0000) == 0x80000000 && _EAX >= 0x80000004)
{
_EAX = 0x80000004;
__asm { cpuid }
cpuidBuf[8] = _EAX;
cpuidBuf[9] = _EBX;
cpuidBuf[10] = _ECX;
cpuidBuf[11] = _EDX;
_EAX = 0x80000003;
__asm { cpuid }
cpuidBuf[4] = _EAX;
cpuidBuf[5] = _EBX;
cpuidBuf[6] = _ECX;
cpuidBuf[7] = _EDX;
_EAX = 0x80000002;
__asm { cpuid }
cpuidBuf[0] = _EAX;
cpuidBuf[1] = _EBX;
cpuidBuf[2] = _ECX;
cpuidBuf[3] = _EDX;
}
CDataStore::PutCString(a1, (int)cpuidBuf);
// --- Total disk capacity of the default drive ---
*cpuidBuf = 0;
cpuidBuf[1] = 0;
GetDiskFreeSpaceExA(NULL, NULL, (PULARGE_INTEGER)cpuidBuf, NULL);
CDataStore::PutInt64(a1, *cpuidBuf, cpuidBuf[1]);
return free(cpuidBuf); // was unk_412FC7 — matches CRT free() signature
}
Loaded Module Enumeration
Immediately after the HWID payload is transmitted, the anti-cheat performs a full enumeration of every module loaded into the client process. Using a Toolhelp32 snapshot, it walks the entire module list and reports each module's base address, size, and file path back to the server. This gives Warmane a complete inventory of every DLL mapped into the WoW process — a common technique for detecting injected cheats, third-party overlays, and unauthorized modifications by cross-referencing loaded modules against a known blacklist.
int __usercall ModuleScan@<eax>(int a1@<edi>)
{
MODULEENTRY32 *modEntry; // ebx - Toolhelp32 module entry buffer
HANDLE hSnapshot; // esi - Toolhelp32 snapshot handle
// Allocate a MODULEENTRY32 structure (sizeof == 568 on x86).
modEntry = (MODULEENTRY32 *)malloc(sizeof(MODULEENTRY32));
modEntry->dwSize = sizeof(MODULEENTRY32);
// Take a snapshot of all modules loaded in the current process.
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0);
// Walk the module list and stream each entry into the outbound packet.
if (Module32First(hSnapshot, modEntry))
{
do
{
CDataStore::PutInt32 (a1, (DWORD)modEntry->modBaseAddr); // base address
CDataStore::PutInt32 (a1, modEntry->modBaseSize); // size in bytes
CDataStore::PutCString(a1, (int)modEntry->szExePath); // full path on disk
}
while (Module32Next(hSnapshot, modEntry));
}
CloseHandle(hSnapshot);
return (int)free(modEntry);
}
Addon Check
On entering the world, the anti-cheat runs AddonCheck — a scan of every loaded addon that compares each addon name against a server-supplied target. When a match is found, it locates the addon's in-memory data block and overwrites strings within it, silently tampering with the loaded addon data without the client or player having any indication it occurred.
void __cdecl AddonCheck(int outPacket, int a2, const char *targetAddonName)
{
int addonCount; // esi - number of addons in the linked list
_DWORD *currentNode; // edi - pointer to current list node
_DWORD *nextNode; // ebx - pointer to next list node
_DWORD *namePtr; // edi - pointer to addon name within node
int metadataPtr; // edi - pointer to addon metadata block
int allocBuf; // eax - server-allocated buffer for exfiltration
int metadataLen; // eax - length of metadata block
int allocBufSaved; // [esp+Ch] - saved allocation pointer
// Walk the global addon linked list at 0xDD0FD8
if ( MEMORY[0xDD0FD8] )
{
addonCount = *MEMORY[0xDD0FD8]; // first DWORD is the count
currentNode = (_DWORD *)(MEMORY[0xDD0FD8] + 4);
while ( addonCount )
{
--addonCount;
nextNode = (_DWORD *)((char *)currentNode + *currentNode); // advance by node stride
namePtr = currentNode + 1; // name follows the stride DWORD
// Compare this addon's name against the server-supplied target
if ( !strcmp(targetAddonName, namePtr) )
{
// Metadata block sits immediately after the null-terminated name
metadataPtr = (int)namePtr + strlen((char *)namePtr) + 1;
// Allocate a server-side buffer for the metadata
allocBuf = strstr(outPacket, metadataPtr);
if ( allocBuf )
{
allocBufSaved = allocBuf;
metadataLen = strlen((char *)metadataPtr);
// Copy the metadata block into the exfiltration buffer
// (skips the length prefix: metadataPtr + metadataLen + 1)
memcpy(allocBufSaved, metadataLen + metadataPtr + 1, metadataLen);
}
}
currentNode = nextNode;
}
}
JUMPOUT(0x818E53); // tail-jump back to the client's Addon Execute Function
}
Active Session Checks
Memory Check
Another routine dispatched during the anti-cheat session is MemoryCheck — a targeted memory read that extracts raw bytes from specified client addresses and serializes them into the outbound packet. The server uses this to verify the integrity of specific memory regions, detecting patches, hooks, or any in-memory modifications to the game client.
int __usercall MemoryCheck@<eax>(int outPacket@<edi>, int inPacket@<esi>)
{
int remainingChecks; // eax
int memAddress; // ebx
int byteCount; // eax
int checkCounter; // [esp+4h] [ebp-4h]
remainingChecks = ((int (__fastcall *)(int))CDataStore::GetInt8)(inPacket);
do
{
checkCounter = remainingChecks;
memAddress = ((int (__fastcall *)(int))CDataStore::GetInt32)(inPacket);
byteCount = ((int (__fastcall *)(int))CDataStore::GetInt8)(inPacket);
((void (__thiscall *)(int, int, int))CDataStore::PutBytes)(outPacket, memAddress, byteCount);
remainingChecks = checkCounter - 1;
}
while ( checkCounter != 1 );
return remainingChecks;
}
The iteration count is read first via GetInt8, then for each iteration the target address is pulled with GetInt32 and the byte count with GetInt8. CDataStore::PutBytes then copies that many bytes from the target address directly into the outbound packet — effectively letting the server read arbitrary regions of client memory.
The underlying CDataStore::PutBytes uses memcpy internally, streaming the raw memory contents into the packet buffer with bounds management handled by InternalFetchWrite:
CDataStore *__thiscall CDataStore::PutBytes(CDataStore *this, unsigned __int8 *a2, unsigned int a3)
{
unsigned int v5; // edi
unsigned int *p_m_base; // ebx
unsigned int m_alloc; // edx
unsigned int m_size; // eax
unsigned __int8 *v9; // eax
if ( a2 )
{
v5 = a3;
sub_401070(this->m_size, a3, 0, 0);
if ( a3 )
{
p_m_base = &this->m_base;
while ( 1 )
{
m_alloc = this->m_alloc;
if ( v5 >= m_alloc )
v5 = this->m_alloc;
if ( v5 <= 1 )
v5 = 1;
m_size = this->m_size;
if ( m_size < *p_m_base || m_size + v5 > m_alloc + *p_m_base )
((void (__thiscall *)(CDataStore *, unsigned int, unsigned int,
unsigned __int8 **, unsigned int *,
unsigned int *, _DWORD, _DWORD))
this->vTable->InternalFetchWrite)(
this, m_size, v5,
&this->m_buffer, &this->m_base, &this->m_alloc, 0, 0);
v9 = &this->m_buffer[this->m_size - *p_m_base];
if ( v9 != a2 )
((void (__cdecl *)(unsigned __int8 *, unsigned __int8 *))memcpy)(v9, a2);
a2 += v5;
this->m_size += v5;
a3 -= v5;
if ( !a3 )
break;
v5 = a3;
}
}
}
else if ( a3 )
{
SErrSetLastError(87);
return this;
}
return this;
}
The write path clamps the chunk size to the current allocation, calls InternalFetchWrite to grow the buffer if needed, then memcpys the source bytes in. The loop continues until all a3 bytes are consumed, making it a fully streaming copy with automatic buffer growth.
Breakpoint Detection
To prevent dynamic analysis, the anti-cheat checks for hardware breakpoints by calling GetThreadContext with the CONTEXT_DEBUG_REGISTERS flag. Rather than importing these functions through the standard IAT — which would be trivially detectable — the function resolves them at runtime by XORing a stored value against a hardcoded key, then walks a manual offset table to locate each API.
int __cdecl GetBreakpoints(int *a1)
{
HANDLE hThread; // ebx - thread handle passed in via a1[0]
CONTEXT *ctx; // edi - pointer to CONTEXT structure
int apiResolver; // esi - obfuscated function pointer base
int contextResult; // eax - return value from GetThreadContext
int closeResult; // eax - return value from CloseHandle
hThread = *a1;
*a1 = 65552; // CONTEXT_DEBUG_REGISTERS (0x10010)
ctx = (_DWORD *)a1[2];
apiResolver = a1[1] ^ 0xD348A6; // deobfuscate GetThreadContext pointer
// Call GetThreadContext — populates ctx with DR0-DR7 debug registers
contextResult = (*(int (__stdcall **)(int, int *))apiResolver)(hThread, a1);
apiResolver -= 252; // step back to CloseHandle pointer
*a1 = contextResult;
// Advance into CONTEXT struct to the Dr7 field (offset 158)
ctx = (_DWORD *)((char *)ctx + 158);
*ctx = 0x90909090; // overwrite Dr7 with NOPs — clears all breakpoints
// CloseHandle(hThread)
closeResult = (*(int (__stdcall **)(_DWORD *, int))apiResolver)(ctx, 4);
// Final cleanup call (offset +212 — likely NtClose or similar)
return (*(int (__stdcall **)(int))(apiResolver + 212))(closeResult);
}
The flag 0x10010 (CONTEXT_DEBUG_REGISTERS) tells GetThreadContext to only populate the debug register fields — DR0 through DR7. It then advances 158 bytes into the CONTEXT struct, landing directly on Dr7, the debug control register that governs which hardware breakpoints are active. Writing 0x90909090 there disables them all.
The API resolution is deliberately obfuscated. a1[1] ^ 0xD348A6 decodes the GetThreadContext pointer at runtime, and subsequent calls are reached by fixed offsets (-252, +212) from that base rather than named imports — making static signature scanning against the IAT ineffective.
Lua String Check
The Lua String Check delivers one or more encrypted Lua chunks from the server, decrypts them in place using a rolling XOR cipher, and executes each one directly inside the client's Lua VM. This is the mechanism that allows Warmane to execute Lua code inside your game client — querying game state, inspecting the environment, or running any arbitrary script expressible in Lua without shipping a new binary to the client.
int __cdecl LuaStringCheck(int a1, int a2, int a3, CDataStore *inPacket)
{
char xorKey; // bl - rolling XOR key, evolves each byte
int remainingBytes; // edi - bytes left in packet after string count
_BYTE *currentStr; // esi - pointer to current string being decrypted
_BYTE *bufEnd; // edi - end of the string buffer
char decryptedByte; // bh - current decrypted byte
int result; // eax
int savedTaintA; // [esp+Ch] - saved dword_D4139C (Lua taint counter)
int savedTaintB; // [esp+10h] - saved dword_D413A0 (Lua exec depth)
_BYTE *strBuf; // [esp+14h] - allocated string buffer BYREF
// Read the initial XOR key and the packed string buffer from the packet
xorKey = CDataStore::GetInt8_4(inPacket);
remainingBytes = inPacket->m_size - inPacket->m_read;
CDataStore::GetStringCount(inPacket, (int)&strBuf, remainingBytes);
currentStr = strBuf;
bufEnd = strBuf + remainingBytes;
do
{
// Decrypt the next null-terminated string using a rolling XOR.
// Each byte updates the key: key += decrypted + 1
// This means decryption of any byte depends on all previous bytes.
strBuf = (int)currentStr;
do
{
decryptedByte = xorKey ^ *currentStr;
xorKey += decryptedByte + 1;
*currentStr++ = decryptedByte;
}
while ( decryptedByte );
// Compile the decrypted string as a Lua chunk
lua_loadbuffer(dword_D3F78C, strBuf, ¤tStr[-strBuf - 1], 0);
// Suppress Lua taint tracking and bump execution depth counter
// to hide this execution from the client's error handling system
savedTaintA = dword_D4139C;
savedTaintB = dword_D413A0;
dword_D4139C = 0;
++dword_D413A0;
// Execute the Lua chunk — no error handler, no return values
lua_pcall(dword_D3F78C, 0, 0, 0);
// Restore taint state and clean up the Lua stack
dword_D4139C = savedTaintA;
dword_D413A0 = savedTaintB;
result = lua_settop(dword_D3F78C, -1);
}
while ( currentStr != bufEnd );
return result;
}
Two details worth highlighting here.
The rolling XOR is not a simple single-key cipher. Each decrypted byte feeds back into the key via key += decrypted + 1, meaning the keystream is entirely dependent on the plaintext. Capturing one packet and replaying it against a patched client produces a different keystream and garbled output — a lightweight but effective replay deterrent.
The dword_D4139C and dword_D413A0 manipulation is deliberate suppression of Blizzard's Lua taint and execution depth tracking. Zeroing the taint counter and incrementing the depth counter before lua_pcall hides the server-injected execution from the client's own error handling and hook system — the Lua VM runs the chunk as if it were trusted first-party code with no visibility to the rest of the scripting environment.
Page Integrity Check
PageCheck walks the entire virtual address space of the client process using VirtualQuery, filters for committed executable pages within a specific size range, and SHA1-hashes 20 bytes at a fixed offset within each matching region. The digest is sent back to the server, which compares it against a known-good value. Any patch, hook, or code injection that lands in the target region will produce a mismatched hash.
int __usercall PageCheck@<eax>(char xorSeed@<cf>, unsigned int baseAddr@<eax>, unsigned int regionTable@<edi>, int outPacket@<esi>)
{
int outPacketCtx; // edi - reconstructed output packet pointer
MEMORY_BASIC_INFORMATION *mbi; // ebx - VirtualQuery result + SHA1 workspace
VirtualQuery_t VirtualQuery; // eax - runtime-resolved VirtualQuery pointer
int currentAddr; // esi - current address being walked
unsigned int queryResult; // eax - bytes returned by VirtualQuery
int protect; // eax - page protection flags
unsigned int regionSize; // eax - current region size
// Reconstruct outPacket pointer from obfuscated arguments
outPacketCtx = (__PAIR64__(regionTable, baseAddr)
- __PAIR64__(*(_DWORD *)(baseAddr - (xorSeed + *(_DWORD *)(regionTable + 2 * outPacket)) + 281),
(unsigned int)xorSeed + *(_DWORD *)(regionTable + 2 * outPacket))) >> 32;
// Allocate buffer: 0x1C for MEMORY_BASIC_INFORMATION + 0x78 for SHA1 state
mbi = (MEMORY_BASIC_INFORMATION *)malloc(0x94);
memset(mbi, 0, 0x94);
// Resolve VirtualQuery pointer from the inbound packet
VirtualQuery = *(VirtualQuery_t *)CDataStore::GetInt32(outPacket - 1);
mbi->VirtualQuery = VirtualQuery;
currentAddr = 0;
for ( queryResult = VirtualQuery(0, mbi, sizeof(MEMORY_BASIC_INFORMATION));
queryResult >= 0x1C;
queryResult = VirtualQuery(currentAddr, mbi, sizeof(MEMORY_BASIC_INFORMATION)) )
{
if ( mbi->State == MEM_COMMIT ) // only committed pages
{
protect = mbi->Protect;
if ( (protect & 0x700) == 0 && // no PAGE_GUARD / PAGE_NOCACHE
(protect & 0xF0) != 0 ) // must have execute permissions
{
regionSize = mbi->RegionSize;
if ( regionSize >= 0xB0000 && regionSize <= 0xB5000 ) // target size range
{
// Report address and size of the matched region
CDataStore::PutInt32(outPacketCtx, currentAddr);
CDataStore::PutInt32(outPacketCtx, mbi->RegionSize);
// SHA1-hash 20 bytes at a fixed offset within the region
memset(&mbi->sha1State, 0, 116);
SHA1Broken::Init(&mbi->sha1State);
SHA1::Update(&mbi->sha1State, currentAddr + 3986, 20);
SHA1::Final(&mbi->sha1State, &mbi->sha1Digest);
CDataStore::PutBytes(outPacketCtx, &mbi->sha1Digest, 20);
}
}
}
currentAddr += mbi->RegionSize;
}
return free(mbi);
}
The memory walk is the standard VirtualQuery pattern — start at address 0, advance by RegionSize each iteration until the call returns fewer than 28 bytes, indicating the end of the address space. The committed + executable filter (MEM_COMMIT, protection mask 0xF0) narrows the scan to code pages only.
The size filter (0xB0000–0xB5000, roughly 700–740 KB) fingerprints a specific module by its known mapped size rather than by name or base address — a more robust target that survives ASLR and module renaming. Notably, this approach is also effective against manually mapped DLLs. A standard LoadLibrary injection will appear in the module list and can be caught by ModuleScan, but a manually mapped DLL bypasses the loader entirely and leaves no entry in the module list. PageCheck catches these by scanning the raw address space — a manually mapped executable region will show up as a committed executable page and, if it falls within the size range, will be hashed and reported regardless of whether any loader knows it exists.
The SHA1 hash at offset 3986 is a fixed-point integrity probe. The server knows exactly what those 20 bytes should contain in an unmodified client. Any cheat, hook, or patch that lands in that region — even a single byte change — produces a different digest and fails the check.
One detail worth flagging: the SHA1 is initialized via SHA1Broken::Init rather than a standard implementation. This suggests a deliberately modified or stripped-down SHA1 — likely to avoid matching known library signatures while still producing a stable, collision-resistant fingerprint for the server's comparison.
Credits
This research would not have been possible without the contributions of the following individuals who provided insight, assistance, and collaboration throughout the reverse engineering process.
- Mike
- Harvestxy
- Lamani
- Ox
⚠ Warning to Warmane
We have a full eye on your anti-cheat system. Every routine will be dumped, documented, and published. Nothing stays hidden.