334 lines
12 KiB
Swift
334 lines
12 KiB
Swift
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..<end]
|
|
let hex = slice.map { String(format: "%02x", $0) }.enumerated()
|
|
.map { $0.offset == 7 ? $0.element + " " : $0.element }
|
|
.joined(separator: " ")
|
|
let ascii = slice.map { ($0 >= 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<UInt32>.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<Unmanaged<CFError>?>?) -> 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<vm_statistics64_data_t>.size / MemoryLayout<integer_t>.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
|
|
)
|
|
}
|
|
}
|