Files
Mobile-Security-Framework-M…/mobsf/DynamicAnalyzer/views/ios/device/report.py
T
6e875fb77b Jailbroken iOS Device Support (#2536)
Enables dynamic analysis on real jailbroken iOS devices connected via USB or WiFi SSH, complementing the existing Corellium-based iOS analysis.              
                                                                                                                                                                                                                                                                                                                                                                                                                                                                
  - SSH connectivity — connects to jailbroken devices over USB (via iproxy port forwarding) or WiFi using Paramiko SSH                                         
  - Frida instrumentation — spawns/attaches Frida on-device, with full hook support matching the existing Corellium flow                                       
  - Environment setup — installs AppSync Unified and Frida server on first run; supports both arm and arm64 devices                                            
  - Dynamic analysis — app file extraction, system log streaming (oslog), screenshot capture, process listing, and report generation
  - SSH terminal — execute shell commands on the device over SSH

---------

Co-authored-by: CylentSec <[email protected]>
Co-authored-by: Oz <[email protected]>
2026-03-23 01:46:03 -07:00

313 lines
12 KiB
Python

# -*- coding: utf_8 -*-
"""Dynamic Analyzer Reporting for iOS devices."""
import logging
import shutil
from pathlib import Path
import json
from django.conf import settings
from django.shortcuts import render
from django.template.defaulttags import register
from django.views.decorators.http import require_http_methods
from django.http import HttpResponse
import mobsf.MalwareAnalyzer.views.Trackers as Trackers
from mobsf.DynamicAnalyzer.views.ios.analysis import (
get_screenshots,
ios_api_analysis,
run_analysis,
)
from mobsf.DynamicAnalyzer.views.ios.device.dynamic_analyzer import (
FAILED,
OK,
validate_and_connect_device,
)
from mobsf.DynamicAnalyzer.tools.webproxy import (
get_http_tools_url,
stop_httptools,
)
from mobsf.MobSF.utils import (
IOS_DEVICE_ID_REGEX,
SSH_DEVICE_ID_REGEX,
base64_decode,
get_md5,
is_md5,
key,
pretty_json,
print_n_send_error_response,
replace,
strict_package_check,
)
from mobsf.MobSF.views.authentication import (
login_required,
)
from mobsf.DynamicAnalyzer.views.common.shared import (
send_response,
)
from mobsf.MobSF.views.authorization import (
Permissions,
permission_required,
)
logger = logging.getLogger(__name__)
register.filter('key', key)
register.filter('replace', replace)
register.filter('pretty_json', pretty_json)
register.filter('base64_decode', base64_decode)
def download_app_data_device(ios_device, checksum):
"""Download App data from device."""
app_dir = Path(settings.UPLD_DIR) / checksum
container_file = app_dir / 'mobsf_app_container_path.txt'
if container_file.exists():
app_container = container_file.read_text(
'utf-8').splitlines()[0].strip()
tarfile = f'/tmp/{checksum}-app-container.tar'
localtar = app_dir / f'{checksum}-app-container.tar'
ios_device.execute_command(f'tar -C {app_container} -cvf {tarfile} .')
ios_device.download_file(tarfile, localtar)
if localtar.exists():
dst = Path(settings.DWD_DIR) / f'{checksum}-app_data.tar'
shutil.copyfile(localtar, dst)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def download_data_device(request, bundle_id, api=False):
"""Download Application Data from Device."""
logger.info('Downloading application data')
data = {
'status': FAILED,
'message': 'Failed to Download application data'}
try:
device_id = request.POST['device_id']
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
if not strict_package_check(bundle_id):
data['message'] = 'Invalid iOS Bundle id'
return send_response(data, api)
checksum = get_md5(bundle_id.encode('utf-8'))
# App Container download
logger.info('Downloading app container data')
download_app_data_device(ios_device, checksum)
# Stop HTTPS Proxy
stop_httptools(get_http_tools_url(request))
# Move HTTP raw logs to download directory
flows = Path.home() / '.httptools' / 'flows'
webf = flows / f'{bundle_id}.flow.txt'
dwd = Path(settings.DWD_DIR)
dweb = dwd / f'{checksum}-web_traffic.txt'
if webf.exists():
shutil.copyfile(webf, dweb)
data = {
'status': OK,
'message': 'Downloaded application data',
}
except Exception as exp:
logger.exception('Downloading application data')
data['message'] = str(exp)
return send_response(data, api)
@login_required
def view_report_device(request, bundle_id, api=False):
"""Dynamic Analysis Report Generation."""
logger.info('iOS Dynamic Analysis Report Generation')
try:
if api:
device_id = request.POST.get('device_id')
else:
device_id = request.GET.get('device_id')
if device_id:
usb = IOS_DEVICE_ID_REGEX.match(device_id)
wifi = SSH_DEVICE_ID_REGEX.match(device_id)
if not (usb or wifi):
return print_n_send_error_response(
request,
'Invalid device id or SSH connection string format',
api)
if not strict_package_check(bundle_id):
# bundle_id is not validated in REST API.
# Also bundleid is not strictly validated
# in URL path.
return print_n_send_error_response(
request,
'Invalid iOS Bundle id',
api)
checksum = get_md5(bundle_id.encode('utf-8'))
app_dir = Path(settings.UPLD_DIR) / checksum
download_dir = settings.DWD_DIR
tools_dir = settings.TOOLS_DIR
frida_log = app_dir / 'mobsf_frida_out.txt'
data_dir = app_dir / 'DYNAMIC_DeviceData'
if not (frida_log.exists() or data_dir.exists()):
msg = ('Dynamic Analysis report is not available '
'for this app. Perform Dynamic Analysis '
'and generate the report.')
return print_n_send_error_response(request, msg, api)
api_analysis = ios_api_analysis(app_dir)
dump_analysis = run_analysis(app_dir, bundle_id, checksum)
trk = Trackers.Trackers(checksum, app_dir, tools_dir)
trackers = trk.get_trackers_domains_or_deps(
dump_analysis['domains'], None)
screenshots = get_screenshots(checksum, download_dir)
logger.info('Report generation completed')
context = {
'hash': checksum,
'device_id': device_id,
'version': settings.MOBSF_VER,
'title': 'iOS Dynamic Analysis Report',
'bundleid': bundle_id,
'trackers': trackers,
'screenshots': screenshots,
'frida_logs': frida_log.exists(),
}
context.update(api_analysis)
context.update(dump_analysis)
template = 'dynamic_analysis/ios/device/dynamic_report.html'
if api:
return context
return render(request, template, context)
except Exception as exp:
logger.exception('Dynamic Analysis Report Generation')
err = f'Error Generating Dynamic Analysis Report. {str(exp)}'
return print_n_send_error_response(request, err, api)
# JSON API + File Download
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def download_file_device(request, api=False):
"""Download file from device."""
err_msg = 'Failed to download file'
data = {
'status': FAILED,
'message': err_msg,
}
try:
remote_path = request.POST['file']
device_id = request.POST['device_id']
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
file_obj = ios_device.download_file_object(remote_path)
if file_obj:
response = HttpResponse(file_obj, content_type='application/octet-stream')
response['Content-Disposition'] = (
f'inline; filename={Path(remote_path).name}')
return response
else:
data['message'] = 'Failed to download file'
except Exception as exp:
logger.exception(err_msg)
data['message'] = str(exp)
return send_response(data, api)
# JSON API
@login_required
def ios_api_monitor(request, api=False):
try:
data = {
'status': FAILED,
'message': 'Failed to get API monitor data',
}
if api:
checksum = request.POST['checksum']
stream = True
else:
checksum = request.GET.get('checksum', '')
stream = request.GET.get('stream', '')
bundle_id = request.GET.get('bundle_id', '')
if bundle_id and not strict_package_check(bundle_id):
data['message'] = 'Invalid iOS Bundle id'
return send_response(data, api)
if not is_md5(checksum):
data['message'] = 'Invalid checksum format'
return send_response(data, api)
if stream:
app_dir = Path(settings.UPLD_DIR) / checksum
apimon_file = app_dir / 'mobsf_dump_file.txt'
data = {}
if not apimon_file.exists():
data['message'] = 'Data does not exist.'
return send_response(data, api)
# Read the file and parse each line as JSON
lines = apimon_file.read_text(encoding='utf-8').splitlines()
# remove all duplicate lines
lines = list(set(lines))
api_data = []
for line in lines:
if line.strip():
try:
# Parse each line as JSON
json_obj = json.loads(line)
# Determine the API type based on the keys in the JSON object
api_type = None
if 'cookies' in json_obj:
api_type = 'Cookies'
elif 'keychain' in json_obj:
api_type = 'Keychain'
elif 'json' in json_obj:
api_type = 'JSON Data'
elif 'network' in json_obj:
api_type = 'Network Request'
elif 'sql' in json_obj:
api_type = 'SQL Query'
elif 'filename' in json_obj:
api_type = 'File Access'
elif 'crypto' in json_obj:
api_type = 'Crypto'
elif 'files' in json_obj:
api_type = 'Files'
elif 'nslog' in json_obj:
api_type = 'Logs'
elif 'credentialstorage' in json_obj:
api_type = 'Credentials'
elif 'nsuserdefaults' in json_obj:
api_type = 'UserDefaults'
elif 'pasteboard' in json_obj:
api_type = 'Pasteboard'
elif 'textinput' in json_obj:
api_type = 'Text Inputs'
elif 'datadir' in json_obj:
api_type = 'Data Directory'
else:
# If no known key, use the first key as API type
api_type = list(json_obj.keys())[
0] if json_obj else 'Unknown'
# Create the data object for DataTable
api_data.append({
'api': api_type,
'data': json_obj,
})
except json.JSONDecodeError:
# Skip invalid JSON lines
continue
data['data'] = api_data
return send_response(data, api)
template = 'dynamic_analysis/ios/api_monitor.html'
return render(request,
template,
{'checksum': checksum,
'bundle_id': bundle_id,
'version': settings.MOBSF_VER,
'title': 'API Monitor'})
except Exception:
logger.exception('API monitor streaming')
err = 'Error in API monitor streaming'
return print_n_send_error_response(request, err, api)