Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@
- **ANGLE Metal PBO bug (CVE-2025-14174)**: The Metal backend allocates the PBO staging buffer using `UNPACK_IMAGE_HEIGHT` instead of the real texture height. Supplying a tiny unpack height then issuing a large `texImage2D` causes a **staging-buffer OOB write** (~240KB in the PoC below).<sup>[[1]](#references)</sup>
- **PAC blockers on arm64e (iOS 26.1)**: TypedArray `m_vector` and JSArray `butterfly` are PAC-signed; forging fake objects with attacker-chosen pointers crashes with `EXC_BAD_ACCESS`/`EXC_ARM_PAC`. Only reusing **already-signed** butterflies (boxed/unboxed reinterpretation) works.<sup>[[1]](#references)</sup><sup>[[2]](#references)</sup>

## Renderer-to-kernel process chain

A browser memory primitive is only the first boundary. Safari executes page JavaScript in the tightly sandboxed `com.apple.WebKit.WebContent` process, while WebGL work is decoded in the more privileged `com.apple.WebKit.GPU` process. DarkSword composed the following reusable sequence; the final two bugs are included only to show why each intermediate primitive was needed.<sup>[[3]](#references)[[4]](#references)</sup>

```text
landing page -> hidden iframe -> rce_loader.js
-> JSC DFG confusion/UAF -> addrof + fakeobj -> WebContent arbitrary R/W
-> dyld PAC bypass -> authenticated native calls
-> ANGLE/WebGL OOB -> code execution in the GPU process
-> XNU copy-on-write bug -> native calls in mediaplaybackd
-> XNU VFS race -> physical and virtual kernel R/W
```

The important selection rule for multi-process chains is **reachability plus privilege gain**: compromise an IPC endpoint the current sandbox may contact, then move into a process with additional filesystem, Mach-service, entitlement, or kernel-facing attack surface. In this chain WebContent can submit graphics commands to the GPU process; the GPU process can reach surfaces used to pivot into `mediaplaybackd`, where the final kernel stage is run.<sup>[[3]](#references)[[4]](#references)</sup>

### Version-gated staged delivery

The loader fingerprinted the iOS version, fetched the matching JSC worker and downloaded later stages only after the preceding primitive succeeded. The 18.4 exploit split work between a main-context module and a Web Worker communicating with `postMessage`, whereas the 18.6/18.7 implementation placed the exploit in the worker and used a placeholder main module. Per-build workers and offset tables are necessary because JSC structure IDs, object layouts, signed-pointer fields and library offsets change even when the underlying bug remains reachable.<sup>[[3]](#references)[[4]](#references)</sup>

This modular design also leaves a useful delivery signature: an iframe loads `frame.html`, which injects `rce_loader.js`; the loader then retrieves names such as `rce_worker_18.6.js`, `rce_worker_18.7.js` and `sbx0_main.js`. Some observed loaders used `sessionStorage.uid` to suppress reinfection and one variant encrypted retrieved stages with an ephemeral ECDH-derived AES key.<sup>[[3]](#references)</sup>

## Triggering the DFG missing barrier → UAF
```js
function triggerUAF(flag, allocCount) {
Expand Down Expand Up @@ -49,7 +70,22 @@ Status on **iOS 26.1 (arm64e)**:
- The confusion primitive works because it **reuses legitimate signed butterflies**; introducing unsigned attacker pointers fails authentication.
- Potential bypass ideas noted: JIT paths that skip auth, gadgets that sign attacker pointers, or pivoting through the ANGLE OOB.

### Legitimate pointer signer as a PAC oracle

Arbitrary write cannot directly forge an arm64e call target because its PAC binds the pointer to a secret key and a modifier. A general bypass pattern is therefore to corrupt the inputs or state of code that must legitimately sign pointers—such as dyld while resolving bindings—and turn it into a **signing oracle**. The reported CVE-2026-20700 stage applied this pattern to dyld, converting WebContent arbitrary R/W into authenticated native function calls; the complete oracle primitive has not been publicly described.<sup>[[3]](#references)[[4]](#references)</sup>

Conceptually, exploitation asks the trusted signer for an authenticated attacker-selected target, places the result in a callback or other authenticated indirect-call slot, and reaches the consuming callsite. The CPU then accepts the pointer without disclosure of the PAC key and without mapping attacker-supplied executable pages.<sup>[[4]](#references)</sup>

### JavaScript-only post-exploitation runtime

DarkSword kept the orchestration, later exploit stages and final payloads in JavaScript. Recovered build paths show separate abstractions for raw memory/native calls (`Chain/Native.js`), per-version offsets (`Chain/OffsetsStruct.js`), PAC and remote calls (`TaskRop/PAC.js`, `TaskRop/RemoteCall.js`), Mach VM operations (`TaskRop/VM.js`), filesystem access (`JSUtils/FileUtils.js`) and cross-process loading (`InjectJS.js`). This replaces a monolithic shellcode payload with modules layered on the memory and authenticated-call primitives.<sup>[[3]](#references)[[4]](#references)</sup>

This design uses JSC's legitimate execution environment instead of the classic “write shellcode, change page permissions, branch” path. It therefore avoids requiring a separate PPL/SPTM bypass merely to execute unsigned native payload code, at the cost of fragile per-version object layouts and offsets. The daemon pivot demonstrates that the model can survive a process boundary: the chain loaded a JavaScriptCore runtime into `mediaplaybackd` and executed the next JavaScript exploit there.<sup>[[3]](#references)[[4]](#references)</sup>

## ANGLE Metal PBO under-allocation → OOB write

A WebGL request crosses several trust boundaries: the WebContent binding serializes renderer-controlled arguments, the GPU-process IPC decoder reconstructs them, ANGLE validates/translates them, and the Metal backend submits the operation. Audit sizes, offsets, indices, formats, dimensions and overflow-safe arithmetic at **every** layer; disagreement between frontend validation and backend allocation can turn a JavaScript call into GPU-process corruption and therefore a sandbox escape.<sup>[[3]](#references)[[4]](#references)</sup>

Use a tiny unpack height to shrink the staging buffer, then upload a large texture so the copy overruns:<sup>[[1]](#references)</sup>
```js
gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, 16); // alloc height
Expand All @@ -67,6 +103,8 @@ Notes:

- [1] [WebKit-UAF-ANGLE-OOB-Analysis - DFG Store Barrier UAF (CVE-2025-43529) & ANGLE Metal PBO OOB (CVE-2025-14174) on iOS 26.1](https://github.com/zeroxjf/WebKit-UAF-ANGLE-OOB-Analysis)
- [2] [CVE-2025-43529 - WebKit JSC DFG StoreBarrierInsertionPhase UAF PoC](https://github.com/jir4vv1t/CVE-2025-43529)
- [3] [Google Threat Intelligence Group - The Proliferation of DarkSword: iOS Exploit Chain Adopted by Multiple Threat Actors](https://cloud.google.com/blog/topics/threat-intelligence/darksword-ios-exploit-chain)
- [4] [8kSec - How Browser Exploits Work: DarkSword, CVE-2025-43529, and the iOS Browser Exploit Chain](https://8ksec.io/how-browser-exploits-work-darksword-ios-cve-2025-43529)

{{#include ../../banners/hacktricks-training.md}}