Files
Pocket-Poster/Pocket Poster/Exploit/BadQuery.swift
T
Mak5er 18a67ec171 Add bad_query support for iOS 26–27.0b4
Integrate forcequitOS/bad_query for sandbox-escaped PosterBoard
writes, on-device container UUID detection (no PC/Nugget required),
descriptor wipe for Reset Collections, and Mond-style WebKit
respring that works under LiveContainer.

Rebrand fork to com.mak5er.Pocket-Poster; credit Mak5er.
2026-08-11 05:21:59 +03:00

283 lines
10 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.
//
// BadQuery.swift
// Pocket Poster
//
// Swift wrapper around forcequitOS/bad_query sandbox escape
// (container_query path traversal via MobileGestalt SystemGroup).
// Supports iOS 26.0 – 26.6.1 / 27.0b4+.
//
import Foundation
enum BadQueryError: LocalizedError {
case invalidPath
case missingPath
case resolveFailed
case createFailed
case outsideSandbox
case kernelRejected
case asprintfFailed
case unknown(Int64)
case listFailed
case containerNotFound(String)
var errorDescription: String? {
switch self {
case .invalidPath:
return "Path must be absolute."
case .missingPath:
return "Target path does not exist."
case .resolveFailed:
return "Failed to resolve containermanager symbols."
case .createFailed:
return "Failed to create container query."
case .outsideSandbox:
return "Path is outside containermanager sandbox."
case .kernelRejected:
return "Kernel refused to issue a sandbox extension."
case .asprintfFailed:
return "Failed to build path traversal string."
case .unknown(let code):
return "bad_query failed with code \(code)."
case .listFailed:
return "Failed to enumerate containers (fsgetpath)."
case .containerNotFound(let bundleId):
return "Could not find container for \(bundleId)."
}
}
static func from(code: Int64) -> BadQueryError {
switch code {
case -255: return .invalidPath
case -254: return .missingPath
case -1: return .resolveFailed
case -2: return .createFailed
case -3: return .outsideSandbox
case -4: return .kernelRejected
case -5: return .asprintfFailed
default: return .unknown(code)
}
}
}
/// RAII-style sandbox extension handle.
final class BadQueryHandle {
private(set) var handle: Int64
let path: String
init(handle: Int64, path: String) {
self.handle = handle
self.path = path
}
deinit {
release()
}
func release() {
guard handle >= 0 else { return }
bad_query_release(handle)
handle = -1
}
}
enum BadQuery {
/// PosterBoard / related bundle identifiers.
static let posterBoardBundleId = "com.apple.PosterBoard"
static let carPlayWallpaperBundleId = "com.apple.CarPlayWallpaper"
// MARK: - Availability
/// True when the MobileGestalt system-group query route resolves symbols.
/// Cheap probe: only checks dlsym path by attempting a known-existing system path with create=true.
static var isAvailable: Bool {
// Probe with a path that should always exist on device; create=true skips lstat
// so we only fail if symbols/query plumbing is broken (patched OS).
let probe = "/var/mobile"
var cPath = probe.utf8CString.map { Int8($0) }
let result = bad_query(&cPath, true, nil, false)
if result >= 0 {
bad_query_release(result)
return true
}
// -3/-4 mean query ran but path/token failed; symbols still work
if result == -3 || result == -4 || result == -254 {
return true
}
// -1/-2 = symbols/query broken
return result != -1 && result != -2
}
// MARK: - Consume / Release
/// Obtain a sandbox extension for an absolute path.
/// - Parameters:
/// - path: Absolute filesystem path
/// - create: If true, skip existence check (use when lstat is blocked by sandbox)
/// - groupIdentifier: Optional app group; nil uses MobileGestalt SystemGroup (iOS 27 preferred)
/// - isGroup: iOS 26 App Group flag route
@discardableResult
static func consume(
path: String,
create: Bool = false,
groupIdentifier: String? = nil,
isGroup: Bool = false
) throws -> BadQueryHandle {
guard path.hasPrefix("/") else { throw BadQueryError.invalidPath }
var cPath = path.utf8CString.map { Int8($0) }
let handle: Int64
if let group = groupIdentifier {
var cGroup = group.utf8CString.map { Int8($0) }
handle = bad_query(&cPath, create, &cGroup, isGroup)
} else {
handle = bad_query(&cPath, create, nil, isGroup)
}
guard handle >= 0 else {
// Retry with create=true if existence check failed under sandbox
if !create && handle == -254 {
return try consume(path: path, create: true, groupIdentifier: groupIdentifier, isGroup: isGroup)
}
throw BadQueryError.from(code: handle)
}
return BadQueryHandle(handle: handle, path: path)
}
// MARK: - Listing
/// Enumerate immediate children under `path` via fsgetpath inode scan.
static func list(path: String, maxInode: Int64 = 2_000_000) throws -> [String] {
var cPath = path.utf8CString.map { Int8($0) }
guard let raw = bad_query_list(&cPath, maxInode) else {
throw BadQueryError.listFailed
}
defer { free(raw) }
let str = String(cString: raw)
return str
.split(whereSeparator: \.isNewline)
.map(String.init)
.filter { !$0.isEmpty }
}
// MARK: - Container discovery
private static let metadataFileName = ".com.apple.mobile_container_manager.metadata.plist"
/// Read MCMMetadataIdentifier from a container root, using bad_query for access.
static func readBundleId(fromContainerPath containerPath: String) -> String? {
let metaPath = (containerPath as NSString).appendingPathComponent(metadataFileName)
guard let token = try? consume(path: metaPath, create: true) else { return nil }
defer { token.release() }
guard let dict = NSDictionary(contentsOfFile: metaPath) as? [String: Any] else {
// Some layouts use a non-hidden name
let alt = (containerPath as NSString).appendingPathComponent("com.apple.mobile_container_manager.metadata.plist")
if let altToken = try? consume(path: alt, create: true) {
defer { altToken.release() }
if let d = NSDictionary(contentsOfFile: alt) as? [String: Any] {
return d["MCMMetadataIdentifier"] as? String
}
}
return nil
}
return dict["MCMMetadataIdentifier"] as? String
}
/// Find application container UUID for a bundle identifier (on-device, no PC).
static func findAppHash(bundleId targetBundleId: String, searchRoots: [String]? = nil) throws -> String {
let roots = searchRoots ?? [
"/var/mobile/Containers/Data/Application",
"/var/mobile/Containers/Data/InternalDaemon",
"/var/mobile/Containers/Data/PluginKitPlugin"
]
for root in roots {
let children: [String]
do {
children = try list(path: root)
} catch {
continue
}
for child in children {
let uuid = (child as NSString).lastPathComponent
// Normalize to the root we know
let containerPath = "\(root)/\(uuid)"
if let id = readBundleId(fromContainerPath: containerPath), id == targetBundleId {
return uuid
}
}
}
throw BadQueryError.containerNotFound(targetBundleId)
}
static func findPosterBoardHash() throws -> String {
try findAppHash(bundleId: posterBoardBundleId)
}
static func findCarPlayHash() throws -> String {
try findAppHash(bundleId: carPlayWallpaperBundleId)
}
// MARK: - Path helpers
static func applicationContainerPath(appHash: String) -> String {
"/var/mobile/Containers/Data/Application/\(appHash)"
}
static func descriptorsPath(appHash: String, ext: String) -> String {
let ver = SymHandler.getExtensionVersion()
return applicationContainerPath(appHash: appHash)
+ "/Library/Application Support/PRBPosterExtensionDataStore/\(ver)/Extensions/\(ext)/descriptors"
}
static func carPlayCachePath(appHash: String) -> String {
applicationContainerPath(appHash: appHash)
+ "/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images"
}
// MARK: - File ops under extension
/// Copy a directory or file into `destinationDirectory` (must already be accessible via handle).
static func copyItem(at source: URL, intoDestinationDirectory destDir: String, named name: String? = nil) throws {
let destName = name ?? source.lastPathComponent
let destURL = URL(fileURLWithPath: destDir).appendingPathComponent(destName)
let fm = FileManager.default
if fm.fileExists(atPath: destURL.path) {
try fm.removeItem(at: destURL)
}
try fm.copyItem(at: source, to: destURL)
}
/// Ensure a directory exists at path while holding a parent extension if needed.
static func ensureDirectory(at path: String) throws {
let fm = FileManager.default
if fm.fileExists(atPath: path) { return }
// Walk up to find an existing ancestor we can open, then create intermediates
let components = path.split(separator: "/").map(String.init)
var built = ""
var lastExisting = ""
for part in components {
built += "/" + part
if fm.fileExists(atPath: built) {
lastExisting = built
}
}
if lastExisting.isEmpty {
// Get extension on the full path with create=true and mkdir
let handle = try consume(path: path, create: true)
defer { handle.release() }
try fm.createDirectory(atPath: path, withIntermediateDirectories: true)
return
}
let handle = try consume(path: lastExisting, create: true)
defer { handle.release() }
try fm.createDirectory(atPath: path, withIntermediateDirectories: true)
}
}