Empezamos intentando conectarnos al router a través de UART y descubrimos que pide un nombre de usuario y una contraseña para iniciar sesión. Hicimos muchos intentos usando conocido credenciales, pero decidimos que era hora de recurrir al análisis estático para intentar averiguar la contraseña correcta.
Obtener el firmware
Como de costumbre, descargamos el firmware del sitio web del proveedor y ejecutamos Binwalk en él. Lamentablemente, Binwalk no fue capaz de reconocer ningún formato de archivo, por lo que tenemos la impresión de que podría estar cifrado de algún modo. Para verificarlo, decidimos ejecutar el análisis de entropía integrado de Binwalk en la imagen descargada. Los resultados se muestran a continuación:
Esto significa que la imagen tenía un (y alta), lo que significa que lo más probable es que la imagen estuviera cifrada. Pero, como teníamos acceso físico al dispositivo, Emilio pudo desoldar el EEPROM de la placa y leer su contenido. Con esta imagen de firmware recién volcada, volvimos a intentar ejecutar Binwalk en ella y, efectivamente, ¡ahora era capaz de reconocer el contenido y extraer todo el sistema de archivos!
Averiguar la contraseña
Lo primero que hicimos fue volcar el /etc/shadow e intentar descifrar el hash. Tuvimos éxito pero esta contraseña no fue aceptada por el prompt de login de la UART. A juzgar por este comportamiento llegamos a la conclusión de que debe haber algo más con este firmware, así que decidimos echar un vistazo al resto del sistema de archivos y ver si podíamos encontrar algo sospechoso.
Validación de credenciales de inicio de sesión
Tras echar un vistazo a /etc/init.d/rcS encontramos que un binario llamado contraseña_consola se ejecuta una vez finalizada la secuencia de iniciación. Nos sorprendió comprobar que este binario no había sido despojado de sus símbolos. De hecho, ¡todos los binarios tenían símbolos! (gracias NEC por facilitarnos el trabajo). He aquí una versión simplificada del binario descompilado (sólo la función principal):
char usuario_pwd_buff[1024];
int usuario_ok;
printf("Inicio de sesión: ");
gets(usuario_pwd_buff);
// (1)
user_ok = strncmp(user_pwd_buff, "root_cheeper", 13) == 0;
// (2)
printf("Contraseña: ");
gets(user_pwd_buff);
// (3)
if ( !user_ok ) goto HANG;
apmib_get(16102, pwcrypt_arg1);
apmib_get(202, mib202);
sprintf(pwcrypt_arg2, "%.2hhx%.2hhx%.2hhx%.2hhx%.2hhx", mib202[0], mib202[1], mib202[2], mib202[3], mib202[4], mib202[5]);
if ( pipe(pipe_fd) < 0 ) {
puts("error de pipe");
return -1;
}
pid = fork();
if ( pid < 0 ) {
puts("error de bifurcación");
close(pipe_fd[0]);
close(pipe_fd[1]);
return -1;
}
if ( pid ) {
close(pipe_fd[1]);
read(pipe_fd[0], valid_pwd, 1024);
v14 = (_BYTE *)strchr(valid_pwd, 'n');
if ( v14 ) *v14 = 0;
close(pipe_fd[0]);
} else {
close(pipe_fd[0]);
close(1);
dup(pipe_fd[1]);
execlp("pwcrypt", "pwcrypt", pwcrypt_arg1, pwcrypt_arg2, 0);
}
pwd_not_ok = strncmp(user_pwd_buff, valid_pwd, 1024);
// (4)
if ( pwd_not_ok ) {
while ( 1 ) sleep(60);
}
return 0;
Esto puede parecer un montón de código, pero la mayor parte es realmente sencillo (podemos ignorar con seguridad todo lo que hay entre los comentarios (3) y (4) por ahora). Lo esencial es que un nombre de usuario se lee de stdin (1) y, a continuación, se coteja con la cadena “root_cheeper” (2), justo antes de leer una contraseña de stdin (3) y comprobándolo con algún búfer (4). Si alguna de las comprobaciones falla, el programa entra en un bucle infinito (en el punto COLGAR etiqueta). Por supuesto, lo que nos gustaría saber es el valor exacto de la cadena en valid_pwd, que se comparará con la contraseña leída de stdin.
Ahora tenemos que prestar atención a lo que ocurre entre (3) y (4). En pocas palabras, se trata de ejecutar pwcrypt con algunos parámetros obtenidos de apmib_get y leyendo su salida en un búfer. Esto significa que debe existir un binario llamado pwcrypt dentro del sistema de ficheros cuya función es generar una contraseña válida a partir de unos datos de entrada.
Efectivamente, encontramos el pwcrypt binario en el /bin del sistema de archivos. Además, encontramos las siguientes cadenas definidas en él:
"¡¡¡argumento no válido!!!" "pwcrypt versión %d.%d.%dn" "pwcrypt [nombre del producto] [dirección wan mac(12byte)]"
Así que este proceso de generación de contraseñas depende de un binario más, llamado flash, que se supone que recupera una clave de hardware aleatoria de la memoria flash (en realidad no sabemos nada de esto con seguridad, pero dados los nombres tan sugerentes de símbolos y programas que se dan aquí podemos suponer que sí, al menos por el momento).
Si continuáramos el análisis como hasta ahora, ahora nos centraríamos en el flash binario. Pero la ingeniería inversa del flash binario es algo pesado, ya que implica muchas llamadas indirectas a funciones y mucho código (como referencia, contraseña_consola tenía un tamaño de unos 4 KB y pwcrypt 9KB, pero flash es un binario de 84 KB). Está claro que intentar leer el código de principio a fin no funcionaría aquí, así que vamos a divagar un rato.
Volvamos una vez más al listado de código de contraseña_consola, el que se ha dado antes. Ahora sabemos que se llama pwcrypt, y que pwcrypt espera recibir tanto el nombre del modelo del dispositivo como su dirección MAC. Pero, ¿cómo contraseña_consola obtener esa información? El código correspondiente se incluye aquí una vez más para mayor comodidad:
apmib_get(16102, pwcrypt_arg1);
apmib_get(202, pwcrypt_arg2);
sprintf(pwcrypt_arg2, "%.2hhx%.2hhx%.2hhx%.2hhx%.2hhx", mib202[0], mib202[1], mib202[2], mib202[3], mib202[4], mib202[5]);
// ...
execlp("pwcrypt", "pwcrypt", pwcrypt_arg1, pwcrypt_arg2, 0);
Lo que esto nos está diciendo es que apmib_get se encarga de alguna manera de recuperar valores relacionados con el dispositivo de la memoria no volátil. Esta funcionalidad está extrañamente relacionada con la que esperábamos de flash, ¿verdad? Además de eso, flash está vinculado contra libapmib que es la biblioteca que define este apmib_get función. Teniendo en cuenta todo ese contexto, tiene sentido ir a ver libapmib para intentar averiguar cómo se leen todos estos valores de la memoria (¡y de qué tipo de memoria se leen!).
Profundizando en libapmib
Decidimos echar un vistazo a libapmib.so y vea cómo apmib_get funciona. Por suerte, como teníamos símbolos de depuración, pudimos ver esto al principio de la función (nótese que hemos nombrado los parámetros porque podemos deducir su semántica de los usos anteriores de esta función):
int apmib_get(int numeric_id,int *out) {
// declaraciones de variables locales...
if (func1(numeric_id,&mib_table,&local_24))) {
Var0 = &mib_table;
} else if (func1(numeric_id,&hwmib_table,&local_24))) {
Var0 = &hwmib_table; }
// más código...
Con esta información nos dimos cuenta de que existe una tabla en memoria que almacena información relacionada con las claves enteras que vimos anteriormente (por ejemplo, al ver contraseña_consola sabemos que la clave para la dirección mac es 202). Además, hay varias tablas, y se buscan en un orden específico cuando apmib_get comienza a buscar un identificador solicitado.
Tras inspeccionar la disposición de estas tablas en la memoria, observamos el siguiente patrón:

Tenga en cuenta que ya hemos añadido algo de información tipográfica, que hemos averiguado echando un vistazo a cómo se utilizaban los elementos de esta tabla dentro de apmib_get.
Después de definir los tipos apropiados, el código quedó así:
int apmib_get(int numeric_id,int *out) {
// declaraciones de variables locales
if (get_tbl_idx(numeric_id,&mib_table,&idx))) {
metadata_tbl = &mib_table;
data_tbl_ptr_ptr = &pMib
} else if (get_tbl_idx(numeric_id,&hwmib_table,&idx)) {
metadata_tbl = &hwmib_table;
data_tbl_ptr_ptr = &pHwSetting;
}
metadata_tbl_entry = &metadata_tbl[idx];
data_tbl_ptr = *data_tbl_ptr_ptr;
switch (metadata_tbl_entry->type) {
// ...
caso 7:
*(_DWORD *)result = *(_DWORD *)(data_tbl_ptr + metadata_tbl_entry->offset);
// ... } // más código
Hemos añadido mucha información a este listado, así que tendremos que hablar un poco de dónde ha salido todo esto.
- Sabíamos que el 3er campo de cada entrada de la tabla indicaba el tipo de la entrada porque, en función de su valor, el valor de resultado se fija de diferentes maneras.
- También sabemos que el primer campo es el id porque es el valor que se está comparando con el primer parámetro de entrada (que si recuerdas, indica la clave a buscar).
- Finalmente, como se puede ver en el 7º caso dentro del switch, los datos que se copian a resultado viene de añadir el 4º campo de la entrada a un puntero dado que denominamos data_tbl_ptr. Por eso hemos decidido llamar a este campo `offset`.
En el fragmento anterior, incluimos sólo el 7º caso del conmutador porque éste es el tipo de entrada de CLAVE_ALEATORIA, que se parece sospechosamente a HW_RANDOM_KEY de antes (de hecho, cuando se utiliza flash get para recuperar HW_RANDOM_KEY, rayas flash el HW_ y sólo busca CLAVE_ALEATORIA que hwmib_table).
Después de calcular todo esto, sólo nos faltaba el valor real de CLAVE_ALEATORIA. Sabíamos que se encuentra en offset 0xD en la tabla pHwSetting, pero nos faltaba el contenido de esa tabla.
Para obtener el contenido de ese búfer, echamos un vistazo a todas las funciones en las que pHwSetting y descubrí que se hacía referencia a él desde apmib_get, apmib_set, apmib_actualizar, apmib_init, apmib_init_HW. De esas funciones, las que parecían más susceptibles de darnos una pista sobre el contenido de pHwSetting eran los que contenían init en el nombre. Rápidamente nos dimos cuenta de que ambos llamaban a una función llamada apmib_hwconf, que parecía estar rellenando el buffer.
Tras la ingeniería inversa apmib_hwconf durante un rato conseguimos descubrir que el contenido del buffer estaba siendo descomprimido desde flash y copiado a RAM. Tras conseguir descomprimir esta imagen, pudimos leer el valor de CLAVE_ALEATORIA utilizando el desplazamiento previamente definido. Para automatizar este proceso, escribimos una herramienta personalizada que puede encontrar en aquí
Ponerlo todo junto
Recapitulemos lo que hemos hecho hasta ahora:
- Para obtener un shell en el dispositivo, un binario llamado
contraseña_consola comprueba la contraseña introducida por el usuario con la devuelta porpwcrypt (que se llama utilizando dos valores conocidos). pwcrypt, a su vez, llama aflash get HW_RANDOM_KEYpara recuperar la clave aleatoria de flash.- Y sabíamos el valor que
flash get HW_RANDOM_KEYdebería devolver para generar la contraseña válida, porque hemos conseguido descomprimir la configuración almacenada en la memoria flash y calcular el offset en el que se supone que debe residir la clave aleatoria de 4 bytes.
En otras palabras, tenemos todo lo que necesitamos para calcular nosotros mismos la contraseña correcta. Sólo necesitamos configurar un entorno que nos permita ejecutar binarios MIPS, y alguna forma de falsear el acceso a la memoria flash.
La primera tarea se logró fácilmente utilizando el modo de usuario qemu y chroot, pero el segundo fue un poco más complicado. Desde pwcrypt sólo utiliza flash una vez para obtener la clave aleatoria de hardware, sustituimos flash con un script bash que siempre devuelve el valor correcto para HW_RANDOM_KEY. A continuación, ejecutamos pwcrypt con el nombre del dispositivo y WAN mac como argumentos para obtener la contraseña correcta.
El proceso exacto puede verse en este vídeo
¡らくらくshellスタート!
Después de todo este esfuerzo, Emilio se dio cuenta de que había un código QR en la parte trasera del dispositivo. Resulta que la gente sube fotos de sus dispositivos y redacta la información que considera sensible, por ejemplo la contraseña WiFi por defecto.
Pero Emilio descubrió que el código QR contiene mucha información. Si lo escaneas con el móvil, te llevará a una página diseñada para agilizar la configuración: Easy QR start (らくらくQRスタート).

El caso es que para configurar el dispositivo, la URL transmite mucha información. Adivina qué información hay, entre otras cosas... La HW_RANDOM_KEY, La dirección MAC de la WAN y el nombre del dispositivo. Así que sólo escaneando este código QR, podríamos haber generado la contraseña shell sin volcar ningún firmware. Hicimos otra herramienta para automatizar este proceso, que que puede encontrar aquí
Créditos:

