├── .gitignore ├── LICENSE ├── README.md ├── dump.js ├── dump.py ├── process.sh └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.ipa 4 | 5 | frida/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alone_Monkey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # frida-ios-dump 2 | Pull a decrypted IPA from a jailbroken device 3 | 4 | 5 | ## Usage 6 | 7 | 1. Install [frida](http://www.frida.re/) on device 8 | 2. `sudo pip install -r requirements.txt --upgrade` 9 | 3. Run usbmuxd/iproxy SSH forwarding over USB (Default 2222 -> 22). e.g. `iproxy 2222 22` 10 | 4. Run ./dump.py `Display name` or `Bundle identifier` 11 | 12 | For SSH/SCP make sure you have your public key added to the target device's ~/.ssh/authorized_keys file. 13 | 14 | ``` 15 | ./dump.py Aftenposten 16 | Start the target app Aftenposten 17 | Dumping Aftenposten to /var/folders/wn/9v1hs8ds6nv_xj7g95zxyl140000gn/T 18 | start dump /var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/AftenpostenApp 19 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/AFNetworking.framework/AFNetworking 20 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/ATInternet_iOS_ObjC_SDK.framework/ATInternet_iOS_ObjC_SDK 21 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/SPTEventCollector.framework/SPTEventCollector 22 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/SPiDSDK.framework/SPiDSDK 23 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCore.dylib 24 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCoreData.dylib 25 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCoreGraphics.dylib 26 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCoreImage.dylib 27 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftCoreLocation.dylib 28 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftDarwin.dylib 29 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftDispatch.dylib 30 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftFoundation.dylib 31 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftObjectiveC.dylib 32 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftQuartzCore.dylib 33 | start dump /private/var/containers/Bundle/Application/66423A80-0AFE-471C-BC9B-B571107D3C27/AftenpostenApp.app/Frameworks/libswiftUIKit.dylib 34 | Generating Aftenposten.ipa 35 | 36 | Done. 37 | ``` 38 | 39 | Congratulations!!! You've got a decrypted IPA file. 40 | 41 | Drag to [MonkeyDev](https://github.com/AloneMonkey/MonkeyDev), Happy hacking! 42 | 43 | ## Support 44 | 45 | Python 2.x and 3.x 46 | 47 | 48 | ### issues 49 | 50 | If the following error occurs: 51 | 52 | * causes device to reboot 53 | * lost connection 54 | * unexpected error while probing dyld of target process 55 | 56 | please open the application before dumping. 57 | 58 | 59 | -------------------------------------------------------------------------------- /dump.js: -------------------------------------------------------------------------------- 1 | Module.ensureInitialized('Foundation'); 2 | 3 | var O_RDONLY = 0; 4 | var O_WRONLY = 1; 5 | var O_RDWR = 2; 6 | var O_CREAT = 512; 7 | 8 | var SEEK_SET = 0; 9 | var SEEK_CUR = 1; 10 | var SEEK_END = 2; 11 | 12 | function allocStr(str) { 13 | return Memory.allocUtf8String(str); 14 | } 15 | 16 | function putStr(addr, str) { 17 | if (typeof addr == "number") { 18 | addr = ptr(addr); 19 | } 20 | return Memory.writeUtf8String(addr, str); 21 | } 22 | 23 | function getByteArr(addr, l) { 24 | if (typeof addr == "number") { 25 | addr = ptr(addr); 26 | } 27 | return Memory.readByteArray(addr, l); 28 | } 29 | 30 | function getU8(addr) { 31 | if (typeof addr == "number") { 32 | addr = ptr(addr); 33 | } 34 | return Memory.readU8(addr); 35 | } 36 | 37 | function putU8(addr, n) { 38 | if (typeof addr == "number") { 39 | addr = ptr(addr); 40 | } 41 | return Memory.writeU8(addr, n); 42 | } 43 | 44 | function getU16(addr) { 45 | if (typeof addr == "number") { 46 | addr = ptr(addr); 47 | } 48 | return Memory.readU16(addr); 49 | } 50 | 51 | function putU16(addr, n) { 52 | if (typeof addr == "number") { 53 | addr = ptr(addr); 54 | } 55 | return Memory.writeU16(addr, n); 56 | } 57 | 58 | function getU32(addr) { 59 | if (typeof addr == "number") { 60 | addr = ptr(addr); 61 | } 62 | return Memory.readU32(addr); 63 | } 64 | 65 | function putU32(addr, n) { 66 | if (typeof addr == "number") { 67 | addr = ptr(addr); 68 | } 69 | return Memory.writeU32(addr, n); 70 | } 71 | 72 | function getU64(addr) { 73 | if (typeof addr == "number") { 74 | addr = ptr(addr); 75 | } 76 | return Memory.readU64(addr); 77 | } 78 | 79 | function putU64(addr, n) { 80 | if (typeof addr == "number") { 81 | addr = ptr(addr); 82 | } 83 | return Memory.writeU64(addr, n); 84 | } 85 | 86 | function getPt(addr) { 87 | if (typeof addr == "number") { 88 | addr = ptr(addr); 89 | } 90 | return Memory.readPointer(addr); 91 | } 92 | 93 | function putPt(addr, n) { 94 | if (typeof addr == "number") { 95 | addr = ptr(addr); 96 | } 97 | if (typeof n == "number") { 98 | n = ptr(n); 99 | } 100 | return Memory.writePointer(addr, n); 101 | } 102 | 103 | function malloc(size) { 104 | return Memory.alloc(size); 105 | } 106 | 107 | function getExportFunction(type, name, ret, args) { 108 | var nptr; 109 | nptr = Module.findExportByName(null, name); 110 | if (nptr === null) { 111 | console.log("cannot find " + name); 112 | return null; 113 | } else { 114 | if (type === "f") { 115 | var funclet = new NativeFunction(nptr, ret, args); 116 | if (typeof funclet === "undefined") { 117 | console.log("parse error " + name); 118 | return null; 119 | } 120 | return funclet; 121 | } else if (type === "d") { 122 | var datalet = Memory.readPointer(nptr); 123 | if (typeof datalet === "undefined") { 124 | console.log("parse error " + name); 125 | return null; 126 | } 127 | return datalet; 128 | } 129 | } 130 | } 131 | 132 | var NSSearchPathForDirectoriesInDomains = getExportFunction("f", "NSSearchPathForDirectoriesInDomains", "pointer", ["int", "int", "int"]); 133 | var wrapper_open = getExportFunction("f", "open", "int", ["pointer", "int", "int"]); 134 | var read = getExportFunction("f", "read", "int", ["int", "pointer", "int"]); 135 | var write = getExportFunction("f", "write", "int", ["int", "pointer", "int"]); 136 | var lseek = getExportFunction("f", "lseek", "int64", ["int", "int64", "int"]); 137 | var close = getExportFunction("f", "close", "int", ["int"]); 138 | var remove = getExportFunction("f", "remove", "int", ["pointer"]); 139 | var access = getExportFunction("f", "access", "int", ["pointer", "int"]); 140 | var dlopen = getExportFunction("f", "dlopen", "pointer", ["pointer", "int"]); 141 | 142 | function getDocumentDir() { 143 | var NSDocumentDirectory = 9; 144 | var NSUserDomainMask = 1; 145 | var npdirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, 1); 146 | return ObjC.Object(npdirs).objectAtIndex_(0).toString(); 147 | } 148 | 149 | function open(pathname, flags, mode) { 150 | if (typeof pathname == "string") { 151 | pathname = allocStr(pathname); 152 | } 153 | return wrapper_open(pathname, flags, mode); 154 | } 155 | 156 | var modules = null; 157 | function getAllAppModules() { 158 | modules = new Array(); 159 | var tmpmods = Process.enumerateModulesSync(); 160 | for (var i = 0; i < tmpmods.length; i++) { 161 | if (tmpmods[i].path.indexOf(".app") != -1) { 162 | modules.push(tmpmods[i]); 163 | } 164 | } 165 | return modules; 166 | } 167 | 168 | var FAT_MAGIC = 0xcafebabe; 169 | var FAT_CIGAM = 0xbebafeca; 170 | var MH_MAGIC = 0xfeedface; 171 | var MH_CIGAM = 0xcefaedfe; 172 | var MH_MAGIC_64 = 0xfeedfacf; 173 | var MH_CIGAM_64 = 0xcffaedfe; 174 | var LC_SEGMENT = 0x1; 175 | var LC_SEGMENT_64 = 0x19; 176 | var LC_ENCRYPTION_INFO = 0x21; 177 | var LC_ENCRYPTION_INFO_64 = 0x2C; 178 | 179 | function pad(str, n) { 180 | return Array(n-str.length+1).join("0")+str; 181 | } 182 | 183 | function swap32(value) { 184 | value = pad(value.toString(16),8) 185 | var result = ""; 186 | for(var i = 0; i < value.length; i=i+2){ 187 | result += value.charAt(value.length - i - 2); 188 | result += value.charAt(value.length - i - 1); 189 | } 190 | return parseInt(result,16) 191 | } 192 | 193 | function dumpModule(name) { 194 | if (modules == null) { 195 | modules = getAllAppModules(); 196 | } 197 | 198 | var targetmod = null; 199 | for (var i = 0; i < modules.length; i++) { 200 | if (modules[i].path.indexOf(name) != -1) { 201 | targetmod = modules[i]; 202 | break; 203 | } 204 | } 205 | if (targetmod == null) { 206 | console.log("Cannot find module"); 207 | return; 208 | } 209 | var modbase = modules[i].base; 210 | var modsize = modules[i].size; 211 | var newmodname = modules[i].name; 212 | var newmodpath = getDocumentDir() + "/" + newmodname + ".fid"; 213 | var oldmodpath = modules[i].path; 214 | 215 | 216 | if(!access(allocStr(newmodpath),0)){ 217 | remove(allocStr(newmodpath)); 218 | } 219 | 220 | var fmodule = open(newmodpath, O_CREAT | O_RDWR, 0); 221 | var foldmodule = open(oldmodpath, O_RDONLY, 0); 222 | 223 | if (fmodule == -1 || foldmodule == -1) { 224 | console.log("Cannot open file" + newmodpath); 225 | return; 226 | } 227 | 228 | var is64bit = false; 229 | var size_of_mach_header = 0; 230 | var magic = getU32(modbase); 231 | var cur_cpu_type = getU32(modbase.add(4)); 232 | var cur_cpu_subtype = getU32(modbase.add(8)); 233 | if (magic == MH_MAGIC || magic == MH_CIGAM) { 234 | is64bit = false; 235 | size_of_mach_header = 28; 236 | }else if (magic == MH_MAGIC_64 || magic == MH_CIGAM_64) { 237 | is64bit = true; 238 | size_of_mach_header = 32; 239 | } 240 | 241 | var BUFSIZE = 4096; 242 | var buffer = malloc(BUFSIZE); 243 | 244 | read(foldmodule, buffer, BUFSIZE); 245 | 246 | var fileoffset = 0; 247 | var filesize = 0; 248 | magic = getU32(buffer); 249 | if(magic == FAT_CIGAM || magic == FAT_MAGIC){ 250 | var off = 4; 251 | var archs = swap32(getU32(buffer.add(off))); 252 | for (var i = 0; i < archs; i++) { 253 | var cputype = swap32(getU32(buffer.add(off + 4))); 254 | var cpusubtype = swap32(getU32(buffer.add(off + 8))); 255 | if(cur_cpu_type == cputype && cur_cpu_subtype == cpusubtype){ 256 | fileoffset = swap32(getU32(buffer.add(off + 12))); 257 | filesize = swap32(getU32(buffer.add(off + 16))); 258 | break; 259 | } 260 | off += 20; 261 | } 262 | 263 | if(fileoffset == 0 || filesize == 0) 264 | return; 265 | 266 | lseek(fmodule, 0, SEEK_SET); 267 | lseek(foldmodule, fileoffset, SEEK_SET); 268 | for(var i = 0; i < parseInt(filesize / BUFSIZE); i++) { 269 | read(foldmodule, buffer, BUFSIZE); 270 | write(fmodule, buffer, BUFSIZE); 271 | } 272 | if(filesize % BUFSIZE){ 273 | read(foldmodule, buffer, filesize % BUFSIZE); 274 | write(fmodule, buffer, filesize % BUFSIZE); 275 | } 276 | }else{ 277 | var readLen = 0; 278 | lseek(foldmodule, 0, SEEK_SET); 279 | lseek(fmodule, 0, SEEK_SET); 280 | while(readLen = read(foldmodule, buffer, BUFSIZE)) { 281 | write(fmodule, buffer, readLen); 282 | } 283 | } 284 | 285 | var ncmds = getU32(modbase.add(16)); 286 | var off = size_of_mach_header; 287 | var offset_cryptid = -1; 288 | var crypt_off = 0; 289 | var crypt_size = 0; 290 | var segments = []; 291 | for (var i = 0; i < ncmds; i++) { 292 | var cmd = getU32(modbase.add(off)); 293 | var cmdsize = getU32(modbase.add(off + 4)); 294 | if (cmd == LC_ENCRYPTION_INFO || cmd == LC_ENCRYPTION_INFO_64) { 295 | offset_cryptid = off + 16; 296 | crypt_off = getU32(modbase.add(off + 8)); 297 | crypt_size = getU32(modbase.add(off + 12)); 298 | } 299 | off += cmdsize; 300 | } 301 | 302 | if (offset_cryptid != -1) { 303 | var tpbuf = malloc(8); 304 | putU64(tpbuf, 0); 305 | lseek(fmodule, offset_cryptid, SEEK_SET); 306 | write(fmodule, tpbuf, 4); 307 | lseek(fmodule, crypt_off, SEEK_SET); 308 | write(fmodule, modbase.add(crypt_off), crypt_size); 309 | } 310 | 311 | close(fmodule); 312 | close(foldmodule); 313 | return newmodpath 314 | } 315 | 316 | function loadAllDynamicLibrary(app_path) { 317 | var defaultManager = ObjC.classes.NSFileManager.defaultManager(); 318 | var errorPtr = Memory.alloc(Process.pointerSize); 319 | Memory.writePointer(errorPtr, NULL); 320 | var filenames = defaultManager.contentsOfDirectoryAtPath_error_(app_path, errorPtr); 321 | for (var i = 0, l = filenames.count(); i < l; i++) { 322 | var file_name = filenames.objectAtIndex_(i); 323 | var file_path = app_path.stringByAppendingPathComponent_(file_name); 324 | if (file_name.hasSuffix_(".framework")) { 325 | var bundle = ObjC.classes.NSBundle.bundleWithPath_(file_path); 326 | if (bundle.isLoaded()) { 327 | console.log("[frida-ios-dump]: " + file_name + " has been loaded. "); 328 | } else { 329 | if (bundle.load()) { 330 | console.log("[frida-ios-dump]: Load " + file_name + " success. "); 331 | } else { 332 | console.log("[frida-ios-dump]: Load " + file_name + " failed. "); 333 | } 334 | } 335 | } else if (file_name.hasSuffix_(".bundle") || 336 | file_name.hasSuffix_(".momd") || 337 | file_name.hasSuffix_(".strings") || 338 | file_name.hasSuffix_(".appex") || 339 | file_name.hasSuffix_(".app") || 340 | file_name.hasSuffix_(".lproj") || 341 | file_name.hasSuffix_(".storyboardc")) { 342 | continue; 343 | } else { 344 | var isDirPtr = Memory.alloc(Process.pointerSize); 345 | Memory.writePointer(isDirPtr,NULL); 346 | defaultManager.fileExistsAtPath_isDirectory_(file_path, isDirPtr); 347 | if (Memory.readPointer(isDirPtr) == 1) { 348 | loadAllDynamicLibrary(file_path); 349 | } else { 350 | if (file_name.hasSuffix_(".dylib")) { 351 | var is_loaded = 0; 352 | for (var j = 0; j < modules.length; j++) { 353 | if (modules[j].path.indexOf(file_name) != -1) { 354 | is_loaded = 1; 355 | console.log("[frida-ios-dump]: " + file_name + " has been dlopen."); 356 | break; 357 | } 358 | } 359 | 360 | if (!is_loaded) { 361 | if (dlopen(allocStr(file_path.UTF8String()), 9)) { 362 | console.log("[frida-ios-dump]: dlopen " + file_name + " success. "); 363 | } else { 364 | console.log("[frida-ios-dump]: dlopen " + file_name + " failed. "); 365 | } 366 | } 367 | } 368 | } 369 | } 370 | } 371 | } 372 | 373 | function handleMessage(message) { 374 | modules = getAllAppModules(); 375 | var app_path = ObjC.classes.NSBundle.mainBundle().bundlePath(); 376 | loadAllDynamicLibrary(app_path); 377 | // start dump 378 | modules = getAllAppModules(); 379 | for (var i = 0; i < modules.length; i++) { 380 | console.log("start dump " + modules[i].path); 381 | var result = dumpModule(modules[i].path); 382 | send({ dump: result, path: modules[i].path}); 383 | } 384 | send({app: app_path.toString()}); 385 | send({done: "ok"}); 386 | recv(handleMessage); 387 | } 388 | 389 | recv(handleMessage); -------------------------------------------------------------------------------- /dump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Author : AloneMonkey 5 | # blog: www.alonemonkey.com 6 | 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | import sys 10 | import codecs 11 | import frida 12 | import threading 13 | import os 14 | import shutil 15 | import time 16 | import argparse 17 | import tempfile 18 | import subprocess 19 | import re 20 | import paramiko 21 | from paramiko import SSHClient 22 | from scp import SCPClient 23 | from tqdm import tqdm 24 | import traceback 25 | 26 | IS_PY2 = sys.version_info[0] < 3 27 | if IS_PY2: 28 | reload(sys) 29 | sys.setdefaultencoding('utf8') 30 | 31 | script_dir = os.path.dirname(os.path.realpath(__file__)) 32 | 33 | DUMP_JS = os.path.join(script_dir, 'dump.js') 34 | 35 | User = 'root' 36 | Password = 'alpine' 37 | Host = 'localhost' 38 | Port = 2222 39 | KeyFileName = None 40 | 41 | TEMP_DIR = tempfile.gettempdir() 42 | PAYLOAD_DIR = 'Payload' 43 | PAYLOAD_PATH = os.path.join(TEMP_DIR, PAYLOAD_DIR) 44 | file_dict = {} 45 | 46 | finished = threading.Event() 47 | 48 | 49 | def get_usb_iphone(): 50 | Type = 'usb' 51 | if int(frida.__version__.split('.')[0]) < 12: 52 | Type = 'tether' 53 | device_manager = frida.get_device_manager() 54 | changed = threading.Event() 55 | 56 | def on_changed(): 57 | changed.set() 58 | 59 | device_manager.on('changed', on_changed) 60 | 61 | device = None 62 | while device is None: 63 | devices = [dev for dev in device_manager.enumerate_devices() if dev.type == Type] 64 | if len(devices) == 0: 65 | print('Waiting for USB device...') 66 | changed.wait() 67 | else: 68 | device = devices[0] 69 | 70 | device_manager.off('changed', on_changed) 71 | 72 | return device 73 | 74 | 75 | def generate_ipa(path, display_name): 76 | ipa_filename = display_name + '.ipa' 77 | 78 | print('Generating "{}"'.format(ipa_filename)) 79 | try: 80 | app_name = file_dict['app'] 81 | 82 | for key, value in file_dict.items(): 83 | from_dir = os.path.join(path, key) 84 | to_dir = os.path.join(path, app_name, value) 85 | if key != 'app': 86 | shutil.move(from_dir, to_dir) 87 | 88 | target_dir = './' + PAYLOAD_DIR 89 | zip_args = ('zip', '-qr', os.path.join(os.getcwd(), ipa_filename), target_dir) 90 | subprocess.check_call(zip_args, cwd=TEMP_DIR) 91 | shutil.rmtree(PAYLOAD_PATH) 92 | except Exception as e: 93 | print(e) 94 | finished.set() 95 | 96 | def on_message(message, data): 97 | t = tqdm(unit='B',unit_scale=True,unit_divisor=1024,miniters=1) 98 | last_sent = [0] 99 | 100 | def progress(filename, size, sent): 101 | baseName = os.path.basename(filename) 102 | if IS_PY2 or isinstance(baseName, bytes): 103 | t.desc = baseName.decode("utf-8") 104 | else: 105 | t.desc = baseName 106 | t.total = size 107 | t.update(sent - last_sent[0]) 108 | last_sent[0] = 0 if size == sent else sent 109 | 110 | if 'payload' in message: 111 | payload = message['payload'] 112 | if 'dump' in payload: 113 | origin_path = payload['path'] 114 | dump_path = payload['dump'] 115 | 116 | scp_from = dump_path 117 | scp_to = PAYLOAD_PATH + '/' 118 | 119 | with SCPClient(ssh.get_transport(), progress = progress, socket_timeout = 60) as scp: 120 | scp.get(scp_from, scp_to) 121 | 122 | chmod_dir = os.path.join(PAYLOAD_PATH, os.path.basename(dump_path)) 123 | chmod_args = ('chmod', '655', chmod_dir) 124 | try: 125 | subprocess.check_call(chmod_args) 126 | except subprocess.CalledProcessError as err: 127 | print(err) 128 | 129 | index = origin_path.find('.app/') 130 | file_dict[os.path.basename(dump_path)] = origin_path[index + 5:] 131 | 132 | if 'app' in payload: 133 | app_path = payload['app'] 134 | 135 | scp_from = app_path 136 | scp_to = PAYLOAD_PATH + '/' 137 | with SCPClient(ssh.get_transport(), progress = progress, socket_timeout = 60) as scp: 138 | scp.get(scp_from, scp_to, recursive=True) 139 | 140 | chmod_dir = os.path.join(PAYLOAD_PATH, os.path.basename(app_path)) 141 | chmod_args = ('chmod', '755', chmod_dir) 142 | try: 143 | subprocess.check_call(chmod_args) 144 | except subprocess.CalledProcessError as err: 145 | print(err) 146 | 147 | file_dict['app'] = os.path.basename(app_path) 148 | 149 | if 'done' in payload: 150 | finished.set() 151 | t.close() 152 | 153 | def compare_applications(a, b): 154 | a_is_running = a.pid != 0 155 | b_is_running = b.pid != 0 156 | if a_is_running == b_is_running: 157 | if a.name > b.name: 158 | return 1 159 | elif a.name < b.name: 160 | return -1 161 | else: 162 | return 0 163 | elif a_is_running: 164 | return -1 165 | else: 166 | return 1 167 | 168 | 169 | def cmp_to_key(mycmp): 170 | """Convert a cmp= function into a key= function""" 171 | 172 | class K: 173 | def __init__(self, obj): 174 | self.obj = obj 175 | 176 | def __lt__(self, other): 177 | return mycmp(self.obj, other.obj) < 0 178 | 179 | def __gt__(self, other): 180 | return mycmp(self.obj, other.obj) > 0 181 | 182 | def __eq__(self, other): 183 | return mycmp(self.obj, other.obj) == 0 184 | 185 | def __le__(self, other): 186 | return mycmp(self.obj, other.obj) <= 0 187 | 188 | def __ge__(self, other): 189 | return mycmp(self.obj, other.obj) >= 0 190 | 191 | def __ne__(self, other): 192 | return mycmp(self.obj, other.obj) != 0 193 | 194 | return K 195 | 196 | 197 | def get_applications(device): 198 | try: 199 | applications = device.enumerate_applications() 200 | except Exception as e: 201 | sys.exit('Failed to enumerate applications: %s' % e) 202 | 203 | return applications 204 | 205 | 206 | def list_applications(device): 207 | applications = get_applications(device) 208 | 209 | if len(applications) > 0: 210 | pid_column_width = max(map(lambda app: len('{}'.format(app.pid)), applications)) 211 | name_column_width = max(map(lambda app: len(app.name), applications)) 212 | identifier_column_width = max(map(lambda app: len(app.identifier), applications)) 213 | else: 214 | pid_column_width = 0 215 | name_column_width = 0 216 | identifier_column_width = 0 217 | 218 | header_format = '%' + str(pid_column_width) + 's ' + '%-' + str(name_column_width) + 's ' + '%-' + str( 219 | identifier_column_width) + 's' 220 | print(header_format % ('PID', 'Name', 'Identifier')) 221 | print('%s %s %s' % (pid_column_width * '-', name_column_width * '-', identifier_column_width * '-')) 222 | line_format = '%' + str(pid_column_width) + 's ' + '%-' + str(name_column_width) + 's ' + '%-' + str( 223 | identifier_column_width) + 's' 224 | for application in sorted(applications, key=cmp_to_key(compare_applications)): 225 | if application.pid == 0: 226 | print(line_format % ('-', application.name, application.identifier)) 227 | else: 228 | print(line_format % (application.pid, application.name, application.identifier)) 229 | 230 | 231 | def load_js_file(session, filename): 232 | source = '' 233 | with codecs.open(filename, 'r', 'utf-8') as f: 234 | source = source + f.read() 235 | script = session.create_script(source) 236 | script.on('message', on_message) 237 | script.load() 238 | 239 | return script 240 | 241 | 242 | def create_dir(path): 243 | path = path.strip() 244 | path = path.rstrip('\\') 245 | if os.path.exists(path): 246 | shutil.rmtree(path) 247 | try: 248 | os.makedirs(path) 249 | except os.error as err: 250 | print(err) 251 | 252 | 253 | def open_target_app(device, name_or_bundleid): 254 | print('Start the target app {}'.format(name_or_bundleid)) 255 | 256 | pid = '' 257 | session = None 258 | display_name = '' 259 | bundle_identifier = '' 260 | for application in get_applications(device): 261 | if name_or_bundleid == application.identifier or name_or_bundleid == application.name: 262 | pid = application.pid 263 | display_name = application.name 264 | bundle_identifier = application.identifier 265 | 266 | try: 267 | if not pid: 268 | pid = device.spawn([bundle_identifier]) 269 | session = device.attach(pid) 270 | device.resume(pid) 271 | else: 272 | session = device.attach(pid) 273 | except Exception as e: 274 | print(e) 275 | 276 | return session, display_name, bundle_identifier 277 | 278 | 279 | def start_dump(session, ipa_name): 280 | print('Dumping {} to {}'.format(display_name, TEMP_DIR)) 281 | 282 | script = load_js_file(session, DUMP_JS) 283 | script.post('dump') 284 | finished.wait() 285 | 286 | generate_ipa(PAYLOAD_PATH, ipa_name) 287 | 288 | if session: 289 | session.detach() 290 | 291 | 292 | if __name__ == '__main__': 293 | parser = argparse.ArgumentParser(description='frida-ios-dump (by AloneMonkey v2.0)') 294 | parser.add_argument('-l', '--list', dest='list_applications', action='store_true', help='List the installed apps') 295 | parser.add_argument('-o', '--output', dest='output_ipa', help='Specify name of the decrypted IPA') 296 | parser.add_argument('-H', '--host', dest='ssh_host', help='Specify SSH hostname') 297 | parser.add_argument('-p', '--port', dest='ssh_port', help='Specify SSH port') 298 | parser.add_argument('-u', '--user', dest='ssh_user', help='Specify SSH username') 299 | parser.add_argument('-P', '--password', dest='ssh_password', help='Specify SSH password') 300 | parser.add_argument('-K', '--key_filename', dest='ssh_key_filename', help='Specify SSH private key file path') 301 | parser.add_argument('target', nargs='?', help='Bundle identifier or display name of the target app') 302 | 303 | args = parser.parse_args() 304 | 305 | exit_code = 0 306 | ssh = None 307 | 308 | if not len(sys.argv[1:]): 309 | parser.print_help() 310 | sys.exit(exit_code) 311 | 312 | device = get_usb_iphone() 313 | 314 | if args.list_applications: 315 | list_applications(device) 316 | else: 317 | name_or_bundleid = args.target 318 | output_ipa = args.output_ipa 319 | # update ssh args 320 | if args.ssh_host: 321 | Host = args.ssh_host 322 | if args.ssh_port: 323 | Port = int(args.ssh_port) 324 | if args.ssh_user: 325 | User = args.ssh_user 326 | if args.ssh_password: 327 | Password = args.ssh_password 328 | if args.ssh_key_filename: 329 | KeyFileName = args.ssh_key_filename 330 | 331 | try: 332 | ssh = paramiko.SSHClient() 333 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 334 | ssh.connect(Host, port=Port, username=User, password=Password, key_filename=KeyFileName) 335 | 336 | create_dir(PAYLOAD_PATH) 337 | (session, display_name, bundle_identifier) = open_target_app(device, name_or_bundleid) 338 | if output_ipa is None: 339 | output_ipa = display_name 340 | output_ipa = re.sub('\.ipa$', '', output_ipa) 341 | if session: 342 | start_dump(session, output_ipa) 343 | except paramiko.ssh_exception.NoValidConnectionsError as e: 344 | print(e) 345 | print('Try specifying -H/--hostname and/or -p/--port') 346 | exit_code = 1 347 | except paramiko.AuthenticationException as e: 348 | print(e) 349 | print('Try specifying -u/--username and/or -P/--password') 350 | exit_code = 1 351 | except Exception as e: 352 | print('*** Caught exception: %s: %s' % (e.__class__, e)) 353 | traceback.print_exc() 354 | exit_code = 1 355 | 356 | if ssh: 357 | ssh.close() 358 | 359 | if os.path.exists(PAYLOAD_PATH): 360 | shutil.rmtree(PAYLOAD_PATH) 361 | 362 | sys.exit(exit_code) 363 | -------------------------------------------------------------------------------- /process.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ORIGINAL_IPA_FOLDER="${HOME}/Music/iTunes/iTunes Media/Mobile Applications" 4 | DECRYPTED_IPA_FOLDER="${HOME}/Hacking/Decrypted Apps" 5 | 6 | IOS_DEPLOY="/usr/local/bin/ios-deploy" 7 | PLISTBUDDY="/usr/libexec/PlistBuddy" 8 | FRIDA_DUMP_PY="./dump.py" 9 | # CFBundleIdentifier in Info.plist to uninstall 10 | 11 | ORIGINAL_IPAS=() 12 | while IFS= read -r -d $'\0'; do 13 | ORIGINAL_IPA=$( echo "${REPLY}" ) 14 | ORIGINAL_IPAS+=( "${ORIGINAL_IPA}" ) 15 | done < <(find "${ORIGINAL_IPA_FOLDER}" -type f -name '*.ipa' -print0) 16 | 17 | echo "Decrypting ${#ORIGINAL_IPAS[@]} apps" 18 | APP_COUNTER=0 19 | for ORIGINAL_IPA in "${ORIGINAL_IPAS[@]}"; do 20 | APP_COUNTER=$((APP_COUNTER + 1)) 21 | echo "#### Decryting app number: ${APP_COUNTER}" 22 | APP_BUNDLE_ID="" 23 | ESCAPED_IPA_PATH=$( echo "${ORIGINAL_IPA}" | sed 's/ /\\ /g' | sed 's/&/\\&/g' ) 24 | 25 | IPA_NAME=$( basename "${ESCAPED_IPA_PATH}" ) 26 | 27 | # Make a temp file for the iTunesMetadata.plist 28 | ITUNESMETADATA_TMP_FILE=$( mktemp "/tmp/iTunesMetadata.plist.XXXXXX" ) || exit 1 29 | 30 | # Unzip iTunesMetadata.plist to temp file 31 | UNZIP_CMD="unzip -p ${ESCAPED_IPA_PATH} iTunesMetadata.plist > ${ITUNESMETADATA_TMP_FILE}" 32 | echo "${UNZIP_CMD}" 33 | eval "${UNZIP_CMD}" || exit 1 34 | 35 | # Extract softwareVersionBundleId from plist file 36 | APP_BUNDLE_ID=$( "${PLISTBUDDY}" -c 'Print softwareVersionBundleId' "${ITUNESMETADATA_TMP_FILE}" ) 37 | echo "App bundle identifier: ${APP_BUNDLE_ID}" 38 | rm "${ITUNESMETADATA_TMP_FILE}" 39 | 40 | # Install the app 41 | IOS_DEPLOY_INSTALL="${IOS_DEPLOY} -W --bundle ${ESCAPED_IPA_PATH}" 42 | echo "${IOS_DEPLOY_INSTALL}" 43 | eval "${IOS_DEPLOY_INSTALL}" 44 | 45 | if [ $? -eq 0 ]; then 46 | sleep 1 47 | 48 | echo "Dumping ${APP_BUNDLE_ID}" 49 | FRIDA_DUMP_CMD="${FRIDA_DUMP_PY} --output ${IPA_NAME} ${APP_BUNDLE_ID}" 50 | echo "${FRIDA_DUMP_CMD}" 51 | eval "${FRIDA_DUMP_CMD}" 52 | fi 53 | 54 | # Uninstall the app 55 | IOS_DEPLOY_UNINSTALL="${IOS_DEPLOY} -W --uninstall_only --bundle_id ${APP_BUNDLE_ID}" 56 | echo "${IOS_DEPLOY_UNINSTALL}" 57 | eval "${IOS_DEPLOY_UNINSTALL}" 58 | done -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto 2 | bcrypt 3 | cffi 4 | colorama 5 | cryptography 6 | enum34 7 | frida-tools 8 | idna 9 | ipaddress 10 | paramiko 11 | prompt-toolkit 12 | pyasn1 13 | pycparser 14 | Pygments 15 | PyNaCl 16 | scp 17 | six 18 | tqdm 19 | wcwidth 20 | --------------------------------------------------------------------------------