276 lines
13 KiB
Swift
276 lines
13 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|