Autarch Will Control The Internet
This commit is contained in:
683
core/iphone_exploit.py
Normal file
683
core/iphone_exploit.py
Normal file
@@ -0,0 +1,683 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user