Skip to content

Commit

Permalink
Add ability to specify gadget version to use with patchers.
Browse files Browse the repository at this point in the history
  • Loading branch information
leonjza committed Feb 13, 2018
1 parent 3dd2aa4 commit 83a1a2c
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 31 deletions.
48 changes: 33 additions & 15 deletions objection/commands/mobile_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


def patch_ios_ipa(source: str, codesign_signature: str, provision_file: str, binary_name: str,
skip_cleanup: bool) -> None:
skip_cleanup: bool, gadget_version: str = None) -> None:
"""
Patches an iOS IPA by extracting, injecting the Frida dylib,
codesigning the dylib and app executable and rezipping the IPA.
Expand All @@ -21,21 +21,30 @@ def patch_ios_ipa(source: str, codesign_signature: str, provision_file: str, bin
:param provision_file:
:param binary_name:
:param skip_cleanup:
:param gadget_version:
:return:
"""

github = Github()
github = Github(gadget_version=gadget_version)
ios_gadget = IosGadget(github)

# get the gadget version numbers
github_version = github.get_latest_version()
# check if a gadget version was specified. if not, get the latest one.
if gadget_version is not None:
github_version = gadget_version
click.secho('Using manually specified version: {0}'.format(gadget_version), fg='green', bold=True)
else:
github_version = github.set_latest_version()
click.secho('Using latest Github gadget version: {0}'.format(github_version), fg='green', bold=True)

# get the local version number of the stored gadget
local_version = ios_gadget.get_local_version('ios_universal')

# check if the local version needs updating. this can be either because
# the version is outdated or we simply don't have the gadget yet
if parse_version(github_version) > parse_version(local_version) or not ios_gadget.gadget_exists():
if parse_version(github_version) != parse_version(local_version) or not ios_gadget.gadget_exists():
# download!
click.secho('Github FridaGadget is v{0}, local is v{1}. Updating...'.format(
click.secho('Remote FridaGadget version is v{0}, local is v{1}. Downloading...'.format(
github_version, local_version), fg='green')

# download, unpack, update local version and cleanup the temp files.
Expand All @@ -44,7 +53,7 @@ def patch_ios_ipa(source: str, codesign_signature: str, provision_file: str, bin
.set_local_version('ios_universal', github_version) \
.cleanup()

click.secho('Using Gadget version: {0}'.format(github_version), fg='green')
click.secho('Patcher will be using Gadget version: {0}'.format(github_version), fg='green')

# start the patching process
patcher = IosPatcher(skip_cleanup=skip_cleanup)
Expand All @@ -67,7 +76,7 @@ def patch_ios_ipa(source: str, codesign_signature: str, provision_file: str, bin


def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup: bool = True,
enable_debug: bool = True) -> None:
enable_debug: bool = True, gadget_version: str = None) -> None:
"""
Patches an Android APK by extracting, patching SMALI, repackaging
and signing a new APK.
Expand All @@ -77,10 +86,11 @@ def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup:
:param pause:
:param skip_cleanup:
:param enable_debug:
:param gadget_version:
:return:
"""

github = Github()
github = Github(gadget_version=gadget_version)
android_gadget = AndroidGadget(github)

# without an architecture set, attempt to determine one using adb
Expand All @@ -101,24 +111,32 @@ def patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup:
# set the architecture we are interested in
android_gadget.set_architecture(architecture)

# get the gadget version numbers
github_version = github.get_latest_version()
# check if a gadget version was specified. if not, get the latest one.
if gadget_version is not None:
github_version = gadget_version
click.secho('Using manually specified version: {0}'.format(gadget_version), fg='green', bold=True)
else:
github_version = github.set_latest_version()
click.secho('Using latest Github gadget version: {0}'.format(github_version), fg='green', bold=True)

# get local version of the stored gadget
local_version = android_gadget.get_local_version('android_' + architecture)

# check if the local version needs updating. this can be either because
# the version is outdated or we simply don't have the gadget yet
if parse_version(github_version) > parse_version(local_version) or not android_gadget.gadget_exists():
# the version is outdated or we simply don't have the gadget yet, or, we want
# a very specific version
if parse_version(github_version) != parse_version(local_version) or not android_gadget.gadget_exists():
# download!
click.secho('Github FridaGadget is v{0}, local is v{1}. Updating...'.format(
click.secho('Remote FridaGadget version is v{0}, local is v{1}. Downloading...'.format(
github_version, local_version), fg='green')

# download, unpack, update local version and cleanup the temp files.
android_gadget.download() \
.unpack() \
.set_local_version('android_' + architecture, github.get_latest_version()) \
.set_local_version('android_' + architecture, github_version) \
.cleanup()

click.secho('Using Gadget version: {0}'.format(github_version), fg='green')
click.secho('Patcher will be using Gadget version: {0}'.format(github_version), fg='green')

patcher = AndroidPatcher(skip_cleanup=skip_cleanup)

Expand Down
10 changes: 8 additions & 2 deletions objection/console/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,17 @@ def device_type():

@cli.command()
@click.option('--source', '-s', help='The source IPA to patch', required=True)
@click.option('--gadget-version', '-V', help=('The gadget version to use. If not '
'specified, the latest version will be used.'), default=None)
@click.option('--codesign-signature', '-c',
help='Codesigning Identity to use. Get it with: `security find-identity -p codesigning -v`',
required=True)
@click.option('--provision-file', '-p', help='The .mobileprovision file to use in the patched .ipa')
@click.option('--binary-name', '-b', help='Name of the Mach-O binary in the IPA (used to patch with Frida)')
@click.option('--skip-cleanup', '-k', is_flag=True,
help='Do not clean temporary files once finished.', show_default=True)
def patchipa(source: str, codesign_signature: str, provision_file: str, binary_name: str, skip_cleanup: bool) -> None:
def patchipa(source: str, gadget_version: str, codesign_signature: str, provision_file: str, binary_name: str,
skip_cleanup: bool) -> None:
"""
Patch an IPA with the FridaGadget dylib.
"""
Expand All @@ -199,13 +202,16 @@ def patchipa(source: str, codesign_signature: str, provision_file: str, binary_n
'`adb shell getprop ro.product.cpu.abi`. If it '
'is not specified, this command will try and '
'determine it automatically.'), required=False)
@click.option('--gadget-version', '-V', help=('The gadget version to use. If not '
'specified, the latest version will be used.'), default=None)
@click.option('--pause', '-p', is_flag=True, help='Pause the patcher before rebuilding the APK.',
show_default=True)
@click.option('--skip-cleanup', '-k', is_flag=True,
help='Do not clean temporary files once finished.', show_default=True)
@click.option('--enable-debug', '-d', is_flag=True,
help='Set the android:debuggable flag to true in the application manifiest.', show_default=True)
def patchapk(source: str, architecture: str, pause: bool, skip_cleanup: bool, enable_debug: bool) -> None:
def patchapk(source: str, architecture: str, gadget_version: str, pause: bool, skip_cleanup: bool,
enable_debug: bool) -> None:
"""
Patch an APK with the frida-gadget.so.
"""
Expand Down
3 changes: 2 additions & 1 deletion objection/utils/patchers/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def download(self):
"""

download_url = self._get_download_url()
click.secho('Downloading from: {0}'.format(download_url), dim=True)

# stream the download using requests
library = requests.get(download_url, stream=True)
Expand All @@ -126,7 +127,7 @@ def download(self):

def _get_download_url(self) -> str:
"""
Determines the download URL to use for the iOS
Determines the download URL to use for the Android
gadget.
:return:
Expand Down
27 changes: 21 additions & 6 deletions objection/utils/patchers/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
class Github(object):
""" Interact with Github """

GITHUB_RELEASE = 'https://api.github.com/repos/frida/frida/releases/latest'
GITHUB_LATEST_RELEASE = 'https://api.github.com/repos/frida/frida/releases/latest'
GITHUB_TAGGED_RELEASE = 'https://api.github.com/repos/frida/frida/releases/tags/{tag}'

def __init__(self):
# the 'context' of this Github instance
gadget_version = None

def __init__(self, gadget_version: str = None):
"""
Init a new instance of Github
"""

if gadget_version:
self.gadget_version = gadget_version

self.request_cache = {}

def _call(self, endpoint: str) -> dict:
Expand All @@ -34,21 +41,29 @@ def _call(self, endpoint: str) -> dict:
# and return it
return results

def get_latest_version(self) -> str:
def set_latest_version(self) -> str:
"""
Call Github and get the tag_name of the latest
release.
:return:
"""

return self._call(self.GITHUB_RELEASE)['tag_name']
self.gadget_version = self._call(self.GITHUB_LATEST_RELEASE)['tag_name']

return self.gadget_version

def get_assets(self) -> dict:
"""
Gets the assets for the latest release.
Gets the assets for the currently selected gadget_version.
:return:
"""

return self._call(self.GITHUB_RELEASE)['assets']
assets = self._call(self.GITHUB_TAGGED_RELEASE.format(tag=self.gadget_version))

if 'assets' not in assets:
raise Exception(('Unable to determine assets for gadget version \'{0}\'. '
'Are you sure this version is available on Github?').format(self.gadget_version))

return assets['assets']
1 change: 1 addition & 0 deletions objection/utils/patchers/ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def download(self):
"""

download_url = self._get_download_url()
click.secho('Downloading from: {0}'.format(download_url), dim=True)

# stream the download using requests
dylib = requests.get(download_url, stream=True)
Expand Down
14 changes: 8 additions & 6 deletions tests/commands/test_mobile_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TestMobilePackages(unittest.TestCase):
@mock.patch('objection.commands.mobile_packages.shutil')
@mock.patch('objection.commands.mobile_packages.os')
def test_patching_ios_ipa(self, mock_os, mock_shutil, mock_iospatcher, mock_iosgadget, mock_github):
mock_github.return_value.get_latest_version.return_value = '1.0'
mock_github.return_value.set_latest_version.return_value = '1.0'
mock_iosgadget.return_value.get_local_version.return_value = '0.9'

mock_iospatcher.return_value.are_requirements_met.return_value = True
Expand All @@ -21,8 +21,9 @@ def test_patching_ios_ipa(self, mock_os, mock_shutil, mock_iospatcher, mock_iosg
with capture(patch_ios_ipa, 'test.ipa', '00-11', '/foo', '', False) as o:
output = o

expected_output = """Github FridaGadget is v1.0, local is v0.9. Updating...
Using Gadget version: 1.0
expected_output = """Using latest Github gadget version: 1.0
Remote FridaGadget version is v1.0, local is v0.9. Downloading...
Patcher will be using Gadget version: 1.0
Copying final ipa from /foo/ipa to current directory...
"""

Expand All @@ -41,7 +42,7 @@ def test_patching_ios_ipa(self, mock_os, mock_shutil, mock_iospatcher, mock_iosg
@mock.patch('objection.commands.mobile_packages.input', create=True)
def test_patching_android_apk(self, mock_input, mock_delegator, mock_os, mock_shutil, mock_androidpatcher,
mock_androidgadget, mock_github):
mock_github.return_value.get_latest_version.return_value = '1.0'
mock_github.return_value.set_latest_version.return_value = '1.0'
mock_androidgadget.return_value.get_local_version.return_value = '0.9'

mock_androidpatcher.return_value.are_requirements_met.return_value = True
Expand All @@ -59,8 +60,9 @@ def test_patching_android_apk(self, mock_input, mock_delegator, mock_os, mock_sh

expected_output = """No architecture specified. Determining it using `adb`...
Detected target device architecture as: x86
Github FridaGadget is v1.0, local is v0.9. Updating...
Using Gadget version: 1.0
Using latest Github gadget version: 1.0
Remote FridaGadget version is v1.0, local is v0.9. Downloading...
Patcher will be using Gadget version: 1.0
Patching paused. The next step is to rebuild the APK. If you require any manual fixes, the current temp directory is:
/foo/apk
Copying final apk from /foo/bar/apk to current directory...
Expand Down
13 changes: 12 additions & 1 deletion tests/utils/patchers/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,21 @@ def test_makes_call_and_gets_latest_version(self, mock_requests):

mock_requests.get.return_value = mock_response

result = self.github.get_latest_version()
result = self.github.set_latest_version()

self.assertEqual(result, self.mock_response['tag_name'])

@mock.patch('objection.utils.patchers.github.requests')
def test_makes_call_and_fails_to_get_assets(self, mock_requests):
mock_response = mock.Mock()
mock_response.status_code = 404
mock_response.json.return_value = {}

mock_requests.get.return_value = mock_response

with self.assertRaises(Exception) as _:
self.github.get_assets()

@mock.patch('objection.utils.patchers.github.requests')
def test_makes_call_and_gets_assets(self, mock_requests):
mock_response = mock.Mock()
Expand Down

0 comments on commit 83a1a2c

Please sign in to comment.