fix: ios 16
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
"""Patch campaign / channel id embedded in type-0x01 and core/business dylibs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import secrets
|
||||
|
||||
# C-string keys immediately before the channel value in type-0x01 __cstring.
|
||||
CHANNEL_ANCHOR = b"u\x00d\x00f\x00s\x00c\x00"
|
||||
CHANNEL_LEN = 32
|
||||
# New channel ids: lowercase alphanumeric. Embedded originals remain hex.
|
||||
_CHANNEL_RE = re.compile(rb"^[0-9a-z]{32}$")
|
||||
_CHANNEL_HEX_RE = re.compile(rb"^[0-9a-f]{32}$")
|
||||
|
||||
|
||||
def gen_channel_id() -> str:
|
||||
"""32 lowercase hex chars (alphanumeric subset; pack-safe)."""
|
||||
return secrets.token_hex(16)
|
||||
|
||||
|
||||
def validate_channel_id(value: str, *, name: str = "--channel-id") -> str:
|
||||
if not isinstance(value, str) or not _CHANNEL_RE.fullmatch(value.encode("ascii")):
|
||||
raise SystemExit(
|
||||
f"{name} must be exactly {CHANNEL_LEN} lowercase [0-9a-z] chars "
|
||||
f"(got {value!r})"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def find_channel_offsets(data: bytes, *, expect_old: bytes | None = None) -> list[int]:
|
||||
"""Return offsets of the 32-byte channel value after CHANNEL_ANCHOR."""
|
||||
offsets: list[int] = []
|
||||
start = 0
|
||||
while True:
|
||||
index = data.find(CHANNEL_ANCHOR, start)
|
||||
if index < 0:
|
||||
break
|
||||
value_at = index + len(CHANNEL_ANCHOR)
|
||||
value = data[value_at : value_at + CHANNEL_LEN]
|
||||
if (
|
||||
len(value) == CHANNEL_LEN
|
||||
and _CHANNEL_HEX_RE.fullmatch(value)
|
||||
and data[value_at + CHANNEL_LEN : value_at + CHANNEL_LEN + 1] == b"\x00"
|
||||
):
|
||||
if expect_old is None or value == expect_old:
|
||||
offsets.append(value_at)
|
||||
start = index + 1
|
||||
return offsets
|
||||
|
||||
|
||||
def patch_channel_in_dylib(
|
||||
data: bytes,
|
||||
new_channel: str,
|
||||
*,
|
||||
old_channel: str | None = None,
|
||||
expect_hits: int = 1,
|
||||
label: str = "dylib",
|
||||
) -> bytes:
|
||||
"""In-place replace type-0x01 channel C-string (must stay {CHANNEL_LEN} bytes)."""
|
||||
new_channel = validate_channel_id(new_channel, name="channel")
|
||||
new_b = new_channel.encode("ascii")
|
||||
old_b = old_channel.encode("ascii") if old_channel else None
|
||||
if old_b is not None:
|
||||
validate_channel_id(old_channel, name="old channel")
|
||||
|
||||
offsets = find_channel_offsets(data, expect_old=old_b)
|
||||
if len(offsets) != expect_hits:
|
||||
raise SystemExit(
|
||||
f"{label}: channel anchor hits={len(offsets)} (want {expect_hits}). "
|
||||
"Secondary payload layout may have changed; re-adapt _channel_patch."
|
||||
)
|
||||
|
||||
buf = bytearray(data)
|
||||
for offset in offsets:
|
||||
buf[offset : offset + CHANNEL_LEN] = new_b
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def find_plain_channel_offsets(data: bytes, channel: bytes) -> list[int]:
|
||||
"""Return offsets of null-terminated plain ASCII channel C-strings."""
|
||||
if len(channel) != CHANNEL_LEN or not _CHANNEL_HEX_RE.fullmatch(channel):
|
||||
raise ValueError("channel must be 32 lowercase hex bytes")
|
||||
needle = channel + b"\x00"
|
||||
offsets: list[int] = []
|
||||
start = 0
|
||||
while True:
|
||||
index = data.find(needle, start)
|
||||
if index < 0:
|
||||
break
|
||||
offsets.append(index)
|
||||
start = index + CHANNEL_LEN
|
||||
return offsets
|
||||
|
||||
|
||||
def patch_plain_channel_in_dylib(
|
||||
data: bytes,
|
||||
new_channel: str,
|
||||
*,
|
||||
old_channel: str,
|
||||
expect_hits: int,
|
||||
label: str = "dylib",
|
||||
) -> bytes:
|
||||
"""Replace plain C-string channel (core / business plugins; no type-0x01 anchor).
|
||||
|
||||
FAT arm64+arm64e binaries typically embed the same string once per slice
|
||||
(expect_hits=2). Length must stay exactly 32 ASCII hex chars.
|
||||
"""
|
||||
new_channel = validate_channel_id(new_channel, name="channel")
|
||||
old_channel = validate_channel_id(old_channel, name="old channel")
|
||||
if new_channel == old_channel:
|
||||
return data
|
||||
|
||||
old_b = old_channel.encode("ascii")
|
||||
new_b = new_channel.encode("ascii")
|
||||
offsets = find_plain_channel_offsets(data, old_b)
|
||||
if len(offsets) != expect_hits:
|
||||
raise SystemExit(
|
||||
f"{label}: plain channel hits={len(offsets)} (want {expect_hits} for "
|
||||
f"{old_channel}). Core/plugin layout may have changed."
|
||||
)
|
||||
|
||||
buf = bytearray(data)
|
||||
for offset in offsets:
|
||||
buf[offset : offset + CHANNEL_LEN] = new_b
|
||||
return bytes(buf)
|
||||
Reference in New Issue
Block a user