Hacking into a Tenda AC7 Wi-Fi router


Introduction

This is a writeup about a weekend spent attempting to hack a router with some friends. Although we had some background in hardware security, this was our first hands-on hardware hacking session. For a first attempt we bought very simple tools: an SPI programmer, a very basic logic analyzer, a UART-to-USB adapter, and Arduino-style accessories (wires, iFixit toolkit, etc.).

Main objective

The primary objective was to obtain root access and extract the firmware so we could analyze the binaries and emulate them (I planned to use Qiling for emulation). Since we were beginners, the underlying goal was to learn hardware-hacking techniques and the overall workflow for extracting firmware from embedded devices.

The Target

We selected the Tenda AC7 (1200) router which is a basic Wi-Fi and Ethernet router sold by Tenda. Because it’s not produced by a very large vendor, we thought it would be a good introductory target for hardware hacking.Tenda router. it's a most basic Wifi and Ethernet router vendored by Tenda, since it isn't too big of a vendor I figured this could be a good introduction to hardware hacking.

"router internals"

Hacking

UART CCESS

After opening the router we found exposed UART pins on the board, which allowed us to obtain an initial shell prompt.

"UART connection"

The UART shell presents two categories of commands: Realtek commands (likely supplied by the SoC vendor) and Tenda commands added by the vendor. The gadgets of particular interest were the Realtek memory-access commands such as db (display byte) and eb (edit bytes), which allow reading and writing arbitrary SoC memory. Most other commands are for configuration and debugging. The shell also includes networking commands and some vendor utilities.

The db and eb commands were especially useful because they let us read raw memory regions of the SoC, which provides a way to extract firmware and kernel images if they are mapped into readable memory.

The first things we tried was to list the file system, but every folders were empty. Maybe the server as somem kind of RBAC and we'll need to escalate our privileges, or it could also be possible that all the files are stored in RAM.

CLI> help
Realtek's command:

Commands    Descriptions                             Usage               
--------    ------------                             -----
db          db <Address> <Len>                                           
dw          dw <Address> <Len>                                           
eth         eth                                                          
wlan0       wlan0                                                        
wlan0-va0   wlan0-va0                                                    
wlan0-va1   wlan0-va1                                                    
wlan0-va2   wlan0-va2                                                    
wlan0-va3   wlan0-va3                                                    
wlan0-vxd   wlan0-vxd                                                    
wlan1       wlan1                                                        
wlan1-va0   wlan1-va0                                                    
wlan1-va1   wlan1-va1                                                    
wlan0-va2   wlan0-va2                                                    
wlan1-va3   wlan1-va3                                                    
wlan1-vxd   wlan1-vxd                                                    
eb          eb <Address> <Value1> <Value2>...                            
ew          ew <Address> <Value1> <Value2>...                            
alg         alg                                                          
brconfig    brconfig                                                     
cat                                                                      
cpuload     cpuload                                                      
date        show system local time                                       
excep       cause exception                                              
fastpath    fastpath                                                     
getmib      getmib                                                       
help        Displays a list of commands                                  
?           Displays a list of commands                                  
hexcat                                                                   
dump        Shows a memory dump                                          
hw_nat      hw_nat                                                       
ib          ib                                                           
idd         idd                                                          
ifconfig    ifconfig                                                     
ip6fw       ip6fw                                                        
ipfw        ipfw                                                         
irf         irf                                                          
iw          iw                                                           
iwpriv      iwpriv                                                       
ll                                                                       
mac         mac <ifname> [mac addr]                                      
mroute      mroute                                                       
ndp         ndp                                                          
ob          ob                                                           
od          od                                                           
orf         orf                                                          
ow          ow                                                           
pdump       dump a  thread                                               
ping        ping                                                         
port_fwd    port_fwd                                                     
version     Shows build version                                          
ps          Shows a list of threads                                      
reboot      Reset the system                                             
reinit      reinit                                                       
reinit_test reinit_test                                                  
reset       Reset the system                                             
route       set static route                                             
rssi        rssi                                                         
alias       show alias table                                             
rtl_vlan    rtl_vlan                                                     
sp          Sets a threads priority                  [thread ID]         
setmib      setmib                                                       
show        show                                                         
tcpstats    tcpstats                                                     
test_skb    test_skb                                                     
kill        Kills a running thread                   [thread ID]         
release     Break a thread out of any wait           [thread ID]         
trigger_port trigger_port                                                 
uptime      Shows system uptime                                          
watchdog    on/off/res/reboot                                            

Tenda's command:
msg         ipnat       spi_unlock  rs          show_arp_bufce_power    
app_debug   build       stat_link   link_status iwpriv      realtek     
splx        thread      mbuf        debug       route       et          
wlconf      fw          ping        ifconfig    syslog      time        
tenda_arp   arp         envram      nvram       restart     reboot      
ifconfig6   cat         route6      ping6       

Boot sequence

The UART boot log was very talkative and provided a number of useful details: the SoC and flash vendor/type, DRAM type, DRAM size and frequency, the flash part number, vendor messages, the kernel decompression address and startup address, MAC address, and the web server and UPnP services started by the system.

In conclusion, the boot messages revealed:
- The SoC (RTL8197F family) and bootloader version/date.
- The flash chip vendor and model (Winbond W25Q16 or similar) and its size (2 MB).
- The kernel image load/start addresses and that the kernel was decompressed at boot.
- The embedded web server GoAhead.

Booting...
init_ram

DDR init OK
init ddr ok

DRAM Type: DDR2
    DRAM frequency: 533MHz
    DRAM Size: 32MB
JEDEC id EF4015, EXT id 0x0000
found w25q16
flash vendor: Winbond
Flash is locked!
w25q16, size=2MB, erasesize=4KB, max_speed_hz=41MHz
auto_mode=0 addr_width=3 erase_opcode=0x00000020
Write PLL1=80c00042
=>CPU Wake-up interrupt happen! GISR=89000080 

RTL8197F-VG boot release version:672 (Sep 18 2020-09:23:56) (999MHz)
Mac addr:58-d9-d5-d2-6b-60
use Switch new descriptor
wait for upgrage
port link 0x000030e0 0x000030e0 0x000030e0 0x000030e0
irq:0x00008080
Jump to image start:0x80700000...
decompressing kernel:
Uncompressing... done, booting the kernel.
done decompressing kernel.
start address: 0xa0000600

...SNIP...

Configuration for Embedthis GoAhead Community Edition
---------------------------------------------
Version:            5.1.0
BuildType:          Release
CPU:                mips
OS:                 ecos
Host:               192.168.0.1
Documents:          
---------------------------------------------
Started http://192.168.0.1:80
Started https://192.168.0.1:443
Started http://fe80:0001::5ad9:d5ff:fed2:6b60:80
Started https://fe80:0001::5ad9:d5ff:fed2:6b60:443
[TPI->tpi_http_start->143]:http start success!
[TPI->tpi_upnp_start->107]:start success!

...SNIP...

This boot sequence gives a lot of future useful informations, from the name of the Chip holding the flash, to the name of the microcontroller and the address where the kernel is situated, as well as the name of the linux kernel.

Firmware dump

We used two complementary approaches to obtain the firmware: direct SPI flash reading and memory dumping via UART.

From SPI

The flash chip on the board is a Winbond W25Q16 (W25Q16JVSSIQ). It communicates via SPI and all its pins were accessible. We initially tried to capture the SPI traffic with a low-end logic analyzer, but it was limited to 24 MHz and couldn’t reliably capture the flash operating at ~130 MHz. So we switched to a dedicated SPI programmer (ch341a) and used flashrom to read the chip directly.

"SPI dump"

~ $ flashrom --verbose --chip "W25Q16.V" --programmer ch341a_spi --read content-v3.bin

The SPI read produced a 2 MB image, which we further analyzed with binwalk.

~ $ binwalk flash-dump.bin  

/home/kaddate/projets/Tenda_Router/tristan_tenda/extract/content.bin
------------------------------------------------------------
DECIMAL                            HEXADECIMAL                        DESCRIPTION
------------------------------------------------------------
35096                              0x8918                             CRC32 polynomial table, little endian
36192                              0x8D60                             gzip compressed data, operating system: Unix, timestamp: 2020-09-18 01:24:02, total size: 39102 bytes
141336                             0x22818                            LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, compressed size: 1503886 bytes, uncompressed size: 5150536 bytes
------------------------------------------------------------

Analyzed 1 file for 85 file signatures (187 magic patterns) in 43.0 milliseconds

From UART

Because the SoC maps the SPI flash into the CPU address space at the address 0xb0000000 into a region of 128MB, we were also able to dump memory via the UART shell using the db command. We requested a large read (about 10 MB) from the flash-mapped region using the db command and saved the textual hexdump from the UART to a file. The dump itself took more than 3 hours.

~ $ db 0xb0000000 0xa00000

Converting the hexdump back into a binary gave a dump that closely matched the SPI read. The fact that the data repeats itself is not explained (see binwalk output), but my guess is that when reading the memory of the Flash Chip using the UART interface, the SoC actually makes read calls to the Chip and it may apply a modulus on the addresses that exceeds its memory, like most CPUs. In any case the UART-based extraction produced the same firmware blobs (DEFLATE/LZMA) as the SPI read.

~$ binwalk laptop_flash-dump-uart.bin                          

                      /home/kaddate/projets/Tenda_Router/UART/laptop_flash-dump-uart.bin
---------------------------------------------------------------------------------------------------------------
DECIMAL                            HEXADECIMAL                        DESCRIPTION
---------------------------------------------------------------------------------------------------------------
35096                              0x8918                             CRC32 polynomial table, little endian
36192                              0x8D60                             gzip compressed data, operating system: Unix, timestamp: 2020-09-18 01:24:02, total size: 39102 bytes
2132248                            0x208918                           CRC32 polynomial table, little endian
2133344                            0x208D60                           gzip compressed data, operating system: Unix, timestamp: 2020-09-18 01:24:02, total size: 39102 bytes
2238488                            0x222818                           LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, compressed size: 1503886 bytes, uncompressed size: 5150536 bytes
4229400                            0x408918                           CRC32 polynomial table, little endian
4230496                            0x408D60                           gzip compressed data, operating system: Unix, timestamp: 2020-09-18 01:24:02, total size: 39102 bytes
4335640                            0x422818                           LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, compressed size: 1503886 bytes, uncompressed size: 5150536 bytes
6326552                            0x608918                           CRC32 polynomial table, little endian
6327648                            0x608D60                           gzip compressed data, operating system: Unix, timestamp: 2020-09-18 01:24:02, total size: 39102 bytes
6432792                            0x622818                           LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, compressed size: 1503886 bytes, uncompressed size: 5150536 bytes
8423704                            0x808918                           CRC32 polynomial table, little endian
8424800                            0x808D60                           gzip compressed data, operating system: Unix, timestamp: 2020-09-18 01:24:02, total size: 39102 bytes
---------------------------------------------------------------------------------------------------------------

Analyzed 1 file for 85 file signatures (187 magic patterns) in 208.0 milliseconds

kernel dump

Another dump possible is the dump of the RAM, We figured that if the Kernel is packed in some ways, the RAM would held the unpacked kernel. We dumped it using the UART terminal and the informations given by the boot and the microcontroller datasheet.

  UART binwalk laptop_memory_dump_uart.bin                 

                                         /home/kaddate/projets/Tenda_Router/UART/laptop_memory_dump_uart.bin
-----------------------------------------------------------------------------------------------------------------------------------------------------
DECIMAL                            HEXADECIMAL                        DESCRIPTION
-----------------------------------------------------------------------------------------------------------------------------------------------------
384                                0x180                              eCos kernel exception handler, MIPS little endian
3377032                            0x338788                           Copyright text: "Copyright (c) Embedthis Software Inc., 1993-2014. All Rights 
                                                                      Reserved.Copyright (c) GoAhead Software"
3449592                            0x34A2F8                           CRC32 polynomial table, little endian
3451968                            0x34AC40                           SHA256 hash constants, little endian
3482168                            0x352238                           SHA256 hash constants, little endian
3507104                            0x3583A0                           AES S-Box
3959612                            0x3C6B3C                           PEM certificate
3967792                            0x3C8B30                           PEM private key
3969468                            0x3C91BC                           PNG image, total size: 1332 bytes
3970804                            0x3C96F4                           PNG image, total size: 392 bytes
3971200                            0x3C9880                           GIF image, 18x18 pixels, total size: 1407 bytes
3972608                            0x3C9E00                           PNG image, total size: 13652 bytes
3986264                            0x3CD358                           PNG image, total size: 848 bytes
3987116                            0x3CD6AC                           PNG image, total size: 1187 bytes
3988304                            0x3CDB50                           PNG image, total size: 15089 bytes
4003396                            0x3D1644                           PNG image, total size: 354 bytes
4003791                            0x3D17CF                           SVG image, total size: 1322 bytes
4005116                            0x3D1CFC                           PNG image, total size: 3421 bytes
4008540                            0x3D2A5C                           PNG image, total size: 939 bytes
4009480                            0x3D2E08                           PNG image, total size: 871 bytes
4010352                            0x3D3170                           PNG image, total size: 1523 bytes
4011915                            0x3D378B                           SVG image, total size: 1267 bytes
4013184                            0x3D3C80                           PNG image, total size: 2809 bytes
4015996                            0x3D477C                           PNG image, total size: 609 bytes
4016608                            0x3D49E0                           PNG image, total size: 767 bytes 
4434540                            0x43AA6C                           Copyright text: "Copyright 2013 Scott Jehl * Licensed under 
                                                                      https://github.com/scottjehl/Respond/blob/master/LICENSE"
4457708                            0x4404EC                           Copyright text: "Copyright 2014 ET.W * Licensed under Apache License v2.0 * * 
                                                                      The REasy UI for router, and themes "
4605608                            0x4646A8                           AES S-Box
5690224                            0x56D370                           AES S-Box
5698928                            0x56F570                           AES S-Box
6880900                            0x68FE84                           CRC32 polynomial table, little endian
-----------------------------------------------------------------------------------------------------------------------------------------------------

Analyzed 1 file for 85 file signatures (187 magic patterns) in 49.0 milliseconds

The RAM dump contained many recognizable artifacts: embedded web resources (HTML/CSS/JS), images, certificates/keys, and cryptographic constants (SHA256, AES S-box tables), as well as strings and license texts referencing embedded components (for example, GoAhead web server and some open-source UI libraries). We also observed and identified eCos Kernel pattern which indicates that we might have dumped the unpacked kernel correctly.

Looking into the Bootloader

Since we don't how the bootloader launches the kernel, we decided to throw it into Ghidra. We started by identifying the entry() function located at 0x0c in the SPI flash and 0xb000000c in the SoC. It first configures the lower bits of a set of proprietary pin registers (REG_PINMUX_04, REG_PINMUX_05), then writes to undocumented UART registers in the rt_uart_1 memory region to print the first boot string: Booting....

After some coprocessor configuration, the bootloader initializes the DRAM. Unfortunately, much of the code interacts with proprietary, undocumented memory regions that are neither described in the datasheet nor accessible from the UART shell. Attempts to read those regions via the UART just return zeros, which suggests they may be protected or bank-switched.

The next major function we identified prepares a buffer using a routine that we identified as memset(), then calls several subroutines that depend heavily on vendor-specific registers and interrupt/handler tables. These routines appear to configure hardware peripherals and low-level runtime state before jumping to the decompression/kernel entry.

Overall, we were able to map out the high-level responsibilities of the boot stages and name several key routines, but we did not reverse every detail. Determining the exact purpose of each pin and register will require significant additional work. Iterating between static analysis in Ghidra, dynamic testing on hardware, and emulation to observe side effects, confirm register semantics, and handle any protection mechanisms.

Notes and lessons learned

  • First and foremost: methodology. Since this was our first hardware-hacking attempt, we learned a lot about how to structure future sessions. We realised we rushed in headfirst and should have been more patient. Spending more time analysing the data and artifacts we found, planning our steps, and iterating between static and dynamic analysis instead of jumping straight to the next action.
  • Hardware hacking can be cheap! However, tool quality matters. While inexpensive tools are great to get started, they can also become a limitation. Our low-end logic analyser couldn’t capture the flash at its actual clock speed, forcing us to fall back on the SPI programmer and UART extraction (which was also very shady). For similar targets, it’s worth investing in (or borrowing) a slightly better analyser or programmer and pogo pins.