Bypassing passwords and getting a shell through UART in a Wi-Fi router
We started out by trying to connect to the router via UART and found out that it asks for a username and password in order to log in. We made many attempts using well-known credentials, but we decided that it was time to resort to static analysis in an attempt to find out the correct password.
Obtaining the firmware
As usual, we downloaded the firmware from the vendor’s website and ran Binwalk on it. Unfortunately Binwalk was unable to recognize any file format, so we get the impression that it might be encrypted somehow. In order to verify it, we decided to run Binwalk’s built-in entropy analysis on the downloaded image. The results are shown below:
This means that the image had a constant (and high) entropy value, which means that the image was most likely encrypted. But, since we did have physical access to the device, Emilio was able to desolder the EEPROM chip from the board and read its contents. With this newly dumped firmware image, we made one more attempt to run Binwalk on it and, sure enough, it was now able to recognize the contents and extract the entire filesystem!
Figuring out the password
The first thing we did was to dump the /etc/shadow
file and try to crack the hash. We succeeded but this password was not accepted by the UART login prompt. Judging from this behavior we concluded that there must be something else going on with this firmware, so we decided to take a look at the rest of the filesystem and see if we could find anything suspicious.
Login credentials’ validation
After taking a look at /etc/init.d/rcS
we found that a binary called console_password
runs after the initiation sequence is over. We were surprised to find that this binary had not been stripped from its symbols. In fact, all binaries had symbols! (thanks NEC for making our job easier). Here is a simplified version of the decompiled binary (the main function only):
char user_pwd_buff[1024]; int user_ok; printf("Login: "); gets(user_pwd_buff); // (1) user_ok = strncmp(user_pwd_buff, "root_cheeper", 13) == 0; // (2) printf("Password: "); 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%.2hhx", mib202[0], mib202[1], mib202[2], mib202[3], mib202[4], mib202[5]); if ( pipe(pipe_fd) < 0 ) { puts("pipe error"); return -1; } pid = fork(); if ( pid < 0 ) { puts("fork error"); 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;
This might seem like a lot of code but most of it is really straightforward (we can safely ignore everything between the comments (3) and (4) for now). The gist of it is that a username gets read from stdin
(1) and then checked against the string
“root_cheeper” (2), just before reading a password from stdin
(3) and checking it against some buffer (4). If any of the checks fail, then the program goes into an infinite loop (at the HANG
label). Of course, what we would like to know is the exact value of the string in valid_pwd
, which will be compared to the password read from stdin
.
We now have to bring our attention to what’s going on between (3) and (4). Long story short, it is just executing
pwcrypt with some parameters obtained from apmib_get
and reading its output into a buffer. What this means is that there must be a binary called
pwcrypt within the filesystem whose function is to generate a valid password based on some input data.
Sure enough, we found the
pwcrypt binary in the /bin
directory of the filesystem. Moreover, we found the following strings defined within it:
"invalid argument !!" "pwcrypt version %d.%d.%d\n" "pwcrypt [product name] [wan mac address(12byte)]"
So this process of password-generation is dependent on one more binary, called
flash, which is supposed to retrieve a random hardware key from flash memory (we don’t actually know any of this for sure, but given such suggestive symbol and program names given here we can assume so, at least for the moment being).
If we continued the analysis as we’ve been doing so far, we would now turn our attention to the
flash binary. But reverse engineering the
flash binary is kind of tiresome, since it involves many indirect function calls and a lot of code (for reference,
console_password was about 4KB in size and
pwcrypt 9KB, but
flash is a 84KB binary). Clearly, just attempting to read the code from start to end wouldn’t work here, so let’s digress for a while.
Let’s turn our attention once more to the code listing for
console_password, the one given earlier. We now know that it is called
pwcrypt, and that
pwcrypt expects to be given both the device’s model name and its MAC address. But how exactly does
console_password get that information? The relevant code is included here once again for convenience:
apmib_get(16102, pwcrypt_arg1); apmib_get(202, pwcrypt_arg2); sprintf(pwcrypt_arg2,"%.2hhx%.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);
What this is telling us is that apmib_get
is somehow in charge of retrieving device-related values from non-volatile memory. This functionality is oddly related to the one we expected from
flash, isn’t it? On top of that,
flash is linked against
libapmib too, which is the library that defines this apmib_get
function. Given all of that context it does make sense to go and look at
libapmib first, in an attempt to try to figure out how all of these values are being read from memory (and what type of memory they are being read from!).
Digging into libapmib
We decided to take a look at
libapmib.so and see how apmib_get
works. Luckily, as we had debug symbols, we were able to see this at the beginning of the function (note that we have named the parameters because we can infer their semantics from the previous usages of this function):
int apmib_get(int numeric_id,int *out) { // local variable declarations... if (func1(numeric_id,&mib_table,&local_24))) { Var0 = &mib_table; } else if (func1(numeric_id,&hwmib_table,&local_24))) { Var0 = &hwmib_table; } // more code...
With this information we noticed that there is a table in memory which stores information related to the integer keys we saw previously (for instance, from looking at
console_password we know that the key for the mac address is
202). Moreover, there are multiple tables, and they are searched in a specific order when apmib_get
begins looking for a requested id.
After inspecting how these tables are laid out in memory we noticed the following pattern:
Note that we’ve already added some typing information, which we’ve figured out by taking a look at how the elements of this table were being used within apmib_get
.
After defining the appropriate types, the code ended up looking like this:
int apmib_get(int numeric_id,int *out) { // local variable declarations 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) { // ... case 7: *(_DWORD *)result = *(_DWORD *)(data_tbl_ptr + metadata_tbl_entry->offset); // ... } // more code
We added a lot of information to this listing, so we will need to talk a little bit about where all of this came from.
– We knew that the 3rd field of each entry in the table indicated the type of the entry because, depending on its value, the value of result
gets set in different ways.
– We also know that the first field is the id because it is the value being compared with the first input parameter (which if you recall, indicates the key to be searched).
– Finally, as you can see from the 7th case inside the switch, the data being copied to result
comes from adding the 4th field of the entry to a given pointer that we named data_tbl_ptr
. This is why we decided to call this field `offset`.
In the snippet above, we included only the 7th case of the switch because this is the entry type of RANDOM_KEY
, which looks suspiciously similar to HW_RANDOM_KEY
from earlier (in fact, when you use flash get
to retrieve HW_RANDOM_KEY
, flash stripes the HW_
prefix and just looks for RANDOM_KEY
in hwmib_table
).
After figuring all of this out, we were only missing the actual value of RANDOM_KEY
. We knew that it is located at offset
0xD in the table pHwSetting
, but we were missing that table’s contents.
In order to get the contents of that buffer, we took a look at all the functions where pHwSetting
is being used, and found that it was being referenced from apmib_get
, apmib_set
, apmib_update
, apmib_init
, apmib_init_HW
. Out of those functions, the ones that seemed more likely to give us a hint about the contents of pHwSetting
were the ones that contained init
in the name. We quickly realized that they were both calling a function named apmib_hwconf
, which seemed to be populating the buffer.
After reverse engineering apmib_hwconf
for a while we managed to discover that the contents of the buffer were being uncompressed from flash and copied to RAM. After managing to decompress this image, we were able to read the value of RANDOM_KEY
using the previously defined offset. In order to automate this process, we wrote a custom tool which you can find here
Putting it all together
Let’s do a recap of what we have done so far:
- To get a shell on the device, a binary called
console_password checks the password entered by the user against the one returned by
pwcrypt (which gets called using two known values).
pwcrypt, in turn, callsflash get HW_RANDOM_KEY
to retrieve the random key from flash.- And we knew the value that
flash get HW_RANDOM_KEY
should return in order to generate the valid password, because we managed to uncompress the settings stored in flash memory and calculate the offset at which the 4-byte random key is supposed to reside.
In other words, we have everything we need to calculate the correct password ourselves. We just needed to set up an environment which allows us to execute MIPS binaries, and some way to fake flash memory access.
The first task was easily achieved using qemu user mode and
chroot, but the second one was a bit more hacky. Since
pwcrypt only uses
flash once to get the hardware random key, we replaced
flash with a bash script that always returns the correct value for HW_RANDOM_KEY
. Then, we executed
pwcrypt with the device name and WAN mac as arguments to get the correct password.
The exact process can be seen in this video
らくらくshellスタート!
After all this effort, Emilio noticed that there was a QR code on the back of the device. It turns out that people upload photos of their devices and redact the information they consider sensitive, for example the default WiFi password.
But Emilio found out that the QR code contains a lot of information. If you scan it with your cell phone, it will lead to a page designed to speed up your setup: Easy QR start (らくらくQRスタート).
The thing is that in order to set up the device, the URL conveys a lot of information. Guess what information is there, among other things… The HW_RANDOM_KEY
, the WAN mac address and the device name! So only by scanning this QR Code, we could have generated the shell password without dumping any firmware. We made another tool to automate this process, which you can find here
Credits:
Related Posts
October 30, 2024
Back to basics: Security recommendations for your team
October is Cybersecurity Awareness Month, a time when we focus on ways to enhance security in our daily lives, both personally and, most…
October 24, 2024
Release v5.7.0
We’ve just released an update that brings significant improvements to Faraday, focusing on solving key challenges in vulnerability…
October 10, 2024
Cybersecurity talks with our CEO Federico Kirschbaum in Uruguay
Our CEO, Federico Kirschbaum, participated in 'Conciencia Digital,' a conference hosted by Netgate Uruguay in Montevideo. Thousands of…