人们正在他们的iPhone上使用iPadOS功能。
People are using iPad OS features on their iPhones

原始链接: https://idevicecentral.com/ios-customization/how-to-enable-ipad-features-like-multitasking-stage-manager-on-iphone-via-mobilegestalt/

## 新的iOS漏洞使iPhone启用iPad功能 最近发现的漏洞影响运行iOS 26.1及26.2 Beta 1的设备,它利用`itunesstored`和`bookassetd`进程中的漏洞来绕过沙盒限制。这允许修改`MobileGestalt.plist`文件,该文件详细描述了设备的关键信息,如型号、功能(灵动岛、舞台管理程序)等。 虽然苹果对这些数据进行了加密,但研究人员已经解密了许多键值对,从而使Nugget和Misaka等工具得以运行。该漏洞利用了一个精心制作的数据库文件来写入受保护的路径,包括`MobileGestalt.plist`,从而有效地欺骗iPhone识别为iPad。 具体来说,将某些键(如`uKc7FPnEO++lVhHWHFlGbQ`,用于标识设备为iPad)添加到plist的`CacheData`部分(需要在`libmobilegestalt.dylib`文件中找到正确的偏移量)可以解锁iPadOS功能,如舞台管理程序和iPad停靠栏。一个Python脚本,基于现有工具,可以促进这种修改。 成功并非保证,可能需要多次尝试,但成功的修改和重启可以将iPad功能带到iPhone上。该漏洞功能强大,允许写入大多数用户拥有的路径,并且可能被集成到现有的修改工具中。

最近Hacker News上的讨论显示,用户成功地在iPhone上启用了iPadOS功能,引发了关于苹果设备限制的争论。虽然有些人认为在较小的iPhone屏幕上这样做不切实际,但核心讨论集中在iPadOS和iOS之间人为的区分上——暗示它们共享代码库,但功能被有意限制。 用户表达了对苹果“锁定”的沮丧,认为这阻止了充分利用iPhone和iPad内部的强大硬件。一位评论员将强大的M1 Mac比作一辆受限的法拉利,更喜欢一台更老旧、能力较弱的笔记本电脑所带来的自由。这种情绪是渴望更开放地访问设备功能,允许安装替代操作系统并释放硬件的全部潜力。 另一方面,有人认为苹果限制功能是为了防止设备不稳定(“变砖”)。
相关文章

原文

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.

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: &section.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).assumingMemo

This 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 the MobileGestalt exploit

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 pymobiledevice3

Once the environment is set up and the prerequisites are installed, you can just run the script:

python3 run.py DEVICE UDID /path/to/MobileGestalt.plist

I 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

联系我们 contact @ memedata.com