Fix Warden module parse fallback and macOS FMOD integrity aliases

This commit is contained in:
Kelsi 2026-02-25 09:26:34 -08:00
parent 1fab17e639
commit fc68c6c6b7
2 changed files with 178 additions and 83 deletions

View file

@ -3,6 +3,7 @@
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <vector>
namespace wowee { namespace wowee {
namespace auth { namespace auth {
@ -41,39 +42,46 @@ bool computeIntegrityHashWin32WithExe(const std::array<uint8_t, 16>& checksumSal
// that distribution rather than a stock 1.12.1 client, so when using Turtle's executable we include // that distribution rather than a stock 1.12.1 client, so when using Turtle's executable we include
// Turtle-specific DLLs as well. // Turtle-specific DLLs as well.
const bool isTurtleExe = (exeName == "TurtleWoW.exe"); const bool isTurtleExe = (exeName == "TurtleWoW.exe");
const char* kFilesBase[] = { // Some macOS client layouts use FMOD dylib naming instead of fmod.dll.
nullptr, // exeName // We accept the first matching filename in each alias group.
"fmod.dll", std::vector<std::vector<std::string>> fileGroups = {
"ijl15.dll", { exeName },
"dbghelp.dll", { "fmod.dll", "fmod.dylib", "libfmod.dylib", "fmodex.dll", "fmodex.dylib", "libfmod.so" },
"unicows.dll", { "ijl15.dll" },
{ "dbghelp.dll" },
{ "unicows.dll" },
}; };
const char* kFilesTurtleExtra[] = {
"twloader.dll",
"twdiscord.dll",
};
std::vector<std::string> files;
files.reserve(1 + 4 + (isTurtleExe ? (sizeof(kFilesTurtleExtra) / sizeof(kFilesTurtleExtra[0])) : 0));
for (const char* f : kFilesBase) {
files.push_back(f ? std::string(f) : exeName);
}
if (isTurtleExe) { if (isTurtleExe) {
for (const char* f : kFilesTurtleExtra) files.push_back(std::string(f)); fileGroups.push_back({ "twloader.dll" });
fileGroups.push_back({ "twdiscord.dll" });
} }
std::vector<uint8_t> allFiles; std::vector<uint8_t> allFiles;
std::string err; for (const auto& group : fileGroups) {
for (const auto& nameStr : files) { bool foundInGroup = false;
std::vector<uint8_t> bytes; std::string groupErr;
std::string path = miscDir;
if (!path.empty() && path.back() != '/') path += '/'; for (const auto& nameStr : group) {
path += nameStr; std::vector<uint8_t> bytes;
if (!readWholeFile(path, bytes, err)) { std::string path = miscDir;
outError = err; if (!path.empty() && path.back() != '/') path += '/';
path += nameStr;
std::string err;
if (!readWholeFile(path, bytes, err)) {
if (groupErr.empty()) groupErr = err;
continue;
}
allFiles.insert(allFiles.end(), bytes.begin(), bytes.end());
foundInGroup = true;
break;
}
if (!foundInGroup) {
outError = groupErr.empty() ? "missing required integrity file group" : groupErr;
return false; return false;
} }
allFiles.insert(allFiles.end(), bytes.begin(), bytes.end());
} }
// HMAC_SHA1(checksumSalt, allFiles) // HMAC_SHA1(checksumSalt, allFiles)

View file

@ -529,78 +529,165 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at " std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at "
<< moduleMemory_ << '\n'; << moduleMemory_ << '\n';
// Parse copy/skip pairs (MaNGOS/TrinityCore format) auto readU16LE = [&](size_t at) -> uint16_t {
// Format: repeated [2B copy_count][copy_count bytes data][2B skip_count] return static_cast<uint16_t>(exeData[at] | (exeData[at + 1] << 8));
// Copy = copy from source to dest, Skip = advance dest pointer (zeros) };
// Terminates when copy_count == 0
size_t pos = 4; // Skip 4-byte size header
size_t destOffset = 0;
int pairCount = 0;
while (pos + 2 <= exeData.size()) { enum class PairFormat {
// Read copy count (2 bytes LE) CopyDataSkip, // [copy][data][skip]
uint16_t copyCount = exeData[pos] | (exeData[pos + 1] << 8); SkipCopyData, // [skip][copy][data]
pos += 2; CopySkipData // [copy][skip][data]
};
if (copyCount == 0) { auto tryParsePairs = [&](PairFormat format,
break; // End of copy/skip pairs std::vector<uint8_t>& imageOut,
} size_t& relocPosOut,
size_t& finalOffsetOut,
int& pairCountOut) -> bool {
imageOut.assign(moduleSize_, 0);
size_t pos = 4; // Skip 4-byte final size header
size_t destOffset = 0;
int pairCount = 0;
if (copyCount > 0) { while (pos + 2 <= exeData.size()) {
if (pos + copyCount > exeData.size()) { uint16_t copyCount = 0;
std::cerr << "[WardenModule] Copy section extends beyond data bounds" << '\n'; uint16_t skipCount = 0;
#ifdef _WIN32
VirtualFree(moduleMemory_, 0, MEM_RELEASE); switch (format) {
#else case PairFormat::CopyDataSkip: {
munmap(moduleMemory_, moduleSize_); copyCount = readU16LE(pos);
#endif pos += 2;
moduleMemory_ = nullptr; if (copyCount == 0) {
return false; relocPosOut = pos;
finalOffsetOut = destOffset;
pairCountOut = pairCount;
imageOut.resize(moduleSize_);
return true;
}
if (pos + copyCount > exeData.size() || destOffset + copyCount > moduleSize_) {
return false;
}
std::memcpy(imageOut.data() + destOffset, exeData.data() + pos, copyCount);
pos += copyCount;
destOffset += copyCount;
if (pos + 2 > exeData.size()) {
return false;
}
skipCount = readU16LE(pos);
pos += 2;
break;
}
case PairFormat::SkipCopyData: {
if (pos + 4 > exeData.size()) {
return false;
}
skipCount = readU16LE(pos);
pos += 2;
copyCount = readU16LE(pos);
pos += 2;
if (skipCount == 0 && copyCount == 0) {
relocPosOut = pos;
finalOffsetOut = destOffset;
pairCountOut = pairCount;
imageOut.resize(moduleSize_);
return true;
}
if (destOffset + skipCount > moduleSize_) {
return false;
}
destOffset += skipCount;
if (pos + copyCount > exeData.size() || destOffset + copyCount > moduleSize_) {
return false;
}
std::memcpy(imageOut.data() + destOffset, exeData.data() + pos, copyCount);
pos += copyCount;
destOffset += copyCount;
break;
}
case PairFormat::CopySkipData: {
if (pos + 4 > exeData.size()) {
return false;
}
copyCount = readU16LE(pos);
pos += 2;
skipCount = readU16LE(pos);
pos += 2;
if (copyCount == 0 && skipCount == 0) {
relocPosOut = pos;
finalOffsetOut = destOffset;
pairCountOut = pairCount;
imageOut.resize(moduleSize_);
return true;
}
if (pos + copyCount > exeData.size() || destOffset + copyCount > moduleSize_) {
return false;
}
std::memcpy(imageOut.data() + destOffset, exeData.data() + pos, copyCount);
pos += copyCount;
destOffset += copyCount;
break;
}
} }
if (destOffset + copyCount > moduleSize_) { if (destOffset + skipCount > moduleSize_) {
std::cerr << "[WardenModule] Copy section exceeds module size" << '\n';
#ifdef _WIN32
VirtualFree(moduleMemory_, 0, MEM_RELEASE);
#else
munmap(moduleMemory_, moduleSize_);
#endif
moduleMemory_ = nullptr;
return false; return false;
} }
destOffset += skipCount;
std::memcpy( pairCount++;
static_cast<uint8_t*>(moduleMemory_) + destOffset,
exeData.data() + pos,
copyCount
);
pos += copyCount;
destOffset += copyCount;
} }
// Read skip count (2 bytes LE) return false;
uint16_t skipCount = 0; };
if (pos + 2 <= exeData.size()) {
skipCount = exeData[pos] | (exeData[pos + 1] << 8);
pos += 2;
}
// Advance dest pointer by skipCount (gaps are zero-filled from memset) std::vector<uint8_t> parsedImage;
destOffset += skipCount; size_t parsedRelocPos = 0;
size_t parsedFinalOffset = 0;
int parsedPairCount = 0;
pairCount++; PairFormat usedFormat = PairFormat::CopyDataSkip;
std::cout << "[WardenModule] Pair " << pairCount << ": copy " << copyCount bool parsed = tryParsePairs(PairFormat::CopyDataSkip, parsedImage, parsedRelocPos, parsedFinalOffset, parsedPairCount);
<< ", skip " << skipCount << " (dest offset=" << destOffset << ")" << '\n'; if (!parsed) {
usedFormat = PairFormat::SkipCopyData;
parsed = tryParsePairs(PairFormat::SkipCopyData, parsedImage, parsedRelocPos, parsedFinalOffset, parsedPairCount);
}
if (!parsed) {
usedFormat = PairFormat::CopySkipData;
parsed = tryParsePairs(PairFormat::CopySkipData, parsedImage, parsedRelocPos, parsedFinalOffset, parsedPairCount);
} }
// Save position — remaining decompressed data contains relocation entries if (parsed) {
relocDataOffset_ = pos; std::memcpy(moduleMemory_, parsedImage.data(), parsedImage.size());
relocDataOffset_ = parsedRelocPos;
std::cout << "[WardenModule] Parsed " << pairCount << " skip/copy pairs, final offset: " const char* formatName = "copy/data/skip";
<< destOffset << "/" << finalCodeSize << '\n'; if (usedFormat == PairFormat::SkipCopyData) formatName = "skip/copy/data";
std::cout << "[WardenModule] Relocation data starts at decompressed offset " << relocDataOffset_ if (usedFormat == PairFormat::CopySkipData) formatName = "copy/skip/data";
<< " (" << (exeData.size() - relocDataOffset_) << " bytes remaining)" << '\n';
std::cout << "[WardenModule] Parsed " << parsedPairCount << " pairs using format "
<< formatName << ", final offset: " << parsedFinalOffset << "/" << finalCodeSize << '\n';
std::cout << "[WardenModule] Relocation data starts at decompressed offset " << relocDataOffset_
<< " (" << (exeData.size() - relocDataOffset_) << " bytes remaining)" << '\n';
return true;
}
// Fallback: copy raw payload (without the 4-byte size header) into module memory.
// This keeps loading alive for servers where packet flow can continue with hash/check fallbacks.
if (exeData.size() > 4) {
size_t rawCopySize = std::min(moduleSize_, exeData.size() - 4);
std::memcpy(moduleMemory_, exeData.data() + 4, rawCopySize);
}
relocDataOffset_ = 0;
std::cerr << "[WardenModule] Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback" << '\n';
return true; return true;
} }