mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 04:59:05 +08:00
Create txm_return_mechanism.md
This commit is contained in:
121
research/devmode_xpc_protocol.md
Normal file
121
research/devmode_xpc_protocol.md
Normal 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.
|
||||
320
research/firmware_manifest_and_origins.md
Normal file
320
research/firmware_manifest_and_origins.md
Normal 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
549
research/iboot_patches.md
Normal 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).
|
||||
75
research/kernel_fairplay_kexts.md
Normal file
75
research/kernel_fairplay_kexts.md
Normal 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 2008–2019 — 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.
|
||||
461
research/kernel_jb_patch_notes.md
Normal file
461
research/kernel_jb_patch_notes.md
Normal 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 |
|
||||
137
research/kernel_patcher_verification.md
Normal file
137
research/kernel_patcher_verification.md
Normal 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.
|
||||
251
research/keyboard_event_pipeline.md
Normal file
251
research/keyboard_event_pipeline.md
Normal 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 |
|
||||
285
research/patch_comparison_all_variants.md
Normal file
285
research/patch_comparison_all_variants.md
Normal 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 |
|
||||
| 4–5 | `mov w0,#0; ret` | `_proc_check_launch_constraints` | Bypass launch constraints | Y | Y | Y |
|
||||
| 6–7 | `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 |
|
||||
| 10–11 | `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 |
|
||||
| 16–25 | `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
711
research/txm_jb_patches.md
Normal 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)
|
||||
156
research/txm_variant_diff.md
Normal file
156
research/txm_variant_diff.md
Normal 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.
|
||||
Reference in New Issue
Block a user