├── 2.py
├── AIEnabler.py
├── README.md
├── backend
├── __pycache__
│ ├── device_manager.cpython-312.pyc
│ ├── device_manager.cpython-39.pyc
│ └── funcs.cpython-312.pyc
├── device_manager.py
├── funcs.py
└── mainwindow.py
├── exploit
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-312.pyc
│ ├── backup.cpython-312.pyc
│ ├── mbdb.cpython-312.pyc
│ └── restore.cpython-312.pyc
├── backup.py
├── mbdb.py
└── restore.py
└── tweak
├── __pycache__
└── eligibility.cpython-312.pyc
├── eligibility.py
└── files
├── eligibility.plist
└── restore.json
/2.py:
--------------------------------------------------------------------------------
1 | from backend.device_manager import DeviceManager
2 | from backend.funcs import prompt
3 | from pymobiledevice3 import usbmux
4 | from tweak.eligibility import EUTweak
5 |
6 | print("Initializing...")
7 |
8 | prompt_options = [
9 | "1. Restore files with no data",
10 | "2. Apply eligibility and config patches",
11 | "3. Restore files with no data and apply patches",
12 | "4. Automated eligibility and config patch spam"
13 | ]
14 |
15 | dev_manager = DeviceManager()
16 |
17 | tweak = EUTweak()
18 | tweak.apply(dev_manager)
--------------------------------------------------------------------------------
/AIEnabler.py:
--------------------------------------------------------------------------------
1 | import os
2 | import tkinter as tk
3 | from tkinter import filedialog
4 | from lxml import etree
5 |
6 | def action_prompt():
7 | print("Select an option:")
8 | print("1. Enable AI")
9 | print("2. Undo AI")
10 | choice = input("Enter 1 or 2: ").strip()
11 |
12 | if choice not in ['1', '2']:
13 | print("Invalid input. Please enter 1 or 2.")
14 | return action_prompt()
15 |
16 | return choice
17 |
18 | def select_file():
19 | root = tk.Tk()
20 | root.withdraw() # Hide the main tkinter window
21 |
22 | file_path = filedialog.askopenfilename(
23 | title="Select a .plist file",
24 | filetypes=(("plist files", "*.plist"), ("all files", "*.*"))
25 | )
26 |
27 | if not file_path:
28 | print("No file selected. Exiting.")
29 | exit()
30 |
31 | return file_path
32 |
33 | def modify_plist(file_path, action_choice):
34 | script_dir = os.path.dirname(os.path.abspath(__file__))
35 | destination_folder = os.path.join(script_dir, 'tweak', 'files')
36 | original_file_path = os.path.join(destination_folder, 'plistbackup')
37 | normal_file_path = os.path.join(destination_folder, 'com.apple.MobileGestalt.plist')
38 | noai_file_path = os.path.join(destination_folder, 'noai.plist')
39 |
40 | if not os.path.exists(destination_folder):
41 | os.makedirs(destination_folder)
42 |
43 | if action_choice == '1':
44 | # Enable AI
45 | if os.path.exists(normal_file_path):
46 | os.rename(normal_file_path, original_file_path)
47 | print(f"Original file backed up as '{original_file_path}'")
48 |
49 | # Load and modify plist
50 | tree = etree.parse(file_path)
51 | root = tree.getroot()
52 | cache_extra_found = False
53 | key_found = False
54 |
55 | # Namespaces are used in plist files, so we need to handle them
56 | ns = {'': 'http://www.apple.com/DTDs/PropertyList-1.0.dtd'}
57 |
58 | # Iterate through the XML elements to find and modify
59 | for elem in root.iter():
60 | if elem.tag == 'key' and elem.text == 'CacheExtra':
61 | cache_extra_found = True
62 | # Create and insert the new key-value pair
63 | for next_elem in elem.itersiblings():
64 | if next_elem.tag == 'dict':
65 | # Add new keys and values
66 | new_key = etree.Element('key')
67 | new_key.text = 'A62OafQ85EJAiiqKn4agtg'
68 | new_value = etree.Element('integer')
69 | new_value.text = '1'
70 | next_elem.append(new_key)
71 | next_elem.append(new_value)
72 | print("Added A62OafQ85EJAiiqKn4agtg with value 1 under CacheExtra")
73 | break
74 |
75 | if elem.tag == 'key' and elem.text == 'h9jDsbgj7xIVeIQ8S3/X3Q':
76 | key_found = True
77 | next_elem = elem.getnext()
78 | if next_elem is not None and next_elem.tag == 'string':
79 | next_elem.text = 'iPhone16,2'
80 | print("Updated h9jDsbgj7xIVeIQ8S3/X3Q to 'iPhone16,2'")
81 |
82 | if not cache_extra_found:
83 | # If 'CacheExtra' is not found, add it with the required elements
84 | cache_extra_elem = etree.Element('key')
85 | cache_extra_elem.text = 'CacheExtra'
86 | dict_elem = etree.Element('dict')
87 | cache_extra_elem.append(dict_elem)
88 | root.append(cache_extra_elem)
89 |
90 | # Add required key-value pairs
91 | new_key = etree.Element('key')
92 | new_key.text = 'A62OafQ85EJAiiqKn4agtg'
93 | new_value = etree.Element('integer')
94 | new_value.text = '1'
95 | dict_elem.append(new_key)
96 | dict_elem.append(new_value)
97 | print("Added CacheExtra with A62OafQ85EJAiiqKn4agtg and 1")
98 |
99 | if not key_found:
100 | # Add the 'h9jDsbgj7xIVeIQ8S3/X3Q' key if not found
101 | new_key = etree.Element('key')
102 | new_key.text = 'h9jDsbgj7xIVeIQ8S3/X3Q'
103 | new_value = etree.Element('string')
104 | new_value.text = 'iPhone16,2'
105 | root.append(new_key)
106 | root.append(new_value)
107 | print("Added h9jDsbgj7xIVeIQ8S3/X3Q with value 'iPhone16,2'")
108 |
109 | tree.write(file_path, xml_declaration=True, encoding='utf-8')
110 |
111 | # Rename modified file and copy it
112 | os.rename(file_path, normal_file_path)
113 | print(f"File copied and renamed to '{normal_file_path}'")
114 |
115 | # Create a copy named noai.plist
116 | with open(normal_file_path, 'r', encoding='utf-8') as file:
117 | content = file.read()
118 |
119 | with open(noai_file_path, 'w', encoding='utf-8') as file:
120 | file.write(content)
121 | print(f"Modified file saved as '{noai_file_path}'")
122 |
123 | elif action_choice == '2':
124 | # Undo AI
125 | if os.path.exists(normal_file_path):
126 | os.remove(normal_file_path)
127 | print(f"Deleted existing file '{normal_file_path}'")
128 |
129 | if os.path.exists(noai_file_path):
130 | os.rename(noai_file_path, normal_file_path)
131 | print(f"Replaced with 'noai.plist' as '{normal_file_path}'")
132 | else:
133 | print(f"'noai.plist' not found in '{destination_folder}'. Prompting for file selection.")
134 |
135 | # Prompt for file selection if 'noai.plist' doesn't exist
136 | plist_file = select_file()
137 | if plist_file:
138 | # Use the selected file to undo AI
139 | os.rename(plist_file, noai_file_path)
140 | print(f"Selected file '{plist_file}' saved as '{noai_file_path}'")
141 | os.rename(noai_file_path, normal_file_path)
142 | print(f"Replaced with selected file as '{normal_file_path}'")
143 | else:
144 | print("No file selected. Exiting.")
145 |
146 | def main():
147 | action_choice = action_prompt()
148 |
149 | plist_file = None
150 | if action_choice == '1':
151 | plist_file = select_file()
152 | print("Select your mobilegestalt file")
153 |
154 | modify_plist(plist_file, action_choice)
155 |
156 | if __name__ == "__main__":
157 | main()
158 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | How to use:
2 | open cmd and cd to the path where you extracted the files to. run pip install pymobiledevice3 tk lxml then run python aienabler.py, select your mobilegestalt file and tell it if your on a ipad or iphone. then it will patch ur mobilegest then run 2.py with your device plugged in and boom,
3 | https://discord.gg/drfshayK5J
4 |
5 | Disclaimer
6 | This is a proof of concept and not a "hack" for apple devices, apple dont sue me
7 |
--------------------------------------------------------------------------------
/backend/__pycache__/device_manager.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nightfallenxyz/AIEnabler/215a192797199fe0a8f3ecd374eec9417e2b8d27/backend/__pycache__/device_manager.cpython-312.pyc
--------------------------------------------------------------------------------
/backend/__pycache__/device_manager.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nightfallenxyz/AIEnabler/215a192797199fe0a8f3ecd374eec9417e2b8d27/backend/__pycache__/device_manager.cpython-39.pyc
--------------------------------------------------------------------------------
/backend/__pycache__/funcs.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nightfallenxyz/AIEnabler/215a192797199fe0a8f3ecd374eec9417e2b8d27/backend/__pycache__/funcs.cpython-312.pyc
--------------------------------------------------------------------------------
/backend/device_manager.py:
--------------------------------------------------------------------------------
1 | from pymobiledevice3 import usbmux
2 | from pymobiledevice3.lockdown import create_using_usbmux
3 |
4 | class DeviceManager:
5 | def __init__(self):
6 | self.devices = []
7 | self.get_devices()
8 | if len(self.devices) != 0:
9 | self.set_device(0)
10 |
11 | def get_devices(self):
12 | self.devices.clear()
13 | connected_devices = usbmux.list_devices()
14 | uuids = set() # added to avoid devices getting added multiple times
15 |
16 | for device in connected_devices:
17 | if device.serial in uuids:
18 | continue
19 |
20 | try:
21 | ld = create_using_usbmux(serial=device.serial)
22 | vals = ld.all_values
23 | dev = {
24 | "uuid": device.serial,
25 | "name": vals['DeviceName'],
26 | "version": vals['ProductVersion'],
27 | "model": vals['ProductType'],
28 | "locale": ld.locale,
29 | "ld": ld
30 | }
31 | self.devices.append(dev)
32 | uuids.add(device.serial)
33 | except Exception as e:
34 | print(f"ERROR with lockdown device with UUID {device.serial}")
35 |
36 | if len(self.devices) == 0:
37 | print("No devices found!")
38 | else:
39 | print(f"{len(self.devices)} device(s) found!")
40 |
41 | def set_device(self, device):
42 | if device != None:
43 | self.device = self.devices[device]
44 | else:
45 | self.device = None
--------------------------------------------------------------------------------
/backend/funcs.py:
--------------------------------------------------------------------------------
1 | from exploit.restore import FileToRestore
2 |
3 | def prompt(options):
4 | print("Select an option:")
5 | print("\n".join(options))
6 | choice = input("Enter your choice: ")
7 | return choice
8 |
9 | def retrieve_restore_files(eligibility_data, config_data):
10 | files = [
11 | FileToRestore(contents=eligibility_data, restore_path="/var/db/os_eligibility/", restore_name="eligibility.plist"),
12 | FileToRestore(contents=config_data, restore_path="/var/MobileAsset/AssetsV2/com_apple_MobileAsset_OSEligibility/purpose_auto/c55a421c053e10233e5bfc15c42fa6230e5639a9.asset/AssetData/", restore_name="Config.plist")
13 | ]
14 | return files
15 |
--------------------------------------------------------------------------------
/backend/mainwindow.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
2 | QMetaObject, QObject, QPoint, QRect,
3 | QSize, QTime, QUrl, Qt)
4 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
5 | QFont, QFontDatabase, QGradient, QIcon,
6 | QImage, QKeySequence, QLinearGradient, QPainter,
7 | QPalette, QPixmap, QRadialGradient, QTransform)
8 | from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFrame,
9 | QHBoxLayout, QLabel, QLineEdit, QMainWindow,
10 | QProgressBar, QScrollArea, QSizePolicy, QSpacerItem,
11 | QStackedWidget, QToolButton, QVBoxLayout, QWidget)
12 | import webbrowser
13 |
14 | class MainWindow(QMainWindow):
15 | def __init__(self, deviceManager, tweak):
16 | super().__init__()
17 |
18 | self.dev_manager = deviceManager
19 | self.tweak = tweak
20 | self.setWindowTitle("EUEnabler")
21 | self.setFixedSize(960, 540)
22 | self.setWindowIcon(QIcon("icon.png"))
23 | screen_geometry = QApplication.primaryScreen().availableGeometry()
24 |
25 | x = (screen_geometry.width() - self.width()) // 2
26 | y = (screen_geometry.height() - self.height()) // 2
27 |
28 | self.move(x, y)
29 | self.setupUI()
30 |
31 | def setupUI(self):
32 | self.main = QWidget(self)
33 |
34 | self.logoBtn = QToolButton(self.main)
35 | self.logoBtn.setObjectName(u"logoBtn")
36 | self.logoBtn.setFixedSize(200, 200)
37 | self.logoBtn.move((self.width() - self.logoBtn.width()) // 2, 50)
38 | self.logoBtn.setStyleSheet(u"QToolButton {\n"
39 | " background-color: transparent;\n"
40 | " padding: 0px;\n"
41 | " border: none;\n"
42 | "}")
43 |
44 | icon = QIcon()
45 | icon.addFile(u"icon.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
46 | self.logoBtn.setIcon(icon)
47 | self.logoBtn.setIconSize(QSize(400, 400))
48 | self.logoBtn.clicked.connect(self.logoBtn_callback)
49 |
50 | self.EUText = QLabel("EUEnabler", self)
51 | font = QFont()
52 | font.setBold(True)
53 | font.setPointSize(24)
54 | self.EUText.setFont(font)
55 | self.EUText.setFixedSize(self.width(), 50)
56 | self.EUText.move(0, 245)
57 | self.EUText.setAlignment(Qt.AlignCenter)
58 |
59 | self.deviceComboBox = QComboBox(self)
60 | self.deviceComboBox.setFixedSize(300, 40)
61 | self.deviceComboBox.move((self.width() - self.deviceComboBox.width()) // 2, 295)
62 | self.deviceComboBox.currentIndexChanged.connect(self.deviceChanged)
63 | self.updateDeviceList()
64 |
65 | icon = QIcon()
66 | icon.addFile(u"icon/check-circle.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
67 | self.applyBtn = QToolButton(self)
68 | self.applyBtn.setObjectName(u"applyBtn")
69 | self.applyBtn.setFixedSize(36, 36)
70 | self.applyBtn.setStyleSheet(u"QToolButton {\n"
71 | " background-color: transparent;\n"
72 | " padding: 0px;\n"
73 | " border: none;\n"
74 | "}")
75 | self.applyBtn.setIcon(icon)
76 | self.applyBtn.setIconSize(QSize(36, 36))
77 | self.applyBtn.clicked.connect(self.applyBtn_callback)
78 | self.applyBtn.move((self.width() - self.applyBtn.width()) // 2, 478)
79 |
80 | self.setCentralWidget(self.main)
81 |
82 | def updateDeviceList(self):
83 | self.deviceComboBox.clear()
84 |
85 | for idx, device in enumerate(self.dev_manager.devices):
86 | self.deviceComboBox.addItem(f" {device.get('name')}", idx)
87 |
88 | if self.dev_manager.devices:
89 | self.deviceComboBox.setCurrentIndex(0)
90 |
91 | def deviceChanged(self, index):
92 | selected_device = self.deviceComboBox.itemData(index)
93 | self.dev_manager.set_device(selected_device)
94 |
95 | def logoBtn_callback(self):
96 | webbrowser.open("https://github.com/nightfallenxyz/EUEnabler-ASH")
97 |
98 | def applyBtn_callback(self):
99 | self.tweak.apply()
100 |
101 | def keyPressEvent(self, event):
102 | if event.key() == Qt.Key_R:
103 | self.dev_manager.get_devices()
104 | self.updateDeviceList()
--------------------------------------------------------------------------------
/exploit/__init__.py:
--------------------------------------------------------------------------------
1 | from tempfile import TemporaryDirectory
2 | from pathlib import Path
3 |
4 | from pymobiledevice3.lockdown import create_using_usbmux
5 | from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service
6 | from pymobiledevice3.exceptions import PyMobileDevice3Exception
7 | from pymobiledevice3.services.diagnostics import DiagnosticsService
8 | from pymobiledevice3.lockdown import LockdownClient
9 |
10 | from . import backup
11 |
12 | def perform_restore(backup: backup.Backup, reboot: bool = False, lockdown_client: LockdownClient = None):
13 | try:
14 | with TemporaryDirectory() as backup_dir:
15 | backup.write_to_directory(Path(backup_dir))
16 |
17 | if lockdown_client == None:
18 | lockdown_client = create_using_usbmux()
19 | with Mobilebackup2Service(lockdown_client) as mb:
20 | mb.restore(backup_dir, system=True, reboot=False, copy=False, source=".")
21 | except PyMobileDevice3Exception as e:
22 | if "Find My" in str(e):
23 | print("Find My must be disabled in order to use this tool.")
24 | print("Disable Find My from Settings (Settings -> [Your Name] -> Find My) and then try again.")
25 | raise e
26 | elif "crash_on_purpose" not in str(e):
27 | raise e
28 | else:
29 | if reboot and lockdown_client != None:
30 | print("Success! Rebooting your device...")
31 | with DiagnosticsService(lockdown_client) as diagnostics_service:
32 | diagnostics_service.restart()
33 | print("Remember to turn Find My back on!")
--------------------------------------------------------------------------------
/exploit/__pycache__/__init__.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nightfallenxyz/AIEnabler/215a192797199fe0a8f3ecd374eec9417e2b8d27/exploit/__pycache__/__init__.cpython-312.pyc
--------------------------------------------------------------------------------
/exploit/__pycache__/backup.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nightfallenxyz/AIEnabler/215a192797199fe0a8f3ecd374eec9417e2b8d27/exploit/__pycache__/backup.cpython-312.pyc
--------------------------------------------------------------------------------
/exploit/__pycache__/mbdb.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nightfallenxyz/AIEnabler/215a192797199fe0a8f3ecd374eec9417e2b8d27/exploit/__pycache__/mbdb.cpython-312.pyc
--------------------------------------------------------------------------------
/exploit/__pycache__/restore.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nightfallenxyz/AIEnabler/215a192797199fe0a8f3ecd374eec9417e2b8d27/exploit/__pycache__/restore.cpython-312.pyc
--------------------------------------------------------------------------------
/exploit/backup.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from datetime import datetime
3 | import plistlib
4 | from pathlib import Path
5 | from base64 import b64decode
6 | from hashlib import sha1
7 | from . import mbdb
8 | from .mbdb import _FileMode
9 | from random import randbytes
10 | from typing import Optional
11 |
12 | # RWX:RX:RX
13 | DEFAULT = _FileMode.S_IRUSR | _FileMode.S_IWUSR | _FileMode.S_IXUSR | _FileMode.S_IRGRP | _FileMode.S_IXGRP | _FileMode.S_IROTH | _FileMode.S_IXOTH
14 |
15 | @dataclass
16 | class BackupFile:
17 | path: str
18 | domain: str
19 |
20 | def to_record(self) -> mbdb.MbdbRecord:
21 | raise NotImplementedError()
22 |
23 | @dataclass
24 | class ConcreteFile(BackupFile):
25 | contents: bytes
26 | owner: int = 0
27 | group: int = 0
28 | inode: Optional[int] = None
29 | mode: _FileMode = DEFAULT
30 |
31 | def to_record(self) -> mbdb.MbdbRecord:
32 | if self.inode is None:
33 | self.inode = int.from_bytes(randbytes(8), "big")
34 | return mbdb.MbdbRecord(
35 | domain=self.domain,
36 | filename=self.path,
37 | link="",
38 | hash=sha1(self.contents).digest(),
39 | key=b"",
40 | mode=self.mode | _FileMode.S_IFREG,
41 | #unknown2=0,
42 | #unknown3=0,
43 | inode=self.inode,
44 | user_id=self.owner,
45 | group_id=self.group,
46 | mtime=int(datetime.now().timestamp()),
47 | atime=int(datetime.now().timestamp()),
48 | ctime=int(datetime.now().timestamp()),
49 | size=len(self.contents),
50 | flags=4,
51 | properties=[]
52 | )
53 |
54 | @dataclass
55 | class Directory(BackupFile):
56 | owner: int = 0
57 | group: int = 0
58 | mode: _FileMode = DEFAULT
59 |
60 | def to_record(self) -> mbdb.MbdbRecord:
61 | return mbdb.MbdbRecord(
62 | domain=self.domain,
63 | filename=self.path,
64 | link="",
65 | hash=b"",
66 | key=b"",
67 | mode=self.mode | _FileMode.S_IFDIR,
68 | #unknown2=0,
69 | #unknown3=0,
70 | inode=0, # inode is not respected for directories
71 | user_id=self.owner,
72 | group_id=self.group,
73 | mtime=int(datetime.now().timestamp()),
74 | atime=int(datetime.now().timestamp()),
75 | ctime=int(datetime.now().timestamp()),
76 | size=0,
77 | flags=4,
78 | properties=[]
79 | )
80 |
81 | @dataclass
82 | class SymbolicLink(BackupFile):
83 | target: str
84 | owner: int = 0
85 | group: int = 0
86 | inode: Optional[int] = None
87 | mode: _FileMode = DEFAULT
88 |
89 | def to_record(self) -> mbdb.MbdbRecord:
90 | if self.inode is None:
91 | self.inode = int.from_bytes(randbytes(8), "big")
92 | return mbdb.MbdbRecord(
93 | domain=self.domain,
94 | filename=self.path,
95 | link=self.target,
96 | hash=b"",
97 | key=b"",
98 | mode=self.mode | _FileMode.S_IFLNK,
99 | #unknown2=0,
100 | #unknown3=0,
101 | inode=self.inode,
102 | user_id=self.owner,
103 | group_id=self.group,
104 | mtime=int(datetime.now().timestamp()),
105 | atime=int(datetime.now().timestamp()),
106 | ctime=int(datetime.now().timestamp()),
107 | size=0,
108 | flags=4,
109 | properties=[]
110 | )
111 |
112 | @dataclass
113 | class Backup:
114 | files: list[BackupFile]
115 |
116 | def write_to_directory(self, directory: Path):
117 | for file in self.files:
118 | if isinstance(file, ConcreteFile):
119 | #print("Writing", file.path, "to", directory / sha1((file.domain + "-" + file.path).encode()).digest().hex())
120 | with open(directory / sha1((file.domain + "-" + file.path).encode()).digest().hex(), "wb") as f:
121 | f.write(file.contents)
122 |
123 | with open(directory / "Manifest.mbdb", "wb") as f:
124 | f.write(self.generate_manifest_db().to_bytes())
125 |
126 | with open(directory / "Status.plist", "wb") as f:
127 | f.write(self.generate_status())
128 |
129 | with open(directory / "Manifest.plist", "wb") as f:
130 | f.write(self.generate_manifest())
131 |
132 | with open(directory / "Info.plist", "wb") as f:
133 | f.write(plistlib.dumps({}))
134 |
135 |
136 | def generate_manifest_db(self): # Manifest.mbdb
137 | records = []
138 | for file in self.files:
139 | records.append(file.to_record())
140 | return mbdb.Mbdb(records=records)
141 |
142 | def generate_status(self) -> bytes: # Status.plist
143 | return plistlib.dumps({
144 | "BackupState": "new",
145 | "Date": datetime.fromisoformat("1970-01-01T00:00:00+00:00"),
146 | "IsFullBackup": False,
147 | "SnapshotState": "finished",
148 | "UUID": "00000000-0000-0000-0000-000000000000",
149 | "Version": "2.4"
150 | })
151 |
152 | def generate_manifest(self) -> bytes: # Manifest.plist
153 | return plistlib.dumps({
154 | "BackupKeyBag": b64decode("""
155 | VkVSUwAAAAQAAAAFVFlQRQAAAAQAAAABVVVJRAAAABDud41d1b9NBICR1BH9JfVtSE1D
156 | SwAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV1JBUAAA
157 | AAQAAAAAU0FMVAAAABRY5Ne2bthGQ5rf4O3gikep1e6tZUlURVIAAAAEAAAnEFVVSUQA
158 | AAAQB7R8awiGR9aba1UuVahGPENMQVMAAAAEAAAAAVdSQVAAAAAEAAAAAktUWVAAAAAE
159 | AAAAAFdQS1kAAAAoN3kQAJloFg+ukEUY+v5P+dhc/Welw/oucsyS40UBh67ZHef5ZMk9
160 | UVVVSUQAAAAQgd0cg0hSTgaxR3PVUbcEkUNMQVMAAAAEAAAAAldSQVAAAAAEAAAAAktU
161 | WVAAAAAEAAAAAFdQS1kAAAAoMiQTXx0SJlyrGJzdKZQ+SfL124w+2Tf/3d1R2i9yNj9z
162 | ZCHNJhnorVVVSUQAAAAQf7JFQiBOS12JDD7qwKNTSkNMQVMAAAAEAAAAA1dSQVAAAAAE
163 | AAAAAktUWVAAAAAEAAAAAFdQS1kAAAAoSEelorROJA46ZUdwDHhMKiRguQyqHukotrxh
164 | jIfqiZ5ESBXX9txi51VVSUQAAAAQfF0G/837QLq01xH9+66vx0NMQVMAAAAEAAAABFdS
165 | QVAAAAAEAAAAAktUWVAAAAAEAAAAAFdQS1kAAAAol0BvFhd5bu4Hr75XqzNf4g0fMqZA
166 | ie6OxI+x/pgm6Y95XW17N+ZIDVVVSUQAAAAQimkT2dp1QeadMu1KhJKNTUNMQVMAAAAE
167 | AAAABVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo2N2DZarQ6GPoWRgTiy/t
168 | djKArOqTaH0tPSG9KLbIjGTOcLodhx23xFVVSUQAAAAQQV37JVZHQFiKpoNiGmT6+ENM
169 | QVMAAAAEAAAABldSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAofe2QSvDC2cV7
170 | Etk4fSBbgqDx5ne/z1VHwmJ6NdVrTyWi80Sy869DM1VVSUQAAAAQFzkdH+VgSOmTj3yE
171 | cfWmMUNMQVMAAAAEAAAAB1dSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo7kLY
172 | PQ/DnHBERGpaz37eyntIX/XzovsS0mpHW3SoHvrb9RBgOB+WblVVSUQAAAAQEBpgKOz9
173 | Tni8F9kmSXd0sENMQVMAAAAEAAAACFdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kA
174 | AAAo5mxVoyNFgPMzphYhm1VG8Fhsin/xX+r6mCd9gByF5SxeolAIT/ICF1VVSUQAAAAQ
175 | rfKB2uPSQtWh82yx6w4BoUNMQVMAAAAEAAAACVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAA
176 | AFdQS1kAAAAo5iayZBwcRa1c1MMx7vh6lOYux3oDI/bdxFCW1WHCQR/Ub1MOv+QaYFVV
177 | SUQAAAAQiLXvK3qvQza/mea5inss/0NMQVMAAAAEAAAACldSQVAAAAAEAAAAA0tUWVAA
178 | AAAEAAAAAFdQS1kAAAAoD2wHX7KriEe1E31z7SQ7/+AVymcpARMYnQgegtZD0Mq2U55u
179 | xwNr2FVVSUQAAAAQ/Q9feZxLS++qSe/a4emRRENMQVMAAAAEAAAAC1dSQVAAAAAEAAAA
180 | A0tUWVAAAAAEAAAAAFdQS1kAAAAocYda2jyYzzSKggRPw/qgh6QPESlkZedgDUKpTr4Z
181 | Z8FDgd7YoALY1g=="""),
182 | "Lockdown": {},
183 | "SystemDomainsVersion": "20.0",
184 | "Version": "9.1"
185 | })
--------------------------------------------------------------------------------
/exploit/mbdb.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from io import BytesIO
3 |
4 | # Mode bitfield
5 | from enum import IntFlag
6 | class _FileMode(IntFlag):
7 | S_IFMT = 0o0170000
8 | S_IFIFO = 0o0010000
9 | S_IFCHR = 0o0020000
10 | S_IFDIR = 0o0040000
11 | S_IFBLK = 0o0060000
12 | S_IFREG = 0o0100000
13 | S_IFLNK = 0o0120000
14 | S_IFSOCK = 0o0140000
15 |
16 | #S_IRWXU = 0o0000700
17 | S_IRUSR = 0o0000400
18 | S_IWUSR = 0o0000200
19 | S_IXUSR = 0o0000100
20 |
21 | #S_IRWXG = 0o0000070
22 | S_IRGRP = 0o0000040
23 | S_IWGRP = 0o0000020
24 | S_IXGRP = 0o0000010
25 |
26 | #S_IRWXO = 0o0000007
27 | S_IROTH = 0o0000004
28 | S_IWOTH = 0o0000002
29 | S_IXOTH = 0o0000001
30 |
31 | S_ISUID = 0o0004000
32 | S_ISGID = 0o0002000
33 | S_ISVTX = 0o0001000
34 |
35 | @dataclass
36 | class MbdbRecord:
37 | domain: str
38 | filename: str
39 | link: str
40 | hash: bytes
41 | key: bytes
42 | mode: _FileMode
43 | inode: int
44 | user_id: int
45 | group_id: int
46 | mtime: int
47 | atime: int
48 | ctime: int
49 | size: int
50 | flags: int
51 | properties: list
52 |
53 | @classmethod
54 | def from_stream(cls, d: BytesIO):
55 | #d = BytesIO(data)
56 |
57 | domain_len = int.from_bytes(d.read(2), "big")
58 | domain = d.read(domain_len).decode("utf-8")
59 |
60 | filename_len = int.from_bytes(d.read(2), "big")
61 | filename = d.read(filename_len).decode("utf-8")
62 |
63 | link_len = int.from_bytes(d.read(2), "big")
64 | link = d.read(link_len).decode("utf-8") if link_len != 0xffff else ""
65 |
66 | hash_len = int.from_bytes(d.read(2), "big")
67 | hash = d.read(hash_len) if hash_len != 0xffff else b""
68 |
69 | key_len = int.from_bytes(d.read(2), "big")
70 | key = d.read(key_len) if key_len != 0xffff else b""
71 |
72 | mode = _FileMode(int.from_bytes(d.read(2), "big"))
73 | #unknown2 = int.from_bytes(d.read(4), "big")
74 | #unknown3 = int.from_bytes(d.read(4), "big")
75 | inode = int.from_bytes(d.read(8), "big")
76 | user_id = int.from_bytes(d.read(4), "big")
77 | group_id = int.from_bytes(d.read(4), "big")
78 | mtime = int.from_bytes(d.read(4), "big")
79 | atime = int.from_bytes(d.read(4), "big")
80 | ctime = int.from_bytes(d.read(4), "big")
81 | size = int.from_bytes(d.read(8), "big")
82 | flags = int.from_bytes(d.read(1), "big")
83 |
84 | properties_count = int.from_bytes(d.read(1), "big")
85 | properties = []
86 |
87 | for _ in range(properties_count):
88 | name_len = int.from_bytes(d.read(2), "big")
89 | name = d.read(name_len).decode("utf-8") if name_len != 0xffff else ""
90 |
91 | value_len = int.from_bytes(d.read(2), "big")
92 | value = d.read(value_len).decode("utf-8") if value_len != 0xffff else ""
93 |
94 | properties.append((name, value))
95 |
96 | return cls(domain, filename, link, hash, key, mode, inode, user_id, group_id, mtime, atime, ctime, size, flags, properties)
97 |
98 | def to_bytes(self) -> bytes:
99 | d = BytesIO()
100 |
101 | d.write(len(self.domain).to_bytes(2, "big"))
102 | d.write(self.domain.encode("utf-8"))
103 |
104 | d.write(len(self.filename).to_bytes(2, "big"))
105 | d.write(self.filename.encode("utf-8"))
106 |
107 | d.write(len(self.link).to_bytes(2, "big"))
108 | d.write(self.link.encode("utf-8"))
109 |
110 | d.write(len(self.hash).to_bytes(2, "big"))
111 | d.write(self.hash)
112 |
113 | d.write(len(self.key).to_bytes(2, "big"))
114 | d.write(self.key)
115 |
116 | d.write(self.mode.to_bytes(2, "big"))
117 | #d.write(self.unknown2.to_bytes(4, "big"))
118 | #d.write(self.unknown3.to_bytes(4, "big"))
119 | d.write(self.inode.to_bytes(8, "big"))
120 | d.write(self.user_id.to_bytes(4, "big"))
121 | d.write(self.group_id.to_bytes(4, "big"))
122 | d.write(self.mtime.to_bytes(4, "big"))
123 | d.write(self.atime.to_bytes(4, "big"))
124 | d.write(self.ctime.to_bytes(4, "big"))
125 | d.write(self.size.to_bytes(8, "big"))
126 | d.write(self.flags.to_bytes(1, "big"))
127 |
128 | d.write(len(self.properties).to_bytes(1, "big"))
129 |
130 | for name, value in self.properties:
131 | d.write(len(name).to_bytes(2, "big"))
132 | d.write(name.encode("utf-8"))
133 |
134 | d.write(len(value).to_bytes(2, "big"))
135 | d.write(value.encode("utf-8"))
136 |
137 | return d.getvalue()
138 |
139 | @dataclass
140 | class Mbdb:
141 | records: list[MbdbRecord]
142 |
143 | @classmethod
144 | def from_bytes(cls, data: bytes):
145 | d = BytesIO(data)
146 |
147 | if d.read(4) != b"mbdb":
148 | raise ValueError("Invalid MBDB file")
149 |
150 | if d.read(2) != b"\x05\x00":
151 | raise ValueError("Invalid MBDB version")
152 |
153 | records = []
154 | while d.tell() < len(data):
155 | records.append(MbdbRecord.from_stream(d))
156 |
157 | return cls(records)
158 |
159 | def to_bytes(self) -> bytes:
160 | d = BytesIO()
161 |
162 | d.write(b"mbdb")
163 | d.write(b"\x05\x00")
164 |
165 | for record in self.records:
166 | d.write(record.to_bytes())
167 |
168 | return d.getvalue()
--------------------------------------------------------------------------------
/exploit/restore.py:
--------------------------------------------------------------------------------
1 | from . import backup, perform_restore
2 | from pymobiledevice3.lockdown import LockdownClient
3 |
4 | class FileToRestore:
5 | def __init__(self, contents: str, restore_path: str, restore_name: str, owner: int = 501, group: int = 501):
6 | self.contents = contents
7 | self.restore_path = restore_path
8 | self.restore_name = restore_name
9 | self.owner = owner
10 | self.group = group
11 |
12 | # files is a list of FileToRestore objects
13 | def restore_files(files: list, reboot: bool = False, lockdown_client: LockdownClient = None):
14 | # create the files to be backed up
15 | files_list = [
16 | backup.Directory("", "RootDomain"),
17 | backup.Directory("Library", "RootDomain"),
18 | backup.Directory("Library/Preferences", "RootDomain"),
19 | ]
20 | # create the links
21 | for file_num in range(len(files)):
22 | files_list.append(backup.ConcreteFile(
23 | f"Library/Preferences/temp{file_num}",
24 | "RootDomain",
25 | owner=files[file_num].owner,
26 | group=files[file_num].group,
27 | contents=files[file_num].contents,
28 | inode=file_num
29 | ))
30 | # add the file paths
31 | for file_num in range(len(files)):
32 | file = files[file_num]
33 | base_path = "/var/backup"
34 | # set it to work in the separate volumes (prevents a bootloop)
35 | if file.restore_path.startswith("/var/mobile/"):
36 | # required on iOS 17.0+ since /var/mobile is on a separate partition
37 | base_path = "/var/mobile/backup"
38 | elif file.restore_path.startswith("/private/var/mobile/"):
39 | base_path = "/private/var/mobile/backup"
40 | elif file.restore_path.startswith("/private/var/"):
41 | base_path = "/private/var/backup"
42 | files_list.append(backup.Directory(
43 | "",
44 | f"SysContainerDomain-../../../../../../../..{base_path}{file.restore_path}",
45 | owner=file.owner,
46 | group=file.group
47 | ))
48 | files_list.append(backup.ConcreteFile(
49 | "",
50 | f"SysContainerDomain-../../../../../../../..{base_path}{file.restore_path}{file.restore_name}",
51 | owner=file.owner,
52 | group=file.group,
53 | contents=b"",
54 | inode=file_num
55 | ))
56 | # break the hard links
57 | for file_num in range(len(files)):
58 | files_list.append(backup.ConcreteFile(
59 | "",
60 | f"SysContainerDomain-../../../../../../../../var/.backup.i/var/root/Library/Preferences/temp{file_num}",
61 | owner=501,
62 | group=501,
63 | contents=b"",
64 | )) # Break the hard link
65 | files_list.append(backup.ConcreteFile("", "SysContainerDomain-../../../../../../../.." + "/crash_on_purpose", contents=b""))
66 |
67 | # create the backup
68 | back = backup.Backup(files=files_list)
69 |
70 | perform_restore(backup=back, reboot=reboot, lockdown_client=lockdown_client)
71 |
72 |
73 | def restore_file(fp: str, restore_path: str, restore_name: str, reboot: bool = False, lockdown_client: LockdownClient = None):
74 | # open the file and read the contents
75 | contents = open(fp, "rb").read()
76 |
77 | base_path = "/var/backup"
78 | if restore_path.startswith("/var/mobile/"):
79 | # required on iOS 17.0+ since /var/mobile is on a separate partition
80 | base_path = "/var/mobile/backup"
81 |
82 | # create the backup
83 | back = backup.Backup(files=[
84 | backup.Directory("", "RootDomain"),
85 | backup.Directory("Library", "RootDomain"),
86 | backup.Directory("Library/Preferences", "RootDomain"),
87 | backup.ConcreteFile("Library/Preferences/temp", "RootDomain", owner=501, group=501, contents=contents, inode=0),
88 | backup.Directory(
89 | "",
90 | f"SysContainerDomain-../../../../../../../..{base_path}{restore_path}",
91 | owner=501,
92 | group=501
93 | ),
94 | backup.ConcreteFile(
95 | "",
96 | f"SysContainerDomain-../../../../../../../..{base_path}{restore_path}{restore_name}",
97 | owner=501,
98 | group=501,
99 | contents=b"",
100 | inode=0
101 | ),
102 | backup.ConcreteFile(
103 | "",
104 | "SysContainerDomain-../../../../../../../../var/.backup.i/var/root/Library/Preferences/temp",
105 | owner=501,
106 | group=501,
107 | contents=b"",
108 | ), # Break the hard link
109 | backup.ConcreteFile("", "SysContainerDomain-../../../../../../../.." + "/crash_on_purpose", contents=b""),
110 | ])
111 |
112 |
113 | perform_restore(backup=back, reboot=reboot, lockdown_client=lockdown_client)
--------------------------------------------------------------------------------
/tweak/__pycache__/eligibility.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nightfallenxyz/AIEnabler/215a192797199fe0a8f3ecd374eec9417e2b8d27/tweak/__pycache__/eligibility.cpython-312.pyc
--------------------------------------------------------------------------------
/tweak/eligibility.py:
--------------------------------------------------------------------------------
1 | from exploit.restore import FileToRestore, restore_files
2 | import json
3 | from pathlib import Path
4 | from pymobiledevice3.services.installation_proxy import InstallationProxyService
5 |
6 | class EUTweak:
7 | def __init__(self, method="2"):
8 | print("Initializing...")
9 | self.files = []
10 | self.set_method(method)
11 |
12 | def set_method(self, method):
13 | self.method = method
14 |
15 | def setup_variables(self, dev_manager):
16 | try:
17 | with open(Path.joinpath(Path.cwd(), 'tweak/files/restore.json'), 'r') as json_file:
18 | json_data = json.load(json_file)
19 |
20 | for file_info in json_data["restore_files"]:
21 | file_to_restore_empty = FileToRestore(
22 | contents=b'',
23 | restore_path=f"/{file_info['path']}",
24 | restore_name=file_info["file"]
25 | )
26 | file_to_restore = FileToRestore(
27 | contents=open(Path.joinpath(Path.cwd(), f'tweak/files/{file_info["file"]}'), 'rb').read(),
28 | restore_path=f"/{file_info['path']}",
29 | restore_name=file_info["file"]
30 | )
31 | self.files.append({
32 | "file_to_restore": file_to_restore,
33 | "file_to_restore_empty": file_to_restore_empty,
34 | "file_info": file_info
35 | })
36 |
37 | if not self.files:
38 | print("No valid files to restore.")
39 |
40 | except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
41 | print(f"Error during setup: {e}")
42 |
43 | def apply(self, dev_manager):
44 | self.setup_variables(dev_manager)
45 | for file_entry in self.files:
46 | file_to_restore = file_entry["file_to_restore"]
47 | file_info = file_entry["file_info"]
48 | print(f"Restoring {file_info.get('file')} to {file_info.get('path')}")
49 | restore_files([file_to_restore], reboot=False, lockdown_client=dev_manager.device.get("ld"))
50 | restore_files([], reboot=True, lockdown_client=dev_manager.device.get("ld"))
51 |
--------------------------------------------------------------------------------
/tweak/files/eligibility.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | OS_ELIGIBILITY_DOMAIN_CALCIUM
6 |
7 | os_eligibility_answer_source_t
8 | 1
9 | os_eligibility_answer_t
10 | 2
11 | status
12 |
13 | OS_ELIGIBILITY_INPUT_CHINA_CELLULAR
14 | 2
15 |
16 |
17 | OS_ELIGIBILITY_DOMAIN_GREYMATTER
18 |
19 | context
20 |
21 | OS_ELIGIBILITY_CONTEXT_ELIGIBLE_DEVICE_LANGUAGES
22 |
23 | en
24 |
25 |
26 | os_eligibility_answer_source_t
27 | 1
28 | os_eligibility_answer_t
29 | 4
30 | status
31 |
32 | OS_ELIGIBILITY_INPUT_DEVICE_LANGUAGE
33 | 3
34 | OS_ELIGIBILITY_INPUT_DEVICE_REGION_CODE
35 | 3
36 | OS_ELIGIBILITY_INPUT_EXTERNAL_BOOT_DRIVE
37 | 3
38 | OS_ELIGIBILITY_INPUT_GENERATIVE_MODEL_SYSTEM
39 | 3
40 | OS_ELIGIBILITY_INPUT_SHARED_IPAD
41 | 3
42 | OS_ELIGIBILITY_INPUT_SIRI_LANGUAGE
43 | 3
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/tweak/files/restore.json:
--------------------------------------------------------------------------------
1 | {
2 | "restore_files": [
3 | {
4 | "file": "com.apple.MobileGestalt.plist",
5 | "path": "/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/"
6 | },
7 | {
8 | "file": "eligibility.plist",
9 | "path": "var/db/os_eligibility/"
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------