mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-06 01:34:27 +00:00
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user