文章总结: 本文介绍了修改frida-ios-dump脚本以支持Windows环境下通过网络连接而非USB连接进行IOSAPP脱壳的方法。作者提供了完整的dump.py脚本代码,该脚本支持远程Frida连接、列出应用、直接通过PID附加等功能,并修复了Windows环境下的一些兼容性问题。文章适用于那些使用Windows系统与IOS设备进行逆向分析的开发者,特别是当USB连接不可用时,可以通过网络连接完成APP脱壳工作。 综合评分: 91 文章分类: 移动安全,逆向分析,安全工具,实战经验
IOS APP 脱壳工具Frida-ios-dump脚本修改
原创
十月的进阶之路
十月的进阶之路
2025年12月13日 11:59 甘肃
0x01、背景
紧接着上一篇《Frida 在Windows+IOS环境下的崩溃》后,我需要使用frida-ios-dump脚本针对IOS APP进行脱壳,但是标准的frida-ios-dump需要使用USB连接的方式来获取设备信息。但还记得么,我这个iphone 6+ios 12.5.7+Windows的搭配缺少一些东西,无法通过USB连接的方式通信。
因此基于官方的firda-ios-dump脚本(官方:https://github.com/AloneMonkey/frida-ios-dump.git)进行一些修正,使得整个脚本可以通过纯粹的网络连接完成脱壳。
0x02、dump.py脚本内容
仅仅修改了下dump.py脚本,其他文件保持一致,内容如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Modified frida-ios-dump:
- Support remote Frida connection (-R host:port)
- Support listing apps (-l)
- Support direct attach by PID (--pid)
- Keep SSH/SCP pulling behavior to fetch dumped files and package into .ipa
Fixes for Windows:
- Use os.chmod instead of external 'chmod'
- Handle missing 'app' key when packaging IPA
- Robust rmtree with onerror handler to fix permissions before deleting
"""
from __future__ import print_function, unicode_literals
import sys
import codecs
import frida
import threading
import os
import shutil
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
import stat
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')
# default SSH (iPhone) params - can be overridden by CLI
User = 'root'
Password = 'alpine'
Host = 'localhost'
Port = 22
KeyFileName = None
TEMP_DIR = tempfile.gettempdir()
PAYLOAD_DIR = 'Payload'
PAYLOAD_PATH = os.path.join(TEMP_DIR, PAYLOAD_DIR)
file_dict = {}
finished = threading.Event()
ssh = None # will be set in main before dump
def get_usb_iphone():
"""原始 USB 获取设备(保留以兼容)"""
Type = 'usb'
try:
if int(frida.__version__.split('.')[0]) < 12:
Type = 'tether'
except Exception:
Type = 'usb'
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 safe_chmod(path, mode):
"""Cross-platform safe chmod (ignore failures on Windows)."""
try:
os.chmod(path, mode)
except Exception:
# On Windows this may fail for many reasons; ignore.
pass
def on_message(message, data):
"""Handle messages from dump.js; pull files over SCP using global ssh."""
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
try:
if'payload'in message:
payload = message['payload']
# payload structures:
# { dump: "/var/.../something.fid", path: "/.../AppName.app/..." }
# { app: "/var/.../AppName.app" }
# { done: "ok" }
if'dump'in payload:
origin_path = payload.get('path')
dump_path = payload.get('dump')
scp_from = dump_path
scp_to = PAYLOAD_PATH + '/'
# ensure ssh exists
if ssh is None:
print("SSH client not ready - cannot SCP")
else:
try:
with SCPClient(ssh.get_transport(), progress=progress, socket_timeout=60) as scp:
scp.get(scp_from, scp_to)
except Exception as e:
print("SCP get failed:", e)
traceback.print_exc()
# set permission using os.chmod (cross-platform)
chmod_dir = os.path.join(PAYLOAD_PATH, os.path.basename(dump_path))
try:
# try set file mode to rw-r-x-r-x (0o655) similar to original intent
safe_chmod(chmod_dir, 0o655)
except Exception:
pass
# map dumped filename -> relative path inside app (after ".app/")
if origin_path:
index = origin_path.find('.app/')
if index != -1:
file_dict[os.path.basename(dump_path)] = origin_path[index + 5:]
else:
file_dict[os.path.basename(dump_path)] = origin_path
else:
file_dict[os.path.basename(dump_path)] = os.path.basename(dump_path)
if'app'in payload:
app_path = payload.get('app')
scp_from = app_path
scp_to = PAYLOAD_PATH + '/'
if ssh is None:
print("SSH client not ready - cannot SCP app dir")
else:
try:
with SCPClient(ssh.get_transport(), progress=progress, socket_timeout=60) as scp:
scp.get(scp_from, scp_to, recursive=True)
except Exception as e:
print("SCP get (app) failed:", e)
traceback.print_exc()
chmod_dir = os.path.join(PAYLOAD_PATH, os.path.basename(app_path))
# try to set directory and children perms
try:
for root, dirs, files in os.walk(chmod_dir):
safe_chmod(root, 0o755)
for f in files:
safe_chmod(os.path.join(root, f), 0o655)
except Exception:
pass
file_dict['app'] = os.path.basename(app_path)
if'done'in payload:
finished.set()
except Exception as e:
print("on_message exception:", e)
traceback.print_exc()
finally:
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:
apps = device.enumerate_applications()
except Exception as e:
raise RuntimeError("Failed to enumerate applications: %s" % e)
return apps
def list_applications(device):
"""列出应用;如果为空则退回列进程,帮助诊断远程设备状态"""
attempts = 5
apps = []
for i in range(attempts):
try:
apps = device.enumerate_applications()
if apps:
break
except Exception as e:
if i == attempts - 1:
print("Failed to enumerate applications after retries: %s" % e)
apps = []
break
time.sleep(0.5)
if not apps:
print("No installed-app list returned via enumerate_applications(). Trying enumerate_processes() to help diagnose:")
try:
procs = device.enumerate_processes()
pid_column_width = max(map(lambda p: len(str(p.pid)), procs)) if procs else 0
name_column_width = max(map(lambda p: len(p.name), procs)) if procs else 0
header_format = '%' + str(pid_column_width) + 's ' + '%-' + str(name_column_width) + 's'
print(header_format % ('PID', 'Process Name'))
print('%s %s' % (pid_column_width * '-', name_column_width * '-'))
for p in sorted(procs, key=lambda x: x.name):
print(header_format % (p.pid, p.name))
except Exception as e:
print("Also failed to enumerate processes: %s" % e)
return
pid_column_width = max(map(lambda app: len('{}'.format(app.pid)), apps))
name_column_width = max(map(lambda app: len(app.name), apps))
identifier_column_width = max(map(lambda app: len(app.identifier), apps))
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 = header_format
for application in sorted(apps, 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):
# remove previous payload path safely
def on_rm_error(func, path2, exc_info):
try:
os.chmod(path2, stat.S_IWRITE)
func(path2)
except Exception:
pass
shutil.rmtree(path, onerror=on_rm_error)
try:
os.makedirs(path)
except os.error as err:
print(err)
def open_target_app(device, name_or_bundleid):
"""
Flexible matching:
- exact match on identifier or name
- substring match (case-insensitive)
"""
print('Start the target app {}'.format(name_or_bundleid))
pid = ''
session = None
display_name = ''
bundle_identifier = ''
try:
apps = get_applications(device)
except Exception as e:
print("Failed to get applications for matching: %s" % e)
apps = []
needle = name_or_bundleid or ''
needle_lower = needle.lower()
for application in apps:
if needle == application.identifier or needle == application.name:
pid = application.pid
display_name = application.name
bundle_identifier = application.identifier
break
if needle_lower and (needle_lower in application.identifier.lower() or needle_lower in application.name.lower()):
pid = application.pid
display_name = application.name
bundle_identifier = application.identifier
break
try:
if not pid:
if bundle_identifier:
pid = device.spawn([bundle_identifier])
session = device.attach(pid)
device.resume(pid)
else:
pid = device.spawn([name_or_bundleid])
session = device.attach(pid)
device.resume(pid)
else:
session = device.attach(pid)
except Exception as e:
print("Error while spawning/attaching: %s" % e)
return session, display_name, bundle_identifier
def start_dump(session, ipa_name):
print('Dumping {} to {}'.format(ipa_name, TEMP_DIR))
script = load_js_file(session, DUMP_JS)
script.post('dump')
# wait until dump.js signals done
finished.wait()
generate_ipa(PAYLOAD_PATH, ipa_name)
if session:
try:
session.detach()
except Exception:
pass
def generate_ipa(path, display_name):
"""
Create Payload/<AppName>/... based on file_dict mappings, then zip into <display_name>.ipa
Robust for cases where 'app' key missing from file_dict.
"""
ipa_filename = display_name + '.ipa'
print('Generating "{}"'.format(ipa_filename))
try:
# determine app_name (this is the folder name under Payload)
if'app'in file_dict:
app_name = file_dict['app'] # this is basename like 'iEC-O2O-Buyer.app'
else:
# try find any .app dir in PAYLOAD_PATH (scp may have copied whole .app)
candidates = [d for d in os.listdir(path) if d.endswith('.app')]
if candidates:
app_name = candidates[0]
else:
# fallback: use display_name + ".app"
app_name = display_name + '.app'
# create the payload app dir
payload_app_dir = os.path.join(path, app_name)
if not os.path.exists(payload_app_dir):
os.makedirs(payload_app_dir, exist_ok=True)
# move each downloaded file into correct relative place under Payload/<app_name>/
for key, rel in list(file_dict.items()):
if key == 'app':
continue
src = os.path.join(path, key)
# if rel is an absolute origin path (no .app/ found earlier), put under root of app
if rel is None:
rel = os.path.basename(src)
target_rel_path = rel
# make sure directories exist
dest = os.path.join(payload_app_dir, target_rel_path)
dest_dir = os.path.dirname(dest)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir, exist_ok=True)
# if src exists, move it; otherwise ignore
if os.path.exists(src):
try:
shutil.move(src, dest)
except Exception as e:
# if move fails, try copy then remove
try:
shutil.copy2(src, dest)
os.remove(src)
except Exception as e2:
print("Failed to move or copy {} -> {}: {}".format(src, dest, e2))
else:
# sometimes scp may have created a directory instead -- try handle dir
if os.path.isdir(os.path.join(path, key)):
try:
shutil.move(os.path.join(path, key), dest)
except Exception as e:
print("Failed to move dir {} -> {}: {}".format(os.path.join(path, key), dest, e))
# now we need a Payload directory at TEMP_DIR/Payload (path already points to it)
target_dir = os.path.join(os.path.dirname(path), PAYLOAD_DIR)
# Ensure the payload dir exists and contains the app folder (payload_app_dir already in path)
# Create zip at current working directory
zip_out = os.path.join(os.getcwd(), ipa_filename)
# Use zip command if available, otherwise use Python zipfile module
try:
# prefer system zip (if installed)
subprocess.check_call(['zip', '-qr', zip_out, PAYLOAD_DIR], cwd=os.path.dirname(path))
except Exception:
# fallback to zipfile
import zipfile
def zipdir(folder, ziph):
for root, dirs, files in os.walk(folder):
for f in files:
fullpath = os.path.join(root, f)
arcname = os.path.relpath(fullpath, os.path.dirname(path))
ziph.write(fullpath, arcname)
with zipfile.ZipFile(zip_out, 'w', zipfile.ZIP_DEFLATED) as zipf:
zipdir(os.path.join(os.path.dirname(path), PAYLOAD_DIR), zipf)
# cleanup local payload path
def on_rm_error(func, p, exc_info):
try:
os.chmod(p, stat.S_IWRITE)
func(p)
except Exception:
pass
if os.path.exists(PAYLOAD_PATH):
shutil.rmtree(PAYLOAD_PATH, onerror=on_rm_error)
print("Generated:", ipa_filename)
except Exception as e:
print("generate_ipa error:", e)
traceback.print_exc()
finished.set()
def main():
global Host, Port, User, Password, KeyFileName, ssh
parser = argparse.ArgumentParser(description='frida-ios-dump (modified for robust remote Frida connection)')
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 (for iPhone SSH)')
parser.add_argument('-p', '--port', dest='ssh_port', help='Specify SSH port', type=int)
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('-R', '--remote', dest='frida_remote', help='Specify Frida server address (host:port)')
parser.add_argument('--pid', type=int, help='PID of target process (attach directly)')
parser.add_argument('target', nargs='?', help='Bundle identifier or display name of the target app')
args = parser.parse_args()
# Validate minimal args
if not len(sys.argv[1:]):
parser.print_help()
sys.exit(0)
# Connect to Frida remote (if provided) or USB
device = None
if args.frida_remote:
parts = args.frida_remote.split(':')
frida_host = parts[0]
frida_port = int(parts[1]) if len(parts) > 1 and parts[1] else 27042
try:
device = frida.get_remote_device(frida_host, frida_port)
print("Connected to remote Frida device at %s:%s (via frida.get_remote_device)" % (frida_host, frida_port))
except Exception as e1:
try:
manager = frida.get_device_manager()
device = manager.add_remote_device("%s:%s" % (frida_host, frida_port))
print("Connected to remote Frida device at %s:%s (via DeviceManager.add_remote_device)" % (frida_host, frida_port))
except Exception as e2:
print("Failed to connect to remote Frida server (tried get_remote_device and add_remote_device).")
print("get_remote_device error: %s" % e1)
print("add_remote_device error: %s" % e2)
sys.exit(1)
else:
device = get_usb_iphone()
# If user requested listing, do it now and exit
if args.list_applications:
try:
list_applications(device)
except Exception as e:
print("Error listing applications: %s" % e)
sys.exit(0)
# Update SSH params from args
if args.ssh_host:
Host = args.ssh_host
if args.ssh_port:
Port = 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
name_or_bundleid = args.target
output_ipa = args.output_ipa
# Establish SSH connection before starting dump (on_message will use ssh)
try:
ssh = SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(Host, port=Port, username=User, password=Password, key_filename=KeyFileName, timeout=20)
except Exception as e:
print("SSH connection failed:", e)
sys.exit(1)
# Prepare payload dir
create_dir(PAYLOAD_PATH)
session = None
display_name = None
bundle_identifier = None
# If PID provided, directly attach to that PID
if args.pid:
pid = args.pid
try:
print("Attaching to PID %d ..." % pid)
session = device.attach(pid)
display_name = "pid_%d" % pid
bundle_identifier = None
print("Attached to PID %d" % pid)
except Exception as e:
print("Failed to attach to PID %d: %s" % (pid, e))
ssh.close()
sys.exit(1)
else:
# fallback to name/bundle-based attach (spawn/attach)
(session, display_name, bundle_identifier) = open_target_app(device, name_or_bundleid)
if output_ipa is None:
output_ipa = display_name or (name_or_bundleid if name_or_bundleid else"dumped_app")
output_ipa = re.sub(r'\.ipa$', '', output_ipa)
if session:
try:
start_dump(session, output_ipa)
except Exception as e:
print("Dump failed:", e)
traceback.print_exc()
else:
print("No session created. Aborting.")
ssh.close()
if os.path.exists(PAYLOAD_PATH):
# cleanup
def on_rm_error(func, p, exc):
try:
os.chmod(p, stat.S_IWRITE)
func(p)
except Exception:
pass
shutil.rmtree(PAYLOAD_PATH, onerror=on_rm_error)
sys.exit(1)
# Cleanup SSH and payload
if ssh:
ssh.close()
if os.path.exists(PAYLOAD_PATH):
def on_rm_error(func, p, exc):
try:
os.chmod(p, stat.S_IWRITE)
func(p)
except Exception:
pass
shutil.rmtree(PAYLOAD_PATH, onerror=on_rm_error)
if __name__ == '__main__':
main()
依旧搭配上一篇的frida 14.2.10使用,通过目标APP名称以及pid运行如下命令即可在当前windows目录生成脱壳后的Mach-O文件。
python dump.py -H 192.168.148.224 -p 22 -R 192.168.148.224:1234 --pid 应用进程 -u root -P alpine -o 应用包名
其它命令保持原来的frida-ios-dump项目保持一致。
愿阅读到这里的你在这个寒冷的冬天有个惬意的周末,byby~~~
查看原文:《IOS APP 脱壳工具Frida-ios-dump脚本修改》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论