Artículos en profundidad sobre la tecnología que da forma al futuro.

Jailbreak de Switch 2: la cadena que rompió Secure Boot

El jailbreak de Switch 2 encadenó un overflow en BASIC, un fallo en fs y una race en el loader. Lo que enseña sobre cadenas de exploits y secure boot.

Una consola portátil conectada a hardware de control remoto, con una palanca robótica pulsando su botón de encendido sobre una mesa de trabajo.
Los equipos modernos de hacking de consolas combinan KVM-over-IP con una palanca Bluetooth que pulsa físicamente el botón de encendido después de cada crash.

Una consola recién salida de fábrica trae un microkernel endurecido, autenticación de punteros en cada dirección de retorno, memoria solo ejecutable, control-flow integrity y una cadena de arranque firmada. Dos semanas después, un hobbyista ejecuta código nativo arbitrario como el servicio de sistema de archivos, el proceso más confiable de la máquina. Esa es la historia del primer jailbreak público de la Switch 2, y debería incomodar a todo ingeniero que construya sistemas seguros. No porque las mitigaciones fallaran (la mayoría funcionó exactamente como se diseñó), sino porque el exploit nunca necesitó vencerlas de frente. Las rodeó, un error de lógica a la vez.

Llevo años en el lado defensivo de la seguridad de infraestructura, y el patrón aquí es uno que he visto en revisiones de incidentes en producción más veces de las que me gustaría: una cadena de errores individualmente poco impresionantes, cada uno ganando el privilegio justo para llegar al siguiente. El punto de apoyo inicial ni siquiera estaba en la Switch 2. Fue un bug de corrupción de memoria en un juego de la Switch 1, el intérprete SmileBASIC, ejecutándose en modo de compatibilidad con versiones anteriores. Desde ahí, la cadena subió por el servicio de sistema de archivos, abusó de una race condition en el cargador de programas y terminó con la capacidad de ejecutar código nativo bajo la identidad de cualquier sysmodule. Vale la pena revisar cada eslabón, porque cada uno corresponde a un error que he visto cometer a equipos en sistemas del lado del servidor también.

Por qué la puerta del navegador estaba cerrada y la del juego no

WebKit es el primer objetivo clásico en los jailbreaks de consolas, y el investigador sí consiguió un punto de apoyo ahí mediante un captive portal y un n-day. Pero se quedó sin salida rápido. La pila del navegador de la Switch 2 está envuelta en tres mitigaciones que se combinan brutalmente bien: la memoria solo ejecutable (XOM) hace que, incluso con lectura y escritura arbitrarias, no puedas localizar gadgets de código útiles; la autenticación de punteros (PAC) impide forjar un puntero de código sin un oráculo de firma; y CFI hace que incluso un puntero firmado válidamente no pase las comprobaciones de tipo en la mayoría de los call sites. Romper cualquiera de ellas es un proyecto de investigación. Romper las tres, encadenadas, desde un sandbox de JavaScript, es una carrera profesional.

Así que el ataque pivotó hacia el inquilino más débil de la máquina: los juegos de terceros de Switch 1. El razonamiento fue frío y correcto. Los binarios de juegos los construyen los estudios, no el equipo de seguridad de Nintendo, así que es mucho menos probable que se compilen con la pila completa de mitigaciones. Además, la Switch 1 nunca tuvo PAC, por lo que todo lo que corre en su entorno de compatibilidad hereda esa debilidad. Y mejor aún: la raíz de confianza de la Switch 1 fue comprometida hace años, lo que significa que su código de juegos puede descifrarse y auditarse sin conexión. SmileBASIC, un intérprete de BASIC gigantesco escrito en C++ que ya había sido explotado en la 3DS, era el objetivo blando evidente.

Hay una lección aquí que va más allá de las consolas: tu postura de seguridad es la postura de seguridad del código menos endurecido que aceptas ejecutar. La compatibilidad hacia atrás es una superficie de ataque que heredas, no una que eliges. El modelo de seguridad de Xbox aprendió esto por las malas en los 2000: los exploits de partidas guardadas en títulos sin endurecer fueron la cuña que abrió un kernel mucho más cerrado, una historia que cubrimos en lo que el hack de la Xbox original enseña sobre los modelos de seguridad de hardware. Veinte años después, la misma jugada sigue funcionando.

El desbordamiento de entero que abrió la primera puerta

El bug de SmileBASIC es un desbordamiento de entero en la comprobación de límites de manual, el tipo de cosa que cualquier checklist de revisión de código seguro dice que hay que detectar, y que sigue llegando a producción de todos modos. El comando de eliminación de arrays del intérprete validaba sus argumentos así:

// intent: ensure the removed range is inside the array
if (position + count <= array.length) {
memmove(backing + position * 8,          // start of the gap
backing + (position + count) * 8, // elements after the gap
(length - position) * 8);          // bytes to copy
}

Al llamar a REMOVE R, &H60000000, &H3FFFFFFF sobre un array de cuatro elementos, position + count da la vuelta y se convierte en 0x9FFFFFFF, que como valor con signo de 32 bits es muy negativo, es decir, claramente «menor» que la longitud. Las multiplicaciones por 8 también desbordan, y terminas ejecutando memmove(backing, backing - 8, 32). El comando muestra después un error de memoria insuficiente, pero la copia ya se había hecho. Y aquí viene lo bonito: los 8 bytes inmediatamente anteriores a cualquier array de SmileBASIC contienen un puntero al propio array. Tras la eliminación, R[0] se interpreta como una cadena cuyo campo de longitud es en realidad ese puntero: una cadena astronómicamente larga por la que puedes leer y escribir. Lectura y escritura arbitrarias, servidas en bandeja por la propia disposición de memoria del intérprete.

Si escribes C o C++ para ganarte la vida, mira esa comprobación un minuto. position + count <= length es el idioma más común para validar límites en código de sistemas, y es incorrecto siempre que la suma pueda desbordarse. La forma correcta es position <= length && count <= length - position, o aritmética con comprobación de desbordamiento. He encontrado variantes de este bug en un parser de paquetes en un trabajo anterior y en un motor de almacenamiento de código abierto. Sobrevive a la revisión de código porque parece obviamente correcto. Es la brecha entre especificación e implementación en miniatura: la especificación dice «valida el rango», la implementación valida algo adyacente al rango, y la diferencia es explotable. Ya escribimos sobre cómo la brecha entre especificación e implementación es donde viven los bugs; los intérpretes de consolas son simplemente donde los encuentran adolescentes motivados en vez de fuzzers.

Escalar hasta el servicio de sistema de archivos con un desbordamiento en el manejo de rutas

La lectura y escritura en espacio de usuario dentro del proceso de un juego no te sirve de nada por sí sola: el diseño de microkernel de Switch hace que cada servicio sea un proceso separado con su propia superficie IPC. El objetivo valioso era fs, el sysmodule de sistema de archivos integrado, por una razón deliciosamente recursiva: fs es el componente que sirve código descifrado al cargador. Si controlas fs, puedes volcar el código de cualquier otro sysmodule para analizarlo sin conexión. El intendente de la fortaleza te entrega los planos.

El fallo era un desbordamiento de heap en la normalización de rutas. El comando IPC GetEntryType normaliza su argumento de ruta, y las rutas que contienen un componente de directorio padre con barra invertida, /..\, entran en un helper que reserva un búfer fijo de 0x301 bytes y luego copia la ruta en él sin una comprobación de longitud adecuada. Una ruta suficientemente larga desborda el búfer hacia objetos adyacentes en el heap. El comentario del propio investigador al descubrirlo fue una sola palabra: «lol».

Ese «lol» debería escocer, porque la normalización de rutas es exactamente el tipo de plomería poco vistosa que nunca recibe el escrutinio de seguridad que sí recibe la criptografía. El exploit convirtió el desbordamiento en lectura y escritura arbitrarias corrompiendo un objeto de caché vecino cuyo bit dirty redirige la E/S de archivos a través de un puntero controlado por el atacante: una primitiva limpia de heap grooming. Luego, con escaneo de memoria, el exploit encontró el objeto de conexión de su propio proceso en las tablas de fs y reescribió el ID de proceso almacenado a 1, concediendo al proceso del juego «FullPermission» sobre el sistema de archivos. Fíjate en lo que acaba de pasar: no se rompió ni una sola comprobación criptográfica. El modelo de permisos se aplicaba mediante datos en memoria, y el atacante podía escribir memoria.

Corte isométrico de una fortaleza de varios pisos con una pequeña figura saltándose puertas cerradas por un estrecho corredor de servicio.
Cada etapa de la cadena superó puertas endurecidas caminando por un pasillo que nadie pensó en vigilar.

Superar las comprobaciones de firma intercambiando el sistema de archivos en pleno vuelo

El eslabón más ingenioso de la cadena es el que derrota la firma de código sin tocar ni una sola clave. El cargador de programas (ldr) verifica dos firmas antes de ejecutar nada: la de Nintendo sobre los metadatos de permisos del proceso y la clave del desarrollador, y la del desarrollador sobre el hash de la cabecera del archivo de contenido. En teoría, ese hash fija transitivamente cada byte del ejecutable. En la práctica, hay un hueco: es trabajo de fs asegurarse de que el sistema de archivos que sirve coincida realmente con el hash que ya reportó, y el atacante tenía lectura y escritura arbitrarias dentro de fs.

Así que el exploit espera hasta que el cargador ha pedido el sistema de archivos del ejecutable y fs ya le ha entregado el hash válido y la firma, y entonces cambia el objeto de sistema de archivos detrás del handle IPC del cargador por uno manipulado que sirve el archivo de metadatos firmado auténtico pero un ejecutable de reemplazo arbitrario. Las comprobaciones de firma pasan porque comprobaban bytes que el atacante nunca necesitó modificar. La sincronización se impone reescribiendo el handle en el que fs espera la conexión del cargador, para que apunte a un evento que controla el atacante desde el proceso del juego: una race condition convertida en una interposición determinista.

La cadena criptográfica era sólida desde el hash de la cabecera hasta el contenido del ejecutable, sobre el papel. El único eslabón que existía solo como supuesto en tiempo de ejecución, «el proceso que sirve los archivos es honesto», fue el que se rompió.

Esto es un bug de time-of-check/time-of-use disfrazado, y es la lección más transferible de todo el análisis. Cada vez que el componente A valida datos y el componente B actúa después sobre ellos mediante una referencia mutable, tienes una ventana de intercambio. Una vez revisé un sistema interno de despliegues que validaba el digest de una imagen de contenedor y luego descargaba por tag desde el registry en el momento del rollout: mismo bug, otro sombrero. La solución en ambos casos es la misma: validar en el punto de uso, o entregar al consumidor un objeto inmutable y verificado en lugar de un handle a algo en lo que tiene que confiar que un tercero lo mantenga honesto. Un diseño defensivo que asume que todo intermediario está comprometido no es paranoia; en una máquina en la que el atacante ya puede escribir parcialmente, es simplemente modelar con precisión. Ese es el argumento central de la ingeniería defensiva para sistemas que resisten catástrofes, y este exploit es un caso de estudio perfecto.

Tomar prestado un handle del kernel y parchear fs desde dentro

El truco final es pequeño y precioso. Para parchear el propio código del proceso fs, el exploit necesitaba un handle del kernel hacia él. Lo consiguió sobrescribiendo en memoria un campo de handle de 4 bytes con el valor mágico 0xffff8001, que el kernel interpreta como «el handle del proceso actual». El proceso del juego llamó a una API inocua, recibió de vuelta su propio handle de proceso, se lo pasó a un proceso pirata con los permisos del sysmodule de carga de código, y ese proceso usó llamadas legítimas al kernel para mapear y parchear el código de fs. Cada operación individual estaba autorizada. La composición fue un compromiso total del proceso más confiable del dispositivo.

Si esto suena familiar, es porque los problemas de confused deputy aparecen en todas partes en cuanto empiezas a mirar: reglas de sudo que permiten un binario inocuo con un archivo de configuración escribible, roles IAM en la nube que pueden pasarse a sí mismos a un servicio más poderoso, sistemas de build que firman lo que les entrega el job de CI. Cubrimos el lado de escalada de privilegios de este patrón en la historia de la escalada de privilegios en Unix: cambian los mecanismos, pero la forma del bug no. Los handles, las capacidades y los tokens son tan fuertes como la integridad de la memoria que los almacena.

Lo que los diseñadores de Secure Boot siguen haciendo mal

Si das un paso atrás, el patrón de toda la cadena es consistente, y es el mismo que hay detrás de la mayoría de las brechas empresariales graves que he revisado: el núcleo verificado aguantó, y todo lo que lo rodeaba no. La ROM de arranque de Nintendo, el esquema de firmas y las mitigaciones del kernel estaban bien. Lo que cayó fue una comprobación de límites en el intérprete, un búfer de normalización de rutas, un campo de permisos almacenado en memoria escribible, una ventana TOCTOU entre dos servicios de confianza y una tabla de handles que tomaba punteros del kernel desde estructuras escribibles desde el espacio de usuario. Nada de esto es exótico. Todo ello es el tipo de cosa que sobrevive a la revisión porque cada parte parece razonable localmente.

  • Audita el código aburrido. Los normalizadores de rutas, las comprobaciones de longitud y las tablas de handles son donde realmente empiezan las cadenas de exploits, no la criptografía. Dedica tiempo de revisión proporcional a la alcanzabilidad por parte del atacante, no a la sofisticación algorítmica.
  • Trata cada brecha de mitigación como una hoja de ruta. Si un proceso del sistema carece de PAC/CFI (una capa de compatibilidad, un binario de terceros, un intérprete heredado), asume que es el punto de entrada y modela a qué puede llegar.
  • Elimina la confianza por posición. Un componente que valida datos y pasa un handle mutable hacia abajo ha creado una ventana de intercambio. Pasa objetos inmutables y autoverificables, o vuelve a verificar en el momento del uso.
  • Asume que la memoria escribible en cualquier parte es memoria escribible en todas partes. Los flags de permisos, los campos de PID y los valores de handles almacenados en memoria alcanzable por el atacante forman parte de tu superficie de ataque, no de tu mecanismo de aplicación.

La otra conclusión incómoda es la de las herramientas. El exploit se desarrolló con un agente de IA al que se le dio acceso físico al hardware mediante dispositivos KVM-over-IP: vídeo de entrada, entrada de mando por USB y un gadget de palanca Bluetooth para pulsar el botón de encendido tras cada crash. Varias instancias del agente trabajaron en paralelo sobre distintas superficies de ataque, día y noche, durante dos semanas. Pienses lo que pienses sobre la investigación de seguridad asistida por IA, la economía del descubrimiento de vulnerabilidades en hardware cerrado acaba de cambiar. Los bugs que antes requerían un experto paciente ahora requieren un experto paciente capaz de supervisar a tres asistentes incansables. Si tu modelo de amenazas asumía que «nadie se molestará en hacer ingeniería inversa de este sysmodule», esa suposición caducó este mes.

La cadena es el producto

Es tentador leer un write-up de jailbreak como una lista de errores de Nintendo. Léelo, en cambio, como un espejo. Cada sistema que he ayudado a defender tenía un equivalente de SmileBASIC (el componente heredado que nadie endureció), un equivalente del búfer de rutas (el código de plomería que superó sus supuestos) y un equivalente de la race del cargador (la validación que confió en un intermediario mutable). La ventaja de los atacantes no es la brillantez en ningún paso concreto: es que piensan en cadenas mientras los defensores piensan en componentes.

Así que el ejercicio para tu equipo esta semana no es «¿podría alguien falsificar nuestras firmas?». Es: elige tu servicio interno más confiable, asume que el atacante ya tiene escritura arbitraria en el proceso menos endurecido que habla con él, y recorre la cadena hacia adelante. ¿Dónde aterriza la primera escalada? ¿A qué puede llegar esa identidad? Encontrarás tu propio sysmodule de fs más pronto de lo que crees. Mejor tú que alguien con un KVM, una palanca Bluetooth y dos semanas de paciencia.