├── .gitignore ├── LICENSE ├── README.md ├── bundlegen ├── inferius ├── requirements.txt └── utils ├── api.py ├── bundle.py ├── dependencies.py ├── device.py ├── ipsw.py ├── manifest.py └── restore.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | env/ 3 | FirmwareBundles/ 4 | IPSW/ 5 | 6 | .DS_Store 7 | futurerestore_error.log 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 m1sta 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inferius 2 | [![License](https://img.shields.io/github/license/m1stadev/Inferius)](https://github.com/m1stadev/Inferius) 3 | [![Stars](https://img.shields.io/github/stars/m1stadev/Inferius)]((https://github.com/m1stadev/Inferius)) 4 | 5 | Inferius is an [xpwn](https://github.com/m1stadev/xpwn)-like tool to create & restore custom IPSWs to 64-bit devices. 6 | 7 | Its current purpose is to downgrade devices (vulnerable to [checkm8](https://github.com/axi0mX/ipwndfu)) to previous iOS versions. However, there are other possible uses for this tool as well. 8 | 9 | ## Notes and Caveats 10 | Before using Inferius, keep in mind that 11 | - **No one but YOU** is fully responsible for any data loss or damage caused to your device 12 | - Downgrades are currently limited to versions compatible with the latest signed SEP version. 13 | - Due to the downgrades being tethered, after restoring a custom IPSW you must patch the bootchain & send it to your device over pwned DFU manually to boot, as described [here](https://dualbootfun.github.io/), or use one of these tools to automate the process for you: 14 | - [Ramiel](https://ramiel.app/) 15 | - [checkra1n](https://checkra.in) 16 | - [PyBoot](https://github.com/MatthewPierson/PyBoot) 17 | - [ra1nsn0w](https://github.com/tihmstar/ra1nsn0w) 18 | 19 | By default, firmware bundles are automatically downloaded from an [external repo](https://github.com/m1stadev/inferius-ext/tree/master/bundles). However, if there isn't a firmware bundle for the device+iOS version combo you're attempting to downgrade to, you'll need to create your own using [bundlegen](https://github.com/m1stadev/Inferius#inferius-bundle-generator). 20 | 21 | [Pull requests](https://github.com/m1stadev/inferius-ext/compare) for new firmware bundles are welcome, as long as the firmware bundle you want to add can create a usable IPSW for the targeted version. 22 | 23 | ## Usage 24 | ```py 25 | ./inferius -d 'Identifier' -f 'IPSW' [-c/-r] [-b 'BUNDLE'] 26 | ``` 27 | 28 | | Option (short) | Option (long) | Description | 29 | |-----------------|-----------------------|------------------------------------------| 30 | | `-d IDENTIFIER` | `--device IDENTIFIER` | Device identifier | 31 | | `-f IPSW` | `--ipsw IPSW` | Path to IPSW | 32 | | `-c` | `--create` | Create custom IPSW | 33 | | `-r` | `--restore` | Restore custom IPSW | 34 | | `-b` | `--bundle BUNDLE` | (Optional) Path to local Firmware Bundle | 35 | | `-u` | `--update` | Keep data while restoring custom IPSW | 36 | 37 | ## Requirements 38 | - A computer running macOS or Linux 39 | - At least **10gbs** of free space on your computer 40 | - An Internet connection 41 | - A 64-bit device (vulnerable to [checkm8](https://github.com/axi0mX/ipwndfu)) 42 | - A firmware bundle for your device & the iOS version to be downgraded to 43 | - If there isn't a firmware bundle for your device + iOS version combo, look at [bundlegen](https://github.com/m1stadev/Inferius#inferius-bundle-generator) 44 | - [libusb](https://libusb.info/) 45 | - [futurerestore](https://github.com/m1stadev/futurerestore) 46 | - futurerestore must be compiled with [my fork of img4tool](https://github.com/m1stadev/img4tool), or else it can't be used with Inferius. 47 | - [libirecovery](https://github.com/libimobiledevice/libirecovery) 48 | - [tsschecker](https://github.com/1Conan/tsschecker) 49 | - Python dependencies: 50 | - `pip3 install -r requirements.txt` 51 | 52 | ## To-Do 53 | - Implement iOS 10 downgrades for A7 devices. 54 | - Update bundle documentation 55 | 56 | # Inferius Bundle Generator 57 | 58 | ## Usage 59 | ```py 60 | ./bundlegen -d 'Identifier' -i 'iOS Version' 61 | ``` 62 | 63 | | Option (short) | Option (long) | Description | 64 | |-----------------|-----------------------|-------------------| 65 | | `-d IDENTIFIER` | `--device IDENTIFIER` | Device identifier | 66 | | `-i VERSION` | `--version VERSION` | iOS version | 67 | 68 | ## Requirements 69 | - A computer running macOS 70 | - At least **250mbs** of free space on your computer 71 | - An Internet connection 72 | - [asr64_patcher](https://github.com/exploit3dguy/asr64_patcher) 73 | - A compiled binary can be found [here](https://github.com/exploit3dguy/asr64_patcher/releases) 74 | - [img4lib](https://github.com/xerub/img4lib) 75 | - A compiled binary can be found [here](https://github.com/xerub/img4lib/releases) 76 | - [kairos](https://github.com/dayt0n/kairos) 77 | - A compiled binary can be found [here](https://github.com/dayt0n/kairos/releases) 78 | - [Kernel64Patcher](https://github.com/Ralph0045/Kernel64Patcher) 79 | - [Link Identity Editor](https://github.com/sbingner/ldid) 80 | - A compiled binary can be found [here](https://github.com/sbingner/ldid/releases) 81 | - Python dependencies: 82 | - `pip3 install -r requirements.txt` 83 | 84 | ## Special thanks 85 | - [exploit3d](https://twitter.com/exploit3dguy) for [asr64_patcher](https://github.com/exploit3dguy/asr64_patcher) 86 | - [NotHereForTheDong](https://github.com/NotHereForTheDong) for creating many Firmware Bundles and beta testing 87 | - [tale](https://twitter.com/aarnavtale), [Chibibowa](https://twitter.com/Chibibowa), and [Moses](https://twitter.com/MosesBuckwalter) for beta testing 88 | 89 | Finally, if you need help or have any questions about Inferius, join my [Discord server](https://m1sta.xyz/discord). 90 | -------------------------------------------------------------------------------- /bundlegen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from argparse import ArgumentParser 4 | from mwclient import Site 5 | from remotezip import RemoteZip 6 | from tempfile import TemporaryDirectory 7 | import bsdiff4 8 | import json 9 | import os 10 | import platform 11 | import plistlib 12 | import requests 13 | import shutil 14 | import subprocess 15 | import sys 16 | import time 17 | 18 | 19 | def check_bin(binary): 20 | if shutil.which(binary) is None: 21 | sys.exit(f"[ERROR] '{binary}' was not found on your system. Exiting.") 22 | 23 | def create_im4p(file, output, tag=None, patch=None): 24 | args = [ 25 | 'img4', 26 | '-i', 27 | file, 28 | '-o', 29 | output 30 | ] 31 | 32 | if tag: 33 | args.append('-A') 34 | args.append('-T') 35 | args.append(tag) 36 | 37 | if patch: 38 | args.append('-P') 39 | args.append(patch) 40 | 41 | img4 = subprocess.run(args, stdout=subprocess.DEVNULL) 42 | if img4.returncode != 0: 43 | sys.exit(f"[ERROR] Packing '{file}' into im4p container failed. Exiting.") 44 | 45 | def decrypt(file, output, kbag=None): 46 | args = [ 47 | 'img4', 48 | '-i', 49 | file, 50 | '-o', 51 | output 52 | ] 53 | 54 | if kbag: 55 | args.append('-k') 56 | args.append(kbag) 57 | 58 | decrypt = subprocess.run(args, stdout=subprocess.DEVNULL) 59 | if decrypt.returncode != 0: 60 | sys.exit(f"[ERROR] Decrypting '{file}' failed. Exiting.") 61 | 62 | def diff_kernel(original, patched, output): 63 | with open(original, 'rb') as f: 64 | original = f.read() 65 | 66 | with open(patched, 'rb') as f: 67 | patched = f.read() 68 | 69 | diff = list() 70 | start_time = time.time() 71 | for i in range(len(original)): 72 | if round(time.time() - start_time) > 60: 73 | sys.exit("[ERROR] Failed to generate img4 patchfile for kernel. Exiting.") 74 | 75 | originalByte = original[i] 76 | patchedByte = patched[i] 77 | 78 | if originalByte != patchedByte: 79 | diff_info = (str(hex(i)), str(hex(originalByte)), str(hex(patchedByte))) 80 | diff.append(diff_info) 81 | 82 | with open(output, 'w') as f: 83 | for line in diff: 84 | f.write(f"{' '.join(line)}\n") 85 | 86 | def get_board(boards): 87 | if len(boards) == 1: 88 | return boards[0] 89 | 90 | print('NOTE: There are multiple boardconfigs for your device! Please choose the correct boardconfig for your device:') 91 | for board in range(len(boards)): 92 | print(f" {board + 1}: {boards[board]['boardconfig']}") 93 | 94 | board = input('Choice: ') 95 | try: 96 | return boards[int(board) - 1] 97 | except: 98 | sys.exit('[ERROR] Invalid input given. Exiting.') 99 | 100 | def get_firm(firms): 101 | if len(firms) == 1: 102 | return firms[0] 103 | 104 | print(f"NOTE: There are multiple builds for iOS {firms[0]['version']}! Please choose the version you wish to make a Firmware Bundle for:") 105 | for firm in range(len(firms)): 106 | print(f" {firm + 1}: {firms[firm]['buildid']}") 107 | 108 | firm = input('Choice: ') 109 | try: 110 | return firms[int(firm) - 1] 111 | except: 112 | sys.exit('[ERROR] Invalid input given. Exiting.') 113 | 114 | def mount_rdsk(rdsk, mountpoint): 115 | args = ( 116 | 'hdiutil', 117 | 'attach', 118 | rdsk, 119 | '-mountpoint', 120 | mountpoint 121 | ) 122 | 123 | mount = subprocess.run(args, stdout=subprocess.DEVNULL) 124 | if mount.returncode != 0: 125 | sys.exit(f"[ERROR] Mounting '{rdsk}' to '{mountpoint}' failed. Exiting.") 126 | 127 | def parse_keys(page): 128 | data = page.replace(' ', '').replace('|', '').splitlines() 129 | wiki_version = page.replace('|', '').splitlines()[1].split('=')[1][1:].replace('MasterGM', 'Master|GM') 130 | wikikeys = dict() 131 | 132 | for x in list(data): 133 | if x in (str(), '}}', '{{keys'): 134 | continue 135 | 136 | new_str = x.split('=') 137 | try: 138 | wikikeys[new_str[0].lower()] = new_str[1] 139 | except IndexError: 140 | continue 141 | 142 | wikikeys['version'] = wiki_version 143 | 144 | return wikikeys 145 | 146 | def patch_bootchain(file, output): 147 | args = ( 148 | 'kairos', 149 | file, 150 | output 151 | ) 152 | 153 | patch = subprocess.run(args, stdout=subprocess.DEVNULL) 154 | if patch.returncode != 0: 155 | sys.exit(f'[ERROR] Patching {file} failed. Exiting.') 156 | 157 | def patch_kernel(file, output): 158 | args = ( 159 | 'Kernel64Patcher', 160 | file, 161 | output, 162 | '-a' 163 | ) 164 | 165 | patch = subprocess.run(args, stdout=subprocess.DEVNULL) 166 | if patch.returncode != 0: 167 | sys.exit('[ERROR] Patching kernel failed. Exiting.') 168 | 169 | def patch_asr(file, output): 170 | args = ( 171 | 'asr64_patcher', 172 | file, 173 | output 174 | ) 175 | 176 | patch = subprocess.run(args, stdout=subprocess.DEVNULL) 177 | if patch.returncode != 0: 178 | sys.exit(f"[ERROR] Patching '{file}' failed. Exiting.") 179 | 180 | extract_ents = subprocess.run(f"ldid -e {file} > {file}.ents.plist", stdout=subprocess.DEVNULL, shell=True) 181 | if extract_ents.returncode != 0: 182 | sys.exit(f'[ERROR] Extracting {file} entitlements failed. Exiting.') 183 | 184 | args = ( 185 | 'ldid', 186 | f'-S{file}.ents.plist', 187 | output 188 | ) 189 | 190 | resign = subprocess.run(args, stdout=subprocess.DEVNULL) 191 | if resign.returncode != 0: 192 | sys.exit(f'[ERROR] Resigning {output} failed. Exiting.') 193 | 194 | def partialzip_extract(url, file, path): 195 | try: 196 | with RemoteZip(url) as f: 197 | f.extract(file, path) 198 | except: 199 | sys.exit(f"[ERROR] Failed to extract '{file}'. Exiting.") 200 | 201 | def partialzip_read(url, file): 202 | try: 203 | with RemoteZip(url) as f: 204 | return f.read(file) 205 | except: 206 | sys.exit(f"[ERROR] Failed to read '{file}'. Exiting.") 207 | 208 | def resize_rdsk(rdsk): 209 | new_size = round(os.path.getsize(rdsk) / float(1<<20)) + 1 210 | args = ( 211 | 'hdiutil', 212 | 'resize', 213 | '-size', 214 | f'{new_size}M', 215 | rdsk 216 | ) 217 | 218 | resize = subprocess.run(args, stdout=subprocess.DEVNULL) 219 | if resize.returncode != 0: 220 | sys.exit(f'[ERROR] Resizing ramdisk failed. Exiting.') 221 | 222 | def unmount_rdsk(mountpoint): 223 | args = ( 224 | 'hdiutil', 225 | 'detach', 226 | mountpoint 227 | ) 228 | 229 | unmount = subprocess.run(args, stdout=subprocess.DEVNULL) 230 | if unmount.returncode != 0: 231 | sys.exit(f"[ERROR] Unmounting ramdisk at '{mountpoint}' failed. Exiting.") 232 | 233 | def main(): 234 | parser = ArgumentParser(description='Inferius Bundle Generator', usage="bundlegen -d 'Identifier' -i 'iOS version'") 235 | parser.add_argument('-d', '--device', help='Device identifier', nargs=1) 236 | parser.add_argument('-i', '--version', help='iOS version', nargs=1) 237 | args = parser.parse_args() 238 | 239 | if platform.system() != 'Darwin': 240 | sys.exit('[ERROR] This script only works on macOS. Exiting.') 241 | 242 | if not args.device or not args.version: 243 | sys.exit(parser.print_help(sys.stderr)) 244 | 245 | check_bin('asr64_patcher') 246 | check_bin('img4') 247 | check_bin('kairos') 248 | check_bin('Kernel64Patcher') 249 | check_bin('ldid') 250 | 251 | identifier = 'P'.join(args.device[0].lower().split('p')) 252 | version = args.version[0] 253 | 254 | try: 255 | ipsw_api = requests.get(f'https://api.ipsw.me/v4/device/{identifier}?type=ipsw').json() 256 | except: 257 | sys.exit(f"[ERROR] '{identifier}' is not a valid device identifier. Exiting.") 258 | 259 | if any(identifier.startswith(device) for device in ('iPhone8', 'iPad6,1')): 260 | sys.exit('[ERROR] A9 devices are not currently supported. Exiting.') 261 | 262 | if not any(firm['version'] == version for firm in ipsw_api['firmwares']): 263 | sys.exit(f"[ERROR] 'iOS {version}' is not a valid iOS version. Exiting.") 264 | 265 | if int(version.split('.')[0]) < 10: 266 | sys.exit(f"[ERROR] iOS {version} is not supported at this time for firmware bundle creation. Exiting.") 267 | 268 | with TemporaryDirectory() as tmpdir: 269 | firm = get_firm([firm for firm in ipsw_api['firmwares'] if firm['version'] == version]) 270 | bundle_name = '_'.join((identifier, version, firm['buildid'])) 271 | bundle = f'{tmpdir}/{bundle_name}' 272 | os.mkdir(bundle) 273 | 274 | print(f"Creating Firmware Bundle for {identifier}, iOS {version}") 275 | bm = plistlib.loads(partialzip_read(firm['url'], 'BuildManifest.plist')) 276 | boardconfig = get_board(ipsw_api['boards'])['boardconfig'] 277 | identity = next(identity for identity in bm['BuildIdentities'] if identity['Info']['DeviceClass'].lower() == boardconfig.lower()) 278 | 279 | print('[1] Grabbing decryption keys...') 280 | keypage_title = f"{identity['Info']['BuildTrain']}_{firm['buildid']}_({identifier})" 281 | keypage = Site('www.theiphonewiki.com').pages[keypage_title] 282 | 283 | if not keypage.exists: 284 | sys.exit(f"[ERROR] Decryption keys for {identifier}, iOS {version} are not on The iPhone Wiki. Exiting.") 285 | 286 | keys = parse_keys(keypage.text()) 287 | 288 | print('[2] Patching bootchain...') 289 | for component in ('iBSS', 'iBEC'): 290 | file = { 291 | 'name': identity['Manifest'][component]['Info']['Path'].split('/')[-1], 292 | 'ipsw_path': identity['Manifest'][component]['Info']['Path'], 293 | 'path': f"{tmpdir}/{identity['Manifest'][component]['Info']['Path']}" 294 | } 295 | 296 | partialzip_extract(firm['url'], file['ipsw_path'], tmpdir) 297 | 298 | decrypt(file['path'], f"{file['path']}.raw", f"{keys[f'{component.lower()}iv']}{keys[f'{component.lower()}key']}") 299 | patch_bootchain(f"{file['path']}.raw", f"{file['path']}.pwn") 300 | create_im4p(f"{file['path']}.pwn", f"{file['path']}.im4p.pwn", component.lower()) 301 | 302 | bsdiff4.file_diff(file['path'], f"{file['path']}.im4p.pwn", f"{bundle}/{file['name'].rsplit('.', 1)[0]}.patch") 303 | 304 | print('[3] Patching kernel...') 305 | kernel = { 306 | 'name': identity['Manifest']['KernelCache']['Info']['Path'].split('/')[-1], 307 | 'ipsw_path': identity['Manifest']['KernelCache']['Info']['Path'], 308 | 'path': f"{tmpdir}/{identity['Manifest']['KernelCache']['Info']['Path']}" 309 | } 310 | 311 | partialzip_extract(firm['url'], kernel['ipsw_path'], tmpdir) 312 | 313 | decrypt(kernel['path'], f"{kernel['path']}.raw") 314 | patch_kernel(f"{kernel['path']}.raw", f"{kernel['path']}.pwn") 315 | diff_kernel(f"{kernel['path']}.raw", f"{kernel['path']}.pwn", f"{kernel['path']}.diff") 316 | create_im4p(kernel['path'], f"{kernel['path']}.im4p.pwn", patch=f"{kernel['path']}.diff") 317 | 318 | bsdiff4.file_diff(kernel['path'], f"{kernel['path']}.im4p.pwn", f"{bundle}/{kernel['name']}.patch") 319 | 320 | print('[4] Patching ramdisk. This may take a while, please wait...') 321 | ramdisk = { 322 | 'name': identity['Manifest']['RestoreRamDisk']['Info']['Path'].split('/')[-1], 323 | 'ipsw_path': identity['Manifest']['RestoreRamDisk']['Info']['Path'], 324 | 'path': f"{tmpdir}/{identity['Manifest']['RestoreRamDisk']['Info']['Path']}" 325 | } 326 | 327 | partialzip_extract(firm['url'], ramdisk['ipsw_path'], tmpdir) 328 | decrypt(ramdisk['path'], f"{ramdisk['path']}_rdsk.dmg") 329 | 330 | mount_rdsk(f"{ramdisk['path']}_rdsk.dmg", f'{tmpdir}/ramdisk') 331 | shutil.move(f'{tmpdir}/ramdisk/usr/sbin/asr', f'{tmpdir}/asr') 332 | unmount_rdsk(f'{tmpdir}/ramdisk') 333 | 334 | patch_asr(f'{tmpdir}/asr', f'{tmpdir}/asr.pwn') 335 | os.chmod(f'{tmpdir}/asr.pwn', 0o755) 336 | 337 | mount_rdsk(f"{ramdisk['path']}_rdsk.dmg", f'{tmpdir}/ramdisk') 338 | try: 339 | shutil.move(f'{tmpdir}/asr.pwn', f'{tmpdir}/ramdisk/usr/sbin/asr') 340 | except OSError: 341 | unmount_rdsk(f'{tmpdir}/ramdisk') 342 | resize_rdsk(f"{ramdisk['path']}_rdsk.dmg") 343 | mount_rdsk(f"{ramdisk['path']}_rdsk.dmg", f'{tmpdir}/ramdisk') 344 | shutil.move(f'{tmpdir}/asr.pwn', f'{tmpdir}/ramdisk/usr/sbin/asr') 345 | 346 | unmount_rdsk(f'{tmpdir}/ramdisk') 347 | create_im4p(f"{ramdisk['path']}_rdsk.dmg", f"{ramdisk['path']}.im4p", 'rdsk') 348 | bsdiff4.file_diff(ramdisk['path'], f"{ramdisk['path']}.im4p", f"{bundle}/{ramdisk['name'][:-4]}.asr.patch") 349 | 350 | print('[5] Making Firmware Bundle...') 351 | info = { 352 | 'identifier': identifier, 353 | 'buildid': firm['buildid'], 354 | 'update_support': False, 355 | 'boards': [ 356 | boardconfig 357 | ], 358 | 'patches': { 359 | 'required': [ 360 | { 361 | 'file': identity['Manifest']['iBSS']['Info']['Path'], 362 | 'patch': f"{identity['Manifest']['iBSS']['Info']['Path'].split('/')[-1].rsplit('.', 1)[0]}.patch" 363 | }, 364 | { 365 | 'file': identity['Manifest']['iBEC']['Info']['Path'], 366 | 'patch': f"{identity['Manifest']['iBEC']['Info']['Path'].split('/')[-1].rsplit('.', 1)[0]}.patch" 367 | }, 368 | { 369 | 'file': ramdisk['ipsw_path'], 370 | 'patch': f"{ramdisk['name'][:-4]}.asr.patch" 371 | }, 372 | { 373 | 'file': kernel['ipsw_path'], 374 | 'patch': f"{kernel['name']}.patch" 375 | } 376 | ] 377 | } 378 | } 379 | 380 | with open(f'{bundle}/Info.json', 'w') as f: 381 | json.dump(info, f, indent=4) 382 | 383 | os.makedirs('FirmwareBundles', exist_ok=True) 384 | bundle_path = f'FirmwareBundles/{bundle_name}.bundle' 385 | if os.path.isfile(bundle_path): 386 | os.remove(bundle_path) 387 | 388 | shutil.make_archive(bundle_path, 'zip', bundle) 389 | os.rename(f'{bundle_path}.zip', bundle_path) 390 | 391 | print(f"Finished creating Firmware Bundle for {identifier}, iOS {version}: '{bundle_path}'.") 392 | 393 | if __name__ == '__main__': 394 | main() 395 | -------------------------------------------------------------------------------- /inferius: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from utils.api import API 4 | from utils.bundle import Bundle 5 | from utils.dependencies import Checks 6 | from utils.device import Device 7 | from utils.ipsw import IPSW 8 | from utils.manifest import Manifest, RestoreManifest 9 | from utils.restore import Restore 10 | import argparse 11 | import platform 12 | import os 13 | import sys 14 | import tempfile 15 | 16 | 17 | def create_ipsw(api, buildmanifest, ipsw_path, tmpdir, bundle_path): 18 | print('Creating custom IPSW') 19 | 20 | bundle = Bundle() 21 | ipsw = IPSW(ipsw_path) 22 | 23 | print('[1] Grabbing Firmware Bundle...') 24 | if bundle_path is not None: 25 | print('Note: Using user provided Firmware Bundle.') 26 | if bundle.verify_bundle(bundle_path, tmpdir, api.api, buildmanifest.buildid, api.board) == False: 27 | sys.exit(f"[ERROR] Bundle '{bundle_path}' is invalid. Exiting.") 28 | 29 | else: 30 | bundle.fetch_bundle(api.device, buildmanifest.version, buildmanifest.buildid, tmpdir) 31 | 32 | print('[2] Verifying IPSW...') 33 | ipsw.verify_ipsw(api.fetch_sha1(buildmanifest.buildid)) 34 | 35 | print('[3] Extracting IPSW...') 36 | extracted_ipsw = f'{tmpdir}/ipsw' 37 | os.mkdir(extracted_ipsw) 38 | ipsw.extract_ipsw(extracted_ipsw) 39 | 40 | print('[4] Patching components...') 41 | bundle.apply_patches(extracted_ipsw) 42 | 43 | buildid = api.api['firmwares'][0]['buildid'] 44 | latest_manifest = Manifest(api.partialzip_read(buildid, 'BuildManifest.plist')) 45 | api.partialzip_extract(buildid, latest_manifest.fetch_component_path(api.board, 'LLB'), extracted_ipsw) 46 | bootloader_ver = api.partialzip_extract(buildid, latest_manifest.fetch_component_path(api.board, 'iBoot'), extracted_ipsw) 47 | 48 | print('[5] Repacking IPSW...') 49 | custom_ipsw = ipsw.create_ipsw(extracted_ipsw, f"{ipsw_path.split('/')[-1].rsplit('.', 1)[0]}_custom.ipsw", bundle.check_update_support(), bootloader_ver) 50 | print(f"Finished creating custom IPSW: '{custom_ipsw}'.") 51 | 52 | return custom_ipsw 53 | 54 | def restore_ipsw(api, buildmanifest, ipsw_path, updating, tmpdir): 55 | print('Restoring custom IPSW') 56 | 57 | Checks() 58 | device = Device(api.device) 59 | ipsw = IPSW(ipsw_path) 60 | restore = Restore(api.device, device.platform) 61 | 62 | print('[1] Verifying custom IPSW...') 63 | ipsw.verify_custom_ipsw(api.device, updating) 64 | 65 | print('[2] Checking for device in pwned DFU...') 66 | device.check_pwndfu() 67 | 68 | print('[3] Extracting bootchain...') 69 | ibss = buildmanifest.fetch_component_path(device.board, 'iBSS') 70 | ipsw.extract_file(ibss, f'{tmpdir}/ibss.im4p') 71 | ibec = buildmanifest.fetch_component_path(device.board, 'iBEC') 72 | ipsw.extract_file(ibec, f'{tmpdir}/ibec.im4p') 73 | 74 | print('[4] Signing bootchain...') 75 | restore.save_blobs(device.ecid, device.board, tmpdir) 76 | restore.sign_component(f'{tmpdir}/ibss.im4p', f'{tmpdir}/ibss.img4') 77 | restore.sign_component(f'{tmpdir}/ibec.im4p', f'{tmpdir}/ibec.img4') 78 | 79 | print('[5] Sending bootchain...') 80 | restore.send_component(f'{tmpdir}/ibss.img4', 'iBSS') 81 | restore.send_component(f'{tmpdir}/ibec.img4', 'iBEC') 82 | 83 | print('[6] Saving SHSH blobs...') 84 | restore.save_blobs(device.ecid, device.board, tmpdir, device.fetch_apnonce()) 85 | 86 | print('[7] Restoring...') 87 | restore.restore(ipsw_path, device.baseband, updating) 88 | print(f'Finished restoring pwned iOS {buildmanifest.version} IPSW to your device. Please boot your iOS device using one of the tools listed in the README.') 89 | 90 | def main(): 91 | parser = argparse.ArgumentParser(description='Inferius - Create & Restore 64-bit custom IPSWs', usage="inferius -d 'identifier' -f 'IPSW' [-c/-r] [-b 'BUNDLE']") 92 | parser.add_argument('-d', '--device', help='Device identifier', nargs=1) 93 | parser.add_argument('-f', '--ipsw', help='Path to IPSW', nargs=1) 94 | parser.add_argument('-c', '--create', help='Create custom IPSW', action='store_true') 95 | parser.add_argument('-r', '--restore', help='Restore custom IPSW', action='store_true') 96 | parser.add_argument('-b', '--bundle', help='(Optional) Path to local Firmware Bundle', nargs=1) 97 | parser.add_argument('-u', '--update', help='Keep data while restoring custom IPSW', action='store_true') 98 | args = parser.parse_args() 99 | 100 | if (not args.device or not args.ipsw) or \ 101 | (not args.create and not args.restore) or \ 102 | (args.update and not args.restore) or \ 103 | (args.bundle and not args.create): 104 | sys.exit(parser.print_help(sys.stderr)) 105 | 106 | if platform.system() == 'Windows': 107 | sys.exit('[ERROR] Inferius does not support Windows. Exiting.') 108 | 109 | identifier = args.device[0] 110 | ipsw_path = args.ipsw[0] 111 | 112 | api = API() 113 | api.check_device(identifier) 114 | api.fetch_api() 115 | api.get_board() 116 | 117 | ipsw = IPSW(ipsw_path) 118 | restoremanifest = RestoreManifest(ipsw.read_file('Restore.plist'), api.board) 119 | if restoremanifest.platform not in (0x8960, 0x7000, 0x7001, 0x8000, 0x8001, 0x8003, 0x8010, 0x8011, 0x8015): 120 | sys.exit(f"[ERROR] '{identifier}' is not supported by Inferius. Exiting.") 121 | 122 | buildmanifest = Manifest(ipsw.read_file('BuildManifest.plist')) 123 | 124 | ver_major = int(buildmanifest.version.split('.')[0]) 125 | if ver_major == 10 and restoremanifest.platform != 0x8960: 126 | sys.exit(f'[ERROR] iOS 10 downgrades are only supported on A7 devices. Exiting.') 127 | 128 | elif not 11 <= ver_major <= 14: 129 | sys.exit(f'[ERROR] iOS {buildmanifest.version} is not supported by Inferius. Exiting.') 130 | 131 | if identifier not in buildmanifest.supported_devices: 132 | sys.exit(f"[ERROR] IPSW '{ipsw_path}' does not support {identifier}. Exiting.") 133 | 134 | if args.bundle: 135 | bundle = args.bundle[0] 136 | else: 137 | bundle = None 138 | 139 | with tempfile.TemporaryDirectory() as tmpdir: 140 | if args.create: 141 | custom_ipsw = create_ipsw(api, buildmanifest, ipsw_path, tmpdir, bundle) 142 | else: 143 | custom_ipsw = ipsw_path 144 | 145 | if args.restore: 146 | restore_ipsw(api, buildmanifest, custom_ipsw, args.update, tmpdir) 147 | 148 | 149 | if __name__ == '__main__': 150 | main() 151 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bsdiff4 2 | mwclient 3 | remotezip 4 | requests 5 | pyusb -------------------------------------------------------------------------------- /utils/api.py: -------------------------------------------------------------------------------- 1 | import remotezip 2 | import requests 3 | import sys 4 | 5 | 6 | class API: 7 | def check_device(self, identifier): 8 | api = requests.get('https://api.ipsw.me/v4/devices').json() 9 | 10 | if identifier not in [device['identifier'] for device in api]: 11 | sys.exit(f"[ERROR] '{identifier}' does not exist. Exiting.") 12 | 13 | self.device = identifier 14 | 15 | def is_signed(self, version): 16 | return any(firm['signed'] == True for firm in self.api['firmwares'] if firm['version'] == version) 17 | 18 | def check_version(self, version): 19 | if not any(firm['version'] == version for firm in self.api['firmwares']): 20 | sys.exit(f"[ERROR] '{version}' does not exist. Exiting.") 21 | 22 | def fetch_api(self, identifier=None): 23 | self.api = requests.get(f'https://api.ipsw.me/v4/device/{identifier if identifier else self.device}?type=ipsw').json() 24 | 25 | def fetch_sha1(self, buildid): 26 | return next(firm['sha1sum'] for firm in self.api['firmwares'] if firm['buildid'] == buildid) 27 | 28 | def get_board(self): 29 | boards = [board['boardconfig'] for board in self.api['boards']] 30 | if len(boards) == 1: 31 | self.board = boards[0] 32 | return 33 | 34 | print('There are multiple boardconfigs for your device! Please choose the correct boardconfig for your device:') 35 | for board in range(len(boards)): 36 | print(f" {board + 1}: {boards[board]}") 37 | 38 | board = input('Choice: ') 39 | try: 40 | board = int(board) - 1 41 | except: 42 | sys.exit('[ERROR] Invalid input given. Exiting.') 43 | else: 44 | if board not in range(len(boards)): 45 | sys.exit('[ERROR] Invalid input given. Exiting.') 46 | 47 | self.board = boards[board] 48 | 49 | def partialzip_extract(self, buildid, component, path): 50 | firm = next(firm for firm in self.api['firmwares'] if firm['buildid'] == buildid) 51 | with remotezip.RemoteZip(firm['url']) as ipsw: 52 | ipsw.extract(component, path) 53 | 54 | return firm['version'] 55 | 56 | def partialzip_read(self, buildid, component): 57 | firm = next(firm for firm in self.api['firmwares'] if firm['buildid'] == buildid) 58 | with remotezip.RemoteZip(firm['url']) as ipsw: 59 | return ipsw.read(component) 60 | -------------------------------------------------------------------------------- /utils/bundle.py: -------------------------------------------------------------------------------- 1 | import bsdiff4 2 | import io 3 | import json 4 | import os 5 | import requests 6 | import sys 7 | import zipfile 8 | 9 | 10 | class Bundle: 11 | def apply_patches(self, ipsw): 12 | with open(f'{self.bundle}/Info.json', 'r') as f: 13 | bundle_data = json.load(f) 14 | 15 | for patches in bundle_data['patches']: 16 | if patches != 'required': 17 | apply_patch = input(f"[NOTE] Would you like to apply '{patches}' patch to your custom IPSW? [Y\\N]: ").lower() 18 | if (apply_patch not in ('y', 'n')) or (apply_patch == 'n'): 19 | continue 20 | 21 | for patch in bundle_data['patches'][patches]: 22 | bsdiff4.file_patch_inplace(f"{ipsw}/{patch['file']}", f"{self.bundle}/{patch['patch']}") 23 | 24 | def check_update_support(self): 25 | with open(f'{self.bundle}/Info.json', 'r') as f: 26 | bundle_data = json.load(f) 27 | 28 | return bundle_data['update_support'] 29 | 30 | def fetch_bundle(self, device, version, buildid, path): 31 | bundle_name = f'{device}_{version}_{buildid}' 32 | bundle = requests.get(f'https://github.com/m1stadev/inferius-ext/raw/master/bundles/{bundle_name}.bundle') 33 | if bundle.status_code == 404: 34 | sys.exit(f'[ERROR] A Firmware Bundle does not exist for {device}, iOS {version}. Exiting.') 35 | 36 | output = f'{path}/{bundle_name}' 37 | with zipfile.ZipFile(io.BytesIO(bundle.content), 'r') as f: 38 | try: 39 | f.extractall(output) 40 | except OSError: 41 | sys.exit('[ERROR] Ran out of storage while extracting Firmware Bundle. Exiting.') 42 | 43 | self.bundle = output 44 | 45 | def fetch_ota_manifest(self, device, path): 46 | manifest = requests.get(f'https://raw.githubusercontent.com/m1stadev/inferius-ext/master/manifests/BuildManifest_{device}.plist') 47 | if manifest.status_code == 404: 48 | sys.exit(f'[ERROR] An OTA manifest does not exist for {device}. Exiting.') 49 | 50 | with open(path, 'wb') as f: 51 | try: 52 | f.write(manifest.content) 53 | except OSError: 54 | sys.exit('[ERROR] Ran out of storage while writing OTA manifest. Exiting.') 55 | 56 | def verify_bundle(self, bundle, tmpdir, api, buildid, boardconfig): 57 | if not zipfile.is_zipfile(bundle): 58 | return False 59 | 60 | try: 61 | with zipfile.ZipFile(bundle, 'r') as f: 62 | try: 63 | bundle_data = json.loads(f.read('Info.json')) 64 | except: 65 | return False 66 | 67 | if not any(firm['buildid'] == buildid for firm in api['firmwares']): 68 | return False 69 | 70 | if not any(board.lower() == boardconfig.lower() for board in bundle_data['boards']): 71 | return False 72 | 73 | except: 74 | return False 75 | 76 | bundle_path = f"{tmpdir}/{bundle.split('/')[-1].rsplit('.', 1)[0]}" 77 | os.mkdir(bundle_path) 78 | with zipfile.ZipFile(bundle) as f: 79 | f.extractall(bundle_path) 80 | 81 | self.bundle = bundle_path 82 | return True 83 | -------------------------------------------------------------------------------- /utils/dependencies.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | import sys 4 | 5 | 6 | class Checks: 7 | def __init__(self): 8 | self.check_bin('futurerestore') 9 | self.check_bin('tsschecker') 10 | self.check_bin('irecovery') 11 | self.check_bin('img4tool') 12 | 13 | def check_bin(self, binary): 14 | if shutil.which(binary) is None: 15 | sys.exit(f"[ERROR] '{binary}' is not installed on your system. Exiting.") 16 | 17 | if binary == 'futurerestore': 18 | fr_ver = subprocess.run((binary), stdout=subprocess.PIPE, universal_newlines=True).stdout 19 | if '-m1sta' not in fr_ver.splitlines()[1]: 20 | sys.exit(f"[ERROR] This futurerestore build cannot be used with Inferius. Exiting.") 21 | 22 | elif binary == 'irecovery': 23 | try: 24 | subprocess.check_call((binary, '-V'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 25 | except subprocess.CalledProcessError: 26 | sys.exit(f"[ERROR] Your irecovery version is too old. Exiting.") 27 | -------------------------------------------------------------------------------- /utils/device.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import subprocess 4 | import sys 5 | import usb, usb.backend.libusb1 6 | 7 | 8 | class Device: 9 | def __init__(self, identifier): 10 | self.device = identifier 11 | self.baseband = self.check_baseband() 12 | self.backend = self.get_backend() 13 | self.platform = self.fetch_platform() 14 | self.board = self.fetch_boardconfig() 15 | self.apnonce = self.fetch_apnonce() 16 | self.ecid = self.fetch_ecid() 17 | 18 | def check_baseband(self): 19 | if self.device.startswith('iPhone'): 20 | return True 21 | 22 | return self.device in ( # All (current) 64-bit cellular iPads vulerable to checkm8. 23 | 'iPad4,2', 24 | 'iPad4,3', 25 | 'iPad4,5', 26 | 'iPad4,6', 27 | 'iPad4,8', 28 | 'iPad4,9', 29 | 'iPad5,2', 30 | 'iPad5,4', 31 | 'iPad6,8', 32 | 'iPad6,4', 33 | 'iPad7,2', 34 | 'iPad7,4', 35 | 'iPad8,3', 36 | 'iPad8,4', 37 | 'iPad8,7', 38 | 'iPad8,8', 39 | 'iPad8,10', 40 | 'iPad8,12', 41 | 'iPad11,2', 42 | 'iPad11,4', 43 | 'iPad13,2', 44 | ) 45 | 46 | def check_pwndfu(self): 47 | device = usb.core.find(idVendor=0x5AC, idProduct=0x1227, backend=self.backend) 48 | if device is None: 49 | sys.exit('[ERROR] Device in DFU mode not found. Exiting.') 50 | 51 | if 'PWND:' not in device.serial_number: 52 | sys.exit('[ERROR] Attempting to restore a device not in Pwned DFU mode. Exiting.') 53 | 54 | def fetch_apnonce(self): 55 | irecv = subprocess.check_output(('irecovery', '-q'), universal_newlines=True) 56 | line = next(l for l in irecv.splitlines() if 'NONC:' in l) 57 | return line.split(' ')[1] 58 | 59 | def get_backend(self): # Attempt to find a libusb 1.0 library to use as pyusb's backend, exit if one isn't found. 60 | directories = ('/usr/lib', '/opt/procursus/lib', '/usr/local/lib') # Common library directories to search 61 | 62 | libusb1 = None 63 | for libdir in directories: 64 | for file in glob.glob(f'{libdir}/**', recursive=True): 65 | if os.path.isdir(file) or (not any(ext in file for ext in ('so', 'dylib'))): 66 | continue 67 | 68 | if 'libusb-1.0' in file: 69 | libusb1 = file 70 | break 71 | 72 | else: 73 | continue 74 | 75 | break 76 | 77 | if libusb1 is None: 78 | sys.exit('[ERROR] libusb is not installed. Install libusb. Exiting.') 79 | 80 | return usb.backend.libusb1.get_backend(find_library=lambda x:libusb1) 81 | 82 | def fetch_boardconfig(self): 83 | irecv = subprocess.run(('irecovery', '-qv'), stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines=True).stderr 84 | line = next(l for l in irecv.splitlines() if 'Connected to' in l) 85 | return line.split(', ')[1].replace('model ', '') 86 | 87 | def fetch_ecid(self): 88 | device = usb.core.find(idVendor=0x5AC, idProduct=0x1227, backend=self.backend) 89 | if device is None: 90 | sys.exit('[ERROR] Device in DFU mode not found. Exiting.') 91 | 92 | ecid = device.serial_number.split(' ')[5].split(':')[1] 93 | 94 | for x in ecid: 95 | if x == '0': 96 | ecid = ecid[1:] 97 | else: 98 | break 99 | 100 | return ecid 101 | 102 | def fetch_platform(self): 103 | device = usb.core.find(idVendor=0x5AC, idProduct=0x1227, backend=self.backend) 104 | if device is None: 105 | sys.exit('[ERROR] Device in DFU mode not found. Exiting.') 106 | 107 | return int(device.serial_number.split(' ')[0].split(':')[1]) 108 | -------------------------------------------------------------------------------- /utils/ipsw.py: -------------------------------------------------------------------------------- 1 | from utils.api import API 2 | import hashlib 3 | import json 4 | import os 5 | import shutil 6 | import sys 7 | import zipfile 8 | 9 | 10 | class IPSW: 11 | def __init__(self, ipsw): 12 | self.ipsw = ipsw 13 | 14 | def create_ipsw(self, path, output, update, bootloader): 15 | os.makedirs('IPSW', exist_ok=True) 16 | 17 | info = { 18 | 'update_support': update, 19 | 'bootloader': bootloader 20 | } 21 | 22 | with open(f'{path}/.Inferius', 'w') as f: 23 | json.dump(info, f) 24 | 25 | custom_ipsw = f'IPSW/{output}' 26 | try: 27 | shutil.make_archive(custom_ipsw, 'zip', path) 28 | except: 29 | sys.exit('[ERROR] Failed to create custom IPSW. Exiting.') 30 | 31 | os.rename(f'{custom_ipsw}.zip', custom_ipsw) 32 | return custom_ipsw 33 | 34 | def extract_file(self, file, output): 35 | try: 36 | with zipfile.ZipFile(self.ipsw, 'r') as ipsw, open(output, 'wb') as f: 37 | f.write(ipsw.read(file)) 38 | except: 39 | sys.exit(f"[ERROR] Failed to extract '{file}' from IPSW. Exiting.") 40 | 41 | def extract_ipsw(self, path): 42 | with zipfile.ZipFile(self.ipsw, 'r') as ipsw: 43 | try: 44 | ipsw.extractall(path) 45 | except: 46 | sys.exit(f"[ERROR] Failed to extract '{self.ipsw}'. Exiting.") 47 | 48 | def read_file(self, file): 49 | try: 50 | with zipfile.ZipFile(self.ipsw, 'r') as ipsw: 51 | return ipsw.read(file) 52 | 53 | except: 54 | sys.exit(f"[ERROR] Failed to read '{file}' from IPSW. Exiting.") 55 | 56 | def verify_ipsw(self, ipsw_sha1): 57 | if not os.path.isfile(self.ipsw): 58 | sys.exit(f"[ERROR] '{self.ipsw}' does not exist. Exiting.") 59 | 60 | if not zipfile.is_zipfile(self.ipsw): 61 | sys.exit(f"[ERROR] '{self.ipsw}' is not a valid IPSW. Exiting.") 62 | 63 | with zipfile.ZipFile(self.ipsw, 'r') as ipsw: 64 | if '.Inferius' in ipsw.namelist(): 65 | sys.exit(f"[ERROR] '{self.ipsw}' is not a stock IPSW. Exiting.") 66 | 67 | sha1 = hashlib.sha1() 68 | with open(self.ipsw, 'rb') as ipsw: 69 | fbuf = ipsw.read(8192) 70 | while len(fbuf) != 0: 71 | sha1.update(fbuf) 72 | fbuf = ipsw.read(8192) 73 | 74 | if ipsw_sha1 != sha1.hexdigest(): 75 | sys.exit(f"[ERROR] '{self.ipsw}' is not a valid IPSW. Exiting.") 76 | 77 | def verify_custom_ipsw(self, device, update): 78 | if not os.path.isfile(self.ipsw): 79 | sys.exit(f"[ERROR] '{self.ipsw}' does not exist. Exiting.") 80 | 81 | if not zipfile.is_zipfile(self.ipsw): 82 | sys.exit(f"[ERROR] '{self.ipsw}' is not a valid IPSW. Exiting.") 83 | 84 | with zipfile.ZipFile(self.ipsw, 'r') as ipsw: 85 | if '.Inferius' not in ipsw.namelist(): 86 | sys.exit(f"[ERROR] '{self.ipsw}' is not a custom IPSW. Exiting.") 87 | 88 | info = json.loads(ipsw.read('.Inferius')) 89 | 90 | if (info['update_support'] == False) and (update == True): 91 | sys.exit('[ERROR] This IPSW does not have support for update restores. Exiting.') 92 | 93 | api = API() 94 | api.fetch_api(device) 95 | if api.is_signed(info['bootloader']) == False: 96 | sys.exit('[ERROR] This IPSW is too old to be used with Inferius. Create a new custom IPSW. Exiting.') 97 | -------------------------------------------------------------------------------- /utils/manifest.py: -------------------------------------------------------------------------------- 1 | import plistlib 2 | 3 | 4 | class Manifest: 5 | def __init__(self, manifest): 6 | self.manifest = plistlib.loads(manifest) 7 | self.version = self.fetch_version() 8 | self.buildid = self.fetch_buildid() 9 | self.supported_devices = self.fetch_supported_devices() 10 | 11 | def fetch_buildid(self): return self.manifest['ProductBuildVersion'] 12 | 13 | def fetch_component_path(self, boardconfig, component): 14 | return next(identity['Manifest'][component]['Info']['Path'] for identity in self.manifest['BuildIdentities'] if identity['Info']['DeviceClass'].lower() == boardconfig.lower()) 15 | 16 | def fetch_supported_devices(self): return self.manifest['SupportedProductTypes'] 17 | 18 | def fetch_version(self): return self.manifest['ProductVersion'] 19 | 20 | class RestoreManifest: 21 | def __init__(self, manifest, boardconfig): 22 | self.platform = self.fetch_platform(boardconfig, plistlib.loads(manifest)) 23 | 24 | def fetch_platform(self, boardconfig, manifest): 25 | for device in manifest['DeviceMap']: 26 | if device['BoardConfig'].lower() != boardconfig.lower(): 27 | continue 28 | 29 | if device['Platform'].startswith('s5l89'): 30 | return int(device['Platform'][3:-1], 16) 31 | 32 | else: 33 | return int(device['Platform'][-4:], 16) 34 | -------------------------------------------------------------------------------- /utils/restore.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import shutil 3 | import subprocess 4 | import os 5 | import sys 6 | import time 7 | 8 | 9 | class Restore: 10 | def __init__(self, identifier, platform): 11 | self.device = identifier 12 | self.platform = platform 13 | 14 | def restore(self, ipsw, cellular, update): 15 | args = [ 16 | 'futurerestore', 17 | '-t', 18 | self.blob, 19 | '--latest-sep' 20 | ] 21 | 22 | if update: 23 | args.append('-u') 24 | 25 | if cellular: 26 | args.append('--latest-baseband') 27 | else: 28 | args.append('--no-baseband') 29 | 30 | args.append(ipsw) 31 | with open('futurerestore_error.log', 'w') as f: 32 | f.write(f"{' '.join(args)}\n\n") 33 | 34 | with open('futurerestore_error.log', 'a') as f: 35 | futurerestore = subprocess.run(args, stderr=subprocess.DEVNULL, stdout=f, universal_newlines=True) 36 | 37 | if os.path.isdir(ipsw.rsplit('.', 1)[0]): 38 | shutil.rmtree(ipsw.rsplit('.', 1)[0]) 39 | 40 | if 'Done: restoring succeeded!' not in futurerestore.stdout: 41 | sys.exit("[ERROR] Restore failed. Log written to 'futurerestore_error.log'. Exiting.") 42 | 43 | os.remove('futurerestore_error.log') 44 | 45 | def save_blobs(self, ecid, boardconfig, path, apnonce=None): 46 | args = [ 47 | 'tsschecker', 48 | '-d', 49 | self.device, 50 | '-B', 51 | boardconfig, 52 | '-e', 53 | f'0x{ecid}', 54 | '-l', 55 | '-s', 56 | '--save-path', 57 | path, 58 | '--nocache' 59 | ] 60 | 61 | if apnonce: 62 | args.append('--apnonce') 63 | args.append(apnonce) 64 | 65 | tsschecker = subprocess.check_output(args, universal_newlines=True) 66 | if 'Saved shsh blobs!' not in tsschecker: 67 | sys.exit('[ERROR] Failed to save blobs. Exiting.') 68 | 69 | if apnonce: 70 | for blob in glob.glob(f'{path}/*.shsh*'): 71 | if blob != self.signing_blob: 72 | self.blob = blob 73 | break 74 | else: 75 | self.signing_blob = glob.glob(f'{path}/*.shsh*')[0] 76 | 77 | def send_component(self, file, component): 78 | if component == 'iBSS' and self.platform in (8960, 8015): #TODO: Reset device via pyusb rather than call an external binary. 79 | irecovery_reset = subprocess.run(('irecovery', '-f', file), stdout=subprocess.DEVNULL) 80 | if irecovery_reset.returncode != 0: 81 | sys.exit('[ERROR] Failed to reset device. Exiting.') 82 | 83 | irecovery = subprocess.run(('irecovery', '-f', file), stdout=subprocess.DEVNULL) 84 | if irecovery.returncode != 0: 85 | sys.exit(f"[ERROR] Failed to send '{component}'. Exiting.") 86 | 87 | if component == 'iBEC': 88 | if 8010 <= self.platform <= 8015: 89 | irecovery_jump = subprocess.run(('irecovery', '-c', 'go'), stdout=subprocess.DEVNULL) 90 | if irecovery_jump.returncode != 0: 91 | sys.exit(f"[ERROR] Failed to boot '{component}'. Exiting.") 92 | 93 | time.sleep(3) 94 | 95 | def sign_component(self, file, output): 96 | args = ( 97 | 'img4tool', 98 | '-c', 99 | output, 100 | '-p', 101 | file, 102 | '-s', 103 | self.signing_blob 104 | ) 105 | 106 | img4tool = subprocess.run(args, stdout=subprocess.DEVNULL) 107 | if img4tool.returncode != 0: 108 | sys.exit(f"[ERROR] Failed to sign '{file.split('/')[-1]}'. Exiting.") 109 | --------------------------------------------------------------------------------