diff --git a/README.md b/README.md index 3c1e06a..adf5cd3 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ Pull a decrypted IPA from a jailbroken device 1. Install [frida](http://www.frida.re/) on device 2. `sudo pip install -r requirements.txt --upgrade` (Python 2.7) - 3. Run usbmuxd/iproxy SSH forwarding over USB (Default 2222 -> 22). e.g. `iproxy 2222 22` - 4. Run ./dump.py `Display name` or `Bundle identifier` + 3. `sudo pip3 install -r requirements.txt --upgrade` (Python 3.x) + 4. Run usbmuxd/iproxy SSH forwarding over USB (Default 2222 -> 22). e.g. `iproxy 2222 22` + 5. 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. diff --git a/dump.js b/dump.js index 9359f55..6ded44a 100644 --- a/dump.js +++ b/dump.js @@ -129,14 +129,15 @@ function getExportFunction(type, name, ret, args) { } } -NSSearchPathForDirectoriesInDomains = getExportFunction("f", "NSSearchPathForDirectoriesInDomains", "pointer", ["int", "int", "int"]); -wrapper_open = getExportFunction("f", "open", "int", ["pointer", "int", "int"]); -read = getExportFunction("f", "read", "int", ["int", "pointer", "int"]); -write = getExportFunction("f", "write", "int", ["int", "pointer", "int"]); -lseek = getExportFunction("f", "lseek", "int64", ["int", "int64", "int"]); -close = getExportFunction("f", "close", "int", ["int"]); -remove = getExportFunction("f", "remove", "int", ["pointer"]); -access = getExportFunction("f", "access", "int", ["pointer", "int"]); +var NSSearchPathForDirectoriesInDomains = getExportFunction("f", "NSSearchPathForDirectoriesInDomains", "pointer", ["int", "int", "int"]); +var wrapper_open = getExportFunction("f", "open", "int", ["pointer", "int", "int"]); +var read = getExportFunction("f", "read", "int", ["int", "pointer", "int"]); +var write = getExportFunction("f", "write", "int", ["int", "pointer", "int"]); +var lseek = getExportFunction("f", "lseek", "int64", ["int", "int64", "int"]); +var close = getExportFunction("f", "close", "int", ["int"]); +var remove = getExportFunction("f", "remove", "int", ["pointer"]); +var access = getExportFunction("f", "access", "int", ["pointer", "int"]); +var dlopen = getExportFunction("f", "dlopen", "pointer", ["pointer", "int"]); function getDocumentDir() { var NSDocumentDirectory = 9; @@ -154,13 +155,11 @@ function open(pathname, flags, mode) { var modules = null; function getAllAppModules() { - if (modules == null) { - modules = new Array(); - var tmpmods = Process.enumerateModulesSync(); - for (var i = 0; i < tmpmods.length; i++) { - if (tmpmods[i].path.indexOf(".app") != -1) { - modules.push(tmpmods[i]); - } + modules = new Array(); + var tmpmods = Process.enumerateModulesSync(); + for (var i = 0; i < tmpmods.length; i++) { + if (tmpmods[i].path.indexOf(".app") != -1) { + modules.push(tmpmods[i]); } } return modules; @@ -314,15 +313,75 @@ function dumpModule(name) { return newmodpath } +function loadAllDynamicLibrary(app_path) { + var defaultManager = ObjC.classes.NSFileManager.defaultManager(); + var errorPtr = Memory.alloc(Process.pointerSize); + Memory.writePointer(errorPtr, NULL); + var filenames = defaultManager.contentsOfDirectoryAtPath_error_(app_path, errorPtr); + for (var i = 0, l = filenames.count(); i < l; i++) { + var file_name = filenames.objectAtIndex_(i); + var file_path = app_path.stringByAppendingPathComponent_(file_name); + if (file_name.hasSuffix_(".framework")) { + var bundle = ObjC.classes.NSBundle.bundleWithPath_(file_path); + if (bundle.isLoaded()) { + console.log("[frida-ios-dump]: " + file_name + " has been loaded. "); + } else { + if (bundle.load()) { + console.log("[frida-ios-dump]: Load " + file_name + " success. "); + } else { + console.log("[frida-ios-dump]: Load " + file_name + " failed. "); + } + } + } else if (file_name.hasSuffix_(".bundle") || + file_name.hasSuffix_(".momd") || + file_name.hasSuffix_(".strings") || + file_name.hasSuffix_(".appex") || + file_name.hasSuffix_(".app") || + file_name.hasSuffix_(".lproj") || + file_name.hasSuffix_(".storyboardc")) { + continue; + } else { + var isDirPtr = Memory.alloc(Process.pointerSize); + Memory.writePointer(isDirPtr,NULL); + defaultManager.fileExistsAtPath_isDirectory_(file_path, isDirPtr); + if (Memory.readPointer(isDirPtr) == 1) { + loadAllDynamicLibrary(file_path); + } else { + if (file_name.hasSuffix_(".dylib")) { + var is_loaded = 0; + for (var j = 0; j < modules.length; j++) { + if (modules[j].path.indexOf(file_name) != -1) { + is_loaded = 1; + console.log("[frida-ios-dump]: " + file_name + " has been dlopen."); + break; + } + } + + if (!is_loaded) { + if (dlopen(allocStr(file_path.UTF8String()), 9)) { + console.log("[frida-ios-dump]: dlopen " + file_name + " success. "); + } else { + console.log("[frida-ios-dump]: dlopen " + file_name + " failed. "); + } + } + } + } + } + } +} + function handleMessage(message) { - //start dump + modules = getAllAppModules(); + var app_path = ObjC.classes.NSBundle.mainBundle().bundlePath(); + loadAllDynamicLibrary(app_path); + // start dump modules = getAllAppModules(); for (var i = 0; i < modules.length; i++) { console.log("start dump " + modules[i].path); - result = dumpModule(modules[i].path); + var result = dumpModule(modules[i].path); send({ dump: result, path: modules[i].path}); } - send({app: ObjC.classes.NSBundle.mainBundle().bundlePath().toString()}); + send({app: app_path.toString()}); send({done: "ok"}); recv(handleMessage); } diff --git a/dump.py b/dump.py index 0d05f2b..c4656bc 100755 --- a/dump.py +++ b/dump.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Author : AloneMonkey @@ -22,9 +22,6 @@ from tqdm import tqdm import traceback -reload(sys) -sys.setdefaultencoding('utf8') - script_dir = os.path.dirname(os.path.realpath(__file__)) DUMP_JS = os.path.join(script_dir, 'dump.js') @@ -43,6 +40,9 @@ def get_usb_iphone(): + Type = 'usb' + if int(frida.__version__.split('.')[0]) < 12: + Type = 'tether' device_manager = frida.get_device_manager() changed = threading.Event() @@ -53,9 +53,9 @@ def on_changed(): device = None while device is None: - devices = [dev for dev in device_manager.enumerate_devices() if dev.type == 'tether'] + devices = [dev for dev in device_manager.enumerate_devices() if dev.type == Type] if len(devices) == 0: - print 'Waiting for USB device...' + print('Waiting for USB device...') changed.wait() else: device = devices[0] @@ -68,7 +68,7 @@ def on_changed(): 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'] @@ -82,9 +82,8 @@ def generate_ipa(path, display_name): 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 + print(e) finished.set() def on_message(message, data): @@ -92,7 +91,7 @@ def on_message(message, data): last_sent = [0] def progress(filename, size, sent): - t.desc = os.path.basename(filename) + t.desc = os.path.basename(filename).decode("utf-8") t.total = size t.update(sent - last_sent[0]) last_sent[0] = 0 if size == sent else sent @@ -104,7 +103,7 @@ def progress(filename, size, sent): dump_path = payload['dump'] scp_from = dump_path - scp_to = PAYLOAD_PATH + u'/' + scp_to = PAYLOAD_PATH + '/' with SCPClient(ssh.get_transport(), progress = progress, socket_timeout = 60) as scp: scp.get(scp_from, scp_to) @@ -114,7 +113,7 @@ def progress(filename, size, sent): try: subprocess.check_call(chmod_args) except subprocess.CalledProcessError as err: - print err + print(err) index = origin_path.find('.app/') file_dict[os.path.basename(dump_path)] = origin_path[index + 5:] @@ -123,7 +122,7 @@ def progress(filename, size, sent): app_path = payload['app'] scp_from = app_path - scp_to = PAYLOAD_PATH + u'/' + scp_to = PAYLOAD_PATH + '/' with SCPClient(ssh.get_transport(), progress = progress, socket_timeout = 60) as scp: scp.get(scp_from, scp_to, recursive=True) @@ -132,7 +131,7 @@ def progress(filename, size, sent): try: subprocess.check_call(chmod_args) except subprocess.CalledProcessError as err: - print err + print(err) file_dict['app'] = os.path.basename(app_path) @@ -188,8 +187,7 @@ def get_applications(device): try: applications = device.enumerate_applications() except Exception as e: - print 'Failed to enumerate applications: %s' % e - return + sys.exit('Failed to enumerate applications: %s' % e) return applications @@ -208,15 +206,15 @@ def list_applications(device): 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 * '-') + 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 application in sorted(applications, key=cmp_to_key(compare_applications)): if application.pid == 0: - print line_format % ('-', application.name, application.identifier) + print(line_format % ('-', application.name, application.identifier)) else: - print line_format % (application.pid, application.name, application.identifier) + print(line_format % (application.pid, application.name, application.identifier)) def load_js_file(session, filename): @@ -238,11 +236,11 @@ def create_dir(path): try: os.makedirs(path) except os.error as err: - print err + print(err) def open_target_app(device, name_or_bundleid): - print 'Start the target app {}'.format(name_or_bundleid) + print('Start the target app {}'.format(name_or_bundleid)) pid = '' session = None @@ -262,13 +260,13 @@ def open_target_app(device, name_or_bundleid): else: session = device.attach(pid) except Exception as e: - print e + print(e) return session, display_name, bundle_identifier def start_dump(session, ipa_name): - print 'Dumping {} to {}'.format(display_name, TEMP_DIR) + print('Dumping {} to {}'.format(display_name, TEMP_DIR)) script = load_js_file(session, DUMP_JS) script.post('dump') @@ -289,11 +287,14 @@ def start_dump(session, ipa_name): exit_code = 0 ssh = None - device = get_usb_iphone() - if len(sys.argv[1:]) == 0: + if not len(sys.argv[1:]): parser.print_help() - elif args.list_applications: + sys.exit(exit_code) + + device = get_usb_iphone() + + if args.list_applications: list_applications(device) else: name_or_bundleid = args.target @@ -312,10 +313,10 @@ def start_dump(session, ipa_name): if session: start_dump(session, output_ipa) except paramiko.ssh_exception.NoValidConnectionsError as e: - print e + print(e) exit_code = 1 except paramiko.AuthenticationException as e: - print e + print(e) exit_code = 1 except Exception as e: print('*** Caught exception: %s: %s' % (e.__class__, e)) @@ -328,4 +329,4 @@ def start_dump(session, ipa_name): if os.path.exists(PAYLOAD_PATH): shutil.rmtree(PAYLOAD_PATH) - sys.exit(exit_code) \ No newline at end of file + sys.exit(exit_code) diff --git a/requirements.txt b/requirements.txt index 7f40df1..a3af4eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ cffi colorama cryptography enum34 -frida +frida-tools idna ipaddress paramiko