From 90c17279d21828843eb2390ffe677132bd0127c3 Mon Sep 17 00:00:00 2001 From: Cameron Lowell Palmer Date: Thu, 1 Feb 2018 16:03:00 +0100 Subject: [PATCH 1/2] Cleaned up the python in the app and made app spawning faster / more reliable --- .gitignore | 4 + README.md | 71 ++++---- app.js | 52 ------ dump.js | 14 +- dump.py | 462 +++++++++++++++++++++++++++++------------------------ 5 files changed, 289 insertions(+), 314 deletions(-) delete mode 100644 app.js diff --git a/.gitignore b/.gitignore index e43b0f9..cbba3b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ .DS_Store +.idea +*.ipa + +frida/ diff --git a/README.md b/README.md index 1a04ed4..3b0b2d5 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,37 @@ # frida-ios-dump -pull decrypted ipa from jailbreak device +Pull a decrypted IPA from a jailbroken device -### Usage +## Usage -## 1. install [frida](http://www.frida.re/) on device and mac - -## 2. iproxy 2222 22 - -## 3. ./dump.py 微信 + 1. Install [frida](http://www.frida.re/) on device and mac + 2. Run usbmuxd/iproxy SSH forwarding over USB (Default 2222 -> 22) + 3. Run ./dump.py `Display name` or `Bundle identifier` ``` -➜ frida-ios-dump ./dump.py 微信 -open target app...... -start dump target app...... -start dump /var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/WeChat -WeChat 100% 68MB 11.4MB/s 00:05 -start dump /private/var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/Frameworks/WCDB.framework/WCDB -WCDB 100% 2555KB 11.0MB/s 00:00 -start dump /private/var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/Frameworks/MMCommon.framework/MMCommon -MMCommon 100% 979KB 10.6MB/s 00:00 -start dump /private/var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/Frameworks/MultiMedia.framework/MultiMedia -MultiMedia 100% 6801KB 11.1MB/s 00:00 -start dump /private/var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/Frameworks/mars.framework/mars -mars 100% 7462KB 11.1MB/s 00:00 -AppIcon60x60@2x.png 100% 2253 230.9KB/s 00:00 -AppIcon60x60@3x.png 100% 4334 834.8KB/s 00:00 -AppIcon76x76@2x~ipad.png 100% 2659 620.6KB/s 00:00 -AppIcon76x76~ipad.png 100% 1523 358.0KB/s 00:00 -AppIcon83.5x83.5@2x~ipad.png 100% 2725 568.9KB/s 00:00 -Assets.car 100% 10MB 11.1MB/s 00:00 -....... -AppIntentVocabulary.plist 100% 197 52.9KB/s 00:00 -AppIntentVocabulary.plist 100% 167 43.9KB/s 00:00 -AppIntentVocabulary.plist 100% 187 50.2KB/s 00:00 -InfoPlist.strings 100% 1720 416.4KB/s 00:00 -TipsPressTalk@2x.png 100% 14KB 2.2MB/s 00:00 -mm.strings 100% 404KB 10.2MB/s 00:00 -network_setting.html 100% 1695 450.4KB/s 00:00 -InfoPlist.strings 100% 1822 454.1KB/s 00:00 -mm.strings 100% 409KB 10.2MB/s 00:00 -network_setting.html 100% 1819 477.5KB/s 00:00 -InfoPlist.strings 100% 1814 466.8KB/s 00:00 -mm.strings 100% 409KB 10.3MB/s 00:00 -network_setting.html 100% 1819 404.9KB/s 00:00 +./dump.py Aftenposten +Start the target app Aftenposten +Dumping Aftenposten to /var/folders/wn/9v1hs8ds6nv_xj7g95zxyl140000gn/T +start dump /var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/AftenpostenApp +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/AFNetworking.framework/AFNetworking +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/ATInternet_iOS_ObjC_SDK.framework/ATInternet_iOS_ObjC_SDK +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/SPTEventCollector.framework/SPTEventCollector +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/SPiDSDK.framework/SPiDSDK +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCore.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCoreData.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCoreGraphics.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCoreImage.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCoreLocation.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftDarwin.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftDispatch.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftFoundation.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftObjectiveC.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftQuartzCore.dylib +start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftUIKit.dylib +Generating Aftenposten.ipa + +Done. ``` -congratulations!!! You've got a decrypted ipa file. +Congratulations!!! You've got a decrypted IPA file. -Drag to [MonkeyDev](https://github.com/AloneMonkey/MonkeyDev), Happy hacking! \ No newline at end of file +Drag to [MonkeyDev](https://github.com/AloneMonkey/MonkeyDev), Happy hacking! diff --git a/app.js b/app.js deleted file mode 100644 index 2bcd376..0000000 --- a/app.js +++ /dev/null @@ -1,52 +0,0 @@ -//by: AloneMonkey - -const LSApplicationWorkspace = ObjC.classes.LSApplicationWorkspace; - -function openApplication(appid){ - const workspace = LSApplicationWorkspace.defaultWorkspace(); - return workspace.openApplicationWithBundleID_(appid); -} - -function getbundleid(name){ - const workspace = LSApplicationWorkspace.defaultWorkspace(); - const apps = workspace.allApplications(); - var result; - for(var index = 0; index < apps.count(); index++){ - var proxy = apps.objectAtIndex_(index); - if(proxy.localizedName() && proxy.localizedName().toString() == name){ - return proxy.bundleIdentifier().toString(); - } - } - return "" -}; - -function getdisplayname(bundleid){ - const workspace = LSApplicationWorkspace.defaultWorkspace(); - const apps = workspace.allApplications(); - var result; - for(var index = 0; index < apps.count(); index++){ - var proxy = apps.objectAtIndex_(index); - if(proxy.bundleIdentifier() && proxy.bundleIdentifier().toString() == bundleid){ - return proxy.localizedName().toString(); - } - } - return "" -} - -function handleMessage(message) { - var bundleid; - var displayname; - if(message['name']){ - displayname = message['name'] - bundleid = getbundleid(displayname); - }else if(message['bundleid']){ - bundleid = message['bundleid'] - displayname = getdisplayname(bundleid); - } - if(bundleid.length > 0){ - openApplication(bundleid); - } - send({ opened: displayname }); -} - -recv(handleMessage); \ No newline at end of file diff --git a/dump.js b/dump.js index d40d191..4c48062 100644 --- a/dump.js +++ b/dump.js @@ -175,9 +175,9 @@ var LC_SEGMENT_64 = 0x19; var LC_ENCRYPTION_INFO = 0x21; var LC_ENCRYPTION_INFO_64 = 0x2C; -function pad(str, n) { - return Array(n-str.length+1).join("0")+str; -} +function pad(str, n) { + return Array(n-str.length+1).join("0")+str; +} function swap32(value) { value = pad(value.toString(16),8) @@ -214,7 +214,7 @@ function dumpModule(name) { if(!access(allocStr(newmodpath),0)){ remove(allocStr(newmodpath)); } - + var fmodule = open(newmodpath, O_CREAT | O_RDWR, 0); var foldmodule = open(oldmodpath, O_RDONLY, 0); @@ -288,7 +288,7 @@ function dumpModule(name) { var segments = []; for (var i = 0; i < ncmds; i++) { var cmd = getU32(modbase.add(off)); - var cmdsize = getU32(modbase.add(off + 4)); + var cmdsize = getU32(modbase.add(off + 4)); if (cmd == LC_ENCRYPTION_INFO || cmd == LC_ENCRYPTION_INFO_64) { offset_cryptid = off + 16; crypt_off = getU32(modbase.add(off + 8)); @@ -305,11 +305,11 @@ function dumpModule(name) { lseek(fmodule, crypt_off, SEEK_SET); write(fmodule, modbase.add(crypt_off), crypt_size); } - + close(fmodule); close(foldmodule); return newmodpath -} +} function handleMessage(message) { //start dump diff --git a/dump.py b/dump.py index eba4198..0732a8a 100755 --- a/dump.py +++ b/dump.py @@ -1,270 +1,306 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#Author : AloneMonkey -#blog: www.alonemonkey.com +# Author : AloneMonkey +# blog: www.alonemonkey.com import sys import codecs import frida -import threading +import threading import os import shutil import time import getopt +import tempfile +import subprocess reload(sys) sys.setdefaultencoding('utf8') -DUMP_JS = './dump.js' -APP_JS = './app.js' -OUTPUT = "Payload" +script_dir = os.path.dirname(os.path.realpath(__file__)) + +DUMP_JS = os.path.join(script_dir, 'dump.js') + +SSH_USER_HOST='root@localhost:' +SSH_PORT=2222 + +TEMP_DIR = tempfile.gettempdir() +PAYLOAD_DIR = 'Payload' +PAYLOAD_PATH = os.path.join(TEMP_DIR, PAYLOAD_DIR) file_dict = {} -opened = threading.Event() finished = threading.Event() -global session -global name -#show usage +# show usage def usage(): - print '-------------------------frida-ios-dump(by AloneMonkey v2.0)----------------------------' - print '\t%-20s\t%s' % ('-h,--help','Show help menu.'); - print '\t%-20s\t%s' % ('displayname','Decrypt the application of the specified display name. ps: ./dump.py 微信'); - print '\t%-20s\t%s' % ('-l','List the app has been installed.'); - print '\t%-20s\t%s' % ('-b bundleid','Decrypt the application of the specified bundleid. ps: ./dump.py com.tencent.xin'); - exit(0) + print '-------------------------frida-ios-dump(by AloneMonkey v2.0)----------------------------' + print '\t%-20s\t%s' % ('-h,--help', 'Show help menu.') + print '\t%-20s\t%s' % ('name', 'Decrypt the application with the specified display name or bundle identifier. ps: ./dump.py 微信') + print '\t%-20s\t%s' % ('-l', 'List the installed apps.') + def get_usb_iphone(): - dManager = frida.get_device_manager(); - changed = threading.Event() - def on_changed(): - changed.set() - dManager.on('changed',on_changed) + dManager = frida.get_device_manager() + changed = threading.Event() - device = None - while device is None: - devices = [dev for dev in dManager.enumerate_devices() if dev.type == 'tether'] - if len(devices) == 0: - print 'Waiting for usb device...' - changed.wait() - else: - device = devices[0] + def on_changed(): + changed.set() - dManager.off('changed',on_changed) - - return device + dManager.on('changed', on_changed) -def gen_ipa(target): - try: - app_name = file_dict["app"] - for key, value in file_dict.items(): - if key != "app": - shutil.move(target +"/"+ key, target + "/" + app_name + "/" + value); - (shotname,extension) = os.path.splitext(app_name) - os.system(u''.join(("zip -qr ", name.replace(" ", "\\ "), ".ipa ./Payload")).encode('utf-8').strip()); - os.system("rm -rf ./Payload"); - except Exception as e: - print e - finished.set(); + device = None + while device is None: + devices = [dev for dev in dManager.enumerate_devices() if dev.type == 'tether'] + if len(devices) == 0: + print 'Waiting for USB device...' + changed.wait() + else: + device = devices[0] + + dManager.off('changed', on_changed) + + return device + + +def generate_ipa(path, display_name, bundle_identifier): + ipa_filename = display_name.replace(' ', '\\ ') + '.ipa' + + print 'Generating {}'.format(ipa_filename) + try: + app_name = file_dict['app'] + + for key, value in file_dict.items(): + from_dir = os.path.join(path, key) + to_dir = os.path.join(path, app_name, value) + if key != 'app': + shutil.move(from_dir, to_dir) + + target_dir = './' + PAYLOAD_DIR + zip_args = ('zip', '-qr', os.path.join(os.getcwd(), ipa_filename), target_dir) + subprocess.check_call(zip_args, cwd=TEMP_DIR) + shutil.rmtree(PAYLOAD_PATH) + print + except Exception as e: + print e + finished.set() + + +def on_message(message, data): + global name + if message.has_key('payload'): + payload = message['payload'] + if payload.has_key('opened'): + name = payload['opened'] + + if payload.has_key('dump'): + origin_path = payload['path'] + dump_path = payload['dump'] + + scp_from = SSH_USER_HOST + dump_path.replace(' ', '\ ') + scp_to = PAYLOAD_PATH + u'/' + scp_args = ('scp', '-P {}'.format(SSH_PORT), scp_from, scp_to) + subprocess.check_call(scp_args) + + chmod_dir = os.path.join(PAYLOAD_PATH, os.path.basename(dump_path)) + chmod_args = ('chmod', '655', chmod_dir) + subprocess.check_call(chmod_args) + + index = origin_path.find('.app/') + file_dict[os.path.basename(dump_path)] = origin_path[index + 5:] + + if payload.has_key('app'): + app_path = payload['app'] + + scp_from = SSH_USER_HOST + app_path.replace(' ', '\ ') + scp_to = PAYLOAD_PATH + u'/' + scp_args = ('scp', '-r', '-P {}'.format(SSH_PORT), scp_from, scp_to) + subprocess.check_call(scp_args) + + chmod_dir = os.path.join(PAYLOAD_PATH, os.path.basename(app_path)) + chmod_args = ('chmod', '755', chmod_dir) + subprocess.check_call(chmod_args) + + file_dict['app'] = os.path.basename(app_path) + + if payload.has_key('done'): + finished.set() -def on_message(message,data): - global name; - if message.has_key('payload'): - payload = message['payload'] - if payload.has_key("opened"): - name = payload["opened"] - opened.set(); - if payload.has_key("dump"): - orign_path = payload["path"] - dumppath = payload["dump"] - os.system(u''.join(("scp -P 2222 root@localhost:", dumppath.replace(" ", "\\\\\ "), u" ./" + OUTPUT + u"/")).encode('utf-8').strip()) - os.system(u''.join(("chmod 655 ", u'./' + OUTPUT + u'/', os.path.basename(dumppath.replace(" ", "\\ ")))).encode('utf-8').strip()) - index = orign_path.find(".app/") - file_dict[os.path.basename(dumppath)] = orign_path[index+5:] - if payload.has_key("app"): - apppath = payload["app"] - os.system(u''.join(("scp -r -P 2222 root@localhost:", apppath.replace(" ", "\\\\\ "), u" ./" + OUTPUT + u"/")).encode('utf-8').strip()) - os.system(u''.join(("chmod 755 ", u'./' + OUTPUT + u'/', os.path.basename(apppath.replace(" ", "\\ ")))).encode('utf-8').strip()) - file_dict["app"] = os.path.basename(apppath) - if payload.has_key("done"): - gen_ipa(os.getcwd()+"/"+OUTPUT) - finished.set(); def compare_applications(a, b): - a_is_running = a.pid != 0 - b_is_running = b.pid != 0 - if a_is_running == b_is_running: - if a.name > b.name: - return 1 - elif a.name < b.name: - return -1 - else: - return 0 - elif a_is_running: + a_is_running = a.pid != 0 + b_is_running = b.pid != 0 + if a_is_running == b_is_running: + if a.name > b.name: + return 1 + elif a.name < b.name: return -1 else: - return 1 + return 0 + elif a_is_running: + return -1 + else: + return 1 + def cmp_to_key(mycmp): - "Convert a cmp= function into a key= function" - class K: - def __init__(self, obj, *args): - self.obj = obj - def __lt__(self, other): - return mycmp(self.obj, other.obj) < 0 - def __gt__(self, other): - return mycmp(self.obj, other.obj) > 0 - def __eq__(self, other): - return mycmp(self.obj, other.obj) == 0 - def __le__(self, other): - return mycmp(self.obj, other.obj) <= 0 - def __ge__(self, other): - return mycmp(self.obj, other.obj) >= 0 - def __ne__(self, other): - return mycmp(self.obj, other.obj) != 0 - return K + 'Convert a cmp= function into a key= function' -def get_applications(): - device = get_usb_iphone() + class K: + def __init__(self, obj, *args): + self.obj = obj - try: - applications = device.enumerate_applications() - except Exception as e: - print "Failed to enumerate applications: %s" % e - exit(1) - return + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 - return applications + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 -def list_applications(): - applications = get_applications() + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 - if len(applications) > 0: - pid_column_width = max(map(lambda app: len("%d" % app.pid), applications)) - name_column_width = max(map(lambda app: len(app.name), applications)) - identifier_column_width = max(map(lambda app: len(app.identifier), applications)) - else: - pid_column_width = 0 - name_column_width = 0 - identifier_column_width = 0 + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 - header_format = "%" + str(pid_column_width) + "s " + "%-" + str(name_column_width) + "s " + "%-" + str(identifier_column_width) + "s" - print header_format % ("PID", "Name", "Identifier") - print "%s %s %s" % (pid_column_width * "-", name_column_width * "-", identifier_column_width * "-") - line_format = "%" + str(pid_column_width) + "s " + "%-" + str(name_column_width) + "s " + "%-" + str(identifier_column_width) + "s" - for app in sorted(applications, key=cmp_to_key(compare_applications)): - if app.pid == 0: - print line_format % ("-", app.name, app.identifier) - else: - print line_format % (app.pid, app.name, app.identifier) + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 -def get_pid_by_bundleid(bundleid): - applications = get_applications() + def __ne__(self, other): + return mycmp(self.obj, other.obj) != 0 - return [app.pid for app in applications if app.identifier == bundleid][0] - -def find_target_app(isbundleid, value): - applications = get_applications() + return K + + +def get_applications(device): + try: + applications = device.enumerate_applications() + except Exception as e: + print 'Failed to enumerate applications: %s' % e + return + + return applications + + +def list_applications(device): + applications = get_applications(device) + + if len(applications) > 0: + pid_column_width = max(map(lambda app: len('{}'.format(app.pid)), applications)) + name_column_width = max(map(lambda app: len(app.name), applications)) + identifier_column_width = max(map(lambda app: len(app.identifier), applications)) + else: + pid_column_width = 0 + name_column_width = 0 + identifier_column_width = 0 + + header_format = '%' + str(pid_column_width) + 's ' + '%-' + str(name_column_width) + 's ' + '%-' + str( + identifier_column_width) + 's' + print header_format % ('PID', 'Name', 'Identifier') + print '%s %s %s' % (pid_column_width * '-', name_column_width * '-', identifier_column_width * '-') + line_format = '%' + str(pid_column_width) + 's ' + '%-' + str(name_column_width) + 's ' + '%-' + str( + identifier_column_width) + 's' + for app in sorted(applications, key=cmp_to_key(compare_applications)): + if app.pid == 0: + print line_format % ('-', app.name, app.identifier) + else: + print line_format % (app.pid, app.name, app.identifier) - if not isbundleid: - return [app for app in applications if app.name == value] - else: - return [app for app in applications if app.identifier == value] def load_js_file(session, filename): - source = '' - with codecs.open(filename,'r','utf-8') as f: - source = source + f.read(); - script = session.create_script(source); - script.on("message",on_message) - script.load() - return script + source = '' + with codecs.open(filename, 'r', 'utf-8') as f: + source = source + f.read() + script = session.create_script(source) + script.on('message', on_message) + script.load() + + return script -def clear_and_quit(session): - if session: - session.detach() - sys.exit(0) def create_dir(path): - path = path.strip() - path = path.rstrip("\\") - if not os.path.exists(path): - os.makedirs(path) - else: - print path + u" is existed!"; + path = path.strip() + path = path.rstrip('\\') + if os.path.exists(path): + print 'Removing {}'.format(path) + shutil.rmtree(path) + os.makedirs(path) -def open_target_app(isbundleid, value): - device = get_usb_iphone(); - name = u'SpringBoard'; - print "open target app......" - session = device.attach(name); - script = load_js_file(session, APP_JS); - if not isbundleid: - script.post({'name': value}) - else: - script.post({'bundleid': value}) - opened.wait(); - session.detach(); - create_dir(os.getcwd()+"/"+OUTPUT) - print 'Waiting for the application to open......' - time.sleep(5); -def start_dump(target): - print "start dump target app......" - device = get_usb_iphone(); - session = device.attach(target); - script = load_js_file(session, DUMP_JS); - script.post("dump"); - finished.wait(); - clear_and_quit(session); +def open_target_app(device, name_or_bundleid): + print 'Start the target app {}'.format(name_or_bundleid) -def dump_by_display_name(display_name): - open_target_app( 0, display_name) - start_dump(display_name); + display_name = '' + bundle_identifier = '' + for application in get_applications(device): + if name_or_bundleid == application.identifier or name_or_bundleid == application.name: + display_name = application.name + bundle_identifier = application.identifier -def dump_by_bundleid(bundleid): - open_target_app( 1, bundleid) - start_dump(get_pid_by_bundleid(bundleid)); + try: + pid = device.spawn([bundle_identifier]) + device.resume(pid) + create_dir(PAYLOAD_PATH) + time.sleep(1) + except Exception as e: + print e + + return (pid, display_name, bundle_identifier) + + +def start_dump(device, pid, display_name, bundle_identifier): + print 'Dumping {} to {}'.format(display_name, TEMP_DIR) + + session = device.attach(pid) + script = load_js_file(session, DUMP_JS) + script.post('dump') + finished.wait() + + generate_ipa(PAYLOAD_PATH, display_name, bundle_identifier) + + if session: + session.detach() def check_args(): - if len(sys.argv) < 2: - usage() - sys.exit(1) + if len(sys.argv) < 2: + usage() + return 1 - try: - opts,args = getopt.getopt(sys.argv[1:],"hlb:",["help"]); - except getopt.GetoptError: - usage() - sys.exit(2) + try: + opts, args = getopt.getopt(sys.argv[1:], 'hl', ['help']) + except getopt.GetoptError: + usage() + return 2 - for opt,value in opts: - if opt in ("-h","--help"): - usage() - if opt in ("-l"): - list_applications() - if opt in ("-b"): - if not find_target_app(1, value): - print "can not find target app '%s'" % value - else: - dump_by_bundleid(value) + for opt, value in opts: + if opt in ('-h', '--help'): + usage() - if len(opts) == 0 and len(sys.argv) == 2: - name = sys.argv[1].decode('utf8'); - if not find_target_app(0, name): - print "can not find target app '%s'" % name - else: - dump_by_display_name(name) - sys.exit(0) + if opt in '-l': + device = get_usb_iphone() + list_applications(device) + + if len(opts) == 0: + name_or_bundleid = ' '.join(sys.argv[1:]) + + device = get_usb_iphone() + print "Device {}".format(device) + (pid, display_name, bundle_identifier) = open_target_app(device, name_or_bundleid) + start_dump(device, pid, display_name, bundle_identifier) + + return 0 + + +if __name__ == '__main__': + exit_code = 0 + try: + exit_code = check_args() + except: + exit_code = 1 + + if os.path.exists(PAYLOAD_PATH): + print 'Deleting ' + PAYLOAD_PATH + shutil.rmtree(PAYLOAD_PATH) + + sys.exit(exit_code) -if __name__ == "__main__": - try: - check_args() - pass - except KeyboardInterrupt: - if session: - session.detach() - sys.exit() - except: - pass \ No newline at end of file From f606152240ef0b284f9367395823c0e0eaa2a7ee Mon Sep 17 00:00:00 2001 From: Cameron Lowell Palmer Date: Mon, 5 Feb 2018 11:41:19 +0100 Subject: [PATCH 2/2] Changed argument parsing to argparse, adding an output naming option. Replaced the use of command-line scp to a paramiko to avoid multiple logins. --- README.md | 2 + dump.py | 165 +++++++++++++++++++++++++++++------------------------ process.sh | 58 +++++++++++++++++++ 3 files changed, 149 insertions(+), 76 deletions(-) create mode 100755 process.sh diff --git a/README.md b/README.md index 3b0b2d5..8c6a8b9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Pull a decrypted IPA from a jailbroken device 2. Run usbmuxd/iproxy SSH forwarding over USB (Default 2222 -> 22) 3. Run ./dump.py `Display name` or `Bundle identifier` +For SSH/SCP make sure you have your public key added to the target device's ~/.ssh/authorized_keys file. + ``` ./dump.py Aftenposten Start the target app Aftenposten diff --git a/dump.py b/dump.py index 0732a8a..33d7cc7 100755 --- a/dump.py +++ b/dump.py @@ -11,9 +11,15 @@ import threading import os import shutil import time -import getopt +import argparse import tempfile import subprocess +import re + +import paramiko +from paramiko import SSHClient +from scp import SCPClient +import traceback reload(sys) sys.setdefaultencoding('utf8') @@ -22,8 +28,10 @@ script_dir = os.path.dirname(os.path.realpath(__file__)) DUMP_JS = os.path.join(script_dir, 'dump.js') -SSH_USER_HOST='root@localhost:' -SSH_PORT=2222 +User = 'root' +Password = 'alpine' +Host = 'localhost' +Port = 2222 TEMP_DIR = tempfile.gettempdir() PAYLOAD_DIR = 'Payload' @@ -33,41 +41,33 @@ file_dict = {} finished = threading.Event() -# show usage -def usage(): - print '-------------------------frida-ios-dump(by AloneMonkey v2.0)----------------------------' - print '\t%-20s\t%s' % ('-h,--help', 'Show help menu.') - print '\t%-20s\t%s' % ('name', 'Decrypt the application with the specified display name or bundle identifier. ps: ./dump.py 微信') - print '\t%-20s\t%s' % ('-l', 'List the installed apps.') - - def get_usb_iphone(): - dManager = frida.get_device_manager() + device_manager = frida.get_device_manager() changed = threading.Event() def on_changed(): changed.set() - dManager.on('changed', on_changed) + device_manager.on('changed', on_changed) device = None while device is None: - devices = [dev for dev in dManager.enumerate_devices() if dev.type == 'tether'] + devices = [dev for dev in device_manager.enumerate_devices() if dev.type == 'tether'] if len(devices) == 0: print 'Waiting for USB device...' changed.wait() else: device = devices[0] - dManager.off('changed', on_changed) + device_manager.off('changed', on_changed) return device -def generate_ipa(path, display_name, bundle_identifier): - ipa_filename = display_name.replace(' ', '\\ ') + '.ipa' +def generate_ipa(path, display_name): + ipa_filename = display_name + '.ipa' - print 'Generating {}'.format(ipa_filename) + print 'Generating "{}"'.format(ipa_filename) try: app_name = file_dict['app'] @@ -89,42 +89,49 @@ def generate_ipa(path, display_name, bundle_identifier): def on_message(message, data): global name - if message.has_key('payload'): + if 'payload' in message: payload = message['payload'] - if payload.has_key('opened'): + if 'opened' in payload: name = payload['opened'] - if payload.has_key('dump'): + if 'dump' in payload: origin_path = payload['path'] dump_path = payload['dump'] - scp_from = SSH_USER_HOST + dump_path.replace(' ', '\ ') + scp_from = dump_path.replace(' ', '\ ') scp_to = PAYLOAD_PATH + u'/' - scp_args = ('scp', '-P {}'.format(SSH_PORT), scp_from, scp_to) - subprocess.check_call(scp_args) + + with SCPClient(ssh.get_transport()) as scp: + scp.get(scp_from, scp_to) chmod_dir = os.path.join(PAYLOAD_PATH, os.path.basename(dump_path)) chmod_args = ('chmod', '655', chmod_dir) - subprocess.check_call(chmod_args) + try: + subprocess.check_call(chmod_args) + except subprocess.CalledProcessError as err: + print err index = origin_path.find('.app/') file_dict[os.path.basename(dump_path)] = origin_path[index + 5:] - if payload.has_key('app'): + if 'app' in payload: app_path = payload['app'] - scp_from = SSH_USER_HOST + app_path.replace(' ', '\ ') + scp_from = app_path.replace(' ', '\ ') scp_to = PAYLOAD_PATH + u'/' - scp_args = ('scp', '-r', '-P {}'.format(SSH_PORT), scp_from, scp_to) - subprocess.check_call(scp_args) + with SCPClient(ssh.get_transport()) as scp: + scp.get(scp_from, scp_to, recursive=True) chmod_dir = os.path.join(PAYLOAD_PATH, os.path.basename(app_path)) chmod_args = ('chmod', '755', chmod_dir) - subprocess.check_call(chmod_args) + try: + subprocess.check_call(chmod_args) + except subprocess.CalledProcessError as err: + print err file_dict['app'] = os.path.basename(app_path) - if payload.has_key('done'): + if 'done' in payload: finished.set() @@ -145,10 +152,10 @@ def compare_applications(a, b): def cmp_to_key(mycmp): - 'Convert a cmp= function into a key= function' + """Convert a cmp= function into a key= function""" class K: - def __init__(self, obj, *args): + def __init__(self, obj): self.obj = obj def __lt__(self, other): @@ -200,11 +207,11 @@ def list_applications(device): print '%s %s %s' % (pid_column_width * '-', name_column_width * '-', identifier_column_width * '-') line_format = '%' + str(pid_column_width) + 's ' + '%-' + str(name_column_width) + 's ' + '%-' + str( identifier_column_width) + 's' - for app in sorted(applications, key=cmp_to_key(compare_applications)): - if app.pid == 0: - print line_format % ('-', app.name, app.identifier) + for application in sorted(applications, key=cmp_to_key(compare_applications)): + if application.pid == 0: + print line_format % ('-', application.name, application.identifier) else: - print line_format % (app.pid, app.name, app.identifier) + print line_format % (application.pid, application.name, application.identifier) def load_js_file(session, filename): @@ -224,12 +231,16 @@ def create_dir(path): if os.path.exists(path): print 'Removing {}'.format(path) shutil.rmtree(path) - os.makedirs(path) + try: + os.makedirs(path) + except os.error as err: + print err def open_target_app(device, name_or_bundleid): print 'Start the target app {}'.format(name_or_bundleid) + pid = -1 display_name = '' bundle_identifier = '' for application in get_applications(device): @@ -240,15 +251,14 @@ def open_target_app(device, name_or_bundleid): try: pid = device.spawn([bundle_identifier]) device.resume(pid) - create_dir(PAYLOAD_PATH) time.sleep(1) except Exception as e: print e - return (pid, display_name, bundle_identifier) + return pid, display_name, bundle_identifier -def start_dump(device, pid, display_name, bundle_identifier): +def start_dump(device, pid, ipa_name): print 'Dumping {} to {}'.format(display_name, TEMP_DIR) session = device.attach(pid) @@ -256,51 +266,54 @@ def start_dump(device, pid, display_name, bundle_identifier): script.post('dump') finished.wait() - generate_ipa(PAYLOAD_PATH, display_name, bundle_identifier) + generate_ipa(PAYLOAD_PATH, ipa_name) if session: session.detach() -def check_args(): - if len(sys.argv) < 2: - usage() - return 1 - - try: - opts, args = getopt.getopt(sys.argv[1:], 'hl', ['help']) - except getopt.GetoptError: - usage() - return 2 - - for opt, value in opts: - if opt in ('-h', '--help'): - usage() - - if opt in '-l': - device = get_usb_iphone() - list_applications(device) - - if len(opts) == 0: - name_or_bundleid = ' '.join(sys.argv[1:]) - - device = get_usb_iphone() - print "Device {}".format(device) - (pid, display_name, bundle_identifier) = open_target_app(device, name_or_bundleid) - start_dump(device, pid, display_name, bundle_identifier) - - return 0 - if __name__ == '__main__': + parser = argparse.ArgumentParser(description='frida-ios-dump (by AloneMonkey v2.0)') + parser.add_argument('-l', '--list', dest='list_applications', action='store_true', help='List the installed apps') + parser.add_argument('-o', '--output', dest='output_ipa', help='Specify name of the decrypted IPA') + parser.add_argument('target', nargs='?', help='Bundle identifier or display name of the target app') + args = parser.parse_args() + exit_code = 0 - try: - exit_code = check_args() - except: - exit_code = 1 + ssh = None + device = get_usb_iphone() + if args.list_applications: + list_applications(device) + else: + print "Device {}".format(device) + name_or_bundleid = args.target + output_ipa = args.output_ipa + + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(Host, port=Port, username=User, password=Password) + + create_dir(PAYLOAD_PATH) + (pid, display_name, bundle_identifier) = open_target_app(device, name_or_bundleid) + if output_ipa is None: + output_ipa = display_name + output_ipa = re.sub('\.ipa$', '', output_ipa) + start_dump(device, pid, output_ipa) + + except paramiko.AuthenticationException as e: + print e + exit_code = 1 + except Exception as e: + print('*** Caught exception: %s: %s' % (e.__class__, e)) + traceback.print_exc() + exit_code = 1 + + if ssh: + ssh.close() if os.path.exists(PAYLOAD_PATH): print 'Deleting ' + PAYLOAD_PATH shutil.rmtree(PAYLOAD_PATH) sys.exit(exit_code) - diff --git a/process.sh b/process.sh new file mode 100755 index 0000000..6ab3927 --- /dev/null +++ b/process.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +ORIGINAL_IPA_FOLDER="${HOME}/Music/iTunes/iTunes Media/Mobile Applications" +DECRYPTED_IPA_FOLDER="${HOME}/Hacking/Decrypted Apps" + +IOS_DEPLOY="/usr/local/bin/ios-deploy" +PLISTBUDDY="/usr/libexec/PlistBuddy" +FRIDA_DUMP_PY="./dump.py" +# CFBundleIdentifier in Info.plist to uninstall + +ORIGINAL_IPAS=() +while IFS= read -r -d $'\0'; do + ORIGINAL_IPA=$( echo "${REPLY}" ) + ORIGINAL_IPAS+=( "${ORIGINAL_IPA}" ) +done < <(find "${ORIGINAL_IPA_FOLDER}" -type f -name '*.ipa' -print0) + +echo "Decrypting ${#ORIGINAL_IPAS[@]} apps" +APP_COUNTER=0 +for ORIGINAL_IPA in "${ORIGINAL_IPAS[@]}"; do + APP_COUNTER=$((APP_COUNTER + 1)) + echo "#### Decryting app number: ${APP_COUNTER}" + APP_BUNDLE_ID="" + ESCAPED_IPA_PATH=$( echo "${ORIGINAL_IPA}" | sed 's/ /\\ /g' | sed 's/&/\\&/g' ) + + IPA_NAME=$( basename "${ESCAPED_IPA_PATH}" ) + + # Make a temp file for the iTunesMetadata.plist + ITUNESMETADATA_TMP_FILE=$( mktemp "/tmp/iTunesMetadata.plist.XXXXXX" ) || exit 1 + + # Unzip iTunesMetadata.plist to temp file + UNZIP_CMD="unzip -p ${ESCAPED_IPA_PATH} iTunesMetadata.plist > ${ITUNESMETADATA_TMP_FILE}" + echo "${UNZIP_CMD}" + eval "${UNZIP_CMD}" || exit 1 + + # Extract softwareVersionBundleId from plist file + APP_BUNDLE_ID=$( "${PLISTBUDDY}" -c 'Print softwareVersionBundleId' "${ITUNESMETADATA_TMP_FILE}" ) + echo "App bundle identifier: ${APP_BUNDLE_ID}" + rm "${ITUNESMETADATA_TMP_FILE}" + + # Install the app + IOS_DEPLOY_INSTALL="${IOS_DEPLOY} -W --bundle ${ESCAPED_IPA_PATH}" + echo "${IOS_DEPLOY_INSTALL}" + eval "${IOS_DEPLOY_INSTALL}" + + if [ $? -eq 0 ]; then + sleep 1 + + echo "Dumping ${APP_BUNDLE_ID}" + FRIDA_DUMP_CMD="${FRIDA_DUMP_PY} --output ${IPA_NAME} ${APP_BUNDLE_ID}" + echo "${FRIDA_DUMP_CMD}" + eval "${FRIDA_DUMP_CMD}" + fi + + # Uninstall the app + IOS_DEPLOY_UNINSTALL="${IOS_DEPLOY} -W --uninstall_only --bundle_id ${APP_BUNDLE_ID}" + echo "${IOS_DEPLOY_UNINSTALL}" + eval "${IOS_DEPLOY_UNINSTALL}" +done \ No newline at end of file