Cinder27 — iOS 17.0-27.1 vulnerability verification lab: 51 modules, 31 per-CVE POC folders, 11 no-CVE module folders, harness app source
This commit is contained in:
commit
1ea76ab005
89 files changed
+16485
No files matched your search
@@ -0,0 +1,275 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Chain
|
||||
|
||||
struct ChainStage: Identifiable {
|
||||
let id: String
|
||||
let index: Int
|
||||
let title: String
|
||||
let goal: String
|
||||
let moduleIDs: [String]
|
||||
let unlock: String
|
||||
}
|
||||
|
||||
enum ChainPlan {
|
||||
static let stages: [ChainStage] = [
|
||||
ChainStage(id: "s0", index: 0,
|
||||
title: tl("权限面测绘"),
|
||||
goal: tl("确定该构建上的实际起点:进程持有哪些 entitlement、沙盒放行到哪一层、mach 命名空间如何组织。后续每一步的收益都要以该基线折算。"),
|
||||
moduleIDs: ["probe.privset", "kern.baseline"],
|
||||
unlock: tl("缺少该基线,无法区分「漏洞所得」与「签名所授」。")),
|
||||
|
||||
ChainStage(id: "s1", index: 1,
|
||||
title: tl("信息泄露"),
|
||||
goal: tl("先取得地址与内容。decmpfs 欠产出将内核页缓冲尾部交回用户态;TIFF 截断条带与 EXIF 短读将进程堆残留交回图像与元数据;CoreMedia bytestream 与 sandbox_extension 越过沙盒边界取回文件内容。"),
|
||||
moduleIDs: ["kern.decmpfs", "iio.tiff.trunc", "iio.jpeg.comment",
|
||||
"cm.fdleak", "sbx.toctou"],
|
||||
unlock: tl("在取得稳定泄露之后,才有条件讨论将溢出写成什么值;否则只能触发崩溃。")),
|
||||
|
||||
ChainStage(id: "s2", index: 2,
|
||||
title: tl("溢出阈值标定"),
|
||||
goal: tl("Type-5 阴影的写入是值可控的,但短溢出会被相邻 VLA 吸收。逐级抬升 R,确定该构建上栈布局开始失效的维度。"),
|
||||
moduleIDs: ["cg.shading.sweep"],
|
||||
unlock: tl("该阈值决定后续所有负载的长度预算。")),
|
||||
|
||||
ChainStage(id: "s3", index: 3,
|
||||
title: tl("值可控写"),
|
||||
goal: tl("在标定所得长度上,以 PDF 中选定的 double 位模式决定被覆盖的指针。崩溃地址即证据 —— 它证明写入内容由攻击者控制,而非仅证明发生了崩溃。"),
|
||||
moduleIDs: ["cg.shading"],
|
||||
unlock: tl("值可控写与地址泄露相加,构成构造可控状态的全部要素;所缺的是将写转为执行的环节。")),
|
||||
|
||||
ChainStage(id: "s4", index: 4,
|
||||
title: tl("内核面"),
|
||||
goal: tl("用户态可触及的内核入口数量有限。ipc_port 请求表索引竞态无 entitlement 门槛;decmpfs 已在泄露层验证。本层度量的是「能否将用户态的错误转为内核侧的状态变化」。"),
|
||||
moduleIDs: ["kern.ipc"],
|
||||
unlock: tl("需达到可复现阈值;低于该阈值时,越狱路径不成立。")),
|
||||
|
||||
ChainStage(id: "s5", index: 5,
|
||||
title: tl("解析器面"),
|
||||
goal: tl("libarchive 的 2 字节堆越界与 CoreMedia 的 28 字节结构体外写,是整套中仅有的两个「写穿过对象边界」的形状。前者精确但浅,后者深且落在保存寄存器区。同一 CVE 下的 CAB 站点与 GPT 分区名越界读一并标定。"),
|
||||
moduleIDs: ["arc.iso.stress", "arc.cab", "cm.rpu.sweep", "kext.gpt"],
|
||||
unlock: tl("两者都需要稳定的堆布局控制,而这正是上一阶段泄露所提供的。")),
|
||||
|
||||
ChainStage(id: "s6", index: 6,
|
||||
title: tl("工具链落地"),
|
||||
goal: tl("将上述产物转为可用形式:IPA 改包、二进制注入、字节级替换、属性改写、数据库读取。本层不依赖任何漏洞,但它是「漏洞能力」最终交付到手上的形态。"),
|
||||
moduleIDs: ["tool.ipa", "tool.zip", "tool.hex", "tool.macho", "tool.paths"],
|
||||
unlock: tl("自检全绿意味着工具链本身可信 —— 此后出现的问题是选择问题,不是工具问题。")),
|
||||
]
|
||||
|
||||
static func modules(for stage: ChainStage) -> [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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user