The newly released itunesstored & bookassetd sbx escape exploit allows us to modify the MobileGestalt.Plist file to change values inside of it.
This file is very important since it contains all the details about the device. Its type, color, model, capabilities like Dynamic Island, Stage Manager, multitasking, etc. are all present inside that file.
Naturally, Apple has encrypted the key-value pairs, but people have managed to figure out most of them over the years.
Modification of the MobileGestalt file has allowed many tweaking applications like Nugget, Misaka, and Picasso to exist over the years.
Recently, developer Duy Tran posted an intriguing video of their iPhone having iPad features like actual app windows, the iPadOS dock, stage manager, etc. This was done with the new exploit that uses a maliciously crafted downloads.28.sqlitedb database to write to paths normally protected by the Sandbox.
Fortunately, MobileGestalt.Plist is one of these paths, and you can actually modify your iPhone to have iPadOS features.
First look of iPadOS on iPhone 17 Pro Max pic.twitter.com/PMynlGLVFw
— Duy Tran (@khanhduytran0) November 15, 2025
Supported iOS versions and devices
The new itunesstored & bookassetd sandbox escape exploit supports all devices on iOS up to iOS 26.1 and iOS 26.2 Beta 1.
This exploit circulated for a while on the internet and was used for iCloud Bypass purposes since it can write to paths and hacktivate.
This will very likely be used to update tools like Nugget, Misaka, etc.
It’s quite a powerful exploit. It can write to most paths controlled/owned by the mobile user. It cannot write to paths owned by the root user.
Obtaining the MobileGestalt.Plist file from the device
There are several ways to go about this. Some Shortcuts allow you to obtain the plist still, tho some of these floating around have been patched.
I didn’t bother. I just made a new Xcode application and read the file at /private/var/containers/Shared/SystemGroup/ systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist
It’s as simple as:
import SwiftUI
import UniformTypeIdentifiers
struct ContentView: View {
@State private var plistData: Any?
@StateObject private var coordinator = DocumentPickerCoordinator()
let plistPath = "/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist"
var body: some View {
VStack(spacing: 20) {
Button("Load Plist") {
loadPlist()
}
if plistData != nil {
Button("Save to Files") {
savePlist()
}
}
}
}
func loadPlist() {
if let data = try? Data(contentsOf: URL(fileURLWithPath: plistPath)),
let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) {
plistData = plist
}
}
func savePlist() {
guard let plist = plistData,
let data = try? PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) else { return }
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("MobileGestalt.plist")
try? data.write(to: tempURL)
let picker = UIDocumentPickerViewController(forExporting: [tempURL], asCopy: true)
picker.delegate = coordinator
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = scene.windows.first,
let root = window.rootViewController {
var top = root
while let presented = top.presentedViewController {
top = presented
}
top.present(picker, animated: true)
}
}
}
class DocumentPickerCoordinator: NSObject, UIDocumentPickerDelegate, ObservableObject {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {}
}This would save your MobileGestalt.Plist file without issue even on iOS 26.1 because Apple still allows iOS apps to read this path, no problem. You can’t write to it this way, but reading works.
Once you have it inside the Files application, you can just AirDrop it to your computer.
Finding the proper MobileGestalt keys to write
There are hundreds of MobileGestalt keys, each controlling something else. These keys are encrypted and look like 1tvy6WfYKVGumYi6Y8E5Og and /bSMNaIuUT58N/BN1nYUjw, etc.
For example:
- uKc7FPnEO++lVhHWHFlGbQ = Device is an iPad
- HV7WDiidgMf7lwAu++Lk5w = Device has TouchID functionality.
- s2UwZpwDQcywU3de47/ilw = Device has a microphone.
And so on. There is a nice article over on TheAppleWiki with all the available MobileGestalt Keys people managed to decrypt over the years.
You need to find the right keys to add to your iPhone’s MobileGestalt that would first make it think it’s an iPad, and then enable iPad features like Stage Manager, Multitasking, etc.
I’ve done the research for you, and you need the following keys:
- uKc7FPnEO++lVhHWHFlGbQ = Device is an iPad.
- mG0AnH/Vy1veoqoLRAIgTA = Device supports Medusa Floating Live Apps
- UCG5MkVahJxG1YULbbd5Bg = Device supports Medusa Overlay Apps
- ZYqko/XM5zD3XBfN5RmaXA = Device supports Medusa Pinned Apps
- nVh/gwNpy7Jv1NOk00CMrw = Device supports MedusaPIP mirroring
- qeaj75wk3HF4DwQ8qbIi7g = Device is capable of enabling Stage Manager
Those are all the keys we need for now.
The uKc7FPnEO++lVhHWHFlGbQ value is what tells the device it is an iPad instead of an iPhone. This MUST be added to the CacheData section of the MobileGestalt plist, not the CacheExtra, otherwise the device WILL BOOTLOOP!
However, we have a problem. CacheData looks like this:

So how the hell do we add the necessary keys to it? It’s all garbled characters. Looks encrypted.
Well, you will need to find the offset of the key you wanna change inside the libmobilegestalt.dylib file. Let me explain.
The uKc7FPnEO++lVhHWHFlGbQ value (iPad) needs to be added to mtrAoWJ3gsq+I90ZnQ0vQw, which is DeviceClassNumber, like this:
mtrAoWJ3gsq+I90ZnQ0vQw : uKc7FPnEO++lVhHWHFlGbQ
Which translates to DeviceClassNumber : 3 (iPad)
But how can you add this if the CacheData section is garbled?
Finding the right offset inside the libmobilegestalt.dylib
The /usr/lib/libMobileGestalt.dylib is not accessible from the sandbox, so we cannot read it via an Xcode-made app like before, but we can dlopen the libmobilegestalt.dylib and parse its segments. If we can do that, we can look for the encrypted key mtrAoWJ3gsq+I90ZnQ0vQw and find the offset.
Then we would know where to place our modified keys.
You can adapt the SwiftUI app from earlier to dlopen the dylib quite easily. Here’s what I came up with. Quick and dirty, based on Duy’s older code from SparseBox:
func findCacheDataOffset() -> Int? {
guard let handle = dlopen("/usr/lib/libMobileGestalt.dylib", RTLD_GLOBAL) else { return nil }
defer { dlclose(handle) }
var headerPtr: UnsafePointer<mach_header_64>?
var imageSlide: Int = 0
for i in 0..<_dyld_image_count() {
if let imageName = _dyld_get_image_name(i),
String(cString: imageName) == "/usr/lib/libMobileGestalt.dylib",
let imageHeader = _dyld_get_image_header(i) {
headerPtr = UnsafeRawPointer(imageHeader).assumingMemoryBound(to: mach_header_64.self)
imageSlide = _dyld_get_image_vmaddr_slide(i)
break
}
}
guard let header = headerPtr else { return nil }
var textCStringAddr: UInt64 = 0
var textCStringSize: UInt64 = 0
var constAddr: UInt64 = 0
var constSize: UInt64 = 0
var curCmd = UnsafeRawPointer(header).advanced(by: MemoryLayout<mach_header_64>.size)
for _ in 0..<header.pointee.ncmds {
let cmd = curCmd.assumingMemoryBound(to: load_command.self)
if cmd.pointee.cmd == LC_SEGMENT_64 {
let segCmd = curCmd.assumingMemoryBound(to: segment_command_64.self)
let segName = String(data: Data(bytes: &segCmd.pointee.segname, count: 16), encoding: .utf8)?.trimmingCharacters(in: .controlCharacters) ?? ""
var sectionPtr = curCmd.advanced(by: MemoryLayout<segment_command_64>.size)
for _ in 0..<Int(segCmd.pointee.nsects) {
let section = sectionPtr.assumingMemoryBound(to: section_64.self)
let sectName = String(data: Data(bytes: §ion.pointee.sectname, count: 16), encoding: .utf8)?.trimmingCharacters(in: .controlCharacters) ?? ""
if segName == "__TEXT" && sectName == "__cstring" {
textCStringAddr = section.pointee.addr
textCStringSize = section.pointee.size
}
if (segName == "__AUTH_CONST" || segName == "__DATA_CONST") && sectName == "__const" {
constAddr = section.pointee.addr
constSize = section.pointee.size
}
sectionPtr = sectionPtr.advanced(by: MemoryLayout<section_64>.size)
}
}
curCmd = curCmd.advanced(by: Int(cmd.pointee.cmdsize))
}
guard textCStringAddr != 0, constAddr != 0 else { return nil }
let textCStringPtr = UnsafeRawPointer(bitPattern: Int(textCStringAddr) + imageSlide)!
var keyPtr: UnsafePointer<CChar>?
var offset = 0
while offset < Int(textCStringSize) {
let currentPtr = textCStringPtr.advanced(by: offset).assumingMemoryBound(to: CChar.self)
let currentString = String(cString: currentPtr)
if currentString == "mtrAoWJ3gsq+I90ZnQ0vQw" {
keyPtr = currentPtr
break
}
offset += currentString.utf8.count + 1
}
guard let keyPtr = keyPtr else { return nil }
let constSectionPtr = UnsafeRawPointer(bitPattern: Int(constAddr) + imageSlide)!.assumingMemoryBound(to: UnsafeRawPointer.self)
var structPtr: UnsafeRawPointer?
for i in 0..<Int(constSize) / 8 {
if constSectionPtr[i] == UnsafeRawPointer(keyPtr) {
structPtr = UnsafeRawPointer(constSectionPtr.advanced(by: i))
break
}
}
guard let structPtr = structPtr else { return nil }
let offsetMetadata = structPtr.advanced(by: 0x9a).assumingMemoThis will give you the offset for the DeviceClassNumber so now you can just write your new value to it.
For this, you can just modify Duy’s Python script, which is based on Hana Kim‘s original files.
I added something like this:
IPAD_KEYS = [
"uKc7FPnEO++lVhHWHFlGbQ",
"mG0AnH/Vy1veoqoLRAIgTA",
"UCG5MkVahJxG1YULbbd5Bg",
"ZYqko/XM5zD3XBfN5RmaXA",
"nVh/gwNpy7Jv1NOk00CMrw",
"qeaj75wk3HF4DwQ8qbIi7g"
]
def write_ipad_to_device_class_with_offset(self, mg_plist, offset):
cache_data = mg_plist.get('CacheData')
cache_extra = mg_plist.get('CacheExtra', {})
if cache_data is None:
self.log("[!] Error: CacheData not found in MobileGestalt", "error")
return False
if not isinstance(cache_data, bytes):
cache_data = bytes(cache_data)
if offset >= len(cache_data) - 8:
self.log(f"[!] Error: Offset {offset} is beyond CacheData bounds ({len(cache_data)} bytes)", "error")
return False
cache_data_array = bytearray(cache_data)
current_value = struct.unpack_from('<Q', cache_data_array, offset)[0]
device_type = 'iPhone' if current_value == 1 else 'iPad' if current_value == 3 else 'Unknown'
self.log(f"[i] Current DeviceClassNumber value: {current_value} ({device_type})", "info")
struct.pack_into('<Q', cache_data_array, offset, 3)
new_value = struct.unpack_from('<Q', cache_data_array, offset)[0]
self.log(f"[i] New DeviceClassNumber value: {new_value} (iPad)", "success")
mg_plist['CacheData'] = bytes(cache_data_array)
for key in IPAD_KEYS:
cache_extra[key] = 1
mg_plist['CacheExtra'] = cache_extra
self.log("[+] Successfully wrote iPad device class to MobileGestalt", "success")
return True
def modify_mobile_gestalt(self, mg_file, output_file):
try:
with open(mg_file, 'rb') as f:
mg_plist = plistlib.load(f)
choice = self.operation_mode.get()
if choice in [1, 2]:
offset = None
offset_str = self.offset_var.get().strip()
if offset_str:
try:
if offset_str.startswith("0x") or offset_str.startswith("0X"):
offset = int(offset_str, 16)
else:
offset = int(offset_str)
self.log(f"[+] Using offset: {offset} (0x{offset:x})", "success")
except ValueError:
self.log("[!] Invalid offset format, skipping CacheData modification", "warning")
offset = None
if choice == 1:
if offset is not None and self.write_ipad_to_device_class_with_offset(mg_plist, offset):
self.log("[+] iPad mode enabled", "success")
elif offset is None:
cache_extra = mg_plist.get('CacheExtra', {})
for key in IPAD_KEYS:
cache_extra[key] = 1
mg_plist['CacheExtra'] = cache_extra
self.log("[!] iPad mode enabled (CacheExtra only - may not fully work without CacheData)", "warning")
else:
self.log("[!] Failed to enable iPad mode", "error")
return False
elif choice == 2:
if offset is not None and self.restore_iphone_device_class_with_offset(mg_plist, offset):
self.log("[+] iPhone mode restored", "success")
elif offset is None:
cache_extra = mg_plist.get('CacheExtra', {})
for key in IPAD_KEYS:
cache_extra.pop(key, None)
mg_plist['CacheExtra'] = cache_extra
self.log("[i] iPhone mode restored (CacheExtra only)", "warning")
else:
self.log("[!] Failed to restore iPhone mode", "error")
return False
else:
self.log("[i] Using file as-is", "info")
with open(output_file, 'wb') as f:
plistlib.dump(mg_plist, f)
self.log(f"[+] SUCCESS: Modified MobileGestalt saved to: {output_file}", "success")
return True
except Exception as e:
self.log(f"[!] Error modifying MobileGestalt: {e}", "error")
return False
That’s it. That’s all the modifications I did to the original bl_sbx Python script from Duy.

Running this with python3 in a venv allowed me to change the device to iPad and enable iPad features.
The exploit doesn’t have a great success rate, so you may need to try this again and again until it succeeds. Once it does, reboot the device. You should be able to access iPad features in Settings.



Setting up the environment for Python3 on macOS
To properly run the Python script on recent macOS and install the dependencies, you must first set up a virtual environment. To do that, you need to run:
cd bl_sbx
python3 -m venv venv
source venv/bin/activate
pip install click requests packaging pymobiledevice3Once the environment is set up and the prerequisites are installed, you can just run the script:
python3 run.py DEVICE UDID /path/to/MobileGestalt.plistI used ideviceinfo, part of libimobiledevice, to get the device UDID, but it’s also available in Finder, 3uTools on Windows, etc.
More iDevice Central Guides
- iOS 17 Jailbreak RELEASED! How to Jailbreak iOS 17 with PaleRa1n
- How to Jailbreak iOS 18.0 – iOS 18.2.1 / iOS 18.3 With Tweaks
- Download iRemovalRa1n Jailbreak (CheckRa1n for Windows)
- Dopamine Jailbreak (Fugu15 Max) Release Is Coming Soon for iOS 15.0 – 15.4.1 A12+
- Cowabunga Lite For iOS 16.2 – 16.4 Released in Beta! Install Tweaks and Themes Without Jailbreak
- Fugu15 Max Jailbreak: All Confirmed Working Rootless Tweaks List
- iOS 14.0 – 16.1.2 – All MacDirtyCow Tools IPAs
- iOS Jailbreak Tools for All iOS Versions
