Introduction️

Various devices marketed in the market both for acquisition by consumers and for rental by Internet service providers’ clients allow their administration through a web portal. This portal may be limited in features and if more advanced configurations are needed, such as configuring the firewall, with the iptables tool, access via console is necessary. The operating system of routers usually is GNU/Linux.️

The console access is usually blocked to prevent problems with incorrect configurations made by inexperienced users, which makes it necessary, for example, to access the serial port by removing the device casing. In other cases, access to the console is allowed through the SSH (Secure Shell) protocol, but access is limited to a restricted console, with pre-defined commands from the manufacturer. These devices usually have a backdoor that allows deploying a command terminal sh or bash with the introduction of specific commands.️

This article will start from a firmware update file for a router, which contains its complete file system to discover the execution flow of the restricted console through reverse engineering of binary files, in order to obtain the full terminal. For this purpose we will use the binwalk program to extract the firmware and Ghidra to disassemble the binary files.️

Firmware extraction

Extracting the file system from the firmware.bin file using binwalk with the -e parameter.️

$ binwalk -e firmware.bin

We found the system extracted in the subdirectory _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

Binary search to analyze.️

The binary must deploy the binary in some form, in this case it must use the C function system to execute the binary /bin/sh or /bin/bash. We can search among the extracted files using the program grep for strings with the parameters -rn.️

$ grep -rn "/bin/sh" .
...
grep: ./_firmware.bin.extracted/squashfs-root-0/lib/private/libcms_cmd.so: coincidencia en fichero binario
...

Among the results found is the library libcms_cmd.so. In its name, it contains cli (Command-line interface), or command line interface.️

Loading binary file into Ghidra

In binary files or libraries, if they have been dynamically compiled, they load code from other files in order to save space. Therefore, it is necessary to load all files into the disassembler and decompiler Ghidra. To do this, we create a new project and use the option File > Batch Import... selecting the folder squashfs-root. We must expand the depth limit of scanning Depth limit to 10 and click on the button Rescan. When the loading of files is finished, we open squashfs-root/lib/private/libcmd_cli.so in the CodeBrowser tool. We perform the analysis with default options.

Analysis of the file libcms_cmd.so️

Let’s use the option Search > Program Text to search for the string /bin/sh. We select the options All Fields and All Blocks. We find a function called cli_processHiddenCmd. This function calls the function prctl_spawnProcess passing as an argument /bin/sh, that is, it launches a console.️

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;
}

Searching for references by the function cli_processHiddenCmd using the right-click option on the name of the function and References > Find references to, we found the function cmsCli_run with the following code snippet.️

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;
	}
  }

Seeing the calls to the functions log_log and syslog we can suppose that the introduced command is saved in the variable local_524. We see that the functions cli_processMsCliCmd and cli_processHiddenCmd are executed. Also, if an incorrect command is introduced more than nine times, the file /var/enableHiddenCMD is deleted and the variables backdoor_lock2, backdoor_lock1 and cmd_fail_times are set to 0. We examine the function cli_processCliCmd.️

if (backdoor_lock1 != '\0') {
iVar2 = strncasecmp(param_1,"getparam sys",8);
if (iVar2 == 0) {
  backdoor_lock2 = 1;
}
else {
  backdoor_lock1 = '\0';
}
}

We observe that if the command entered is getparam sys, the variable backdoor_lock2 is set to 1. We can assume that if the value of backdoor_lock2 and backdoor_lock1 is equal to 1 and there exists the file /var/enableHiddenCMD, it will allow us to execute commands. If we search for a function that changes the value of backdoor_lock1, we find the function 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;

Below we find, in the help section, the command to execute, software show.️

memcpy(aiStack_178,"\nUsage: software show\n       software help\n",0x2e);

In the function processSYSCmd, which processes the command getparam sys, it is observed that if the backdoor variables have a value different from \0, the file /var/enableHiddenCMD will be created.️

if ((backdoor_lock1 != '\0') && (backdoor_lock2 != '\0')) {
  prctl_runCommandInShellBlocking("echo 1 > /var/enableHiddenCMD");
  backdoor_lock2 = '\0';
  backdoor_lock1 = '\0';
}

We cannot execute commands or access the console yet with this file. We need to run a special command found in the cli_processHiddenCmd function.️

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);

The command to execute is 4j7b4c.6mf9ak,1- whenever the file /var/enableHiddenCMD exists. The following is a password request.️

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.");
}

In the code, we find that in the variable acStack_1a0 the string lo5c0poilo.2fj1 is stored along with the content of the variable mac3. If the password content is valid, the command /bin/sh seen previously is executed.️

pcVar7 = prctl_runCommandInShellBlocking;
if (iVar1 == 0) {
param_1 = &DAT_000b2ad0;
}
goto LAB_0003bee0;

Observing the variable name, mac3, it can be referred to as the MAC address, and three letters of it. After several tests, it is confirmed that it refers to the last three bytes of the MAC address in uppercase, for example, F0F1F2. Therefore, the sought password is lo5c0poilo.2fj1F0F1F2. Upon entering the correct password, a sh console opens as user root. We have full access to the system.️

> software show
> getparam sys
> 4j7b4c.6mf9ak,1-
shell Password: lo5c0poilo.2fj1F0F1F2
# whoami
root

Conclusion️

Commercial routers for sale are usually limited in terms of customization towards end users, although they often have backdoors used by the technical service to diagnose problems.️