diff --git a/.gitignore b/.gitignore index abb7f2a..9d59a96 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ FirmwareBundles/ IPSW/ .DS_Store -futurerestore_error.log +restore.log diff --git a/README.md b/README.md index b19dcce..0a67663 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![License](https://img.shields.io/github/license/m1stadev/Inferius)](https://github.com/m1stadev/Inferius) [![Stars](https://img.shields.io/github/stars/m1stadev/Inferius)]((https://github.com/m1stadev/Inferius)) -Inferius is an [xpwn](https://github.com/m1stadev/xpwn)-like tool to create & restore custom IPSWs to 64-bit devices. +Inferius is an [xpwn](https://github.com/OothecaPickle/xpwn)-like tool to create & restore custom IPSWs to 64-bit devices. 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. @@ -44,6 +44,7 @@ By default, firmware bundles are automatically downloaded from an [external repo - [libusb](https://libusb.info/) - [futurerestore](https://github.com/m1stadev/futurerestore) - futurerestore must be compiled with [my fork of img4tool](https://github.com/m1stadev/img4tool), or else it can't be used with Inferius. +- [img4tool](https://github.com/tihmstar/img4tool) - [libirecovery](https://github.com/libimobiledevice/libirecovery) - [tsschecker](https://github.com/1Conan/tsschecker) - Python dependencies: diff --git a/bundlegen b/bundlegen index 8dffee1..56bbe50 100755 --- a/bundlegen +++ b/bundlegen @@ -38,7 +38,7 @@ def create_im4p(file, output, tag=None, patch=None): args.append('-P') args.append(patch) - img4 = subprocess.run(args, stdout=subprocess.DEVNULL) + img4 = subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) if img4.returncode != 0: sys.exit(f"[ERROR] Packing '{file}' into im4p container failed. Exiting.") diff --git a/inferius b/inferius deleted file mode 100755 index f8cabea..0000000 --- a/inferius +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env python3 - -from utils.api import API -from utils.bundle import Bundle -from utils.dependencies import Checks -from utils.device import Device -from utils.ipsw import IPSW -from utils.manifest import Manifest, RestoreManifest -from utils.restore import Restore -import argparse -import platform -import os -import sys -import tempfile - - -def create_ipsw(api, buildmanifest, ipsw_path, tmpdir, bundle_path): - print('Creating custom IPSW') - - bundle = Bundle() - ipsw = IPSW(ipsw_path) - - print('[1] Grabbing Firmware Bundle...') - if bundle_path is not None: - print('Note: Using user provided Firmware Bundle.') - if bundle.verify_bundle(bundle_path, tmpdir, api.api, buildmanifest.buildid, api.board) == False: - sys.exit(f"[ERROR] Bundle '{bundle_path}' is invalid. Exiting.") - - else: - bundle.fetch_bundle(api.device, buildmanifest.version, buildmanifest.buildid, tmpdir) - - print('[2] Verifying IPSW...') - ipsw.verify_ipsw(api.fetch_sha1(buildmanifest.buildid)) - - print('[3] Extracting IPSW...') - extracted_ipsw = f'{tmpdir}/ipsw' - os.mkdir(extracted_ipsw) - ipsw.extract_ipsw(extracted_ipsw) - - print('[4] Patching components...') - bundle.apply_patches(extracted_ipsw) - - buildid = api.api['firmwares'][0]['buildid'] - latest_manifest = Manifest(api.partialzip_read(buildid, 'BuildManifest.plist')) - api.partialzip_extract(buildid, latest_manifest.fetch_component_path(api.board, 'LLB'), extracted_ipsw) - bootloader_ver = api.partialzip_extract(buildid, latest_manifest.fetch_component_path(api.board, 'iBoot'), extracted_ipsw) - - print('[5] Repacking IPSW...') - custom_ipsw = ipsw.create_ipsw(extracted_ipsw, f"{ipsw_path.split('/')[-1].rsplit('.', 1)[0]}_custom.ipsw", bundle.check_update_support(), bootloader_ver) - print(f"Finished creating custom IPSW: '{custom_ipsw}'.") - - return custom_ipsw - -def restore_ipsw(api, buildmanifest, ipsw_path, updating, tmpdir): - print('Restoring custom IPSW') - - Checks() - device = Device(api.device) - ipsw = IPSW(ipsw_path) - restore = Restore(api.device, device.platform) - - print('[1] Verifying custom IPSW...') - ipsw.verify_custom_ipsw(api.device, updating) - - print('[2] Checking for device in pwned DFU...') - device.check_pwndfu() - - print('[3] Extracting bootchain...') - ibss = buildmanifest.fetch_component_path(device.board, 'iBSS') - ipsw.extract_file(ibss, f'{tmpdir}/ibss.im4p') - ibec = buildmanifest.fetch_component_path(device.board, 'iBEC') - ipsw.extract_file(ibec, f'{tmpdir}/ibec.im4p') - - print('[4] Signing bootchain...') - restore.save_blobs(device.ecid, device.board, tmpdir) - restore.sign_component(f'{tmpdir}/ibss.im4p', f'{tmpdir}/ibss.img4') - restore.sign_component(f'{tmpdir}/ibec.im4p', f'{tmpdir}/ibec.img4') - - print('[5] Sending bootchain...') - restore.send_component(f'{tmpdir}/ibss.img4', 'iBSS') - restore.send_component(f'{tmpdir}/ibec.img4', 'iBEC') - - print('[6] Saving SHSH blobs...') - restore.save_blobs(device.ecid, device.board, tmpdir, device.fetch_apnonce()) - - print('[7] Restoring...') - restore.restore(ipsw_path, device.baseband, updating) - 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.') - -def main(): - parser = argparse.ArgumentParser(description='Inferius - Create & Restore 64-bit custom IPSWs', usage="inferius -d 'identifier' -f 'IPSW' [-c/-r] [-b 'BUNDLE']") - parser.add_argument('-d', '--device', help='Device identifier', nargs=1) - parser.add_argument('-f', '--ipsw', help='Path to IPSW', nargs=1) - parser.add_argument('-c', '--create', help='Create custom IPSW', action='store_true') - parser.add_argument('-r', '--restore', help='Restore custom IPSW', action='store_true') - parser.add_argument('-b', '--bundle', help='(Optional) Path to local Firmware Bundle', nargs=1) - parser.add_argument('-u', '--update', help='Keep data while restoring custom IPSW', action='store_true') - args = parser.parse_args() - - if (not args.device or not args.ipsw) or \ - (not args.create and not args.restore) or \ - (args.update and not args.restore) or \ - (args.bundle and not args.create): - sys.exit(parser.print_help(sys.stderr)) - - if platform.system() == 'Windows': - sys.exit('[ERROR] Inferius does not support Windows. Exiting.') - - identifier = args.device[0] - ipsw_path = args.ipsw[0] - - api = API() - api.check_device(identifier) - api.fetch_api() - api.get_board() - - ipsw = IPSW(ipsw_path) - restoremanifest = RestoreManifest(ipsw.read_file('Restore.plist'), api.board) - if restoremanifest.platform not in (0x8960, 0x7000, 0x7001, 0x8000, 0x8001, 0x8003, 0x8010, 0x8011, 0x8015): - sys.exit(f"[ERROR] '{identifier}' is not supported by Inferius. Exiting.") - - buildmanifest = Manifest(ipsw.read_file('BuildManifest.plist')) - - ver_major = int(buildmanifest.version.split('.')[0]) - if ver_major == 10 and restoremanifest.platform != 0x8960: - sys.exit(f'[ERROR] iOS 10 downgrades are only supported on A7 devices. Exiting.') - - elif not 11 <= ver_major <= 14: - sys.exit(f'[ERROR] iOS {buildmanifest.version} is not supported by Inferius. Exiting.') - - if identifier not in buildmanifest.supported_devices: - sys.exit(f"[ERROR] IPSW '{ipsw_path}' does not support {identifier}. Exiting.") - - if args.bundle: - bundle = args.bundle[0] - else: - bundle = None - - with tempfile.TemporaryDirectory() as tmpdir: - if args.create: - custom_ipsw = create_ipsw(api, buildmanifest, ipsw_path, tmpdir, bundle) - else: - custom_ipsw = ipsw_path - - if args.restore: - restore_ipsw(api, buildmanifest, custom_ipsw, args.update, tmpdir) - - -if __name__ == '__main__': - main() diff --git a/inferius/__main__.py b/inferius/__main__.py new file mode 100755 index 0000000..591bfcc --- /dev/null +++ b/inferius/__main__.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 + +from pathlib import Path +from pymobiledevice3.restore.device import Device +from pymobiledevice3.restore.ipsw.ipsw import IPSW +from pymobiledevice3.irecv import IRecv + +from utils.api import API +from utils.bundle import Bundle + +# from utils.device import Device +# from utils.ipsw import IPSW +from utils.manifest import Manifest, RestoreManifest +from utils.restore import Restore + +import argparse +import platform +import sys +import tempfile + + +sys.tracebacklimit = 0 + + +def create_ipsw( + api: API, buildmanifest: Manifest, bundle: Bundle, ipsw: IPSW, tmpdir: Path +): + print('Creating custom IPSW') + + print('[1] Grabbing Firmware Bundle...') + if bundle.bundle is not None: + print('Note: Using user provided Firmware Bundle.') + try: + bundle.verify_bundle(tmpdir, api.api, buildmanifest.buildid, api.board) + except: + sys.exit(f'[ERROR] Bundle is invalid: {bundle.bundle}. Exiting.') + + else: + bundle.fetch_bundle( + api.device, buildmanifest.version, buildmanifest.buildid, tmpdir + ) + + print('[2] Verifying IPSW...') + ipsw.verify_ipsw(api.fetch_sha1(buildmanifest.buildid)) + + print('[3] Extracting IPSW...') + extracted_ipsw = tmpdir / 'ipsw' + extracted_ipsw.mkdir() + + ipsw.extract_ipsw(extracted_ipsw) + + print('[4] Patching components...') + bundle.apply_patches(extracted_ipsw) + + buildid = api.api['firmwares'][0]['buildid'] + latest_manifest = Manifest( + api.partialzip_read(buildid, 'BuildManifest.plist'), api.board + ) + + api.partialzip_extract(buildid, latest_manifest.get_path('LLB'), extracted_ipsw) + api.partialzip_extract(buildid, latest_manifest.get_path('iBoot'), extracted_ipsw) + + print('[5] Repacking IPSW...') + ipsw.ipsw = ipsw.create_ipsw( + extracted_ipsw, + f'{ipsw.ipsw.stem}_custom.ipsw', + bundle.check_update_support(), + api.api['firmwares'][0]['version'], + ) + print(f"Finished creating custom IPSW: '{ipsw.ipsw}'.\n") + + return ipsw + + +def restore_ipsw( + api: API, buildmanifest: Manifest, ipsw: Path, updating: bool, tmpdir: Path +): + print('Restoring custom IPSW') + + print('[1] Verifying custom IPSW...') + # ipsw.verify_custom_ipsw(api, updating) + + print('[2] Checking for device in pwned DFU...') + device = Device(irecv=IRecv()) + if 'PWND' not in device.irecv._device_info.keys(): + sys.exit( + '[ERROR] Attempting to restore a device not in Pwned DFU mode. Exiting.' + ) + + sys.exit('im out!') + + print('[4] Saving SHSH blobs...') + if buildmanifest.version[0] == 10: + otamanifest = api.fetch_ota_manifest(api.device, tmpdir) + else: + otamanifest = None + + shsh_path = tmpdir / 'shsh' + shsh_path.mkdir() + if buildmanifest.version[0] == 10: + restore.save_blobs( + device.data['ECID'], device.board, shsh_path, manifest=otamanifest + ) + else: + restore.save_blobs(device.data['ECID'], device.board, shsh_path) + + print('[5] Signing bootchain...') + rdsk_img = tmpdir / 'rdsk.dmg.img4' + kern_img = tmpdir / 'rkrn.img4' + restore.sign_image(ramdisk, rdsk_img) + restore.sign_image(kernel, kern_img, tag='rkrn') + + print('[6] Restoring...') + if buildmanifest.version[0] == 10: + manifest = Manifest(api.partialzip_read('14G60', 'BuildManifest.plist')) + sep = api.partialzip_extract('14G60', manifest.get_path('RestoreSEP'), tmpdir) + else: + sep = None + + restore.restore( + ipsw.ipsw, + rdsk_img, + kern_img, + device.baseband, + updating, + sep=sep, + manifest=otamanifest, + ) + + print( + f'Finished restoring custom IPSW to your device. Please boot your device using one of the tools listed in the README.\n' + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description='Inferius - Create & Restore 64-bit custom IPSWs', + usage="inferius -d 'identifier' -f 'IPSW' [-c/-r] [-b 'BUNDLE']", + ) + parser.add_argument( + '-d', '--device', help='Device identifier', nargs=1, required=True + ) + parser.add_argument( + '-f', '--ipsw', help='Path to IPSW', nargs=1, type=Path, required=True + ) + parser.add_argument( + '-c', '--create', help='Create custom IPSW', action='store_true' + ) + parser.add_argument( + '-r', '--restore', help='Restore custom IPSW', action='store_true' + ) + parser.add_argument( + '-b', + '--bundle', + help='(Optional) Path to local Firmware Bundle', + nargs=1, + type=Path, + ) + parser.add_argument( + '-u', + '--update', + help='(Optional) Keep data while restoring custom IPSW', + action='store_true', + ) + args = parser.parse_args() + + if ( + (not args.device or not args.ipsw) + or (not args.create and not args.restore) + or (args.update and not args.restore) + or (args.bundle and not args.create) + ): + sys.exit(parser.print_help(sys.stderr)) + + if platform.system() == 'Windows': + sys.exit('[ERROR] Inferius does not support Windows. Exiting.') + + print('Inferius - Create & Restore 64-bit custom IPSWs\n') + + api = API(args.device[0]) + api.fetch_api() + api.fetch_board() + + ipsw = IPSW(args.ipsw[0]) + restoremanifest = RestoreManifest(ipsw.read_file('Restore.plist'), api.board) + if restoremanifest.platform not in ( + 0x8960, + 0x7000, + 0x7001, + 0x8000, + 0x8001, + 0x8003, + 0x8010, + 0x8011, + 0x8015, + ): + sys.exit(f"[ERROR] '{api.device}' is not supported by Inferius. Exiting.") + + buildmanifest = Manifest(ipsw.read_file('BuildManifest.plist'), api.board) + + if not 10 <= buildmanifest.version[0] <= 14: + sys.exit( + f"[ERROR] iOS {'.'.join(str(_) for _ in buildmanifest.version)} is not supported by Inferius. Exiting." + ) + + if buildmanifest.version[0] == 10 and restoremanifest.platform != 0x8960: + sys.exit( + f'[ERROR] iOS 10 downgrades are only supported on A7 devices. Exiting.' + ) + + if args.device[0] not in buildmanifest.supported_devices: + sys.exit(f"[ERROR] IPSW: {ipsw} does not support {api.device}. Exiting.") + + bundle = Bundle(args.bundle[0] if args.bundle else None) + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + + if args.create: + custom_ipsw = create_ipsw(api, buildmanifest, bundle, ipsw, tmpdir) + else: + custom_ipsw = ipsw + + if args.restore: + restore_ipsw(api, buildmanifest, args.ipsw[0], args.update, tmpdir) + + +if __name__ == '__main__': + main() diff --git a/inferius/api.py b/inferius/api.py new file mode 100644 index 0000000..299a110 --- /dev/null +++ b/inferius/api.py @@ -0,0 +1,133 @@ +from pathlib import Path +from remotezip import RemoteZip +from typing import Optional +from utils import errors + +import requests + + +class API: + def __init__(self, identifier: str) -> None: + self.device = identifier + self.api = self.fetch_api() + self.board = self.fetch_board() + + def is_signed(self, version: str) -> bool: + return any( + firm['signed'] == True + for firm in self.api['firmwares'] + if firm['version'] == version + ) + + def fetch_api(self) -> Optional[dict]: + try: + return requests.get(f'https://api.ipsw.me/v4/device/{self.device}').json() + except requests.exceptions.JSONDecodeError: + raise errors.NotFoundError( + f"Device does not exist: {self.device}." + ) from None + + def fetch_board(self) -> Optional[str]: + boards = [ + board['boardconfig'].lower() + for board in self.api['boards'] + if board['boardconfig'].lower().endswith('ap') + ] + if len(boards) == 1: + return boards[0] + + else: + print( + 'There are multiple board configs for your device! Please choose the correct board config for your device:' + ) + for b in range(len(boards)): + print(f" {b + 1}: {boards[b]}") + + board = input('Choice: ') + try: + board = int(board) - 1 + except: + raise TypeError(f'Invalid choice given: {board}.') from None + else: + if board not in range(len(boards)): + raise ValueError(f'Incorrect choice given: {board}.') from None + + return boards[board] + + def fetch_ota_manifest(self, device: str, path: Path) -> Optional[Path]: + r = requests.get( + f'https://github.com/m1stadev/inferius-ext/raw/master/manifests/BuildManifest_{device}.plist' + ) + if r.status_code == 404: + raise errors.NotFoundError( + f'An OTA manifest does not exist for device: {device}.' + ) + + manifest = path / 'otamanifest.plist' + with manifest.open('wb') as f: + try: + f.write(r.content) + except OSError: + raise IOError(f'Failed to write OTA manifest to: {manifest}.') from None + + return manifest + + def fetch_sha1(self, buildid: str) -> str: + try: + sha1 = next( + firm['sha1sum'] + for firm in self.api['firmwares'] + if firm['buildid'] == buildid + ) + except StopIteration: + raise errors.NotFoundError( + f'Firmware does not exist with buildid: {buildid}.' + ) from None + + return sha1 + + def partialzip_extract( + self, buildid: str, component: str, path: Path + ) -> Optional[Path]: + try: + url = next( + firm['url'] + for firm in self.api['firmwares'] + if firm['buildid'] == buildid + ) + except StopIteration: + raise errors.NotFoundError( + f'Firmware does not exist with buildid: {buildid}.' + ) from None + + with RemoteZip(url) as ipsw: + try: + ipsw.extract(component, path) + except KeyError: + raise errors.NotFoundError( + f'Component does not exist: {component}.' + ) from None + except OSError: + raise IOError(f'Failed to partialzip component: {component}.') from None + + return path / component + + def partialzip_read(self, buildid: str, component: str) -> Optional[bytes]: + try: + url = next( + firm['url'] + for firm in self.api['firmwares'] + if firm['buildid'] == buildid + ) + except StopIteration: + raise errors.NotFoundError( + f'Firmware does not exist with buildid: {buildid}.' + ) from None + + with RemoteZip(url) as ipsw: + try: + return ipsw.read(component) + except KeyError: + raise errors.NotFoundError( + f'File does not exist in IPSW: {component}.' + ) from None diff --git a/inferius/bundle.py b/inferius/bundle.py new file mode 100644 index 0000000..29f581d --- /dev/null +++ b/inferius/bundle.py @@ -0,0 +1,100 @@ +from pathlib import Path +from remotezip import RemoteZip, RemoteIOError +from typing import Optional +from utils import errors + +import bsdiff4 +import json +import zipfile + + +class Bundle: + def __init__(self, bundle: Optional[Path] = None): + self.bundle = bundle + + def apply_patches(self, ipsw: Path) -> None: + with (self.bundle / 'Info.json').open('r') as f: + bundle_data = json.load(f) + + for patches in bundle_data['patches']: + if patches != 'required': + apply_patch = input( + f"[NOTE] Would you like to apply '{patches}' patch to your custom IPSW? [Y/N]: " + ).lower() + if apply_patch == 'n': + continue + + elif apply_patch not in ('y', 'n'): + print('[WARN] Invalid input, skipping patch...') + continue + + for patch in bundle_data['patches'][patches]: + if any(_ in patch['file'] for _ in ('iBSS', 'iBEC')): + continue + + bsdiff4.file_patch_inplace( + ipsw / patch['file'], self.bundle / patch['patch'] + ) + + def check_update_support(self) -> bool: + with (self.bundle / 'Info.json').open('r') as f: + bundle_data = json.load(f) + + return bundle_data['update_support'] + + def fetch_bundle( + self, device: str, version: tuple, buildid: str, path: Path + ) -> None: + bundle_name = '_'.join([device, '.'.join([str(_) for _ in version]), buildid]) + + bundle = path / bundle_name + bundle.mkdir() + + try: + with RemoteZip( + f'https://github.com/m1stadev/inferius-ext/raw/master/bundles/{bundle_name}.bundle' + ) as rz: + try: + rz.extractall(bundle) + except OSError: + raise IOError( + f'Failed to download firmware bundle to: {bundle}.' + ) from None + + except RemoteIOError: + raise errors.NotFoundError( + f"A firmware bundle does not exist for device: {device}, OS: {'.'.join([str(_) for _ in version])}." + ) from None + + self.bundle = bundle + + def verify_bundle( + self, path: Path, api: dict, buildid: str, boardconfig: str + ) -> None: + if not self.bundle.exists(): + raise errors.NotFoundError( + f'Firmware bundle does not exist: {self.bundle}.' + ) + + if not zipfile.is_zipfile(self.bundle): + raise errors.CorruptError(f'Firmware bundle is corrupt: {self.bundle}.') + + if not any(_['buildid'] == buildid for _ in api['firmwares']): + return + + try: + with zipfile.ZipFile(self.bundle, 'r') as f: + bundle_data = json.loads(f.read('Info.json')) + + next(_.lower() == boardconfig.lower() for _ in bundle_data['boards']) + + except StopIteration: + return + + bundle = path / self.bundle.stem + bundle.mkdir() + + with zipfile.ZipFile(self.bundle) as f: + f.extractall(bundle) + + self.bundle = bundle diff --git a/inferius/dependencies.py b/inferius/dependencies.py new file mode 100644 index 0000000..521cf94 --- /dev/null +++ b/inferius/dependencies.py @@ -0,0 +1,38 @@ +from utils import errors + +import shutil +import subprocess + + +class Checks: + def __init__(self): + self.check_bin("futurerestore") + self.check_bin("tsschecker") + self.check_bin("irecovery") + self.check_bin("img4tool") + + def check_bin(self, binary: str) -> None: + if shutil.which(binary) is None: + raise errors.DependencyError(f"Binary not found on your PC: {binary}.") + + if binary == "futurerestore": + fr_help = subprocess.check_output( + (binary, "--help"), stderr=subprocess.DEVNULL, universal_newlines=True + ) + + if ( + "--skip-blob" not in fr_help + ): # Inferius relies on the '--skip-blob' option + raise errors.DependencyError( + "This FutureRestore build is too old be used with Inferius." + ) + + elif binary == "irecovery": + try: + subprocess.check_call( + (binary, "-V"), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + except subprocess.CalledProcessError: + raise errors.DependencyError( + "This iRecovery build is too old to be used with Inferius." + ) from None diff --git a/inferius/device.py b/inferius/device.py new file mode 100644 index 0000000..677b149 --- /dev/null +++ b/inferius/device.py @@ -0,0 +1,50 @@ +from pathlib import Path +from typing import Optional +from utils import errors, usb + +import json + + +class Device: + def __init__(self, identifier): + self.identifier = identifier + self.data = self._get_data() + self.board = self.fetch_board() + + def _get_data(self) -> Optional[dict]: + device = usb.get_device(usb.DFU) + + device_data = dict() + for item in ( + *device.serial_number.split(), + *usb.get_string(device, device.bDescriptorType).split(), + ): + name, value = item.split(':') + if name in ('NONC', 'SNON'): + device_data[name] = bytes.fromhex(value) + elif value.startswith('[') and value.endswith(']'): + device_data[name] = value + else: + device_data[name] = int(value, 16) + + usb.release_device(device) + return device_data + + def fetch_board(self) -> Optional[str]: + device_info = Path('utils/devices.json') + try: + with device_info.open('r') as f: + data = json.load(f) + + except FileNotFoundError: + raise errors.CorruptError( + f'File missing from Inferius: {device_info}' + ) from None + + return next( + _['boardconfig'] + for _ in data + if _['identifier'] == self.identifier + and _['bdid'] == self.data['BDID'] + and _['cpid'] == self.data['CPID'] + ) diff --git a/inferius/devices.json b/inferius/devices.json new file mode 100644 index 0000000..3a19c43 --- /dev/null +++ b/inferius/devices.json @@ -0,0 +1,320 @@ +[ + { + "identifier": "AppleTV5,3", + "boardconfig": "j42dap", + "bdid": 52, + "cpid": 28672 + }, + { + "identifier": "AppleTV6,2", + "boardconfig": "j105aap", + "bdid": 2, + "cpid": 32785 + }, + { + "identifier": "iPad4,1", + "boardconfig": "j71ap", + "bdid": 16, + "cpid": 35168 + }, + { + "identifier": "iPad4,2", + "boardconfig": "j72ap", + "bdid": 18, + "cpid": 35168 + }, + { + "identifier": "iPad4,3", + "boardconfig": "j73ap", + "bdid": 20, + "cpid": 35168 + }, + { + "identifier": "iPad4,4", + "boardconfig": "j85ap", + "bdid": 10, + "cpid": 35168 + }, + { + "identifier": "iPad4,5", + "boardconfig": "j86ap", + "bdid": 12, + "cpid": 35168 + }, + { + "identifier": "iPad4,6", + "boardconfig": "j87ap", + "bdid": 14, + "cpid": 35168 + }, + { + "identifier": "iPad4,7", + "boardconfig": "j85map", + "bdid": 50, + "cpid": 35168 + }, + { + "identifier": "iPad4,8", + "boardconfig": "j86map", + "bdid": 52, + "cpid": 35168 + }, + { + "identifier": "iPad4,9", + "boardconfig": "j87map", + "bdid": 54, + "cpid": 35168 + }, + { + "identifier": "iPad5,1", + "boardconfig": "j96ap", + "bdid": 8, + "cpid": 28672 + }, + { + "identifier": "iPad5,2", + "boardconfig": "j97ap", + "bdid": 10, + "cpid": 28672 + }, + { + "identifier": "iPad5,3", + "boardconfig": "j81ap", + "bdid": 6, + "cpid": 28673 + }, + { + "identifier": "iPad5,4", + "boardconfig": "j82ap", + "bdid": 2, + "cpid": 28673 + }, + { + "identifier": "iPad6,11", + "boardconfig": "j71sap", + "bdid": 16, + "cpid": 32768 + }, + { + "identifier": "iPad6,11", + "boardconfig": "j71tap", + "bdid": 16, + "cpid": 32771 + }, + { + "identifier": "iPad6,12", + "boardconfig": "j72sap", + "bdid": 18, + "cpid": 32768 + }, + { + "identifier": "iPad6,12", + "boardconfig": "j72tap", + "bdid": 18, + "cpid": 32771 + }, + { + "identifier": "iPad6,3", + "boardconfig": "j127ap", + "bdid": 8, + "cpid": 32769 + }, + { + "identifier": "iPad6,4", + "boardconfig": "j128ap", + "bdid": 10, + "cpid": 32769 + }, + { + "identifier": "iPad6,7", + "boardconfig": "j98aap", + "bdid": 16, + "cpid": 32769 + }, + { + "identifier": "iPad6,8", + "boardconfig": "j99aap", + "bdid": 18, + "cpid": 32769 + }, + { + "identifier": "iPad7,1", + "boardconfig": "j120ap", + "bdid": 12, + "cpid": 32785 + }, + { + "identifier": "iPad7,11", + "boardconfig": "j171ap", + "bdid": 28, + "cpid": 32784 + }, + { + "identifier": "iPad7,12", + "boardconfig": "j172ap", + "bdid": 30, + "cpid": 32784 + }, + { + "identifier": "iPad7,2", + "boardconfig": "j121ap", + "bdid": 14, + "cpid": 32785 + }, + { + "identifier": "iPad7,3", + "boardconfig": "j207ap", + "bdid": 4, + "cpid": 32785 + }, + { + "identifier": "iPad7,4", + "boardconfig": "j208ap", + "bdid": 6, + "cpid": 32785 + }, + { + "identifier": "iPad7,5", + "boardconfig": "j71bap", + "bdid": 24, + "cpid": 32784 + }, + { + "identifier": "iPad7,6", + "boardconfig": "j72bap", + "bdid": 26, + "cpid": 32784 + }, + { + "identifier": "iPhone10,1", + "boardconfig": "d20ap", + "bdid": 2, + "cpid": 32789 + }, + { + "identifier": "iPhone10,2", + "boardconfig": "d21ap", + "bdid": 4, + "cpid": 32789 + }, + { + "identifier": "iPhone10,3", + "boardconfig": "d22ap", + "bdid": 6, + "cpid": 32789 + }, + { + "identifier": "iPhone10,4", + "boardconfig": "d201ap", + "bdid": 10, + "cpid": 32789 + }, + { + "identifier": "iPhone10,5", + "boardconfig": "d211ap", + "bdid": 12, + "cpid": 32789 + }, + { + "identifier": "iPhone10,6", + "boardconfig": "d221ap", + "bdid": 14, + "cpid": 32789 + }, + { + "identifier": "iPhone6,1", + "boardconfig": "n51ap", + "bdid": 0, + "cpid": 35168 + }, + { + "identifier": "iPhone6,2", + "boardconfig": "n53ap", + "bdid": 2, + "cpid": 35168 + }, + { + "identifier": "iPhone7,1", + "boardconfig": "n56ap", + "bdid": 4, + "cpid": 28672 + }, + { + "identifier": "iPhone7,2", + "boardconfig": "n61ap", + "bdid": 6, + "cpid": 28672 + }, + { + "identifier": "iPhone8,1", + "boardconfig": "n71ap", + "bdid": 4, + "cpid": 32768 + }, + { + "identifier": "iPhone8,1", + "boardconfig": "n71map", + "bdid": 4, + "cpid": 32771 + }, + { + "identifier": "iPhone8,2", + "boardconfig": "n66ap", + "bdid": 6, + "cpid": 32768 + }, + { + "identifier": "iPhone8,2", + "boardconfig": "n66map", + "bdid": 6, + "cpid": 32771 + }, + { + "identifier": "iPhone8,4", + "boardconfig": "n69ap", + "bdid": 2, + "cpid": 32771 + }, + { + "identifier": "iPhone8,4", + "boardconfig": "n69uap", + "bdid": 2, + "cpid": 32768 + }, + { + "identifier": "iPhone9,1", + "boardconfig": "d10ap", + "bdid": 8, + "cpid": 32784 + }, + { + "identifier": "iPhone9,2", + "boardconfig": "d11ap", + "bdid": 10, + "cpid": 32784 + }, + { + "identifier": "iPhone9,3", + "boardconfig": "d101ap", + "bdid": 12, + "cpid": 32784 + }, + { + "identifier": "iPhone9,4", + "boardconfig": "d111ap", + "bdid": 14, + "cpid": 32784 + }, + { + "identifier": "iPod7,1", + "boardconfig": "n102ap", + "bdid": 16, + "cpid": 28672 + }, + { + "identifier": "iPod9,1", + "boardconfig": "n112ap", + "bdid": 22, + "cpid": 32784 + } +] \ No newline at end of file diff --git a/inferius/errors.py b/inferius/errors.py new file mode 100644 index 0000000..4edaeba --- /dev/null +++ b/inferius/errors.py @@ -0,0 +1,26 @@ +class InferiusError(Exception): + pass + + +class DependencyError(InferiusError): + pass + + +class NotFoundError(InferiusError): + pass + + +class DeviceError(InferiusError): + pass + + +class CorruptError(InferiusError): + pass + + +class BadIPSWError(CorruptError): + pass + + +class RestoreError(InferiusError): + pass diff --git a/inferius/ipsw.py b/inferius/ipsw.py new file mode 100644 index 0000000..345de09 --- /dev/null +++ b/inferius/ipsw.py @@ -0,0 +1,105 @@ +from pathlib import Path +from typing import Optional +from utils.api import API +from utils import errors + +import hashlib +import json +import shutil +import zipfile + + +class IPSW: + def __init__(self, ipsw: Path): + self.ipsw = ipsw + + def __str__(self) -> str: + return str(self.ipsw) + + def create_ipsw( + self, path: Path, filename: str, update: bool, bootloader: str + ) -> Optional[Path]: + ipsw = Path(f'IPSW/{filename}') + ipsw.parent.mkdir(exist_ok=True) + + info = {'update_support': update, 'bootloader': bootloader} + + with (path / '.Inferius').open('w') as f: + json.dump(info, f) + + try: + shutil.make_archive(ipsw, 'zip', path) + except: + raise OSError(f'Failed to create custom IPSW at path: {ipsw}.') from None + + return ipsw.with_suffix(ipsw.suffix + '.zip').rename(ipsw.with_suffix('.ipsw')) + + def extract_file(self, file: str, output: Path) -> Path: + try: + with zipfile.ZipFile(self.ipsw, 'r') as ipsw, output.open('wb') as f: + f.write(ipsw.read(file)) + + except KeyError: + raise errors.NotFoundError(f'File not in IPSW: {file}.') from None + + except OSError: + raise IOError(f'Failed to extract file from IPSW: {file}.') from None + + def extract_ipsw(self, path: Path) -> None: + with zipfile.ZipFile(self.ipsw, 'r') as ipsw: + try: + ipsw.extractall(path) + except OSError: + raise OSError(f'Failed to extract IPSW: {self.ipsw}.') from None + + def read_file(self, file: str) -> Optional[bytes]: + try: + with zipfile.ZipFile(self.ipsw, 'r') as ipsw: + return ipsw.read(file) + + except KeyError: + raise errors.NotFoundError(f'File not in IPSW: {file}.') from None + + def verify_ipsw(self, sha1: str) -> None: + if not self.ipsw.is_file(): + raise errors.NotFoundError(f'IPSW does not exist: {self.ipsw}.') + + if not zipfile.is_zipfile(self.ipsw): + raise errors.BadIPSWError(f'IPSW is corrupt: {self.ipsw}.') + + with zipfile.ZipFile(self.ipsw, 'r') as ipsw: + if '.Inferius' in ipsw.namelist(): + raise errors.BadIPSWError(f'IPSW has been modified: {self.ipsw}.') + + hash = hashlib.sha1() + with self.ipsw.open('rb') as ipsw: + fbuf = ipsw.read(65536) + while len(fbuf) > 0: + hash.update(fbuf) + fbuf = ipsw.read(65536) + + if sha1 != hash.hexdigest(): + raise errors.BadIPSWError(f'IPSW is corrupt: {self.ipsw}.') + + def verify_custom_ipsw(self, api: API, update: bool) -> None: + if not self.ipsw.is_file(): + raise errors.NotFoundError(f'IPSW does not exist: {self.ipsw}.') + + if not zipfile.is_zipfile(self.ipsw): + raise errors.BadIPSWError(f'IPSW is corrupt: {self.ipsw}.') + + with zipfile.ZipFile(self.ipsw, 'r') as ipsw: + if '.Inferius' not in ipsw.namelist(): + raise errors.BadIPSWError(f'IPSW is not custom: {self.ipsw}.') + + info = json.loads(ipsw.read('.Inferius')) + + if (info['update_support'] == False) and (update == True): + raise errors.BadIPSWError( + f'IPSW does not support update restores: {self.ipsw}.' + ) + + if api.is_signed(info['bootloader']) == False: + raise errors.BadIPSWError( + f'IPSW is too old to be used with Inferius: {self.ipsw}. A new custom IPSW must be created.' + ) diff --git a/inferius/manifest.py b/inferius/manifest.py new file mode 100644 index 0000000..abafbc9 --- /dev/null +++ b/inferius/manifest.py @@ -0,0 +1,60 @@ +from typing import Optional +from utils import errors + +import plistlib + + +class Manifest: + def __init__(self, manifest: bytes, board: str): + self._manifest = plistlib.loads(manifest) + + self.version = tuple( + int(_) for _ in self._manifest['ProductVersion'].split('.') + ) + self.buildid = self._manifest['ProductBuildVersion'] + self.supported_devices = self._manifest['SupportedProductTypes'] + + # Get proper capitalization for board + self.board = next( + _['Info']['DeviceClass'] + for _ in self._manifest['BuildIdentities'] + if _['Info']['DeviceClass'].lower() == board.lower() + ) + + self.id = next( + _ + for _ in range(len(self._manifest['BuildIdentities'])) + if self._manifest['BuildIdentities'][_]['Info']['DeviceClass'] == self.board + ) + + def get_path(self, component: str) -> str: + if ( + component + not in self._manifest['BuildIdentities'][self.id]['Manifest'].keys() + ): + raise errors.NotFoundError(f'Component not found in manifest: {component}.') + + return self._manifest['BuildIdentities'][self.id]['Manifest'][component][ + 'Info' + ]['Path'] + + def dump(self) -> bytes: + return plistlib.dumps(self._manifest) + + +class RestoreManifest: + def __init__(self, manifest: bytes, boardconfig: str): + self._manifest = plistlib.loads(manifest) + self.boardconfig = boardconfig + + @property + def platform(self) -> Optional[int]: + for device in self._manifest['DeviceMap']: + if device['BoardConfig'].lower() != self.boardconfig.lower(): + continue + + if device['Platform'].startswith('s5l89'): + return int(device['Platform'][3:-1], 16) + + else: + return int(device['Platform'][-4:], 16) diff --git a/inferius/restore.py b/inferius/restore.py new file mode 100644 index 0000000..13c55e2 --- /dev/null +++ b/inferius/restore.py @@ -0,0 +1,142 @@ +from pathlib import Path +from utils.device import Device +from utils import errors + +import shutil +import subprocess + + +class Restore: + def __init__(self, device: Device): + self.device = device + + def restore( + self, + ipsw: Path, + rdsk: Path, + kern: Path, + cellular: bool, + update: bool, + *, + sep: Path = None, + manifest: Path = None, + ) -> None: + args = [ + 'futurerestore', + '-t', + str(self.blob), + '--use-pwndfu', + '--no-cache', + '--skip-blob', + '--rdsk', + str(rdsk), + '--rkrn', + str(kern), + ] + + if sep and manifest: + args.append('-s') + args.append(str(sep)) + + args.append('-m') + args.append(str(manifest)) + + else: + args.append('--latest-sep') + + if cellular: + args.append('--latest-baseband') + else: + args.append('--no-baseband') + + if update: + args.append('-u') + + args.append(str(ipsw)) + + log = Path('restore.log') + log.unlink(missing_ok=True) + + try: + futurerestore = subprocess.check_output( + args, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + + except subprocess.CalledProcessError as process: + with log.open('w') as f: + f.write(f"{' '.join([_ for _ in args])}\n\n") + f.write(process.stdout) + + raise errors.RestoreError( + f"[ERROR] Restore failed. Log written to '{log.name}'. Exiting." + ) from None + + if Path(ipsw.stem).is_dir(): + shutil.rmtree(ipsw.stem) + + if 'Done: restoring succeeded!' not in futurerestore: + with log.open('w') as f: + f.write(f"{' '.join([_ for _ in args])}\n\n") + f.write(futurerestore) + + raise errors.RestoreError( + f"[ERROR] Restore failed. Log written to '{log.name}'. Exiting." + ) + + def save_blobs( + self, ecid: str, boardconfig: str, path: Path, *, manifest: Path = None + ) -> None: + args = [ + 'tsschecker', + '-d', + self.device.identifier, + '-B', + boardconfig, + '-e', + ecid, + '--save-path', + str(path), + '-s', + ] + + if manifest: + args.append('-m') + args.append(str(manifest)) + else: + args.append('-l') + args.append('--nocache') + + try: + if 'Saved shsh blobs!' not in subprocess.check_output( + args, stderr=subprocess.STDOUT, universal_newlines=True + ): + raise errors.RestoreError('Failed to save SHSH blobs.') + except subprocess.CalledProcessError: + raise errors.RestoreError('Failed to save SHSH blobs.') from None + + self.blob = tuple(path.glob('*.shsh*'))[0] + + def sign_image(self, image: Path, output: Path, tag: str = None) -> None: + args = [ + 'img4tool', + '-c', + str(output), + '-p', + str(image), + '-s', + str(self.blob), + ] + + if tag: + if len(tag) != 4: + raise errors.RestoreError(f'Invalid IMG4 tag: {tag}.') + + args.append('-t') + args.append(tag.lower()) + + try: + subprocess.check_call(args, stdout=subprocess.DEVNULL) + except subprocess.CalledProcessError: + raise errors.RestoreError(f'Failed to sign image: {image}.') from None diff --git a/inferius/usb.py b/inferius/usb.py new file mode 100644 index 0000000..c776bd6 --- /dev/null +++ b/inferius/usb.py @@ -0,0 +1,73 @@ +from pathlib import Path +from typing import Optional +from utils import errors + +import usb, usb.backend.libusb1, usb.util + + +RECOVERY = 0x1281 +DFU = 0x1227 + + +def _get_backend() -> Optional[ + usb.backend.libusb1._LibUSB +]: # Attempt to find a libusb 1.0 library to use as pyusb's backend, exit if one isn't found. + directories = ( + '/usr/local/lib', + '/opt/procursus/lib', + '/usr/lib', + ) # Common library directories to search + + libusb1 = None + for libdir in directories: + for file in Path(libdir).glob('libusb-1.0.0.*'): + if not file.is_file() or (file.suffix not in ('.so', '.dylib')): + continue + + libusb1 = file + break + + else: + continue + + break + + if libusb1 is None: + raise errors.DependencyError('libusb not found on your PC.') + + return usb.backend.libusb1.get_backend(find_library=lambda _: libusb1) + + +def get_device(mode: int) -> Optional[usb.core.Device]: + if mode not in (DFU, RECOVERY): + raise errors.DeviceError(f'Invalid mode specified: {mode}.') + + device: usb.core.Device = usb.core.find( + idVendor=0x5AC, idProduct=mode, backend=_get_backend() + ) + + if device is None: + raise errors.DeviceError(f'Device not found.') + + return device + + +def get_string(device: usb.core.Device, index: int) -> Optional[str]: + return usb.util.get_string(device, index) + + +def send_cmd(device: usb.core.Device, cmd: str) -> None: + if device.idProduct != RECOVERY: + raise errors.DeviceError( + 'Commands can only be send to a device in Recovery mode.' + ) + + device.ctrl_transfer(0x40, 1, 0, 0, cmd) + + +def release_device(device: usb.core.Device) -> None: + usb.util.dispose_resources(device) + + +def reset_device(device: usb.core.Device) -> None: + device.reset() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..a444c92 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,355 @@ +[[package]] +name = "black" +version = "22.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.5.18.1" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.11.4" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pyusb" +version = "1.2.1" +description = "Python USB access module" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[[package]] +name = "remotezip" +version = "0.9.4" +description = "Access zip file content hosted remotely without downloading the full file." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = "*" +tabulate = "*" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "tabulate" +version = "0.8.9" +description = "Pretty-print tabular data" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typed-ast" +version = "1.5.4" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "zipp" +version = "3.8.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "a373b182efd8cc1de3557e6a005a9378ab741e1e7a9b6931da8f7941babafac1" + +[metadata.files] +black = [ + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, +] +certifi = [ + {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, + {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"}, + {file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pyusb = [ + {file = "pyusb-1.2.1-py3-none-any.whl", hash = "sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36"}, + {file = "pyusb-1.2.1.tar.gz", hash = "sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9"}, +] +remotezip = [ + {file = "remotezip-0.9.4.tar.gz", hash = "sha256:8bed7d1fd3f096c15e480d05492d84537ac401b473ba109e0b30611452ac8e57"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +tabulate = [ + {file = "tabulate-0.8.9-py3-none-any.whl", hash = "sha256:d7c013fe7abbc5e491394e10fa845f8f32fe54f8dc60c6622c6cf482d25d47e4"}, + {file = "tabulate-0.8.9.tar.gz", hash = "sha256:eb1d13f25760052e8931f2ef80aaf6045a6cceb47514db8beab24cded16f13a7"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typed-ast = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] +typing-extensions = [ + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, +] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] +zipp = [ + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..94e953b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,45 @@ +[tool.poetry] +name = "inferius" +version = "0.1.0" +description = "A Python CLI tool for creating & restoring custom firmwares to an iOS device." +authors = ["m1stadev "] +license = "MIT" +readme = "README.md" +repository = "https://github.com/m1stadev/Inferius" +keywords = ["restore", "pwndfu", "ios", "checkm8"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Programming Language :: Python :: 3.7", + "Topic :: Utilities" +] +packages = [ + { include = "inferius" } +] + +[tool.poetry.scripts] +pyimg4 = "inferius.__main__:main" + +[tool.poetry.dependencies] +python = "^3.7" +pyusb = "^1.2.1" +remotezip = "^0.9.4" +requests = "^2.27.1" + +[tool.poetry.dev-dependencies] +black = {version = "^22.1.0", python = "^3.7"} +isort = {version = "^5.10.1", python = "^3.7"} + +[tool.black] +skip-string-normalization = true + +[tool.isort] +profile = "black" +src_paths = ["inferius"] + +[tool.poetry.urls] +"Bug Tracker" = "https://github.com/m1stadev/Inferius/issues" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt index 0d223d7..c1ab475 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ bsdiff4 mwclient remotezip requests +pymobiledevice3 pyusb \ No newline at end of file diff --git a/utils/api.py b/utils/api.py deleted file mode 100644 index fb88e65..0000000 --- a/utils/api.py +++ /dev/null @@ -1,59 +0,0 @@ -import remotezip -import requests -import sys - - -class API: - def check_device(self, identifier): - api = requests.get('https://api.ipsw.me/v4/devices').json() - - if identifier not in [device['identifier'] for device in api]: - sys.exit(f"[ERROR] '{identifier}' does not exist. Exiting.") - - self.device = identifier - - def is_signed(self, version): - return any(firm['signed'] == True for firm in self.api['firmwares'] if firm['version'] == version) - - def check_version(self, version): - if not any(firm['version'] == version for firm in self.api['firmwares']): - sys.exit(f"[ERROR] '{version}' does not exist. Exiting.") - - def fetch_api(self, identifier=None): - self.api = requests.get(f'https://api.ipsw.me/v4/device/{identifier if identifier else self.device}?type=ipsw').json() - - def fetch_sha1(self, buildid): - return next(firm['sha1sum'] for firm in self.api['firmwares'] if firm['buildid'] == buildid) - - def get_board(self): - boards = [board['boardconfig'] for board in self.api['boards']] - if len(boards) == 1: - self.board = boards[0] - return - - print('There are multiple boardconfigs for your device! Please choose the correct boardconfig for your device:') - for board in range(len(boards)): - print(f" {board + 1}: {boards[board]}") - - board = input('Choice: ') - try: - board = int(board) - 1 - except: - sys.exit('[ERROR] Invalid input given. Exiting.') - else: - if board not in range(len(boards)): - sys.exit('[ERROR] Invalid input given. Exiting.') - - self.board = boards[board] - - def partialzip_extract(self, buildid, component, path): - firm = next(firm for firm in self.api['firmwares'] if firm['buildid'] == buildid) - with remotezip.RemoteZip(firm['url']) as ipsw: - ipsw.extract(component, path) - - return firm['version'] - - def partialzip_read(self, buildid, component): - firm = next(firm for firm in self.api['firmwares'] if firm['buildid'] == buildid) - with remotezip.RemoteZip(firm['url']) as ipsw: - return ipsw.read(component) diff --git a/utils/bundle.py b/utils/bundle.py deleted file mode 100644 index 052921a..0000000 --- a/utils/bundle.py +++ /dev/null @@ -1,82 +0,0 @@ -import bsdiff4 -import io -import json -import os -import requests -import sys -import zipfile - - -class Bundle: - def apply_patches(self, ipsw): - with open(f'{self.bundle}/Info.json', 'r') as f: - bundle_data = json.load(f) - - for patches in bundle_data['patches']: - if patches != 'required': - apply_patch = input(f"[NOTE] Would you like to apply '{patches}' patch to your custom IPSW? [Y\\N]: ").lower() - if (apply_patch not in ('y', 'n')) or (apply_patch == 'n'): - continue - - for patch in bundle_data['patches'][patches]: - bsdiff4.file_patch_inplace(f"{ipsw}/{patch['file']}", f"{self.bundle}/{patch['patch']}") - - def check_update_support(self): - with open(f'{self.bundle}/Info.json', 'r') as f: - bundle_data = json.load(f) - - return bundle_data['update_support'] - - def fetch_bundle(self, device, version, buildid, path): - bundle_name = f'{device}_{version}_{buildid}' - bundle = requests.get(f'https://github.com/m1stadev/inferius-ext/raw/master/bundles/{bundle_name}.bundle') - if bundle.status_code == 404: - sys.exit(f'[ERROR] A Firmware Bundle does not exist for {device}, iOS {version}. Exiting.') - - output = f'{path}/{bundle_name}' - with zipfile.ZipFile(io.BytesIO(bundle.content), 'r') as f: - try: - f.extractall(output) - except OSError: - sys.exit('[ERROR] Ran out of storage while extracting Firmware Bundle. Exiting.') - - self.bundle = output - - def fetch_ota_manifest(self, device, path): - manifest = requests.get(f'https://raw.githubusercontent.com/m1stadev/inferius-ext/master/manifests/BuildManifest_{device}.plist') - if manifest.status_code == 404: - sys.exit(f'[ERROR] An OTA manifest does not exist for {device}. Exiting.') - - with open(path, 'wb') as f: - try: - f.write(manifest.content) - except OSError: - sys.exit('[ERROR] Ran out of storage while writing OTA manifest. Exiting.') - - def verify_bundle(self, bundle, tmpdir, api, buildid, boardconfig): - if not zipfile.is_zipfile(bundle): - return False - - try: - with zipfile.ZipFile(bundle, 'r') as f: - try: - bundle_data = json.loads(f.read('Info.json')) - except: - return False - - if not any(firm['buildid'] == buildid for firm in api['firmwares']): - return False - - if not any(board.lower() == boardconfig.lower() for board in bundle_data['boards']): - return False - - except: - return False - - bundle_path = f"{tmpdir}/{bundle.split('/')[-1].rsplit('.', 1)[0]}" - os.mkdir(bundle_path) - with zipfile.ZipFile(bundle) as f: - f.extractall(bundle_path) - - self.bundle = bundle_path - return True diff --git a/utils/dependencies.py b/utils/dependencies.py deleted file mode 100644 index 3c3b253..0000000 --- a/utils/dependencies.py +++ /dev/null @@ -1,26 +0,0 @@ -import shutil -import subprocess -import sys - - -class Checks: - def __init__(self): - self.check_bin('futurerestore') - self.check_bin('tsschecker') - self.check_bin('irecovery') - self.check_bin('img4tool') - - def check_bin(self, binary): - if shutil.which(binary) is None: - sys.exit(f"[ERROR] '{binary}' is not installed on your system. Exiting.") - - if binary == 'futurerestore': - fr_ver = subprocess.run((binary), stdout=subprocess.PIPE, universal_newlines=True).stdout - if '-m1sta' not in fr_ver.splitlines()[1]: - sys.exit(f"[ERROR] This futurerestore build cannot be used with Inferius. Exiting.") - - elif binary == 'irecovery': - try: - subprocess.check_call((binary, '-V'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessError: - sys.exit(f"[ERROR] Your irecovery version is too old. Exiting.") diff --git a/utils/device.py b/utils/device.py deleted file mode 100644 index d60dfe5..0000000 --- a/utils/device.py +++ /dev/null @@ -1,107 +0,0 @@ -import glob -import os -import subprocess -import sys -import usb, usb.backend.libusb1 - - -class Device: - def __init__(self, identifier): - self.device = identifier - self.baseband = self.check_baseband() - self.backend = self.get_backend() - self.platform = self.fetch_platform() - self.board = self.fetch_boardconfig() - self.apnonce = self.fetch_apnonce() - self.ecid = self.fetch_ecid() - - def check_baseband(self): - if self.device.startswith('iPhone'): - return True - - return self.device in ( # All (current) 64-bit cellular iPads vulerable to checkm8. - 'iPad4,2', - 'iPad4,3', - 'iPad4,5', - 'iPad4,6', - 'iPad4,8', - 'iPad4,9', - 'iPad5,2', - 'iPad5,4', - 'iPad6,8', - 'iPad6,4', - 'iPad7,2', - 'iPad7,4', - 'iPad8,3', - 'iPad8,4', - 'iPad8,7', - 'iPad8,8', - 'iPad8,10', - 'iPad8,12', - 'iPad11,2', - 'iPad11,4', - 'iPad13,2', - ) - - def check_pwndfu(self): - device = usb.core.find(idVendor=0x5AC, idProduct=0x1227, backend=self.backend) - if device is None: - sys.exit('[ERROR] Device in DFU mode not found. Exiting.') - - if 'PWND:' not in device.serial_number: - sys.exit('[ERROR] Attempting to restore a device not in Pwned DFU mode. Exiting.') - - def fetch_apnonce(self): - irecv = subprocess.check_output(('irecovery', '-q'), universal_newlines=True) - line = next(l for l in irecv.splitlines() if 'NONC:' in l) - return line.split(' ')[1] - - def get_backend(self): # Attempt to find a libusb 1.0 library to use as pyusb's backend, exit if one isn't found. - directories = ('/usr/lib', '/opt/procursus/lib', '/usr/local/lib') # Common library directories to search - - libusb1 = None - for libdir in directories: - for file in glob.glob(f'{libdir}/**', recursive=True): - if os.path.isdir(file) or (not any(ext in file for ext in ('so', 'dylib'))): - continue - - if 'libusb-1.0' in file: - libusb1 = file - break - - else: - continue - - break - - if libusb1 is None: - sys.exit('[ERROR] libusb is not installed. Install libusb. Exiting.') - - return usb.backend.libusb1.get_backend(find_library=lambda x:libusb1) - - def fetch_boardconfig(self): - irecv = subprocess.run(('irecovery', '-qv'), stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines=True).stderr - line = next(l for l in irecv.splitlines() if 'Connected to' in l) - return line.split(', ')[1].replace('model ', '') - - def fetch_ecid(self): - device = usb.core.find(idVendor=0x5AC, idProduct=0x1227, backend=self.backend) - if device is None: - sys.exit('[ERROR] Device in DFU mode not found. Exiting.') - - ecid = device.serial_number.split(' ')[5].split(':')[1] - - for x in ecid: - if x == '0': - ecid = ecid[1:] - else: - break - - return ecid - - def fetch_platform(self): - device = usb.core.find(idVendor=0x5AC, idProduct=0x1227, backend=self.backend) - if device is None: - sys.exit('[ERROR] Device in DFU mode not found. Exiting.') - - return int(device.serial_number.split(' ')[0].split(':')[1]) diff --git a/utils/ipsw.py b/utils/ipsw.py deleted file mode 100644 index d0b2d53..0000000 --- a/utils/ipsw.py +++ /dev/null @@ -1,96 +0,0 @@ -from utils.api import API -import hashlib -import json -import os -import shutil -import sys -import zipfile - - -class IPSW: - def __init__(self, ipsw): - self.ipsw = ipsw - - def create_ipsw(self, path, output, update, bootloader): - os.makedirs('IPSW', exist_ok=True) - - info = { - 'update_support': update, - 'bootloader': bootloader - } - - with open(f'{path}/.Inferius', 'w') as f: - json.dump(info, f) - - custom_ipsw = f'IPSW/{output}' - try: - shutil.make_archive(custom_ipsw, 'zip', path) - except: - sys.exit('[ERROR] Failed to create custom IPSW. Exiting.') - - os.rename(f'{custom_ipsw}.zip', custom_ipsw) - return custom_ipsw - - def extract_file(self, file, output): - try: - with zipfile.ZipFile(self.ipsw, 'r') as ipsw, open(output, 'wb') as f: - f.write(ipsw.read(file)) - except: - sys.exit(f"[ERROR] Failed to extract '{file}' from IPSW. Exiting.") - - def extract_ipsw(self, path): - with zipfile.ZipFile(self.ipsw, 'r') as ipsw: - try: - ipsw.extractall(path) - except: - sys.exit(f"[ERROR] Failed to extract '{self.ipsw}'. Exiting.") - - def read_file(self, file): - try: - with zipfile.ZipFile(self.ipsw, 'r') as ipsw: - return ipsw.read(file) - - except: - sys.exit(f"[ERROR] Failed to read '{file}' from IPSW. Exiting.") - - def verify_ipsw(self, ipsw_sha1): - if not os.path.isfile(self.ipsw): - sys.exit(f"[ERROR] '{self.ipsw}' does not exist. Exiting.") - - if not zipfile.is_zipfile(self.ipsw): - sys.exit(f"[ERROR] '{self.ipsw}' is not a valid IPSW. Exiting.") - - with zipfile.ZipFile(self.ipsw, 'r') as ipsw: - if '.Inferius' in ipsw.namelist(): - sys.exit(f"[ERROR] '{self.ipsw}' is not a stock IPSW. Exiting.") - - sha1 = hashlib.sha1() - with open(self.ipsw, 'rb') as ipsw: - fbuf = ipsw.read(8192) - while len(fbuf) != 0: - sha1.update(fbuf) - fbuf = ipsw.read(8192) - - if ipsw_sha1 != sha1.hexdigest(): - sys.exit(f"[ERROR] '{self.ipsw}' is not a valid IPSW. Exiting.") - - def verify_custom_ipsw(self, device, update): - if not os.path.isfile(self.ipsw): - sys.exit(f"[ERROR] '{self.ipsw}' does not exist. Exiting.") - - if not zipfile.is_zipfile(self.ipsw): - sys.exit(f"[ERROR] '{self.ipsw}' is not a valid IPSW. Exiting.") - - with zipfile.ZipFile(self.ipsw, 'r') as ipsw: - if '.Inferius' not in ipsw.namelist(): - sys.exit(f"[ERROR] '{self.ipsw}' is not a custom IPSW. Exiting.") - - info = json.loads(ipsw.read('.Inferius')) - - if (info['update_support'] == False) and (update == True): - sys.exit('[ERROR] This IPSW does not have support for update restores. Exiting.') - - api = API() - api.fetch_api(device) - if api.is_signed(info['bootloader']) == False: - sys.exit('[ERROR] This IPSW is too old to be used with Inferius. Create a new custom IPSW. Exiting.') diff --git a/utils/manifest.py b/utils/manifest.py deleted file mode 100644 index 7242dc8..0000000 --- a/utils/manifest.py +++ /dev/null @@ -1,33 +0,0 @@ -import plistlib - - -class Manifest: - def __init__(self, manifest): - self.manifest = plistlib.loads(manifest) - self.version = self.fetch_version() - self.buildid = self.fetch_buildid() - self.supported_devices = self.fetch_supported_devices() - - def fetch_buildid(self): return self.manifest['ProductBuildVersion'] - - def fetch_component_path(self, boardconfig, component): - return next(identity['Manifest'][component]['Info']['Path'] for identity in self.manifest['BuildIdentities'] if identity['Info']['DeviceClass'].lower() == boardconfig.lower()) - - def fetch_supported_devices(self): return self.manifest['SupportedProductTypes'] - - def fetch_version(self): return self.manifest['ProductVersion'] - -class RestoreManifest: - def __init__(self, manifest, boardconfig): - self.platform = self.fetch_platform(boardconfig, plistlib.loads(manifest)) - - def fetch_platform(self, boardconfig, manifest): - for device in manifest['DeviceMap']: - if device['BoardConfig'].lower() != boardconfig.lower(): - continue - - if device['Platform'].startswith('s5l89'): - return int(device['Platform'][3:-1], 16) - - else: - return int(device['Platform'][-4:], 16) diff --git a/utils/restore.py b/utils/restore.py deleted file mode 100644 index 400fb3a..0000000 --- a/utils/restore.py +++ /dev/null @@ -1,108 +0,0 @@ -import glob -import shutil -import subprocess -import os -import sys -import time - - -class Restore: - def __init__(self, identifier, platform): - self.device = identifier - self.platform = platform - - def restore(self, ipsw, cellular, update): - args = [ - 'futurerestore', - '-t', - self.blob, - '--latest-sep' - ] - - if update: - args.append('-u') - - if cellular: - args.append('--latest-baseband') - else: - args.append('--no-baseband') - - args.append(ipsw) - with open('futurerestore_error.log', 'w') as f: - f.write(f"{' '.join(args)}\n\n") - - with open('futurerestore_error.log', 'a') as f: - futurerestore = subprocess.run(args, stderr=subprocess.DEVNULL, stdout=f, universal_newlines=True) - - if os.path.isdir(ipsw.rsplit('.', 1)[0]): - shutil.rmtree(ipsw.rsplit('.', 1)[0]) - - if 'Done: restoring succeeded!' not in futurerestore.stdout: - sys.exit("[ERROR] Restore failed. Log written to 'futurerestore_error.log'. Exiting.") - - os.remove('futurerestore_error.log') - - def save_blobs(self, ecid, boardconfig, path, apnonce=None): - args = [ - 'tsschecker', - '-d', - self.device, - '-B', - boardconfig, - '-e', - f'0x{ecid}', - '-l', - '-s', - '--save-path', - path, - '--nocache' - ] - - if apnonce: - args.append('--apnonce') - args.append(apnonce) - - tsschecker = subprocess.check_output(args, universal_newlines=True) - if 'Saved shsh blobs!' not in tsschecker: - sys.exit('[ERROR] Failed to save blobs. Exiting.') - - if apnonce: - for blob in glob.glob(f'{path}/*.shsh*'): - if blob != self.signing_blob: - self.blob = blob - break - else: - self.signing_blob = glob.glob(f'{path}/*.shsh*')[0] - - def send_component(self, file, component): - if component == 'iBSS' and self.platform in (8960, 8015): #TODO: Reset device via pyusb rather than call an external binary. - irecovery_reset = subprocess.run(('irecovery', '-f', file), stdout=subprocess.DEVNULL) - if irecovery_reset.returncode != 0: - sys.exit('[ERROR] Failed to reset device. Exiting.') - - irecovery = subprocess.run(('irecovery', '-f', file), stdout=subprocess.DEVNULL) - if irecovery.returncode != 0: - sys.exit(f"[ERROR] Failed to send '{component}'. Exiting.") - - if component == 'iBEC': - if 8010 <= self.platform <= 8015: - irecovery_jump = subprocess.run(('irecovery', '-c', 'go'), stdout=subprocess.DEVNULL) - if irecovery_jump.returncode != 0: - sys.exit(f"[ERROR] Failed to boot '{component}'. Exiting.") - - time.sleep(3) - - def sign_component(self, file, output): - args = ( - 'img4tool', - '-c', - output, - '-p', - file, - '-s', - self.signing_blob - ) - - img4tool = subprocess.run(args, stdout=subprocess.DEVNULL) - if img4tool.returncode != 0: - sys.exit(f"[ERROR] Failed to sign '{file.split('/')[-1]}'. Exiting.")