Skip to content

Commit

Permalink
(fix) extract and set bundleid & update applesign usage
Browse files Browse the repository at this point in the history
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
  • Loading branch information
leonjza committed Apr 6, 2021
1 parent 23ba6b0 commit bb33bce
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 40 deletions.
6 changes: 4 additions & 2 deletions objection/commands/mobile_packages.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion objection/console/cli.py
Expand Up @@ -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.
"""
Expand Down
125 changes: 89 additions & 36 deletions objection/utils/patchers/ios.py
Expand Up @@ -158,7 +158,10 @@ class IosPatcher(BasePlatformPatcher):
},
'unzip': {
'installation': 'macOS builtin command'
}
},
'plutil': {
'installation': 'macOS builtin command'
},
}

def __init__(self, skip_cleanup: bool = False):
Expand All @@ -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')
Expand All @@ -185,17 +189,25 @@ 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:
"""

# 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)
Expand All @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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)

Expand All @@ -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>[^\.]*\.\(.*\)<\/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>[^\.]*\.\(.*\)<\/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
Expand Down
3 changes: 2 additions & 1 deletion tests/utils/patchers/test_ios.py
Expand Up @@ -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')

0 comments on commit bb33bce

Please sign in to comment.