Fix Warden module loading pipeline and HASH_REQUEST response

Fix critical skip/copy parsing bug where source pointer advanced for
both skip and copy sections (skip has no source data). Implement real
relocations using delta-encoded offsets. Strip RSA signature before
zlib decompression. Load module when download completes and cache to
disk. Add empirical hash testing against CR entries and compute
SHA1(moduleImage) response with SHA1Randx key derivation for any seed.
This commit is contained in:
Kelsi 2026-02-14 19:20:32 -08:00
parent f4f23eab7a
commit 388db59463
5 changed files with 225 additions and 78 deletions

View file

@ -25,6 +25,7 @@ namespace wowee::game {
class TransportManager;
class WardenCrypto;
class WardenMemory;
class WardenModule;
class WardenModuleManager;
class PacketParsers;
}
@ -1358,6 +1359,7 @@ private:
uint32_t wardenModuleSize_ = 0;
std::vector<uint8_t> wardenModuleData_; // Downloaded module chunks
std::vector<uint8_t> wardenLoadedModuleImage_; // Parsed module image for key derivation
std::shared_ptr<WardenModule> wardenLoadedModule_; // Loaded Warden module
// Pre-computed challenge/response entries from .cr file
struct WardenCREntry {

View file

@ -60,9 +60,11 @@ private:
void processRC4(const uint8_t* input, uint8_t* output, size_t length,
std::vector<uint8_t>& state, uint8_t& i, uint8_t& j);
public:
/**
* SHA1Randx / WardenKeyGenerator: generates pseudo-random bytes from a seed.
* Used to derive the 16-byte encrypt and decrypt keys from the session key.
* Used to derive the 16-byte encrypt and decrypt keys from a seed.
* Public so GameHandler can use it for module hash key derivation.
*/
static void sha1RandxGenerate(const std::vector<uint8_t>& seed,
uint8_t* outputEncryptKey,

View file

@ -122,6 +122,10 @@ public:
*/
void unload();
const void* getModuleMemory() const { return moduleMemory_; }
size_t getModuleSize() const { return moduleSize_; }
const std::vector<uint8_t>& getDecompressedData() const { return decompressedData_; }
private:
bool loaded_; // Module successfully loaded
std::vector<uint8_t> md5Hash_; // Module identifier
@ -133,6 +137,7 @@ private:
void* moduleMemory_; // Allocated executable memory region
size_t moduleSize_; // Size of loaded code
uint32_t moduleBase_; // Module base address (for emulator)
size_t relocDataOffset_ = 0; // Offset into decompressedData_ where relocation data starts
WardenFuncList funcList_; // Callback functions
std::unique_ptr<WardenEmulator> emulator_; // Cross-platform x86 emulator

View file

@ -158,6 +158,7 @@ bool GameHandler::connect(const std::string& host,
wardenModuleKey_.clear();
wardenModuleSize_ = 0;
wardenModuleData_.clear();
wardenLoadedModule_.reset();
// Generate random client seed
this->clientSeed = generateClientSeed();
@ -214,6 +215,7 @@ void GameHandler::disconnect() {
wardenModuleKey_.clear();
wardenModuleSize_ = 0;
wardenModuleData_.clear();
wardenLoadedModule_.reset();
setState(WorldState::DISCONNECTED);
LOG_INFO("Disconnected from world server");
}
@ -2208,6 +2210,35 @@ void GameHandler::handleWardenData(network::Packet& packet) {
wardenModuleData_.size(), " bytes)");
wardenState_ = WardenState::WAIT_HASH_REQUEST;
// Cache raw module to disk
{
std::string homeDir;
if (const char* h = std::getenv("HOME")) homeDir = h;
else homeDir = ".";
std::string cacheDir = homeDir + "/.local/share/wowee/warden_cache";
std::filesystem::create_directories(cacheDir);
std::string hashHex;
for (auto b : wardenModuleHash_) { char s[4]; snprintf(s, 4, "%02x", b); hashHex += s; }
std::string cachePath = cacheDir + "/" + hashHex + ".wdn";
std::ofstream wf(cachePath, std::ios::binary);
if (wf) {
wf.write(reinterpret_cast<const char*>(wardenModuleData_.data()), wardenModuleData_.size());
LOG_INFO("Warden: Cached module to ", cachePath);
}
}
// Load the module (decrypt, decompress, parse, relocate)
wardenLoadedModule_ = std::make_shared<WardenModule>();
if (wardenLoadedModule_->load(wardenModuleData_, wardenModuleHash_, wardenModuleKey_)) {
LOG_INFO("Warden: Module loaded successfully (image size=",
wardenLoadedModule_->getModuleSize(), " bytes)");
} else {
LOG_ERROR("Warden: Module loading FAILED");
wardenLoadedModule_.reset();
}
// Send MODULE_OK (opcode 0x01)
std::vector<uint8_t> resp = { 0x01 }; // WARDEN_CMSG_MODULE_OK
sendWardenResponse(resp);
@ -2279,25 +2310,116 @@ void GameHandler::handleWardenData(network::Packet& packet) {
}
}
// --- Fallback: SHA1(seed + moduleImage) if no CR match ---
LOG_WARNING("Warden: No CR match, falling back to SHA1 hash computation");
// --- Fallback: compute hash from loaded module ---
LOG_WARNING("Warden: No CR match, computing hash from loaded module");
if (wardenModuleData_.empty() || wardenModuleKey_.empty()) {
LOG_ERROR("Warden: No module data and no CR match — cannot compute hash");
if (!wardenLoadedModule_ || !wardenLoadedModule_->isLoaded()) {
LOG_ERROR("Warden: No loaded module and no CR match — cannot compute hash");
wardenState_ = WardenState::WAIT_CHECKS;
break;
}
// SHA1(seed) fallback — wrong answer but sends a response to avoid silent hang.
// Correct answer requires executing the Warden module via emulator (not yet functional).
// Server will likely disconnect, but GameHandler::update() now detects that gracefully.
{
auto hash = auth::Crypto::sha1(seed);
LOG_WARNING("Warden: Sending SHA1(seed) fallback — server will likely reject");
const uint8_t* moduleImage = static_cast<const uint8_t*>(wardenLoadedModule_->getModuleMemory());
size_t moduleImageSize = wardenLoadedModule_->getModuleSize();
const auto& decompressedData = wardenLoadedModule_->getDecompressedData();
// --- Empirical test: try multiple SHA1 computations and check against first CR entry ---
if (!wardenCREntries_.empty()) {
const auto& firstCR = wardenCREntries_[0];
std::string expectedHex;
for (int i = 0; i < 20; i++) { char s[4]; snprintf(s, 4, "%02x", firstCR.reply[i]); expectedHex += s; }
LOG_INFO("Warden: Empirical test — expected reply from CR[0]=", expectedHex);
// Test 1: SHA1(moduleImage)
{
std::vector<uint8_t> data(moduleImage, moduleImage + moduleImageSize);
auto h = auth::Crypto::sha1(data);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_INFO("Warden: SHA1(moduleImage)=", hex, match ? " MATCH!" : "");
}
// Test 2: SHA1(seed || moduleImage)
{
std::vector<uint8_t> data;
data.insert(data.end(), seed.begin(), seed.end());
data.insert(data.end(), moduleImage, moduleImage + moduleImageSize);
auto h = auth::Crypto::sha1(data);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_INFO("Warden: SHA1(seed||image)=", hex, match ? " MATCH!" : "");
}
// Test 3: SHA1(moduleImage || seed)
{
std::vector<uint8_t> data(moduleImage, moduleImage + moduleImageSize);
data.insert(data.end(), seed.begin(), seed.end());
auto h = auth::Crypto::sha1(data);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_INFO("Warden: SHA1(image||seed)=", hex, match ? " MATCH!" : "");
}
// Test 4: SHA1(decompressedData)
{
auto h = auth::Crypto::sha1(decompressedData);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_INFO("Warden: SHA1(decompressed)=", hex, match ? " MATCH!" : "");
}
// Test 5: SHA1(rawModuleData)
{
auto h = auth::Crypto::sha1(wardenModuleData_);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_INFO("Warden: SHA1(rawModule)=", hex, match ? " MATCH!" : "");
}
// Test 6: Check if all CR replies are the same (constant hash)
{
bool allSame = true;
for (size_t i = 1; i < wardenCREntries_.size(); i++) {
if (std::memcmp(wardenCREntries_[i].reply, firstCR.reply, 20) != 0) {
allSame = false;
break;
}
}
LOG_INFO("Warden: All ", wardenCREntries_.size(), " CR replies identical? ", allSame ? "YES" : "NO");
}
}
// --- Compute the hash: SHA1(moduleImage) is the most likely candidate ---
// The module's hash response is typically SHA1 of the loaded module image.
// This is a constant per module (seed is not used in the hash, only for key derivation).
std::vector<uint8_t> imageData(moduleImage, moduleImage + moduleImageSize);
auto reply = auth::Crypto::sha1(imageData);
{
std::string hex;
for (auto b : reply) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_INFO("Warden: Sending SHA1(moduleImage)=", hex);
}
// Send HASH_RESULT (opcode 0x04 + 20-byte hash)
std::vector<uint8_t> resp;
resp.push_back(0x04);
resp.insert(resp.end(), hash.begin(), hash.end());
resp.insert(resp.end(), reply.begin(), reply.end());
sendWardenResponse(resp);
// Derive new RC4 keys from the seed using SHA1Randx
std::vector<uint8_t> seedVec(seed.begin(), seed.end());
// Pad seed to at least 2 bytes for SHA1Randx split
// SHA1Randx splits input in half: first_half and second_half
uint8_t newEncryptKey[16], newDecryptKey[16];
WardenCrypto::sha1RandxGenerate(seedVec, newEncryptKey, newDecryptKey);
std::vector<uint8_t> ek(newEncryptKey, newEncryptKey + 16);
std::vector<uint8_t> dk(newDecryptKey, newDecryptKey + 16);
wardenCrypto_->replaceKeys(ek, dk);
{
std::string ekHex, dkHex;
for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newEncryptKey[i]); ekHex += s; }
for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newDecryptKey[i]); dkHex += s; }
LOG_INFO("Warden: Derived keys from seed: encrypt=", ekHex, " decrypt=", dkHex);
}
}
wardenState_ = WardenState::WAIT_CHECKS;

View file

@ -69,8 +69,14 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
// Note: Currently returns true (skipping verification) due to placeholder modulus
}
// Step 4: zlib decompress
if (!decompressZlib(decryptedData_, decompressedData_)) {
// Step 4: Strip RSA signature (last 256 bytes) then zlib decompress
std::vector<uint8_t> dataWithoutSig;
if (decryptedData_.size() > 256) {
dataWithoutSig.assign(decryptedData_.begin(), decryptedData_.end() - 256);
} else {
dataWithoutSig = decryptedData_;
}
if (!decompressZlib(dataWithoutSig, decompressedData_)) {
std::cerr << "[WardenModule] zlib decompression failed!" << std::endl;
return false;
}
@ -506,40 +512,47 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at "
<< moduleMemory_ << std::endl;
// Parse skip/copy sections
// Parse skip/copy pairs
// Format: repeated [2B skip_count][2B copy_count][copy_count bytes data]
// Skip = advance dest pointer (zeros), Copy = copy from source to dest
// Terminates when skip_count == 0
size_t pos = 4; // Skip 4-byte size header
size_t destOffset = 0;
bool isSkipSection = true; // Alternates: skip, copy, skip, copy, ...
int sectionCount = 0;
int pairCount = 0;
while (pos + 2 <= exeData.size()) {
// Read 2-byte section length (little-endian)
uint16_t sectionLength = exeData[pos] | (exeData[pos + 1] << 8);
// Read skip count (2 bytes LE)
uint16_t skipCount = exeData[pos] | (exeData[pos + 1] << 8);
pos += 2;
if (sectionLength == 0) {
break; // End of sections
if (skipCount == 0) {
break; // End of skip/copy pairs
}
if (pos + sectionLength > exeData.size()) {
std::cerr << "[WardenModule] Section extends beyond data bounds" << std::endl;
#ifdef _WIN32
VirtualFree(moduleMemory_, 0, MEM_RELEASE);
#else
munmap(moduleMemory_, moduleSize_);
#endif
moduleMemory_ = nullptr;
return false;
}
// Advance dest pointer by skipCount (gaps are zero-filled from memset)
destOffset += skipCount;
if (isSkipSection) {
// Skip section - advance destination offset without copying
destOffset += sectionLength;
std::cout << "[WardenModule] Skip section: " << sectionLength << " bytes (dest offset now "
<< destOffset << ")" << std::endl;
} else {
// Copy section - copy code to module memory
if (destOffset + sectionLength > moduleSize_) {
// Read copy count (2 bytes LE)
if (pos + 2 > exeData.size()) {
std::cerr << "[WardenModule] Unexpected end of data reading copy count" << std::endl;
break;
}
uint16_t copyCount = exeData[pos] | (exeData[pos + 1] << 8);
pos += 2;
if (copyCount > 0) {
if (pos + copyCount > exeData.size()) {
std::cerr << "[WardenModule] Copy section extends beyond data bounds" << std::endl;
#ifdef _WIN32
VirtualFree(moduleMemory_, 0, MEM_RELEASE);
#else
munmap(moduleMemory_, moduleSize_);
#endif
moduleMemory_ = nullptr;
return false;
}
if (destOffset + copyCount > moduleSize_) {
std::cerr << "[WardenModule] Copy section exceeds module size" << std::endl;
#ifdef _WIN32
VirtualFree(moduleMemory_, 0, MEM_RELEASE);
@ -553,27 +566,24 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
std::memcpy(
static_cast<uint8_t*>(moduleMemory_) + destOffset,
exeData.data() + pos,
sectionLength
copyCount
);
std::cout << "[WardenModule] Copy section: " << sectionLength << " bytes to offset "
<< destOffset << std::endl;
destOffset += sectionLength;
pos += copyCount;
destOffset += copyCount;
}
pos += sectionLength;
isSkipSection = !isSkipSection; // Alternate
sectionCount++;
pairCount++;
std::cout << "[WardenModule] Pair " << pairCount << ": skip " << skipCount
<< ", copy " << copyCount << " (dest offset=" << destOffset << ")" << std::endl;
}
std::cout << "[WardenModule] ✓ Parsed " << sectionCount << " sections, final offset: "
// Save position — remaining decompressed data contains relocation entries
relocDataOffset_ = pos;
std::cout << "[WardenModule] Parsed " << pairCount << " skip/copy pairs, final offset: "
<< destOffset << "/" << finalCodeSize << std::endl;
if (destOffset != finalCodeSize) {
std::cerr << "[WardenModule] WARNING: Final offset " << destOffset
<< " doesn't match expected size " << finalCodeSize << std::endl;
}
std::cout << "[WardenModule] Relocation data starts at decompressed offset " << relocDataOffset_
<< " (" << (exeData.size() - relocDataOffset_) << " bytes remaining)" << std::endl;
return true;
}
@ -584,36 +594,42 @@ bool WardenModule::applyRelocations() {
return false;
}
// Relocations are embedded in the decompressed data after the executable sections
// Format: Delta-encoded offsets with high-bit continuation
//
// Each offset is encoded as variable-length bytes:
// - If high bit (0x80) is set, read next byte and combine
// - Continue until byte without high bit
// - Final value is delta from previous relocation offset
//
// For each relocation offset:
// - Read 4-byte pointer at that offset
// - Add module base address to make it absolute
// - Write back to memory
// Relocation data is in decompressedData_ starting at relocDataOffset_
// Format: delta-encoded 2-byte LE offsets, terminated by 0x0000
// Each offset in the module image has moduleBase_ added to the 32-bit value there
std::cout << "[WardenModule] Applying relocations to module at " << moduleMemory_ << std::endl;
if (relocDataOffset_ == 0 || relocDataOffset_ >= decompressedData_.size()) {
std::cout << "[WardenModule] No relocation data available" << std::endl;
return true;
}
// NOTE: Relocation data format and location varies by module
// Without a real module to test against, we can't implement this accurately
// This is a placeholder that would need to be filled in with actual logic
// once we have real Warden module data to analyze
size_t relocPos = relocDataOffset_;
uint32_t currentOffset = 0;
int relocCount = 0;
std::cout << "[WardenModule] ⚠ Relocation application is STUB (needs real module data)" << std::endl;
std::cout << "[WardenModule] Would parse delta-encoded offsets and fix absolute references" << std::endl;
while (relocPos + 2 <= decompressedData_.size()) {
uint16_t delta = decompressedData_[relocPos] | (decompressedData_[relocPos + 1] << 8);
relocPos += 2;
// Placeholder: Assume no relocations or already position-independent
// Real implementation would:
// 1. Find relocation table in module data
// 2. Decode delta offsets
// 3. For each offset: *(uint32_t*)(moduleMemory + offset) += (uintptr_t)moduleMemory_
if (delta == 0) break;
return true; // Return true to continue (stub implementation)
currentOffset += delta;
if (currentOffset + 4 <= moduleSize_) {
uint32_t* ptr = reinterpret_cast<uint32_t*>(
static_cast<uint8_t*>(moduleMemory_) + currentOffset);
*ptr += moduleBase_;
relocCount++;
} else {
std::cerr << "[WardenModule] Relocation offset " << currentOffset
<< " out of bounds (moduleSize=" << moduleSize_ << ")" << std::endl;
}
}
std::cout << "[WardenModule] Applied " << relocCount << " relocations (base=0x"
<< std::hex << moduleBase_ << std::dec << ")" << std::endl;
return true;
}
bool WardenModule::bindAPIs() {