From bb33bce3ca9c36482951081e3d3721645f963124 Mon Sep 17 00:00:00 2001 From: Leon Jacobs Date: Tue, 6 Apr 2021 09:58:34 +0200 Subject: [PATCH] (fix) extract and set bundleid & update applesign usage For free accounts, the bundleid needs to be set. (see: nowsecure/node-applesign#113). A hecky fix to shell out and grep that out of the mobile provision is added. It can also be manually set to something else with the `--bundle-id` flags. Fixes #434 --- objection/commands/mobile_packages.py | 6 +- objection/console/cli.py | 4 +- objection/utils/patchers/ios.py | 125 ++++++++++++++++++-------- tests/utils/patchers/test_ios.py | 3 +- 4 files changed, 98 insertions(+), 40 deletions(-) diff --git a/objection/commands/mobile_packages.py b/objection/commands/mobile_packages.py index e911e34d..f42075c5 100644 --- a/objection/commands/mobile_packages.py +++ b/objection/commands/mobile_packages.py @@ -12,11 +12,13 @@ def patch_ios_ipa(source: str, codesign_signature: str, provision_file: str, binary_name: str, skip_cleanup: bool, unzip_unicode: bool, gadget_version: str = None, - pause: bool = False, gadget_config: str = None, script_source: str = None) -> None: + pause: bool = False, gadget_config: str = None, script_source: str = None, + bundle_id: str = None) -> None: """ Patches an iOS IPA by extracting, injecting the Frida dylib, codesigning the dylib and app executable and rezipping the IPA. + :param bundle_id: :param source: :param codesign_signature: :param provision_file: @@ -67,7 +69,7 @@ def patch_ios_ipa(source: str, codesign_signature: str, provision_file: str, bin if not patcher.are_requirements_met(): return - patcher.set_provsioning_profile(provision_file=provision_file) + patcher.set_provsioning_profile(provision_file=provision_file, bundle_id=bundle_id) patcher.extract_ipa(unzip_unicode, ipa_source=source) patcher.set_application_binary(binary=binary_name) patcher.patch_and_codesign_binary( diff --git a/objection/console/cli.py b/objection/console/cli.py index b086d123..1fc22fb0 100644 --- a/objection/console/cli.py +++ b/objection/console/cli.py @@ -306,8 +306,10 @@ def device_type(): @click.option('--script-source', '-l', default=None, help=( 'A script file to use with the the "path" config type. ' 'Remember that use the name of this file in your "path". It will be next to the config.'), show_default=False) +@click.option('--bundle-id', '-b', default=None, help='The bundleid to set when codesigning the IPA') def patchipa(source: str, gadget_version: str, codesign_signature: str, provision_file: str, binary_name: str, - skip_cleanup: bool, pause: bool, unzip_unicode: bool, gadget_config: str, script_source: str) -> None: + skip_cleanup: bool, pause: bool, unzip_unicode: bool, gadget_config: str, script_source: str, + bundle_id: str) -> None: """ Patch an IPA with the FridaGadget dylib. """ diff --git a/objection/utils/patchers/ios.py b/objection/utils/patchers/ios.py index a377487a..f783bfbf 100644 --- a/objection/utils/patchers/ios.py +++ b/objection/utils/patchers/ios.py @@ -158,7 +158,10 @@ class IosPatcher(BasePlatformPatcher): }, 'unzip': { 'installation': 'macOS builtin command' - } + }, + 'plutil': { + 'installation': 'macOS builtin command' + }, } def __init__(self, skip_cleanup: bool = False): @@ -175,6 +178,7 @@ def __init__(self, skip_cleanup: bool = False): self.patched_ipa_path = None self.patched_codesigned_ipa_path = None self.skip_cleanup = skip_cleanup + self.bundle_id = None # temp_file to copy an IPA to _, self.temp_file = tempfile.mkstemp(suffix='.ipa') @@ -185,10 +189,11 @@ def __init__(self, skip_cleanup: bool = False): # cleanup the temp_directory to work with self._cleanup_extracted_data() - def set_provsioning_profile(self, provision_file: str = None) -> None: + def set_provsioning_profile(self, provision_file: str = None, bundle_id: str = None) -> None: """ Sets the provision file to use during patching. + :param bundle_id: :param provision_file: :return: """ @@ -196,6 +201,13 @@ def set_provsioning_profile(self, provision_file: str = None) -> None: # have provision file? set it and be done if provision_file: self.provision_file = provision_file + + if bundle_id: + click.secho('Setting bundleid to specified value: {}'.format(bundle_id), dim=True) + self.bundle_id = bundle_id + else: + self._set_bundle_id_from_profile() + return click.secho('No provision file specified, searching for one...', bold=True) @@ -219,17 +231,11 @@ def set_provsioning_profile(self, provision_file: str = None) -> None: _, decoded_location = tempfile.mkstemp('decoded_provision') # Decode the mobile provision using macOS's security cms tool - delegator.run(self.list2cmdline( - [ - self.required_commands['security']['location'], - 'cms', - '-D', - '-i', - pf, - '-o', - decoded_location - ] - ), timeout=self.command_run_timeout) + delegator.run(self.list2cmdline([ + self.required_commands['security']['location'], + 'cms', '-D', '-i', pf, + '-o', decoded_location + ]), timeout=self.command_run_timeout) # read the expiration date from the profile with open(decoded_location, 'rb') as f: @@ -254,6 +260,12 @@ def set_provsioning_profile(self, provision_file: str = None) -> None: click.secho('Found a valid provisioning profile', fg='green', bold=True) self.provision_file = sorted(expirations, key=expirations.get, reverse=True)[0] + if bundle_id: + click.secho('Setting bundleid to specified value: {}'.format(bundle_id), dim=True) + self.bundle_id = bundle_id + else: + self._set_bundle_id_from_profile() + def extract_ipa(self, unzip_unicode, ipa_source: str) -> None: """ Extracts a source IPA into the temporary directories. @@ -346,15 +358,13 @@ def patch_and_codesign_binary(self, frida_gadget: str, codesign_signature: str, shutil.copyfile(gadget_config, os.path.join(self.app_folder, 'Frameworks', 'FridaGadget.config')) # patch the app binary - load_library_output = delegator.run(self.list2cmdline( - [ - self.required_commands['insert_dylib']['location'], - '--strip-codesig', - '--inplace', - '@executable_path/Frameworks/FridaGadget.dylib', - self.app_binary - ] - ), timeout=self.command_run_timeout) + load_library_output = delegator.run(self.list2cmdline([ + self.required_commands['insert_dylib']['location'], + '--strip-codesig', + '--inplace', + '@executable_path/Frameworks/FridaGadget.dylib', + self.app_binary + ]), timeout=self.command_run_timeout) # check if the insert_dylib call may have failed if 'Added LC_LOAD_DYLIB' not in load_library_output.out: @@ -380,8 +390,7 @@ def patch_and_codesign_binary(self, frida_gadget: str, codesign_signature: str, '-v', '-s', codesign_signature, - dylib]) - ) + dylib])) def archive_and_codesign(self, original_name: str, codesign_signature: str) -> None: """ @@ -413,18 +422,19 @@ def zipdir(path, ziph): self.patched_codesigned_ipa_path = os.path.join(self.temp_directory, os.path.basename( '{0}-frida-codesigned.ipa'.format(os.path.splitext(original_name)[0]))) - ipa_codesign = delegator.run(self.list2cmdline( - [ - self.required_commands['applesign']['location'], - '-i', - codesign_signature, - '-m', - self.provision_file, - '-o', - self.patched_codesigned_ipa_path, - self.patched_ipa_path - ] - ), timeout=self.command_run_timeout) + ipa_codesign = delegator.run(self.list2cmdline([ + self.required_commands['applesign']['location'], + '--identity', + codesign_signature, + '--mobileprovision', + self.provision_file, + '--bundleid', + self.bundle_id, + '--clone-entitlements', + '--output', + self.patched_codesigned_ipa_path, + self.patched_ipa_path + ]), timeout=self.command_run_timeout) click.secho(ipa_codesign.err, dim=True) @@ -437,6 +447,49 @@ def get_patched_ipa_path(self) -> str: return self.patched_codesigned_ipa_path + def _set_bundle_id_from_profile(self): + """ + Extracts and sets a bundle id from a decoded mobileprovision + + :return: + """ + + if not self.provision_file: + click.secho('Provisioning profile not set. Skipping bundleid extraction', dim=True) + return + + _, decoded_location = tempfile.mkstemp('decoded_provision') + + # Decode the mobile provision using macOS's security cms tool + delegator.run(self.list2cmdline([ + self.required_commands['security']['location'], + 'cms', '-D', '-i', self.provision_file, + '-o', decoded_location + ]), timeout=self.command_run_timeout) + + # https://stackoverflow.com/a/66820375 + # security cms -D -i your.mobileprovision | plutil -extract + # Entitlements.application-identifier xml1 -o - - | grep string | + # sed 's/^[^\.]*\.\(.*\)<\/string>$/\1/g' + c = delegator.run(self.list2cmdline([ + 'cat', decoded_location + ]), timeout=self.command_run_timeout).pipe(self.list2cmdline([ + self.required_commands['plutil']['location'], + '-extract', 'Entitlements.application-identifier', 'xml1', '-o', '-', '-' + ]), timeout=self.command_run_timeout).pipe(self.list2cmdline([ + 'grep', 'string' + ]), timeout=self.command_run_timeout).pipe(self.list2cmdline([ + 'sed', r's/^[^\.]*\.\(.*\)<\/string>$/\1/g' + ]), timeout=self.command_run_timeout) + + if len(c.out) > 0: + self.bundle_id = c.out.strip() + + click.secho('Mobile provision bundle identifier is: {}'.format(self.bundle_id), dim=True) + + # cleanup the temp path + os.remove(decoded_location) + def _cleanup_extracted_data(self) -> None: """ Small helper method to cleanup temporary files created diff --git a/tests/utils/patchers/test_ios.py b/tests/utils/patchers/test_ios.py index 6b2f8e5f..a0799c53 100644 --- a/tests/utils/patchers/test_ios.py +++ b/tests/utils/patchers/test_ios.py @@ -61,8 +61,9 @@ def test_can_find_asset_download_url(self): class TestIosPatcher(unittest.TestCase): @mock.patch('objection.utils.patchers.ios.IosPatcher.__init__', mock.Mock(return_value=None)) @mock.patch('objection.utils.patchers.ios.IosPatcher.__del__', mock.Mock(return_value=None)) + @mock.patch('objection.utils.patchers.ios.click.secho', mock.Mock(return_value=None)) def test_sets_provisioning_profile(self): patcher = IosPatcher() - patcher.set_provsioning_profile('profile.mobileprovision') + patcher.set_provsioning_profile('profile.mobileprovision', 'com.foo.bar') self.assertEqual(patcher.provision_file, 'profile.mobileprovision')