Bypassing upgrade limitations on a TP-Link TL-WR841N

Introduction

Hello! In this blogpost we’ll continue with our TP-Link TL-WR841N Saga. Today’s post will explain the steps that I followed to bypass a silly limitation in the Router’s Stock firmware for Latin America (ES) models that did not allow me to install the latest US firmware.

Router and firmware versions per region

As I wanted to hunt for new vulnerabilities in the Router’s firmware, I decided to install the latest firmware available.

latest Firmware version when US region is selected

This is the latest firmware available for download for the United States region. Released on April 09 2021, is still hot. This update has been released even after the product as reached is end of life (EOL).

I proceeded to download it and tried to install it, but I ended up the following error message:

Error updating the firmware

It says that the device did not accept the firmware. As I was connected over the serial interface I took a look at the screen while the update process was running and I saw the following error:

Error updating the firmware printed in the serial interface

At this point I decided to take a look at what was happening under the hood.

Analysis of the Firmware’s update process and rediscovering CVE-2019-19143 on it

I decided to use a combination of static and dynamic analysis to understand what was happening, taking advantage of the debugging capabilities that I obtained before. If you want to read more about it you can refer to the previous post of this saga.

I started decompressing the installed firmware. I had a copy of it, that you can find here. I used binwalk for this task:

binwalk -eM TL-WR841Nv14_ES_0.9.1_4.16_up_boot[180515-rel41770].bin

To figure out where start looking I executed the upgrade process again, this time using Burp Proxy to analyze every HTTP request performed by the web application.

Below you can find a shortened version with the important information.

POST /cgi/softup HTTP/1.1
Host: 192.168.0.1
Content-Length: 4063991
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2Q0BPMrYW8nfAK4B
Cookie: Authorization=Basic XXXXXXXX
...

As we can see there is a POST request to /cgi/softup that needs authentication. We have some interesting information already:

  • The firmware is uploaded and something is validated on the router’s side that kicks us out
  • The endpoint in charge of the update is /cgi/softup

Side note: rediscovering CVE-2019-19143

One of the first things that I tried was replaying the update request without the Cookie header. To my surprise IT WORKED!. It was possible to upload a new firmware to the router without authentication. After some googling I found that this issue was already known and tagged as CVE-2019-19143. The router checks that the Referer header matches the Router IP address. A “funny” thing is that this check is wrongly performed, as it only checks that the IP in the header contains the Router IP. I tried with the following value: Referer: http://192.168.0.122111 and it also worked.

Coming back to our main objective, I decided to take a look at the file system to see if there was any /cgi/ folder or any softup file. I found nothing. I concluded that the filepath was being processed by the webserver, in this case the httpd binary.

I loaded it in Ghidra and searched for that string:

function using the softup URL

As the image above shows, I found it being used as an argument for the function http_alias_addEntryByArg. I assumed that this function was some kind of “Handler installer”, and the function called when the URI of interest was requested was http_rpm_update. I proceeded to analyze it.

Function http_rpm_update

Decompiled function code provided by Ghidra (Just showing important content):

int http_rpm_update(int *param_1)

{
...
        iVar2 = strcmp(acStack2356,"filename");
        if (iVar2 == 0) {
          DAT_00438738 = local_30;
          bVar1 = true;
          puVar4 = auStack2612;
          uVar3 = 0x100;
        }
      }
      if (bVar1) {
        param_1[0xe] = (int)FUN_00408400;
        puVar4 = &DAT_0040c7a4;
      }
      else {
        ...
      }
      sprintf(acStack2100,"<html><head></head><body>%s</body></html>",puVar4);
      param_1[7] = 0;
      param_1[0xf] = (int)g_http_file_pTypeHTML;
      iVar2 = http_io_output(param_1,acStack2100);
      iVar2 = (iVar2 != -1) - 1;
    }
  }
  return iVar2;
}

Taking a quick look at this function I found some clues that led me to believe it could be the one being executed when the update process was called. Before starting to analyze it, I tried to identify key points like the following ones:

  • The returned value was the one I was receiving: <html><head></head><body>%s</body></html>
  • The function processed input looking for a file
  • There were some names suggesting “update buffers”

Based on the ideas above, I decided to avoid doing a full analysis of this function and only focusing on understand what I needed to modify or change to accomplish my goal. I tried to understand how the process continued, and discarding options I learned that it called function FUN_00408400. This function after some checks called rdp_updateFirmware, I knew I was closer!.

This function was being imported by the httpd binary from the libcmm.so shared object. It’s primary function was calling (finally) the function doing the actual job, rsl_sys_updateFirmware.

rsl_sys_updateFirmware

The decompiled code for this function is:

The first thing that I did was trying to be sure that this one was the function I was looking for. Luckily at the end of it we can see the same error message that we saw in our Serial terminal when we tried to update the firmware: Firmware version check failed and the hex number 0xbd7 which in decimal is 3031, the same that in the error message.

My main goal was to be able to land around line 23. That’s after some checks that the code was doing to determine if it was possible to install the firmware in the device.

At this point I thought that what I was trying to do could end up bricking my router, after all, I was bypassing checks made with the purpose of validating hardware parameters. To reduce this possibility I performed a diff with Meld between the decompressed stock firmware and the US firmware that I wanted to install. I noticed changes but the main structure was the same and I decided to take the risk.

Bypassing the update checks

To bypass the annoying checks I decided to debug the httpd binary and change its execution flow to land in the code I needed to execute. I already had gdb-server in the router so I attached it to the httpd process as follows:

./var/tmp # ./gdbserver-buildroot 0.0.0.0:8888 --attach <httpd_pid>
Attached; pid = 319
Listening on port 8888

In my local machine I used gdb-multiarch to connect to the remote gdb-server:

cd /path_to_local_copy_of_the_firmware/usr/bin/
gdb-multiarch httpd

(gdb) target remote 192.168.0.1:8888
Remote debugging using 192.168.0.1:8888
...
0x2bad75cc in ?? () from target:/lib/libc.so.0

I put a breakpoint in one of the earliest calls done by the verification checks rsl_dev_getProductVer, continued the execution and started the upgrade process:

(gdb) br rsl_dev_getProductVer

Breakpoint 1 at 0x2b9a76ac
(gdb) c
Continuing.

After a few seconds I hit the breakpoint:

Breakpoint 1, 0x2b9a76ac in rsl_dev_getProductVer () from target:/lib/libcmm.so
=> 0x2b9a76ac <rsl_dev_getProductVer+12>:	30 80 85 8f	lw	a1,-32720(gp)
(gdb) 

The following image will illustrate the purpose of this idea better than words:

assembly code for update function

As we can see in the assembly listing, once the execution returns from rsl_dev_getProductVer there is a branch instruction that must be taken. Otherwise, execution continues at label LAB_0002b0f8 and we are presented with an error message. To be able to update our firmware the program execution should continue at LAB_0002b120.

Back on track with our BP, I put a breakpoint in the return address (RA Register) once the function rsl_dev_getProductVer was called and landed exactly were I wanted:

Breakpoint 1, 0x2b9a76ac in rsl_dev_getProductVer () from target:/lib/libcmm.so
=> 0x2b9a76ac <rsl_dev_getProductVer+12>:	30 80 85 8f	lw	a1,-32720(gp)
(gdb) info r
          zero       at       v0       v1       a0       a1       a2       a3
 R0   00000000 00000001 00000000 00000300 7fbde0f0 7fbddd98 00000004 2ba3fe50 
            t0       t1       t2       t3       t4       t5       t6       t7
 R8   0000a32a 31000000 00302e34 00000000 00000000 00000000 08410014 000001e4 
            s0       s1       s2       s3       s4       s5       s6       s7
 R16  2bb3e008 003e0200 2bb3e008 00000348 00ff0000 00040100 7fbde298 7fbde33b 
            t8       t9       k0       k1       gp       sp       s8       ra
 R24  00000318 2b9a76a0 00000000 00000000 2ba70300 7fbde150 004230d4 2b9a70ec 
        status       lo       hi badvaddr    cause       pc
      0100ff13 00000284 00000000 2ba11694 50800024 2b9a76ac 
          fcsr      fir      hi1      lo1      hi2      lo2      hi3      lo3
      00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
        dspctl  restart
      00000000 00000000 
(gdb) br *0x2b9a70ec
Breakpoint 2 at 0x2b9a70ec
(gdb) c
Continuing.

Breakpoint 2, 0x2b9a70ec in rsl_sys_updateFirmware () from target:/lib/libcmm.so
=> 0x2b9a70ec <rsl_sys_updateFirmware+264>:	20 00 bc 8f	lw	gp,32(sp)
(gdb) x/10i $pc
=> 0x2b9a70ec <rsl_sys_updateFirmware+264>:	lw	gp,32(sp)
   0x2b9a70f0 <rsl_sys_updateFirmware+268>:	beq	s3,v0,0x2b9a7120 <rsl_sys_updateFirmware+316>
   0x2b9a70f4 <rsl_sys_updateFirmware+272>:	nop
   0x2b9a70f8 <rsl_sys_updateFirmware+276>:	lw	a1,-32720(gp)
   0x2b9a70fc <rsl_sys_updateFirmware+280>:	lw	a3,-32720(gp)
   0x2b9a7100 <rsl_sys_updateFirmware+284>:	lw	t9,-32508(gp)
   0x2b9a7104 <rsl_sys_updateFirmware+288>:	li	a0,8
   0x2b9a7108 <rsl_sys_updateFirmware+292>:	addiu	a1,a1,-28560
   0x2b9a710c <rsl_sys_updateFirmware+296>:	li	a2,3031
   0x2b9a7110 <rsl_sys_updateFirmware+300>:	jalr	t9
   0x2b9a7114 <rsl_sys_updateFirmware+304>:	addiu	a3,a3,-14844
(gdb) 

I single stepped one instruction (si command), changed the $PC to address 0x2b9a7120 to continue execution (set $pc = 0x2b9a7120) and prayed to the hardware gods for a flawless update:

(gdb) si
0x2b9a70f0 in rsl_sys_updateFirmware () from target:/lib/libcmm.so
=> 0x2b9a70f0 <rsl_sys_updateFirmware+268>:	0b 00 62 12	beq	s3,v0,0x2b9a7120 <rsl_sys_updateFirmware+316>

(gdb) set $pc=0x2b9a7120
(gdb) c
Continuing.
[Detaching after fork from child process 2995]

I checked the serial output and saw several messages like this one:

...
piflash_ioctl_write, Write to 0x00210000 length 0x10000, ret 0, retlen 0x10000
#spiflash_ioctl_read, Read from 0x00220000 length 0x10000, ret 0, retlen 0x10000
.
...

I held my breath until the process finished:

latest firmware version installed

As we can see the latest version was correctly installed!. With the method we can freely update and downgrade the firmware as needed. In future posts I’ll continue playing with this router, see you!

Written on May 14, 2021