
El dispositivo en cuestión es cualquier SSD de la serie MX500. Estos SSD están controlados por un controlador Sillicon-Motion SM2259 (los lotes antiguos tenían un controlador más antiguo, Sillicon-Motion SM2258, pero el enfoque principal de este documento es el más reciente).
SM2259 es un micro-controlador SATA 6Gb/s de 4 canales que cuenta con una CPU de 32 bits little-endian basada en la arquitectura ARC.
Al observar el firmware más reciente aplicable a la fecha de este documento, que es M3CR046, se identificaron y confirmaron varios problemas, tanto estática como dinámicamente.
Todos los problemas fueron identificados en el mecanismo de actualización de firmware del controlador, que corresponde al manejador del micro-controlador del comando ATA PIO DOWNLOAD-MICROCODE (0x92), específicamente en la lógica que descarga el firmware usando el método de offsets, que corresponde a los subcomandos 0x03 y 0x0E.
Todos los bugs cubiertos en este documento fueron verificados en un SSD Crucial MX500 de 500GB (CT500MX500SSD1), con controlador SM2259H-AC ejecutando el FW M3CR046 con chips de memoria flash NY112, usando un PC con CPU x86_64.
El código del FW está mapeado a la dirección base 0x80020000, y el manejador ATA vulnerable se encuentra en la dirección 0x80024A9C. Una versión descompilada de la función se puede encontrar en resources/download_microcode_handler.c para su conveniencia.
Para aquellos que prefieran no lidiar con los detalles técnicos y solo quieran entender el resultado final, por favor consulte la sección de Preguntas Frecuentes más abajo.
Como M3CR046 contiene múltiples imágenes de firmware de las cuales el mecanismo de actualización de firmware elige la apropiada (quizás dependiendo de los chips de memoria flash reales utilizados, u otras características de hardware), este documento cubrirá los detalles de la primera variante de firmware (ya que es el firmware soportado en nuestro disco específico, y por lo tanto solo pudimos probar esa variante de firmware). Sin embargo, los bugs presentados en este documento parecen aplicar a todas las variantes de firmware, pero puede haber algunas diferencias en los detalles específicos al reproducirlos.
Este problema se refiere a los casos en los que el primer fragmento enviado es de tamaño mayor que 0x200 sectores. Si miramos dentro del manejador de comandos ATA, específicamente en la lógica que se ejecuta cuando el tamaño del fragmento es mayor que el tamaño de sector y cuando el fragmento es el primero que se envía:

Esto establece algunas variables basadas en el siguiente offset (que en nuestro caso, dado que solo hemos enviado un solo fragmento hasta ahora, es la longitud del fragmento en sectores) y en una variable llamada lower_bound_fw_offset que es el offset de bloque (es decir, offset en granularidad de sectores) dentro de la imagen de descarga de entrada donde se espera que se encuentre nuestra imagen de firmware. Este es un valor hardcodeado por variante de firmware, que en nuestro caso (la primera variante), es igual a 0.
En este caso, se produce un underflow al calcular el resultado de la resta para some_index, haciendo que some_index sea tan alto como 0xFFFF. Este es un comportamiento inesperado, ya que según la lógica que mueve los datos al búfer de descarga:

Observamos que la dirección de origen desde la cual se copian los datos podría no ser válida dado el valor inesperado calculado para some_index.
Al probar esto dinámicamente enviando una solicitud de actualización de firmware con el primer fragmento de tamaño mayor que 0x200 sectores, el controlador se cuelga y ni siquiera envía una respuesta a la solicitud original. Esto es consistente y fácilmente reproducible.
Es probable que esto ocurra debido a una referencia inválida a la dirección de origen calculada, lo que luego desencadena una excepción que causa el cuelgue del controlador. Esto no ha sido probado, sino que es una conjetura que podría explicar el cuelgue.
La imagen de descarga de entrada (para M3CR046) es de tamaño 0x242400 bytes, y dentro de esta imagen hay 3 imágenes de firmware internas de las cuales solo una es finalmente escrita a la flash después de un proceso de actualización de firmware, cada una de estas imágenes es de tamaño 0xC0C00 bytes (o 0x606 sectores).
Esto significa que, cuando el mecanismo de actualización de firmware extrae la copia correcta del firmware de la imagen de descarga de entrada, debe verificar que su tamaño no exceda 0xC0C00 bytes.
El controlador efectivamente intenta hacerlo, pero hay algunos casos límite que pueden llevar a un comportamiento inesperado. Echemos un vistazo al siguiente fragmento (que comparte algo de código con el bug anterior):

Si el fragmento actual es de tamaño mayor que 0x200 sectores y no es el primer fragmento en la secuencia, entonces se copiarán 0x200 sectores (0x40000 bytes) a la vez. Luego, hay una verificación cuyo propósito es truncar los bytes sobrantes del número de bytes a copiar si el tamaño total de la imagen de firmware excede higher_bound_fw_offset (que en nuestro caso, es 0x606 sectores, ya que el tamaño del firmware debería ser exactamente este).
Esta lógica tiene sentido en general, pero hay un fallo: si el último fragmento enviado hace que el siguiente offset sea demasiado alto, de modo que el número de bytes sobrantes exceda 0x200 sectores (o 0x40000 bytes), entonces curr_bytes_to_copy obtiene un valor "negativo", que hace underflow hasta aproximadamente ~4GB (~0xFFFFFFFF). Como vimos antes, esta variable se usa para determinar el número de bytes a transferir al búfer de descarga.
Si observamos dentro de r_maybe_some_efficient_data_transfer, vemos el siguiente fragmento de código:

Lo que significa que el tamaño de copia se trunca a 32MB (desde el tamaño de copia original de ~4GB), pero sigue siendo un número grande que también podría causar un comportamiento indefinido si el rango de memoria que comienza en 0x40000000 es de tamaño menor que 32MB.
Al probar esto dinámicamente enviando fragmentos ATA para llegar a un offset de 0x600, y luego enviando un fragmento grande de tamaño 0x207 sectores para provocar el underflow, el controlador se cuelga nuevamente, probablemente debido a un acceso a memoria inválido durante la copia.
Este bug es más interesante que el anterior, porque aunque no tenemos una sobrescritura controlada (sino más bien una sobrescritura grande que posiblemente provoca una excepción que cuelga el controlador), si la función que mueve los datos al búfer de descarga realmente logra transferir esa cantidad de datos antes de fallar (sobrescribiendo el rango de memoria que se encuentra justo después del búfer de descarga en la memoria principal), entonces quizás el comportamiento del manejador de excepciones pueda ser alterado según los datos sobrescritos. Eso posiblemente podría ocurrir si, por ejemplo, el manejador de excepciones lee un puntero del área sobrescrita y luego salta a él (este caso específico no es particularmente probable, pero con un poco más de investigación, algo de ese tipo podría descubrirse).
Como se ha dicho, el tamaño de la imagen de descarga es de 0x242400 (o 0x1212 sectores). El firmware verifica que el tamaño total de la imagen transferida no exceda este tamaño comprobando que el siguiente offset no exceda 0x1212 sectores. Esta verificación tiene sentido, pero el cálculo del siguiente offset es defectuoso:

Si el offset actual es 0x600 sectors, y el siguiente comando ATA a procesar es de tamaño suficientemente grande (digamos 0xFC00 sectores, lo cual está permitido por el estándar ATA), entonces el siguiente offset se desborda (wrap around), de modo que la verificación mencionada no funciona correctamente:

O en otras palabras, en el caso habitual, el mecanismo de actualización de firmware reiniciaría su máquina de estados y devolvería un error, pero si enviamos un fragmento muy grande, continuaríamos procesándolo. El siguiente fragmento de código muestra cómo se realiza la transferencia:

Recordemos en este punto que si el número de sectores a transferir es mayor que 0x200 sectores y el fragmento actual no es el primero, entonces se copian 0x200 sectores a la vez al búfer de descarga. Esto es muy interesante, porque significa que podemos copiar aproximadamente 0x200 sectores (o 0x40000 bytes) más allá del búfer de descarga, sobrescribiendo datos en la memoria principal. Por ejemplo, si el offset actual es 0x605 sectores y suministramos un tamaño de fragmento de 0xF9FB sectores, entonces __next_offset obtiene el valor 0 debido al wrap around. El índice de origen desde el cual comienza la copia es 0, y curr_bytes_to_copy obtiene el valor de 0x40000. Como actualmente estamos en el offset 0x605 sectores, entonces g_blocks_copied obtiene el valor de 0x605. Como el offset actual es de hecho válido (y el siguiente también), entonces se activa la operación de copia al búfer de descarga, causando una sobrescritura masiva de poco menos de bytes más allá del final del búfer de descarga.
Esta es una primitiva fuerte que permite un desbordamiento del búfer del controlador mucho más controlado (que no cuelga el controlador inmediatamente como en los casos anteriores), y puede llevar a la ejecución de código con una certeza mucho mayor que el bug anterior (pero aún así, se necesita más investigación sobre qué exactamente se coloca después del búfer de descarga en la memoria principal para determinar las características de explotación).
Todos estos bugs fueron verificados en una máquina Ubuntu 22.04 de 64 bits usando el controlador SCSI estándar de Linux a través de la interfaz SG_IO. Cabe señalar que para reproducir el Bug #3 con este controlador específico, se deben habilitar las páginas grandes (huge pages) y asignar una sola página de 1GB para la solicitud grande. La razón de esto es que, aparentemente, este controlador requiere que toda la solicitud ATA esté en bloques de memoria física contiguos. Como la solicitud tiene un tamaño cercano a ~30MB, las páginas de 2MB no son suficientes, y por lo tanto las páginas de 1GB son el siguiente (y último) tamaño disponible en nuestro sistema de prueba.
Sin embargo, también se debe señalar que esto no significa que sea un paso necesario para desencadenar el bug, porque quizás hay otros trucos que permiten enviar solicitudes ATA grandes que no hemos cubierto aún. Habilitar las páginas grandes fue simplemente la ruta más rápida para confirmar este bug. Además de esto, el único requisito previo necesario para desencadenar todos estos bugs es tener los permisos necesarios para enviar paquetes ATA (típicamente, acceso root a la PC que se comunica con el controlador).
El código fuente que reproduce todos los bugs mencionados se proporciona como parte de este repositorio. Para el Bug #1 y el Bug #2, el comportamiento esperado es que el disco se cuelgue hasta el próximo ciclo de encendido. Para el Bug #3, el código fuente proporcionado no necesariamente hace que el controlador se cuelgue, pero sí realiza una sobrescritura grande más allá del búfer de descarga.
Como se ha dicho, dado que los bugs fueron verificados en una máquina Ubuntu 22.04 de 64 bits, el proceso de compilación debe realizarse en una máquina similar. No hay garantías para otras distribuciones o sistemas operativos.
Para compilar, ejecute lo siguiente en el directorio raíz del proyecto:
cmake -B build && make
El proceso de compilación genera 3 binarios, todos los cuales estarán disponibles en el directorio build con los nombres CVE_MX500_BUG_1, CVE_MX500_BUG_2 y CVE_MX500_BUG_3, que corresponden a los archivos fuente que desencadenan el Bug #1, el Bug #2 y el Bug #3, respectivamente.
Cada binario espera recibir la ruta del dispositivo del SSD MX500, y debe ejecutarse con privilegios de root. Por ejemplo:
sudo ./build/CVE_MX500_BUG1 /dev/sda
Depende del objetivo final de un posible atacante. Si todo lo que quieren es acceso completo de lectura/escritura al almacenamiento de tu disco, entonces estar dentro de tu PC ya es suficiente. Sin embargo, ¿y si ese atacante quiere llevar esto unos pasos más allá? Si el FW de un disco está firmado digitalmente, entonces el Bug #3 puede permitir a un atacante omitir la verificación de firma del firmware, permitiendo al atacante insertar una carga maliciosa en el firmware del disco. Una vez dentro, esa carga queda muy bien oculta, sobrevive a los formateos del disco e incluso puede asegurarse de sobrevivir a las actualizaciones de firmware del controlador. Lo que tal carga podría hacer realmente está más allá del alcance de este documento, por lo que no se discutirá.
Es probable que la respuesta sea un gran NO. La cantidad de I+D necesaria para llevar a cabo tal ataque es muy alta, y (MUY probablemente) estaría al alcance de actores de amenazas muy serios. A menos que seas buscado por gobiernos, es extremadamente improbable que esto te afecte de alguna manera.
El proveedor no respondió a múltiples correos electrónicos sobre estos problemas durante meses. Para que un CVE sea realmente publicado, se debe proporcionar un enlace público a la CNA asignada. Lamentablemente, enviarles la información de forma privada no es así como funciona.
Los bugs mencionados en este documento fueron descubiertos originalmente en mayo de 2024. Se contactó a Micron múltiples veces desde entonces (a través de su correo de seguridad oficial), y no hubo respuesta de su parte. Se notificó a MITRE en julio de 2024, y se asignó un CVE en agosto de 2024. A finales de agosto de 2024, este repositorio se hizo público (unos días después de que MITRE aprobara el CVE).
Como los firmwares M3CR04X anteriores a M3CR046 ya no están disponibles para descargar, no está claro si están afectados, pero si tuviera que adivinar, diría que sí. En cuanto a versiones aún más antiguas, por ejemplo, M3CR033, según el análisis estático, parece que allí existen bugs muy similares.
El controlador en cuestión, SM2259, está integrado en SSD de otros proveedores también. Es posible que los proveedores modifiquen alguna parte del código del firmware, pero también diría que definitivamente es posible que estos bugs (o otros muy similares) estén presentes en SSD de otros proveedores.
Este CVE ha sido publicado por MITRE. También ha sido analizado por NVD con una puntuación CVSS 3.0 de 6.7 (media).
Si ha identificado imprecisiones o errores en la descripción, o tiene problemas para reproducir estos bugs, por favor contácteme en log1kxd at gmail.com.
0x40000