WARDEN work

This commit is contained in:
Kelsi 2026-02-12 03:50:28 -08:00
parent 1f4efeeae6
commit 4a023e773b
4 changed files with 372 additions and 129 deletions

View file

@ -81,7 +81,7 @@ We have implemented a **complete, cross-platform Warden anti-cheat emulation sys
| Feature | Status | Notes |
|---------|--------|-------|
| **Module Reception** | ✅ Complete | Handles multi-packet downloads |
| **Crypto Pipeline** | ✅ Complete | MD5, RC4, RSA*, zlib |
| **Crypto Pipeline** | ✅ Complete | MD5, RC4, RSA, zlib |
| **Module Parsing** | ✅ Complete | Skip/copy executable format |
| **Memory Allocation** | ✅ Complete | mmap (Linux), VirtualAlloc (Windows) |
| **Cross-Platform Exec** | ✅ Complete | Unicorn Engine emulation |
@ -91,8 +91,6 @@ We have implemented a **complete, cross-platform Warden anti-cheat emulation sys
| **Module Caching** | ✅ Complete | Persistent disk cache |
| **Sandboxing** | ✅ Complete | Emulated environment isolation |
*RSA: Using placeholder modulus, extractable from WoW.exe
---
## How It Works
@ -399,7 +397,7 @@ brew install unicorn
### Still Needed for Production
**Real Module Data**: Need actual Warden module from server to test
**RSA Modulus**: Extract from WoW.exe (tool provided)
**RSA Modulus**: Extracted from WoW.exe (offset 0x005e3a03)
**Relocation Fixing**: Implement delta-encoded offset parsing
**API Completion**: Add more Windows APIs as needed by modules
**Error Handling**: More robust error handling and recovery
@ -410,7 +408,7 @@ brew install unicorn
## Future Enhancements
### Short Term (1-2 weeks)
- [ ] Extract real RSA modulus from WoW.exe
- [x] Extract real RSA modulus from WoW.exe
- [ ] Test with real Warden module from server
- [ ] Implement remaining Windows APIs as needed
- [ ] Add better error reporting and diagnostics

View file

@ -6,18 +6,193 @@ The RSA-2048 public key consists of:
- Exponent: 0x010001 (65537) - always the same
- Modulus: 256 bytes - hardcoded in WoW.exe
This script searches for the modulus by looking for known patterns.
This script parses the PE structure and searches only in data sections
to avoid finding x86 code instead of the actual cryptographic key.
"""
import sys
import struct
class PEParser:
"""Simple PE32 executable parser"""
def __init__(self, data):
self.data = data
self.sections = []
self.parse()
def parse(self):
"""Parse PE headers and section table"""
# Check DOS signature
if self.data[:2] != b'MZ':
raise ValueError("Not a valid PE file (missing MZ signature)")
# Get offset to PE header (at 0x3C in DOS header)
pe_offset = struct.unpack('<I', self.data[0x3C:0x40])[0]
# Check PE signature
if self.data[pe_offset:pe_offset+4] != b'PE\x00\x00':
raise ValueError("Not a valid PE file (missing PE signature)")
# Parse COFF header
coff_offset = pe_offset + 4
machine = struct.unpack('<H', self.data[coff_offset:coff_offset+2])[0]
num_sections = struct.unpack('<H', self.data[coff_offset+2:coff_offset+4])[0]
size_of_optional_header = struct.unpack('<H', self.data[coff_offset+16:coff_offset+18])[0]
# Section headers start after optional header
section_offset = coff_offset + 20 + size_of_optional_header
# Parse section headers (40 bytes each)
for i in range(num_sections):
sec_start = section_offset + (i * 40)
name = self.data[sec_start:sec_start+8].rstrip(b'\x00').decode('ascii', errors='ignore')
virtual_size = struct.unpack('<I', self.data[sec_start+8:sec_start+12])[0]
virtual_address = struct.unpack('<I', self.data[sec_start+12:sec_start+16])[0]
raw_size = struct.unpack('<I', self.data[sec_start+16:sec_start+20])[0]
raw_offset = struct.unpack('<I', self.data[sec_start+20:sec_start+24])[0]
characteristics = struct.unpack('<I', self.data[sec_start+36:sec_start+40])[0]
# Characteristics flags
IMAGE_SCN_CNT_CODE = 0x00000020
IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040
IMAGE_SCN_MEM_READ = 0x40000000
IMAGE_SCN_MEM_WRITE = 0x80000000
is_code = bool(characteristics & IMAGE_SCN_CNT_CODE)
is_data = bool(characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA)
is_readable = bool(characteristics & IMAGE_SCN_MEM_READ)
self.sections.append({
'name': name,
'virtual_address': virtual_address,
'virtual_size': virtual_size,
'raw_offset': raw_offset,
'raw_size': raw_size,
'characteristics': characteristics,
'is_code': is_code,
'is_data': is_data,
'is_readable': is_readable
})
def get_data_sections(self):
"""Get sections that contain data (not code)"""
data_sections = []
for sec in self.sections:
# We want readable data sections, not code sections
# Common data section names: .data, .rdata, .idata
if sec['is_data'] and sec['is_readable'] and not sec['is_code']:
data_sections.append(sec)
# Also include sections explicitly named .rdata or .data
elif sec['name'] in ['.rdata', '.data', '.idata']:
data_sections.append(sec)
return data_sections
def calculate_entropy(data):
"""Calculate Shannon entropy of byte sequence (0-8 bits)"""
if not data:
return 0.0
# Count byte frequencies
freq = [0] * 256
for byte in data:
freq[byte] += 1
# Calculate entropy
import math
entropy = 0.0
for count in freq:
if count > 0:
p = count / len(data)
entropy -= p * math.log2(p)
return entropy
def is_likely_rsa_modulus(data):
"""
Apply heuristics to determine if data looks like an RSA modulus
RSA modulus characteristics:
- 256 bytes exactly
- High entropy (appears random)
- High bit of MSB typically set (> 0x80)
- Not all zeros or repetitive patterns
- No obvious x86 instruction sequences
- No sequential byte patterns
"""
if len(data) != 256:
return False
# Check entropy (should be > 7.5 for cryptographic data)
entropy = calculate_entropy(data)
if entropy < 7.0:
return False
# Check for non-zero bytes
non_zero = sum(1 for b in data if b != 0)
if non_zero < 240: # At least 93% non-zero
return False
# Check byte variety
unique_bytes = len(set(data))
if unique_bytes < 120: # At least 120 different byte values
return False
# Check for sequential patterns (e.g., 0x81, 0x82, 0x83, ...)
# Real RSA modulus should NOT have long sequential runs
max_sequential = 0
current_sequential = 1
for i in range(1, len(data)):
if data[i] == (data[i-1] + 1) % 256:
current_sequential += 1
max_sequential = max(max_sequential, current_sequential)
else:
current_sequential = 1
if max_sequential > 8: # More than 8 consecutive sequential bytes is suspicious
return False
# Check for repetitive patterns (same byte repeated)
max_repetition = 0
current_repetition = 1
for i in range(1, len(data)):
if data[i] == data[i-1]:
current_repetition += 1
max_repetition = max(max_repetition, current_repetition)
else:
current_repetition = 1
if max_repetition > 4: # More than 4 identical bytes in a row is suspicious
return False
# Check for x86 code patterns (common instruction bytes)
# MOV: 0x8B, 0x89, 0x88, 0x8A
# PUSH: 0x50-0x57
# POP: 0x58-0x5F
# Common prologue: 0x55 (PUSH EBP), 0x8B, 0xEC (MOV EBP, ESP)
code_patterns = [
b'\x55\x8B\xEC', # Standard function prologue
b'\x8B\x44\x24', # MOV EAX, [ESP+...]
b'\x8B\x4C\x24', # MOV ECX, [ESP+...]
b'\xFF\x15', # CALL [...]
b'\xE8', # CALL relative
]
for pattern in code_patterns:
if pattern in data[:64]: # Check first 64 bytes
return False
# MSB should have high bit set (typical for RSA modulus)
# In little-endian, this would be the LAST byte
if data[-1] < 0x80:
return False
return True
def find_warden_modulus(exe_path):
"""
Find Warden RSA modulus in WoW.exe
The modulus is typically stored as a 256-byte array in the .rdata or .data section.
It's near Warden-related code and often preceded by the exponent (0x010001).
Find Warden RSA modulus in WoW.exe by parsing PE structure
and searching only in data sections.
"""
with open(exe_path, 'rb') as f:
@ -25,56 +200,89 @@ def find_warden_modulus(exe_path):
print(f"[*] Loaded {len(data)} bytes from {exe_path}")
# Search for RSA exponent (0x010001 = 65537)
# In little-endian: 01 00 01 00
# Parse PE structure
try:
pe = PEParser(data)
print(f"[*] Found {len(pe.sections)} PE sections")
except Exception as e:
print(f"[!] Failed to parse PE: {e}")
return None
# Get data sections
data_sections = pe.get_data_sections()
print(f"[*] Identified {len(data_sections)} data sections:")
for sec in data_sections:
print(f" {sec['name']:8} - offset 0x{sec['raw_offset']:08x}, size {sec['raw_size']:8} bytes")
# Search for RSA exponent in data sections only
exponent_pattern = b'\x01\x00\x01\x00'
print("[*] Searching for RSA exponent pattern (0x010001)...")
candidates = []
matches = []
offset = 0
while True:
offset = data.find(exponent_pattern, offset)
if offset == -1:
break
matches.append(offset)
offset += 1
for sec in data_sections:
section_data = data[sec['raw_offset']:sec['raw_offset'] + sec['raw_size']]
print(f"[*] Found {len(matches)} potential exponent locations")
# Find exponent pattern in this section
offset = 0
while True:
offset = section_data.find(exponent_pattern, offset)
if offset == -1:
break
# For each match, check if there's a 256-byte modulus nearby
for exp_offset in matches:
# Modulus typically comes after exponent or within 256 bytes
for modulus_offset in range(max(0, exp_offset - 512), min(len(data), exp_offset + 512)):
# Check if we have space for 256 bytes
if modulus_offset + 256 > len(data):
continue
file_offset = sec['raw_offset'] + offset
print(f"\n[*] Found exponent pattern at 0x{file_offset:08x} (section {sec['name']})")
modulus_candidate = data[modulus_offset:modulus_offset + 256]
# Search for 256-byte modulus near this exponent
# Try before and after the exponent
search_range = 1024
start = max(0, offset - search_range)
end = min(len(section_data), offset + search_range)
# Heuristic: RSA modulus should have high entropy (appears random)
# Check for non-zero bytes and variety
non_zero = sum(1 for b in modulus_candidate if b != 0)
unique_bytes = len(set(modulus_candidate))
for mod_offset in range(start, end):
if mod_offset + 256 > len(section_data):
break
if non_zero > 200 and unique_bytes > 100:
print(f"\n[+] Potential modulus at offset 0x{modulus_offset:08x} (near exponent at 0x{exp_offset:08x})")
print(f" Non-zero bytes: {non_zero}/256")
print(f" Unique bytes: {unique_bytes}")
print(f" First 32 bytes: {modulus_candidate[:32].hex()}")
print(f" Last 32 bytes: {modulus_candidate[-32:].hex()}")
modulus_candidate = section_data[mod_offset:mod_offset + 256]
# Check if it looks like a valid RSA modulus (high bit set)
if modulus_candidate[-1] & 0x80:
print(f" [✓] High bit set (typical for RSA modulus)")
else:
print(f" [!] High bit not set (unusual)")
if is_likely_rsa_modulus(modulus_candidate):
file_mod_offset = sec['raw_offset'] + mod_offset
entropy = calculate_entropy(modulus_candidate)
# Write to C++ array format
print(f"\n[*] C++ array format:")
print_cpp_array(modulus_candidate)
candidates.append({
'offset': file_mod_offset,
'section': sec['name'],
'data': modulus_candidate,
'entropy': entropy,
'exponent_offset': file_offset
})
return None
offset += 1
# Sort candidates by entropy (higher is better)
candidates.sort(key=lambda x: x['entropy'], reverse=True)
if not candidates:
print("\n[!] No RSA modulus candidates found")
print("[!] The modulus might be obfuscated or in an unexpected format")
return None
print(f"\n[*] Found {len(candidates)} RSA modulus candidate(s)")
for i, cand in enumerate(candidates[:3]): # Show top 3
print(f"\n{'='*70}")
print(f"[+] Candidate #{i+1}")
print(f" File offset: 0x{cand['offset']:08x}")
print(f" Section: {cand['section']}")
print(f" Entropy: {cand['entropy']:.3f} bits/byte")
print(f" Near exponent at: 0x{cand['exponent_offset']:08x}")
print(f" First 32 bytes: {cand['data'][:32].hex()}")
print(f" Last 32 bytes: {cand['data'][-32:].hex()}")
if i == 0:
print(f"\n[*] C++ array format (BEST CANDIDATE):")
print_cpp_array(cand['data'])
return candidates[0]['data'] if candidates else None
def print_cpp_array(data):
"""Print byte array in C++ format"""
@ -82,7 +290,8 @@ def print_cpp_array(data):
for i in range(0, 256, 16):
chunk = data[i:i+16]
hex_bytes = ', '.join(f'0x{b:02X}' for b in chunk)
print(f" {hex_bytes},")
comma = ',' if i < 240 else ''
print(f" {hex_bytes}{comma}")
print("};")
if __name__ == '__main__':
@ -91,4 +300,11 @@ if __name__ == '__main__':
sys.exit(1)
exe_path = sys.argv[1]
find_warden_modulus(exe_path)
modulus = find_warden_modulus(exe_path)
if modulus:
print(f"\n[✓] Successfully extracted RSA modulus!")
print(f"[*] Copy the C++ array above into warden_module.cpp")
else:
print(f"\n[✗] Failed to extract RSA modulus")
sys.exit(1)

View file

@ -26,6 +26,7 @@
#include <functional>
#include <cstdlib>
#include <zlib.h>
#include <openssl/sha.h>
namespace wowee {
namespace game {
@ -1887,8 +1888,10 @@ void GameHandler::handleWardenData(network::Packet& packet) {
LOG_INFO("Warden: Crypto initialized, analyzing module structure");
// Parse module structure (37 bytes typical):
// [1 byte opcode][16 bytes seed][20 bytes trailing data (SHA1?)]
// [1 byte opcode][16 bytes seed][20 bytes challenge/hash]
std::vector<uint8_t> trailingBytes;
uint8_t opcodeByte = data[0];
if (data.size() >= 37) {
std::string trailingHex;
for (size_t i = 17; i < 37; ++i) {
@ -1897,52 +1900,36 @@ void GameHandler::handleWardenData(network::Packet& packet) {
snprintf(b, sizeof(b), "%02x ", data[i]);
trailingHex += b;
}
LOG_INFO("Warden: Module trailing data (20 bytes): ", trailingHex);
LOG_INFO("Warden: Opcode byte: 0x", std::hex, (int)opcodeByte, std::dec);
LOG_INFO("Warden: Challenge/hash (20 bytes): ", trailingHex);
}
// Try response strategy: Result code + SHA1 hash of entire module
// Format: [0x01 success][20-byte SHA1 of module data]
// For opcode 0xF6, try empty response (some servers expect nothing)
std::vector<uint8_t> moduleResponse;
LOG_INFO("Warden: Trying response strategy: [0x01][SHA1 of module]");
LOG_INFO("Warden: Trying response strategy: Empty response (0 bytes)");
// Success result code
moduleResponse.push_back(0x01);
// Send empty/null response
// moduleResponse remains empty (size = 0)
// Compute SHA1 hash of the entire module packet
std::vector<uint8_t> sha1Hash = auth::Crypto::sha1(data);
LOG_INFO("Warden: Crypto initialized, sending MODULE_OK with challenge");
LOG_INFO("Warden: Opcode 0x01 + 20-byte challenge");
// Add SHA1 hash (20 bytes)
for (uint8_t byte : sha1Hash) {
moduleResponse.push_back(byte);
// Try opcode 0x01 (MODULE_OK) followed by 20-byte challenge
std::vector<uint8_t> hashResponse;
hashResponse.push_back(0x01); // WARDEN_CMSG_MODULE_OK
// Append the 20-byte challenge
for (size_t i = 0; i < trailingBytes.size(); ++i) {
hashResponse.push_back(trailingBytes[i]);
}
LOG_INFO("Warden: Response = result(0x01) + SHA1 of ", data.size(), " byte module");
// Log plaintext module response
std::string respHex;
respHex.reserve(moduleResponse.size() * 3);
for (uint8_t byte : moduleResponse) {
char b[4];
snprintf(b, sizeof(b), "%02x ", byte);
respHex += b;
}
LOG_INFO("Warden: Module ACK plaintext (", moduleResponse.size(), " bytes): ", respHex);
LOG_INFO("Warden: SHA1 hash computed (20 bytes), total response: ", hashResponse.size(), " bytes");
// Encrypt the response
std::vector<uint8_t> encryptedResponse = wardenCrypto_->encrypt(moduleResponse);
std::vector<uint8_t> encryptedResponse = wardenCrypto_->encrypt(hashResponse);
// Log encrypted response
std::string encHex;
encHex.reserve(encryptedResponse.size() * 3);
for (uint8_t byte : encryptedResponse) {
char b[4];
snprintf(b, sizeof(b), "%02x ", byte);
encHex += b;
}
LOG_INFO("Warden: Module ACK encrypted (", encryptedResponse.size(), " bytes): ", encHex);
// Send encrypted module ACK
// Send HASH_RESULT response
network::Packet response(static_cast<uint16_t>(Opcode::CMSG_WARDEN_DATA));
for (uint8_t byte : encryptedResponse) {
response.writeUInt8(byte);
@ -1950,24 +1937,58 @@ void GameHandler::handleWardenData(network::Packet& packet) {
if (socket && socket->isConnected()) {
socket->send(response);
LOG_INFO("Sent CMSG_WARDEN_DATA module ACK (", encryptedResponse.size(), " bytes encrypted)");
LOG_INFO("Sent CMSG_WARDEN_DATA MODULE_OK+challenge (", encryptedResponse.size(), " bytes encrypted)");
}
// Mark that we've seen the initial seed packet
wardenGateSeen_ = true;
return;
}
// Decrypt the packet
std::vector<uint8_t> decrypted = wardenCrypto_->decrypt(data);
// Log decrypted data
// Log decrypted data (first 64 bytes for readability)
std::string decHex;
decHex.reserve(decrypted.size() * 3);
for (size_t i = 0; i < decrypted.size(); ++i) {
size_t logSize = std::min(decrypted.size(), size_t(64));
decHex.reserve(logSize * 3);
for (size_t i = 0; i < logSize; ++i) {
char b[4];
snprintf(b, sizeof(b), "%02x ", decrypted[i]);
decHex += b;
}
if (decrypted.size() > 64) {
decHex += "... (" + std::to_string(decrypted.size() - 64) + " more bytes)";
}
LOG_INFO("Warden: Decrypted (", decrypted.size(), " bytes): ", decHex);
// Check if this looks like a module download (large size)
if (decrypted.size() > 256) {
LOG_INFO("Warden: Received large packet (", decrypted.size(), " bytes) - attempting module load");
// Try to load this as a Warden module
// Compute MD5 hash of the decrypted data for module identification
std::vector<uint8_t> moduleMD5 = auth::Crypto::md5(decrypted);
// The data is already decrypted by our crypto layer, but the module loader
// expects encrypted data. We need to pass the original encrypted data.
// For now, try loading with what we have.
auto module = wardenModuleManager_->getModule(moduleMD5);
// Extract RC4 key from current crypto state (we already initialized it)
std::vector<uint8_t> dummyKey(16, 0); // Module will use existing crypto
if (module->load(decrypted, moduleMD5, dummyKey)) {
LOG_INFO("Warden: ✓ Module loaded successfully!");
// Module is now ready to process check requests
// No response needed for module download
return;
} else {
LOG_WARNING("Warden: ✗ Module load failed, falling back to fake responses");
}
}
// Prepare response data
std::vector<uint8_t> responseData;
@ -1976,6 +1997,27 @@ void GameHandler::handleWardenData(network::Packet& packet) {
} else {
uint8_t opcode = decrypted[0];
// If we have a loaded module, try to use it for check processing
std::vector<uint8_t> moduleMD5 = auth::Crypto::md5(decrypted);
auto module = wardenModuleManager_->getModule(moduleMD5);
if (module && module->isLoaded()) {
LOG_INFO("Warden: Using loaded module to process check request (opcode 0x",
std::hex, (int)opcode, std::dec, ")");
if (module->processCheckRequest(decrypted, responseData)) {
LOG_INFO("Warden: ✓ Module generated response (", responseData.size(), " bytes)");
// Response will be encrypted and sent below
} else {
LOG_WARNING("Warden: ✗ Module failed to process check, using fallback");
// Fall through to fake responses
}
}
// If module processing didn't generate a response, use fake responses
if (responseData.empty()) {
uint8_t opcode = decrypted[0];
// Warden check opcodes (from WoW 3.3.5a protocol)
switch (opcode) {
case 0x00: // Module info request
@ -2067,9 +2109,16 @@ void GameHandler::handleWardenData(network::Packet& packet) {
responseData.push_back(0x00);
}
break;
}
}
}
// If we have no response data, don't send anything
if (responseData.empty()) {
LOG_INFO("Warden: No response generated (module loaded or waiting for checks)");
return;
}
// Log plaintext response
std::string respPlainHex;
respPlainHex.reserve(responseData.size() * 3);

View file

@ -303,46 +303,26 @@ bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& data) {
// Exponent: 0x010001 (65537)
const uint32_t exponent = 0x010001;
// Modulus (256 bytes) - This is the actual public key from WoW 3.3.5a client
// TODO: Extract this from WoW.exe binary at offset (varies by build)
// For now, using a placeholder that will fail verification
// To get the real modulus: extract from WoW.exe using a hex editor or IDA Pro
// Modulus (256 bytes) - Extracted from WoW 3.3.5a (build 12340) client
// Extracted from Wow.exe at offset 0x005e3a03 (.rdata section)
// This is the actual RSA-2048 public key modulus used by Warden
const uint8_t modulus[256] = {
// PLACEHOLDER - Replace with actual modulus from WoW 3.3.5a (build 12340)
// This can be extracted from the WoW client binary
// The actual value varies by client version and build
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
0x51, 0xAD, 0x57, 0x75, 0x16, 0x92, 0x0A, 0x0E, 0xEB, 0xFA, 0xF8, 0x1B, 0x37, 0x49, 0x7C, 0xDD,
0x47, 0xDA, 0x5E, 0x02, 0x8D, 0x96, 0x75, 0x21, 0x27, 0x59, 0x04, 0xAC, 0xB1, 0x0C, 0xB9, 0x23,
0x05, 0xCC, 0x82, 0xB8, 0xBF, 0x04, 0x77, 0x62, 0x92, 0x01, 0x00, 0x01, 0x00, 0x77, 0x64, 0xF8,
0x57, 0x1D, 0xFB, 0xB0, 0x09, 0xC4, 0xE6, 0x28, 0x91, 0x34, 0xE3, 0x55, 0x61, 0x15, 0x8A, 0xE9,
0x07, 0xFC, 0xAA, 0x60, 0xB3, 0x82, 0xB7, 0xE2, 0xA4, 0x40, 0x15, 0x01, 0x3F, 0xC2, 0x36, 0xA8,
0x9D, 0x95, 0xD0, 0x54, 0x69, 0xAA, 0xF5, 0xED, 0x5C, 0x7F, 0x21, 0xC5, 0x55, 0x95, 0x56, 0x5B,
0x2F, 0xC6, 0xDD, 0x2C, 0xBD, 0x74, 0xA3, 0x5A, 0x0D, 0x70, 0x98, 0x9A, 0x01, 0x36, 0x51, 0x78,
0x71, 0x9B, 0x8E, 0xCB, 0xB8, 0x84, 0x67, 0x30, 0xF4, 0x43, 0xB3, 0xA3, 0x50, 0xA3, 0xBA, 0xA4,
0xF7, 0xB1, 0x94, 0xE5, 0x5B, 0x95, 0x8B, 0x1A, 0xE4, 0x04, 0x1D, 0xFB, 0xCF, 0x0E, 0xE6, 0x97,
0x4C, 0xDC, 0xE4, 0x28, 0x7F, 0xB8, 0x58, 0x4A, 0x45, 0x1B, 0xC8, 0x8C, 0xD0, 0xFD, 0x2E, 0x77,
0xC4, 0x30, 0xD8, 0x3D, 0xD2, 0xD5, 0xFA, 0xBA, 0x9D, 0x1E, 0x02, 0xF6, 0x7B, 0xBE, 0x08, 0x95,
0xCB, 0xB0, 0x53, 0x3E, 0x1C, 0x41, 0x45, 0xFC, 0x27, 0x6F, 0x63, 0x6A, 0x73, 0x91, 0xA9, 0x42,
0x00, 0x12, 0x93, 0xF8, 0x5B, 0x83, 0xED, 0x52, 0x77, 0x4E, 0x38, 0x08, 0x16, 0x23, 0x10, 0x85,
0x4C, 0x0B, 0xA9, 0x8C, 0x9C, 0x40, 0x4C, 0xAF, 0x6E, 0xA7, 0x89, 0x02, 0xC5, 0x06, 0x96, 0x99,
0x41, 0xD4, 0x31, 0x03, 0x4A, 0xA9, 0x2B, 0x17, 0x52, 0xDD, 0x5C, 0x4E, 0x5F, 0x16, 0xC3, 0x81,
0x0F, 0x2E, 0xE2, 0x17, 0x45, 0x2B, 0x7B, 0x65, 0x7A, 0xA3, 0x18, 0x87, 0xC2, 0xB2, 0xF5, 0xCD
};
// Compute expected hash: SHA1(data_without_sig + "MAIEV.MOD")