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. // sizeofcmds is attacker-controlled in a malformed header — clamp before indexing, // or the zeroing loop below walks off the end of the image. let streamStart = h.headerSize let oldEnd = min(streamStart + Int(h.sizeofcmds), out.count) 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..