Tighten executable and wrapped MPQ validation
This commit is contained in:
+1
-16
@@ -330,16 +330,7 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
|
||||
|
||||
@staticmethod
|
||||
def _validate_client_identity(data):
|
||||
"""Require immutable 1.12.1/5875 anchors for a real PE client.
|
||||
|
||||
Unit fixtures intentionally use synthetic non-PE buffers. The live Apply
|
||||
path validates a real 32-bit PE before this helper is reached, so those
|
||||
synthetic buffers can exercise the fixed-offset policy without weakening
|
||||
the real installer gate.
|
||||
"""
|
||||
if bytes(data[:2]) != b"MZ":
|
||||
return
|
||||
|
||||
"""Require immutable Vanilla 1.12.1/5875 anchors before fixed-offset tweaks."""
|
||||
build = bytes(
|
||||
data[_CLIENT_BUILD_OFFSET:_CLIENT_BUILD_OFFSET + len(_CLIENT_BUILD)]
|
||||
)
|
||||
@@ -385,12 +376,6 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
|
||||
|
||||
def _validate_staged_numeric_states(self, data, source_states, desired):
|
||||
"""The upstream patcher may only leave source bytes or write our selection."""
|
||||
# Legacy unit fixtures are synthetic byte buffers, not executable files.
|
||||
# The real transaction always produces an MZ/PE image and therefore
|
||||
# always takes the strict source-or-selected validation path below.
|
||||
if bytes(data[:2]) != b"MZ":
|
||||
return
|
||||
|
||||
if not isinstance(source_states, dict):
|
||||
raise RuntimeError(
|
||||
"Vanilla Tweaks source fingerprint is missing; refusing to normalize."
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import os
|
||||
import struct
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import remote_packages
|
||||
import setup_tool_dynamic as dynamic
|
||||
|
||||
|
||||
def _classic_mpq_bytes(
|
||||
*,
|
||||
archive_size=96,
|
||||
hash_table_offset=32,
|
||||
block_table_offset=48,
|
||||
inner_magic=b"MPQ\x1A",
|
||||
):
|
||||
header = inner_magic + struct.pack(
|
||||
"<IIHHIIII",
|
||||
32,
|
||||
archive_size,
|
||||
0,
|
||||
3,
|
||||
hash_table_offset,
|
||||
block_table_offset,
|
||||
1,
|
||||
1,
|
||||
)
|
||||
data = bytearray(archive_size)
|
||||
data[:32] = header
|
||||
return data
|
||||
|
||||
|
||||
def _write_wrapped_mpq(
|
||||
path,
|
||||
*,
|
||||
header_offset=32,
|
||||
user_header_size=16,
|
||||
inner_magic=b"MPQ\x1A",
|
||||
hash_table_offset=32,
|
||||
block_table_offset=48,
|
||||
):
|
||||
inner = _classic_mpq_bytes(
|
||||
inner_magic=inner_magic,
|
||||
hash_table_offset=hash_table_offset,
|
||||
block_table_offset=block_table_offset,
|
||||
)
|
||||
total_size = max(16, header_offset + len(inner))
|
||||
data = bytearray(total_size)
|
||||
user_data_size = max(0, header_offset - user_header_size)
|
||||
data[:16] = b"MPQ\x1B" + struct.pack(
|
||||
"<III",
|
||||
user_data_size,
|
||||
header_offset,
|
||||
user_header_size,
|
||||
)
|
||||
if 0 <= header_offset <= total_size - len(inner):
|
||||
data[header_offset:header_offset + len(inner)] = inner
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(data)
|
||||
|
||||
|
||||
class StrictStagedExecutableValidationTests(unittest.TestCase):
|
||||
def test_non_mz_buffer_does_not_bypass_numeric_validation(self):
|
||||
tool = object.__new__(dynamic.ModernWowSetupTool)
|
||||
desired = {
|
||||
"fov": 1.9199,
|
||||
"farclip": 1500.0,
|
||||
"frill": 300.0,
|
||||
"nameplate": 41.0,
|
||||
"maxcam": 100.0,
|
||||
"sound": 64,
|
||||
"sound_bytes": b"64\x00\x00",
|
||||
}
|
||||
source_states = {
|
||||
"fov": struct.pack("<f", 1.5708),
|
||||
"farclip": struct.pack("<f", 777.0),
|
||||
"frill": struct.pack("<f", 70.0),
|
||||
"nameplate": struct.pack("<f", 20.0),
|
||||
"maxcam": struct.pack("<f", 50.0),
|
||||
"sound": b"12\x00\x00",
|
||||
}
|
||||
data = bytearray(0x46795C + 16)
|
||||
for key, offset, _label, _minimum, _maximum in dynamic._NUMERIC_SITES:
|
||||
data[offset:offset + 4] = source_states[key]
|
||||
sound_key, sound_offset, _label, _minimum, _maximum = dynamic._SOUND_SITE
|
||||
data[sound_offset:sound_offset + 4] = source_states[sound_key]
|
||||
|
||||
struct.pack_into("<f", data, 0x40FED8, 2345.0)
|
||||
with self.assertRaisesRegex(RuntimeError, "produced by vanilla-tweaks"):
|
||||
tool._validate_staged_numeric_states(data, source_states, desired)
|
||||
|
||||
def test_client_identity_has_no_non_mz_fixture_bypass(self):
|
||||
data = bytearray(0x46795C + 16)
|
||||
with self.assertRaisesRegex(RuntimeError, "build 5875"):
|
||||
dynamic.ModernWowSetupTool._validate_client_identity(data)
|
||||
|
||||
|
||||
class WrappedMpqValidationTests(unittest.TestCase):
|
||||
def test_accepts_valid_user_data_wrapped_mpq(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = os.path.join(temp, "wrapped.mpq")
|
||||
_write_wrapped_mpq(path)
|
||||
dynamic._strict_verify_mpq(path)
|
||||
|
||||
def test_rejects_user_data_wrapper_with_invalid_archive_offset(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = os.path.join(temp, "wrapped.mpq")
|
||||
data = bytearray(128)
|
||||
data[:16] = b"MPQ\x1B" + struct.pack("<III", 0, 8, 16)
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(data)
|
||||
with self.assertRaisesRegex(
|
||||
remote_packages.RemotePackageError,
|
||||
"invalid nested archive offset",
|
||||
):
|
||||
dynamic._strict_verify_mpq(path)
|
||||
|
||||
def test_rejects_user_data_wrapper_with_out_of_bounds_archive_offset(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = os.path.join(temp, "wrapped.mpq")
|
||||
data = bytearray(128)
|
||||
data[:16] = b"MPQ\x1B" + struct.pack("<III", 0, 120, 16)
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(data)
|
||||
with self.assertRaisesRegex(
|
||||
remote_packages.RemotePackageError,
|
||||
"invalid nested archive offset",
|
||||
):
|
||||
dynamic._strict_verify_mpq(path)
|
||||
|
||||
def test_rejects_user_data_wrapper_with_invalid_header_size(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = os.path.join(temp, "wrapped.mpq")
|
||||
data = bytearray(128)
|
||||
data[:16] = b"MPQ\x1B" + struct.pack("<III", 24, 32, 8)
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(data)
|
||||
with self.assertRaisesRegex(
|
||||
remote_packages.RemotePackageError,
|
||||
"invalid nested archive offset",
|
||||
):
|
||||
dynamic._strict_verify_mpq(path)
|
||||
|
||||
def test_rejects_wrapper_with_corrupt_nested_mpq(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = os.path.join(temp, "wrapped.mpq")
|
||||
_write_wrapped_mpq(path, inner_magic=b"NOPE")
|
||||
with self.assertRaisesRegex(
|
||||
remote_packages.RemotePackageError,
|
||||
"not a valid MPQ archive",
|
||||
):
|
||||
dynamic._strict_verify_mpq(path)
|
||||
|
||||
def test_rejects_wrapped_mpq_with_out_of_bounds_tables(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = os.path.join(temp, "wrapped.mpq")
|
||||
_write_wrapped_mpq(path, block_table_offset=88)
|
||||
with self.assertRaisesRegex(
|
||||
remote_packages.RemotePackageError,
|
||||
"out-of-bounds block table",
|
||||
):
|
||||
dynamic._strict_verify_mpq(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -76,6 +76,30 @@ class VanillaTweaksNormalizationTests(unittest.TestCase):
|
||||
filename="WoW_Modernized.exe",
|
||||
):
|
||||
data = bytearray(0x46795C + 16)
|
||||
|
||||
# Use a minimal but structurally valid PE32 header. The selected PE
|
||||
# offset deliberately places COFF Characteristics at 0x126, matching
|
||||
# the real client's Large Address Aware patch location.
|
||||
pe_offset = 0x110
|
||||
data[:2] = b"MZ"
|
||||
struct.pack_into("<I", data, 0x3C, pe_offset)
|
||||
data[pe_offset:pe_offset + 4] = b"PE\x00\x00"
|
||||
struct.pack_into("<H", data, pe_offset + 4, 0x014C)
|
||||
struct.pack_into("<H", data, pe_offset + 6, 3)
|
||||
struct.pack_into("<H", data, pe_offset + 20, 0x00E0)
|
||||
struct.pack_into("<H", data, pe_offset + 24, 0x010B)
|
||||
|
||||
data[
|
||||
setup_tool_dynamic._CLIENT_BUILD_OFFSET:
|
||||
setup_tool_dynamic._CLIENT_BUILD_OFFSET
|
||||
+ len(setup_tool_dynamic._CLIENT_BUILD)
|
||||
] = setup_tool_dynamic._CLIENT_BUILD
|
||||
data[
|
||||
setup_tool_dynamic._CLIENT_VERSION_OFFSET:
|
||||
setup_tool_dynamic._CLIENT_VERSION_OFFSET
|
||||
+ len(setup_tool_dynamic._CLIENT_VERSION)
|
||||
] = setup_tool_dynamic._CLIENT_VERSION
|
||||
|
||||
data[0x0C1ECF:0x0C1ED1] = quick1
|
||||
data[0x0C2B25:0x0C2B27] = quick2
|
||||
data[0x3A4869] = background
|
||||
@@ -437,7 +461,6 @@ class VanillaTweaksNormalizationTests(unittest.TestCase):
|
||||
camera=False,
|
||||
custom_glues=True,
|
||||
)
|
||||
tool._inspect_wow_executable = lambda _path: (True, "ok")
|
||||
foreign = (0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xF1)
|
||||
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
@@ -509,7 +532,6 @@ class VanillaTweaksNormalizationTests(unittest.TestCase):
|
||||
camera=False,
|
||||
custom_glues=False,
|
||||
)
|
||||
tool._inspect_wow_executable = lambda _path: (True, "ok")
|
||||
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
self._write_exe(
|
||||
@@ -532,12 +554,12 @@ class VanillaTweaksNormalizationTests(unittest.TestCase):
|
||||
b"\x75\x10",
|
||||
b"\x75\x0B",
|
||||
0x27,
|
||||
1.919862,
|
||||
b"64\x00\x00",
|
||||
farclip=3000.0,
|
||||
frill=300.0,
|
||||
nameplate=41.0,
|
||||
maxcam=100.0,
|
||||
1.5708,
|
||||
b"12\x00\x00",
|
||||
farclip=777.0,
|
||||
frill=70.0,
|
||||
nameplate=20.0,
|
||||
maxcam=50.0,
|
||||
laa_patched=True,
|
||||
camera_patched=True,
|
||||
custom_patched=True,
|
||||
|
||||
Reference in New Issue
Block a user