Full security platform with web dashboard, 16 Flask blueprints, 26 modules, autonomous AI agent, WebUSB hardware support, and Archon Android companion app. Includes Hash Toolkit, debug console, anti-stalkerware shield, Metasploit/RouterSploit integration, WireGuard VPN, OSINT reconnaissance, and multi-backend LLM support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
684 lines
30 KiB
Python
684 lines
30 KiB
Python
"""
|
|
AUTARCH iPhone Exploitation Manager
|
|
Local USB device access via libimobiledevice tools.
|
|
Device info, screenshots, syslog, app management, backup extraction,
|
|
filesystem mounting, provisioning profiles, port forwarding.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import json
|
|
import time
|
|
import shutil
|
|
import sqlite3
|
|
import subprocess
|
|
import plistlib
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, Any
|
|
|
|
from core.paths import get_data_dir, find_tool
|
|
|
|
|
|
class IPhoneExploitManager:
|
|
"""All iPhone USB exploitation logic using libimobiledevice."""
|
|
|
|
# Tools we look for
|
|
TOOLS = [
|
|
'idevice_id', 'ideviceinfo', 'idevicepair', 'idevicename',
|
|
'idevicedate', 'idevicescreenshot', 'idevicesyslog',
|
|
'idevicecrashreport', 'idevicediagnostics', 'ideviceinstaller',
|
|
'idevicebackup2', 'ideviceprovision', 'idevicedebug',
|
|
'ideviceactivation', 'ifuse', 'iproxy',
|
|
]
|
|
|
|
def __init__(self):
|
|
self._base = get_data_dir() / 'iphone_exploit'
|
|
for sub in ('backups', 'screenshots', 'recon', 'apps', 'crash_reports'):
|
|
(self._base / sub).mkdir(parents=True, exist_ok=True)
|
|
# Find available tools
|
|
self._tools = {}
|
|
for name in self.TOOLS:
|
|
path = find_tool(name)
|
|
if not path:
|
|
path = shutil.which(name)
|
|
if path:
|
|
self._tools[name] = path
|
|
|
|
def _udid_dir(self, category, udid):
|
|
d = self._base / category / udid
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
return d
|
|
|
|
def _run(self, tool_name, args, timeout=30):
|
|
"""Run a libimobiledevice tool."""
|
|
path = self._tools.get(tool_name)
|
|
if not path:
|
|
return '', f'{tool_name} not found', 1
|
|
cmd = [path] + args
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
return result.stdout, result.stderr, result.returncode
|
|
except subprocess.TimeoutExpired:
|
|
return '', 'Command timed out', 1
|
|
except Exception as e:
|
|
return '', str(e), 1
|
|
|
|
def _run_udid(self, tool_name, udid, args, timeout=30):
|
|
"""Run tool with -u UDID flag."""
|
|
return self._run(tool_name, ['-u', udid] + args, timeout=timeout)
|
|
|
|
def get_status(self):
|
|
"""Get availability of libimobiledevice tools."""
|
|
available = {name: bool(path) for name, path in self._tools.items()}
|
|
total = len(self.TOOLS)
|
|
found = sum(1 for v in available.values() if v)
|
|
return {
|
|
'tools': available,
|
|
'total': total,
|
|
'found': found,
|
|
'ready': found >= 3, # At minimum need idevice_id, ideviceinfo, idevicepair
|
|
}
|
|
|
|
# ── Device Management ────────────────────────────────────────────
|
|
|
|
def list_devices(self):
|
|
"""List connected iOS devices."""
|
|
stdout, stderr, rc = self._run('idevice_id', ['-l'])
|
|
if rc != 0:
|
|
return []
|
|
devices = []
|
|
for line in stdout.strip().split('\n'):
|
|
udid = line.strip()
|
|
if udid:
|
|
info = self.device_info_brief(udid)
|
|
devices.append({
|
|
'udid': udid,
|
|
'name': info.get('DeviceName', ''),
|
|
'model': info.get('ProductType', ''),
|
|
'ios_version': info.get('ProductVersion', ''),
|
|
})
|
|
return devices
|
|
|
|
def device_info(self, udid):
|
|
"""Get full device information."""
|
|
stdout, stderr, rc = self._run_udid('ideviceinfo', udid, [])
|
|
if rc != 0:
|
|
return {'error': stderr or 'Cannot get device info'}
|
|
info = {}
|
|
for line in stdout.split('\n'):
|
|
if ':' in line:
|
|
key, _, val = line.partition(':')
|
|
info[key.strip()] = val.strip()
|
|
return info
|
|
|
|
def device_info_brief(self, udid):
|
|
"""Get key device info (name, model, iOS version)."""
|
|
keys = ['DeviceName', 'ProductType', 'ProductVersion', 'BuildVersion',
|
|
'SerialNumber', 'UniqueChipID', 'WiFiAddress', 'BluetoothAddress']
|
|
info = {}
|
|
for key in keys:
|
|
stdout, _, rc = self._run_udid('ideviceinfo', udid, ['-k', key])
|
|
if rc == 0:
|
|
info[key] = stdout.strip()
|
|
return info
|
|
|
|
def device_info_domain(self, udid, domain):
|
|
"""Get device info for a specific domain."""
|
|
stdout, stderr, rc = self._run_udid('ideviceinfo', udid, ['-q', domain])
|
|
if rc != 0:
|
|
return {'error': stderr}
|
|
info = {}
|
|
for line in stdout.split('\n'):
|
|
if ':' in line:
|
|
key, _, val = line.partition(':')
|
|
info[key.strip()] = val.strip()
|
|
return info
|
|
|
|
def pair_device(self, udid):
|
|
"""Pair with device (requires user trust on device)."""
|
|
stdout, stderr, rc = self._run_udid('idevicepair', udid, ['pair'])
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
def unpair_device(self, udid):
|
|
"""Unpair from device."""
|
|
stdout, stderr, rc = self._run_udid('idevicepair', udid, ['unpair'])
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
def validate_pair(self, udid):
|
|
"""Check if device is properly paired."""
|
|
stdout, stderr, rc = self._run_udid('idevicepair', udid, ['validate'])
|
|
return {'success': rc == 0, 'paired': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
def get_name(self, udid):
|
|
"""Get device name."""
|
|
stdout, stderr, rc = self._run_udid('idevicename', udid, [])
|
|
return {'success': rc == 0, 'name': stdout.strip()}
|
|
|
|
def set_name(self, udid, name):
|
|
"""Set device name."""
|
|
stdout, stderr, rc = self._run_udid('idevicename', udid, [name])
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
def get_date(self, udid):
|
|
"""Get device date/time."""
|
|
stdout, stderr, rc = self._run_udid('idevicedate', udid, [])
|
|
return {'success': rc == 0, 'date': stdout.strip()}
|
|
|
|
def set_date(self, udid, timestamp):
|
|
"""Set device date (epoch timestamp)."""
|
|
stdout, stderr, rc = self._run_udid('idevicedate', udid, ['-s', str(timestamp)])
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
def restart_device(self, udid):
|
|
"""Restart device."""
|
|
stdout, stderr, rc = self._run_udid('idevicediagnostics', udid, ['restart'])
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
def shutdown_device(self, udid):
|
|
"""Shutdown device."""
|
|
stdout, stderr, rc = self._run_udid('idevicediagnostics', udid, ['shutdown'])
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
def sleep_device(self, udid):
|
|
"""Put device to sleep."""
|
|
stdout, stderr, rc = self._run_udid('idevicediagnostics', udid, ['sleep'])
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
# ── Screenshot & Syslog ──────────────────────────────────────────
|
|
|
|
def screenshot(self, udid):
|
|
"""Take a screenshot."""
|
|
out_dir = self._udid_dir('screenshots', udid)
|
|
filename = f'screen_{int(time.time())}.png'
|
|
filepath = str(out_dir / filename)
|
|
stdout, stderr, rc = self._run_udid('idevicescreenshot', udid, [filepath])
|
|
if rc == 0 and os.path.exists(filepath):
|
|
return {'success': True, 'path': filepath, 'size': os.path.getsize(filepath)}
|
|
return {'success': False, 'error': (stderr or stdout).strip()}
|
|
|
|
def syslog_dump(self, udid, duration=5):
|
|
"""Capture syslog for a duration."""
|
|
out_dir = self._udid_dir('recon', udid)
|
|
logfile = str(out_dir / f'syslog_{int(time.time())}.txt')
|
|
try:
|
|
proc = subprocess.Popen(
|
|
[self._tools.get('idevicesyslog', 'idevicesyslog'), '-u', udid],
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
|
)
|
|
time.sleep(duration)
|
|
proc.terminate()
|
|
stdout, _ = proc.communicate(timeout=3)
|
|
with open(logfile, 'w') as f:
|
|
f.write(stdout)
|
|
return {'success': True, 'path': logfile, 'lines': len(stdout.split('\n'))}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def syslog_grep(self, udid, pattern, duration=5):
|
|
"""Capture syslog and grep for pattern (passwords, tokens, etc)."""
|
|
result = self.syslog_dump(udid, duration=duration)
|
|
if not result['success']:
|
|
return result
|
|
matches = []
|
|
try:
|
|
with open(result['path']) as f:
|
|
for line in f:
|
|
if re.search(pattern, line, re.IGNORECASE):
|
|
matches.append(line.strip())
|
|
except Exception:
|
|
pass
|
|
return {'success': True, 'matches': matches, 'count': len(matches), 'pattern': pattern}
|
|
|
|
def crash_reports(self, udid):
|
|
"""Pull crash reports from device."""
|
|
out_dir = self._udid_dir('crash_reports', udid)
|
|
stdout, stderr, rc = self._run_udid('idevicecrashreport', udid,
|
|
['-e', str(out_dir)], timeout=60)
|
|
if rc == 0:
|
|
files = list(out_dir.iterdir()) if out_dir.exists() else []
|
|
return {'success': True, 'output_dir': str(out_dir),
|
|
'count': len(files), 'output': stdout.strip()}
|
|
return {'success': False, 'error': (stderr or stdout).strip()}
|
|
|
|
# ── App Management ───────────────────────────────────────────────
|
|
|
|
def list_apps(self, udid, app_type='user'):
|
|
"""List installed apps. type: user, system, all."""
|
|
flags = {
|
|
'user': ['-l', '-o', 'list_user'],
|
|
'system': ['-l', '-o', 'list_system'],
|
|
'all': ['-l', '-o', 'list_all'],
|
|
}
|
|
args = flags.get(app_type, ['-l'])
|
|
stdout, stderr, rc = self._run_udid('ideviceinstaller', udid, args, timeout=30)
|
|
if rc != 0:
|
|
return {'success': False, 'error': (stderr or stdout).strip(), 'apps': []}
|
|
apps = []
|
|
for line in stdout.strip().split('\n'):
|
|
line = line.strip()
|
|
if not line or line.startswith('CFBundle') or line.startswith('Total'):
|
|
continue
|
|
# Format: com.example.app, "App Name", "1.0"
|
|
parts = line.split(',', 2)
|
|
if parts:
|
|
app = {'bundle_id': parts[0].strip().strip('"')}
|
|
if len(parts) >= 2:
|
|
app['name'] = parts[1].strip().strip('"')
|
|
if len(parts) >= 3:
|
|
app['version'] = parts[2].strip().strip('"')
|
|
apps.append(app)
|
|
return {'success': True, 'apps': apps, 'count': len(apps)}
|
|
|
|
def install_app(self, udid, ipa_path):
|
|
"""Install an IPA on device."""
|
|
if not os.path.isfile(ipa_path):
|
|
return {'success': False, 'error': f'File not found: {ipa_path}'}
|
|
stdout, stderr, rc = self._run_udid('ideviceinstaller', udid,
|
|
['-i', ipa_path], timeout=120)
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
def uninstall_app(self, udid, bundle_id):
|
|
"""Uninstall an app by bundle ID."""
|
|
stdout, stderr, rc = self._run_udid('ideviceinstaller', udid,
|
|
['-U', bundle_id], timeout=30)
|
|
return {'success': rc == 0, 'bundle_id': bundle_id, 'output': (stdout or stderr).strip()}
|
|
|
|
# ── Backup & Data Extraction ─────────────────────────────────────
|
|
|
|
def create_backup(self, udid, encrypted=False, password=''):
|
|
"""Create a full device backup."""
|
|
backup_dir = str(self._base / 'backups')
|
|
args = ['backup', '--full', backup_dir]
|
|
if encrypted and password:
|
|
args = ['backup', '--full', backup_dir, '-p', password]
|
|
stdout, stderr, rc = self._run_udid('idevicebackup2', udid, args, timeout=600)
|
|
backup_path = os.path.join(backup_dir, udid)
|
|
success = os.path.isdir(backup_path)
|
|
return {
|
|
'success': success,
|
|
'backup_path': backup_path if success else None,
|
|
'encrypted': encrypted,
|
|
'output': (stdout or stderr).strip()[:500],
|
|
}
|
|
|
|
def list_backups(self):
|
|
"""List available local backups."""
|
|
backup_dir = self._base / 'backups'
|
|
backups = []
|
|
if backup_dir.exists():
|
|
for d in backup_dir.iterdir():
|
|
if d.is_dir():
|
|
manifest = d / 'Manifest.db'
|
|
info_plist = d / 'Info.plist'
|
|
backup_info = {'udid': d.name, 'path': str(d)}
|
|
if manifest.exists():
|
|
backup_info['has_manifest'] = True
|
|
backup_info['size_mb'] = sum(
|
|
f.stat().st_size for f in d.rglob('*') if f.is_file()
|
|
) / (1024 * 1024)
|
|
if info_plist.exists():
|
|
try:
|
|
with open(info_plist, 'rb') as f:
|
|
plist = plistlib.load(f)
|
|
backup_info['device_name'] = plist.get('Device Name', '')
|
|
backup_info['product_type'] = plist.get('Product Type', '')
|
|
backup_info['ios_version'] = plist.get('Product Version', '')
|
|
backup_info['date'] = str(plist.get('Last Backup Date', ''))
|
|
except Exception:
|
|
pass
|
|
backups.append(backup_info)
|
|
return {'backups': backups, 'count': len(backups)}
|
|
|
|
def extract_backup_sms(self, backup_path):
|
|
"""Extract SMS/iMessage from a backup."""
|
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
|
if not os.path.exists(manifest):
|
|
return {'success': False, 'error': 'Manifest.db not found'}
|
|
try:
|
|
conn = sqlite3.connect(manifest)
|
|
cur = conn.cursor()
|
|
# Find SMS database file hash
|
|
cur.execute("SELECT fileID FROM Files WHERE relativePath = 'Library/SMS/sms.db' AND domain = 'HomeDomain'")
|
|
row = cur.fetchone()
|
|
conn.close()
|
|
if not row:
|
|
return {'success': False, 'error': 'SMS database not found in backup'}
|
|
file_hash = row[0]
|
|
sms_db = os.path.join(backup_path, file_hash[:2], file_hash)
|
|
if not os.path.exists(sms_db):
|
|
return {'success': False, 'error': f'SMS db file not found: {file_hash}'}
|
|
# Query messages
|
|
conn = sqlite3.connect(sms_db)
|
|
cur = conn.cursor()
|
|
cur.execute('''
|
|
SELECT m.rowid, m.text, m.date, m.is_from_me,
|
|
h.id AS handle_id, h.uncanonicalized_id
|
|
FROM message m
|
|
LEFT JOIN handle h ON m.handle_id = h.rowid
|
|
ORDER BY m.date DESC LIMIT 500
|
|
''')
|
|
messages = []
|
|
for row in cur.fetchall():
|
|
# Apple timestamps: seconds since 2001-01-01
|
|
apple_epoch = 978307200
|
|
ts = row[2]
|
|
if ts and ts > 1e17:
|
|
ts = ts / 1e9 # nanoseconds
|
|
date_readable = ''
|
|
if ts:
|
|
try:
|
|
date_readable = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts + apple_epoch))
|
|
except (ValueError, OSError):
|
|
pass
|
|
messages.append({
|
|
'id': row[0], 'text': row[1] or '', 'date': date_readable,
|
|
'is_from_me': bool(row[3]),
|
|
'handle': row[4] or row[5] or '',
|
|
})
|
|
conn.close()
|
|
return {'success': True, 'messages': messages, 'count': len(messages)}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def extract_backup_contacts(self, backup_path):
|
|
"""Extract contacts from backup."""
|
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
|
if not os.path.exists(manifest):
|
|
return {'success': False, 'error': 'Manifest.db not found'}
|
|
try:
|
|
conn = sqlite3.connect(manifest)
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT fileID FROM Files WHERE relativePath = 'Library/AddressBook/AddressBook.sqlitedb' AND domain = 'HomeDomain'")
|
|
row = cur.fetchone()
|
|
conn.close()
|
|
if not row:
|
|
return {'success': False, 'error': 'AddressBook not found in backup'}
|
|
file_hash = row[0]
|
|
ab_db = os.path.join(backup_path, file_hash[:2], file_hash)
|
|
if not os.path.exists(ab_db):
|
|
return {'success': False, 'error': 'AddressBook file not found'}
|
|
conn = sqlite3.connect(ab_db)
|
|
cur = conn.cursor()
|
|
cur.execute('''
|
|
SELECT p.rowid, p.First, p.Last, p.Organization,
|
|
mv.value AS phone_or_email
|
|
FROM ABPerson p
|
|
LEFT JOIN ABMultiValue mv ON p.rowid = mv.record_id
|
|
ORDER BY p.Last, p.First
|
|
''')
|
|
contacts = {}
|
|
for row in cur.fetchall():
|
|
rid = row[0]
|
|
if rid not in contacts:
|
|
contacts[rid] = {
|
|
'first': row[1] or '', 'last': row[2] or '',
|
|
'organization': row[3] or '', 'values': []
|
|
}
|
|
if row[4]:
|
|
contacts[rid]['values'].append(row[4])
|
|
conn.close()
|
|
contact_list = list(contacts.values())
|
|
return {'success': True, 'contacts': contact_list, 'count': len(contact_list)}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def extract_backup_call_log(self, backup_path):
|
|
"""Extract call history from backup."""
|
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
|
if not os.path.exists(manifest):
|
|
return {'success': False, 'error': 'Manifest.db not found'}
|
|
try:
|
|
conn = sqlite3.connect(manifest)
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT fileID FROM Files WHERE relativePath LIKE '%CallHistory%' AND domain = 'HomeDomain'")
|
|
row = cur.fetchone()
|
|
conn.close()
|
|
if not row:
|
|
return {'success': False, 'error': 'Call history not found in backup'}
|
|
file_hash = row[0]
|
|
ch_db = os.path.join(backup_path, file_hash[:2], file_hash)
|
|
if not os.path.exists(ch_db):
|
|
return {'success': False, 'error': 'Call history file not found'}
|
|
conn = sqlite3.connect(ch_db)
|
|
cur = conn.cursor()
|
|
cur.execute('''
|
|
SELECT ROWID, address, date, duration, flags, country_code
|
|
FROM ZCALLRECORD ORDER BY ZDATE DESC LIMIT 200
|
|
''')
|
|
flag_map = {4: 'incoming', 5: 'outgoing', 8: 'missed'}
|
|
calls = []
|
|
apple_epoch = 978307200
|
|
for row in cur.fetchall():
|
|
ts = row[2]
|
|
date_readable = ''
|
|
if ts:
|
|
try:
|
|
date_readable = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts + apple_epoch))
|
|
except (ValueError, OSError):
|
|
pass
|
|
calls.append({
|
|
'id': row[0], 'address': row[1] or '', 'date': date_readable,
|
|
'duration': row[3] or 0, 'type': flag_map.get(row[4], str(row[4])),
|
|
'country': row[5] or '',
|
|
})
|
|
conn.close()
|
|
return {'success': True, 'calls': calls, 'count': len(calls)}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def extract_backup_notes(self, backup_path):
|
|
"""Extract notes from backup."""
|
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
|
if not os.path.exists(manifest):
|
|
return {'success': False, 'error': 'Manifest.db not found'}
|
|
try:
|
|
conn = sqlite3.connect(manifest)
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT fileID FROM Files WHERE relativePath LIKE '%NoteStore.sqlite%' AND domain = 'AppDomainGroup-group.com.apple.notes'")
|
|
row = cur.fetchone()
|
|
conn.close()
|
|
if not row:
|
|
return {'success': False, 'error': 'Notes database not found in backup'}
|
|
file_hash = row[0]
|
|
notes_db = os.path.join(backup_path, file_hash[:2], file_hash)
|
|
if not os.path.exists(notes_db):
|
|
return {'success': False, 'error': 'Notes file not found'}
|
|
conn = sqlite3.connect(notes_db)
|
|
cur = conn.cursor()
|
|
cur.execute('''
|
|
SELECT n.Z_PK, n.ZTITLE, nb.ZDATA, n.ZMODIFICATIONDATE
|
|
FROM ZICCLOUDSYNCINGOBJECT n
|
|
LEFT JOIN ZICNOTEDATA nb ON n.Z_PK = nb.ZNOTE
|
|
WHERE n.ZTITLE IS NOT NULL
|
|
ORDER BY n.ZMODIFICATIONDATE DESC LIMIT 100
|
|
''')
|
|
apple_epoch = 978307200
|
|
notes = []
|
|
for row in cur.fetchall():
|
|
body = ''
|
|
if row[2]:
|
|
try:
|
|
body = row[2].decode('utf-8', errors='replace')[:500]
|
|
except Exception:
|
|
body = '[binary data]'
|
|
date_readable = ''
|
|
if row[3]:
|
|
try:
|
|
date_readable = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(row[3] + apple_epoch))
|
|
except (ValueError, OSError):
|
|
pass
|
|
notes.append({'id': row[0], 'title': row[1] or '', 'body': body, 'date': date_readable})
|
|
conn.close()
|
|
return {'success': True, 'notes': notes, 'count': len(notes)}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def list_backup_files(self, backup_path, domain='', path_filter=''):
|
|
"""List all files in a backup's Manifest.db."""
|
|
manifest = os.path.join(backup_path, 'Manifest.db')
|
|
if not os.path.exists(manifest):
|
|
return {'success': False, 'error': 'Manifest.db not found'}
|
|
try:
|
|
conn = sqlite3.connect(manifest)
|
|
cur = conn.cursor()
|
|
query = 'SELECT fileID, domain, relativePath, flags FROM Files'
|
|
conditions = []
|
|
params = []
|
|
if domain:
|
|
conditions.append('domain LIKE ?')
|
|
params.append(f'%{domain}%')
|
|
if path_filter:
|
|
conditions.append('relativePath LIKE ?')
|
|
params.append(f'%{path_filter}%')
|
|
if conditions:
|
|
query += ' WHERE ' + ' AND '.join(conditions)
|
|
query += ' LIMIT 500'
|
|
cur.execute(query, params)
|
|
files = []
|
|
for row in cur.fetchall():
|
|
files.append({
|
|
'hash': row[0], 'domain': row[1],
|
|
'path': row[2], 'flags': row[3],
|
|
})
|
|
conn.close()
|
|
return {'success': True, 'files': files, 'count': len(files)}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def extract_backup_file(self, backup_path, file_hash, output_name=None):
|
|
"""Extract a specific file from backup by its hash."""
|
|
src = os.path.join(backup_path, file_hash[:2], file_hash)
|
|
if not os.path.exists(src):
|
|
return {'success': False, 'error': f'File not found: {file_hash}'}
|
|
out_dir = self._base / 'recon' / 'extracted'
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
dest = str(out_dir / (output_name or file_hash))
|
|
shutil.copy2(src, dest)
|
|
return {'success': True, 'path': dest, 'size': os.path.getsize(dest)}
|
|
|
|
# ── Filesystem ───────────────────────────────────────────────────
|
|
|
|
def mount_filesystem(self, udid, mountpoint=None):
|
|
"""Mount device filesystem via ifuse."""
|
|
if 'ifuse' not in self._tools:
|
|
return {'success': False, 'error': 'ifuse not installed'}
|
|
if not mountpoint:
|
|
mountpoint = str(self._base / 'mnt' / udid)
|
|
os.makedirs(mountpoint, exist_ok=True)
|
|
stdout, stderr, rc = self._run('ifuse', ['-u', udid, mountpoint])
|
|
return {'success': rc == 0, 'mountpoint': mountpoint, 'output': (stderr or stdout).strip()}
|
|
|
|
def mount_app_documents(self, udid, bundle_id, mountpoint=None):
|
|
"""Mount a specific app's Documents folder via ifuse."""
|
|
if 'ifuse' not in self._tools:
|
|
return {'success': False, 'error': 'ifuse not installed'}
|
|
if not mountpoint:
|
|
mountpoint = str(self._base / 'mnt' / udid / bundle_id)
|
|
os.makedirs(mountpoint, exist_ok=True)
|
|
stdout, stderr, rc = self._run('ifuse', ['-u', udid, '--documents', bundle_id, mountpoint])
|
|
return {'success': rc == 0, 'mountpoint': mountpoint, 'output': (stderr or stdout).strip()}
|
|
|
|
def unmount_filesystem(self, mountpoint):
|
|
"""Unmount a previously mounted filesystem."""
|
|
try:
|
|
subprocess.run(['fusermount', '-u', mountpoint], capture_output=True, timeout=10)
|
|
return {'success': True, 'mountpoint': mountpoint}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
# ── Provisioning Profiles ────────────────────────────────────────
|
|
|
|
def list_profiles(self, udid):
|
|
"""List provisioning profiles on device."""
|
|
stdout, stderr, rc = self._run_udid('ideviceprovision', udid, ['list'], timeout=15)
|
|
if rc != 0:
|
|
return {'success': False, 'error': (stderr or stdout).strip(), 'profiles': []}
|
|
profiles = []
|
|
current = {}
|
|
for line in stdout.split('\n'):
|
|
line = line.strip()
|
|
if line.startswith('ProvisionedDevices'):
|
|
continue
|
|
if ' - ' in line and not current:
|
|
current = {'id': line.split(' - ')[0].strip(), 'name': line.split(' - ', 1)[1].strip()}
|
|
elif line == '' and current:
|
|
profiles.append(current)
|
|
current = {}
|
|
if current:
|
|
profiles.append(current)
|
|
return {'success': True, 'profiles': profiles, 'count': len(profiles)}
|
|
|
|
def install_profile(self, udid, profile_path):
|
|
"""Install a provisioning/configuration profile."""
|
|
if not os.path.isfile(profile_path):
|
|
return {'success': False, 'error': f'File not found: {profile_path}'}
|
|
stdout, stderr, rc = self._run_udid('ideviceprovision', udid,
|
|
['install', profile_path], timeout=15)
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
def remove_profile(self, udid, profile_id):
|
|
"""Remove a provisioning profile."""
|
|
stdout, stderr, rc = self._run_udid('ideviceprovision', udid,
|
|
['remove', profile_id], timeout=15)
|
|
return {'success': rc == 0, 'output': (stdout or stderr).strip()}
|
|
|
|
# ── Port Forwarding ──────────────────────────────────────────────
|
|
|
|
def port_forward(self, udid, local_port, device_port):
|
|
"""Set up port forwarding via iproxy (runs in background)."""
|
|
if 'iproxy' not in self._tools:
|
|
return {'success': False, 'error': 'iproxy not installed'}
|
|
try:
|
|
proc = subprocess.Popen(
|
|
[self._tools['iproxy'], '-u', udid, str(local_port), str(device_port)],
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
)
|
|
time.sleep(0.5)
|
|
if proc.poll() is not None:
|
|
_, err = proc.communicate()
|
|
return {'success': False, 'error': err.decode().strip()}
|
|
return {'success': True, 'pid': proc.pid,
|
|
'local': local_port, 'device': device_port}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
# ── Device Fingerprint ───────────────────────────────────────────
|
|
|
|
def full_fingerprint(self, udid):
|
|
"""Get comprehensive device fingerprint."""
|
|
fp = self.device_info(udid)
|
|
# Add specific domains
|
|
for domain in ['com.apple.disk_usage', 'com.apple.mobile.battery',
|
|
'com.apple.mobile.internal', 'com.apple.international']:
|
|
domain_info = self.device_info_domain(udid, domain)
|
|
if 'error' not in domain_info:
|
|
fp[f'domain_{domain.split(".")[-1]}'] = domain_info
|
|
return fp
|
|
|
|
def export_recon_report(self, udid):
|
|
"""Export full reconnaissance report."""
|
|
out_dir = self._udid_dir('recon', udid)
|
|
report = {
|
|
'udid': udid,
|
|
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
'device_info': self.device_info(udid),
|
|
'pair_status': self.validate_pair(udid),
|
|
'apps': self.list_apps(udid),
|
|
'profiles': self.list_profiles(udid),
|
|
}
|
|
report_path = str(out_dir / f'report_{int(time.time())}.json')
|
|
with open(report_path, 'w') as f:
|
|
json.dump(report, f, indent=2, default=str)
|
|
return {'success': True, 'report_path': report_path}
|
|
|
|
|
|
# ── Singleton ──────────────────────────────────────────────────────
|
|
|
|
_manager = None
|
|
|
|
def get_iphone_manager():
|
|
global _manager
|
|
if _manager is None:
|
|
_manager = IPhoneExploitManager()
|
|
return _manager
|