368 lines
11 KiB
Python
Executable File
368 lines
11 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Author : AloneMonkey
|
|
# blog: www.alonemonkey.com
|
|
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
import sys
|
|
import codecs
|
|
import frida
|
|
import threading
|
|
import os
|
|
import time
|
|
import argparse
|
|
import tempfile
|
|
import subprocess
|
|
import re
|
|
import paramiko
|
|
from paramiko import SSHClient
|
|
from scp import SCPClient
|
|
from tqdm import tqdm
|
|
import traceback
|
|
|
|
IS_PY2 = sys.version_info[0] < 3
|
|
if IS_PY2:
|
|
reload(sys)
|
|
sys.setdefaultencoding('utf8')
|
|
|
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
DUMP_JS = os.path.join(script_dir, 'dump.js')
|
|
|
|
User = 'root'
|
|
Password = 'alpine'
|
|
Host = 'localhost'
|
|
Port = 2222
|
|
KeyFileName = None
|
|
|
|
TEMP_DIR = tempfile.gettempdir()
|
|
PAYLOAD_DIR = 'Payload'
|
|
PAYLOAD_PATH = os.path.join(TEMP_DIR, PAYLOAD_DIR)
|
|
file_dict = {}
|
|
|
|
finished = threading.Event()
|
|
|
|
|
|
def get_usb_iphone():
|
|
Type = 'usb'
|
|
if int(frida.__version__.split('.')[0]) < 12:
|
|
Type = 'tether'
|
|
device_manager = frida.get_device_manager()
|
|
changed = threading.Event()
|
|
|
|
def on_changed():
|
|
changed.set()
|
|
|
|
device_manager.on('changed', on_changed)
|
|
|
|
device = None
|
|
while device is None:
|
|
devices = [dev for dev in device_manager.enumerate_devices() if dev.type == Type]
|
|
if len(devices) == 0:
|
|
print('Waiting for USB device...')
|
|
changed.wait()
|
|
else:
|
|
device = devices[0]
|
|
|
|
device_manager.off('changed', on_changed)
|
|
|
|
return device
|
|
|
|
|
|
def generate_ipa(path, display_name):
|
|
ipa_filename = display_name + '.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':
|
|
try:
|
|
os.rename(from_dir, to_dir)
|
|
except FileExistsError:
|
|
os.remove(to_dir)
|
|
os.rename(from_dir, to_dir)
|
|
target_dir = './' + PAYLOAD_DIR
|
|
zip_args = ("7z", "a", "-r", f"{os.getcwd()}/{ipa_filename}.zip", "-w", f"{target_dir}", "-mem=AES256")
|
|
subprocess.check_call(zip_args, cwd=TEMP_DIR)
|
|
os.rename(f"{os.getcwd()}/{ipa_filename}.zip", f"{os.getcwd()}/{ipa_filename}")
|
|
os.system('rmdir /S /Q "{}"'.format(PAYLOAD_PATH))
|
|
except Exception as e:
|
|
print(f"{type(e)}: {e}")
|
|
finished.set()
|
|
|
|
def on_message(message, data):
|
|
t = tqdm(unit='B',unit_scale=True,unit_divisor=1024,miniters=1)
|
|
last_sent = [0]
|
|
|
|
def progress(filename, size, sent):
|
|
baseName = os.path.basename(filename)
|
|
if IS_PY2 or isinstance(baseName, bytes):
|
|
t.desc = baseName.decode("utf-8")
|
|
else:
|
|
t.desc = baseName
|
|
t.total = size
|
|
t.update(sent - last_sent[0])
|
|
last_sent[0] = 0 if size == sent else sent
|
|
|
|
if 'payload' in message:
|
|
payload = message['payload']
|
|
if 'dump' in payload:
|
|
origin_path = payload['path']
|
|
dump_path = payload['dump']
|
|
|
|
scp_from = dump_path
|
|
scp_to = PAYLOAD_PATH + '/'
|
|
|
|
with SCPClient(ssh.get_transport(), progress = progress, socket_timeout = 60) 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)
|
|
"""
|
|
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 'app' in payload:
|
|
app_path = payload['app']
|
|
|
|
scp_from = app_path
|
|
scp_to = PAYLOAD_PATH + '/'
|
|
with SCPClient(ssh.get_transport(), progress = progress, socket_timeout = 60) 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)
|
|
"""
|
|
try:
|
|
subprocess.check_call(chmod_args)
|
|
except subprocess.CalledProcessError as err:
|
|
print(err)
|
|
"""
|
|
file_dict['app'] = os.path.basename(app_path)
|
|
|
|
if 'done' in payload:
|
|
finished.set()
|
|
t.close()
|
|
|
|
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:
|
|
return -1
|
|
else:
|
|
return 1
|
|
|
|
|
|
def cmp_to_key(mycmp):
|
|
"""Convert a cmp= function into a key= function"""
|
|
|
|
class K:
|
|
def __init__(self, obj):
|
|
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
|
|
|
|
|
|
def get_applications(device):
|
|
try:
|
|
applications = device.enumerate_applications()
|
|
except Exception as e:
|
|
sys.exit('Failed to enumerate applications: %s' % e)
|
|
|
|
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 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 % (application.pid, application.name, application.identifier))
|
|
|
|
|
|
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
|
|
|
|
|
|
def create_dir(path):
|
|
path = path.strip()
|
|
path = path.rstrip('\\')
|
|
if os.path.exists(path):
|
|
os.system('rmdir /S /Q "{}"'.format(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 = ''
|
|
session = None
|
|
display_name = ''
|
|
bundle_identifier = ''
|
|
for application in get_applications(device):
|
|
if name_or_bundleid == application.identifier or name_or_bundleid == application.name:
|
|
pid = application.pid
|
|
display_name = application.name
|
|
bundle_identifier = application.identifier
|
|
|
|
try:
|
|
if not pid:
|
|
pid = device.spawn([bundle_identifier])
|
|
session = device.attach(pid)
|
|
device.resume(pid)
|
|
else:
|
|
session = device.attach(pid)
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
return session, display_name, bundle_identifier
|
|
|
|
|
|
def start_dump(session, ipa_name):
|
|
print('Dumping {} to {}'.format(display_name, TEMP_DIR))
|
|
|
|
script = load_js_file(session, DUMP_JS)
|
|
script.post('dump')
|
|
finished.wait()
|
|
|
|
generate_ipa(PAYLOAD_PATH, ipa_name)
|
|
|
|
if session:
|
|
session.detach()
|
|
|
|
|
|
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('-H', '--host', dest='ssh_host', help='Specify SSH hostname')
|
|
parser.add_argument('-p', '--port', dest='ssh_port', help='Specify SSH port')
|
|
parser.add_argument('-u', '--user', dest='ssh_user', help='Specify SSH username')
|
|
parser.add_argument('-P', '--password', dest='ssh_password', help='Specify SSH password')
|
|
parser.add_argument('-K', '--key_filename', dest='ssh_key_filename', help='Specify SSH private key file path')
|
|
parser.add_argument('target', nargs='?', help='Bundle identifier or display name of the target app')
|
|
|
|
args = parser.parse_args()
|
|
|
|
exit_code = 0
|
|
ssh = None
|
|
|
|
if not len(sys.argv[1:]):
|
|
parser.print_help()
|
|
sys.exit(exit_code)
|
|
|
|
device = get_usb_iphone()
|
|
|
|
if args.list_applications:
|
|
list_applications(device)
|
|
else:
|
|
name_or_bundleid = args.target
|
|
output_ipa = args.output_ipa
|
|
# update ssh args
|
|
if args.ssh_host:
|
|
Host = args.ssh_host
|
|
if args.ssh_port:
|
|
Port = int(args.ssh_port)
|
|
if args.ssh_user:
|
|
User = args.ssh_user
|
|
if args.ssh_password:
|
|
Password = args.ssh_password
|
|
if args.ssh_key_filename:
|
|
KeyFileName = args.ssh_key_filename
|
|
|
|
try:
|
|
ssh = paramiko.SSHClient()
|
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
ssh.connect(Host, port=Port, username=User, password=Password, key_filename=KeyFileName)
|
|
|
|
create_dir(PAYLOAD_PATH)
|
|
(session, 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)
|
|
if session:
|
|
start_dump(session, output_ipa)
|
|
except paramiko.ssh_exception.NoValidConnectionsError as e:
|
|
print(e)
|
|
print('Try specifying -H/--hostname and/or -p/--port')
|
|
exit_code = 1
|
|
except paramiko.AuthenticationException as e:
|
|
print(e)
|
|
print('Try specifying -u/--username and/or -P/--password')
|
|
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):
|
|
os.system('rmdir /S /Q "{}"'.format(PAYLOAD_PATH))
|
|
|
|
sys.exit(exit_code)
|