Menu Zamknij

Hacking into TP-Link Archer C6 – shell access without physical disassembly

This story begins with simple need – ability to programmatically obtain all devices connected to my Wi-Fi router – TP-Link Archer C6. None of the existing libraries support this version of Web UI. The issue with the simplest solution (just flashing OpenWRT) – I rely on proprietary OneMesh technology, which enables better home mesh-networking. So, some hacking needed to be done.

First SSH attempt

If you try to SSH to Archer C6 using standard port 22 you’ll be surprised to see port open and even accepting admin’s password (the same as for Web UI). But the only thing you can do is set up of port forwarding. All other channels are disabled.

Example response from ssh:

PTY allocation request failed on channel 0
shell request failed on channel 0

Turns out this is for mobile apps, like Tether to control router. Not very helpful since we don’t know network topology and services behind it. More on that at official TP-Link website: https://www.tp-link.com/us/support/faq/2462/

New target – modify config backup and restore it

Apparently, a configuration backup file you can download (and then restore) from Web UI contains flags for dropbear (small SSH server) that can enable typical usage of SSH.

Obviously, the fun begins when the backup is encrypted – file command says it’s type is data. Some places on the Internet suggest that reverse engineering binaries in a firmware file (freely available on TP-Link’s website) may reveal a hard-coded encryption key.

Great resources on that include: https://forum.openwrt.org/t/build-for-tp-link-archer-c1200-ac1200/2547/40 and https://github.com/knownunown/tpl-c900-openwrt-install

Getting hands on firmware

First, we need to download firmware – it’s on https://www.tp-link.com/pl/support/download/archer-c6/#Firmware, the latest one for C6 v2 which I own is Archer C6(EU)_V2_200902. For some reason build number is not in the same format in Web UI as on the download page, but you can easily work it out. One crucial note – you must select the proper region, as US version differs from EU one.

In ZIP archive we have manuals and bin file with firmware, which we can easily extract using binwalk:

daniel@yggdrasil Archer C6(EU)_V2_200902 % binwalk -e ArcherC6v2_eu-up-ver1-3-6-P1\[20200902-rel65591\].bin 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
8489          0x2129          uImage header, header size: 64 bytes, header CRC: 0x91FC4042, created: 2020-07-01 10:19:12, image size: 1047980 bytes, Data Address: 0x80060000, Entry Point: 0x80060000, data CRC: 0x635DC658, OS: Linux, CPU: MIPS, image type: Multi-File Image, compression type: lzma, image name: "MIPS OpenWrt Linux-3.3.8"
8561          0x2171          LZMA compressed data, properties: 0x6D, dictionary size: 8388608 bytes, uncompressed size: 3043948 bytes
1056534       0x101F16        Squashfs filesystem, little endian, version 4.0, compression:xz, size: 6874966 bytes, 1595 inodes, blocksize: 1048576 bytes, created: 2020-07-31 08:49:37
7931694       0x79072E        XML document, version: "1.0"
7943484       0x79353C        gzip compressed data, from Unix, last modified: 2020-09-02 10:13:11

daniel@yggdrasil Archer C6(EU)_V2_200902 % 

The squashfs file is automatically extracted and there we have an unencrypted filesystem of what’s going to land on router’s flash memory. 1:1.

Other researchers had success with locating crypto.lua (which is a central cryptographic library for UI and management tools of the router) and extracting encryption key and IV for AES256 in ECB mode – simply by listing strings. I didn’t have such luck.

daniel@yggdrasil _ArcherC6v2_eu-up-ver1-3-6-P1[20200902-rel65591].bin.extracted % sha256sum squashfs-root/usr/lib/lua/luci/model/crypto.lua 
669fe09728b472feb3175edf9f6fd0cc9c61f91521c1b2480a55f3cae2fa5cbf  squashfs-root/usr/lib/lua/luci/model/crypto.lua
daniel@yggdrasil _ArcherC6v2_eu-up-ver1-3-6-P1[20200902-rel65591].bin.extracted % strings squashfs-root/usr/lib/lua/luci/model/crypto.lua 
LuaQ
require
luci.sys
nixio
luci.util
module
luci.model.crypto
package
seeall
aes-256-cbc
openssl zlib -e %s | openssl 
 -e %s
openssl 
 -d %s %s | openssl zlib -d
 -e %s %s
 -d %s %s
-in %q
-k %q
-kfile /etc/secretkey
enc_file
dec_file
dump_to_file
enc_file_entry
dec_file_entry
type
string
/etc/secretkey
require
luarsa
aes_enc_file
type
string
/etc/secretkey
require
luarsa
aes_dec_file
type
string
require
luarsa
aes_enc
type
string
require
luarsa
aes_dec
open
write
close
enc_file
dec_file
daniel@yggdrasil _ArcherC6v2_eu-up-ver1-3-6-P1[20200902-rel65591].bin.extracted % 

Two things stand out. First – mentioned mode of AES256 differs (here it’s CBC; nice comparison here – https://datalocker.com/what-is-the-difference-between-ecb-mode-versus-cbc-mode-aes-encryption/).

Secondly we see mention of -kfile /etc/secretkey which suggests that instead of a hard-coded key in the library we have some key file located on flash memory. That key file is nowhere to be found in firmware files, it’s also not referenced by plaintext name in any other place that could create it dynamically, for example from MAC address. As I verified after successfully obtaining the shell – it’s not present on the device itself, so this must be some unused functionality. The only thing that turned out on Google is that cryptic presentation: https://hitcon.org/2020/slides/LEAYA%20Last%20Exploitation%20%E7%B5%A2%20An%20Embedded%20System%20Detection%20and%20Response.pdf

Other files and discovery of mybackup

After some time spent on investigating /etc/secretkey I proceeded to study other files on squashfs.

My gold mine appeared to be a directory /usr/bin/tr069/ and especially two Lua scripts related to backup/restore – which are not compiled to bytecode!

check_device_sn: Lua script text executable, ASCII text
check_params:    Lua script text executable, ASCII text
diagnostic:      Lua script text executable, ASCII text
generate_hosts:  Lua script text executable, ASCII text
get_wifimac:     ASCII text
mybackup:        Lua script text executable, UTF-8 Unicode text
myddns:          Lua script text executable, ASCII text
mynat:           POSIX shell script text executable, ASCII text
mypasswd:        Lua script text executable, ASCII text
myportforward:   Lua script text executable, ASCII text
myppp:           Lua script text executable, ASCII text
myremote:        Lua script text executable, ASCII text
myrestore:       Lua script text executable, UTF-8 Unicode text
myupgrade:       Lua script text executable, ASCII text

Here’s the most interesting part of mybackup:

#!/usr/bin/lua

local firm = require "luci.controller.admin.firmware"
local util = require "luci.util"
local cry = require "luci.model.crypto"
local configtool = require "luci.sys.config"
local fs = require "luci.fs"
local uci = require "luci.model.uci"
local uci_r = uci.cursor()

function backup()
    local model = uci_r:get("locale", "sysinfo", "model") or ""
    if model == "Archer C6 v2" then
        -- 备份extern分区, 如有特殊情况的分区,再特殊处理
        local extern_partitions = uci_r:get_profile("backup_restore", "extern_partition") or nil
        if extern_partitions ~= nil then
            extern_partitions = util.split(extern_partitions, " ")
            os.execute("mkdir /tmp/backupfolder >/dev/null 2>&1")

            for i, v in ipairs(extern_partitions) do
                if v ~= nil then
                    luci.sys.exec("nvrammanager -r /tmp/backupfolder/ori-backup-" .. v .. ".bin -p " .. v .. " >/dev/null 2>&1")
                end
            end

            --打包
            luci.sys.exec("nvrammanager -r /tmp/backupfolder/ori-backup-user-config.bin -p user-config >/dev/null 2>&1")
            configtool.decryfile("/tmp/backupfolder/ori-backup-user-config.bin", "/tmp/backupfolder/ori-backup-user-config.bin")
            --delCloudCfgfrom("/tmp/backupfolder/ori-backup-user-config.bin")
            os.execute("tar -cf /tmp/backup -C /tmp/backupfolder . >/dev/null 2>&1")
            luci.sys.exec("rm -rf /tmp/backupfolder >/dev/null 2>&1")
        else
            luci.sys.exec("nvrammanager -r /tmp/backup -p user-config >/dev/null 2>&1")
            configtool.decryfile("/tmp/backup", "/tmp/backup")
            --delCloudCfgfrom("/tmp/backup")
        end     
        --luci.sys.exec("cat /dev/".. get_mtd("userconf") .. " > /tmp/backup")
        --luci.sys.exec("nvrammanager -r /tmp/backup -p  user-config   >/dev/null 2>&1")

        local cry = require "luci.model.crypto"
        cry.enc_file("/tmp/backup", "/tmp/backup.cry", "0123456789abcdef    ")
        luci.sys.exec("rm /tmp/backup")
        luci.sys.exec("cp /tmp/backup.cry /tmp/save-backup-userconf.bin; rm /tmp/backup.cry")
    else

We can quite safely assume enc_file from package luci.model.crypto takes parameters in the following order: file to encrypt, target file, encryption key. So, the encryption key is 0123456789abcdef____, where _ are space characters. I guess that those spaces on the end are there to make grepping over binaries harder.

First attempt to decrypt config file

Having encryption key I tried to adapt the command I found on OpenWRT forum, but it didn’t work. Apparently, there are some unknown variations to that AES encryption, or it’s neither CBC nor ECB.

daniel@yggdrasil Downloads % openssl enc -aes-256-cbc -d -in config.bin -out config.tar -k '0123456789abcdef    '
bad magic number
daniel@yggdrasil Downloads % 

Reverse engineering crypto.lua

It was time to investigate that crypto.lua library I mentioned on the beginning.

I certainly lack skills to replicate reverse engineering described on https://pentestmag.com/unscrambling-lua/

I could only confirm that in my case it’s also Lua5.1 bytecode for mips:isa32r2 (32-bit) architecture. As author of linked research I also tried luadec and additionally unluac (https://sourceforge.net/projects/unluac/) but both complained on wrong symbols, presumably due to incorrect architecture. My lack of insight into Lua also didn’t help. I also tried onlinedisassembler.com, but this one showed the entire file as .data section – it was unable to find an entry point for execution.

Stopping point for me was working on instruction level, especially since I don’t know MIPS architecture well.

Another approach

After some break from research had a simple idea – since bytecode in Lua binaries on router firmware is scrambled using some extensions to Lua compiler/runtime it’s enough just to execute that crypto.lua library using embedded Lua runtime and code excerpts from mybackup and myrestore. There was no obvious reason why that would be blocked in any additional way – there were no weird includes in that library that could show for example serial number dependent bavahiour. So, I just needed a MIPS emulator.

QEMU to the rescue

That section will describe how to quickly set up MIPS VM with Debian. It’s heavily based on https://www.novetta.com/2018/02/emulating-embedded-linux-systems-with-qemu/

Preferably in screen we need to run:

apt install qemu-system-mips screen
mkdir linux_mips; cd linux_mips
wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-3.2.0-4-4kc-malta
wget https://people.debian.org/~aurel32/qemu/mips/debian_wheezy_mips_standard.qcow2
qemu-system-mips -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1" -nographic -nic user,hostfwd=tcp::2222-:22

The above setup boots VM with root password root and forwards localhost:2222 to sshd on VM.

Now we need to transfer squashfs there. To avoid installing squashfs tools on VM however we can unpack it using unsquashfs and repack it with tar czf squashfs-root.tar.gz. Then we need to transfer the file over SCP. Additionally, an encrypted backup file should be on VM as well.

And now let’s chroot to C6’s firmware

root@sandbox2:~# ssh -p 2222 127.0.0.1
[email protected]'s password: 
Linux debian-mips 3.2.0-4-4kc-malta #1 Debian 3.2.51-1 mips

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Feb 20 21:59:23 2021 from 10.0.2.2
root@debian-mips:~# 
root@debian-mips:~# tar zxf squashfs-root.tar.gz
root@debian-mips:~# cd squashfs-root/
root@debian-mips:~/squashfs-root# chroot . ./bin/sh


BusyBox v1.19.4 (2020-08-24 18:31:00 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

/ # 

Decryption and extraction

Assuming that our working directory with backup file is /root in chroot (~/squashfs-root/root/ on host Linux) we can try the simplest approach – running interactive interpreter of provided Lua with crypto.lua loaded:

/ # lua -i /usr/lib/lua/luci/model/crypto.lua 
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio (double int32)
> cry = require "luci.model.crypto"
> cry.dec_file("/root/ArcherC6v220200902136n.bin", "/root/backup.decrypted", "0123456789abcdef    ")
CONFIG_TP_AES_MAGIC_NUM:acac!
htonl tp_magic_num:acac!
htonl org_file_len:3e00!
> 

Seems to do something! Let’s see what was written into /root/backup.decrypted:

root@debian-mips:~/squashfs-root/root# ls
ArcherC6v220200902136n.bin  backup.decrypted
root@debian-mips:~/squashfs-root/root# file backup.decrypted 
backup.decrypted: POSIX tar archive (GNU)
root@debian-mips:~/squashfs-root/root# 

It worked! Let’s dive further:

root@debian-mips:~/squashfs-root/root# tar xvf backup.decrypted
./
./ori-backup-user-config.bin
./ori-backup-certificate.bin
root@debian-mips:~/squashfs-root/root# file ori-backup-user-config.bin
ori-backup-user-config.bin: gzip compgressed data, from Unix, last modified: Sat Feb 20 02:00:03 2021, max compression
root@debian-mips:~/squashfs-root/root# mv ori-backup-user-confi.bin ori-backup-user-config.gz
root@debian-mips:~/squashfs-root/root# gzip -d ori-backup-user-config.gz 
root@debian-mips:~/squashfs-root/root# file ori-backup-user-config
ori-backup-user-config: POSIX tar archive (GNU)
root@debian-mips:~/squashfs-root/root# tar xvf ori-backup-user-config
tmp/user-config.xml
root@debian-mips:~/squashfs-root/root# file tmp/user-config.xml 
tmp/user-config.xml: XML document text
root@debian-mips:~/squashfs-root/root# 

After few layes of compression we have XML file with configuration, that includes dropbear section.

Adding RemoteSSH parameter

Necessary changes to user-config.xml:

root@debian-mips:~/squashfs-root/root# diff -y tmp/user-config.xml tmp/user-config.xml.backup | grep RemoteSSH -C5
<dropbear>                                  <dropbear>
<dropbear name="dropbear">                  <dropbear name="dropbear">
<RootPasswordAuth>on</RootPasswordAuth>     <RootPasswordAuth>on</RootPasswordAuth>
<Port>22</Port>                             <Port>22</Port>
<SysAccountLogin>off</SysAccountLogin>      <SysAccountLogin>off</SysAccountLogin>
<RemoteSSH>on</RemoteSSH>                <
<PasswordAuth>on</PasswordAuth>             <PasswordAuth>on</PasswordAuth>
</dropbear>                                 </dropbear>
</dropbear>                                 </dropbear>
<firewall>                                  <firewall>
<defaults name="firewall_default">          <defaults name="firewall_default">
root@debian-mips:~/squashfs-root/root# 

Backup structure investigation

Compressing and encrypting back should be easy, right? Well, to compress it back correctly I needed to spend some time to confirm the actual layout of nested files, whether . directory should be in tarball and few other quirks of tar and gzip tools.

Next section will only describe happy path and why some parameters were used – the discovery process took me a few hours in which I heavily used man and hexdump paired with diff to compare files. Any broken nesting of containers caused the following error in Web UI when I tried to upload the prepared file:

Backup structure, when visualized, is kind of weird, but it’s created on router with different tools so it may make sense.

Few parameters to gzip and tar require explanation:

  • tar -C allows keeping desired paths in tarball – with, or without separate directory node – both of those being part of backup have different approaches here
  • tar -b1 sets nonstandard block size (same concept as sector size on disk); such low value reduces zero-padding of file, which could cause nvram partition overflow if set to default 20 – in my case it was 21 kB vs 16 kB
  • gzip -n removes original filename from archive metadata
  • gzip -9 sets max compression (as it is on original routine)

Happy path of compression and encryption

First – compression that can be done outside chrooted environment:

root@debian-mips:~/squashfs-root/root# tar -b1 -cvf ori-backup-user-config -C . tmp/user-config.xml
user-config.xml
root@debian-mips:~/squashfs-root/root# gzip -n9 ori-backup-user-config 
root@debian-mips:~/squashfs-root/root# mv ori-backup-user-config.gz ori-backup-user-config.bin
root@debian-mips:~/squashfs-root/root# mkdir oris
root@debian-mips:~/squashfs-root/root# cp {ori-backup-certificate.bin,ori-backup-user-config.bin} oris
root@debian-mips:~/squashfs-root/root# tar -b1 -cvf backup.decrypted -C oris .
./
./ori-backup-user-config.bin
./ori-backup-certificate.bin
root@debian-mips:~/squashfs-root/root# 

Second – encryption using Lua library:

/ # lua -i /usr/lib/lua/luci/model/crypto.lua 
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio (double int32)
> cry = require "luci.model.crypto"
> cry.enc_file("/root/backup.decrypted", "/root/new_config.bin", "0123456789abcdef    ")
> 

This is a place where I could strace Lua interpreter and see what commands are actually called, but I was very impatient and this would require adding some additional stuff to VM which anyway didn’t have internet connection to connect to repositories. I will definitely revisit that later.

Testing time

After I extracted that new_config.bin from MIPS VM via sandbox VM to my workstation I could finally upload proper hacked backup to Web UI of router. After a few minutes waiting for reboot I still had Internet connection and router UI worked fine. Time for SSH then:

It works and admin user (while having non-zero UID) anyway has root user privileges. Mission accomplished.

Afterword

If TP-Link really wants to block users from modifying backup files – they didn’t learn their lesson about hard-coding encryption keys anywhere in firmware. Scrambling bytecode of Lua by replacing instructions is nice, but useless if keys are downloadable from the Internet.

For security researchers who deal with obfuscated binaries quite often there’s QEMU way which allows just to execute decryption/encryption process to obtain interesting data.

Unfortunately, I won’t be able to share my backup files as they contain quite few sensitive information (including PPTP credentials to my ISP) and stripping every secret is time-consuming. However, you can grab the original factory config from firmware.bin – there’s one entry XML document that contains it.

Addition – oneliner for massing strings extraction

One thing that helped me a lot when searching for various strings (especially needed when symbol names are not obfuscated in binaries) is that one-liner which creates a single text file with all strings in all files in a given directory (I executed it on rootfs of firmware squashfs):

for f in `find . -type f`; do echo "=== $f"; strings $f; done > ../squashfs_strings.txt

5 komentarzy

  1. DDS

    Another router with modified OpenWRT as stock firmware and LuCI as webui.
    Unfortunately, it looks like the rootfs is SquashFS which means there’s not a lot you can modify without rebuilding the firmware file even with SSH access.
    Not the first time I see a router hacked by modifying config backups. I actually did this myself to an ISP supplied ADB DSL router (Broadcom based) with restricted „admin” user to gain access to real admin users. It was a simple as modifying password hashes in plaintext XML backup file and uploading it back to the router.
    That box also had a pre-soldered serial header which allowed open access to the CFE bootloader. In theory, I could’ve used this to modify the firmware to allow shell access, but since the router was technically owned by my ISP, I decided not to bother.

  2. Alexey

    Thank you for great tutorial. The only thing I was unable to reproduce is decryption by hand so I had to write small script as excerpt from usr/bin/tr069/myrestore (and same for encryption 🙂 ):
    #!/usr/bin/lua

    local firm = require „luci.controller.admin.firmware”
    local cry = require „luci.model.crypto”
    local configtool = require „luci.sys.config”
    local fs = require „luci.fs”
    local util = require „luci.util”
    local uci = require „luci.model.uci”
    local uci_r = uci.cursor()
    local BACKUP_ORIGIN_FILENAME = „/tmp/backup”

    cry.dec_file(„/tmp/ArcherC6v220201126132n.bin”, BACKUP_ORIGIN_FILENAME, „0123456789abcdef „)

  3. H4H

    Great and easy explanation for even main-windows user!
    But in my case there’s weren’t any „tr069”, only crypto.lua, but your magic word were correct for my fw version.
    Archer20C60_V2_181227

  4. OutsiderLost

    That’s great! I was able to successfully change a few settings that are not in the hands of the user! 🙂

    After uploading, and download, looked back at „backup.bin”, it looks like nothing has changed.

    However, 'ssh’ gives an error after logging in:
    'failed on channel 0′

    Already tried several methods to eliminate it, but it seems unsolvable.
    (I use Ubuntu to login).

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *