diff --git a/README.md b/README.md index afc33b6..56ff1e4 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,15 @@ the split DSC. # Usage ``` -Usage: python -m ipsw_parser [OPTIONS] COMMAND [ARGS]... +Usage: ipsw-parser [OPTIONS] COMMAND [ARGS]... + + CLI utility for extracting info from IPSW files Options: --help Show this message and exit. Commands: - extract extract .ipsw into filesystem layout - info parse given .ipsw basic info + device-support Create DeviceSupport directory + extract Extract .ipsw into filesystem layout + info Parse given .ipsw basic info ``` diff --git a/ipsw_parser/__main__.py b/ipsw_parser/__main__.py index cc547ec..db4877f 100644 --- a/ipsw_parser/__main__.py +++ b/ipsw_parser/__main__.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 import logging +import plistlib +from datetime import datetime from pathlib import Path from typing import IO from zipfile import ZipFile @@ -23,14 +25,15 @@ @click.group() -def cli(): +def cli() -> None: + """ CLI utility for extracting info from IPSW files """ pass @cli.command('info') @click.argument('file', type=click.Path(exists=True, file_okay=True, dir_okay=False)) -def info(file): - """ parse given .ipsw basic info """ +def info(file) -> None: + """ Parse given .ipsw basic info """ ipsw = IPSW(ZipFile(file)) print(f'SupportedProductTypes: {ipsw.build_manifest.supported_product_types}') print(f'ProductVersion: {ipsw.build_manifest.product_version}') @@ -46,8 +49,8 @@ def info(file): @cli.command('extract') @click.argument('file', type=click.Path(exists=True, file_okay=True, dir_okay=False)) @click.argument('output', type=click.Path(exists=False)) -def extract(file: IO, output: str): - """ extract .ipsw into filesystem layout """ +def extract(file: IO, output: str) -> None: + """ Extract .ipsw into filesystem layout """ output = Path(output) if not output.exists(): @@ -59,5 +62,28 @@ def extract(file: IO, output: str): path=output, members=[f for f in ipsw.archive.filelist if f.filename.startswith('Firmware')]) +@cli.command('device-support') +@click.argument('file', type=click.Path(exists=True, file_okay=True, dir_okay=False)) +def device_support(file: IO) -> None: + """ Create DeviceSupport directory """ + device_support_path = Path('~/Library/Developer/Xcode/iOS DeviceSupport').expanduser() + ipsw = IPSW(ZipFile(file)) + device_support_path /= (f'{ipsw.build_manifest.supported_product_types[0]} ' + f'{ipsw.build_manifest.product_version} ({ipsw.build_manifest.product_build_version})') + build_identity = ipsw.build_manifest.build_identities[0] + symbols_path = device_support_path / 'Symbols' + build_identity.extract_dsc(symbols_path) + for file in (symbols_path / 'private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld').iterdir(): + file.unlink() + (device_support_path / 'Info.plist').write_bytes(plistlib.dumps({ + 'DSC Extractor Version': '1228.0.0.0.0', + 'DateCollected': datetime.now(), + 'Version': '16.0', + })) + (device_support_path / '.finalized').write_bytes(plistlib.dumps({})) + (device_support_path / '.processed_dyld_shared_cache_arm64e').touch() + (device_support_path / '.processing_lock').touch() + + if __name__ == '__main__': cli() diff --git a/ipsw_parser/build_identity.py b/ipsw_parser/build_identity.py index 80a4151..6865d12 100644 --- a/ipsw_parser/build_identity.py +++ b/ipsw_parser/build_identity.py @@ -3,7 +3,7 @@ from collections import UserDict from pathlib import Path from tempfile import TemporaryDirectory -from typing import List, Mapping +from typing import List, Mapping, Optional from cached_property import cached_property from plumbum import local @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -def _extract_dmg(buf: bytes, output: Path) -> None: +def _extract_dmg(buf: bytes, output: Path, sub_path: Optional[Path] = None) -> None: ipsw = local['ipsw'] hdiutil = local['hdiutil'] # darwin system statistically have problems cleaning up after detaching the mountpoint @@ -34,7 +34,11 @@ def _extract_dmg(buf: bytes, output: Path) -> None: hdiutil('attach', '-mountpoint', mnt, dmg) try: - shutil.copytree(mnt, output, symlinks=True, dirs_exist_ok=True) + if sub_path is None: + src = mnt + else: + src = mnt / sub_path + shutil.copytree(src, output, symlinks=True, dirs_exist_ok=True) except shutil.Error: # when overwriting the same files, some of them don't contain write permissions pass @@ -42,6 +46,22 @@ def _extract_dmg(buf: bytes, output: Path) -> None: hdiutil('detach', '-force', mnt) +def _split_dsc(root: Path) -> None: + ipsw = local['ipsw'] + dsc_paths = [ + root / 'System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64', + root / 'System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e', + root / 'private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64', + root / 'private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e'] + + for dsc in dsc_paths: + if not dsc.exists(): + continue + + logger.info(f'splitting DSC: {dsc}') + ipsw('dyld', 'split', dsc, '-o', root) + + class BuildIdentity(UserDict): def __init__(self, build_manifest, data): super().__init__(data) @@ -116,8 +136,19 @@ def populate_tss_request_parameters(self, parameters: Mapping, additional_keys: if requires_uid_mode is not None: parameters['RequiresUIDMode'] = requires_uid_mode + def extract_dsc(self, output: Path) -> None: + build_identity = self.build_manifest.build_identities[0] + if not build_identity.has_component('Cryptex1,SystemOS'): + return + + device_support_symbols_path = output / 'private/preboot/Cryptexes/OS/System' + device_support_symbols_path.mkdir(parents=True, exist_ok=True) + + _extract_dmg(build_identity.get_component('Cryptex1,SystemOS').data, device_support_symbols_path, + sub_path=Path('System')) + _split_dsc(output) + def extract(self, output: Path) -> None: - ipsw = local['ipsw'] logger.info(f'extracting into: {output}') build_identity = self.build_manifest.build_identities[0] @@ -147,14 +178,4 @@ def extract(self, output: Path) -> None: logger.info(f'extracting {name} into: {cryptex_path}') _extract_dmg(build_identity.get_component(name).data, cryptex_path) - dsc_paths = [output / 'System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64', - output / 'System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e', - output / 'System/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64', - output / 'System/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e'] - - for dsc in dsc_paths: - if not dsc.exists(): - continue - - logger.info(f'splitting DSC: {dsc}') - ipsw('dyld', 'split', dsc, '-o', output) + _split_dsc(output) diff --git a/ipsw_parser/ipsw.py b/ipsw_parser/ipsw.py index 7d2636a..55c2a6e 100644 --- a/ipsw_parser/ipsw.py +++ b/ipsw_parser/ipsw.py @@ -30,7 +30,8 @@ class IPSW: def __init__(self, archive: zipfile.ZipFile): self.archive = archive self._logger = logging.getLogger(__file__) - self.build_manifest = BuildManifest(self, self.archive.read(next(f for f in self.archive.namelist() if f.startswith('BuildManifest') and f.endswith('.plist')))) + self.build_manifest = BuildManifest(self, self.archive.read( + next(f for f in self.archive.namelist() if f.startswith('BuildManifest') and f.endswith('.plist')))) @cached_property def restore_version(self) -> bytes: