Create txm_return_mechanism.md

This commit is contained in:
Lakr
2026-03-04 13:34:02 +08:00
parent 55a53e7179
commit 641d5b5a79
26 changed files with 1840 additions and 1507 deletions

View File

@@ -0,0 +1,121 @@
# Developer Mode via AMFI XPC
How iOS developer mode is enabled programmatically, based on TrollStore's implementation.
## XPC Service
**Mach service:** `com.apple.amfi.xpc`
AMFI (Apple Mobile File Integrity) daemon exposes an XPC endpoint for developer mode control on iOS 16+.
## Required Entitlement
```
com.apple.private.amfi.developer-mode-control = true
```
Without this entitlement the XPC connection to amfid is rejected.
## Message Protocol
Messages are serialized using private CoreFoundation-XPC bridge functions:
```objc
extern xpc_object_t _CFXPCCreateXPCMessageWithCFObject(CFTypeRef obj);
extern CFTypeRef _CFXPCCreateCFObjectFromXPCMessage(xpc_object_t obj);
```
### Request
NSDictionary with a single key:
```objc
@{@"action": @(action)}
```
### Actions
| Action | Value | Behavior |
| -------------------- | ----- | ---------------------------------------------------------------------------- |
| `kAMFIActionArm` | 0 | Arm developer mode — takes effect on next reboot, user must select "Turn On" |
| `kAMFIActionDisable` | 1 | Disable developer mode immediately |
| `kAMFIActionStatus` | 2 | Query current state |
### Response
XPC reply dict contains a `"cfreply"` key holding the CF-serialized response:
```objc
xpc_object_t cfReply = xpc_dictionary_get_value(reply, "cfreply");
NSDictionary *dict = _CFXPCCreateCFObjectFromXPCMessage(cfReply);
```
Response fields:
| Key | Type | Description |
| --------- | -------- | ------------------------------------------------ |
| `success` | BOOL | Whether the XPC call succeeded |
| `status` | BOOL | Current developer mode state (for Status action) |
| `armed` | BOOL | Whether armed for reboot (for Arm action) |
| `error` | NSString | Error description if success is false |
## Arming Flow
1. Query status (`kAMFIActionStatus`)
2. If already enabled, done
3. Send arm (`kAMFIActionArm`)
4. Device must reboot; user selects "Turn On" in the prompt
5. Developer mode is now active
Arming does **not** enable developer mode immediately. It sets a flag that triggers the enable prompt on the next reboot. Disabling (`kAMFIActionDisable`) takes effect immediately.
## TrollStore Reference
Source: `references/TrollStore/RootHelper/devmode.m`
TrollStore separates privileges: the main app has no AMFI entitlement; all privileged operations go through RootHelper which has `com.apple.private.amfi.developer-mode-control`.
Key functions:
- `checkDeveloperMode()` — returns current state, YES on iOS <16 (devmode doesn't exist)
- `armDeveloperMode(BOOL *alreadyEnabled)` check + arm in one call
- `startConnection()` creates and resumes XPC connection to `com.apple.amfi.xpc`
- `sendXPCRequest()` CF dict XPC message sync reply CF dict
## vphoned Implementation
Added as `devmode` capability in vphoned guest agent:
### Protocol Messages
**Status query:**
```json
{"t": "devmode", "action": "status"}
{"t": "ok", "enabled": true}
```
**Enable (arm):**
```json
{"t": "devmode", "action": "enable"}
{"t": "ok", "already_enabled": false, "msg": "developer mode armed, reboot to activate"}
```
### Entitlements
Added to `scripts/vphoned/entitlements.plist`:
```xml
<key>com.apple.private.amfi.developer-mode-control</key>
<true/>
```
### Host-Side API (VPhoneControl.swift)
```swift
let status = try await control.sendDevModeStatus() // -> DevModeStatus (enabled: Bool)
let result = try await control.sendDevModeEnable() // -> DevModeEnableResult (alreadyEnabled: Bool, message: String)
```
Both methods use `sendRequest()` with pending request tracking the response is returned via `async throws`, not logged to console. Callers (e.g. `VPhoneMenuConnect`) display results as `NSAlert` sheets on the VM window.

View File

@@ -0,0 +1,320 @@
# Firmware Manifest & Component Origins
The erase install firmware is a **hybrid** of three source sets:
1. **PCC vresearch101ap** — boot chain (LLB/iBSS/iBEC/iBoot) and security monitors (SPTM/TXM)
2. **PCC vphone600ap** — runtime components (DeviceTree, SEP, KernelCache, RecoveryMode)
3. **iPhone 17,3** — OS image, trust caches, filesystem
The VM hardware identifies as **vresearch101ap** (BDID 0x90) in DFU mode, so the
BuildManifest identity must use vresearch101ap fields for TSS/SHSH signing. However,
runtime components use the **vphone600** variant because its DeviceTree sets MKB `dt=1`
(allows boot without system keybag), its SEP firmware matches the vphone600 device tree,
and `hardware target` reports as `vphone600ap` for proper iPhone emulation.
`fw_prepare.sh` downloads both IPSWs, merges cloudOS firmware into the iPhone
restore directory, then `fw_manifest.py` generates the hybrid BuildManifest.
---
## 1. Multi-Source IPSW Comparison
### Identity Count Overview
| Source | Identities | DeviceClasses |
| -------------- | ---------- | ------------------------------------------------------- |
| iPhone 26.1 | 5 | All d47ap |
| iPhone 26.3 | 5 | All d47ap |
| CloudOS 26.1 | 6 | j236cap, j475dap, vphone600ap (x2), vresearch101ap (x2) |
| KnownWork 26.1 | 5 | All vresearch101ap |
### CloudOS 26.1 Identity Structure (6 identities)
| Index | DeviceClass | Variant | BuildStyle | Manifest Keys |
| ----- | -------------- | --------------------------------------------------- | ---------------------- | ---------------------------- |
| [0] | j236cap | Darwin Cloud Customer Erase Install (IPSW) | RELEASE build | 37 keys (server hardware) |
| [1] | j475dap | Darwin Cloud Customer Erase Install (IPSW) | unknown (no path) | 0 keys (empty placeholder) |
| [2] | vphone600ap | Darwin Cloud Customer Erase Install (IPSW) | RELEASE build | 29 keys (includes UI assets) |
| [3] | vresearch101ap | Darwin Cloud Customer Erase Install (IPSW) | RELEASE build | 20 keys (no UI assets) |
| [4] | vphone600ap | Research Darwin Cloud Customer Erase Install (IPSW) | RESEARCH_RELEASE build | 29 keys (research kernel) |
| [5] | vresearch101ap | Research Darwin Cloud Customer Erase Install (IPSW) | RESEARCH_RELEASE build | 20 keys (research kernel) |
Key distinctions:
- CloudOS[2] vs [4] (vphone600ap): [2] uses RELEASE boot chain + release kernelcache; [4] uses RESEARCH_RELEASE + research kernelcache + txm.iphoneos.research.im4p
- CloudOS[3] vs [5] (vresearch101ap): Same pattern — [3] is RELEASE, [5] is RESEARCH_RELEASE
- **vphone600ap has components vresearch101ap lacks**: RecoveryMode, AppleLogo, Battery\*, RestoreLogo, SEP (vphone600 variant)
- vresearch101ap has only 20 manifest keys (no UI assets, no RecoveryMode)
### vphone600ap vs vresearch101ap Key Differences
| Property | vphone600ap | vresearch101ap |
| -------------- | ----------------------------------- | -------------------------------------- |
| Ap,ProductType | iPhone99,11 | ComputeModule14,2 |
| Ap,Target | VPHONE600AP | VRESEARCH101AP |
| ApBoardID | 0x91 | 0x90 |
| DeviceTree | DeviceTree.vphone600ap.im4p | DeviceTree.vresearch101ap.im4p |
| SEP | sep-firmware.vphone600.RELEASE.im4p | sep-firmware.vresearch101.RELEASE.im4p |
| RecoveryMode | recoverymode@2556~iphone-USBc.im4p | **NOT PRESENT** |
| MKB dt flag | dt=1 (keybag-less boot OK) | dt=0 (fatal keybag error) |
---
## 2. Component Source Tracing
### Boot Chain (from PCC vresearch101ap)
| Component | Source Identity | File | Patches Applied |
| ------------- | ----------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------ |
| **AVPBooter** | PCC vresearch1 | `AVPBooter*.bin` (vm dir) | DGST validation bypass (`mov x0, #0`) |
| **iBSS** | PROD (vresearch101ap release) | `Firmware/dfu/iBSS.vresearch101.RELEASE.im4p` | Serial labels + image4 callback bypass |
| **iBEC** | PROD (vresearch101ap release) | `Firmware/dfu/iBEC.vresearch101.RELEASE.im4p` | Serial labels + image4 callback + boot-args |
| **LLB** | PROD (vresearch101ap release) | `Firmware/all_flash/LLB.vresearch101.RELEASE.im4p` | Serial labels + image4 callback + boot-args + rootfs + panic (6 patches) |
| **iBoot** | RES (vresearch101ap research) | `Firmware/all_flash/iBoot.vresearch101.RESEARCH_RELEASE.im4p` | Not patched (only research identity carries iBoot) |
### Security Monitors (from PCC, shared across board configs)
| Component | Source Identity | File | Patches Applied |
| ------------------------------------- | --------------- | --------------------------------------- | ------------------------------------------- |
| **Ap,RestoreSecurePageTableMonitor** | PROD | `Firmware/sptm.vresearch1.release.im4p` | Not patched |
| **Ap,RestoreTrustedExecutionMonitor** | PROD | `Firmware/txm.iphoneos.release.im4p` | Not patched |
| **Ap,SecurePageTableMonitor** | PROD | `Firmware/sptm.vresearch1.release.im4p` | Not patched |
| **Ap,TrustedExecutionMonitor** | RES (research) | `Firmware/txm.iphoneos.research.im4p` | Trustcache bypass (`mov x0, #0` at 0x2C1F8) |
### Runtime Components (from PCC vphone600ap)
| Component | Source Identity | File | Patches Applied |
| ---------------------- | -------------------------- | -------------------------------------------------------- | -------------------------------------- |
| **DeviceTree** | VP (vphone600ap release) | `Firmware/all_flash/DeviceTree.vphone600ap.im4p` | Not patched |
| **RestoreDeviceTree** | VP | `Firmware/all_flash/DeviceTree.vphone600ap.im4p` | Not patched |
| **SEP** | VP | `Firmware/all_flash/sep-firmware.vphone600.RELEASE.im4p` | Not patched |
| **RestoreSEP** | VP | `Firmware/all_flash/sep-firmware.vphone600.RELEASE.im4p` | Not patched |
| **KernelCache** | VPR (vphone600ap research) | `kernelcache.research.vphone600` | 25 dynamic patches via KernelPatcher |
| **RestoreKernelCache** | VP (vphone600ap release) | `kernelcache.release.vphone600` | Not patched (used during restore only) |
| **RecoveryMode** | VP | `Firmware/all_flash/recoverymode@2556~iphone-USBc.im4p` | Not patched |
> **Important**: KernelCache (installed to disk, patched) uses the **research** variant.
> RestoreKernelCache (used during restore process only) uses the **release** variant.
> Only vphone600ap identities carry RecoveryMode — vresearch101ap does not.
### OS / Filesystem (from iPhone)
| Component | Source | Notes |
| ------------------------------------ | ---------------------------------- | ------------------ |
| **OS** | iPhone `iPhone17,3` erase identity | iPhone OS image |
| **SystemVolume** | iPhone erase | Root hash |
| **StaticTrustCache** | iPhone erase | Static trust cache |
| **Ap,SystemVolumeCanonicalMetadata** | iPhone erase | Metadata / mtree |
### Ramdisk (from PCC)
| Component | Source | Notes |
| --------------------- | ----------------------------- | --------------------- |
| **RestoreRamDisk** | PROD (vresearch101ap release) | CloudOS erase ramdisk |
| **RestoreTrustCache** | PROD | Ramdisk trust cache |
---
## 3. Why the Hybrid Approach
### Why Not All-vresearch101?
The vresearch101ap device tree sets MKB `dt=0`, causing a **fatal keybag error** during boot:
```
MKB_INIT: dt = 0, bootarg = 0
MKB_INIT: FATAL KEYBAG ERROR: failed to load system bag
REBOOTING INTO RECOVERY MODE.
```
Also missing the RecoveryMode entry.
### Why Not All-vphone600?
The DFU hardware identifies as BDID 0x90 (vresearch101ap). Using vphone600ap identity
(BDID 0x91) fails TSS/SHSH signing and idevicerestore identity matching
(`Unable to find a matching build identity`).
### Solution
vresearch101ap identity fields for DFU/TSS + vphone600 runtime components for a working
boot environment. The vphone600ap device tree sets `dt=1`, allowing boot without a
pre-existing system keybag:
```
MKB_INIT: dt = 1, bootarg = 0
MKB_INIT: No system keybag loaded.
```
The SEP firmware must match the device tree (vphone600 SEP with vphone600 DT).
---
## 4. Patched Components Summary
All 6 patched components in `fw_patch.py` come from **PCC (cloudOS)**:
| # | Component | Source Board | Patch Count | Purpose |
| --- | ----------- | ----------------- | ----------- | ---------------------------------------------------------- |
| 1 | AVPBooter | vresearch1 | 1 | Bypass DGST signature validation |
| 2 | iBSS | vresearch101 | 2 | Enable serial output + bypass image4 verification |
| 3 | iBEC | vresearch101 | 3 | Enable serial + bypass image4 + inject boot-args |
| 4 | LLB | vresearch101 | 6 | Serial + image4 + boot-args + rootfs mount + panic handler |
| 5 | TXM | shared (iphoneos) | 1 | Bypass trustcache validation |
| 6 | KernelCache | vphone600 | 25 | APFS seal, MAC policy, debugger, launch constraints, etc. |
All 4 CFW-patched binaries in `patchers/cfw.py` / `cfw_install.sh` come from **iPhone**:
| # | Binary | Source | Purpose |
| --- | -------------------- | ------------------------- | ----------------------------------------------------------- |
| 1 | seputil | iPhone (Cryptex SystemOS) | Gigalocker UUID patch (`/%s.gl``/AA.gl`) |
| 2 | launchd_cache_loader | iPhone (Cryptex SystemOS) | NOP cache validation check |
| 3 | mobileactivationd | iPhone (Cryptex SystemOS) | Force `should_hactivate` to return true |
| 4 | launchd.plist | iPhone (Cryptex SystemOS) | Inject bash/dropbear/trollvnc/vphoned/rpcserver_ios daemons |
---
## 5. idevicerestore Identity Selection
Source: `idevicerestore/src/idevicerestore.c` lines 2195-2242
### Matching Algorithm
idevicerestore selects a Build Identity by iterating through all `BuildIdentities` and returning the **first match** based on two fields:
1. **`Info.DeviceClass`** — case-insensitive match against device `hardware_model`
2. **`Info.Variant`** — substring match against the requested variant string
For DFU erase restore, the search variant is `"Erase Install (IPSW)"` (defined in `idevicerestore.h`).
### Matching Modes
```c
// Exact match
if (strcmp(str, variant) == 0) return ident;
// Partial match (when exact=0)
if (strstr(str, variant) && !strstr(str, "Research")) return ident;
```
**Critical**: Partial matching **excludes** variants containing `"Research"`. This means:
- `"Darwin Cloud Customer Erase Install (IPSW)"` — matches (contains "Erase Install (IPSW)", no "Research")
- `"Research Darwin Cloud Customer Erase Install (IPSW)"` — skipped (contains "Research")
### What idevicerestore Does NOT Check
- ApBoardID / ApChipID (used after selection, not for matching)
- Identity index or count (no hardcoded indices)
### Conclusion for Single Identity
A BuildManifest with **one identity** works fine. The loop iterates once, and if
DeviceClass and Variant match, it's returned. No minimum identity count required.
---
## 6. TSS/SHSH Signing
The TSS request sent to `gs.apple.com` includes:
- `ApBoardID = 144` (0x90) — must match vresearch101ap
- `ApChipID = 65025` (0xFE01)
- `Ap,ProductType = ComputeModule14,2`
- `Ap,Target = VRESEARCH101AP`
- Digests for all 21 manifest components
Apple's TSS server signs based on these identity fields + component digests.
Using vphone600ap identity (BDID 0x91) would fail because the DFU device
reports BDID 0x90.
---
## 7. Final Design: Single DFU Erase Identity
Since vphone-cli always boots via DFU restore, only one Build Identity is needed.
### Identity Metadata (fw_manifest.py)
```
DeviceClass = vresearch101ap (from C[PROD] deep copy)
Variant = Darwin Cloud Customer Erase Install (IPSW)
Ap,ProductType = ComputeModule14,2
Ap,Target = VRESEARCH101AP
Ap,TargetType = vresearch101
ApBoardID = 0x90
ApChipID = 0xFE01
ApSecurityDomain = 0x01
FDRSupport = False
```
### Source Variable Map
```
PROD = C[vresearch101ap release] — boot chain, SPTM, RestoreTXM, ramdisk, RestoreTrustCache
RES = C[vresearch101ap research] — iBoot, TXM research
VP = C[vphone600ap release] — DeviceTree, RestoreDeviceTree, SEP, RestoreSEP, RestoreKernelCache, RecoveryMode
VPR = C[vphone600ap research] — KernelCache (patched by fw_patch.py)
I_ERASE = I[iPhone erase] — OS, trust caches, system volume
```
### All 21 Manifest Entries
```
Boot chain (PROD): LLB, iBSS, iBEC
Research iBoot (RES): iBoot
Security monitors (PROD): Ap,RestoreSPTM, Ap,RestoreTXM, Ap,SPTM
Research TXM (RES): Ap,TXM
Device tree (VP): DeviceTree, RestoreDeviceTree
SEP (VP): SEP, RestoreSEP
Kernel (VPR/VP): KernelCache (research), RestoreKernelCache (release)
Recovery (VP): RecoveryMode
Ramdisk (PROD): RestoreRamDisk, RestoreTrustCache
iPhone OS (I_ERASE): OS, StaticTrustCache, SystemVolume, Ap,SVC Metadata
```
### Full Manifest Component List
```
LLB ← PROD
iBSS ← PROD
iBEC ← PROD
iBoot ← RES
Ap,RestoreSecurePageTableMonitor ← PROD
Ap,RestoreTrustedExecutionMonitor← PROD
Ap,SecurePageTableMonitor ← PROD
Ap,TrustedExecutionMonitor ← RES
DeviceTree ← VP
RestoreDeviceTree ← VP
SEP ← VP
RestoreSEP ← VP
KernelCache ← VPR (research, patched)
RestoreKernelCache ← VP (release, unpatched)
RecoveryMode ← VP
RestoreRamDisk ← PROD
RestoreTrustCache ← PROD
Ap,SystemVolumeCanonicalMetadata ← I_ERASE
OS ← I_ERASE
StaticTrustCache ← I_ERASE
SystemVolume ← I_ERASE
```
---
## 8. Restore.plist
```
DeviceMap: [d47ap (iPhone), vphone600ap, vresearch101ap]
ProductTypes: [iPhone17,3, ComputeModule14,1, ComputeModule14,2, Mac14,14, iPhone99,11]
```
---
## TL;DR
**Boot chain = vresearch101 (matches DFU hardware); runtime = vphone600 (keybag-less boot); OS = iPhone.**
The firmware is a PCC shell wrapping an iPhone core. The vresearch101 boot chain
handles DFU/TSS signing. The vphone600 device tree + SEP + kernel provide the
runtime environment. The iPhone userland is patched post-install for activation
bypass, jailbreak tools, and persistent SSH/VNC.

549
research/iboot_patches.md Normal file
View File

@@ -0,0 +1,549 @@
# iBoot Patch Analysis: iBSS / iBEC / LLB
Analysis of iBoot patches for vresearch101 from PCC-CloudOS 26.3 (23D128).
## Source Files
All six vresearch101 iBoot variants share just two unique **payload** binaries
(after IM4P decode/decompress):
| Variant | IM4P Size | Raw Size | Payload SHA256 (first 16) | Fourcc |
| ------------- | --------- | -------- | ------------------------- | ------ |
| iBSS RELEASE | 303068 | 605312 | `4c9e7df663af76fa` | ibss |
| iBEC RELEASE | 303098 | 605312 | `4c9e7df663af76fa` | ibec |
| LLB RELEASE | 303068 | 605312 | `4c9e7df663af76fa` | illb |
| iBSS RESEARCH | 308188 | 622512 | `8c3cc980f25f9027` | ibss |
| iBEC RESEARCH | 308218 | 622512 | `8c3cc980f25f9027` | ibec |
| LLB RESEARCH | 308188 | 622512 | `8c3cc980f25f9027` | illb |
**Key finding:** This "identical" claim is strictly about the decoded payload bytes.
At the IM4P container level, iBSS/iBEC/LLB are still different files (different
fourcc and full-file hashes). Within each build variant (RELEASE or RESEARCH),
the decoded payload bytes are identical.
Mode/stage identity is therefore not encoded as different payload binaries in
these pristine IPSW extracts; it comes from how the boot chain loads and treats
each image.
`fw_patch.py` targets the RELEASE variants, matching the BuildManifest identity
(`PCC RELEASE` for LLB/iBSS/iBEC). The dynamic patcher works on both variants.
> Note: if you compare files under `vm/...` **after** running patch scripts,
> RELEASE payloads will no longer be identical (expected), because mode-specific
> patches are applied to iBEC/LLB.
## Binary Layout
Single flat ROM segment (no Mach-O, no sections):
| Property | RELEASE | RESEARCH |
| ----------- | ----------------- | ----------------- |
| Base VA | `0x7006C000` | `0x7006C000` |
| Size | 605312 (591.1 KB) | 622512 (607.9 KB) |
| Compression | BVX2 (LZFSE) | BVX2 (LZFSE) |
| Encrypted | No | No |
File offset = VA `0x7006C000`.
## Patch Summary
### Base Patches (`fw_patch.py` via `IBootPatcher`)
| # | Patch | iBSS | iBEC | LLB | Total |
| --- | ---------------------- | :---: | :---: | :----: | :---: |
| 1 | Serial labels (×2) | ✅ | ✅ | ✅ | 2 |
| 2 | image4 callback bypass | ✅ | ✅ | ✅ | 2 |
| 3 | Boot-args redirect | — | ✅ | ✅ | 3 |
| 4 | Rootfs bypass | — | — | ✅ | 5 |
| 5 | Panic bypass | — | — | ✅ | 1 |
| | **Subtotal** | **4** | **7** | **13** | |
### JB Extension Patch (implemented)
| # | Patch | Base | JB |
| --- | ----------------------------------- | :--: | :-: |
| 6 | **Skip generate_nonce** (iBSS only) | — | ✅ |
Status: implemented in `IBootJBPatcher.patch_skip_generate_nonce()` and applied
by `fw_patch_jb.py` (JB flow). This follows the current pipeline split where
base boot patching stays minimal and nonce control is handled in JB/research flow.
## Patch Details (RELEASE variant, 26.3)
### Patch 1: Serial Labels
**Purpose:** Replace two `===...===` banner strings with descriptive labels for
serial log identification.
**Anchoring:** Find runs of ≥20 `=` characters in the binary. There are exactly
4 such runs, but only the first 2 are the banners (the other 2 are
`"Start of %s serial output"` / `"End of %s serial output"` format strings).
| Patch | File Offset | VA | Original | Patched |
| ------- | ----------- | ------------ | ---------- | ------------- |
| Label 1 | `0x084549` | `0x700F0549` | `=====...` | `Loaded iBSS` |
| Label 2 | `0x0845F4` | `0x700F05F4` | `=====...` | `Loaded iBSS` |
Label text changes per mode: `Loaded iBSS` / `Loaded iBEC` / `Loaded LLB`.
**Containing function:** `sub_7006F71C` (main boot function, ~0x9B4 bytes).
### Patch 2: image4_validate_property_callback
**Purpose:** Force the image4 property validation callback to always return 0
(success), bypassing signature/property verification for all image4 objects.
**Function:** `sub_70075350` (~0xA98 bytes) — the image4 property callback handler.
Dispatches on 4-char property tags (BORD, CHIP, CEPO, CSEC, DICE, BNCH, etc.)
and validates each against expected values.
**Anchoring pattern:**
1. `B.NE` followed immediately by `MOV X0, X22`
2. `CMP` within 8 instructions before the `B.NE`
3. `MOVN W22, #0` or `MOV W22, #-1` (setting error return = -1) within 64 instructions before
The `B.NE` is the stack canary check at the function epilogue. `X22` holds the
computed return value (0 = success, -1 = failure). The patch forces return 0
regardless of validation results.
| Patch | File Offset | VA | Original | Patched |
| ----------- | ----------- | ------------ | ----------------- | ------------ |
| NOP b.ne | `0x009D14` | `0x70075D14` | `B.NE 0x70075E50` | `NOP` |
| Force ret=0 | `0x009D18` | `0x70075D18` | `MOV X0, X22` | `MOV X0, #0` |
**Context (function epilogue):**
```
70075CFC MOV W22, #0xFFFFFFFF ; error return code
70075D00 LDUR X8, [X29, #var_60] ; load stack canary
70075D04 ADRL X9, "160D" ; expected canary
70075D0C LDR X9, [X9]
70075D10 CMP X9, X8 ; canary check
70075D14 B.NE loc_70075E50 ; → panic if mismatch ← NOP
70075D18 MOV X0, X22 ; return x22 ← MOV X0, #0
70075D1C LDP X29, X30, [SP, ...] ; epilogue
...
70075D38 RETAB
```
### Patch 3: Boot-args (iBEC / LLB only)
**Purpose:** Replace the default boot-args format string `"%s"` with
`"serial=3 -v debug=0x2014e %s"` to enable serial output, verbose boot,
and debug flags.
**Anchoring:**
1. Find `"rd=md0"` string → search nearby for standalone `"%s"` (NUL-terminated)
2. Find `ADRP+ADD X2` pair referencing that `"%s"` offset
3. Write new string to a NUL-padded area, redirect ADRP+ADD to it
| Patch | File Offset | VA | Description |
| ------- | ----------- | ------------ | ---------------------- |
| String | `0x023F40` | `0x700D5F40` | New boot-args string |
| ADRP x2 | `0x0122E0` | `0x700DE2E0` | Redirect to new page |
| ADD x2 | `0x0122E4` | `0x700DE2E4` | Redirect to new offset |
### Patch 4: Rootfs Bypass (LLB only)
**Purpose:** 5 patches that bypass root filesystem signature verification,
allowing modified rootfs to boot.
| # | File Offset | VA | Original | Patched | Anchor |
| --- | ----------- | ------------ | ------------- | ------- | --------------------- |
| 4a | `0x02B068` | `0x700D7068` | `CBZ W0, ...` | `B ...` | error code `0x3B7` |
| 4b | `0x02AD20` | `0x700D6D20` | `B.HS ...` | `NOP` | `CMP X8, #0x400` |
| 4c | `0x02B0BC` | `0x700D70BC` | `CBZ W0, ...` | `B ...` | error code `0x3C2` |
| 4d | `0x02ED6C` | `0x700DAD6C` | `CBZ X8, ...` | `NOP` | `LDR X8, [xN, #0x78]` |
| 4e | `0x02EF68` | `0x700DAF68` | `CBZ W0, ...` | `B ...` | error code `0x110` |
**Anchoring techniques:**
- **4a, 4c, 4e:** Find unique `MOV W8, #<error>` instruction, the `CBZ` is 4 bytes
before. Convert conditional branch to unconditional `B` (same target).
- **4b:** Find unique `CMP X8, #0x400`, NOP the `B.HS` that follows.
- **4d:** Scan backwards from error `0x110` for `LDR X8, [xN, #0x78]` + `CBZ X8`,
NOP the `CBZ`.
### Patch 5: Panic Bypass (LLB only)
**Purpose:** Prevent panic when a specific boot check fails.
**Anchoring:** Find `MOV W8, #0x328` followed by `MOVK W8, #0x40, LSL #16`
(forming constant `0x400328`), walk forward to `BL; CBNZ W0`, NOP the `CBNZ`.
| Patch | File Offset | VA | Original | Patched |
| -------- | ----------- | ------------ | -------------- | ------- |
| NOP cbnz | `0x01A038` | `0x70086038` | `CBNZ W0, ...` | `NOP` |
### Patch 6: Skip generate_nonce (iBSS only, JB flow)
**Purpose:** Skip nonce generation to preserve the existing AP nonce. Required for
deterministic DFU restore — without this, iBSS generates a random nonce on each
boot, which can interfere with the restore process.
**Function:** `sub_70077064` (~0x1C00 bytes) — iBSS platform initialization.
**Anchoring:** Find `"boot-nonce"` string reference via ADRP+ADD, then scan forward
for: `TBZ/TBNZ W0, #0` + `MOV W0, #0` + `BL` pattern. Convert `TBZ/TBNZ` to unconditional `B`.
| Patch | File Offset | VA | Original | Patched |
| ---------- | ----------- | ------------ | ------------------------ | -------------- |
| Skip nonce | `0x00B7B8` | `0x700777B8` | `TBZ W0, #0, 0x700777F0` | `B 0x700777F0` |
**Disassembly context:**
```
70077750 ADD X8, X8, #("boot-nonce" - ...) ; 1st ref: read nonce env var
70077754 BL sub_70079590 ; env_get
...
7007778C ADRL X8, "boot-nonce" ; 2nd ref: nonce generation block
70077798 ADD X8, X8, #("dram-vendor" - ...)
7007779C BL sub_70079570 ; env_set
700777A0 BL sub_700797B4
...
700777B4 BL sub_7009F620 ; check if nonce needed
700777B8 TBZ W0, #0, loc_700777F0 ; skip if bit0=0 ← patch to B
700777BC MOV W0, #0
700777C0 BL sub_70087414 ; generate_nonce(0)
700777C4 STR X0, [SP, ...] ; store nonce
...
700777F0 ADRL X8, "dram-vendor" ; continue init
```
The `generate_nonce` function (`sub_70087414`) calls a random number generator
(`sub_70083FA4`) to create a new 64-bit nonce and stores it in the platform state.
The patch makes the `TBZ` unconditional so the nonce generation block is always
skipped, preserving whatever nonce was already set (or leaving it empty).
**Current placement (rewrite/JB path):**
This patch is intentionally kept in the JB extension path (`fw_patch_jb.py` +
`IBootJBPatcher`) so the base flow remains unchanged. Use JB flow when you need
deterministic nonce behavior for restore/research scenarios.
## RELEASE vs RESEARCH_RELEASE Variants
Both variants work with all dynamic patches. Offsets differ but the patcher
finds them by pattern matching:
| Patch | RELEASE offset | RESEARCH offset |
| -------------------------------- | -------------- | --------------- |
| Serial label 1 | `0x084549` | `0x0861C9` |
| Serial label 2 | `0x0845F4` | `0x086274` |
| image4 callback (nop) | `0x009D14` | `0x00A0DC` |
| image4 callback (mov) | `0x009D18` | `0x00A0E0` |
| Skip generate_nonce _(JB patch)_ | `0x00B7B8` | `0x00BC08` |
`fw_patch.py` targets RELEASE, matching the BuildManifest identity
(PCC RELEASE for LLB/iBSS/iBEC). The reference script used RESEARCH_RELEASE.
Both work — the dynamic patcher is variant-agnostic.
## Cross-Version Comparison (26.1 → 26.3)
Reference hardcoded offsets (26.1 RESEARCH_RELEASE) vs dynamic patcher results
(26.3 RELEASE):
| Patch | 26.1 (hardcoded) | 26.3 RELEASE (dynamic) | 26.3 RESEARCH (dynamic) |
| -------------- | ---------------- | ---------------------- | ----------------------- |
| Serial label 1 | `0x84349` | `0x84549` | `0x861C9` |
| Serial label 2 | `0x843F4` | `0x845F4` | `0x86274` |
| image4 nop | `0x09D10` | `0x09D14` | `0x0A0DC` |
| image4 mov | `0x09D14` | `0x09D18` | `0x0A0E0` |
| generate_nonce | `0x1B544` | `0x0B7B8` | `0x0BC08` |
Offsets shift significantly between versions and variants, confirming that
hardcoded offsets would break. The dynamic patcher handles all combinations.
## Appendix: IDA Pseudocode / Disassembly
### A. Serial Label Banners (`ibss_main` @ `0x7006F71C`)
```
ibss_main (ROM @ 0x7006fa98):
; --- banner 1 ---
7006faa8 ADRL X0, "\n\n=======================================\n" ; 0x700F0546
7006fab0 BL serial_printf
7006fab4 ADRL X20, "::\n"
7006fabc MOV X0, X20
7006fac0 BL serial_printf
...
; :: <build info lines> ::
...
7006fc30 BL serial_printf
7006fc34 MOV X0, X20
7006fc38 BL serial_printf
; --- banner 2 ---
7006fc3c ADRL X0, "=======================================\n\n" ; 0x700F05F3
7006fc44 BL serial_printf
7006fc48 BL sub_700C8674
```
Patcher writes `"Loaded iBSS"` at banner+1 (offset into the `===...===` run).
### B. image4_validate_property_callback (`0x70075350`)
**Pseudocode:**
```c
// image4_validate_property_callback — dispatches on image4 property tags.
// Returns 0 on success, -1 on failure.
// X22 accumulates the return code throughout the function.
//
// Property tags handled (FourCC → hex):
// BORD=0x424F5244 CHIP=0x43484950 CEPO=0x4345504F CSEC=0x43534543
// DICE=0x45434944 EPRO=0x4550524F ESEC=0x45534543 EKEY=0x454B4559
// DPRO=0x4450524F SDOM=0x53444F4D CPRO=0x4350524F BNCH=0x424E4348
// pndp=0x706E6470 osev=0x6F736576 nrde=0x6E726465 slvn=0x736C766E
// dpoc=0x64706F63 anrd=0x616E7264 exrm=0x6578726D hclo=0x68636C6F
// AMNM=0x414D4E4D
//
int64_t image4_validate_property_callback(tag, a2, capture_mode, a4, ...) {
if (MEMORY[0x701004D8] != 1)
goto dispatch;
// Handle ASN1 types 1, 2, 4 via registered callbacks
switch (*(_QWORD *)(a2 + 16)) {
case 1: if (callback_bool) callback_bool(tag, capture_mode == 1, value); break;
case 2: if (callback_int) callback_int(tag, capture_mode == 1, value); break;
case 4: if (callback_data) callback_data(tag, capture_mode == 1, ptr, ptr, end, ...); break;
default: log_printf(0, "Unknown ASN1 type %llu\n"); return -1;
}
dispatch:
// Main tag dispatch (capture_mode: 0=verify, 1=capture)
if (capture_mode == 1) {
switch (tag) {
case 'BORD': ... // board ID
case 'CHIP': ... // chip ID
...
}
} else if (capture_mode == 0) {
switch (tag) {
case 'BNCH': ... // boot nonce hash
case 'CEPO': ... // certificate epoch
...
}
}
// ... (21 property handlers)
return x22; // 0=success, -1=failure
}
```
**Epilogue disassembly (patch site):**
```
; At this point X22 = return value (0 or -1)
70075CFC MOV W22, #0xFFFFFFFF ; set error return = -1
70075D00 LDUR X8, [X29, #var_60] ; load saved stack cookie
70075D04 ADRL X9, "160D" ; expected cookie value
70075D0C LDR X9, [X9]
70075D10 CMP X9, X8 ; stack canary check
70075D14 B.NE loc_70075E50 ; → stack_chk_fail ◄── PATCH 2a: NOP
70075D18 MOV X0, X22 ; return x22 ◄── PATCH 2b: MOV X0, #0
70075D1C LDP X29, X30, [SP, ...] ; restore callee-saved
70075D20 LDP X20, X19, [SP, ...]
70075D24 LDP X22, X21, [SP, ...]
70075D28 LDP X24, X23, [SP, ...]
70075D2C LDP X26, X25, [SP, ...]
70075D30 LDP X28, X27, [SP, ...]
70075D34 ADD SP, SP, #0x110
70075D38 RETAB
```
Effect: function always returns 0 (success) regardless of property validation.
### C. generate_nonce (`0x70087414`)
**Pseudocode:**
```c
// generate_nonce — creates a random 64-bit AP nonce.
// Called from platform_init when boot-nonce environment needs a new nonce.
//
uint64_t generate_nonce() {
platform_state *ps = get_platform_state();
if (ps->flags & 2) // nonce already generated?
goto return_existing;
uint64_t nonce = random64(0); // generate random 64-bit value
ps->nonce = nonce; // store at offset +40
ps->flags |= 2; // mark nonce as valid
if (ps->nonce_lo == 0) { // sanity check
get_platform_state2();
log_assert(1630); // "nonce is zero" assertion
return_existing:
nonce = ps->nonce;
}
return nonce;
}
```
### D. Skip generate_nonce — `platform_init` (`0x70077064`)
**Disassembly (boot-nonce handling region):**
```
; --- Phase 1: read existing boot-nonce from env ---
70077744 ADRL X8, "effective-security-mode-ap"
7007774C STP X8, X8, [SP, #var_238]
70077750 ADD X8, X8, #("boot-nonce" - ...) ; 1st ref to "boot-nonce"
70077754 BL env_get ; read boot-nonce env var
70077758 STP X24, X24, [SP, #var_2F0]
7007775C ADRL X7, ...
70077764 BL sub_7007968C
70077768 ADD X6, X19, #0x20
7007776C BL env_check_property ; check if boot-nonce exists
70077770 TBZ W0, #0, loc_7007778C ; if no existing nonce, skip
70077774 BL sub_700BF1D8 ; get security mode
70077778 MOV X23, X0
7007777C BL sub_700795D8
70077780 CCMP X0, X2, #2, CS
70077784 B.CS loc_70078C44 ; error path
70077788 BL sub_700798D0
; --- Phase 2: generate new nonce (PATCHED OUT) ---
7007778C ADRL X8, "boot-nonce" ; 2nd ref to "boot-nonce"
70077794 STP X8, X8, [SP, #var_238]
70077798 ADD X8, X8, #("dram-vendor" - ...)
7007779C BL env_set ; set boot-nonce env key
700777A0 BL env_clear
700777A4 ADRL X7, ...
700777AC BL sub_7007968C
700777B0 ADD X6, X19, #0x20
700777B4 BL env_check_property ; check if nonce generation needed
700777B8 TBZ W0, #0, loc_700777F0 ; ◄── PATCH 6: change to B (always skip)
700777BC MOV W0, #0
700777C0 BL generate_nonce ; generate_nonce(0) — SKIPPED
700777C4 STR X0, [SP, #var_190] ; store nonce result
700777C8 BL sub_70079680
700777CC ADD X8, SP, #var_190
700777D0 LDR W9, [SP, #var_214]
700777D4 STR X9, [SP, #var_2F0]
700777D8 ADRL X7, ...
700777E0 ADD X4, SP, #var_190
700777E4 ADD X5, SP, #var_190
700777E8 ADD X6, X8, #8
700777EC BL sub_700A8F24 ; commit nonce to env
; --- Phase 3: continue with dram-vendor init ---
700777F0 ADRL X8, "dram-vendor" ; ◄── branch target (skip lands here)
700777F8 STP X8, X8, [SP, #var_238]
700777FC ADD X8, X8, #("dram-vendor-id" - ...)
70077800 BL env_get
```
**Patch effect:** `TBZ W0, #0, 0x700777F0``B 0x700777F0`
Unconditionally skips the `generate_nonce(0)` call and all nonce storage logic,
jumping directly to the "dram-vendor" init. Preserves any existing AP nonce from
a previous boot or NVRAM.
## Appendix E: Nonce Skip — IDA Pseudocode Before/After
### generate_nonce (`sub_70087414`)
```c
unsigned __int64 generate_nonce()
{
platform_state *ps = get_platform_state();
if ( (ps->flags & 2) != 0 ) // nonce already generated?
goto return_existing;
uint64_t nonce = random64(0); // generate random 64-bit value
*(uint64_t *)(ps + 40) = nonce; // store nonce
*(uint32_t *)ps |= 2u; // mark nonce as valid
if ( !*(uint32_t *)(ps + 40) ) // sanity: nonce_lo == 0?
{
v4 = get_platform_state2();
log_assert(v4, 1630); // "nonce is zero" assertion
return_existing:
nonce = *(uint64_t *)(ps + 40); // return existing nonce
}
return nonce;
}
```
### platform_init — boot-nonce region: BEFORE patch
```c
// --- Phase 1: read existing boot-nonce from env ---
env_get(..., /*0x70077754*/
"effective-security-mode-ap",
"effective-security-mode-ap", ...);
env_check_property(...); /*0x7007776c*/
if ( (v271 & 1) != 0 ) /*0x70077770*/
{
// existing nonce found — security mode check
v97 = get_security_mode(); /*0x70077778*/
v279 = validate_security(v97); /*0x7007777c*/
if ( !v42 || v279 >= v280 ) /*0x70077780*/
goto LABEL_311; // error path
}
// --- Phase 2: set boot-nonce env, check if generation needed ---
env_set(..., /*0x7007779c*/
"boot-nonce",
"boot-nonce", ...);
env_clear(); /*0x700777a0*/
v290 = env_check_property(...); /*0x700777b4*/
if ( (v290 & 1) != 0 ) /*0x700777b8 ← TBZ W0, #0*/
{
nonce = generate_nonce(); /*0x700777c4 ← BL generate_nonce*/
sub_70079680(nonce); /*0x700777c8*/
sub_700A8F24(...); /*0x700777ec — commit nonce to env*/
}
// --- Phase 3: continue with dram-vendor init ---
env_get(..., /*0x70077800*/
"dram-vendor",
"dram-vendor", ...);
```
### platform_init — boot-nonce region: AFTER patch
```c
// --- Phase 2: set boot-nonce env ---
env_set(..., /*0x7007779c*/
"boot-nonce",
"boot-nonce", ...);
env_clear(); /*0x700777a0*/
v290 = env_check_property(...); /*0x700777b4*/
// generate_nonce() block ELIMINATED by decompiler
// (unconditional B at 0x700777B8 makes it dead code)
// --- Phase 3: continue with dram-vendor init ---
v298 = env_get(..., /*0x70077800*/
"dram-vendor",
"dram-vendor", ...);
```
**Patch effect in decompiler:** The entire `if` block containing `generate_nonce()`
is removed. The decompiler recognizes the unconditional `B` creates dead code and
eliminates it entirely — execution flows straight from `env_check_property()` to
the `"dram-vendor"` env_get.
### Byte Comparison
Reference: `patch(0x1b544, 0x1400000e)` (26.1 RESEARCH, hardcoded)
| | Reference (26.1) | Dynamic (26.3 RELEASE) | Dynamic (26.3 RESEARCH) |
| ------------ | ------------------- | ---------------------- | ----------------------- |
| **Offset** | `0x1B544` | `0x0B7B8` | `0x0BC08` |
| **Original** | `TBZ W0, #0, +0x38` | `TBZ W0, #0, +0x38` | `TBZ W0, #0, +0x38` |
| **Patched** | `B +0x38` | `B +0x38` | `B +0x38` |
| **Bytes** | `0E 00 00 14` | `0E 00 00 14` | `0E 00 00 14` |
All three produce byte-identical `0x1400000E` — same branch delta `+0x38` (14 words)
across all variants. Only the file offset differs between versions.
## Status
`patch_skip_generate_nonce()` is active in the JB path via
`IBootJBPatcher` and `fw_patch_jb.py` (iBSS JB component enabled).

View File

@@ -0,0 +1,75 @@
# FairPlay IOKit Extensions in PCC Kernel
**Kernel:** `kernelcache.research.vphone600` (PCC/cloudOS, vphone600ap research)
**Total kexts in kernel:** 161
**FairPlay kexts found:** 2
---
## 1. com.apple.driver.AvpFairPlayDriver
| Field | Value |
| ----------------- | --------------------------------------------------- |
| Path | `/System/Library/Extensions/AvpFairPlayDriver.kext` |
| Version | 2.9.0 |
| Executable Size | 2,920 bytes |
| IOClass | `AvpFairPlayDriver` |
| IOProviderClass | `AppleVirtIOTransport` |
| IOUserClientClass | `AvpFairPlayUserClient` |
| PCI Match | `0x1a08106b` |
| Dependencies | AppleVirtIO, IOKit, libkern |
**Notes:**
- Virtualization-specific FairPlay driver — matches on a VirtIO PCI device ID (`0x1a08106b`).
- Tiny kext (2.9 KB), acts as a paravirtual bridge to the host-side FairPlay backend.
- Implies the host Virtualization.framework exposes a FairPlay VirtIO device to the guest.
- Has two IOKit personalities:
- `AvpFairPlayDriver` — matches `AppleVirtIOTransport` with `IOVirtIOPrimaryMatch: 0x1a08106b`
- `AvpFairPlayDriver Transport` — matches `IOPCIDevice` with `IOPCIPrimaryMatch: 0x1a08106b` (published by `com.apple.driver.AppleVirtIO`)
---
## 2. com.apple.driver.FairPlayIOKit
| Field | Value |
| ----------------- | --------------------------------------------------------- |
| Path | `/System/Library/Extensions/FairPlayIOKit.kext` |
| Version | 72.15.0 |
| Executable Size | 269,440 bytes |
| IOClass | `com_apple_driver_FairPlayIOKit` |
| IOProviderClass | `IOResources` (always-match) |
| IOUserClientClass | `com_apple_driver_FairPlayIOKitUserClient` |
| IOMatchCategory | `FairPlayIOKit` |
| IOProbeScore | 1000 |
| Dependencies | bsd, **dsep**, iokit, libkern, mach, private, unsupported |
**Notes:**
- Full FairPlay DRM framework kext (269 KB of code).
- Matches on `IOResources` — loads unconditionally at boot.
- Provides FairPlay services to userland via `com_apple_driver_FairPlayIOKitUserClient`.
- Depends on `com.apple.kpi.dsep` (data-at-rest encryption / DRM subsystem).
- Copyright 20082019 — long-lived Apple DRM component.
---
## String Occurrences
| Pattern | Count |
| ------------------------------------ | ----- |
| `FairPlay` (case-sensitive) | 139 |
| `fairplay` (lowercase) | 27 |
| `com.apple.driver.FairPlayIOKit` | 6 |
| `com.apple.driver.AvpFairPlayDriver` | 3 |
Lowercase `fairplay` strings include launch constraint labels: `com.apple.fairplayd`, `com.apple.fairplayd.A2`, `com.apple.fairplayd.A2.dev`, `com.apple.fairplayd.G1` — these are userland daemon identifiers referenced in kernel launch constraint plists.
---
## Implications
- Both kexts are present in the non-JB PCC boot kernel and will load at boot.
- `AvpFairPlayDriver` is the VM-aware component — it bridges FairPlay operations to the host via VirtIO. This is unique to the virtualized (PV=3) environment.
- `FairPlayIOKit` is the standard iOS FairPlay kext, providing DRM primitives to userland processes (e.g., `fairplayd`, media frameworks).
- For research purposes, these kexts may need to be patched or neutralized if FairPlay enforcement interferes with instrumentation or custom binaries.

View File

@@ -0,0 +1,461 @@
# Kernel JB Remaining Patches — Research Notes
Last updated: 2026-03-04
## Overview
`scripts/patchers/kernel_jb.py` has 24 patch methods in `find_all()`. Current status:
- **24 PASSING**: All patches implemented and functional
- **0 FAILING**
Two methods added since initial document: `patch_shared_region_map`, `patch_io_secure_bsd_root`.
Three previously failing patches (`patch_nvram_verify_permission`, `patch_thid_should_crash`, `patch_hook_cred_label_update_execve`) have been implemented — see details below.
Upstream reference: `/Users/qaq/Documents/GitHub/super-tart-vphone/CFW/patch_fw.py`
Test kernel: `vm/iPhone17,3_26.1_23B85_Restore/kernelcache.release.vphone600` (IM4P-wrapped, bvx2 compressed)
Key facts about the kernel:
- **0 symbols resolved** (fully stripped)
- `base_va = 0xFFFFFE0007004000` (typical PCC)
- `kern_text = 0xA74000 - 0x24B0000`
- All offsets in `kernel.py` helpers are **file offsets** (not VA)
- `bl_callers` dict: keyed by file offset → list of caller file offsets
---
## Patch 1: `patch_nvram_verify_permission` — FAILING
### Upstream Reference
```python
# patch __ZL16verifyPermission16IONVRAMOperationPKhPKcb
patch(0x1234034, 0xd503201f) # NOP
```
One single NOP at file offset `0x1234034`. The BL being NOPed calls memmove (3114 callers).
### Function Analysis
**Function start**: `0x1233E40` (PACIBSP)
**Function end**: `0x1234094` (next PACIBSP)
**Size**: `0x254` bytes
**BL callers**: 0 (IOKit virtual method, dispatched via vtable)
**Instruction**: `retab` at end
#### Full BL targets in the function:
| Offset | Delta | Target | Callers | Likely Identity |
| --------- | ------ | --------- | ------- | -------------------------- |
| 0x1233F0C | +0x0CC | 0x0AD10DC | 6190 | lck_rw_done / lock_release |
| 0x1234034 | +0x1F4 | 0x12CB0D0 | 3114 | **memmove** ← PATCH THIS |
| 0x1234048 | +0x208 | 0x0ACB418 | 423 | OSObject::release |
| 0x1234070 | +0x230 | 0x0AD029C | 4921 | lck_rw_lock_exclusive |
| 0x123407C | +0x23C | 0x0AD10DC | 6190 | lck_rw_done |
| 0x123408C | +0x24C | 0x0AD10DC | 6190 | lck_rw_done |
#### Key instructions in the function:
- `CASA` at +0x54 (offset 0x1233E94) — atomic compare-and-swap for lock acquisition
- `CASL` at 3 locations — lock release
- 4x `BLRAA` — authenticated indirect calls through vtable pointers
- `movk x17, #0xcda1, lsl #48` — PAC discriminator for IONVRAMController class
- `RETAB` — PAC return
- `mov x8, #-1; str x8, [x19]` — cleanup pattern near end
- `ubfiz x2, x8, #3, #0x20` before BL memmove — size = count \* 8
#### "Remove from array" pattern (at patch site):
```
0x1233FD8: adrp x8, #0x272f000
0x1233FDC: ldr x8, [x8, #0x10] ; load observer list struct
0x1233FE0: cbz x8, skip ; if null, skip
0x1233FE4: ldr w11, [x8, #0x10] ; load count
0x1233FE8: cbz w11, skip ; if 0, skip
0x1233FEC: mov x10, #0 ; index = 0
0x1233FF0: ldr x9, [x8, #0x18] ; load array base
loop:
0x1233FF4: add x12, x9, x10, lsl #3
0x1233FF8: ldr x12, [x12] ; array[index]
0x1233FFC: cmp x12, x19 ; compare with self
0x1234000: b.eq found
0x1234004: add x10, x10, #1 ; index++
0x1234008: cmp x11, x10
0x123400C: b.ne loop
found:
0x1234014: sub w11, w11, #1 ; count--
0x1234018: str w11, [x8, #0x10] ; store
0x123401C: subs w8, w11, w10 ; remaining
0x1234020: b.ls skip
0x1234024: ubfiz x2, x8, #3, #0x20 ; size = remaining * 8
0x1234028: add x0, x9, w10, uxtw #3
0x123402C: add w8, w10, #1
0x1234030: add x1, x9, w8, uxtw #3
0x1234034: bl memmove ; ← NOP THIS
```
### What I've Tried (and Failed)
1. **"krn." string anchor** → Leads to function at `0x11F7EE8`, NOT `0x1233E40`. Wrong function entirely.
2. **"nvram-write-access" entitlement string** → Also leads to a different function.
3. **CASA + 0 callers + retab + ubfiz + memmove filter****332 matches**. All IOKit virtual methods follow the same "remove observer from array" pattern with CASA locking.
4. **IONVRAMController metaclass string** → Found at `0xA2FEB`. Has ADRP+ADD refs at `0x125D2C0`, `0x125D310`, `0x125D38C` (metaclass constructors). These set up the metaclass, NOT instance methods.
5. **Chained fixup pointer search for IONVRAMController string** → Failed (different encoding).
### Findings That DO Work
**IONVRAMController vtable found via chained fixup search:**
The verifyPermission function at `0x1233E40` is referenced as a chained fixup pointer in `__DATA_CONST`:
```
__DATA_CONST @ 0x7410B8: raw=0x8011377101233E40 → decoded=0x1233E40 (verifyPermission)
```
**Vtable layout at 0x7410B8:**
| Vtable Idx | File Offset | Content | First Insn |
| ------------- | ----------- | -------------------- | ---------- |
| [-3] 0x7410A0 | | NULL | |
| [-2] 0x7410A8 | | NULL | |
| [-1] 0x7410B0 | | NULL | |
| [0] 0x7410B8 | 0x1233E40 | **verifyPermission** | pacibsp |
| [1] 0x7410C0 | 0x1233BF0 | sister method | pacibsp |
| [2] 0x7410C8 | 0x10EA4E0 | | ret |
| [3] 0x7410D0 | 0x10EA4D8 | | mov |
**IONVRAMController metaclass constructor pattern:**
```
0x125D2C0: pacibsp
adrp x0, #0x26fe000
add x0, x0, #0xa38 ; x0 = metaclass obj @ 0x26FEA38
adrp x1, #0xa2000
add x1, x1, #0xfeb ; x1 = "IONVRAMController" @ 0xA2FEB
adrp x2, #0x26fe000
add x2, x2, #0xbf0 ; x2 = superclass metaclass @ 0x26FEBF0
mov w3, #0x88 ; w3 = instance size = 136
bl OSMetaClass::OSMetaClass() ; [5236 callers]
adrp x16, #0x76d000
add x16, x16, #0xd60
add x16, x16, #0x10 ; x16 = metaclass vtable @ 0x76DD70
movk x17, #0xcda1, lsl #48 ; PAC discriminator
pacda x16, x17
str x16, [x0] ; store PAC'd metaclass vtable
retab
```
**There's ALSO a combined class registration function at 0x12376D8** that registers multiple classes and references the instance vtable:
```
0x12377F8: adrp x16, #0x741000
add x16, x16, #0x0a8 ; → 0x7410A8 (vtable[-2])
```
Wait — it actually points to `0x7410A8`, not `0x7410B8`. The vtable pointer with the +0x10 adjustment gives `0x7410A8 + 0x10 = 0x7410B8` which is entry [0]. This is how IOKit vtables work: the isa pointer stores `vtable_base + 0x10` to skip the RTTI header.
### Proposed Dynamic Strategy
**Chain**: "IONVRAMController" string → ADRP+ADD refs → metaclass constructor → extract instance size `0x88` → find the combined class registration function (0x12376D8) that calls OSMetaClass::OSMetaClass() with `mov w3, #0x88` AND uses "IONVRAMController" name → extract the vtable base from the ADRP+ADD+ADD that follows → vtable[0] = verifyPermission → find BL to memmove-like target (>2000 callers) and NOP it.
**Alternative (simpler)**: From the metaclass constructor, extract the PAC discriminator `#0xcda1` and the instance size `#0x88`. Then search \_\_DATA_CONST for chained fixup pointer entries where:
- The preceding 3 entries (at -8, -16, -24) are NULL (vtable header)
- The decoded function pointer has 0 BL callers
- The function contains CASA
- The function ends with RETAB
- The function contains a BL to memmove (>2000 callers)
- **The function contains `movk x17, #0xcda1`** (the IONVRAMController PAC discriminator)
This last filter is the KEY discriminator. Among the 332 candidate functions, only IONVRAMController methods use PAC disc `0xcda1`. Combined with "first entry in vtable" (preceded by 3 nulls), this should be unique.
**Simplest approach**: Search all chained fixup pointers in \_\_DATA_CONST where:
1. Preceded by 3 null entries (vtable start)
2. Decoded target is a function in kern_text
3. Function contains `movk x17, #0xcda1, lsl #48`
4. Function contains BL to target with >2000 callers (memmove)
5. NOP that BL
---
## Patch 2: `patch_thid_should_crash` — FAILING
### Upstream Reference
```python
# patch _thid_should_crash to 0
patch(0x67EB50, 0x0)
```
Writes 4 bytes of zero at file offset `0x67EB50`.
### Analysis
- Offset `0x67EB50` is in a **DATA segment** (not code)
- The current value at this offset is **already 0x00000000** in the test kernel
- This is a sysctl boolean variable (`kern.thid_should_crash`)
- The patch is effectively a **no-op** on this kernel
### What I've Tried
1. **Symbol resolution** → 0 symbols, fails.
2. **"thid_should_crash" string** → Found, but has **no ADRP+ADD code references**. The string is in `__PRELINK_INFO` (XML plist), not in a standalone `__cstring` section.
3. **Sysctl structure search** → Searched for a raw VA pointer to the string in DATA segments. Failed because the string VA is in the plist text, not a standalone pointer.
4. **Pattern search for value=1** → The value is already 0 at the upstream offset, so searching for value=1 finds nothing.
### Proposed Dynamic Strategy
The variable at `0x67EB50` is in the kernel's `__DATA` segment (BSS or initialized data). Since:
- The string is only in `__PRELINK_INFO` (plist), not usable as a code anchor
- The variable has no symbols
- The value is already 0
**Option A: Skip this patch gracefully.** If the value is already 0, the patch has no effect. Log a message and return True (success, nothing to do).
**Option B: Find via sysctl table structure.** The sysctl_oid structure in \_\_DATA contains:
- A pointer to the name string
- A pointer to the data variable
- Various flags
But the name string pointer would be a chained fixup pointer to the string in \_\_PRELINK_INFO, which is hard to search for.
**Option C: Find via `__PRELINK_INFO` plist parsing.** Parse the XML plist to find the `_PrelinkKCID` or sysctl registration info. This is complex and fragile.
**Recommended: Option A** — the variable is already 0 in PCC kernels. Emit a write-zero anyway at the upstream-equivalent location if we can find it, or just return True if we can't find the variable (safe no-op).
Actually, better approach: search `__DATA` segments for a `sysctl_oid` struct. The struct layout includes:
```c
struct sysctl_oid {
struct sysctl_oid_list *oid_parent; // +0x00
SLIST_ENTRY(sysctl_oid) oid_link; // +0x08
int oid_number; // +0x10
int oid_kind; // +0x14
void *oid_arg1; // +0x18 → points to the variable
int oid_arg2; // +0x20
const char *oid_name; // +0x28 → points to "thid_should_crash" string
...
};
```
So search all `__DATA` segments for an 8-byte value at offset +0x28 that decodes to the "thid_should_crash" string offset. Then read +0x18 to get the variable pointer.
But the string is in \_\_PRELINK_INFO, which complicates decoding the chained fixup pointer.
---
## Patch 3: `patch_hook_cred_label_update_execve` — FAILING
### Upstream Reference
```python
# Shellcode at 0xAB17D8 (46 instructions, ~184 bytes)
# Two critical BL targets:
# BL _vfs_context_current at idx 9: 0x940851AC → target = 0xCC5EAC
# BL _vnode_getattr at idx 17: 0x94085E69 → target = 0xCC91C0
# Ops table patch at 0xA54518: redirect to shellcode
# B _hook_cred_label_update_execve at idx 44: 0x146420B7 → target = 0x239A0B4
```
### Why It Fails
The patch needs two kernel functions that have **no symbols**:
- `_vfs_context_current` at file offset `0xCC5EAC`
- `_vnode_getattr` at file offset `0xCC91C0`
Without these, the shellcode can't be assembled (the BL offsets depend on the target addresses).
### Analysis of \_vfs_context_current (0xCC5EAC)
```
Expected: A very short function (2-4 instructions) that:
- Reads the current thread (mrs xN, TPIDR_EL1 or load from per-CPU data)
- Loads the VFS context from the thread struct
- Returns it in x0
Should have extremely high caller count (VFS is used everywhere).
```
Let me verify: check `bl_callers.get(0xCC5EAC, [])` — should have many callers.
### Analysis of \_vnode_getattr (0xCC91C0)
```
Expected: A moderate-sized function that:
- Takes (vnode, vnode_attr, vfs_context) parameters
- Calls the vnode op (VNOP_GETATTR)
- Returns error code
Should have moderate caller count (hundreds).
```
### Finding Strategy for \_vfs_context_current
1. **From sandbox ops table**: We already have `_find_sandbox_ops_table_via_conf()`. The hook_cred_label_update_execve entry (index 16) in the ops table points to the original sandbox hook function (at `0x239A0B4` per upstream).
2. **From the original hook function**: Disassemble the original hook function. It likely calls `_vfs_context_current` (to get the VFS context for vnode operations). Find the BL target in the hook that has a very high caller count — that's likely `_vfs_context_current`.
3. **Pattern match**: Search kern_text for short functions (size < 0x20) with:
- `mrs xN, TPIDR_EL1` instruction
- Very high caller count (>1000)
- Return type is pointer (loads from struct offset)
### Finding Strategy for \_vnode_getattr
1. **From the original hook function**: The hook function likely also calls `_vnode_getattr`. Find BL targets in the hook that have moderate caller count.
2. **String anchor**: Search for `"vnode_getattr"` string (not in plist but in `__cstring`). Find ADRP+ADD refs, trace to function.
3. **Pattern match**: The function signature includes a `vnode_attr` structure initialization with size `0x380`.
### Proposed Implementation
```
1. Find sandbox ops table → read entry at index 16 → get original hook func
2. Disassemble original hook function
3. Find _vfs_context_current: BL target in the hook with highest caller count (>1000)
4. Find _vnode_getattr: BL target that:
- Has moderate callers (50-1000)
- The calling site has nearby `mov wN, #0x380` (vnode_attr struct size)
5. With both functions found, build shellcode and patch ops table
```
---
## Patch Status Summary
| Patch | Status | Implementation |
| ----------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- |
| nvram_verify_permission | IMPLEMENTED | Uses "krn." string anchor → NOP TBZ/TBNZ guard near string ref |
| thid_should_crash | IMPLEMENTED | Multi-strategy: symbol lookup, sysctl_oid struct scanning, ADRP+ADD fallback |
| hook_cred_label_update_execve | IMPLEMENTED | Inline vfs_context via `mrs x8, tpidr_el1` + `stp`; vnode_getattr via string anchor; dynamic hook index + code cave |
---
## Previously Fixed Patches
### patch_task_for_pid — FIXED
**Problem**: Old code searched for "proc_ro_ref_task" string → wrong function.
**Solution**: Pattern search: 0 BL callers + 2x ldadda + 2x `ldr wN,[xN,#0x490]; str wN,[xN,#0xc]` + movk #0xc8a2 + non-panic BL >500 callers. NOP the second `ldr wN,[xN,#0x490]`.
**Upstream**: `patch(0xFC383C, 0xd503201f)` — NOP in function at `0xFC3718`.
### patch_load_dylinker — FIXED
**Problem**: Old code searched for "/usr/lib/dyld" → wrong function (0 BL callers, no string ref).
**Solution**: Search for functions with 3+ `TST xN, #-0x40000000000000; B.EQ; MOVK xN, #0xc8a2` triplets and 0 BL callers. Replace LAST TST with unconditional B to B.EQ target.
**Upstream**: `patch(0x1052A28, B #0x44)` — in function at `0x105239C`.
### patch_syscallmask_apply_to_proc — FIXED
**Problem**: `bl_callers` key bug: code used `target + self.base_va` but bl_callers is keyed by file offset.
**Fix**: Changed to `self.bl_callers.get(target, [])` at line ~1661.
**Status**: Now PASSING (40 patches emitted for shellcode + redirect).
### patch_nvram_verify_permission — FIXED
**Problem**: 332 identical IOKit methods match structural filter; "krn." string leads to wrong function.
**Solution**: Uses "krn." string anchor to find the function, then NOPs TBZ/TBNZ guard near the string ref. Different mechanism from upstream (NOP BL memmove) but achieves the same NVRAM bypass.
### patch_thid_should_crash — FIXED
**Problem**: String in `__PRELINK_INFO` plist (no code refs); value already `0x00000000` in PCC kernel.
**Solution**: Multi-strategy approach — symbol lookup, string search + sysctl_oid struct scanning (checking forward 128 bytes for chained fixup pointers), and ADRP+ADD fallback.
### patch_hook_cred_label_update_execve — FIXED
**Problem**: Needed `_vfs_context_current` and `_vnode_getattr` — 0 symbols available.
**Solution**: Eliminated `_vfs_context_current` entirely — shellcode constructs vfs_context inline on stack via `mrs x8, tpidr_el1` + `stp x8, x0, [sp, #0x70]`. `_vnode_getattr` found via "vnode_getattr" string anchor. Hook index found dynamically (scan first 30 ops entries). Code cave allocated via `_find_code_cave(180)`.
---
## Environment Notes
### Running on macOS (current)
```bash
cd /Users/qaq/Documents/GitHub/vphone-cli
source .venv/bin/activate
python3 -c "
import sys; sys.path.insert(0, 'scripts')
from fw_patch import load_firmware
from patchers.kernel_jb import KernelJBPatcher
_, data, _, _ = load_firmware('vm/iPhone17,3_26.1_23B85_Restore/kernelcache.release.vphone600')
p = KernelJBPatcher(data)
patches = p.find_all()
print(f'Total patches: {len(patches)}')
"
```
### Running on Linux (cloud)
Requirements:
- Python 3.10+
- `pip install capstone keystone-engine pyimg4`
- Note: `keystone-engine` may need `cmake` and C++ compiler on Linux
- Copy the kernelcache file and upstream reference
- The `setup_venv.sh` script has macOS-specific keystone dylib handling — on Linux, pip install should work directly
Files needed:
- `scripts/patchers/kernel.py` (base class)
- `scripts/patchers/kernel_jb.py` (JB patcher)
- `scripts/patchers/__init__.py`
- `scripts/fw_patch.py` (for `load_firmware()`)
- `vm/iPhone17,3_26.1_23B85_Restore/kernelcache.release.vphone600` (test kernel)
- `/Users/qaq/Documents/GitHub/super-tart-vphone/CFW/patch_fw.py` (upstream reference)
### Quick Test Script
```python
#!/usr/bin/env python3
"""Test all 24 JB kernel patch methods."""
import sys
sys.path.insert(0, 'scripts')
from fw_patch import load_firmware
from patchers.kernel_jb import KernelJBPatcher
_, data, _, _ = load_firmware('vm/iPhone17,3_26.1_23B85_Restore/kernelcache.release.vphone600')
p = KernelJBPatcher(data, verbose=True)
patches = p.find_all()
print(f'\n>>> Total: {len(patches)} patches from 24 methods')
```
---
## Upstream Offsets Reference (iPhone17,3 26.1 23B85)
| Symbol / Patch | File Offset | Notes |
| -------------------------------- | ------------------ | ------------------------------- |
| kern_text start | 0xA74000 | |
| kern_text end | 0x24B0000 | |
| base_va | 0xFFFFFE0007004000 | |
| \_thid_should_crash var | 0x67EB50 | DATA, value=0 |
| \_task_for_pid func | 0xFC3718 | patch at 0xFC383C |
| \_load_dylinker patch | 0x1052A28 | TST → B |
| verifyPermission func | 0x1233E40 | patch BL at 0x1234034 |
| verifyPermission vtable | 0x7410B8 | \_\_DATA_CONST |
| IONVRAMController metaclass | 0x26FEA38 | |
| IONVRAMController metaclass ctor | 0x125D2C0 | refs "IONVRAMController" string |
| IONVRAMController PAC disc | 0xcda1 | movk x17, #0xcda1 |
| IONVRAMController instance size | 0x88 | mov w3, #0x88 |
| \_vfs_context_current | 0xCC5EAC | (from upstream BL encoding) |
| \_vnode_getattr | 0xCC91C0 | (from upstream BL encoding) |
| shellcode cave (upstream) | 0xAB1740 | syscallmask |
| shellcode cave 2 (upstream) | 0xAB17D8 | hook_cred_label |
| sandbox ops table (hook entry) | 0xA54518 | index 16 |
| \_hook_cred_label_update_execve | 0x239A0B4 | original hook func |
| memmove | 0x12CB0D0 | 3114 callers |
| OSMetaClass::OSMetaClass() | 0x10EA790 | 5236 callers |
| \_panic | varies | 8000+ callers typically |

View File

@@ -0,0 +1,137 @@
# Binary Kernelcache Patch Verification Report
Date: 2026-02-27
## Scope
Verify that the dynamic kernel patch finder (`scripts/patchers/kernel.py`) produces
the same binary result as the legacy hardcoded patch list on vphone600, then
apply the dynamic patcher to a freshly extracted vresearch101 kernelcache.
## Inputs
- Original vphone600 raw kernel: `/tmp/kc_vphone600_orig.raw`
- vphone600 upstream hardcoded patch list: `super-tart-vphone-private/CFW/patch_fw.py`
- Dynamic patcher: `scripts/patchers/kernel.py`
- VM kernelcache image (vresearch101): `VM/iPhone17,3_26.1_23B85_Restore/kernelcache.research.vresearch101`
## Method
1. Apply legacy hardcoded patches to `/tmp/kc_vphone600_orig.raw` using binary
replacement (32-bit writes) and save as `/tmp/kc_vphone600_upstream.raw`.
2. Run `KernelPatcher.find_all()` on `/tmp/kc_vphone600_orig.raw`, apply all
dynamic patches, and save as `/tmp/kc_vphone600_dynamic.raw`.
3. Compare the two patched binaries with `cmp -l`.
4. Re-extract a clean vresearch101 kernelcache using `pyimg4 im4p extract`, save
as `/tmp/kc_vresearch1_orig.raw`.
5. Run the dynamic patcher on `/tmp/kc_vresearch1_orig.raw`, save as
`/tmp/kc_vresearch1_dynamic.raw`.
## Outputs
- `/tmp/kc_vphone600_upstream.raw`
- `/tmp/kc_vphone600_dynamic.raw`
- `/tmp/kc_vresearch1_orig.raw`
- `/tmp/kc_vresearch1_dynamic.raw`
## Checksums (SHA-256)
- `/tmp/kc_vphone600_orig.raw`:
`b6846048f3a60eab5f360fcc0f3dcb5198aa0476c86fb06eb42f6267cdbfcae0`
- `/tmp/kc_vphone600_upstream.raw`:
`373e016d34ae5a2d8ba7ba96c920f4f6700dea503e3689d06a99e90ebec701c8`
- `/tmp/kc_vphone600_dynamic.raw`:
`373e016d34ae5a2d8ba7ba96c920f4f6700dea503e3689d06a99e90ebec701c8`
- `/tmp/kc_vresearch1_orig.raw`:
`c673c9b8226ea774d1d935427760e2e9a48200fd1daf0ef584dc88df0dccefde`
- `/tmp/kc_vresearch1_dynamic.raw`:
`f36a78ce59c658df85ecdead56d46370a1107181689091cf798e529664f6e2b5`
## vphone600: Hardcoded vs Dynamic
Result: **byte-identical** output between hardcoded and dynamic patching.
- `KernelPatcher` patches found: 25
- Hardcoded patches applied: 25
- `cmp -l /tmp/kc_vphone600_upstream.raw /tmp/kc_vphone600_dynamic.raw`:
no output (files identical)
### Hardcoded Patch List (vphone600)
Offsets and 32-bit patch values, taken from `patch_fw.py`:
| # | Offset (hex) | Patch value | Purpose |
| --- | -----------: | ----------: | ------------------------------------------ |
| 1 | 0x2476964 | 0xD503201F | \_apfs_vfsop_mount root snapshot NOP |
| 2 | 0x23CFDE4 | 0xD503201F | \_authapfs_seal_is_broken NOP |
| 3 | 0x00F6D960 | 0xD503201F | \_bsd_init rootvp NOP |
| 4 | 0x163863C | 0x52800000 | \_proc_check_launch_constraints mov w0,#0 |
| 5 | 0x1638640 | 0xD65F03C0 | \_proc_check_launch_constraints ret |
| 6 | 0x12C8138 | 0xD2800020 | \_PE_i_can_has_debugger mov x0,#1 |
| 7 | 0x12C813C | 0xD65F03C0 | \_PE_i_can_has_debugger ret |
| 8 | 0x00FFAB98 | 0xD503201F | TXM post-validation NOP (tbnz) |
| 9 | 0x16405AC | 0x6B00001F | postValidation cmp w0,w0 |
| 10 | 0x16410BC | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (1) |
| 11 | 0x16410C8 | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (2) |
| 12 | 0x242011C | 0x52800000 | \_apfs_graft mov w0,#0 |
| 13 | 0x2475044 | 0xEB00001F | \_apfs_vfsop_mount cmp x0,x0 |
| 14 | 0x2476C00 | 0x52800000 | \_apfs_mount_upgrade_checks mov w0,#0 |
| 15 | 0x248C800 | 0x52800000 | \_handle_fsioc_graft mov w0,#0 |
| 16 | 0x23AC528 | 0xD2800000 | \_hook_file_check_mmap mov x0,#0 |
| 17 | 0x23AC52C | 0xD65F03C0 | \_hook_file_check_mmap ret |
| 18 | 0x23AAB58 | 0xD2800000 | \_hook_mount_check_mount mov x0,#0 |
| 19 | 0x23AAB5C | 0xD65F03C0 | \_hook_mount_check_mount ret |
| 20 | 0x23AA9A0 | 0xD2800000 | \_hook_mount_check_remount mov x0,#0 |
| 21 | 0x23AA9A4 | 0xD65F03C0 | \_hook_mount_check_remount ret |
| 22 | 0x23AA80C | 0xD2800000 | \_hook_mount_check_umount mov x0,#0 |
| 23 | 0x23AA810 | 0xD65F03C0 | \_hook_mount_check_umount ret |
| 24 | 0x23A5514 | 0xD2800000 | \_hook_vnode_check_rename mov x0,#0 |
| 25 | 0x23A5518 | 0xD65F03C0 | \_hook_vnode_check_rename ret |
## TXM Patch Details
Dynamic patcher locates the `"TXM [Error]: CodeSignature"` string, finds the
following `tbnz` in the log/error path, and NOPs it.
### vphone600 disassembly around the patch (0xFFAB98)
Before:
```
0x00FFAB90: mov w0, #5
0x00FFAB94: ldrb w8, [x19, #6]
0x00FFAB98: tbnz w8, #0, #0xffac80
0x00FFAB9C: ldp x29, x30, [sp, #0x100]
```
After:
```
0x00FFAB90: mov w0, #5
0x00FFAB94: ldrb w8, [x19, #6]
0x00FFAB98: nop
0x00FFAB9C: ldp x29, x30, [sp, #0x100]
```
## vresearch101: Dynamic Patch Run
Extraction:
```
pyimg4 im4p extract \
-i VM/iPhone17,3_26.1_23B85_Restore/kernelcache.research.vresearch101 \
-o /tmp/kc_vresearch1_orig.raw
```
Dynamic patcher results:
- Patches found/applied: 25
- TXM patch location: `0xFA6B98` (NOP `tbnz w8, #0, #0xfa6c80`)
- Patched output: `/tmp/kc_vresearch1_dynamic.raw`
## Conclusion
For vphone600, the dynamic patcher output is byte-identical to the legacy
hardcoded patch list, indicating functional equivalence on this kernelcache.
The same dynamic patcher also successfully patches the freshly extracted
vresearch101 kernelcache with the expected TXM NOP and a full 25-patch set.

View File

@@ -0,0 +1,251 @@
# Virtualization.framework Keyboard Event Pipeline
Reverse engineering findings for the keyboard event pipeline in Apple's
Virtualization.framework (macOS 26.2, version 259.3.3.0.0). Documents how
keyboard events flow from the macOS host to the virtual iPhone guest.
---
## Event Flow Architecture
There are two pipelines for sending keyboard events to the VM:
### Pipeline 1: \_VZKeyEvent -> sendKeyEvents: (Standard Keys)
```
_VZKeyEvent(type, keyCode)
-> _VZKeyboard.sendKeyEvents:(NSArray<_VZKeyEvent>)
-> table lookup: keyCode -> intermediate index
-> pack: uint64_t = (index << 32) | is_key_down
-> std::vector<uint64_t>
-> [if type==2] sendKeyboardEventsHIDReport:keyboardID: (switch -> IOHIDEvent -> HID reports)
-> [fallback] eventSender.sendKeyboardEvents:keyboardID: (VzCore C++ layer)
```
### Pipeline 2: \_processHIDReports (Raw HID Reports)
```
Raw HID report bytes
-> std::span<const unsigned char>{data_ptr, length}
-> std::vector<std::span<...>>{begin, end, cap}
-> VZVirtualMachine._processHIDReports:forDevice:deviceType:
-> XpcEncoder::encode_data(span) -> xpc_data_create
-> XPC to VMM process
```
---
## \_VZKeyEvent Structure
From IDA + LLDB inspection:
```c
struct _VZKeyEvent { // sizeof = 0x18
uint8_t isa[8]; // offset 0x00 -- ObjC isa pointer
uint16_t _keyCode; // offset 0x08 -- Apple VK code (0x00-0xB2)
uint8_t _pad[6]; // offset 0x0A -- padding
int64_t _type; // offset 0x10 -- 0 = keyDown, 1 = keyUp
};
```
Initializer: `_VZKeyEvent(type: Int64, keyCode: UInt16)`
---
## \_VZKeyboard Object Layout
From LLDB memory dump:
```
+0x00: isa
+0x08: _eventSender (weak, id<_VZHIDAdditions, _VZKeyboardEventSender>)
+0x10: _deviceIdentifier (uint32_t) -- value 1 for first keyboard
+0x18: type (int64_t) -- 0 for USB keyboard, 2 for type that tries HIDReport first
```
---
## Lookup Tables in sendKeyEvents:
Two tables indexed by Apple VK keyCode (0x00-0xB2, 179 entries x 8 bytes each):
**Table 1** (validity flags): All valid entries = `0x0000000100000000` (bit 32 set).
Invalid entries = 0.
**Table 2** (intermediate indices): Maps Apple VK codes to internal indices (0x00-0x72).
The tables are OR'd: `combined = table1[vk] | table2[vk]`. Bit 32 check validates
the entry. The lower 32 bits of combined become the intermediate index.
### Sample Table 2 Entries
| Apple VK | Key | Table2 (Index) | HID Page | HID Usage |
| -------- | ------- | -------------- | -------- | --------- |
| 0x00 | A | 0x00 | 7 | 0x04 |
| 0x01 | S | 0x12 | 7 | 0x16 |
| 0x24 | Return | 0x24 | 7 | 0x28 |
| 0x31 | Space | 0x29 | 7 | 0x2C |
| 0x35 | Escape | 0x25 | 7 | 0x29 |
| 0x38 | Shift | 0x51 | 7 | 0xE1 |
| 0x37 | Command | 0x53 | 7 | 0xE3 |
### Invalid VK Codes (both tables = 0, silently dropped)
0x48 (Volume Up), 0x49 (Volume Down), 0x4A (Mute), and many others.
---
## Packed Event Format (std::vector<uint64_t>)
Each element in the vector sent to `sendKeyboardEvents:keyboardID:`:
```
bits 63:32 = intermediate_index (from table2, lower 32 bits of combined)
bits 31:1 = 0
bit 0 = is_key_down (1 = down, 0 = up)
```
---
## sendKeyboardEventsHIDReport Switch Statement
For type-2 keyboards, the intermediate index is mapped to
`IOHIDEventCreateKeyboardEvent(page, usage)` via a large switch.
### Standard Keyboard Entries (HID Page 7)
| Index | HID Page | HID Usage | Meaning |
| --------- | -------- | --------- | -------------------- |
| 0x00-0x19 | 7 | 4-29 | Letters a-z |
| 0x1A-0x23 | 7 | 30-39 | Digits 1-0 |
| 0x24 | 7 | 40 | Return |
| 0x25 | 7 | 41 | Escape |
| 0x29 | 7 | 44 | Space |
| 0x48-0x4B | 7 | 79-82 | Arrow keys |
| 0x50-0x53 | 7 | 224-227 | L-Ctrl/Shift/Alt/Cmd |
### Consumer / System Entries (Non-Standard Pages)
| Index | HID Page | HID Usage | Meaning |
| ----- | -------- | --------- | ------------------------ |
| 0x6E | **12** | 671 | **Consumer Volume Down** |
| 0x6F | **12** | 674 | **Consumer Volume Up** |
| 0x70 | **12** | 207 | **Consumer Play/Pause** |
| 0x71 | **12** | 545 | **Consumer Snapshot** |
| 0x72 | **1** | 155 | **Generic Desktop Wake** |
**Home/Menu (Consumer page 0x0C, usage 0x40) has NO intermediate index.** It cannot
be sent through Pipeline 1 at all.
---
## \_processHIDReports Parameter Format
From IDA decompilation of
`VZVirtualMachine._processHIDReports:forDevice:deviceType:` at 0x2301b2310.
The `void *` parameter is a **pointer to std::vector<std::span<const unsigned char>>**:
```
Level 3 (outermost): std::vector (24 bytes, passed by pointer)
+0x00: __begin_ (pointer to span array)
+0x08: __end_ (pointer past last span)
+0x10: __end_cap_ (capacity pointer)
Level 2: std::span (16 bytes per element in the array)
+0x00: data_ptr (const unsigned char *)
+0x08: length (size_t)
Level 1 (innermost): raw HID report bytes
```
The function iterates spans in the vector:
```c
begin = *vec; // vec->__begin_
end = *(vec + 1); // vec->__end_
for (span = begin; span != end; span += 16) {
data_ptr = *(uint64_t*)span;
length = *(uint64_t*)(span + 8);
encoder.encode_data(data_ptr, length); // -> xpc_data_create
}
```
**deviceType**: 0 = keyboard, 1 = pointing device
**device**: device identifier (uint32_t, matches `_VZKeyboard._deviceIdentifier`)
---
## Crash Analysis: Why Raw Bytes Crashed
Passing raw `[0x40, 0x00]` as the `void*` parameter:
1. Function reads bytes as vector struct: begin = 0x0040 (first 8 bytes), end = garbage
2. Dereferences begin as span pointer -> reads from address ~0x0040
3. Gets garbage data_ptr (0x700420e) and garbage length (0x300000020 = 12GB)
4. `xpc_data_create(0x700420e, 0x300000020)` -> EXC_BAD_ACCESS in memcpy
The three-level indirection (vector -> span -> bytes) must be constructed correctly
or the framework will dereference invalid pointers.
---
## Swift Implementation Notes
### Accessing \_VZKeyboard
```swift
// Get keyboards array
let arr = Dynamic(vm)._keyboards.asObject as? NSArray
let keyboard = arr?.object(at: 0) as AnyObject
// _deviceIdentifier is an ivar, not a property -- use KVC
(keyboard as? NSObject)?.value(forKey: "_deviceIdentifier") as? UInt32
```
### Constructing std::vector<uint64_t> for sendKeyboardEvents
```swift
let data = UnsafeMutablePointer<UInt64>.allocate(capacity: 1)
data.pointee = (index << 32) | (isKeyDown ? 1 : 0)
var vec = (data, data.advanced(by: 1), data.advanced(by: 1))
withUnsafeMutablePointer(to: &vec) { vecPtr in
Dynamic(vm).sendKeyboardEvents(UnsafeMutableRawPointer(vecPtr), keyboardID: deviceId)
}
```
### Constructing vector<span<unsigned char>> for \_processHIDReports
```swift
let reportPtr = UnsafeMutablePointer<UInt8>.allocate(capacity: N)
// fill report bytes...
let spanPtr = UnsafeMutablePointer<Int>.allocate(capacity: 2)
spanPtr[0] = Int(bitPattern: reportPtr) // data pointer
spanPtr[1] = N // length
let vecPtr = UnsafeMutablePointer<Int>.allocate(capacity: 3)
vecPtr[0] = Int(bitPattern: UnsafeRawPointer(spanPtr)) // begin
vecPtr[1] = Int(bitPattern: UnsafeRawPointer(spanPtr).advanced(by: 16)) // end
vecPtr[2] = vecPtr[1] // cap
Dynamic(vm)._processHIDReports(UnsafeRawPointer(vecPtr), forDevice: deviceId, deviceType: 0)
```
---
## Source Files
- Class dumps: `/Users/qaq/Documents/GitHub/super-tart-vphone-private/Virtualization_26.2-class-dump/`
- IDA database: dyld_shared_cache_arm64e with Virtualization.framework
### Key Functions Analyzed
| Function | Address |
| ------------------------------------------------------------------------------- | ----------- |
| `-[_VZKeyboard sendKeyEvents:]` | 0x2301b2f54 |
| `-[_VZKeyboard sendKeyboardEventsHIDReport:keyboardID:]` | 0x2301b3230 |
| `-[VZVirtualMachine(_VZHIDAdditions) _processHIDReports:forDevice:deviceType:]` | 0x2301b2310 |
| `-[VZVirtualMachineView _sendKeyEventsToVirtualMachine:]` | -- |
| `-[_VZHIDEventMonitor getHIDReportsFromHIDEvent:]` | 0x2301b2af0 |

View File

@@ -0,0 +1,285 @@
# Patch Comparison: Regular / Development / Jailbreak
Three firmware variants are available, each building on the previous:
- **Regular** (`make fw_patch` + `make cfw_install`) — Minimal patches for VM boot with signature bypass and SSV override.
- **Development** (`make fw_patch_dev` + `make cfw_install_dev`) — Regular + TXM entitlement/developer-mode bypasses + launchd jetsam fix. Enables debugging and code signing flexibility without full jailbreak.
- **Jailbreak** (`make fw_patch_jb` + `make cfw_install_jb`) — Regular + comprehensive security bypass across iBSS, TXM, kernel, and userland. Full code execution, sandbox escape, and package management.
## Boot Chain Patches
### AVPBooter
| # | Patch | Purpose | Regular | Dev | JB |
| --- | ------------ | -------------------------------- | :-----: | :-: | :-: |
| 1 | `mov x0, #0` | DGST signature validation bypass | Y | Y | Y |
### iBSS
| # | Patch | Purpose | Regular | Dev | JB |
| --- | --------------------------------- | ----------------------------------------------------------- | :-----: | :-: | :-: |
| 1 | Serial labels (2x) | "Loaded iBSS" in serial log | Y | Y | Y |
| 2 | image4_validate_property_callback | Signature bypass (`b.ne` → NOP, `mov x0,x22``mov x0,#0`) | Y | Y | Y |
| 3 | Skip generate_nonce | Keep apnonce stable for SHSH (`tbz` → unconditional `b`) | — | — | Y |
### iBEC
| # | Patch | Purpose | Regular | Dev | JB |
| --- | --------------------------------- | ----------------------------------------- | :-----: | :-: | :-: |
| 1 | Serial labels (2x) | "Loaded iBEC" in serial log | Y | Y | Y |
| 2 | image4_validate_property_callback | Signature bypass | Y | Y | Y |
| 3 | Boot-args redirect | ADRP+ADD → `serial=3 -v debug=0x2014e %s` | Y | Y | Y |
### LLB
| # | Patch | Purpose | Regular | Dev | JB |
| --- | --------------------------------- | ----------------------------------------- | :-----: | :-: | :-: |
| 1 | Serial labels (2x) | "Loaded LLB" in serial log | Y | Y | Y |
| 2 | image4_validate_property_callback | Signature bypass | Y | Y | Y |
| 3 | Boot-args redirect | ADRP+ADD → `serial=3 -v debug=0x2014e %s` | Y | Y | Y |
| 4 | Rootfs bypass (5 patches) | Allow edited rootfs loading | Y | Y | Y |
| 5 | Panic bypass | NOP `cbnz` after `mov w8,#0x328` check | Y | Y | Y |
### TXM
The three variants use different TXM patchers. Regular uses `txm.py` (1 patch), Dev uses `txm_dev.py` (10 patches), JB uses `txm_jb.py` (12 patches).
| # | Patch | Purpose | Regular | Dev | JB |
| --- | ------------------------------------------------- | ----------------------------------------------------------- | :-----: | :-: | :-: |
| 1 | Trustcache binary-search bypass | `bl hash_cmp``mov x0, #0` | Y | Y | Y |
| 2 | Selector24 hash extraction: NOP LDR X1 | Bypass CS hash flag extraction | — | — | Y |
| 3 | Selector24 hash extraction: NOP BL | Bypass CS hash flag check | — | — | Y |
| 4 | get-task-allow (selector 41\|29) | `bl``mov x0, #1` — allow get-task-allow | — | Y | Y |
| 5 | Selector42\|29 shellcode: branch to cave | Redirect dispatch stub to shellcode | — | Y | Y |
| 6 | Selector42\|29 shellcode: NOP pad | UDF → NOP in code cave | — | Y | Y |
| 7 | Selector42\|29 shellcode: `mov x0, #1` | Set return value to true | — | Y | Y |
| 8 | Selector42\|29 shellcode: `strb w0, [x20, #0x30]` | Set manifest flag | — | Y | Y |
| 9 | Selector42\|29 shellcode: `mov x0, x20` | Restore context pointer | — | Y | Y |
| 10 | Selector42\|29 shellcode: branch back | Return from shellcode to stub+4 | — | Y | Y |
| 11 | Debugger entitlement (selector 42\|37) | `bl``mov w0, #1` — allow `com.apple.private.cs.debugger` | — | Y | Y |
| 12 | Developer mode bypass | NOP conditional guard before deny path | — | Y | Y |
### Kernelcache
Regular and Dev share the same 25 base kernel patches. JB adds 34 additional patches.
#### Base patches (all variants)
| # | Patch | Function | Purpose | Regular | Dev | JB |
| ----- | -------------------------- | -------------------------------- | ---------------------------------------- | :-----: | :-: | :-: |
| 1 | NOP `tbnz w8,#5` | `_apfs_vfsop_mount` | Skip "root snapshot" sealed volume check | Y | Y | Y |
| 2 | NOP conditional | `_authapfs_seal_is_broken` | Skip "root volume seal" panic | Y | Y | Y |
| 3 | NOP conditional | `_bsd_init` | Skip "rootvp not authenticated" panic | Y | Y | Y |
| 45 | `mov w0,#0; ret` | `_proc_check_launch_constraints` | Bypass launch constraints | Y | Y | Y |
| 67 | `mov x0,#1` (2x) | `PE_i_can_has_debugger` | Enable kernel debugger | Y | Y | Y |
| 8 | NOP | `_postValidation` | Skip AMFI post-validation | Y | Y | Y |
| 9 | `cmp w0,w0` | `_postValidation` | Force comparison true | Y | Y | Y |
| 1011 | `mov w0,#1` (2x) | `_check_dyld_policy_internal` | Allow dyld loading | Y | Y | Y |
| 12 | `mov w0,#0` | `_apfs_graft` | Allow APFS graft | Y | Y | Y |
| 13 | `cmp x0,x0` | `_apfs_vfsop_mount` | Skip mount check | Y | Y | Y |
| 14 | `mov w0,#0` | `_apfs_mount_upgrade_checks` | Allow mount upgrade | Y | Y | Y |
| 15 | `mov w0,#0` | `_handle_fsioc_graft` | Allow fsioc graft | Y | Y | Y |
| 1625 | `mov x0,#0; ret` (5 hooks) | Sandbox MACF ops table | Stub 5 sandbox hooks | Y | Y | Y |
#### JB-only kernel patches
| # | Patch | Function | Purpose | Regular | Dev | JB |
| --- | ---------------------------- | ------------------------------------ | ------------------------------------------ | :-----: | :-: | :-: |
| 26 | Function rewrite | `AMFIIsCDHashInTrustCache` | Always return true + store hash | — | — | Y |
| 27 | Shellcode + branch | `_cred_label_update_execve` | Set cs_flags (platform+entitlements) | — | — | Y |
| 28 | `cmp w0,w0` | `_postValidation` (additional) | Force validation pass | — | — | Y |
| 29 | Shellcode + branch | `_syscallmask_apply_to_proc` | Patch zalloc_ro_mut for syscall mask | — | — | Y |
| 30 | Shellcode + ops redirect | `_hook_cred_label_update_execve` | vnode_getattr ownership + suid propagation | — | — | Y |
| 31 | `mov x0,#0; ret` (20+ hooks) | Sandbox MACF ops (extended) | Stub remaining 20+ sandbox hooks | — | — | Y |
| 32 | `cmp xzr,xzr` | `_task_conversion_eval_internal` | Allow task conversion | — | — | Y |
| 33 | `mov x0,#0; ret` | `_proc_security_policy` | Bypass security policy | — | — | Y |
| 34 | NOP (2x) | `_proc_pidinfo` | Allow pid 0 info | — | — | Y |
| 35 | `b` (skip panic) | `_convert_port_to_map_with_flavor` | Skip kernel map panic | — | — | Y |
| 36 | NOP | `_vm_fault_enter_prepare` | Skip fault check | — | — | Y |
| 37 | `b` (skip check) | `_vm_map_protect` | Allow VM protect | — | — | Y |
| 38 | NOP + `mov x8,xzr` | `___mac_mount` | Bypass MAC mount check | — | — | Y |
| 39 | NOP | `_dounmount` | Allow unmount | — | — | Y |
| 40 | `mov x0,#0` | `_bsd_init` (2nd) | Skip auth at @%s:%d | — | — | Y |
| 41 | NOP (2x) | `_spawn_validate_persona` | Skip persona validation | — | — | Y |
| 42 | NOP | `_task_for_pid` | Allow task_for_pid | — | — | Y |
| 43 | `b` (skip check) | `_load_dylinker` | Allow dylinker loading | — | — | Y |
| 44 | `cmp x0,x0` | `_shared_region_map_and_slide_setup` | Force shared region | — | — | Y |
| 45 | NOP BL | `_verifyPermission` (NVRAM) | Allow NVRAM writes | — | — | Y |
| 46 | `b` (skip check) | `_IOSecureBSDRoot` | Skip secure root check | — | — | Y |
| 47 | Syscall 439 + shellcode | kcall10 (`SYS_kas_info` replacement) | Kernel arbitrary call from userspace | — | — | Y |
| 48 | Zero out | `_thid_should_crash` | Prevent GUARD_TYPE_MACH_PORT crash | — | — | Y |
## CFW Installation Patches
### Binary patches applied over SSH ramdisk
| # | Patch | Binary | Purpose | Regular | Dev | JB |
| --- | ----------------------- | -------------------- | ----------------------------------------- | :-----: | :-: | :-: |
| 1 | `/%s.gl``/AA.gl` | seputil | Gigalocker UUID fix | Y | Y | Y |
| 2 | NOP cache validation | launchd_cache_loader | Allow modified launchd.plist | Y | Y | Y |
| 3 | `mov x0,#1; ret` | mobileactivationd | Activation bypass | Y | Y | Y |
| 4 | Plist injection | launchd.plist | bash/dropbear/trollvnc/vphoned daemons | Y | Y | Y |
| 5 | `b` (skip jetsam guard) | launchd | Prevent jetsam panic on boot | — | Y | Y |
| 6 | LC_LOAD_DYLIB injection | launchd | Load `/cores/launchdhook.dylib` at launch | — | — | Y |
### Installed components
| # | Component | Description | Regular | Dev | JB |
| --- | ------------------------ | ----------------------------------------------------------------- | :-----: | :-: | :-: |
| 1 | Cryptex SystemOS + AppOS | Decrypt AEA + mount + copy to device | Y | Y | Y |
| 2 | GPU driver | AppleParavirtGPUMetalIOGPUFamily bundle | Y | Y | Y |
| 3 | iosbinpack64 | Jailbreak tools (base set) | Y | Y | Y |
| 4 | iosbinpack64 dev overlay | Replace `rpcserver_ios` with dev build | — | Y | — |
| 5 | vphoned | vsock HID/control daemon (built + signed) | Y | Y | Y |
| 6 | LaunchDaemons | bash, dropbear, trollvnc, rpcserver_ios, vphoned plists | Y | Y | Y |
| 7 | Procursus bootstrap | Bootstrap filesystem + optional Sileo deb | — | — | Y |
| 8 | BaseBin hooks | systemhook.dylib, launchdhook.dylib, libellekit.dylib → `/cores/` | — | — | Y |
## Summary
| Component | Regular | Dev | JB |
| ------------------------ | :-----: | :----: | :----: |
| AVPBooter | 1 | 1 | 1 |
| iBSS | 2 | 2 | 3 |
| iBEC | 3 | 3 | 3 |
| LLB | 6 | 6 | 6 |
| TXM | 1 | 10 | 12 |
| Kernel | 25 | 25 | 59 |
| **Boot chain total** | **38** | **47** | **84** |
| | | | |
| CFW binary patches | 4 | 5 | 6 |
| CFW installed components | 6 | 7 | 8 |
| **CFW total** | **10** | **12** | **14** |
| | | | |
| **Grand total** | **48** | **59** | **98** |
### What each variant adds
**Regular → Dev** (+11 patches):
- TXM: +9 patches (get-task-allow, selector42|29 shellcode, debugger entitlement, developer mode bypass)
- CFW: +1 binary patch (launchd jetsam), +1 component (dev rpcserver_ios overlay)
**Regular → JB** (+50 patches):
- iBSS: +1 (nonce skip)
- TXM: +11 (hash extraction NOP, get-task-allow, selector42|29 shellcode, debugger entitlement, dev mode bypass)
- Kernel: +34 (trustcache, execve, sandbox, task/VM, memory, kcall10)
- CFW: +2 binary patches (launchd jetsam + dylib injection), +2 components (procursus + BaseBin hooks)
## JB Install Flow (`make cfw_install_jb`)
- Entry: `scripts/cfw_install_jb.sh` runs `scripts/cfw_install.sh` with `CFW_SKIP_HALT=1`, then continues with JB phases.
- Added JB phases in install pipeline:
- `JB-1`: patch `/mnt1/sbin/launchd` via `inject-dylib` (adds `/cores/launchdhook.dylib` LC_LOAD_DYLIB) + `patch-launchd-jetsam` (dynamic string+xref).
- `JB-2`: unpack procursus bootstrap (`bootstrap-iphoneos-arm64.tar.zst`) into `/mnt5/<bootManifestHash>/jb-vphone/procursus`.
- `JB-3`: deploy BaseBin hook dylibs (`systemhook.dylib`, `launchdhook.dylib`, `libellekit.dylib`) to `/mnt1/cores/`, re-signed with ldid + signcert.p12.
- JB resources now packaged in:
- `scripts/resources/cfw_jb_input.tar.zst`
- contains:
- `jb/bootstrap-iphoneos-arm64.tar.zst`
- `jb/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb`
- `basebin/*.dylib` (BaseBin hooks for JB-3)
## Dynamic Implementation Log (JB Patchers)
### TXM (`txm_jb.py`)
All TXM JB patches are implemented with dynamic binary analysis and
keystone/capstone-encoded instructions only.
1. `selector24 A1` (2x nop: LDR + BL)
- Locator: unique guarded `mov w0,#0xa1` site, scan for `ldr x1,[xN,#0x38] ; add x2 ; bl ; ldp` pattern.
- Patch bytes: keystone `nop` on the LDR and the BL.
2. `selector41/29 get-task-allow`
- Locator: xref to `"get-task-allow"` + nearby `bl` followed by `tbnz w0,#0`.
- Patch bytes: keystone `mov x0, #1`.
3. `selector42/29 shellcode trampoline`
- Locator:
- Find dispatch stub pattern `bti j ; mov x0,x20 ; bl ; mov x1,x21 ; mov x2,x22 ; bl ; b`.
- Select stub whose second `bl` target is the debugger-gate function (pattern verified by string-xref + call-shape).
- Find executable UDF cave dynamically.
- Patch bytes:
- Stub head -> keystone `b #cave`.
- Cave payload -> `nop ; mov x0,#1 ; strb w0,[x20,#0x30] ; mov x0,x20 ; b #return`.
4. `selector42/37 debugger entitlement`
- Locator: xref to `"com.apple.private.cs.debugger"` + strict nearby call-shape
(`mov x0,#0 ; mov x2,#0 ; bl ; tbnz w0,#0`).
- Patch bytes: keystone `mov w0, #1`.
5. `developer mode bypass`
- Locator: xref to `"developer mode enabled due to system policy configuration"`
- nearest guard branch on `w9`.
- Patch bytes: keystone `nop`.
#### TXM Binary-Alignment Validation
- `patch.upstream.raw` generated from upstream-equivalent TXM static patch semantics.
- `patch.dyn.raw` generated by `TXMJBPatcher` on the same input.
- Result: byte-identical (`cmp -s` success, SHA-256 matched).
### Kernelcache (`kernel_jb.py`)
All 24 kernel JB patch methods are implemented in `scripts/patchers/kernel_jb.py`
with capstone semantic matching and keystone-generated patch bytes only:
**Group A: Core patches**
1. `AMFIIsCDHashInTrustCache` function rewrite
- Locator: semantic function-body matcher in AMFI text.
- Patch: `mov x0,#1 ; cbz x2,+8 ; str x0,[x2] ; ret`.
2. AMFI execve kill path bypass (2 BL sites)
- Locator: string xref to `"AMFI: hook..execve() killing"` (fallback `"execve() killing"`),
then function-local early `bl` + `cbz/cbnz w0` pair matcher.
- Patch: `bl -> mov x0,#0` at two helper callsites.
3. `task_conversion_eval_internal` guard bypass
- Locator: unique cmp/branch motif:
`ldr xN,[xN,#imm] ; cmp xN,x0 ; b.eq ; cmp xN,x1 ; b.eq`.
- Patch: `cmp xN,x0 -> cmp xzr,xzr`.
4. Extended sandbox MACF hook stubs (25 hooks, JB-only set)
- Locator: dynamic `mac_policy_conf -> mpc_ops` discovery, then hook-index resolution.
- Patch per hook function: `mov x0,#0 ; ret`.
- JB extended indices include vnode/proc hooks beyond base 5 hooks.
**Group B: Simple patches (string-anchored / pattern-matched)**
5. `_postValidation` additional CMP bypass
6. `_proc_security_policy` stub (mov x0,#0; ret)
7. `_proc_pidinfo` pid-0 guard NOP (2 sites)
8. `_convert_port_to_map_with_flavor` panic skip
9. `_vm_fault_enter_prepare` PMAP check NOP
10. `_vm_map_protect` permission check skip
11. `___mac_mount` MAC check bypass (NOP + mov x8,xzr)
12. `_dounmount` MAC check NOP
13. `_bsd_init` auth bypass (mov x0,#0)
14. `_spawn_validate_persona` NOP (2 sites)
15. `_task_for_pid` proc_ro security copy NOP
16. `_load_dylinker` PAC rebase bypass
17. `_shared_region_map_and_slide_setup` force (cmp x0,x0)
18. `_verifyPermission` (NVRAM) NOP
19. `_IOSecureBSDRoot` check skip
20. `_thid_should_crash` zero out
**Group C: Complex shellcode patches**
21. `_cred_label_update_execve` cs_flags shellcode
22. `_syscallmask_apply_to_proc` filter mask shellcode
23. `_hook_cred_label_update_execve` ops table + vnode_getattr shellcode
24. `kcall10` syscall 439 replacement shellcode
## Cross-Version Dynamic Snapshot
Validated using pristine inputs from `updates-cdn/`:
| Case | TXM_JB_PATCHES | KERNEL_JB_PATCHES |
| ------------------- | -------------: | ----------------: |
| PCC 26.1 (`23B85`) | 14 | 59 |
| PCC 26.3 (`23D128`) | 14 | 59 |
| iOS 26.1 (`23B85`) | 14 | 59 |
| iOS 26.3 (`23D127`) | 14 | 59 |
> Note: These emit counts were captured at validation time and may differ from
> the current source if methods were subsequently refactored. The TXM JB patcher
> currently has 5 methods emitting 11 patches; the kernel JB patcher has 24
> methods. Actual emit counts depend on how many dynamic targets resolve per binary.
All patches are applied dynamically via string anchors, instruction patterns, and cross-reference analysis — no hardcoded offsets — ensuring portability across iOS versions.

711
research/txm_jb_patches.md Normal file
View File

@@ -0,0 +1,711 @@
# TXM Jailbreak Patch Analysis
Analysis of 6 logical TXM jailbreak patches (11 instruction modifications) applied by `txm_jb.py` on the RESEARCH variant
of TXM from iPhone17,3 / PCC-CloudOS 26.x.
## TXM Execution Model
TXM runs at a guested exception level (GL) under SPTM's supervision:
```
SPTM (GL2) — Secure Page Table Monitor
↕ svc #0
TXM (GL1) — Trusted Execution Monitor
↕ trap
Kernel (EL1/GL0)
```
SPTM dispatches selector calls into TXM. TXM **cannot** execute SPTM code
(instruction fetch permission fault). TXM must return to SPTM via `svc #0`.
---
## Return Path: TXM → SPTM
All TXM functions return through this chain:
```
TXM function
→ bl return_helper (0x26c04)
→ bl return_trap_stub (0x49b40)
→ movk x16, ... (set SPTM return code)
→ b trampoline (0x60000)
→ pacibsp
→ svc #0 ← traps to SPTM
→ retab ← resumes after SPTM returns
```
### Trampoline at 0x60000 (`__TEXT_BOOT_EXEC`)
```asm
0x060000: pacibsp
0x060004: svc #0 ; supervisor call → SPTM handles the trap
0x060008: retab ; return after SPTM gives control back
```
### Return Trap Stub at 0x49B40 (`__TEXT_EXEC`)
```asm
0x049B40: bti c
0x049B44: movk x16, #0, lsl #48
0x049B48: movk x16, #0xfd, lsl #32 ; x16 = 0x000000FD00000000
0x049B4C: movk x16, #0, lsl #16 ; (SPTM return code identifier)
0x049B50: movk x16, #0
0x049B54: b #0x60000 ; → trampoline → svc #0
```
x16 carries a return code that SPTM uses to identify which TXM operation completed.
### Return Helper at 0x26C04 (`__TEXT_EXEC`)
```asm
0x026C04: pacibsp
0x026C08: stp x20, x19, [sp, #-0x20]!
0x026C0C: stp x29, x30, [sp, #0x10]
0x026C10: add x29, sp, #0x10
0x026C14: mov x19, x0 ; save result code
0x026C18: bl #0x29024 ; get TXM context
0x026C1C: ldrb w8, [x0] ; check context flag
0x026C20: cbz w8, #0x26c30
0x026C24: mov x20, x0
0x026C28: bl #0x29010 ; cleanup if flag set
0x026C2C: strb wzr, [x20, #0x58]
0x026C30: mov x0, x19 ; restore result
0x026C34: bl #0x49b40 ; → svc #0 → SPTM
```
---
## Error Handler
### Error Handler at 0x25924
```asm
0x025924: pacibsp
...
0x025978: stp x19, x20, [sp] ; x19=error_code, x20=param
0x02597C: adrp x0, #0x1000
0x025980: add x0, x0, #0x8d8 ; format string
0x025984: bl #0x25744 ; log error → eventually svc #0
```
### Panic Format
`TXM [Error]: CodeSignature: selector: 24 | 0xA1 | 0x30 | 1`
This is a **kernel-side** message (not in TXM binary). The kernel receives the
non-zero return from the `svc #0` trap and formats the error:
`selector: <selector_num> | <low_byte> | <mid_byte> | <high_byte>`
For `0x000130A1`: low=`0xA1`, mid=`0x30`, high=`0x1``| 0xA1 | 0x30 | 1`
---
## Why `ret`/`retab` Fails
> **Historical reference:** These three failed attempts document why patching TXM
> functions to return early via normal return instructions does not work. The only
> viable path is to let the function proceed through the normal `svc #0` return chain
> (see [Return Path](#return-path-txm--sptm) above).
### Attempt 1: `mov x0, #0; retab` replacing PACIBSP
```
0x026C80: mov x0, #0 ; (was pacibsp)
0x026C84: retab ; verify PAC on LR → FAIL
```
**Result**: `[TXM] Unhandled synchronous exception at pc 0x...6C84`
RETAB tries to verify the PAC signature on LR. Since PACIBSP was replaced,
LR was never signed. RETAB detects the invalid PAC → exception.
### Attempt 2: `mov x0, #0; ret` replacing PACIBSP
```
0x026C80: mov x0, #0 ; (was pacibsp)
0x026C84: ret ; jump to LR (SPTM address)
```
**Result**: `[TXM] Unhandled synchronous exception at pc 0x...FA88` (SPTM space)
`ret` strips PAC and jumps to clean LR, which points to SPTM code (the caller).
TXM cannot execute SPTM code → **instruction fetch permission fault** (ESR EC=0x20, IFSC=0xF).
### Attempt 3: `pacibsp; mov x0, #0; retab`
```
0x026C80: pacibsp ; signs LR correctly
0x026C84: mov x0, #0
0x026C88: retab ; verifies PAC (OK), jumps to LR (SPTM address)
```
**Result**: Same permission fault — RETAB succeeds (PAC valid), but the return
address is in SPTM space. TXM still cannot execute there.
**Conclusion**: No form of `ret`/`retab` works because the **caller is SPTM**
and TXM cannot return to SPTM via normal returns. The only way back is `svc #0`.
---
## Address Mapping
| Segment | VM Address | File Offset | Size |
| ------------------ | -------------------- | ----------- | --------- |
| `__TEXT_EXEC` | `0xFFFFFFF017020000` | `0x1c000` | `0x44000` |
| `__TEXT_BOOT_EXEC` | `0xFFFFFFF017064000` | `0x60000` | `0xc000` |
Conversion: `VA = file_offset - 0x1c000 + 0xFFFFFFF017020000` (for `__TEXT_EXEC`)
---
## TXM Selector Dispatch
All TXM operations enter through a single dispatch function (`sub_FFFFFFF01702AE80`),
a large switch on the selector number (1-51). Each case validates arguments and calls
a dedicated handler. Relevant selectors:
| Selector | Handler | Purpose |
| -------- | ----------------------------------------- | ------------------------------------------------- |
| 24 | `sub_FFFFFFF017024834` → validation chain | CodeSignature validation |
| 41 | `sub_FFFFFFF017023558` | Process entitlement setup (get-task-allow) |
| 42 | `sub_FFFFFFF017023368` | Debug memory mapping |
| --- | `sub_FFFFFFF017023A20` | Developer mode configuration (called during init) |
The dispatcher passes raw page pointers through `sub_FFFFFFF0170280A4` (a bounds
validator that returns the input pointer unchanged) before calling handlers.
---
## Patch 1-2: CodeSignature Hash Comparison Bypass (selector 24)
**Error**: `TXM [Error]: CodeSignature: selector: 24 | 0xA1 | 0x30 | 1`
### Addresses
| File Offset | VA | Original Instruction | Patch |
| ----------- | -------------------- | ------------------------- | ----- |
| `0x313ec` | `0xFFFFFFF0170353EC` | `LDR X1, [X20, #0x38]` | NOP |
| `0x313f4` | `0xFFFFFFF0170353F4` | `BL sub_FFFFFFF0170335F8` | NOP |
### selector24 Full Control Flow (0x026C80 - 0x026E7C)
The selector 24 handler is the entry point for all CodeSignature validation.
Understanding its full control flow is essential context for why the NOP approach
was chosen over earlier redirect-based patches.
```
0x026C80: pacibsp ; prologue
0x026C84: sub sp, sp, #0x70
0x026C88-0x026C98: save x19-x30, setup fp
0x026CA0-0x026CB4: save args to x20-x25
0x026CB8: bl #0x29024 ; get TXM context → x0
0x026CBC: adrp x8, #0x6c000 ; flag address (page)
0x026CC0: add x8, x8, #0x5c0 ; flag address (offset)
0x026CC4: ldrb w8, [x8] ; flag check
0x026CC8: cbnz w8, #0x26cfc ; if flag → error 0xA0
0x026CCC: mov x19, x0 ; save context
0x026CD4: cmp w25, #2 ; switch on sub-selector (arg0)
b.gt → check 3,4,5
0x026CDC: cbz w25 → case 0
0x026CE4: b.eq → case 1
0x026CEC: b.ne → default (0xA1 error)
case 0: setup, b 0x26dc0
case 1: flag check, setup, b 0x26dfc
case 2: bl 0x1e0e8, b 0x26db8
case 3: bl 0x1e148, b 0x26db8
case 4: bl 0x1e568, b 0x26db8
case 5: flag → { mov x0,#0; b 0x26db8 } or { bl 0x1e70c; b 0x26db8 }
default: mov w0, #0xa1; b 0x26d00 (error path)
0x026DB8: and w8, w0, #0xffff ; result processing
0x026DBC: and x9, x0, #0xffffffffffff0000
0x026DC0: mov w10, w8
0x026DC4: orr x9, x9, x10
0x026DC8: str x9, [x19, #8] ; store result to context
0x026DCC: cmp w8, #0
0x026DD0: csetm x0, ne ; x0 = 0 (success) or -1 (error)
0x026DD4: bl #0x26c04 ; return via svc #0 ← SUCCESS RETURN
ERROR PATH:
0x026D00: mov w1, #0
0x026D04: bl #0x25924 ; error handler → svc #0
```
#### Existing Success Path
The function already has a success path at `0x026D30` (reached by case 5 when flag is set):
```asm
0x026D30: mov x0, #0 ; success result
0x026D34: b #0x26db8 ; → process result → str [x19,#8] → bl return_helper
```
> **Historical note:** An earlier approach tried redirecting to this success path by
> patching 2 instructions at `0x26CBC` (`mov x19, x0` / `b #0x26D30`). This was
> replaced with the more surgical NOP approach below because the redirect did not
> properly handle the hash validation return value.
#### Error Codes
| Return Value | Meaning |
| ------------ | ----------------------------------------- |
| `0x00` | Success (only via case 5 flag path) |
| `0xA0` | Early flag check failure |
| `0xA1` | Unknown sub-selector / validation failure |
| `0x130A1` | Hash mismatch (hash presence != flag) |
| `0x22DA1` | Version-dependent validation failure |
### Function: `sub_FFFFFFF0170353B8` --- CS hash flags validator
**Call chain**: selector 24 → `sub_FFFFFFF017024834` (CS handler) →
`sub_FFFFFFF0170356F8` (CS validation pipeline) → `sub_FFFFFFF017035A00`
(multi-step validation, step 4 of 8) → `sub_FFFFFFF0170353B8`
### Decompiled (pre-patch)
```c
// sub_FFFFFFF0170353B8(manifest_ptr, version)
__int64 __fastcall sub_FFFFFFF0170353B8(__int64 **a1, unsigned int a2)
{
__int64 v4 = **a1;
__int64 v7 = 0; // hash data pointer
int v6 = 0; // hash flags
// Patch 1: NOP removes arg load (LDR X1, [X20, #0x38])
// Patch 2: NOP removes this call entirely:
sub_FFFFFFF0170335F8(a1[6], a1[7], &v6); // extract hash flags from CS blob
sub_FFFFFFF017033718(a1[6], a1[7], &v7); // extract hash data pointer
if ( a2 >= 6 && *(v4 + 8) )
return 0xA1; // 161
// Critical comparison: does hash presence match flags?
if ( (v7 != 0) == ((v6 & 2) >> 1) )
return 0x130A1; // 77985 — hash mismatch
// ... further version-dependent checks return 0xA1 or 0x22DA1
}
```
### What `sub_FFFFFFF0170335F8` does
Extracts hash flags from the CodeSignature blob header. Reads `bswap32(*(blob + 12))`
into the output parameter (the flags bitmask). Bit 1 of the flags indicates whether
a code hash is present.
### What `sub_FFFFFFF017033718` does
Locates the hash data within the CodeSignature blob. Validates blob header version
(`bswap32(*(blob+8)) >> 9 >= 0x101`), then follows a length-prefixed string pointer
at offset 48 to find the hash data. Returns the hash data pointer via output param.
### Effect of NOP
With `sub_FFFFFFF0170335F8` NOPed, `v6` stays at its initialized value of **0**.
This means `(v6 & 2) >> 1 = 0` (hash-present flag is cleared). As long as
`sub_FFFFFFF017033718` returns a non-null hash pointer (`v7 != 0`), the comparison
becomes `(1 == 0)`**false**, so the `0x130A1` error is skipped. The function
falls through to the version checks which return success for version <= 5.
This effectively bypasses CodeSignature hash validation --- the hash data exists
in the blob but the hash-present flag is suppressed, so the consistency check passes.
### `txm_jb.py` dynamic finder: `patch_selector24_hash_extraction_nop()`
Scans for `mov w0, #0xa1` as a unique anchor to locate the CS hash validator function,
finds PACIBSP to determine function start, then matches the pattern
`LDR X1,[Xn,#0x38]` / `ADD X2,...` / `BL` / `LDP` within it. NOPs the `LDR X1`
(arg setup) and the `BL hash_flags_extract` (call).
### UUID Canary Verification
To confirm which TXM variant is loaded during boot, XOR the last byte of `LC_UUID`:
| | UUID |
| -------------------------- | -------------------------------------- |
| Original research | `0FFA437D-376F-3F8E-AD26-317E2111655D` |
| Original release | `3C1E0E65-BFE2-3113-9C65-D25926C742B4` |
| Canary (research XOR 0x01) | `0FFA437D-376F-3F8E-AD26-317E2111655C` |
Panic log `TXM UUID:` line confirmed canary `...655C`**patched research TXM IS loaded**.
The problem was exclusively in the selector24 patch logic (the earlier redirect approach
did not properly handle the hash validation return value).
---
## Patch 3: get-task-allow Force True (selector 41)
**Error**: `TXM [Error]: selector: 41 | 29`
### Address
| File Offset | VA | Original Instruction | Patch |
| ----------- | -------------------- | ------------------------- | ------------ |
| `0x1f5d4` | `0xFFFFFFF0170235D4` | `BL sub_FFFFFFF017022A30` | `MOV X0, #1` |
### Function: `sub_FFFFFFF017023558` --- selector 41 handler
**Call chain**: selector 41 → `sub_FFFFFFF0170280A4` (ptr validation) →
`sub_FFFFFFF017023558`
### Decompiled (pre-patch)
```c
// sub_FFFFFFF017023558(manifest)
__int64 __fastcall sub_FFFFFFF017023558(__int64 a1)
{
// Check developer mode is enabled (byte_FFFFFFF017070F24)
if ( (byte_FFFFFFF017070F24 & 1) == 0 )
return 27; // developer mode not enabled
// Check license-to-operate entitlement (always first)
sub_FFFFFFF017022A30(0, "research.com.apple.license-to-operate", 0);
// Lock manifest
sub_FFFFFFF017027074(a1, 0, 0);
if ( *(a1 + 36) == 1 ) // special manifest type
goto error_path; // return via panic(0x81)
// === PATCHED INSTRUCTION ===
// Original: BL sub_FFFFFFF017022A30 — entitlement_lookup(manifest, "get-task-allow", 0)
// Patched: MOV X0, #1
if ( (sub_FFFFFFF017022A30(a1, "get-task-allow", 0) & 1) != 0 ) // TBNZ w0, #0
{
v3 = 0; // success
*(a1 + 0x30) = 1; // set get-task-allow flag on manifest
}
else
{
v3 = 29; // ERROR 29: no get-task-allow entitlement
}
sub_FFFFFFF01702717C(a1, 0); // unlock manifest
return v3;
}
```
### Assembly at patch site
```asm
FFFFFFF0170235C4 ADRL X1, "get-task-allow"
FFFFFFF0170235CC MOV X0, X19 ; manifest object
FFFFFFF0170235D0 MOV X2, #0
FFFFFFF0170235D4 BL sub_FFFFFFF017022A30 ; <-- PATCHED to MOV X0, #1
FFFFFFF0170235D8 TBNZ W0, #0, loc_... ; always taken when x0=1
```
### Effect
Replaces the entitlement lookup call with a constant `1`. The subsequent `TBNZ W0, #0`
always takes the branch to the success path, which sets `*(manifest + 0x30) = 1`
(the get-task-allow flag byte). Every process now has get-task-allow, enabling
debugging via `task_for_pid` and LLDB attach.
### What `sub_FFFFFFF017022A30` does
Universal entitlement lookup function. When `a1 != 0`, it resolves the manifest's
entitlement dictionary and searches for the named key via `sub_FFFFFFF017036294`.
Returns a composite status word where bit 0 indicates the entitlement was found.
### `txm_jb.py` dynamic finder: `patch_get_task_allow_force_true()`
Searches for string refs to `"get-task-allow"`, then scans forward for the pattern
`BL X / TBNZ w0, #0, Y`. Patches the BL to `MOV X0, #1`.
---
## Patch 4: selector 42|29 Shellcode (Debug Mapping Gate)
**Error**: `TXM [Error]: selector: 42 | 29`
### Addresses
| File Offset | VA | Patch |
| ----------- | -------------------- | -------------------------- |
| `0x2717c` | `0xFFFFFFF01702B17C` | `B #0x36238` (→ shellcode) |
| `0x5d3b4` | `0xFFFFFFF0170613B4` | `NOP` (pad) |
| `0x5d3b8` | `0xFFFFFFF0170613B8` | `MOV X0, #1` |
| `0x5d3bc` | `0xFFFFFFF0170613BC` | `STRB W0, [X20, #0x30]` |
| `0x5d3c0` | `0xFFFFFFF0170613C0` | `MOV X0, X20` |
| `0x5d3c4` | `0xFFFFFFF0170613C4` | `B #-0x36244` (→ 0xB180) |
### Context: Dispatcher case 42
```asm
; jumptable case 42 entry in sub_FFFFFFF01702AE80:
FFFFFFF01702B178 BTI j
FFFFFFF01702B17C MOV X0, X20 ; <-- PATCHED to B shellcode
FFFFFFF01702B180 BL sub_FFFFFFF0170280A4 ; validate pointer
FFFFFFF01702B184 MOV X1, X21
FFFFFFF01702B188 MOV X2, X22
FFFFFFF01702B18C BL sub_FFFFFFF017023368 ; selector 42 handler
FFFFFFF01702B190 B loc_FFFFFFF01702B344 ; return result
```
### Shellcode (at zero-filled code cave in `__TEXT_EXEC`)
```asm
; 0xFFFFFFF0170613B4 — cave was all zeros
NOP ; pad (original 0x00000000)
MOV X0, #1 ; value to store
STRB W0, [X20, #0x30] ; force manifest->get_task_allow = 1
MOV X0, X20 ; restore original instruction (was at 0xB17C)
B #-0x36244 ; jump back to 0xFFFFFFF01702B180 (BL validate)
```
### Why this is needed
Selector 42's handler `sub_FFFFFFF017023368` checks the get-task-allow byte early:
```c
// sub_FFFFFFF017023368(manifest, addr, size)
// ... after debugger entitlement check ...
v8 = atomic_load((unsigned __int8 *)(a1 + 48)); // offset 0x30
if ( (v8 & 1) == 0 )
{
v6 = 29; // ERROR 29: get-task-allow not set
goto unlock_and_return;
}
// ... proceed with debug memory mapping ...
```
Selector 41 (patch 3) sets this byte during entitlement validation, but
there are code paths where selector 42 can be called before selector 41 has run
for a given manifest. The shellcode ensures the flag is always set at the dispatch
level before the handler even sees it.
### `sub_FFFFFFF0170280A4` --- pointer validator
```c
// Validates page alignment and bounds, returns input pointer unchanged
unsigned __int64 sub_FFFFFFF0170280A4(unsigned __int64 a1) {
if ( (a1 & ~0x3FFF) == 0 ) panic(64);
if ( a1 >= 0xFFFFFFFFFFFFC000 ) panic(66);
// ... bounds checks ...
return (a1 & ~0x3FFF) + (a1 & 0x3FFF); // == a1
}
```
Since the validator returns the pointer unchanged, `x20` (raw arg) and the validated
pointer both refer to the same object. The shellcode's `STRB W0, [X20, #0x30]`
writes to the correct location.
### `txm_jb.py` dynamic finder: `patch_selector42_29_shellcode()`
1. Finds the "debugger gate function" via string refs to `"com.apple.private.cs.debugger"`
2. Locates the dispatch stub by matching `BTI j / MOV X0, X20 / BL / MOV X1, X21 / MOV X2, X22 / BL debugger_gate / B`
3. Finds a zero-filled code cave via `_find_udf_cave()` near the stub
4. Emits the branch + shellcode + branch-back
---
## Patch 5: Debugger Entitlement Force True (selector 42)
**Error**: `TXM [Error]: selector: 42 | 37`
### Address
| File Offset | VA | Original Instruction | Patch |
| ----------- | -------------------- | ------------------------- | ------------ |
| `0x1f3b8` | `0xFFFFFFF0170233B8` | `BL sub_FFFFFFF017022A30` | `MOV W0, #1` |
### Function: `sub_FFFFFFF017023368` --- selector 42 handler (debug memory mapping)
### Assembly at patch site
```asm
; Check com.apple.private.cs.debugger entitlement
FFFFFFF0170233A8 ADRL X1, "com.apple.private.cs.debugger"
FFFFFFF0170233B0 MOV X0, #0 ; check global manifest (a1=0)
FFFFFFF0170233B4 MOV X2, #0
FFFFFFF0170233B8 BL sub_FFFFFFF017022A30 ; <-- PATCHED to MOV W0, #1
FFFFFFF0170233BC TBNZ W0, #0, loc_... ; always taken when w0=1
FFFFFFF0170233C0 ADRL X8, fallback_flag ; secondary check (also bypassed)
FFFFFFF0170233C8 LDRB W8, [X8, #offset]
FFFFFFF0170233CC TBNZ W8, #0, loc_... ; secondary bypass path
FFFFFFF0170233D0 ADRL X0, "disallowed non-debugger initiated debug mapping"
FFFFFFF0170233D8 BL sub_FFFFFFF017025B7C ; log error
FFFFFFF0170233DC MOV W20, #0x25 ; error 37
FFFFFFF0170233E0 B unlock_return
```
### Decompiled (pre-patch)
```c
// First check in sub_FFFFFFF017023368 after input validation:
if ( (sub_FFFFFFF017022A30(0, "com.apple.private.cs.debugger", 0) & 1) == 0 )
{
// Fallback: check a static byte flag
if ( (fallback_flag & 1) == 0 )
{
log("disallowed non-debugger initiated debug mapping");
return 37; // 0x25
}
}
// Continue with debug mapping...
```
### Effect
Replaces the entitlement lookup with `MOV W0, #1`. The `TBNZ W0, #0` always
branches to the success path, bypassing both the entitlement check and the
fallback flag check. This allows any process to create debug memory mappings
regardless of whether it has `com.apple.private.cs.debugger`.
### `txm_jb.py` dynamic finder: `patch_debugger_entitlement_force_true()`
Searches for string refs to `"com.apple.private.cs.debugger"`, then matches
the pattern: `mov x0, #0 / mov x2, #0 / bl X / tbnz w0, #0, Y`. Patches the BL
to `MOV W0, #1`.
---
## Patch 6: Developer Mode Bypass
### Address
| File Offset | VA | Original Instruction | Patch |
| ----------- | -------------------- | ----------------------------------- | ----- |
| `0x1FA58` | `0xFFFFFFF017023A58` | `TBNZ W9, #0, loc_FFFFFFF017023A6C` | NOP |
### Function: `sub_FFFFFFF017023A20` --- developer mode configuration
Called during TXM initialization to determine and store the developer mode state.
The result is stored in `byte_FFFFFFF017070F24`, which is the gate flag checked by
selector 41 (`sub_FFFFFFF017023558`).
### Assembly at patch site
```asm
; Check system policy configuration
FFFFFFF017023A50 LDR X9, [X8, #off_FFFFFFF0170146C0]
FFFFFFF017023A54 LDRB W9, [X9, #0x4D] ; load system policy byte
FFFFFFF017023A58 TBNZ W9, #0, loc_FFFFFFF017023A6C ; <-- PATCHED to NOP
; Fall through to force-enable:
FFFFFFF017023A5C MOV W20, #1 ; developer_mode = ENABLED
FFFFFFF017023A60 ADRL X0, "developer mode enabled due to system policy configuration"
FFFFFFF017023A68 B log_and_store
```
### Decompiled (pre-patch)
```c
__int64 sub_FFFFFFF017023A20(__int64 manifest)
{
char devmode;
// Check 1: PCC research variant flag
if ( pcc_research_flag )
{
devmode = 1;
goto apply;
}
// Check 2: System policy (patched here)
byte policy = *(system_config_ptr + 0x4D);
if ( (policy & 1) != 0 ) // <-- TBNZ jumps past force-enable
goto normal_path; // to xART / user-config checks
// Force-enable path (reached by NOPing the TBNZ):
devmode = 1;
log("developer mode enabled due to system policy configuration");
goto apply;
normal_path:
// ... xART availability check ...
// ... user configuration check ...
// May set devmode = 0 (disabled) based on config
apply:
byte_FFFFFFF017070F24 = devmode; // global developer mode state
return result;
}
```
### Effect
NOPing the `TBNZ` makes execution always fall through to `MOV W20, #1`, forcing
developer mode enabled regardless of the system policy byte. Without this:
- The `TBNZ` would jump to `loc_FFFFFFF017023A6C` (the normal path)
- The normal path checks xART availability, device tree flags, and user configuration
- On PCC VMs, this can result in developer mode being **disabled**
Developer mode is a **prerequisite** for selectors 41 and 42 --- the selector 41
handler returns error 27 immediately if `byte_FFFFFFF017070F24` is not set:
```c
// In sub_FFFFFFF017023558 (selector 41):
if ( (byte_FFFFFFF017070F24 & 1) == 0 )
return 27; // developer mode not enabled
```
### `txm_jb.py` dynamic finder: `patch_developer_mode_bypass()`
Searches for string refs to `"developer mode enabled due to system policy
configuration"`, then scans backwards for a `tbz/tbnz/cbz/cbnz` instruction
matching `w9, #0`. NOPs it.
---
## Patch Dependency Chain
The patches have a logical ordering --- later patches depend on earlier ones:
```
Patch 6: Developer Mode Bypass
| Forces byte_FFFFFFF017070F24 = 1
|
|---> Patch 3: get-task-allow Force True (selector 41)
| Requires developer mode (checks byte_FFFFFFF017070F24)
| Forces manifest[0x30] = 1
|
|---> Patch 4: selector 42|29 Shellcode
| Forces manifest[0x30] = 1 at dispatch level
| Safety net for Patch 3 (covers cases where sel 42 runs before sel 41)
|
|---> Patch 5: Debugger Entitlement Force True (selector 42)
| Bypasses com.apple.private.cs.debugger check
| Allows debug memory mapping for all processes
|
└---> Patches 1-2: CodeSignature Hash Bypass (selector 24)
Independent — bypasses CS hash validation in the signature chain
```
### Boot-time flow
1. TXM initializes → `sub_FFFFFFF017023A20` runs → **Patch 6** forces devmode ON
2. Process loads → selector 24 validates CodeSignature → **Patches 1-2** skip hash check
3. Process requests entitlements → selector 41 → **Patch 3** grants get-task-allow
4. Debugger attaches → selector 42 → **Patch 4** pre-sets flag + **Patch 5** grants debugger ent
5. Debug mapping succeeds → LLDB can attach to any process
---
## Summary Table
| # | File Offset | VA | Function | Patch | Purpose |
| --- | ----------- | -------------------- | -------------------------------------------- | ---------------------- | --------------------------------- |
| 1 | `0x313ec` | `0xFFFFFFF0170353EC` | `sub_FFFFFFF0170353B8` (CS hash validator) | NOP | Remove hash flag load |
| 2 | `0x313f4` | `0xFFFFFFF0170353F4` | `sub_FFFFFFF0170353B8` (CS hash validator) | NOP | Skip hash flag extraction call |
| 3 | `0x1f5d4` | `0xFFFFFFF0170235D4` | `sub_FFFFFFF017023558` (selector 41) | `MOV X0, #1` | Force get-task-allow = true |
| 4 | `0x2717c` | `0xFFFFFFF01702B17C` | `sub_FFFFFFF01702AE80` (dispatcher, case 42) | `B shellcode` | Redirect to shellcode cave |
| 4a | `0x5d3b4` | `0xFFFFFFF0170613B4` | code cave (zeros) | `NOP` | Shellcode padding |
| 4b | `0x5d3b8` | `0xFFFFFFF0170613B8` | code cave | `MOV X0, #1` | Set value for flag |
| 4c | `0x5d3bc` | `0xFFFFFFF0170613BC` | code cave | `STRB W0, [X20,#0x30]` | Force get-task-allow flag |
| 4d | `0x5d3c0` | `0xFFFFFFF0170613C0` | code cave | `MOV X0, X20` | Restore original instruction |
| 4e | `0x5d3c4` | `0xFFFFFFF0170613C4` | code cave | `B back` | Return to dispatcher |
| 5 | `0x1f3b8` | `0xFFFFFFF0170233B8` | `sub_FFFFFFF017023368` (selector 42) | `MOV W0, #1` | Force debugger entitlement = true |
| 6 | `0x1FA58` | `0xFFFFFFF017023A58` | `sub_FFFFFFF017023A20` (devmode init) | NOP | Force developer mode ON |
**Total**: 6 logical patches, 11 instruction modifications (counting shellcode), enabling:
- CodeSignature bypass (patches 1-2)
- Universal get-task-allow (patches 3-4)
- Universal debugger entitlement (patch 5)
- Forced developer mode (patch 6)

View File

@@ -0,0 +1,156 @@
# TXM Variant Analysis: release vs research
Analysis of TXM (Trusted Execution Monitor) variants from iPhone17,3 26.3 (23D127)
and PCC-CloudOS 26.3 (23D128) IPSWs.
## Source Files
| Source | Variant | IM4P Size | SHA256 |
| ------- | -------- | --------- | --------------------- |
| cloudos | release | 161025 | `3453eb476cfb53d8...` |
| cloudos | research | 161028 | `93ad9e382d8c6353...` |
| iphone | release | 161025 | `3453eb476cfb53d8...` |
| iphone | research | 161028 | `93ad9e382d8c6353...` |
**Key finding:** Both IPSWs contain identical TXM files (same SHA256).
The TXM binary is shared across iPhone and cloudOS IPSWs.
## Decompressed Binary Overview
| Property | RELEASE | RESEARCH |
| ----------------- | --------------------- | --------------------- |
| Compressed size | 160726 bytes | 160729 bytes |
| Decompressed size | 458784 bytes | 458784 bytes |
| Compression | BVX2 (LZFSE) | BVX2 (LZFSE) |
| Format | Mach-O 64-bit ARM64 | Mach-O 64-bit ARM64 |
| SHA256 | `bfc493e3c7b7dc00...` | `62f40b9cd32a2a03...` |
| File type | 2 (MH_EXECUTE) | 2 (MH_EXECUTE) |
| Load commands | 11 | 11 |
| Flags | `0x00200001` | `0x00200001` |
## Mach-O Segments
Both variants have identical segment layout:
| Segment | VM Address | VM Size | File Offset | File Size |
| ------------------ | -------------------- | --------- | ----------- | --------- |
| `__TEXT` | `0xfffffff017004000` | `0x10000` | `0x0` | `0x10000` |
| `__DATA_CONST` | `0xfffffff017014000` | `0xc000` | `0x10000` | `0xc000` |
| `__TEXT_EXEC` | `0xfffffff017020000` | `0x44000` | `0x1c000` | `0x44000` |
| `__TEXT_BOOT_EXEC` | `0xfffffff017064000` | `0xc000` | `0x60000` | `0xc000` |
| `__DATA` | `0xfffffff017070000` | `0x4000` | `0x6c000` | `0x4000` |
| `__LINKEDIT` | `0xfffffff017074000` | `0x4000` | `0x70000` | `0x20` |
Segment layout identical: **True**
## Diff Summary
- Total differing bytes: **3358** / 458784 (0.73%)
- Diff regions (16-byte merge gap): **87**
### Diffs by Segment
| Segment | Regions | Bytes Changed | % of Segment |
| ------------- | ------- | ------------- | ------------ |
| `__TEXT` | 3 | 3304 | 5.04% |
| `__TEXT_EXEC` | 84 | 409 | 0.15% |
## Diff Classification
### 1. Build Identifier String (Primary Difference)
The largest diff region (`0x17c5` - `0x2496`, 3282 bytes) is in the `__TEXT` segment
string/const data area. The key difference is the build variant identifier:
| Offset | RELEASE | RESEARCH |
| -------- | ------------------------------------------------ | ------------------------------------------------- |
| `0x17c5` | `lease.TrustedExecutionMonitor_Guarded-182.40.3` | `search.TrustedExecutionMonitor_Guarded-182.40.3` |
| `0xcb7f` | `lease` | `search` |
Full build string:
- **RELEASE:** `release.TrustedExecutionMonitor_Guarded-182.40.3`
- **RESEARCH:** `research.TrustedExecutionMonitor_Guarded-182.40.3`
Because `"research"` (8 chars) is 1 byte longer than `"release"` (7 chars),
all subsequent strings in `__TEXT` are shifted by +1 byte,
causing a cascade of instruction-level diffs in code that references these strings.
### 2. String Reference Adjustments (Code Diffs)
The remaining diffs are in `__TEXT_EXEC` — all `ADD` instruction immediate adjustments
compensating for the 1-byte string shift:
```
RELEASE: add x8, x8, #0x822 ; points to string at original offset
RESEARCH: add x8, x8, #0x823 ; points to same string, shifted +1
```
- ADD immediate adjustments: **84** regions (all in `__TEXT_EXEC`)
- Other code diffs: **0** regions
- String data regions: **3** regions in `__TEXT` (3304 bytes total)
Sample code diffs (first 10):
| Offset | RELEASE instruction | RESEARCH instruction |
| --------- | -------------------- | -------------------- |
| `0x2572c` | `add x8, x8, #0x822` | `add x8, x8, #0x823` |
| `0x25794` | `add x8, x8, #0x861` | `add x8, x8, #0x862` |
| `0x257d8` | `add x0, x0, #0x877` | `add x0, x0, #0x878` |
| `0x25980` | `add x0, x0, #0x8d7` | `add x0, x0, #0x8d8` |
| `0x25ac8` | `add x0, x0, #0x8a1` | `add x0, x0, #0x8a2` |
| `0x25af0` | `add x4, x4, #0x8eb` | `add x4, x4, #0x8ec` |
| `0x25b78` | `add x0, x0, #0x8f9` | `add x0, x0, #0x8fa` |
| `0x25c34` | `add x2, x2, #0x911` | `add x2, x2, #0x912` |
| `0x25c58` | `add x2, x2, #0x919` | `add x2, x2, #0x91a` |
| `0x25c98` | `add x0, x0, #0x927` | `add x0, x0, #0x928` |
### 3. Functional Differences
**None.** All code diffs are string pointer adjustments caused by the 1-byte
shift from `"release"` to `"research"`. The two variants are **functionally
identical** — same logic, same security policies, same code paths.
## Security-Relevant Strings
Both variants contain identical security-relevant strings:
| Offset | String |
| -------- | --------------------------------- |
| `0xd31` | `restricted execution mode` |
| `0x1919` | `debug-enabled` |
| `0x1a4e` | `darwinos-security-environment` |
| `0x1ad0` | `security-mode-change-enable` |
| `0x1b4b` | `amfi-only-platform-code` |
| `0x1bd6` | `research-enabled` |
| `0x1c4c` | `sec-research-device-erm-enabled` |
| `0x1cca` | `vmm-present` |
| `0x1d33` | `sepfw-load-at-boot` |
| `0x1de8` | `sepfw-never-boot` |
| `0x1e85` | `osenvironment` |
| `0x1ec4` | `device-recovery` |
| `0x1f81` | `TrustCache` |
| `0x202a` | `iboot-build-variant` |
| `0x20a9` | `development` |
| `0x23da` | `image4 dispatch` |
## Implications for Patching
1. **Either variant works** — the code is functionally identical.
2. **`fw_patch.py` uses the research variant** (`txm.iphoneos.research.im4p`)
because the `iboot-build-variant` device tree property in PCC VMs is set to
`"research"`, and TXM validates this matches its own embedded variant string.
3. **String-based patch anchors** that reference the build variant string
(`"release"` / `"research"`) will match at different offsets — patchers should
use variant-agnostic anchors (e.g., `mov w19, #0x2446` as in `txm.py`).
4. **The 3-byte IM4P size difference** (161025 vs 161028 bytes) comes from the
extra byte in `"research"` plus LZFSE compression variance.
5. **Both IPSWs ship the same TXM** — no need to prefer one source over the other.
## Conclusion
The TXM `release` and `research` variants are **cosmetically different but
functionally identical**. The only real difference is the embedded build variant
string (`"release"` vs `"research"`), which causes a 1-byte cascade in string
offsets and corresponding `ADD` immediate adjustments in code.
Both IPSWs (iPhone and cloudOS) ship the same pair of TXM binaries.