Introducción
Diversos dispositivos comercializados en el mercado tanto de forma de adquisición por parte de consumidores tanto de forma de alquiler por parte de clientes de proveedores de Internet permiten su administración a través de un portal web. Este portal puede estar limitado en características y si se necesitan configuraciones más avanzados, como la configuración del cortafuegos, con la herramienta iptables, hace necesario el acceso por consola. El sistema operativo de los routers suele ser GNU/Linux.
El acceso a la consola suele estar bloqueado, para evitar problemas con configuraciones incorrectas realizadas por usuarios no experimentados, lo que hace necesario, por ejemplo, acceder al puerto serial retirando la carcasa del dispositivo. En otros casos, se permite el acceso a la consola a través del protocolo SSH (Secure Shell), pero el acceso se encuentra limitado a una consola restringida, con unos comandos predefinidos por el fabricante. Estos dispositivos usualmente disponen de una puerta trasera que permite desplegar una terminal de comandos sh o bash con la introducción de comandos específicos.
En este artículo se va a partir de un archivo de actualización del firmware de un router, que contiene su sistema de archivos completo para mediante la ingeniería inversa de sus archivos binarios, descubrir el flujo de ejecución de la consola restringida para poder obtener la terminal completa. Para ello utilizaremos el programa binwalk para la extracción del firmware y Ghidra para descompilar los archivos binarios.
Extracción del firmware
Partiendo del archivo firmware.bin, extraeremos su sistema de archivos utilizando la herramienta binwalk con el parámetro -e.
$ binwalk -e firmware.bin
Encontramos el sistema extraído en el subdirectorio _firmware.bin.extracted/squashfs-root
$ ls _firmware.bin.extracted/squashfs-root
app bin cfg_upgrade data debug dev etc lib linuxrc mnt opt proc sbin sys tmp usr usrcfg var vmlinux.lz webs
Búsqueda del binario a analizar
El binario debe de desplegar el binario de alguna forma, en este caso debe de utilizar la función system de C para ejecutar el binario /bin/sh o /bin/bash. Podemos buscar entre los archivos extraídos utilizando el programa grep por cadenas, con los parámetros -rn.
$ grep -rn "/bin/sh" .
...
grep: ./_firmware.bin.extracted/squashfs-root-0/lib/private/libcms_cmd.so: coincidencia en fichero binario
...
Entre los resultados encontrados está la biblioteca libcms_cmd.so. En su nombre se encuentra cli (Command-line interface), o interfaz de línea de comandos.
Carga del archivo binario en Ghidra
En los archivos binarios o en las bibliotecas, si han sido compiladas de forma dinámica, cargan código de otros archivos con el objetivo de ahorrar espacio. Por lo que es necesario cargar todos los archivos en el desensamblador y descompilador Ghidra. Para ello creamos un nuevo proyecto y utilizamos la opción File > Batch Import... seleccionando la carpeta squashfs-root. Debemos de ampliar el límite de profundidad de escaneo Depth limit a 10 y pulsar en el botón Rescan. Cuando termine la carga de los archivos abrimos squashfs-root/lib/private/libcmd_cli.so en la herramienta CodeBrowser. Realizamos el análisis con las opciones predeterminadas.
Análisis del archivo libcms_cmd.so
Vamos a utilizar la opción Search > Program Text para buscar por la cadena /bin/sh. Seleccionamos las opciones All Fields y All Blocks. Encontramos una función llamada cli_processHiddenCmd. Esta función llama a la función prctl_spawnProcess pasándole como parámetro /bin/sh, es decir, es desplegada una consola.
memset(&local_16c,0,0x38);
local_164 = 1;
local_16c = "/bin/sh";
local_158 = 1;
local_168 = "-c sh";
local_154 = 2;
local_150 = 0xffffffff;
local_14c = 0x32;
local_1d8 = 0;
local_1d4 = 0;
local_1d0 = 0;
local_1cc = 0;
iVar1 = prctl_spawnProcess(&local_16c,&local_1d8);
if (iVar1 == 0) {
local_1e4 = 1;
local_1e0 = local_1d8;
prctl_collectProcess(&local_1e4,&local_1d8);
return 1;
}
Buscando referencias por la función cli_processHiddenCmd usando la opción de clic derecho en el nombre de la función y References > Find references to, encontramos la función cmsCli_run con el siguiente fragmento de código.
log_log(7,"processInput",0x1bf,"read =>%s<=",local_524);
syslog(2,"[MS]cli Input: %s\n",local_524);
if (local_524[0] != '\0') {
iVar2 = cli_processCliCmd(local_524);
if (((iVar2 == 0x2682) && (iVar2 = cli_processMsCliCmd(local_524), iVar2 != 1)) &&
(iVar2 = cli_processHiddenCmd(local_524), iVar2 != 1)) {
log_log(7,"processInput",0x1e1,"unrecognized command %s",local_524);
printf("%s: command not found\n",local_524);
cmd_fail_times = cmd_fail_times + 1;
if (9 < cmd_fail_times) {
__stream = fopen("/var/enableHiddenCMD","r+");
if (__stream != (FILE *)0x0) {
fclose(__stream);
prctl_runCommandInShellBlocking("rm -rf /var/enableHiddenCMD");
}
backdoor_lock2 = 0;
backdoor_lock1 = 0;
cmd_fail_times = 0;
}
}
Viendo las llamadas a la función log_log y syslog podemos suponer que el comando introducido se guarda en la variable local_524. Vemos que se ejecutan las funciones cli_processMsCliCmd y cli_processHiddenCmd. También observamos que si se introduce un comando incorrecto por más de nueve veces se elimina el archivo /var/enableHiddenCMD y se establecen las variables backdoor_lock2, backdoor_lock1 y cmd_fail_times a 0. Examinamos la función cli_processCliCmd.
if (backdoor_lock1 != '\0') {
iVar2 = strncasecmp(param_1,"getparam sys",8);
if (iVar2 == 0) {
backdoor_lock2 = 1;
}
else {
backdoor_lock1 = '\0';
}
}
Observamos que si el comando introducido es getparam sys se establece la variable backdoor_lock2 a 1. Podemos suponer que si el valor de backdoor_lock2 y backdoor_lock1 es igual a 1 junto a que exista el archivo /var/enableHiddenCMD nos permitirá ejecutar comandos. Si buscamos una función que cambie el valor de backdoor_lock1, encontramos la función processSoftwareCmd.
if (iVar2 == 0) {
printf("SW version : %s\n",*(undefined4 *)(local_198 + 0x20));
printf("Internal SW version : %s\n",*(undefined4 *)(local_198 + 0x24));
printf("Build Timestamp : %s\n",_build_timestamp);
cmsObj_free(&local_198);
}
else {
log_log(3,"processSoftwareCmd",0x18b5,"Could not get device info object, ret=%d",
iVar2);
}
backdoor_lock1 = 1;
return;
Más abajo encontramos, en la ayuda, el comando a ejecutar, software show.
memcpy(aiStack_178,"\nUsage: software show\n software help\n",0x2e);
A continuación, en la función processSYSCmd, que es la función que procesa el comando getparam sys, observamos que si las variables de backdoor tienen un valor distinto a \0 se creará el archivo /var/enableHiddenCMD.
if ((backdoor_lock1 != '\0') && (backdoor_lock2 != '\0')) {
prctl_runCommandInShellBlocking("echo 1 > /var/enableHiddenCMD");
backdoor_lock2 = '\0';
backdoor_lock1 = '\0';
}
Con el archivo creado, aún no podemos ejecutar comandos ni se ha desplegado la consola. Necesitamos ejecutar un comando especial, el cual lo encontramos en la función cli_processHiddenCmd.
local_1e8[0] = 0x7368;
memset(acStack_134,0,0x100);
memcpy(acStack_1c8,"4j7b4c.6mf9ak,1-",0x10);
if ((currPerm & 0xc0) == 0) {
return 0;
}
__stream = fopen("/var/enableHiddenCMD","r+");
if (__stream == (FILE *)0x0) {
iVar1 = strncasecmp((char *)param_1,(char *)local_1e8,2);
if (iVar1 != 0) {
return 0;
}
}
else {
fclose(__stream);
}
iVar1 = strncasecmp((char *)param_1,acStack_1c8,__n);
El comando a ejecutar 4j7b4c.6mf9ak,1-, siempre que exista el archivo /var/enableHiddenCMD. A continuación encontramos la solicitud de una contraseña.
sprintf(acStack_1a0,"%s%s","lo5c0poilo.2fj1",&mac3);
...
while( true ) {
local_1b8[0] = '\0';
pcVar3 = getpass("shell Password: ");
if (pcVar3 != (char *)0x0) {
strcpy(local_1b8,pcVar3);
sVar2 = strlen(pcVar3);
memset(pcVar3,0,sVar2);
}
strcpy(acStack_188,"sd26t13gb3");
iVar1 = strcmp(acStack_1a0,local_1b8);
if (((iVar1 == 0) && (iVar1 = strncasecmp((char *)param_1,acStack_1c8,__n), iVar1 == 0)) ||
((iVar1 = strcmp(acStack_188,local_1b8), iVar1 == 0 &&
(iVar1 = strncasecmp((char *)param_1,(char *)local_1e8,__n), iVar1 == 0)))) break;
iVar5 = iVar5 + 1;
if (iVar5 == 3) {
printf("Authorization failed after trying %d times!!!.\n",3);
param_1 = (undefined2 *)0x3;
pcVar7 = sleep;
LAB_0003bee0:
(*pcVar7)(param_1);
return 1;
}
puts("Incorrect! Try again.");
}
En el código encontramos que en la variable acStack_1a0 se guarda la cadena lo5c0poilo.2fj1 junto al contenido de la variable mac3. Si el contenido de la contraseña es válido se ejecuta el comando /bin/sh visto previamente.
pcVar7 = prctl_runCommandInShellBlocking;
if (iVar1 == 0) {
param_1 = &DAT_000b2ad0;
}
goto LAB_0003bee0;
Observando el nombre de la variable, mac3, se puede referir a la dirección MAC, y a tres letras de ella. Tras varias pruebas se comprueba que se refiere a los tres últimos bytes de la dirección MAC en mayúscula, por ejemplo, F0F1F2. Por lo que la contraseña buscada es lo5c0poilo.2fj1F0F1F2. Tras introducir la contraseña correcta se abre una consola sh como el usuario root. Tenemos acceso completo al sistema.
> software show
> getparam sys
> 4j7b4c.6mf9ak,1-
shell Password: lo5c0poilo.2fj1F0F1F2
# whoami
root
Conclusión
Los routers comerciales en venta suelen estar limitados en cuanto a personalización hacia los usuarios finales, aunque suelen guardar puertas traseras utilizadas por el servicio técnico para diagnosticar problemas.