- Replace bad_query with forcequitOS/worse_query (App Group based) - Read the App Group from the code signature entitlements - Add pathleak container enumeration and iOS 27 system container lookup - Route descriptor/CarPlay writes through worse_query - Add sign_personal.sh and POCKET_POSTER_SELFTEST diagnostics - Keep bad_query as an iOS 26 fallback
340 lines
15 KiB
Swift
340 lines
15 KiB
Swift
//
|
|
// PosterBoardManager.swift
|
|
// Pocket Poster
|
|
//
|
|
// Created by lemin on 5/31/25.
|
|
//
|
|
|
|
import Foundation
|
|
import ZIPFoundation
|
|
import UIKit
|
|
|
|
class PosterBoardManager: ObservableObject {
|
|
static let ShortcutURL = "https://www.icloud.com/shortcuts/a28d2c02ca11453cb5b8f91c12cfa692"
|
|
static let WallpapersURL = "https://cowabun.ga/wallpapers"
|
|
|
|
static let MaxTendies = 10
|
|
|
|
static let shared = PosterBoardManager()
|
|
|
|
@Published var selectedTendies: [URL] = []
|
|
@Published var videos: [LoadInfo] = []
|
|
|
|
func getTendiesStoreURL() -> URL {
|
|
let tendiesStoreURL = SymHandler.getDocumentsDirectory().appendingPathComponent("KFC Bucket", conformingTo: .directory)
|
|
// create it if it doesn't exist
|
|
if !FileManager.default.fileExists(atPath: tendiesStoreURL.path()) {
|
|
try? FileManager.default.createDirectory(at: tendiesStoreURL, withIntermediateDirectories: true)
|
|
}
|
|
return tendiesStoreURL
|
|
}
|
|
|
|
func setSystemLanguage(to new_lang: String) -> Bool {
|
|
// Load Preferences private frameworks if needed
|
|
dlopen("/System/Library/PrivateFrameworks/SettingsFoundation.framework/SettingsFoundation", RTLD_NOW)
|
|
dlopen("/System/Library/PrivateFrameworks/Preferences.framework/Preferences", RTLD_NOW)
|
|
dlopen("/System/Library/PrivateFrameworks/InternationalSupport.framework/InternationalSupport", RTLD_NOW)
|
|
|
|
var langManager: NSObject = NSObject()
|
|
if #available(iOS 18.0, *) {
|
|
guard let obj = objc_getClass("IPSettingsUtilities") as? NSObject else { return false }
|
|
langManager = obj
|
|
} else {
|
|
guard let obj = objc_getClass("PSLanguageSelector") as? NSObject else { return false }
|
|
langManager = obj
|
|
}
|
|
|
|
if let success = langManager.perform(Selector(("setLanguage:")), with: new_lang) {
|
|
return success != nil
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Wipe PosterBoard custom descriptor collections using worse_query (real delete).
|
|
/// Falls back to language-toggle trick on older exploit path.
|
|
func resetCollections(appHash: String) throws {
|
|
if SymHandler.prefersSandboxEscape {
|
|
try wipeDescriptorsViaEscape(appHash: appHash)
|
|
return
|
|
}
|
|
|
|
// Legacy: language toggle forces PosterBoard to rebuild collections
|
|
guard let lang = UserDefaults.standard.stringArray(forKey: "AppleLanguages")?.first else {
|
|
throw ApplyError.unexpected(info: "Could not read system language for reset.")
|
|
}
|
|
guard setSystemLanguage(to: lang) else {
|
|
throw ApplyError.unexpected(info: "Language API failed. Reset collections manually in Settings → Language & Region.")
|
|
}
|
|
}
|
|
|
|
/// Delete everything under each PRBPosterExtensionDataStore/*/Extensions/*/descriptors.
|
|
private func wipeDescriptorsViaEscape(appHash: String) throws {
|
|
let ver = SymHandler.getExtensionVersion()
|
|
let extensionsRoot = SandboxEscape.applicationContainerPath(appHash: appHash)
|
|
+ "/Library/Application Support/PRBPosterExtensionDataStore/\(ver)/Extensions"
|
|
|
|
// Open parent with sandbox extension. A missing path simply means there
|
|
// 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() }
|
|
|
|
let fm = FileManager.default
|
|
guard fm.fileExists(atPath: extensionsRoot) else {
|
|
return
|
|
}
|
|
|
|
let extDirs = try fm.contentsOfDirectory(atPath: extensionsRoot)
|
|
var wiped = 0
|
|
for extName in extDirs {
|
|
let descriptorsPath = (extensionsRoot as NSString)
|
|
.appendingPathComponent(extName)
|
|
.appending("/descriptors")
|
|
|
|
// Ensure we hold an extension on the descriptors dir itself
|
|
let descHandle = try? SandboxEscape.consume(path: descriptorsPath, create: true)
|
|
defer { descHandle?.release() }
|
|
|
|
guard fm.fileExists(atPath: descriptorsPath) else { continue }
|
|
|
|
let items = (try? fm.contentsOfDirectory(atPath: descriptorsPath)) ?? []
|
|
for item in items {
|
|
if item == "__MACOSX" || item.hasPrefix(".") { continue }
|
|
let full = (descriptorsPath as NSString).appendingPathComponent(item)
|
|
try? fm.removeItem(atPath: full)
|
|
wiped += 1
|
|
}
|
|
}
|
|
print("resetCollections: wiped \(wiped) descriptor item(s)")
|
|
}
|
|
|
|
func openPosterBoard() -> Bool {
|
|
guard let obj = objc_getClass("LSApplicationWorkspace") as? NSObject else { return false }
|
|
let workspace = obj.perform(Selector(("defaultWorkspace")))?.takeUnretainedValue() as? NSObject
|
|
|
|
if let success = workspace?.perform(Selector(("openApplicationWithBundleID:")), with: "com.apple.PosterBoard") {
|
|
return success != nil
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func unzipFile(at url: URL) throws -> URL {
|
|
let fileName = url.deletingPathExtension().lastPathComponent
|
|
// Replace spaces and %20 with underscores
|
|
let normalizedFileName = fileName.replacingOccurrences(of: "[ \\%20]", with: "_", options: .regularExpression)
|
|
let fileData = try Data(contentsOf: url)
|
|
let fileManager = FileManager()
|
|
|
|
// Write the file to the Documents Directory
|
|
let path = SymHandler.getDocumentsDirectory().appendingPathComponent("UnzipItems", conformingTo: .directory).appendingPathComponent(UUID().uuidString)
|
|
if !FileManager.default.fileExists(atPath: path.path()) {
|
|
try? FileManager.default.createDirectory(at: path, withIntermediateDirectories: true)
|
|
}
|
|
|
|
// Remove All files in this directory
|
|
let existingFiles = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
|
for fileUrl in existingFiles
|
|
{
|
|
try FileManager.default.removeItem(at: fileUrl)
|
|
}
|
|
let zipURL = path.appending(path: normalizedFileName)
|
|
|
|
// Save our Zip file
|
|
try fileData.write(to: zipURL, options: [.atomic])
|
|
|
|
// Unzip the Zipped Up File
|
|
var destinationURL = path
|
|
if FileManager.default.fileExists(atPath: zipURL.path())
|
|
{
|
|
destinationURL.append(path: "directory")
|
|
try fileManager.unzipItem(at: zipURL, to: destinationURL)
|
|
}
|
|
|
|
return destinationURL
|
|
}
|
|
|
|
func runShortcut(named name: String) {
|
|
guard let urlEncodedName = name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
|
let url = URL(string: "shortcuts://run-shortcut?name=\(name)") else { return }
|
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
}
|
|
|
|
func getDescriptorsFromTendie(_ url: URL) throws -> [String: [URL]]? {
|
|
for dir in try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
|
|
let fileName = dir.lastPathComponent
|
|
if fileName.lowercased() == "container" {
|
|
// container support, find the extensions
|
|
let extDir = dir.appending(path: "Library/Application Support/PRBPosterExtensionDataStore/61/Extensions")
|
|
var retList: [String: [URL]] = [:]
|
|
for ext in try FileManager.default.contentsOfDirectory(at: extDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
|
|
let descrDir = ext.appendingPathComponent("descriptors")
|
|
retList[ext.lastPathComponent] = [descrDir]
|
|
}
|
|
return retList
|
|
}
|
|
else if fileName.lowercased() == "descriptor" || fileName.lowercased() == "descriptors" || fileName.lowercased() == "ordered-descriptor" || fileName.lowercased() == "ordered-descriptors" { // TODO: Add ordered descriptors
|
|
return ["com.apple.WallpaperKit.CollectionsPoster": [dir]]
|
|
}
|
|
else if fileName.lowercased() == "video-descriptor" || fileName.lowercased() == "video-descriptors" {
|
|
return ["com.apple.PhotosUIPrivate.PhotosPosterProvider": [dir]]
|
|
}
|
|
}
|
|
// TODO: Add error handling here
|
|
return nil
|
|
}
|
|
|
|
func randomizeWallpaperId(url: URL) throws {
|
|
let randomizedID = Int.random(in: 9999...99999)
|
|
var files = [URL]()
|
|
if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
|
|
for case let fileURL as URL in enumerator
|
|
{
|
|
do
|
|
{
|
|
let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey])
|
|
if fileAttributes.isRegularFile!
|
|
{
|
|
files.append(fileURL)
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
print(error, fileURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setPlistValue(file: String, key: String, value: Any, recursive: Bool = true) {
|
|
// thanks gpt
|
|
guard let plistData = FileManager.default.contents(atPath: file),
|
|
var plist = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: Any] else {
|
|
return
|
|
}
|
|
|
|
plist[key] = value
|
|
|
|
guard let updatedData = try? PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
try updatedData.write(to: URL(fileURLWithPath: file))
|
|
} catch {
|
|
print("Failed to write updated plist: \(error)")
|
|
}
|
|
}
|
|
|
|
for file in files {
|
|
switch file.lastPathComponent {
|
|
case "com.apple.posterkit.provider.descriptor.identifier":
|
|
try String(randomizedID).data(using: .utf8)?.write(to: file)
|
|
|
|
case "com.apple.posterkit.provider.contents.userInfo":
|
|
setPlistValue(file: file.path(), key: "wallpaperRepresentingIdentifier", value: randomizedID)
|
|
|
|
case "Wallpaper.plist":
|
|
setPlistValue(file: file.path(), key: "identifier", value: randomizedID, recursive: false)
|
|
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyTendies(appHash: String) throws {
|
|
// organize the descriptors into their respective extensions
|
|
var extList: [String: [URL]] = [:]
|
|
// create the video first
|
|
if videos.count > 0 {
|
|
extList["com.apple.WallpaperKit.CollectionsPoster"] = []
|
|
for video in videos {
|
|
switch video.loadState {
|
|
case .loaded(let movie):
|
|
do {
|
|
let newVideo = try VideoHandler.createCaml(from: movie.url, autoReverses: video.autoReverses)
|
|
extList["com.apple.WallpaperKit.CollectionsPoster"]?.append(newVideo)
|
|
} catch {
|
|
print(error.localizedDescription)
|
|
}
|
|
default:
|
|
print("Video not loaded!")
|
|
}
|
|
}
|
|
}
|
|
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Extracting tendies...", comment: "happens when unzipping the files"))
|
|
for url in selectedTendies {
|
|
let unzippedDir = try unzipFile(at: url)
|
|
guard let descriptors = try getDescriptorsFromTendie(unzippedDir) else { continue } // TODO: Add error handling
|
|
extList.merge(descriptors) { (first, second) in first + second }
|
|
}
|
|
|
|
defer {
|
|
SymHandler.cleanup()
|
|
}
|
|
|
|
let useSandboxEscape = SymHandler.prefersSandboxEscape
|
|
if useSandboxEscape {
|
|
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: "worse_query sandbox escape…")
|
|
}
|
|
|
|
for (ext, descriptorsList) in extList {
|
|
var foldersToWrite: [URL] = []
|
|
for descriptors in descriptorsList {
|
|
for descr in try FileManager.default.contentsOfDirectory(at: descriptors, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
|
|
if descr.lastPathComponent != "__MACOSX" {
|
|
try randomizeWallpaperId(url: descr)
|
|
foldersToWrite.append(descr)
|
|
}
|
|
}
|
|
}
|
|
|
|
if useSandboxEscape {
|
|
// iOS 26/27: direct write via the container_query sandbox extension
|
|
do {
|
|
try SymHandler.writeDescriptorsViaEscape(appHash: appHash, ext: ext, descriptorFolders: foldersToWrite)
|
|
} catch {
|
|
// Fall back to legacy .Trash symlink if the escape path fails
|
|
print("worse_query apply failed, falling back to symlink: \(error)")
|
|
try applyDescriptorsViaSymlink(appHash: appHash, ext: ext, folders: foldersToWrite)
|
|
}
|
|
} else {
|
|
try applyDescriptorsViaSymlink(appHash: appHash, ext: ext, folders: foldersToWrite)
|
|
}
|
|
SymHandler.cleanup()
|
|
}
|
|
|
|
// clean up all possible files
|
|
for url in selectedTendies {
|
|
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent("UnzipItems", conformingTo: .directory))
|
|
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent(url.lastPathComponent))
|
|
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent(url.deletingPathExtension().lastPathComponent))
|
|
}
|
|
}
|
|
|
|
/// Legacy exploit: symlink Documents/.Trash → descriptors, then trashItem into it.
|
|
private func applyDescriptorsViaSymlink(appHash: String, ext: String, folders: [URL]) throws {
|
|
let _ = try SymHandler.createDescriptorsSymlink(appHash: appHash, ext: ext)
|
|
for descr in folders {
|
|
let newURL = SymHandler.getDocumentsDirectory().appendingPathComponent(UUID().uuidString, conformingTo: .directory)
|
|
try FileManager.default.copyItem(at: descr, to: newURL)
|
|
try FileManager.default.trashItem(at: newURL, resultingItemURL: nil)
|
|
}
|
|
}
|
|
|
|
static func clearCache() throws {
|
|
SymHandler.cleanup()
|
|
let docDir = SymHandler.getDocumentsDirectory()
|
|
for file in try FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil) {
|
|
if file.lastPathComponent != "CarPlayPhotos" {
|
|
try FileManager.default.removeItem(at: file)
|
|
}
|
|
}
|
|
}
|
|
}
|