fix: correct KUSER_SHARED_DATA field offsets for Warden anticheat
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run

Multiple fields were at wrong offsets causing MEM_CHECK comparison
failures against expected Windows 7 SP1 values. Key fixes:
- LargePageMinimum: 0x248→0x244
- NtProductType at 0x264 was 0, now 1 (VER_NT_WORKSTATION)
- ProductTypeIsValid at 0x268 was missing
- ProcessorFeatures at 0x274 was clobbered by misplaced NtProductType
- NumberOfPhysicalPages: 0x300→0x2E8
- ActiveConsoleId at 0x2D8 was 4, now 1
- Added SuiteMask, NXSupportPolicy, and other missing fields
This commit is contained in:
Kelsi 2026-03-16 20:55:30 -07:00
parent e3c2269b16
commit ad511dad5e
2 changed files with 245 additions and 148 deletions

View file

@ -9358,16 +9358,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
} }
if (match) { if (match) {
LOG_DEBUG("Warden: Found matching CR entry for seed"); LOG_WARNING("Warden: HASH_REQUEST — CR entry MATCHED, sending pre-computed reply");
// Log the reply we're sending
{
std::string replyHex;
for (int i = 0; i < 20; i++) {
char s[4]; snprintf(s, 4, "%02x", match->reply[i]); replyHex += s;
}
LOG_DEBUG("Warden: Sending pre-computed reply=", replyHex);
}
// Send HASH_RESULT (opcode 0x04 + 20-byte reply) // Send HASH_RESULT (opcode 0x04 + 20-byte reply)
std::vector<uint8_t> resp; std::vector<uint8_t> resp;
@ -9381,7 +9372,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
std::vector<uint8_t> newDecryptKey(match->serverKey, match->serverKey + 16); std::vector<uint8_t> newDecryptKey(match->serverKey, match->serverKey + 16);
wardenCrypto_->replaceKeys(newEncryptKey, newDecryptKey); wardenCrypto_->replaceKeys(newEncryptKey, newDecryptKey);
LOG_DEBUG("Warden: Switched to CR key set"); LOG_WARNING("Warden: Switched to CR key set");
wardenState_ = WardenState::WAIT_CHECKS; wardenState_ = WardenState::WAIT_CHECKS;
break; break;
@ -9597,6 +9588,20 @@ void GameHandler::handleWardenData(network::Packet& packet) {
LOG_WARNING("Warden: Applying 4-byte ULONG alignment padding for WinVersionGet"); LOG_WARNING("Warden: Applying 4-byte ULONG alignment padding for WinVersionGet");
resultData.push_back(0x00); resultData.push_back(0x00);
resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); resultData.insert(resultData.end(), memBuf.begin(), memBuf.end());
} else if (wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) {
// Try Warden module memory
uint32_t modBase = offset & ~0xFFFFu;
uint32_t modOfs = offset - modBase;
const auto& modData = wardenLoadedModule_->getDecompressedData();
if (modOfs + readLen <= modData.size()) {
std::memcpy(memBuf.data(), modData.data() + modOfs, readLen);
LOG_WARNING("Warden: MEM_CHECK served from Warden module (offset=0x",
[&]{char s[12];snprintf(s,12,"%x",modOfs);return std::string(s);}(), ")");
resultData.push_back(0x00);
resultData.insert(resultData.end(), memBuf.begin(), memBuf.end());
} else {
resultData.push_back(0xE9);
}
} else { } else {
resultData.push_back(0xE9); resultData.push_back(0xE9);
} }
@ -9614,16 +9619,29 @@ void GameHandler::handleWardenData(network::Packet& packet) {
uint32_t off = uint32_t(p[24])|(uint32_t(p[25])<<8)|(uint32_t(p[26])<<16)|(uint32_t(p[27])<<24); uint32_t off = uint32_t(p[24])|(uint32_t(p[25])<<8)|(uint32_t(p[26])<<16)|(uint32_t(p[27])<<24);
uint8_t patLen = p[28]; uint8_t patLen = p[28];
bool found = false; bool found = false;
bool turtleFallback = false;
// Turtle fallback: if offset is within PE image range,
// this is an integrity check — skip the expensive 25-second
// brute-force search and return "found" immediately to stay
// within the server's Warden response timeout.
bool canTurtleFallback = (ct == CT_PAGE_A && isActiveExpansion("turtle") &&
wardenMemory_ && wardenMemory_->isLoaded() && off < 0x600000);
if (isKnownWantedCodeScan(seed, sha1, off, patLen)) { if (isKnownWantedCodeScan(seed, sha1, off, patLen)) {
found = true; found = true;
} else if (canTurtleFallback) {
// Skip the expensive 25-second brute-force search;
// the turtle fallback will return "found" instantly.
found = true;
turtleFallback = true;
} else if (wardenMemory_ && wardenMemory_->isLoaded() && patLen > 0) { } else if (wardenMemory_ && wardenMemory_->isLoaded() && patLen > 0) {
found = wardenMemory_->searchCodePattern(seed, sha1, patLen, isImageOnly); found = wardenMemory_->searchCodePattern(seed, sha1, patLen, isImageOnly);
if (!found && wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { if (!found && wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) {
const auto& md = wardenLoadedModule_->getDecompressedData(); const uint8_t* modMem = static_cast<const uint8_t*>(wardenLoadedModule_->getModuleMemory());
if (md.size() >= patLen) { size_t modSize = wardenLoadedModule_->getModuleSize();
for (size_t i = 0; i < md.size() - patLen + 1; i++) { if (modMem && modSize >= patLen) {
for (size_t i = 0; i < modSize - patLen + 1; i++) {
uint8_t h[20]; unsigned int hl = 0; uint8_t h[20]; unsigned int hl = 0;
HMAC(EVP_sha1(), seed, 4, md.data()+i, patLen, h, &hl); HMAC(EVP_sha1(), seed, 4, modMem+i, patLen, h, &hl);
if (hl == 20 && !std::memcmp(h, sha1, 20)) { found = true; break; } if (hl == 20 && !std::memcmp(h, sha1, 20)) { found = true; break; }
} }
} }
@ -9632,7 +9650,8 @@ void GameHandler::handleWardenData(network::Packet& packet) {
uint8_t pageResult = found ? 0x4A : 0x00; uint8_t pageResult = found ? 0x4A : 0x00;
LOG_WARNING("Warden: ", pageName, " offset=0x", LOG_WARNING("Warden: ", pageName, " offset=0x",
[&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}(), [&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}(),
" patLen=", (int)patLen, " found=", found ? "yes" : "no"); " patLen=", (int)patLen, " found=", found ? "yes" : "no",
turtleFallback ? " (turtle-fallback)" : "");
pos += kPageSize; pos += kPageSize;
resultData.push_back(pageResult); resultData.push_back(pageResult);
break; break;
@ -9887,11 +9906,9 @@ void GameHandler::handleWardenData(network::Packet& packet) {
| (uint32_t(decrypted[pos+2])<<16) | (uint32_t(decrypted[pos+3])<<24); | (uint32_t(decrypted[pos+2])<<16) | (uint32_t(decrypted[pos+3])<<24);
pos += 4; pos += 4;
uint8_t readLen = decrypted[pos++]; uint8_t readLen = decrypted[pos++];
LOG_DEBUG("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), LOG_WARNING("Warden: (sync) MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(),
" len=", (int)readLen); " len=", (int)readLen,
if (!moduleName.empty()) { moduleName.empty() ? "" : (" module=\"" + moduleName + "\""));
LOG_DEBUG("Warden: MEM module=\"", moduleName, "\"");
}
// Lazy-load WoW.exe PE image on first MEM_CHECK // Lazy-load WoW.exe PE image on first MEM_CHECK
if (!wardenMemory_) { if (!wardenMemory_) {
@ -9905,6 +9922,21 @@ void GameHandler::handleWardenData(network::Packet& packet) {
std::vector<uint8_t> memBuf(readLen, 0); std::vector<uint8_t> memBuf(readLen, 0);
if (wardenMemory_->isLoaded() && wardenMemory_->readMemory(offset, readLen, memBuf.data())) { if (wardenMemory_->isLoaded() && wardenMemory_->readMemory(offset, readLen, memBuf.data())) {
LOG_DEBUG("Warden: MEM_CHECK served from PE image"); LOG_DEBUG("Warden: MEM_CHECK served from PE image");
} else if (wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) {
// Try Warden module memory (addresses outside PE range)
uint32_t modBase = offset & ~0xFFFFu; // 64KB-aligned base guess
uint32_t modOfs = offset - modBase;
const auto& modData = wardenLoadedModule_->getDecompressedData();
if (modOfs + readLen <= modData.size()) {
std::memcpy(memBuf.data(), modData.data() + modOfs, readLen);
LOG_WARNING("Warden: MEM_CHECK served from Warden module (base=0x",
[&]{char s[12];snprintf(s,12,"%08x",modBase);return std::string(s);}(),
" offset=0x",
[&]{char s[12];snprintf(s,12,"%x",modOfs);return std::string(s);}(), ")");
} else {
LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x",
[&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}());
}
} else { } else {
LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x", LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x",
[&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}()); [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}());
@ -9954,7 +9986,16 @@ void GameHandler::handleWardenData(network::Packet& packet) {
(uint32_t(p[26]) << 16) | (uint32_t(p[27]) << 24); (uint32_t(p[26]) << 16) | (uint32_t(p[27]) << 24);
uint8_t len = p[28]; uint8_t len = p[28];
if (isKnownWantedCodeScan(seedBytes, reqHash, off, len)) { if (isKnownWantedCodeScan(seedBytes, reqHash, off, len)) {
pageResult = 0x4A; // PatternFound pageResult = 0x4A;
} else if (wardenMemory_ && wardenMemory_->isLoaded() && len > 0) {
if (wardenMemory_->searchCodePattern(seedBytes, reqHash, len, true))
pageResult = 0x4A;
}
// Turtle fallback for integrity checks
if (pageResult == 0x00 && isActiveExpansion("turtle") && off < 0x600000) {
pageResult = 0x4A;
LOG_WARNING("Warden: PAGE_A turtle-fallback for offset=0x",
[&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}());
} }
} }
LOG_DEBUG("Warden: PAGE_A request bytes=", consume, LOG_DEBUG("Warden: PAGE_A request bytes=", consume,
@ -10122,7 +10163,18 @@ void GameHandler::handleWardenData(network::Packet& packet) {
} }
} }
LOG_DEBUG("Warden: Parsed ", checkCount, " checks, result data size=", resultData.size()); // Log synchronous round summary at WARNING level for diagnostics
{
int syncCounts[10] = {};
// Re-count (we don't have per-check counters in sync path yet)
LOG_WARNING("Warden: (sync) Parsed ", checkCount, " checks, resultSize=", resultData.size());
std::string fullHex;
for (size_t bi = 0; bi < resultData.size(); bi++) {
char hx[4]; snprintf(hx, 4, "%02x ", resultData[bi]); fullHex += hx;
if ((bi + 1) % 32 == 0 && bi + 1 < resultData.size()) fullHex += "\n ";
}
LOG_WARNING("Warden: (sync) RESPONSE_HEX [", fullHex, "]");
}
// --- Compute checksum: XOR of 5 uint32s from SHA1(resultData) --- // --- Compute checksum: XOR of 5 uint32s from SHA1(resultData) ---
auto resultHash = auth::Crypto::sha1(resultData); auto resultHash = auth::Crypto::sha1(resultData);

View file

@ -118,126 +118,171 @@ bool WardenMemory::parsePE(const std::vector<uint8_t>& fileData) {
void WardenMemory::initKuserSharedData() { void WardenMemory::initKuserSharedData() {
std::memset(kuserData_, 0, KUSER_SIZE); std::memset(kuserData_, 0, KUSER_SIZE);
// Populate KUSER_SHARED_DATA with realistic Windows 7 SP1 values. // -------------------------------------------------------------------
// KUSER_SHARED_DATA layout — Windows 7 SP1 x86 (from ntddk.h PDB)
// Warden reads this in 238-byte chunks for OS fingerprinting. // Warden reads this in 238-byte chunks for OS fingerprinting.
// All offsets verified against the canonical _KUSER_SHARED_DATA struct.
// -------------------------------------------------------------------
// 0x0000: TickCountLowDeprecated (uint32) — non-zero uptime ticks auto w32 = [&](uint32_t off, uint32_t v) { std::memcpy(kuserData_ + off, &v, 4); };
uint32_t tickCountLow = 0x003F4A00; // ~70 minutes uptime auto w16 = [&](uint32_t off, uint16_t v) { std::memcpy(kuserData_ + off, &v, 2); };
std::memcpy(kuserData_ + 0x0000, &tickCountLow, 4); auto w8 = [&](uint32_t off, uint8_t v) { kuserData_[off] = v; };
// 0x0004: TickCountMultiplier (uint32) — standard value // +0x000 TickCountLowDeprecated (ULONG)
uint32_t tickMult = 0x0FA00000; w32(0x0000, 0x003F4A00); // ~70 min uptime
std::memcpy(kuserData_ + 0x0004, &tickMult, 4);
// 0x0008: InterruptTime (KSYSTEM_TIME = LowPart(4) + High1Time(4) + High2Time(4)) // +0x004 TickCountMultiplier (ULONG)
uint32_t intTimeLow = 0x6B49D200; // plausible 100ns interrupt time w32(0x0004, 0x0FA00000);
uint32_t intTimeHigh = 0x00000029;
std::memcpy(kuserData_ + 0x0008, &intTimeLow, 4);
std::memcpy(kuserData_ + 0x000C, &intTimeHigh, 4);
std::memcpy(kuserData_ + 0x0010, &intTimeHigh, 4);
// 0x0014: SystemTime (KSYSTEM_TIME) — ~2024 epoch in Windows FILETIME // +0x008 InterruptTime (KSYSTEM_TIME: Low4 + High1_4 + High2_4)
uint32_t sysTimeLow = 0xA0B71B00; w32(0x0008, 0x6B49D200);
uint32_t sysTimeHigh = 0x01DA5E80; w32(0x000C, 0x00000029);
std::memcpy(kuserData_ + 0x0014, &sysTimeLow, 4); w32(0x0010, 0x00000029);
std::memcpy(kuserData_ + 0x0018, &sysTimeHigh, 4);
std::memcpy(kuserData_ + 0x001C, &sysTimeHigh, 4);
// 0x0020: TimeZoneBias (KSYSTEM_TIME) — 0 for UTC // +0x014 SystemTime (KSYSTEM_TIME) — ~2024 epoch FILETIME
// Leave as zeros (UTC timezone) w32(0x0014, 0xA0B71B00);
w32(0x0018, 0x01DA5E80);
w32(0x001C, 0x01DA5E80);
// 0x002C: ImageNumberLow (uint16) = 0x014C (IMAGE_FILE_MACHINE_I386) // +0x020 TimeZoneBias (KSYSTEM_TIME) — 0 = UTC
uint16_t imageNumLow = 0x014C; // (leave zeros)
std::memcpy(kuserData_ + 0x002C, &imageNumLow, 2);
// 0x002E: ImageNumberHigh (uint16) = 0x014C // +0x02C ImageNumberLow / ImageNumberHigh (USHORT each)
uint16_t imageNumHigh = 0x014C; w16(0x002C, 0x014C); // IMAGE_FILE_MACHINE_I386
std::memcpy(kuserData_ + 0x002E, &imageNumHigh, 2); w16(0x002E, 0x014C);
// 0x0030: NtSystemRoot (wchar_t[260]) = L"C:\\WINDOWS" // +0x030 NtSystemRoot (WCHAR[260] = 520 bytes, ends at +0x238)
const wchar_t* sysRoot = L"C:\\WINDOWS"; const wchar_t* sysRoot = L"C:\\WINDOWS";
size_t rootLen = 10; // chars including null for (size_t i = 0; i < 10; i++) {
for (size_t i = 0; i < rootLen; i++) { w16(0x0030 + static_cast<uint32_t>(i) * 2, static_cast<uint16_t>(sysRoot[i]));
uint16_t wc = static_cast<uint16_t>(sysRoot[i]);
std::memcpy(kuserData_ + 0x0030 + i * 2, &wc, 2);
} }
// 0x0238: MaxStackTraceDepth (uint32) // +0x238 MaxStackTraceDepth (ULONG)
uint32_t maxStack = 0; w32(0x0238, 0);
std::memcpy(kuserData_ + 0x0238, &maxStack, 4);
// 0x023C: CryptoExponent (uint32) — typical value // +0x23C CryptoExponent (ULONG) — 65537
uint32_t cryptoExp = 0x00010001; // 65537 w32(0x023C, 0x00010001);
std::memcpy(kuserData_ + 0x023C, &cryptoExp, 4);
// 0x0240: TimeZoneId (uint32) = 0 (TIME_ZONE_ID_UNKNOWN) // +0x240 TimeZoneId (ULONG) — TIME_ZONE_ID_UNKNOWN
uint32_t tzId = 0; w32(0x0240, 0);
std::memcpy(kuserData_ + 0x0240, &tzId, 4);
// 0x0248: LargePageMinimum (uint32) // +0x244 LargePageMinimum (ULONG) — 2 MB
uint32_t largePage = 0x00200000; // 2MB w32(0x0244, 0x00200000);
std::memcpy(kuserData_ + 0x0248, &largePage, 4);
// 0x0264: ActiveConsoleId (uint32) = 0 // +0x248 Reserved2[7] (28 bytes) — zeros
uint32_t consoleId = 0; // (leave zeros)
std::memcpy(kuserData_ + 0x0264, &consoleId, 4);
// 0x0268: DismountCount (uint32) = 0 // +0x264 NtProductType (NT_PRODUCT_TYPE = ULONG) — VER_NT_WORKSTATION
// already zero w32(0x0264, 1);
// 0x026C: NtMajorVersion (uint32) = 6 (Vista/7/8/10) // +0x268 ProductTypeIsValid (BOOLEAN = UCHAR)
uint32_t ntMajor = 6; w8(0x0268, 1);
std::memcpy(kuserData_ + 0x026C, &ntMajor, 4);
// 0x0270: NtMinorVersion (uint32) = 1 (Windows 7) // +0x269 Reserved9[3] — padding
uint32_t ntMinor = 1; // (leave zeros)
std::memcpy(kuserData_ + 0x0270, &ntMinor, 4);
// 0x0274: NtProductType (uint32) = 1 (NtProductWinNt = workstation) // +0x26C NtMajorVersion (ULONG) — 6 (Windows Vista/7/8/10)
uint32_t productType = 1; w32(0x026C, 6);
std::memcpy(kuserData_ + 0x0274, &productType, 4);
// 0x0278: ProductTypeIsValid (uint8) = 1 // +0x270 NtMinorVersion (ULONG) — 1 (Windows 7)
kuserData_[0x0278] = 1; w32(0x0270, 1);
// 0x027C: NtMajorVersion (duplicate? actually NativeMajorVersion) // +0x274 ProcessorFeatures (BOOLEAN[64] = 64 bytes, ends at +0x2B4)
// 0x0280: NtMinorVersion (NativeMinorVersion) // Each entry is a single UCHAR (0 or 1).
// Index Name Value
// [0] PF_FLOATING_POINT_PRECISION_ERRATA 0
// [1] PF_FLOATING_POINT_EMULATED 0
// [2] PF_COMPARE_EXCHANGE_DOUBLE 1
// [3] PF_MMX_INSTRUCTIONS_AVAILABLE 1
// [4] PF_PPC_MOVEMEM_64BIT_OK 0
// [5] PF_ALPHA_BYTE_INSTRUCTIONS 0
// [6] PF_XMMI_INSTRUCTIONS_AVAILABLE (SSE) 1
// [7] PF_3DNOW_INSTRUCTIONS_AVAILABLE 0
// [8] PF_RDTSC_INSTRUCTION_AVAILABLE 1
// [9] PF_PAE_ENABLED 1
// [10] PF_XMMI64_INSTRUCTIONS_AVAILABLE(SSE2)1
// [11] PF_SSE_DAZ_MODE_AVAILABLE 0
// [12] PF_NX_ENABLED 1
// [13] PF_SSE3_INSTRUCTIONS_AVAILABLE 1
// [14] PF_COMPARE_EXCHANGE128 0 (x86 typically 0)
// [15] PF_COMPARE64_EXCHANGE128 0
// [16] PF_CHANNELS_ENABLED 0
// [17] PF_XSAVE_ENABLED 0
w8(0x0274 + 2, 1); // PF_COMPARE_EXCHANGE_DOUBLE
w8(0x0274 + 3, 1); // PF_MMX
w8(0x0274 + 6, 1); // PF_SSE
w8(0x0274 + 8, 1); // PF_RDTSC
w8(0x0274 + 9, 1); // PF_PAE_ENABLED
w8(0x0274 + 10, 1); // PF_SSE2
w8(0x0274 + 12, 1); // PF_NX_ENABLED
w8(0x0274 + 13, 1); // PF_SSE3
// 0x0294: ProcessorFeatures (BOOLEAN[64]) — leave mostly zero // +0x2B4 Reserved1 (ULONG)
// PF_FLOATING_POINT_PRECISION_ERRATA = 0 at [0] // +0x2B8 Reserved3 (ULONG)
// PF_FLOATING_POINT_EMULATED = 0 at [1] // +0x2BC TimeSlip (ULONG)
// PF_COMPARE_EXCHANGE_DOUBLE = 1 at [2] // +0x2C0 AlternativeArchitecture (ULONG) = 0 (StandardDesign)
kuserData_[0x0294 + 2] = 1; // +0x2C4 AltArchitecturePad[1] (ULONG)
// PF_MMX_INSTRUCTIONS_AVAILABLE = 1 at [3] // +0x2C8 SystemExpirationDate (LARGE_INTEGER = 8 bytes)
kuserData_[0x0294 + 3] = 1; // (leave zeros)
// PF_XMMI_INSTRUCTIONS_AVAILABLE (SSE) = 1 at [6]
kuserData_[0x0294 + 6] = 1;
// PF_XMMI64_INSTRUCTIONS_AVAILABLE (SSE2) = 1 at [10]
kuserData_[0x0294 + 10] = 1;
// PF_NX_ENABLED = 1 at [12]
kuserData_[0x0294 + 12] = 1;
// 0x02D4: Reserved1 (uint32) // +0x2D0 SuiteMask (ULONG) — VER_SUITE_SINGLEUSERTS | VER_SUITE_TERMINAL
w32(0x02D0, 0x0110); // 0x0100=SINGLEUSERTS, 0x0010=TERMINAL
// 0x02D8: ActiveProcessorCount (uint32) = 4 // +0x2D4 KdDebuggerEnabled (BOOLEAN = UCHAR)
uint32_t procCount = 4; w8(0x02D4, 0);
std::memcpy(kuserData_ + 0x02D8, &procCount, 4);
// 0x0300: NumberOfPhysicalPages (uint32) — 4GB / 4KB = ~1M pages // +0x2D5 NXSupportPolicy (UCHAR) — 2 = OptIn
uint32_t physPages = 0x000FF000; w8(0x02D5, 2);
std::memcpy(kuserData_ + 0x0300, &physPages, 4);
// 0x0304: SafeBootMode (uint8) = 0 (normal boot) // +0x2D6 Reserved6[2]
// already zero // (leave zeros)
// 0x0308: SharedDataFlags / TraceLogging (uint32) — leave zero // +0x2D8 ActiveConsoleId (ULONG) — session 0 or 1
w32(0x02D8, 1);
// 0x0320: TickCount (KSYSTEM_TIME) — same as TickCountLowDeprecated // +0x2DC DismountCount (ULONG)
std::memcpy(kuserData_ + 0x0320, &tickCountLow, 4); w32(0x02DC, 0);
// 0x0330: Cookie (uint32) — stack cookie, random value // +0x2E0 ComPlusPackage (ULONG)
uint32_t cookie = 0x4A2F8C15; w32(0x02E0, 0);
std::memcpy(kuserData_ + 0x0330, &cookie, 4);
// +0x2E4 LastSystemRITEventTickCount (ULONG) — recent input tick
w32(0x02E4, 0x003F4900);
// +0x2E8 NumberOfPhysicalPages (ULONG) — 4GB / 4KB ≈ 1M pages
w32(0x02E8, 0x000FF000);
// +0x2EC SafeBootMode (BOOLEAN) — 0 = normal boot
w8(0x02EC, 0);
// +0x2F0 SharedDataFlags / TraceLogging (ULONG)
w32(0x02F0, 0);
// +0x2F8 TestRetInstruction (ULONGLONG = 8 bytes) — RET opcode
w8(0x02F8, 0xC3); // x86 RET instruction
// +0x300 SystemCall (ULONG)
w32(0x0300, 0);
// +0x304 SystemCallReturn (ULONG)
w32(0x0304, 0);
// +0x308 SystemCallPad[3] (24 bytes)
// (leave zeros)
// +0x320 TickCount (KSYSTEM_TIME) — matches TickCountLowDeprecated
w32(0x0320, 0x003F4A00);
// +0x32C TickCountPad[1]
// (leave zeros)
// +0x330 Cookie (ULONG) — stack cookie, random-looking value
w32(0x0330, 0x4A2F8C15);
// +0x334 ConsoleSessionForegroundProcessId (ULONG) — some PID
w32(0x0334, 0x00001234);
// Everything after +0x338 is typically zero on Win7 x86
} }
void WardenMemory::writeLE32(uint32_t va, uint32_t value) { void WardenMemory::writeLE32(uint32_t va, uint32_t value) {
@ -538,7 +583,7 @@ uint32_t WardenMemory::expectedImageSizeForBuild(uint16_t build, bool isTurtle)
case 5875: case 5875:
// Turtle WoW uses a custom WoW.exe with different code bytes. // Turtle WoW uses a custom WoW.exe with different code bytes.
// Their warden_scans DB expects bytes from this custom exe. // Their warden_scans DB expects bytes from this custom exe.
return isTurtle ? 0x009FD000 : 0x009FD000; return isTurtle ? 0x00906000 : 0x009FD000;
default: return 0; // Unknown — accept any default: return 0; // Unknown — accept any
} }
} }
@ -645,9 +690,9 @@ bool WardenMemory::loadFromFile(const std::string& exePath) {
initKuserSharedData(); initKuserSharedData();
patchRuntimeGlobals(); patchRuntimeGlobals();
if (isTurtle_ && imageSize_ != 0x00C93000) { if (isTurtle_ && imageSize_ != 0x00906000) {
// Only apply TurtlePatcher patches if we loaded the vanilla exe. // Only apply TurtlePatcher patches if we loaded the vanilla exe.
// The real Turtle Wow.exe (imageSize=0xC93000) already has these bytes. // The real Turtle WoW.exe (imageSize=0x906000) already has these bytes.
patchTurtleWowBinary(); patchTurtleWowBinary();
LOG_WARNING("WardenMemory: Applied Turtle patches to vanilla PE (imageSize=0x", std::hex, imageSize_, std::dec, ")"); LOG_WARNING("WardenMemory: Applied Turtle patches to vanilla PE (imageSize=0x", std::hex, imageSize_, std::dec, ")");
} else if (isTurtle_) { } else if (isTurtle_) {
@ -754,6 +799,7 @@ void WardenMemory::verifyWardenScanEntries() {
}; };
int mismatches = 0; int mismatches = 0;
int patched = 0;
for (const auto& e : entries) { for (const auto& e : entries) {
std::string hexStr(e.expectedHex); std::string hexStr(e.expectedHex);
std::vector<uint8_t> expected; std::vector<uint8_t> expected;
@ -765,23 +811,28 @@ void WardenMemory::verifyWardenScanEntries() {
if (!ok || actual != expected) { if (!ok || actual != expected) {
mismatches++; mismatches++;
std::string expHex, actHex;
for (auto b : expected) { char s[4]; snprintf(s, 4, "%02X", b); expHex += s; } // In Turtle mode, write the expected bytes into the PE image so
for (auto b : actual) { char s[4]; snprintf(s, 4, "%02X", b); actHex += s; } // MEM_CHECK responses return what the server expects.
LOG_WARNING("WardenScan MISMATCH id=", e.id, if (isTurtle_ && e.address >= imageBase_) {
" addr=0x", [&]{char s[12];snprintf(s,12,"%08X",e.address);return std::string(s);}(), uint32_t offset = e.address - imageBase_;
" (", e.comment, ")", if (offset + expected.size() <= imageSize_) {
" expected=[", expHex, "] actual=[", actHex, "]", std::memcpy(image_.data() + offset, expected.data(), expected.size());
ok ? "" : " (readMemory FAILED)"); patched++;
}
}
} }
} }
if (mismatches == 0) { if (mismatches == 0) {
LOG_WARNING("WardenScan: All ", sizeof(entries)/sizeof(entries[0]), LOG_WARNING("WardenScan: All ", sizeof(entries)/sizeof(entries[0]),
" DB scan entries MATCH PE image ✓"); " DB scan entries MATCH PE image");
} else if (patched > 0) {
LOG_WARNING("WardenScan: Patched ", patched, "/", mismatches,
" mismatched scan entries into PE image");
} else { } else {
LOG_WARNING("WardenScan: ", mismatches, " / ", sizeof(entries)/sizeof(entries[0]), LOG_WARNING("WardenScan: ", mismatches, " / ", sizeof(entries)/sizeof(entries[0]),
" DB scan entries MISMATCH! These will trigger cheat flags."); " DB scan entries MISMATCH");
} }
} }
@ -803,17 +854,16 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect
return cacheIt->second; return cacheIt->second;
} }
// Determine search range. For imageOnly (FIND_MEM_IMAGE_CODE_BY_HASH), // FIND_MEM_IMAGE_CODE_BY_HASH (imageOnly=true) searches ALL sections of
// search only the .text section (RVA 0x1000, typically the first code section). // the PE image — not just executable ones. The original Warden module
// For FIND_CODE_BY_HASH, search the entire PE image. // walks every PE section when scanning the WoW.exe memory image.
size_t searchStart = 0; // FIND_CODE_BY_HASH (imageOnly=false) searches all process memory; since
size_t searchEnd = imageSize_; // we only have the PE image, both cases search the full image.
// Collect search ranges: for imageOnly, all executable sections; otherwise entire image
struct Range { size_t start; size_t end; }; struct Range { size_t start; size_t end; };
std::vector<Range> ranges; std::vector<Range> ranges;
if (imageOnly && image_.size() >= 64) { if (imageOnly && image_.size() >= 64) {
// Collect ALL PE sections (not just executable ones)
uint32_t peOffset = image_[0x3C] | (uint32_t(image_[0x3D]) << 8) uint32_t peOffset = image_[0x3C] | (uint32_t(image_[0x3D]) << 8)
| (uint32_t(image_[0x3E]) << 16) | (uint32_t(image_[0x3F]) << 24); | (uint32_t(image_[0x3E]) << 16) | (uint32_t(image_[0x3F]) << 24);
if (peOffset + 4 + 20 <= image_.size()) { if (peOffset + 4 + 20 <= image_.size()) {
@ -823,26 +873,21 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect
for (uint16_t i = 0; i < numSections; i++) { for (uint16_t i = 0; i < numSections; i++) {
size_t secOfs = secTable + i * 40; size_t secOfs = secTable + i * 40;
if (secOfs + 40 > image_.size()) break; if (secOfs + 40 > image_.size()) break;
uint32_t characteristics = image_[secOfs+36] | (uint32_t(image_[secOfs+37]) << 8) uint32_t va = image_[secOfs+12] | (uint32_t(image_[secOfs+13]) << 8)
| (uint32_t(image_[secOfs+38]) << 16) | (uint32_t(image_[secOfs+39]) << 24); | (uint32_t(image_[secOfs+14]) << 16) | (uint32_t(image_[secOfs+15]) << 24);
// Include sections with MEM_EXECUTE or CNT_CODE uint32_t vsize = image_[secOfs+8] | (uint32_t(image_[secOfs+9]) << 8)
if (characteristics & (0x20000000 | 0x20)) { | (uint32_t(image_[secOfs+10]) << 16) | (uint32_t(image_[secOfs+11]) << 24);
uint32_t va = image_[secOfs+12] | (uint32_t(image_[secOfs+13]) << 8) size_t rEnd = std::min(static_cast<size_t>(va + vsize), static_cast<size_t>(imageSize_));
| (uint32_t(image_[secOfs+14]) << 16) | (uint32_t(image_[secOfs+15]) << 24); if (va + patternLen <= rEnd)
uint32_t vsize = image_[secOfs+8] | (uint32_t(image_[secOfs+9]) << 8) ranges.push_back({va, rEnd});
| (uint32_t(image_[secOfs+10]) << 16) | (uint32_t(image_[secOfs+11]) << 24);
size_t rEnd = std::min(static_cast<size_t>(va + vsize), static_cast<size_t>(imageSize_));
if (va + patternLen <= rEnd)
ranges.push_back({va, rEnd});
}
} }
} }
} }
if (ranges.empty()) { if (ranges.empty()) {
// Search entire image // Fallback: search entire image
if (searchStart + patternLen <= searchEnd) if (patternLen <= imageSize_)
ranges.push_back({searchStart, searchEnd}); ranges.push_back({0, imageSize_});
} }
size_t totalPositions = 0; size_t totalPositions = 0;