622 lines
20 KiB
Python
622 lines
20 KiB
Python
"""Patch PLServerPool DGA helper to return fixed domain lists (probe/failover kept)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
from dataclasses import dataclass, field
|
|
|
|
from _common import OLD_DEP, OLD_REP, pack_seed
|
|
|
|
_SUB_SP_E0 = 0xD10383FF
|
|
_MURMUR = bytes.fromhex("21368f52e1c6b372")
|
|
_NOP = 0xD503201F
|
|
|
|
MAX_DOMAINS_PER_POOL = 8
|
|
MAX_DOMAIN_LEN = 63
|
|
|
|
|
|
@dataclass
|
|
class SlicePatch:
|
|
file_offset: int
|
|
size: int
|
|
cpu_subtype: int
|
|
|
|
|
|
@dataclass
|
|
class SliceInfo:
|
|
blob: bytes
|
|
file_offset: int
|
|
cpu_subtype: int
|
|
sections: dict[str, tuple[int, int]] = field(default_factory=dict) # name -> (vm/file off, size)
|
|
|
|
@property
|
|
def is_arm64e(self) -> bool:
|
|
return bool(self.cpu_subtype & 0x80000000)
|
|
|
|
|
|
def iter_slices(data: bytes) -> list[SlicePatch]:
|
|
magic = struct.unpack_from("<I", data, 0)[0]
|
|
if magic in (0xBEBAFECA, 0xCAFEBABE):
|
|
nfat = struct.unpack_from(">I", data, 4)[0]
|
|
out: list[SlicePatch] = []
|
|
for i in range(nfat):
|
|
o = 8 + i * 20
|
|
_ct, cs, soff, ssize, _align = struct.unpack_from(">IIIII", data, o)
|
|
out.append(SlicePatch(soff, ssize, cs))
|
|
return out
|
|
return [SlicePatch(0, len(data), 0)]
|
|
|
|
|
|
def _parse_slice(data: bytes, sl: SlicePatch) -> SliceInfo:
|
|
blob = data[sl.file_offset : sl.file_offset + sl.size]
|
|
info = SliceInfo(blob=blob, file_offset=sl.file_offset, cpu_subtype=sl.cpu_subtype)
|
|
_magic, _ct, _cs, _ft, ncmds = struct.unpack_from("<IIIII", blob, 0)
|
|
off = 32
|
|
for _ in range(ncmds):
|
|
cmd, cmdsize = struct.unpack_from("<II", blob, off)
|
|
if cmd == 0x19: # LC_SEGMENT_64
|
|
nsects = struct.unpack_from("<I", blob, off + 64)[0]
|
|
so = off + 72
|
|
for _s in range(nsects):
|
|
sn = blob[so : so + 16].split(b"\x00")[0].decode()
|
|
saddr = struct.unpack_from("<Q", blob, so + 32)[0]
|
|
ssize = struct.unpack_from("<Q", blob, so + 40)[0]
|
|
sfo = struct.unpack_from("<I", blob, so + 48)[0]
|
|
# In these binaries vmaddr == fileoff for most sections.
|
|
info.sections[sn] = (sfo if sfo else saddr, ssize)
|
|
so += 80
|
|
off += cmdsize
|
|
return info
|
|
|
|
|
|
def normalize_domain(raw: str) -> str:
|
|
value = raw.strip()
|
|
if not value:
|
|
raise SystemExit("empty domain")
|
|
for prefix in ("https://", "http://"):
|
|
if value.lower().startswith(prefix):
|
|
value = value[len(prefix) :]
|
|
value = value.split("/")[0].strip()
|
|
if ":" in value:
|
|
host, port = value.rsplit(":", 1)
|
|
if port.isdigit():
|
|
value = host
|
|
if len(value) > MAX_DOMAIN_LEN:
|
|
raise SystemExit(f"domain longer than {MAX_DOMAIN_LEN}: {value!r}")
|
|
if not all(32 <= ord(ch) < 127 for ch in value):
|
|
raise SystemExit(f"domain must be ASCII: {value!r}")
|
|
return value
|
|
|
|
|
|
def parse_domain_list(values: list[str] | None, *, label: str) -> list[str]:
|
|
if not values:
|
|
raise SystemExit(f"{label}: provide at least one domain")
|
|
out: list[str] = []
|
|
for item in values:
|
|
for part in str(item).split(","):
|
|
part = part.strip()
|
|
if part:
|
|
out.append(normalize_domain(part))
|
|
if not out:
|
|
raise SystemExit(f"{label}: provide at least one domain")
|
|
if len(out) > MAX_DOMAINS_PER_POOL:
|
|
raise SystemExit(f"{label}: at most {MAX_DOMAINS_PER_POOL} domains")
|
|
seen: set[str] = set()
|
|
uniq: list[str] = []
|
|
for d in out:
|
|
if d not in seen:
|
|
seen.add(d)
|
|
uniq.append(d)
|
|
return uniq
|
|
|
|
|
|
def pack_domain_tables(dep: list[str], rep: list[str]) -> tuple[bytes, bytes]:
|
|
def one(domains: list[str]) -> bytes:
|
|
return bytes([len(domains)]) + b"".join(d.encode("ascii") + b"\x00" for d in domains)
|
|
|
|
return one(dep), one(rep)
|
|
|
|
|
|
def _enc_bl(pc: int, target: int) -> int:
|
|
imm = (target - pc) // 4
|
|
if not (-0x2000000 <= imm < 0x2000000):
|
|
raise SystemExit(f"bl out of range {pc:#x}->{target:#x}")
|
|
return 0x94000000 | (imm & 0x3FFFFFF)
|
|
|
|
|
|
def _enc_b(pc: int, target: int) -> int:
|
|
imm = (target - pc) // 4
|
|
if not (-0x2000000 <= imm < 0x2000000):
|
|
raise SystemExit(f"b out of range {pc:#x}->{target:#x}")
|
|
return 0x14000000 | (imm & 0x3FFFFFF)
|
|
|
|
|
|
def _enc_adr(rd: int, pc: int, target: int) -> int:
|
|
imm = target - pc
|
|
if not (-1048576 <= imm < 1048576):
|
|
raise SystemExit(f"adr out of range {pc:#x}->{target:#x}")
|
|
immlo = imm & 3
|
|
immhi = (imm >> 2) & 0x7FFFF
|
|
return 0x10000000 | (immlo << 29) | (immhi << 5) | rd
|
|
|
|
|
|
def _enc_adrp(rd: int, pc: int, target: int) -> int:
|
|
imm = (target >> 12) - (pc >> 12)
|
|
if not (-1048576 <= imm < 1048576):
|
|
raise SystemExit(f"adrp out of range {pc:#x}->{target:#x}")
|
|
immlo = imm & 3
|
|
immhi = (imm >> 2) & 0x1FFFFF
|
|
return 0x90000000 | (immlo << 29) | (immhi << 5) | rd
|
|
|
|
|
|
def _enc_ldr64_uoff(rt: int, rn: int, offset: int) -> int:
|
|
if offset % 8:
|
|
raise SystemExit("ldr offset must be 8-aligned")
|
|
imm12 = offset // 8
|
|
if not (0 <= imm12 <= 0xFFF):
|
|
raise SystemExit(f"ldr offset too large: {offset}")
|
|
return 0xF9400000 | (imm12 << 10) | (rn << 5) | rt
|
|
|
|
|
|
def _decode_ptr(raw: int, blob_len: int) -> int | None:
|
|
"""Decode plain or dyld-chained rebase pointer to a file/vm offset."""
|
|
if 0 < raw < blob_len:
|
|
return raw
|
|
# dyld_chained_ptr_64_rebase / arm64e variants: low 36 bits often hold target
|
|
target = raw & ((1 << 36) - 1)
|
|
if 0 < target < blob_len:
|
|
return target
|
|
return None
|
|
|
|
|
|
def _find_cfstring_for_cstring(info: SliceInfo, cstring_off: int) -> int:
|
|
blob = info.blob
|
|
# Fast path: plain pointer
|
|
ptr = struct.pack("<Q", cstring_off)
|
|
start = 0
|
|
while True:
|
|
i = blob.find(ptr, start)
|
|
if i < 0:
|
|
break
|
|
if i >= 16:
|
|
cfs = i - 16
|
|
length = struct.unpack_from("<Q", blob, cfs + 24)[0]
|
|
if length == 32:
|
|
return cfs
|
|
start = i + 1
|
|
|
|
# arm64e: scan __cfstring
|
|
off, size = info.sections.get("__cfstring", (0, 0))
|
|
if size:
|
|
for i in range(0, size, 32):
|
|
base = off + i
|
|
_isa, _flags, raw, length = struct.unpack_from("<QQQQ", blob, base)
|
|
if length != 32:
|
|
continue
|
|
tgt = _decode_ptr(raw, len(blob))
|
|
if tgt == cstring_off:
|
|
return base
|
|
raise SystemExit(f"CFString not found for cstring @{cstring_off:#x}")
|
|
|
|
|
|
def _find_stub_for_selector(info: SliceInfo, name: bytes) -> int:
|
|
blob = info.blob
|
|
name_off = blob.find(name + b"\x00")
|
|
if name_off < 0:
|
|
raise SystemExit(f"missing selector {name!r}")
|
|
|
|
selrefs: list[int] = []
|
|
# plain
|
|
ptr = struct.pack("<Q", name_off)
|
|
start = 0
|
|
while True:
|
|
i = blob.find(ptr, start)
|
|
if i < 0:
|
|
break
|
|
selrefs.append(i)
|
|
start = i + 1
|
|
# chained
|
|
off, size = info.sections.get("__objc_selrefs", (0, 0))
|
|
if size:
|
|
for i in range(0, size, 8):
|
|
base = off + i
|
|
raw = struct.unpack_from("<Q", blob, base)[0]
|
|
if _decode_ptr(raw, len(blob)) == name_off:
|
|
selrefs.append(base)
|
|
|
|
if not selrefs:
|
|
raise SystemExit(f"missing selref for {name!r}")
|
|
|
|
stubs_off, stubs_size = info.sections.get("__objc_stubs", (0xC0000, 0x40000))
|
|
lo = stubs_off
|
|
hi = stubs_off + stubs_size if stubs_size else min(len(blob), 0x100000)
|
|
|
|
for selref in selrefs:
|
|
for i in range(lo, hi, 4):
|
|
ins = struct.unpack_from("<I", blob, i)[0]
|
|
if (ins & 0x9F000000) != 0x90000000 or (ins & 0x1F) != 1:
|
|
continue
|
|
immlo = (ins >> 29) & 3
|
|
immhi = (ins >> 5) & 0x1FFFFF
|
|
imm = (immhi << 2) | immlo
|
|
if imm & (1 << 20):
|
|
imm -= 1 << 21
|
|
page = ((i >> 12) + imm) << 12
|
|
ins2 = struct.unpack_from("<I", blob, i + 4)[0]
|
|
if (ins2 & 0xFFC00000) != 0xF9400000 or (ins2 & 0x1F) != 1:
|
|
continue
|
|
imm12 = (ins2 >> 10) & 0xFFF
|
|
if page + imm12 * 8 == selref:
|
|
return i
|
|
raise SystemExit(f"missing objc stub for selector {name!r}")
|
|
|
|
|
|
def _find_classrefs(info: SliceInfo, body: int) -> tuple[int, int]:
|
|
blob = info.blob
|
|
cr_off, cr_size = info.sections.get("__objc_classrefs", (0x127E80, 0x400))
|
|
cr_lo, cr_hi = cr_off, cr_off + cr_size
|
|
hits: list[int] = []
|
|
|
|
# LDR literal (common in arm64)
|
|
for pc in range(body, body + 0x100, 4):
|
|
ins = struct.unpack_from("<I", blob, pc)[0]
|
|
if (ins & 0xFF000000) != 0x58000000:
|
|
continue
|
|
imm19 = (ins >> 5) & 0x7FFFF
|
|
if imm19 & 0x40000:
|
|
imm19 -= 0x80000
|
|
lit = pc + imm19 * 4
|
|
if cr_lo <= lit < cr_hi:
|
|
hits.append(lit)
|
|
|
|
# ADRP+LDR
|
|
for pc in range(body, body + 0x100, 4):
|
|
ins = struct.unpack_from("<I", blob, pc)[0]
|
|
if (ins & 0x9F000000) != 0x90000000:
|
|
continue
|
|
rd = ins & 0x1F
|
|
immlo = (ins >> 29) & 3
|
|
immhi = (ins >> 5) & 0x1FFFFF
|
|
imm = (immhi << 2) | immlo
|
|
if imm & (1 << 20):
|
|
imm -= 1 << 21
|
|
page = ((pc >> 12) + imm) << 12
|
|
if not (cr_lo <= page < cr_hi or cr_lo <= page + 0xFFF < cr_hi + 0x1000):
|
|
continue
|
|
ins2 = struct.unpack_from("<I", blob, pc + 4)[0]
|
|
if (ins2 & 0xFFC00000) != 0xF9400000:
|
|
continue
|
|
if ((ins2 >> 5) & 0x1F) != rd:
|
|
continue
|
|
imm12 = (ins2 >> 10) & 0xFFF
|
|
lit = page + imm12 * 8
|
|
if cr_lo <= lit < cr_hi:
|
|
hits.append(lit)
|
|
|
|
# de-dupe preserve order
|
|
uniq: list[int] = []
|
|
for h in hits:
|
|
if h not in uniq:
|
|
uniq.append(h)
|
|
if len(uniq) >= 2:
|
|
return uniq[0], uniq[1]
|
|
if len(uniq) == 1:
|
|
# NSString classref usually follows NSMutableArray
|
|
return uniq[0], uniq[0] + 8
|
|
# last resort: first two slots
|
|
return cr_off, cr_off + 8
|
|
|
|
|
|
def _collect_branch_targets(
|
|
blob: bytes, lo: int, hi: int, *, ops: tuple[int, ...] = (0x94000000,)
|
|
) -> list[int]:
|
|
out: list[int] = []
|
|
for i in range(lo, min(hi, len(blob) - 4), 4):
|
|
ins = struct.unpack_from("<I", blob, i)[0]
|
|
op = ins & 0xFC000000
|
|
if op not in ops:
|
|
continue
|
|
imm = ins & 0x3FFFFFF
|
|
if imm & 0x2000000:
|
|
imm -= 0x4000000
|
|
out.append(i + imm * 4)
|
|
return out
|
|
|
|
|
|
def _discover_dga(blob: bytes) -> tuple[int, int, int]:
|
|
idx = blob.find(_MURMUR)
|
|
if idx < 0:
|
|
raise SystemExit("DGA murmur constant not found")
|
|
body = None
|
|
for back in range(0, 0x300, 4):
|
|
addr = idx - back
|
|
if addr >= 0 and struct.unpack_from("<I", blob, addr)[0] == _SUB_SP_E0:
|
|
body = addr
|
|
break
|
|
if body is None:
|
|
raise SystemExit("DGA prologue not found")
|
|
entry = body
|
|
if body >= 8:
|
|
ins_b = struct.unpack_from("<I", blob, body - 8)[0]
|
|
ins_pac = struct.unpack_from("<I", blob, body - 4)[0]
|
|
if (ins_b & 0xFC000000) == 0x14000000 and ins_pac in (0xD503237F, 0xD503233F):
|
|
entry = body - 8
|
|
end = body + 0x360
|
|
for a in range(body + 0x80, body + 0x400, 4):
|
|
if a + 4 > len(blob):
|
|
break
|
|
if struct.unpack_from("<I", blob, a)[0] == _SUB_SP_E0:
|
|
end = a
|
|
break
|
|
return entry, body, end
|
|
|
|
|
|
def _resolve_runtime_stubs(blob: bytes, body: int, end: int) -> dict[str, int]:
|
|
"""Map objc_retain / release / retainAutoreleased / autoreleaseReturnValue stubs."""
|
|
early = _collect_branch_targets(blob, body, body + 0x50, ops=(0x94000000,))
|
|
all_bl = _collect_branch_targets(blob, body, end, ops=(0x94000000,))
|
|
all_b = _collect_branch_targets(blob, body, end, ops=(0x14000000,))
|
|
if not early:
|
|
raise SystemExit("DGA helper has no early bl (objc_retain)")
|
|
retain = early[0]
|
|
page = retain & ~0xFFF
|
|
# libobjc stub island on same 4K page
|
|
island = sorted({t for t in (all_bl + all_b) if (t & ~0xFFF) == page})
|
|
if retain not in island:
|
|
island = sorted(set(island + [retain]))
|
|
# Typical layout near retain: ... autoreleaseReturn, release, retain, retainAutoreleased
|
|
lower = [t for t in island if t < retain]
|
|
higher = [t for t in island if t > retain]
|
|
release = lower[-1] if lower else None
|
|
auto_ret = lower[-2] if len(lower) >= 2 else (lower[0] if lower else None)
|
|
retain_auto = higher[0] if higher else None
|
|
# Fallbacks if ordering differs
|
|
if release is None and len(island) >= 2:
|
|
release = next((t for t in island if t != retain), None)
|
|
if retain_auto is None and len(island) >= 3:
|
|
retain_auto = next((t for t in island if t not in (retain, release)), None)
|
|
if auto_ret is None:
|
|
auto_ret = next((t for t in all_b if (t & ~0xFFF) == page), None)
|
|
if None in (retain, release, retain_auto, auto_ret):
|
|
raise SystemExit(
|
|
f"runtime stubs incomplete island={[hex(x) for x in island]} "
|
|
f"retain={retain!r} release={release!r} retainAuto={retain_auto!r} autoRet={auto_ret!r}"
|
|
)
|
|
return {
|
|
"retain": retain,
|
|
"release": release,
|
|
"retainAutoreleased": retain_auto,
|
|
"autoreleaseReturn": auto_ret,
|
|
}
|
|
|
|
|
|
def _apply_shellcode(
|
|
blob: bytes,
|
|
*,
|
|
entry: int,
|
|
end: int,
|
|
stubs: dict[str, int],
|
|
class_array: int,
|
|
class_string: int,
|
|
dep_cf: int,
|
|
rep_cf: int,
|
|
dep_pack: bytes,
|
|
rep_pack: bytes,
|
|
label: str,
|
|
) -> bytes:
|
|
avail = end - entry
|
|
code: list[int] = []
|
|
labels: dict[str, int] = {}
|
|
pending: list[tuple[int, str, str]] = []
|
|
|
|
def pc() -> int:
|
|
return entry + len(code) * 4
|
|
|
|
def emit(ins: int) -> None:
|
|
code.append(ins & 0xFFFFFFFF)
|
|
|
|
def mark(name: str) -> None:
|
|
labels[name] = pc()
|
|
|
|
def bl(target: int) -> None:
|
|
emit(_enc_bl(pc(), target))
|
|
|
|
def b_label(name: str) -> None:
|
|
pending.append((len(code), "b", name))
|
|
emit(0)
|
|
|
|
def cbz(rt: int, name: str) -> None:
|
|
pending.append((len(code), f"cbz{rt}", name))
|
|
emit(0)
|
|
|
|
def cbnz(rt: int, name: str) -> None:
|
|
pending.append((len(code), f"cbnz{rt}", name))
|
|
emit(0)
|
|
|
|
def adr(rd: int, name: str) -> None:
|
|
pending.append((len(code), f"adr{rd}", name))
|
|
emit(0)
|
|
|
|
def adrp_ldr(rd: int, abs_addr: int) -> None:
|
|
p = pc()
|
|
emit(_enc_adrp(rd, p, abs_addr))
|
|
emit(_enc_ldr64_uoff(rd, rd, abs_addr & 0xFFF))
|
|
|
|
# Save every callee-saved reg we touch (x19-x22, x25). Omitting these
|
|
# corrupts _generateDomainsLocked and aborts before any /sync probe.
|
|
emit(0xA9BC7BFD) # stp x29, x30, [sp, #-0x40]!
|
|
emit(0xA9014FF4) # stp x20, x19, [sp, #0x10]
|
|
emit(0xA90257F6) # stp x22, x21, [sp, #0x20]
|
|
emit(0xA90367FA) # stp x26, x25, [sp, #0x30]
|
|
emit(0x910103FD) # add x29, sp, #0x40
|
|
emit(0xAA0003F3) # mov x19, x0 ; seed
|
|
bl(stubs["retain"])
|
|
|
|
emit(0xAA1303E0)
|
|
adr(2, "dep_cf")
|
|
bl(stubs["isEqualToString"])
|
|
cbz(0, "check_rep")
|
|
adr(21, "dep_table")
|
|
b_label("build")
|
|
|
|
mark("check_rep")
|
|
emit(0xAA1303E0)
|
|
adr(2, "rep_cf")
|
|
bl(stubs["isEqualToString"])
|
|
cbz(0, "empty")
|
|
adr(21, "rep_table")
|
|
b_label("build")
|
|
|
|
mark("empty")
|
|
adrp_ldr(0, class_array)
|
|
emit(0xD2800002)
|
|
bl(stubs["arrayWithCapacity"])
|
|
bl(stubs["retainAutoreleased"])
|
|
emit(0xAA0003F4)
|
|
b_label("done")
|
|
|
|
mark("build")
|
|
emit(0x394002B6)
|
|
emit(0x910006B5)
|
|
adrp_ldr(0, class_array)
|
|
emit(0x2A1603E2)
|
|
bl(stubs["arrayWithCapacity"])
|
|
bl(stubs["retainAutoreleased"])
|
|
emit(0xAA0003F4)
|
|
|
|
mark("loop")
|
|
cbz(22, "done")
|
|
adrp_ldr(0, class_string)
|
|
emit(0xAA1503E2)
|
|
bl(stubs["stringWithUTF8"])
|
|
bl(stubs["retainAutoreleased"])
|
|
emit(0xAA0003F9)
|
|
emit(0xAA1403E0)
|
|
emit(0xAA1903E2)
|
|
bl(stubs["addObject"])
|
|
emit(0xAA1903E0)
|
|
bl(stubs["release"])
|
|
mark("scan")
|
|
emit(0x394002A8)
|
|
emit(0x910006B5)
|
|
cbnz(8, "scan")
|
|
emit(0x510006D6)
|
|
b_label("loop")
|
|
|
|
mark("done")
|
|
emit(0xAA1303E0) # mov x0, x19
|
|
bl(stubs["release"])
|
|
emit(0xAA1403E0) # mov x0, x20 ; NSArray*
|
|
emit(0xA94367FA) # ldp x26, x25, [sp, #0x30]
|
|
emit(0xA94257F6) # ldp x22, x21, [sp, #0x20]
|
|
emit(0xA9414FF4) # ldp x20, x19, [sp, #0x10]
|
|
emit(0xA8C47BFD) # ldp x29, x30, [sp], #0x40
|
|
emit(_enc_b(pc(), stubs["autoreleaseReturn"]))
|
|
|
|
table_off = entry + len(code) * 4
|
|
if table_off % 4:
|
|
while (entry + len(code) * 4) % 4:
|
|
emit(_NOP)
|
|
table_off = entry + len(code) * 4
|
|
dep_table = table_off
|
|
rep_table = table_off + len(dep_pack)
|
|
abs_map = {
|
|
"dep_cf": dep_cf,
|
|
"rep_cf": rep_cf,
|
|
"dep_table": dep_table,
|
|
"rep_table": rep_table,
|
|
}
|
|
|
|
for idx, kind, name in pending:
|
|
p = entry + idx * 4
|
|
if kind.startswith("adr"):
|
|
rd = int(kind[3:])
|
|
code[idx] = _enc_adr(rd, p, abs_map[name])
|
|
continue
|
|
target = labels[name]
|
|
imm19 = (target - p) // 4
|
|
if kind == "b":
|
|
code[idx] = _enc_b(p, target)
|
|
elif kind.startswith("cbz"):
|
|
rt = int(kind[3:])
|
|
code[idx] = 0x34000000 | ((imm19 & 0x7FFFF) << 5) | rt
|
|
elif kind.startswith("cbnz"):
|
|
rt = int(kind[4:])
|
|
code[idx] = 0x35000000 | ((imm19 & 0x7FFFF) << 5) | rt
|
|
else:
|
|
raise SystemExit(f"{label}: bad fixup {kind}")
|
|
|
|
payload = b"".join(struct.pack("<I", ins) for ins in code) + dep_pack + rep_pack
|
|
if len(payload) > avail:
|
|
raise SystemExit(
|
|
f"{label}: need {len(payload)} bytes, only {avail} free in DGA region"
|
|
)
|
|
|
|
new_blob = bytearray(blob)
|
|
new_blob[entry : entry + avail] = payload + b"\x00" * (avail - len(payload))
|
|
return bytes(new_blob)
|
|
|
|
|
|
def patch_fixed_domains_in_dylib(
|
|
data: bytes,
|
|
deployment_domains: list[str],
|
|
reporting_domains: list[str],
|
|
*,
|
|
deployment_seed: str,
|
|
reporting_seed: str,
|
|
label: str,
|
|
) -> bytes:
|
|
dep = parse_domain_list(deployment_domains, label="deployment")
|
|
rep = parse_domain_list(reporting_domains, label="reporting")
|
|
dep_pack, rep_pack = pack_domain_tables(dep, rep)
|
|
out = bytearray(data)
|
|
seed_dep = pack_seed(deployment_seed)
|
|
seed_rep = pack_seed(reporting_seed)
|
|
|
|
for si, sl in enumerate(iter_slices(data)):
|
|
info = _parse_slice(bytes(out), sl)
|
|
# re-parse from current out
|
|
info = _parse_slice(bytes(out), sl)
|
|
blob = info.blob
|
|
entry, body, end = _discover_dga(blob)
|
|
|
|
dep_cs = blob.find(seed_dep)
|
|
rep_cs = blob.find(seed_rep)
|
|
if dep_cs < 0 or rep_cs < 0:
|
|
dep_cs = blob.find(OLD_DEP)
|
|
rep_cs = blob.find(OLD_REP)
|
|
if dep_cs < 0 or rep_cs < 0:
|
|
raise SystemExit(f"{label} slice{si}: seed cstrings not found")
|
|
|
|
dep_cf = _find_cfstring_for_cstring(info, dep_cs)
|
|
rep_cf = _find_cfstring_for_cstring(info, rep_cs)
|
|
runtime = _resolve_runtime_stubs(blob, body, end)
|
|
stubs = {
|
|
**runtime,
|
|
"isEqualToString": _find_stub_for_selector(info, b"isEqualToString:"),
|
|
"arrayWithCapacity": _find_stub_for_selector(info, b"arrayWithCapacity:"),
|
|
"addObject": _find_stub_for_selector(info, b"addObject:"),
|
|
"stringWithUTF8": _find_stub_for_selector(info, b"stringWithUTF8String:"),
|
|
}
|
|
class_array, class_string = _find_classrefs(info, body)
|
|
patched = _apply_shellcode(
|
|
blob,
|
|
entry=entry,
|
|
end=end,
|
|
stubs=stubs,
|
|
class_array=class_array,
|
|
class_string=class_string,
|
|
dep_cf=dep_cf,
|
|
rep_cf=rep_cf,
|
|
dep_pack=dep_pack,
|
|
rep_pack=rep_pack,
|
|
label=f"{label}/slice{si}",
|
|
)
|
|
out[sl.file_offset : sl.file_offset + sl.size] = patched
|
|
print(
|
|
f"{label} slice{si}: fixed domains @ {entry:#x}..{end:#x} "
|
|
f"dep={len(dep)} rep={len(rep)} arm64e={info.is_arm64e}"
|
|
)
|
|
|
|
return bytes(out)
|