
Extractor de firmware para microprocesadores CH55x
El extractor de firmware CH55x se utiliza para leer el firmware de los circuitos integrados CH55x. No hay soporte integrado en los dispositivos para leer directamente el firmware a través del bootloader. Sin embargo, existe una funcionalidad para verificar el contenido del firmware de 8 bytes a la vez contra los datos proporcionados. Esta funcionalidad es vulnerable a un ataque de temporización (timing attack) de libro de texto y es lo que se explota aquí para permitir la extracción del firmware de estos dispositivos. Se puede acceder al bootloader mediante USB o UART. Este extractor de firmware solo funciona a través de UART, porque la mayor latencia del USB dificultaría este ataque. Los chips probados son CH552 y CH554, y las versiones del bootloader 2.4 y 2.5. La solución de hardware para el extractor de firmware se basa en un STM32 Blue Pill, porque son fácilmente disponibles, baratos y tienen el rendimiento necesario.
El bootloader ha sido previamente leído del dispositivo y su protocolo de comunicación ha sido sometido a ingeniería inversa. A continuación se muestra un comando de verificación aproximadamente correcto y la función de verificación utilizada en el bootloader. Hay algunas salvaguardas que requieren que la longitud sea un múltiplo de 8, que la dirección esté alineada a un límite de 8 bytes, que la dirección sea inferior a 0x3800 y que no haya errores de verificación previos. La última salvaguarda significa que se necesita un reinicio del CH55x después de cada verificación fallida.
Podemos ver que la función de verificación retorna inmediatamente cuando un byte de la verificación falla. Esto significa que cuantos más bytes sean correctos, más tiempo tardará la función de verificación. Este es el ejemplo de libro de texto de un ataque de temporización que se puede explotar.
unsigned char verifycmd[] = {
// 0x57, 0xab, // La magic de UART no se incluye en la función de verificación
0xa6, // Comando de verificación
5 + len, // Constante 5 más la longitud de los datos a verificar
0, // Sin uso
addr_low, // Byte bajo de la dirección
addr_high, // Byte alto de la dirección
0, 0, 0, // Sin uso
0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, // Datos contra los que verificar
checksum
}
unsigned char verify(unsignec char *cmdbuffer)
{
static char prev_verify_error;
unsigned char len = cmdbuffer[1]-5
unsigned short addr = cmdbuffer[3] + cmdbuffer[4] << 8;
if (len & 0x07 || addr & 0x07 || addr > 0x3800 || prev_verify_error) {
return 0xfe;
}
for (int i=0; i < len; i++) {
// La clave se puede establecer a través del bootloader, y CBYTE[] significa memoria de código
if(key[i & 0x07] ^ cmdbuffer[8+i] ^ CBYTE[addr+i]) {
prev_verify_error = 1;
return 0xf5;
}
}
return 0;
}
Mediante prueba y error descubrimos que cada byte correcto extiende el tiempo de ejecución de la función de verificación en aproximadamente 4,2 µs. La velocidad de baudios utilizada por el bootloader para la comunicación UART es 57600 (independientemente de lo que se lea en otros sitios), lo que significa que la transmisión de un bit tarda aproximadamente 17,4 µs. La relación entre estos dos tiempos es importante, porque parece que la respuesta se envía con una fluctuación (jitter) de también ~17 µs. Entonces intentamos distinguir respuestas que difieren en 4,2 µs en el tiempo de respuesta de la función de verificación, pero que pueden diferir hasta 17 µs debido a la fluctuación UART (temporización del reloj). Esto parece una tarea difícil, pero es posible hacerlo mediante métodos estadísticos.
Podemos determinar si un byte fue correcto o no intentando verificarlo varias veces y registrando los resultados. Supongamos, por ejemplo, que el tiempo más corto posible para recibir una respuesta si el primer byte es incorrecto es de 30 µs. Entonces, con la fluctuación UART, podemos esperar un tiempo máximo de respuesta de 30 µs + 17,4 µs = 47,4 µs para un primer byte inválido. Al mismo tiempo, un primer byte válido y un segundo byte inválido desplazarían este "rango" a 34,2 µs a 51,6 µs. Añadiendo algunos márgenes, podemos concluir que el primer carácter fue inválido si el tiempo de respuesta es inferior a ~33 µs. De manera similar, podemos decir que el primer carácter fue válido si el tiempo de respuesta fue superior a 48 µs. Esta es la base del ataque de temporización utilizado para extraer el firmware.
Esta no es una herramienta completamente automática para la extracción de firmware: será necesaria la modificación del código fuente y su recompilación (usando VS Code con PlatformIO). La razón principal es que las características de temporización exactas para cada byte difieren entre configuraciones y deberán ajustarse. El proceso de ajuste podría automatizarse, pero ese no era el objetivo de este proyecto. El ajuste principal se realiza a través de la variable prober_limits. Por ejemplo, prober_limits[0] contiene los límites para el byte 0 de los 8 bytes a verificar. Si el tiempo de respuesta está por debajo de .invalid_under_time, sabemos que el byte era inválido. Si está por encima de .valid_over_time, sabemos que el byte era correcto. También hay un .min_delta que permite avanzar al siguiente byte antes de saber con certeza si el byte es correcto o no.
struct ProberByteLimits prober_limits[8] = {
{
// Byte 0
.invalid_under_time = 33,
.valid_over_time = 50,
.min_delta = 30
}, // ...
Para encontrar los valores adecuados, se recomienda poner .invalid_under_time a 0, .valid_over_time a, p. ej., 100, y .min_delta se puede mantener en 30. Esto significa que el probador no encontrará el primer byte correcto, pero el progreso se imprimirá en la UART del PC anfitrión. Verá algo similar a:
[0x0000]=0x01? min=31 max=47 tries=63
min=31 max=48 tries=127
min=31 max=48 tries=191
min=30 max=48 tries=255
min=30 max=48 tries=319
Suponiendo que el primer carácter probado es inválido, podemos usar estos valores min/max con un desplazamiento de 1 o 2 para determinar los límites adecuados a usar. Por ejemplo, arriba, podríamos poner .invalid_under_time a 33 y .valid_over_time a 50. El probador debería entonces probar diferentes valores hasta encontrar uno válido, lo que se vería así:
...
[0x0000]=0x7d? min=31 max=39 tries= 7
[0x0000]=0x02? min=40 max=56 tries= 7
[0x0001]=0x01? min=35 max=52 tries=34
Vemos que el último intento en la dirección 0x0000 tuvo un tiempo de respuesta máximo de 56 µs, lo que significa que ese era el byte correcto. El probador avanza entonces al siguiente byte y continúa el proceso. Tenga en cuenta que los límites de los 8 bytes deben ajustarse por separado, pero una vez hecho, funcionará para toda la memoria. Cuando se hayan encontrado 8 bytes correctos, los imprimirá en formato ihex:
:0800000002002932ffffffff9f
Registrando la salida UART en un archivo de texto se puede filtrar con ^: para extraer el contenido completo de la memoria en formato ihex.
A continuación se muestra un circuito de ejemplo para la extracción de firmware. Dos transistores permiten apagar la alimentación del CH55x desde el Blue Pill. Esto se recomienda en lugar de usar solo el reinicio por software, porque el reinicio por software solo funciona cuando el CH55x está en modo bootloader (y el bootloader tiene un tiempo de espera tras el cual inicia el código de aplicación). Las resistencias en la UART hacia el CH55x se incluyen porque sospecho que las resistencias internas de pull-up del CH55x podrían realimentar la alimentación desde la UART incluso cuando la alimentación está apagada. La resistencia de 10k entre V33 y P3.6 es necesaria para poner el CH55x en modo bootloader. En el Blue Pill, PA11 está conectado al pin RX de la UART para poder medir el tiempo de la respuesta usando el temporizador 1.

Ajustando y usando esta herramienta, se puede extraer el firmware de los dispositivos CH55X. El proceso de extracción no es rápido, pero extraerá los 14 kb en uno o dos días. Depende un poco del ajuste y de lo bien que coincida la tabla de frecuencias utilizada con el ensamblador real del firmware. Desafortunadamente, el código fuente es un poco un desastre. Hice esta herramienta porque necesitaba el firmware de un dispositivo CH554, y ahora que lo he obtenido, no hay una razón real para seguir trabajando en la herramienta en sí. No obstante, debería resultar útil en caso de que alguien necesite extraer firmware de dispositivos CH55x.