Fix C24: patch_kcall10 sysent table base, chained fixup encoding, PAC signing

Three bugs caused NOT_BOOT (timeout):

1. Wrong sysent table base: first _nosys match is entry 428, not entry 0.
   Entry 0 is the indirect syscall handler. Fixed with backward scan.

2. Raw VA written to chained fixup pointer slot: struct.pack("<Q", cave_va)
   corrupts the fixup chain from sysent[439] onward. Fixed with proper
   auth rebase encoding (_encode_chained_auth_ptr).

3. Missing PAC parameters: dispatch uses BLRAA X8, X17 with X17=0xBCAD.
   Chained fixup must encode diversity=0xBCAD, key=IA, addrDiv=0.
   Chain 'next' field preserved from original entry.

Boot-tested OK via testing ramdisk.
This commit is contained in:
Lakr
2026-03-04 21:41:44 +08:00
parent 894c2d1551
commit c9fd521659
3 changed files with 314 additions and 126 deletions
+131 -66
View File
@@ -1,97 +1,162 @@
# C24 `patch_kcall10` # C24 `patch_kcall10`
## Status: NOT_BOOT (timeout, no panic) ## Status: BOOT OK
Previous status: NOT_BOOT (timeout, no panic).
## How the patch works ## How the patch works
- Source: `scripts/patchers/kernel_jb_patch_kcall10.py`. - Source: `scripts/patchers/kernel_jb_patch_kcall10.py`.
- Locator strategy: - Locator strategy:
1. Resolve `_nosys` (symbol or `mov w0,#0x4e; ret` pattern). 1. Resolve `_nosys` (symbol or `mov w0,#0x4e; ret` pattern).
2. Scan DATA segments for `sysent` table signature (entry decoding points to `_nosys`). 2. Scan DATA segments for first entry whose decoded pointer == `_nosys`.
3. Compute `sysent[439]` (`SYS_kas_info`) entry offset. 3. **Scan backward** in 24-byte steps from the match to find the real table start
(entry 0 is the indirect syscall handler, NOT `_nosys`).
4. Compute `sysent[439]` (`SYS_kas_info`) entry offset from the real base.
- Patch action: - Patch action:
- Inject `kcall10` shellcode in code cave (argument marshalling + `blr x16` + result write-back). - Inject `kcall10` shellcode in code cave (argument marshalling + `BLR X16` + result write-back).
- Rewrite `sysent[439]` fields: - Rewrite `sysent[439]` fields using **proper chained fixup encoding**:
- `sy_call` -> cave VA - `sy_call` → auth rebase pointer to cave (diversity=0xBCAD, key=IA, addrDiv=0)
- `sy_munge32` -> `_munge_wwwwwwww` (if resolved) - `sy_munge32` `_munge_wwwwwwww` (if resolved, same encoding)
- return type + arg metadata. - return type + arg metadata (non-pointer fields, written directly).
## Root cause analysis (completed)
Three bugs were identified, all contributing to the NOT_BOOT failure:
### Bug 1: Wrong sysent table base (CRITICAL)
The old code searched DATA segments for the first entry whose decoded pointer matched
`_nosys` and treated that as `sysent[0]`. But in XNU, entry 0 is the **indirect syscall
handler** (`sub_FFFFFE00080073B0`, calls audit then returns ENOSYS) — NOT the simple
`_nosys` function (`sub_FFFFFE0007F6901C`, just returns 78).
The first `_nosys` match appeared **428 entries** into the table:
- Old (wrong) sysent base: file 0x73E078, VA 0xFFFFFE0007742078
- Real sysent base: file 0x73B858, VA 0xFFFFFE000773F858
This meant the patcher was writing to `sysent[439+428] = sysent[867]`, which is way
past the end of the 558-entry table. The patcher was corrupting unrelated DATA.
**Verification via IDA:**
- Syscall dispatch function `sub_FFFFFE00081279E4` uses `off_FFFFFE000773F858` as
the sysent base: `v26 = &off_FFFFFE000773F858[3 * v25]` (3 qwords = 24 bytes/entry).
- Dispatch caps syscall number at 0x22E (558 entries max).
- Real `sysent[439]` at VA 0xFFFFFE0007742180 has `sy_call` = `sub_FFFFFE0008077978`
(returns 45 / ENOTSUP = `kas_info` stub).
**Fix:** After finding any `_nosys` match, scan backward in 24-byte steps. Each step
validates: (a) `sy_call` decodes to a code range, (b) metadata fields are reasonable
(`narg ≤ 12`, `arg_bytes ≤ 96`). Stop when validation fails or segment boundary reached.
Limited to 558 entries max to prevent runaway scanning.
### Bug 2: Raw VA written to chained fixup pointer (CRITICAL)
The old code wrote `struct.pack("<Q", cave_va)` — a raw 8-byte virtual address — to
`sysent[439].sy_call`. On arm64e kernelcaches, DATA segment pointers use **chained fixup
encoding**, not raw VAs:
## Patcher output
``` ```
_nosys found DYLD_CHAINED_PTR_64_KERNEL_CACHE auth rebase:
sysent table at file offset 0x73E078 bit[63]: isAuth = 1
Shellcode at file offset 0x00AB1720 (VA 0xFFFFFE0007AB5720) bits[62:51]: next (12 bits, 4-byte stride delta to next fixup)
sysent[439] at file offset 0x007409A0 (VA 0xFFFFFE00077449A0) bits[50:49]: key (0=IA, 1=IB, 2=DA, 3=DB)
35 patches emitted (32 shellcode + 3 sysent fields) bit[48]: addrDiv (1 = address-diversified)
Note: _munge_wwwwwwww NOT found — sy_munge32 field not patched bits[47:32]: diversity (16-bit PAC discriminator)
bits[31:30]: cacheLevel (0 for single-level)
bits[29:0]: target (file offset)
``` ```
## Root cause analysis (in progress) Writing a raw VA (e.g., `0xFFFFFE0007AB5720`) produces:
- `isAuth=1` (bit63 of kernel VA is 1)
- `next`, `key`, `addrDiv`, `diversity` = **garbage** from VA bits
- `target` = bits[31:0] of VA = wrong file offset
### Primary suspect: chained fixup pointer format mismatch This corrupts the chained fixup chain from `sysent[439]` onward, silently breaking
all subsequent syscall entries. This explains the NOT_BOOT timeout: no panic because
the corruption doesn't hit early boot syscalls, but init and daemons use corrupted
handlers.
The sysent table lives in a DATA segment. On arm64e kernelcaches, DATA segment **Fix:** Implemented `_encode_chained_auth_ptr()` that properly encodes:
pointers use **chained fixup encoding**, not raw virtual addresses: - `target` = cave file offset (bits[29:0])
- `diversity` = 0xBCAD (bits[47:32])
- `key` = 0/IA (bits[50:49])
- `addrDiv` = 0 (bit[48])
- `next` = preserved from original entry (bits[62:51])
- `isAuth` = 1 (bit[63])
- **Auth rebase** (bit63=1): `file_offset = bits[31:0]`, plus diversity/key metadata ### Bug 3: Missing PAC signing parameters
- **Non-auth rebase** (bit63=0): `VA = (bits[50:43] << 56) | bits[42:0]`
The patcher writes `struct.pack("<Q", cave_va)` — a raw 8-byte VA — to `sysent[439].sy_call`. The syscall dispatch at `0xFFFFFE0008127CC8`:
This is **not valid chained fixup format**. The kernel's pointer fixup chain will either: ```asm
MOV X17, #0xBCAD
BLRAA X8, X17 ; PAC-authenticated indirect call
```
1. Misinterpret the raw VA as a chained pointer and decode it to a wrong address ALL syscall `sy_call` pointers are called via `BLRAA X8, X17` with fixed discriminator
2. Break the fixup chain, corrupting subsequent sysent entries `X17 = 0xBCAD`. The chained fixup resolver PAC-signs each pointer during boot according
3. The pointer simply won't be resolved, leaving a garbage function pointer to its metadata (diversity, key, addrDiv). For the dispatch to authenticate correctly:
- `diversity` must be `0xBCAD`
- `key` must be `0` (IA, matching BLRAA = key A)
- `addrDiv` must be `0` (fixed discriminator, not address-blended)
This explains the NOT_BOOT (timeout) behavior — no panic because the corrupted The old code didn't set any of these — the raw VA had garbage metadata, so the
pointer is never dereferenced during early boot (syscall 439 is not called during fixup resolver would PAC-sign with wrong parameters, causing BLRAA to fail at runtime.
init), but the fixup chain corruption may silently break other syscall entries,
preventing the system from booting properly.
### Fix approach (TODO) **Fix:** `_encode_chained_auth_ptr()` sets all three fields correctly.
1. **Read raw bytes at sysent[0] and sysent[439]** via IDA MCP to confirm the ### Non-issue: BLR X16 in shellcode
chained fixup pointer format (auth vs non-auth rebase)
2. **Implement `_encode_chained_ptr()`** that produces the correct encoding:
- For auth rebase: set bit63=1, encode file offset in bits[31:0], set
appropriate key/diversity fields
- For non-auth rebase: encode VA with high8 bits in [50:43] and low43 in [42:0]
3. **Use encoded pointer** when writing `sy_call` and `sy_munge32`
4. **Verify the fixup chain** — sysent entries may be part of a linked chain
where each entry's `next` field points to the next pointer to fix up.
Breaking this chain corrupts all subsequent entries.
### Secondary concerns The shellcode uses `BLR X16` (raw indirect branch without PAC authentication) to call
the user-provided kernel function pointer. This is correct:
- `BLR Xn` strips PAC bits and branches to the resulting address
- It does NOT authenticate — so it works regardless of whether the pointer is PAC-signed
- The kernel function pointer is provided from userspace (raw VA), so no PAC involved
- **Missing `_munge_wwwwwwww`**: The symbol wasn't found. Without the correct ### Note: Missing `_munge_wwwwwwww`
munge function, the kernel may not properly marshal syscall arguments from
userspace. This may cause a panic when the syscall is actually invoked.
- **Code cave in __TEXT_EXEC**: The shellcode is placed at 0xAB1720 in __TEXT_EXEC.
Need to verify this region is executable at runtime (KTRR/CTRR may lock it).
- **BLR x16 in shellcode**: The shellcode uses `BLR X16` (raw encoding
`0xD63F0200`). On PAC-enabled kernels, this may need to be `BLRAAZ X16` or
similar authenticated branch to avoid PAC traps.
### Sysent table structure The symbol `_munge_wwwwwwww` was not found in this kernelcache. Without the munge
function, the kernel won't marshal 32-bit userspace arguments for this syscall.
This is only relevant for 32-bit callers; 64-bit callers pass arguments directly
and should work fine. The `sy_munge32` field is left unpatched (original value).
## Sysent table structure
``` ```
struct sysent { struct sysent {
sy_call_t *sy_call; // +0: function pointer (8 bytes) sy_call_t *sy_call; // +0: function pointer (8 bytes, chained fixup)
munge_t *sy_arg_munge32; // +8: argument munge function (8 bytes) munge_t *sy_arg_munge32; // +8: argument munge function (8 bytes, chained fixup)
int32_t sy_return_type; // +16: return type (4 bytes) int32_t sy_return_type; // +16: return type (4 bytes, plain int)
int16_t sy_narg; // +20: number of arguments (2 bytes) int16_t sy_narg; // +20: number of arguments (2 bytes, plain int)
uint16_t sy_arg_bytes; // +22: argument byte count (2 bytes) uint16_t sy_arg_bytes; // +22: argument byte count (2 bytes, plain int)
}; // total: 24 bytes per entry }; // total: 24 bytes per entry, max 558 entries (0x22E)
``` ```
### Key addresses ## Key addresses (corrected)
- sysent table: file 0x73E078, VA 0xFFFFFE0007742078 - Dispatch function: VA 0xFFFFFE00081279E4 (`sub_FFFFFE00081279E4`)
- sysent[439]: file 0x7409A0, VA 0xFFFFFE00077449A0 - Real sysent base: file 0x73B858, VA 0xFFFFFE000773F858 (`off_FFFFFE000773F858`)
- Code cave: file 0xAB1720, VA 0xFFFFFE0007AB5720 - Old (wrong) sysent base: file 0x73E078, VA 0xFFFFFE0007742078 (428 entries in)
- _nosys: found by pattern match - Real sysent[439]: file 0x73E180, VA 0xFFFFFE0007742180
- Original `sy_call` = `sub_FFFFFE0008077978` (returns 45/ENOTSUP = kas_info stub)
- Old (wrong) sysent[439]: file 0x7409A0, VA 0xFFFFFE00077449A0 (actually entry 867)
- Code cave: file 0xAB1720, VA 0xFFFFFE0007AB5720 (in __TEXT_EXEC)
- `_nosys`: `sub_FFFFFE0007F6901C` (file offset 0xF6501C), returns 78/ENOSYS
## Chained fixup data (from IDA analysis)
```
Dispatch sysent[0]: sy_call = sub_FFFFFE00080073B0 (indirect syscall, audit+ENOSYS)
sy_munge32 = NULL, ret=1, narg=0, bytes=0
Dispatch sysent[1]: sy_call = sub_FFFFFE0007FB0B6C (exit)
sy_munge32 = sub_FFFFFE0007C6AC2C, ret=0, narg=1, bytes=4
Dispatch sysent[439]: sy_call = sub_FFFFFE0008077978 (kas_info, returns ENOTSUP)
sy_munge32 = sub_FFFFFE0007C6AC4C, ret=1, narg=3, bytes=12
```
## Expected outcome ## Expected outcome
- Replace syscall 439 handler with arbitrary 10-arg kernel call trampoline behavior. - Replace syscall 439 handler with arbitrary 10-arg kernel call trampoline.
- Proper chained fixup encoding preserves the fixup chain for all subsequent entries.
- PAC signing with diversity=0xBCAD matches the dispatch's BLRAA authentication.
## Risk ## Risk
- Syscall table rewrite is extremely invasive; wrong pointer encoding breaks - Syscall table rewrite is invasive, but proper chained fixup encoding and chain
the fixup chain and can silently corrupt many syscall handlers. preservation should make it safe.
- BLR without PAC authentication may cause kernel traps. - Code cave in __TEXT_EXEC is within the KTRR-protected region — already validated
as executable in C23 testing.
@@ -277,6 +277,10 @@ with capstone semantic matching and keystone-generated patch bytes only:
- Inline trampoline (B cave at function entry) replaces ops table pointer rewrite - Inline trampoline (B cave at function entry) replaces ops table pointer rewrite
- Ops table pointer modification breaks chained fixup integrity → PAC failures - Ops table pointer modification breaks chained fixup integrity → PAC failures
24. `kcall10` syscall 439 replacement shellcode 24. `kcall10` syscall 439 replacement shellcode
- Sysent table base found via backward scan from first `_nosys` match (entry 0 is indirect syscall, not `_nosys`)
- `sy_call` encoded as auth rebase chained fixup pointer (diversity=0xBCAD, key=IA, addrDiv=0)
- Matches dispatch's `BLRAA X8, X17` with `X17=0xBCAD` PAC authentication
- Chain `next` field preserved from original entry to maintain fixup chain integrity
## Cross-Version Dynamic Snapshot ## Cross-Version Dynamic Snapshot
+179 -60
View File
@@ -2,13 +2,123 @@
from .kernel_jb_base import asm, _rd32, _rd64, RET, NOP, struct from .kernel_jb_base import asm, _rd32, _rd64, RET, NOP, struct
# Max sysent entries in XNU (dispatch clamps at 0x22E = 558).
_SYSENT_MAX_ENTRIES = 558
# Each sysent entry is 24 bytes.
_SYSENT_ENTRY_SIZE = 24
# PAC discriminator used by the syscall dispatch (MOV X17, #0xBCAD; BLRAA X8, X17).
_SYSENT_PAC_DIVERSITY = 0xBCAD
class KernelJBPatchKcall10Mixin: class KernelJBPatchKcall10Mixin:
def _find_sysent_table(self, nosys_off):
"""Find the real sysent table base.
Strategy:
1. Find any DATA entry whose decoded pointer == _nosys.
2. Scan backward in 24-byte steps to find the true table start
(entry 0 is the indirect syscall handler, NOT _nosys).
3. Validate each backward entry: sy_call decodes to a code range
AND the metadata fields (narg, arg_bytes) look reasonable.
Previous bug: the old code took the first _nosys match as entry 0,
but _nosys first appears at entry ~428 (varies by XNU build).
"""
# Step 1: find any _nosys-matching entry
nosys_entry = -1
seg_start = -1
for seg_name, vmaddr, fileoff, filesize, _ in self.all_segments:
if "DATA" not in seg_name:
continue
for off in range(fileoff, fileoff + filesize - _SYSENT_ENTRY_SIZE, 8):
val = _rd64(self.raw, off)
decoded = self._decode_chained_ptr(val)
if decoded == nosys_off:
# Verify: next entry should also have valid sy_call
val2 = _rd64(self.raw, off + _SYSENT_ENTRY_SIZE)
decoded2 = self._decode_chained_ptr(val2)
if decoded2 > 0 and any(
s <= decoded2 < e for s, e in self.code_ranges
):
nosys_entry = off
seg_start = fileoff
break
if nosys_entry >= 0:
break
if nosys_entry < 0:
return -1
self._log(
f" [*] _nosys entry found at foff 0x{nosys_entry:X}, "
f"scanning backward for table start"
)
# Step 2: scan backward to find entry 0
base = nosys_entry
entries_back = 0
while base - _SYSENT_ENTRY_SIZE >= seg_start:
if entries_back >= _SYSENT_MAX_ENTRIES:
break
prev = base - _SYSENT_ENTRY_SIZE
# Check sy_call decodes to valid code
val = _rd64(self.raw, prev)
decoded = self._decode_chained_ptr(val)
if decoded <= 0 or not any(
s <= decoded < e for s, e in self.code_ranges
):
break
# Check metadata looks like a sysent entry
narg = struct.unpack_from("<H", self.raw, prev + 20)[0]
arg_bytes = struct.unpack_from("<H", self.raw, prev + 22)[0]
if narg > 12 or arg_bytes > 96:
break
base = prev
entries_back += 1
self._log(
f" [+] sysent table base at foff 0x{base:X} "
f"({entries_back} entries before first _nosys)"
)
return base
def _encode_chained_auth_ptr(self, target_foff, next_val, diversity=0,
key=0, addr_div=0):
"""Encode an arm64e kernel cache auth rebase chained fixup pointer.
Layout (DYLD_CHAINED_PTR_64_KERNEL_CACHE):
bits[29:0]: target (file offset)
bits[31:30]: cacheLevel (0)
bits[47:32]: diversity (16 bits)
bit[48]: addrDiv
bits[50:49]: key (0=IA, 1=IB, 2=DA, 3=DB)
bits[62:51]: next (12 bits, 4-byte stride delta to next fixup)
bit[63]: isAuth (1)
"""
val = (
(target_foff & 0x3FFFFFFF)
| ((diversity & 0xFFFF) << 32)
| ((addr_div & 1) << 48)
| ((key & 3) << 49)
| ((next_val & 0xFFF) << 51)
| (1 << 63)
)
return struct.pack("<Q", val)
def _extract_chain_next(self, raw_val):
"""Extract the 'next' chain field from a raw chained fixup pointer."""
return (raw_val >> 51) & 0xFFF
def patch_kcall10(self): def patch_kcall10(self):
"""Replace SYS_kas_info (syscall 439) with kcall10 shellcode. """Replace SYS_kas_info (syscall 439) with kcall10 shellcode.
Anchor: find _nosys function by pattern, then search DATA segments Anchor: find _nosys function by pattern, then search DATA segments
for the sysent table (first entry points to _nosys). for the sysent table base (backward scan from first _nosys entry).
The sysent dispatch uses BLRAA X8, X17 with X17=0xBCAD, so all
sy_call pointers must be PAC-signed with key=IA, diversity=0xBCAD,
addrDiv=0. We encode the cave pointer as a proper auth rebase
chained fixup entry to match.
""" """
self._log("\n[JB] kcall10: syscall 439 replacement") self._log("\n[JB] kcall10: syscall 439 replacement")
@@ -30,26 +140,8 @@ class KernelJBPatchKcall10Mixin:
munge_off = off munge_off = off
break break
# Search for sysent table in DATA segments # Find sysent table (real base via backward scan)
sysent_off = -1 sysent_off = self._find_sysent_table(nosys_off)
for seg_name, vmaddr, fileoff, filesize, _ in self.all_segments:
if "DATA" not in seg_name:
continue
for off in range(fileoff, fileoff + filesize - 24, 8):
val = _rd64(self.raw, off)
decoded = self._decode_chained_ptr(val)
if decoded == nosys_off:
# Verify: sysent[1] should also point to valid code
val2 = _rd64(self.raw, off + 24)
decoded2 = self._decode_chained_ptr(val2)
if decoded2 > 0 and any(
s <= decoded2 < e for s, e in self.code_ranges
):
sysent_off = off
break
if sysent_off >= 0:
break
if sysent_off < 0: if sysent_off < 0:
self._log(" [-] sysent table not found") self._log(" [-] sysent table not found")
return False return False
@@ -57,7 +149,7 @@ class KernelJBPatchKcall10Mixin:
self._log(f" [+] sysent table at file offset 0x{sysent_off:X}") self._log(f" [+] sysent table at file offset 0x{sysent_off:X}")
# Entry 439 (SYS_kas_info) # Entry 439 (SYS_kas_info)
entry_439 = sysent_off + 439 * 24 entry_439 = sysent_off + 439 * _SYSENT_ENTRY_SIZE
# Find code cave for kcall10 shellcode (~128 bytes = 32 instructions) # Find code cave for kcall10 shellcode (~128 bytes = 32 instructions)
cave = self._find_code_cave(128) cave = self._find_code_cave(128)
@@ -66,58 +158,85 @@ class KernelJBPatchKcall10Mixin:
return False return False
# Build kcall10 shellcode # Build kcall10 shellcode
# Syscall args arrive via the saved state on the stack.
# arg[0] = pointer to a userspace buffer with {func_ptr, arg0..arg9}.
# We unpack, call func_ptr(arg0..arg9), write results back.
parts = [ parts = [
asm("ldr x10, [sp, #0x40]"), # 0 asm("ldr x10, [sp, #0x40]"), # 0
asm("ldp x0, x1, [x10, #0]"), # 1 asm("ldp x0, x1, [x10, #0]"), # 1
asm("ldp x2, x3, [x10, #0x10]"), # 2 asm("ldp x2, x3, [x10, #0x10]"), # 2
asm("ldp x4, x5, [x10, #0x20]"), # 3 asm("ldp x4, x5, [x10, #0x20]"), # 3
asm("ldp x6, x7, [x10, #0x30]"), # 4 asm("ldp x6, x7, [x10, #0x30]"), # 4
asm("ldp x8, x9, [x10, #0x40]"), # 5 asm("ldp x8, x9, [x10, #0x40]"), # 5
asm("ldr x10, [x10, #0x50]"), # 6 asm("ldr x10, [x10, #0x50]"), # 6
asm("mov x16, x0"), # 7 asm("mov x16, x0"), # 7
asm("mov x0, x1"), # 8 asm("mov x0, x1"), # 8
asm("mov x1, x2"), # 9 asm("mov x1, x2"), # 9
asm("mov x2, x3"), # 10 asm("mov x2, x3"), # 10
asm("mov x3, x4"), # 11 asm("mov x3, x4"), # 11
asm("mov x4, x5"), # 12 asm("mov x4, x5"), # 12
asm("mov x5, x6"), # 13 asm("mov x5, x6"), # 13
asm("mov x6, x7"), # 14 asm("mov x6, x7"), # 14
asm("mov x7, x8"), # 15 asm("mov x7, x8"), # 15
asm("mov x8, x9"), # 16 asm("mov x8, x9"), # 16
asm("mov x9, x10"), # 17 asm("mov x9, x10"), # 17
asm("stp x29, x30, [sp, #-0x10]!"), # 18 asm("stp x29, x30, [sp, #-0x10]!"), # 18
bytes([0x00, 0x02, 0x3F, 0xD6]), # 19: BLR x16 bytes([0x00, 0x02, 0x3F, 0xD6]), # 19: BLR x16
asm("ldp x29, x30, [sp], #0x10"), # 20 asm("ldp x29, x30, [sp], #0x10"), # 20
asm("ldr x11, [sp, #0x40]"), # 21 asm("ldr x11, [sp, #0x40]"), # 21
NOP, # 22 NOP, # 22
asm("stp x0, x1, [x11, #0]"), # 23 asm("stp x0, x1, [x11, #0]"), # 23
asm("stp x2, x3, [x11, #0x10]"), # 24 asm("stp x2, x3, [x11, #0x10]"), # 24
asm("stp x4, x5, [x11, #0x20]"), # 25 asm("stp x4, x5, [x11, #0x20]"), # 25
asm("stp x6, x7, [x11, #0x30]"), # 26 asm("stp x6, x7, [x11, #0x30]"), # 26
asm("stp x8, x9, [x11, #0x40]"), # 27 asm("stp x8, x9, [x11, #0x40]"), # 27
asm("str x10, [x11, #0x50]"), # 28 asm("str x10, [x11, #0x50]"), # 28
asm("mov x0, #0"), # 29 asm("mov x0, #0"), # 29
asm("ret"), # 30 asm("ret"), # 30
NOP, # 31 NOP, # 31
] ]
for i, part in enumerate(parts): for i, part in enumerate(parts):
self.emit(cave + i * 4, part, f"shellcode+{i * 4} [kcall10]") self.emit(cave + i * 4, part, f"shellcode+{i * 4} [kcall10]")
# Patch sysent[439] # ── Patch sysent[439] with proper chained fixup encoding ────────
cave_va = self.base_va + cave #
# The old code wrote raw VAs (struct.pack("<Q", cave_va)) which
# breaks the chained fixup chain and produces invalid PAC-signed
# pointers. We now encode as auth rebase pointers matching the
# dispatch's BLRAA X8, X17 (X17=0xBCAD).
# Read original raw value to preserve the chain 'next' field
old_sy_call_raw = _rd64(self.raw, entry_439)
call_next = self._extract_chain_next(old_sy_call_raw)
self.emit( self.emit(
entry_439, entry_439,
struct.pack("<Q", cave_va), self._encode_chained_auth_ptr(
f"sysent[439].sy_call = 0x{cave_va:X} [kcall10]", cave,
next_val=call_next,
diversity=_SYSENT_PAC_DIVERSITY,
key=0, # IA
addr_div=0, # fixed discriminator (not address-blended)
),
f"sysent[439].sy_call = cave 0x{cave:X} "
f"(auth rebase, div=0xBCAD, next={call_next}) [kcall10]",
) )
if munge_off >= 0: if munge_off >= 0:
munge_va = self.base_va + munge_off old_sy_munge_raw = _rd64(self.raw, entry_439 + 8)
munge_next = self._extract_chain_next(old_sy_munge_raw)
self.emit( self.emit(
entry_439 + 8, entry_439 + 8,
struct.pack("<Q", munge_va), self._encode_chained_auth_ptr(
f"sysent[439].sy_munge32 = 0x{munge_va:X} [kcall10]", munge_off,
next_val=munge_next,
diversity=_SYSENT_PAC_DIVERSITY,
key=0,
addr_div=0,
),
f"sysent[439].sy_munge32 = 0x{munge_off:X} "
f"(auth rebase, next={munge_next}) [kcall10]",
) )
# sy_return_type = SYSCALL_RET_UINT64_T (7) # sy_return_type = SYSCALL_RET_UINT64_T (7)