This commit is contained in:
hashbro
2026-08-08 06:09:30 +08:00
parent 91196b8832
commit 94f686e668
4 changed files with 218 additions and 16 deletions
+139
View File
@@ -0,0 +1,139 @@
"""Force Deployment/Reporting URL scheme from https to http for lab proxy testing.
Core/type0x01 build URLs with the cstring/CFString format ``https://%@``.
daily.html plugin URLs are already ``http://[HOST_PLACEHOLDER]/...``.
``https://%@`` (len 10) is replaced with ``http://%@\\0`` (9 visible chars + pad)
and matching CFString length fields are updated 10 → 9. Do **not** use a trailing
slash in the format string — paths already start with ``/``, which produced
``https://host//api/...``.
"""
from __future__ import annotations
import struct
from _path_patch import _arm64e_target, _fat_slices, _parse_macho
OLD = b"https://%@"
# Previous mistaken same-length patch (caused host//path).
LEGACY_SLASH = b"http://%@/"
# 10-byte slot: 9-char format + NUL pad (original terminator becomes a second NUL).
NEW_SLOT = b"http://%@\x00"
NEW_VISIBLE = b"http://%@"
def _patch_thin_scheme(data: bytes, *, expect_hits: int, label: str) -> bytes:
macho = _parse_macho(data, label=label)
base_subtype = macho.cpu_subtype & 0x00FFFFFF
architecture = (
"arm64e" if base_subtype == 2 else "arm64" if base_subtype == 0 else ""
)
if architecture not in {"arm64", "arm64e"}:
raise SystemExit(f"{label}: unsupported CPU subtype {base_subtype}")
cstring = macho.section("__TEXT", "__cstring")
region = bytearray(data[cstring.offset : cstring.offset + cstring.size])
# Prefer migrating legacy slash form, else patch original https form.
if region.count(LEGACY_SLASH) == expect_hits and region.count(OLD) == 0:
source = LEGACY_SLASH
elif region.count(OLD) == expect_hits:
source = OLD
elif (
region.count(NEW_VISIBLE) >= expect_hits
and region.count(OLD) == 0
and region.count(LEGACY_SLASH) == 0
):
# Already patched to http://%@ (NUL-terminated).
return data
else:
raise SystemExit(
f"{label}: expected {expect_hits} {OLD!r} or {LEGACY_SLASH!r} in "
f"__cstring; found https={region.count(OLD)} slash={region.count(LEGACY_SLASH)}"
)
hits: list[int] = []
start = 0
while True:
hit = region.find(source, start)
if hit < 0:
break
hits.append(hit)
start = hit + 1
if len(hits) != expect_hits:
raise SystemExit(
f"{label}: __cstring scheme hits={len(hits)}, expected {expect_hits}"
)
for hit in hits:
region[hit : hit + len(NEW_SLOT)] = NEW_SLOT
buf = bytearray(data)
buf[cstring.offset : cstring.offset + cstring.size] = region
cfstring = macho.section("__DATA_CONST", "__cfstring")
if cfstring.size % 32:
raise SystemExit(f"{label}: __cfstring size is not record-aligned")
patched_lengths = 0
for hit in hits:
path_vmaddr = cstring.addr + hit
for record_offset in range(
cfstring.offset, cfstring.offset + cfstring.size, 32
):
raw = struct.unpack_from("<Q", buf, record_offset + 16)[0]
length = struct.unpack_from("<Q", buf, record_offset + 24)[0]
target = raw if architecture == "arm64" else _arm64e_target(raw)
if target != path_vmaddr:
continue
if length not in (len(OLD), len(NEW_VISIBLE)):
raise SystemExit(
f"{label}: scheme CFString length={length}, expected "
f"{len(OLD)} or {len(NEW_VISIBLE)}"
)
struct.pack_into("<Q", buf, record_offset + 24, len(NEW_VISIBLE))
patched_lengths += 1
break
else:
raise SystemExit(
f"{label}: no CFString pointing at scheme cstring vmaddr {path_vmaddr:#x}"
)
if patched_lengths != expect_hits:
raise SystemExit(
f"{label}: patched {patched_lengths} CFString lengths, expected {expect_hits}"
)
return bytes(buf)
def patch_deployment_scheme_to_http(
data: bytes,
*,
expect_hits: int,
label: str,
) -> bytes:
"""Patch ``https://%@`` → ``http://%@`` (CFString length 9) in thin or fat dylibs.
``expect_hits`` is the total number of format strings across the whole file
(2 for fat core, 1 for thin type0x01).
"""
slices = _fat_slices(data, label=label)
if slices is None:
return _patch_thin_scheme(data, expect_hits=expect_hits, label=label)
if expect_hits % len(slices) != 0:
raise SystemExit(
f"{label}: expect_hits={expect_hits} not divisible by fat slices={len(slices)}"
)
per_slice = expect_hits // len(slices)
buf = bytearray(data)
for index, slice_info in enumerate(slices):
thin = bytes(buf[slice_info.offset : slice_info.offset + slice_info.size])
patched = _patch_thin_scheme(
thin, expect_hits=per_slice, label=f"{label}[slice{index}]"
)
if len(patched) != slice_info.size:
raise SystemExit(f"{label}: fat slice size changed after scheme patch")
buf[slice_info.offset : slice_info.offset + slice_info.size] = patched
return bytes(buf)