Emulating the Tenda AC15 Wi-Fi Router with EMUX

by Saumil Shah @therealsaumil

June 2020 (revised October 2021)

TL;DR:

The Tenda AC15 Wi-Fi Router is a popular dual-band Gigabit WiFi router. For all its hardware prowess, the underlying software is rather atrocious It is rife with vulnerabilities and notorious for harvesting private data and sending it "somewhere in the cloud". I emulated the AC15 router to study the software's inner workings.

Emulating the AC15 router was a challenging exercise. The firmware couldn't be obtained via the standard UART extraction technique that I usually use for most IoT devices. The AC15 doesn't store all its configuration data in the nvram, some of it is in a separate MTD partition. Launching the AC15's start up programs caused them to crash and the system to reboot, because certain custom library calls didn't return the values that were expected, due to missing emulated hardware. It took quite a bit of reverse engineering to figure out which functions we need to shim using LD_PRELOAD.

Eventually, the labour bore fruit.

EMUX Main Menu Tenda AC15 Emulation

The emulation process was an great learning experience for me, which I present here. Read on!

g00diez where?

The emulated Tenda AC15 is included in the EMUX Docker Image. You're welcome.

Introduction

The COVID-19 lockdown saw me working hard at EMUX, fixing bugs and adding new features for my upcoming ARM IoT Firmware Laboratory training. I wanted to show off EMUX's nvram handling capabilities and also to emulate a "popular" ARM IoT device that readers can easily get hardware access to.

The Tenda AC15 is complex enough when it comes to emulation. This case study is written more as a tutorial for emulating your own hardware device.

The approach described here does not require you to have a physical Tenda router on hand. All you need is a copy of the Tenda firmware from its website.

I subsequently intend to publish two more articles

Contents

Section 1 - Obtaining the Firmware

There are three popular techniques for obtaining the firmware. My preference is to grab the firmware from the physical router. This way, we can also obtain a snapshot of the configuration stored in its nvram.

Inside the Tenda AC15

  1. Discover the UART, access the /dev/console, hopefully find a shell and dump the /dev/mtdblocks. This is my preferred technique.
  2. Extract the firmware directly from the SPI Flash memory using an EEPROM Programmer.
  3. binwalk the firmware update published on the vendor's website (or CDROM). This is my least preferred technique since it doesn't give us a copy of the nvram contents.

I shall cover techniques 1 and 2 in a separate post.

1.1 Download the AC15 Firmware

My router is running firmware version 15.03.05.18. Grab it from http://down.tendacn.com/uploadfile/AC15/US_AC15V1.0BR_V15.03.05.18_multi_TD01.zip

Tenda Firmware Download

1.2 binwalk the firmware

Our goal is to "footprint" the firmware and eventually perform a baseline static analysis of the extracted file system. We are looking for:

# binwalk -e US_AC15V1.0BR_V15.03.05.18_multi_TD01.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
64            0x40            TRX firmware header, little endian,
                              image size: 10559488 bytes, CRC32: 0xFA817870,
                              flags: 0x0, version: 1, header size: 28 bytes,
                              loader offset: 0x1C, linux kernel offset: 0x1C9CD4,
                              rootfs offset: 0x0
92            0x5C            LZMA compressed data, properties: 0x5D,
                              dictionary size: 65536 bytes,
                              uncompressed size: 4585280 bytes
1875220       0x1C9D14        Squashfs filesystem, little endian, version 4.0,
                              compression:xz, size: 8680744 bytes, 926 inodes,
                              blocksize: 131072 bytes,
                              created: 2017-05-10 14:10:50

Contents of _US_AC15V1.0BR_V15.03.05.18_multi_TD01.bin.extracted:

-rw-r--r--    1 root     krafty     8680744 Jun  2 12:11 1C9D14.squashfs
-rw-r--r--    1 root     krafty     4585280 Jun  2 12:11 5C
-rw-r--r--    1 root     krafty    10559460 Jun  2 12:11 5C.7z
drwxr-xr-x   15 root     root          4096 May 10  2017 squashfs-root

Contents of squashfs-root:

drwxr-xr-x    2 root     root          4096 May 10  2017 bin
drwxr-xr-x    2 root     root          4096 May 10  2017 cfg
drwxr-xr-x    2 root     root          4096 May 10  2017 dev
lrwxrwxrwx    1 root     root             8 Jun  2 12:11 etc -> /var/etc
drwxr-xr-x    8 root     root          4096 May 10  2017 etc_ro
lrwxrwxrwx    1 root     root             9 Jun  2 12:11 home -> /var/home
lrwxrwxrwx    1 root     root            11 Jun  2 12:11 init -> bin/busybox
drwxr-xr-x    3 root     root          4096 May 10  2017 lib
drwxr-xr-x    2 root     root          4096 May 10  2017 mnt
drwxr-xr-x    2 root     root          4096 May 10  2017 proc
lrwxrwxrwx    1 root     root             9 Jun  2 12:11 root -> /var/root
drwxr-xr-x    2 root     root          4096 May 10  2017 sbin
drwxr-xr-x    2 root     root          4096 May 10  2017 sys
drwxr-xr-x    2 root     root          4096 May 10  2017 tmp
drwxr-xr-x    6 root     root          4096 May 10  2017 usr
drwxr-xr-x    6 root     root          4096 May 10  2017 var
lrwxrwxrwx    1 root     root            11 Jun  2 12:11 webroot -> var/webroot
drwxr-xr-x    8 root     root          4096 May 10  2017 webroot_ro

Strings dump of file 5C:

:
root=/dev/mtdblock2 console=ttyS0,115200 init=init earlyprintk debug
:
Linux version 2.6.36.4brcmarm (root@linux-bkb8) (gcc version 4.5.3 (Buildroot 20
12.02) ) #1 SMP PREEMPT Wed May 10 21:54:27 CST 2017
%s version %s (root@linux-bkb8) (gcc version 4.5.3 (Buildroot 2012.02) ) %s
:
2.6.36.4brcmarm SMP preempt mod_unload modversions ARMv7
:

We now have the kernel version, boot arguments and contents of the root file system.

1.3 Tracing our way through the init process

From the boot arguments, we see that the kernel invokes /sbin/init as the first userland process after starting up. init in turn shall inspect /etc/inittab and proceed with the userland portion of the startup accordingly.

/etc/inittab doesn't exist at boot time, however we realise that the /etc_ro directory is copied over to /etc/ at some point in the initialisation process.

Contents of /etc_ro/inittab:

::sysinit:/etc_ro/init.d/rcS
ttyS0::respawn:/sbin/sulogin
::ctrlaltdel:/bin/umount -a -r
::shutdown:/usr/sbin/wl radio off
::shutdown:/usr/sbin/wl -i eth2 radio off

The startup script /etc_ro/init.d/rcS performs the following actions:

:
cfmd &
echo '' > /proc/sys/kernel/hotplug
udevd &
logserver &

tendaupload &
if [ -e /etc/nginx/conf/nginx_init.sh ]; then
    sh /etc/nginx/conf/nginx_init.sh
fi

moniter &
telnetd &
:
  1. Creates ramfs and mount points
  2. Copies /etc_ro to /etc
  3. Copies /webroot_ro to /webroot
  4. Launches the startup daemons
  5. The following daemons are started:

The startup process is summarised in the diagram below:

Tenda init scripts

Section 2 - Emulation attempt #1

To create a new EMUX device, follow these steps:

Creating a device in EMUX

  1. Create a device directory in EMUX for the Tenda AC15.
  2. Choose a kernel.
  3. Copy the root file system into the device directory.
  4. We shall leave nvram.ini untouched for now, since we don't have the nvram contents.
  5. Edit the device config file.
  6. Create the Tenda record in /emux/devices

2.1 Creating the Tenda device directory

We will use the ID TENDA for Create a new device directory under /emux by copying the /emux/template directory as /emux/TENDA:

$ cd /emux/
$ cp -r template TENDA

2.2 Choosing a kernel

I will choose kernel 2.6.39.4 as the base kernel for emulating the Tenda AC15. This kernel is already included with the EMUX release. I will describe how to compile your own kernel (specifically version 2.6.36.4 to match the version running on the router) in a separate article.

The kernel is present as zImage-2.6.39.4-vexpress.

2.3 Copy the root file system

Move the squashfs-root directory extracted by binwalk (Section 1.2) into /emux/TENDA/squashfs-root.

2.4 nvram.ini

Since we do not have the contents of nvram, we shall leave nvram.ini as-is. Later, we shall populate nvram.ini once we gain more insight into the Tenda startup process.

2.5 Edit the config file

Contents of /emux/TENDA/config:

# Tenda AC15 EMUX configuration
#
id=TENDA
nvram=nvram.ini
rootfs=squashfs-root
randomize_va_space=0
initcommands="/etc_ro/init.d/rcS;/bin/sh"

2.6 Create a record in /emux/devices

TENDA,qemu-system-arm-6.0.0,vexpress-a9,,,256M,zImage-2.6.39.4-vexpress,VEXPRESS2,Tenda AC15 Wi-Fi Router

2.7 Starting the Tenda AC15, attempt #1

The /emux/TENDA directory looks like:

/emux/TENDA
       |
       |--- config
       |--- kernel
       |      |
       |      `--- zImage-2.6.39.4-vexpress
       |
       |--- nvram.ini
       `--- squashfs-root

Start the EMUX Launcher with the Tenda AC15 kernel

EMUX Launcher for Tenda AC15

From the EMUX Docker shell, start the Tenda AC15 init scripts

Start the Tenda AC15 init scripts

Output: (few errors have been removed)

Starting Tenda AC15 Wi-Fi Router
Loading nvram from nvram.ini
key1='value1'
nv_get_config_data: Can't get config data
:      :
key10='value10'
>>> Starting TENDA
[+] chroot /emux/TENDA/squashfs-root /.emux/emuxinit
mkdir: can't create directory '/var/run': File exists
init_core_dump 1816: rlim_cur = 0, rlim_max = 0
init_core_dump 1825: open core dump success
init_core_dump 1834: rlim_cur = 5242880, rlim_max = 5242880
func:InitCfm begin load_mib....
Could not open mtd device 2
[get_mtd_size] Can't open file:CFM
[get_mtd_size] Can't open file:CFM_BACKUP
malloc buf failed
load mib failed.
flash_to_file:open Policy failed.
going to rewrite flash...
open Policy failed.
rewrite flash ok...

######## STARTING PROGRAM #########
spawn-fcgi: child spawned successfully: PID: 683
~ # /dev/nvram: No such device

###########################################################
###nvram partition is destory, restore by default and reboot##
###########################################################
Could not open mtd device 1
envram_init: read flash error
/dev/nvram: No such device
load nvram from /webroot/nvram_default.cfg...
/dev/nvram: No such device
/dev/nvram: No such device

(Sometime after this the kernel reboots)

In summary, we have:

First Attempt

Section 3 - Reverse Engineering with Ghidra

I used Ghidra to reverse engineer the binaries invoked by /etc_ro/init.d/rcS along with various shared libraries found in /lib

These are the functions I reversed in the first pass:

Reversing First Pass

3.1 Code snippets from reversed functions - part 1

InitCfm was revealed from the startup messages.

int InitCfm(void)
{
    :
    printf("func:%s begin load_mib....\n","InitCfm");
    load_mib(0);     // next step
    :
}

and so was load_mib

int load_mib(int flags)
{
  :
  mib_open();
  iVar1 = mib_Load("/webroot/default.cfg",flags);     // important filename
  :
}

Inspection of /webroot_ro/default.cfg reveals that it stores the factory default configuration of the AC15.

int mib_Load(char *filename,int flags)
{
  :
  iVar1 = get_cfm_blk_size_from_cache();
    :
    if ((flags != 0) || (flag_init = nvram_cfm_init(buf_00,iVar1 << 3), flag_init == 0)) {
    :
    :
}

Next, reverse get_cfm_blk_size_from_cache and nvram_cfm_init

int get_cfm_blk_size_from_cache(void)
{
  :
  iVar1 = get_flash_type();
  if (iVar1 == 4) {
    cfm_size_from_cache = 0x20000;
  }
  else {
    :
  }
  return cfm_size_from_cache;
}

Observation: get_flash_type()'s return value is checked to see if it is 4

uint get_flash_type(void)
{
  int iVar1;
  uint uVar2;
  byte local_34 [32];
  int local_14;

  local_14 = mtd_open(&DAT_0002de2c,2);
  if (local_14 < 0) {
    fwrite("Could not open mtd device 2\n",1,0x1c,stderr);
    uVar2 = 0xffffffff;
  }
  else {
    iVar1 = ioctl(local_14,0x80204d01,local_34);
    if (iVar1 == 0) {
      close(local_14);
      uVar2 = (uint)local_34[0];
    }
    else {
      fwrite("Could not get mtd device info\n",1,0x1e,stderr);
      close(local_14);
      uVar2 = 0xffffffff;
    }
  }
  return uVar2;
}

Observation: In the absence of MTD emulation, it is imperative to fake get_flash_type() = 4

3.2 Code snippets from reversed functions - part 2

Continuing with nvram_cfm_init

int nvram_cfm_init(char *buf,int len)
{
  :
  iVar1 = get_flash_type();
  if (iVar1 == 4) {
    ret = cfm_file_init(buf,len);
  }
  else {
    ret = cfm_mtd_init(buf);
  }
  return ret;
}

Observation: If get_flash_type() == 4 then the code execution follows cfm_file_init and not cfm_mtd_init. The latter would prove to be difficult in the light of missing MTD hardware.

int cfm_file_init(char *buf,int len)
{
  :
  __fd = mount("/dev/mtdblock7","/cfg","jffs2",0,(void *)0x0);
  if (__fd != 0) {
    puts("Erase nflash MTD partition and mount again");
    system("/bin/flash_erase /dev/mtd7 0x0 0x0");
    __fd = mount("/dev/mtdblock7","/cfg","jffs2",0,(void *)0x0);
    if (__fd != 0) {
      printf("Mount nflash MTD jffs2 partition %s to %s failed\n","/dev/mtdblock7",&DAT_0001e984);
    }
  }
  pcVar1 = cfm_get_new_file_name();
  __file = pcVar1;
  if (pcVar1 == (char *)0x0) {
    __file = "/cfg/mib.cfg";
  }
  :
  : // long function
  :
}

Observation: cfm_file_init opens /cfg/mib.cfg for reading initial configuration values, after mounting /dev/mtdblock7 to /cfg.

char * cfm_get_new_file_name(void)
{
  :
  iVar1 = cfm_get_new_file_flag(&cfm_flag,"/cfg/mib.cfg");
  if (iVar1 == 0) {
    cfm_flag = 0;
  }
  iVar1 = cfm_get_new_file_flag(&cfm_backup_flag,"/cfg/mib_backup.cfg");
  if (iVar1 == 0) {
    cfm_backup_flag = 0;
  }
  :
}

Observation: cfm_get_new_file_name looks for /cfg/mib.cfg and /cfg/mib_backup.cfg. Eventually we shall see that these files are created by this function with configuration data populated from /webroot_ro/default.cfg. (3.1)

3.3 Error message "nvram partition is destroy"

From the startup messages, we see an error message nvram partition is destroy flash past and thereafter the system reboots. Using Ghidra to search for this error message leads us to FUN_0000e640 in cfmd:

void FUN_0000e460(void)
{
  __s1 = (void *)bcm_nvram_get("default_nvram");
  if ((__s1 == (void *)0x0) || (iVar1 = memcmp(__s1,"default_nvram",0xd), iVar1 != 0)) {
    puts("\x1b[0;32;31m\n###########################################################");
    puts("###nvram partition is destory, restore by default and reboot##");
    printf("###########################################################\n\x1b[m");
    RestoreNvram();
    doSystemCmd("reboot");
  }
  return;
}

Observation: FUN_0000e460 calls:

3.4 bcm_nvram_ functions

char * bcm_nvram_get(char *name)
{
  :
  iVar2 = bcm_nvram_init((void *)0x0);
      :
      sVar3 = read(nvram_fd,off,__size);
  :
  return value;
}

Observation: in the absence of physican nvram, we need to map this function to nvram_get in libnvram-armx

int bcm_nvram_init(void *unused)
{
  :
  if (nvram_fd < 0) {
    nvram_fd = open("/dev/nvram",2);
    if (-1 < nvram_fd) {
      nvram_buf = (char *)mmap((void *)0x0,0x10000,1,1,nvram_fd,0);
      if (nvram_buf != (char *)0xffffffff) {
        fcntl(nvram_fd,2,1);
        return 0;
      }
      close(nvram_fd);
      nvram_fd = -1;
    }
    perror("/dev/nvram");
    piVar1 = __errno_location();
    iVar2 = *piVar1;
  }
  :
}

Observation: All the /dev/nvram error messages are coming from here.

void RestoreNvram(void)
{
  envram_to_nvram();
  bcm_nvram_restore();
  return;
}
int bcm_nvram_restore(void)
{
  pcVar1 = bcm_nvram_get("image_boot");
  :
  __stream = fopen("/webroot/nvram_default.cfg","r");
  if (__stream != (FILE *)0x0) {
    printf("load nvram from %s...\n","/webroot/nvram_default.cfg");
    while (__src = fgets(mib_val,0x100,__stream), __src != (char *)0x0) {
        :
        bcm_nvram_set(mib_val,__src);
        memset(mib_val,0,0x100);
        :
    }
    fclose(__stream);
  }
  if (pcVar1 != (char *)0x0) {
    bcm_nvram_set("image_boot",imboot_value);
    bcm_nvram_commit();
  }
  bcm_nvram_set("default_nvram","default_nvram");
  iVar3 = bcm_nvram_commit();
  return iVar3;
}

Observation: If the system detects an empty nvram, RestoreNvram and bcm_nvram_restore populates the nvram with values from /webroot_ro/nvram_default.cfg.

Section 4 - Emulation attempt #2 - hooked functions

To proceed with the emulation, it is necessary to:

  1. map the following functions:
  2. fake get_flash_type to always return 4
  3. populate the nvram with meaningful values

4.1 tenda_hooks.c

I wrote tenda_hooks.c along the same lines as my earlier work custom_nvram_r6250.c.

Source: https://github.com/therealsaumil/custom_nvram/blob/master/tenda_hooks.c

Pre-compiled tenda_hooks.so and tenda_hooks_verbose.so along with libnvram-armx.so are available at https://github.com/therealsaumil/custom_nvram/

4.2 Using tenda_hooks.so

To use these hooks, we need to LD_PRELOAD them before invoking /etc_ro/init.d/rcS

The contents of /emux/TENDA/config are now as follows:

# Tenda AC15 EMUX configuration
#
id=TENDA
nvram=
rootfs=squashfs-root
randomize_va_space=0
initcommands="export LD_PRELOAD=/lib/libnvram-armx.so:/lib/tenda_hooks_verbose.so;/etc_ro/init.d/rcS;/bin/sh"

4.3 Preparing for a limited emulation

My goal was to let cfmd run FUN_0000e460() successfully, so that RestoreNvram() can complete its task in populating the nvram values. And I wanted to grab the freshly populated nvram values before the router rebooted from doSystemCmd("reboot"). as described in 3.3

First, we change /sbin/reboot as follows:

#!/bin/sh
echo "*** REBOOT INVOKED ***" > /dev/console
sleep 600

Next, we edit /etc_ro/init.d/rcS to prevent startup daemons subsequent to cfmd from running:

:
chmod +x /etc/mdev.conf

cfmd &
read -p "*** cfmd started *** " # Pause
echo '' > /proc/sys/kernel/hotplug
udevd &
logserver &
:

4.4 Starting the Tenda AC15, attempt #2

  1. Start the EMUX Launcher with the Tenda AC15 kernel
  2. From the EMUX Docker shell, start the Tenda AC15 init scripts

The startup output is as follows: (similar lines have been truncated)

Starting Tenda AC15 Wi-Fi Router
Syntax: ./run-init <ini file>
>>> Starting TENDA
[+] chroot /emux/TENDA/squashfs-root /.emux/emuxinit
mkdir: can't create directory '/var/run': File exists
*** cfmd started *** init_core_dump 1816: rlim_cur = 0, rlim_max = 0
init_core_dump 1825: open core dump success
[0x400821bc] system('echo /tmp/core-%e-%p-%t-%s > /proc/sys/kernel/core_pattern') = 0
init_core_dump 1834: rlim_cur = 5242880, rlim_max = 5242880
func:InitCfm begin load_mib....
[0x40054598] get_flash_type() = 4
[0x400555b0] get_flash_type() = 4
Erase nflash MTD partition and mount again
flash_erase: error!: /dev/mtd7
         error 2 (No such file or directory)
[0x400550dc] system('/bin/flash_erase /dev/mtd7 0x0 0x0') = 65280
Mount nflash MTD jffs2 partition /dev/mtdblock7 to /cfg failed
Failed to open persistent data file: /cfg/mib.cfg
open: No such file or directory
Failed to open persistent data file: /cfg/mib.cfg
open: No such file or directory
Failed to open persistent data file: (null)
open: No such file or directory
loading default config from /webroot/default.cfg file...
[0x400cb9b4] Can't open
nv_get_config_data: Can't get config data
bcm_nvram_get('et0macaddr') = ''
[0x400cb9b4] bcm_nvram_get('et0macaddr') = ''
[0x400cb9b4] bcm_nvram_get('et0macaddr') = ''
[0x400cb9b4] bcm_nvram_get('et0macaddr') = ''
[0x400cbbc4] bcm_nvram_get('1:macaddr') = ''
[0x400cbbdc] bcm_nvram_get('1:macaddr') = ''
get_wlan_mac 700: get macaddr from nvram error
 not exist!(mib.c restore_change_wifi_ssid)
Could not open mtd device 1
envram_init: read flash error
Could not open mtd device 1
envram_init: read flash error
Could not open mtd device 1
envram_init: read flash error
error power wl2g.power [rate:2]
Could not open mtd device 1
envram_init: read flash error
error power wl5g.power [rate:5]
flash_to_file:open Policy failed.
going to rewrite flash...
open Policy failed.
rewrite flash ok...
[0x0000e48c] bcm_nvram_get('default_nvram') = ''

###########################################################
###nvram partition is destory, restore by default and reboot##
###########################################################
Could not open mtd device 1
envram_init: read flash error
[0x40057834] bcm_nvram_get('image_boot') = ''
[0x40057854] bcm_nvram_get('image_boot') = ''
load nvram from /webroot/nvram_default.cfg...
[0x400579ec] [nvram 0] bcm_nvram_set('0:boardvendor', '0x14E4')
[0x400579ec] [nvram 0] bcm_nvram_set('0:ledbh10', '7')
[0x400579ec] [nvram 0] bcm_nvram_set('0:leddc', '0xffff')
[0x400579ec] [nvram 0] bcm_nvram_set('1:ledbh2', '7')
:            :         :
:            :         :
:            :         :
[0x400579ec] [nvram 0] bcm_nvram_set('wl_txbf_imp', '0')
[0x400579ec] [nvram 0] bcm_nvram_set('wl0_txbf_imp', '0')
[0x400579ec] [nvram 0] bcm_nvram_set('wl1_txbf_imp', '0')
[0x40057a70] [nvram 0] bcm_nvram_set('image_boot', '')
/dev/nvram: No such device
[0x40057a90] [nvram 0] bcm_nvram_set('default_nvram', 'default_nvram')
/dev/nvram: No such device
[0x400821bc] system('reboot') = 2

There are around 1000+ bcm_nvram_set() calls, all triggered by RestoreNvram. The emulation is paused at this point. It seems our nvram will now contain meaningful values.

4.5 Creating nvram_AC15.ini

To grab the nvram contents populated by RestoreNvram, open the EMUX HOSTFS Shell from the EMUX terminal, and run the following:

EMUX HOSTFS [TENDA]:~> nvram show > /emux/TENDA/nvram_AC15.ini

As an aside, the /cfg directory is still empty. At some point, it will get populated with mib.cfg as observed in 3.2

Section 5 - The Emulated Tenda AC15

Armed with working data in nvram_AC15.ini, we can attempt to run the full emulation.

Before we proceed, we need to restore /sbin/reboot and /etc_ro/init.d/rcS to their original versions.

5.1 Starting the Tenda AC15, attempt #3

Set the nvram= parameter in /emux/TENDA/config as follows:

# Tenda AC15 EMUX configuration
#
id=TENDA
nvram=nvram_AC15.ini
rootfs=squashfs-root
randomize_va_space=0
initcommands="export LD_PRELOAD=/lib/libnvram-armx.so:/lib/tenda_hooks_verbose.so;/etc_ro/init.d/rcS;/bin/sh"

Start the emulation once again as follows:

  1. Start the EMUX Launcher with the Tenda AC15 kernel
  2. From the EMUX Docker shell, start the Tenda AC15 init scripts

The startup process continues past cfmd. The system does not reboot. A condensed version of the output (with only the relevant parts) is shown below:

Starting Tenda AC15 Wi-Fi Router
Loading nvram from nvram_fresh.ini
0:boardvendor='0x14E4'
Can't open
nv_get_config_data: Can't get config data
0:ledbh10='7'
0:leddc='0xffff'
1:ledbh2='7'
:    :
:    :
wl0_txbf_imp='0'
wl1_txbf_imp='0'
default_nvram='default_nvram'
vlan2ports='0  5'
vlan1ports='1  2  3  4  5*'
>>> Starting TENDA
[+] chroot /emux/TENDA/squashfs-root /.emux/emuxinit
mkdir: can't create directory '/var/run': File exists
init_core_dump 1816: rlim_cur = 0, rlim_max = 0
init_core_dump 1825: open core dump success
init_core_dump 1834: rlim_cur = 5242880, rlim_max = 5242880
func:InitCfm begin load_mib....
[0x40054598] get_flash_type() = 4
[0x400555b0] get_flash_type() = 4
Erase nflash MTD partition and mount again

######## STARTING PROGRAM #########
flash_erase: error!: /dev/mtd7
         error 2 (No such file or directory)
[0x400550dc] system('/bin/flash_erase /dev/mtd7 0x0 0x0') = 65280
Mount nflash MTD jffs2 partition /dev/mtdblock7 to /cfg failed
crc = b5fad1ed, newcrc = b5fad1ed
spawn-fcgi: child spawned successfully: PID: 1492
~ # [0x40054598] get_flash_type() = 4
[0x40054598] get_flash_type() = 4
flash_to_file:open Policy failed.
going to rewrite flash...
open Policy failed.
rewrite flash ok...
:       :
:       :
[0x400821bc] system('ifconfig br0 hw ether 00:01:02:03:04:05') = 256
[0x400821bc] system('ifconfig vlan1 up') = 0
[0x400821bc] system('brctl addbr br0') = 0
[0x400821bc] system('brctl setfd br0 0') = 0
[0x400821bc] system('brctl stp br0 on') = 0
[0x400821bc] system('brctl addif br0 vlan1') = 0
[0x400821bc] system('ifconfig br0 up') = 0
[0x400821bc] system('ifconfig br0 192.168.0.1 netmask 255.255.255.0 ') = 0
Sat Jan  1 00:00:00 UTC 2000
:       :
:       :
iptables: goahead: 4: main----51

goahead: 4: initPlatform----398

goahead: 4: websOpen----233

The system starts up. Looking at some of the system() commands from the output above, we observe that a Bridge Interface br0 has been created and assigned the IP address 192.168.0.1. We can inspect the network interfaces and listening ports from the EMUX HOSTFS Shell:

EMUX HOSTFS [TENDA]:~> ifconfig -a
:         :
br0       Link encap:Ethernet  HWaddr 52:54:00:12:34:56  
          inet addr:192.168.0.1  Bcast:192.168.0.255  Mask:255.255.255.0
          inet6 addr: fe80::1/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:17 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:3388 (3.3 KiB)

br0:dname Link encap:Ethernet  HWaddr 52:54:00:12:34:56  
          inet addr:172.17.245.103  Bcast:172.17.255.255  Mask:255.255.255.255
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1

eth0      Link encap:Ethernet  HWaddr 52:54:00:12:34:56  
          inet addr:192.168.100.2  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::5054:ff:fe12:3456/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:23660 errors:0 dropped:0 overruns:0 frame:0
          TX packets:21755 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:19665957 (18.7 MiB)  TX bytes:3404166 (3.2 MiB)
:         :

Listening ports:

EMUX HOSTFS [TENDA]:~> netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:9000            0.0.0.0:*               LISTEN      
tcp        0      0 172.17.245.103:80       0.0.0.0:*               LISTEN      
tcp        0      0 192.168.0.1:80          0.0.0.0:*               LISTEN      
tcp        0      0 127.0.0.1:10002         0.0.0.0:*               LISTEN      
tcp        0      0 127.0.0.1:10003         0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:10004           0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:8180            0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:5500            0.0.0.0:*               LISTEN      
tcp        0      0 127.0.0.1:8188          0.0.0.0:*               LISTEN      
tcp        0      0 127.0.0.1:10004         127.0.0.1:45174         ESTABLISHED
tcp        0      0 127.0.0.1:56915         127.0.0.1:10003         ESTABLISHED
tcp        0      0 127.0.0.1:10003         127.0.0.1:56915         ESTABLISHED
tcp        0      0 192.168.100.2:838       192.168.100.1:2049      ESTABLISHED
tcp        0      0 192.168.100.2:22        192.168.100.1:34216     ESTABLISHED
tcp        0      0 127.0.0.1:45174         127.0.0.1:10004         ESTABLISHED
tcp        0      0 192.168.100.2:22        192.168.100.1:34218     ESTABLISHED
tcp        0      0 :::22                   :::*                    LISTEN      
tcp        0      0 :::23                   :::*                    LISTEN

Running processes:

EMUX HOSTFS [TENDA]:~> ps ax
:    :        :
1880 root     cfmd
1881 root     cfmd
1882 root     netctrl
1883 root     time_check
1884 root     httpd
1885 root     multiWAN
1886 root     ucloud_v2 -l 4
1887 root     business_proc -l 4
1890 root     time_check
1892 root     time_check
1903 root     business_proc -l 4
1904 root     business_proc -l 4
1905 root     ucloud_v2 -l 4
1922 root     netctrl
1923 root     netctrl
:    :        :

The webserver httpd is indeed running, however it is inaccessible due to the IP address of the network interface it is bound to (br0).

5.2 Fixing the "factory defaults"

As discussed in 3.1 the factory defaults are be loaded from mib_Load("/webroot/default.cfg", flags)

Inspecting /webroot_ro/default.cfg reveals the lan.ip parameter which is set as follows:

lan.ip=192.168.0.1

We also notice files /cfg/mib.cfg and /cfg/mib_backup.cfg are freshly created.

To change the default IP address to 192.168.100.2 as hardcoded in EMUX, I edited the /webroot_ro/default.cfg file:

lan.ip=192.168.100.2
lan.mask=255.255.255.0

I also erased /cfg/mib.cfg and /cfg/mib_backup.cfg.

5.3 Starting the Tenda AC15, final attempt

Now that our observations are complete, we can switch from tenda_hooks_verbose to tenda_hooks, to avoid excessive output. All we want to verify is the IP address should be set to 192.168.100.2 and that the web server should be accessible from a browser.

Edit /emux/TENDA/config:

# Tenda AC15 EMUX configuration
#
id=TENDA
nvram=nvram_AC15.ini
rootfs=squashfs-root
randomize_va_space=0
initcommands="export LD_PRELOAD=/lib/libnvram-armx.so:/lib/tenda_hooks.so;/etc_ro/init.d/rcS;/bin/sh"

Start the emulation once again as follows:

  1. Start the EMUX Launcher with the Tenda AC15 kernel
  2. From the EMUX Docker shell, start the Tenda AC15 init scripts

One by one all the services and daemons start up.

Checking the IP address of br0:

EMUX HOSTFS [TENDA]:~> ifconfig br0
br0       Link encap:Ethernet  HWaddr 52:54:00:12:34:56  
         inet addr:192.168.100.2  Bcast:192.168.100.255  Mask:255.255.255.0
         inet6 addr: fe80::1/64 Scope:Link
         UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
         RX packets:0 errors:0 dropped:0 overruns:0 frame:0
         TX packets:50 errors:0 dropped:0 overruns:0 carrier:0
         collisions:0 txqueuelen:0
         RX bytes:0 (0.0 B)  TX bytes:14282 (13.9 KiB)

Checking the listening ports:

EMUX HOSTFS [TENDA]:~> netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:9000            0.0.0.0:*               LISTEN      
tcp        0      0 172.17.245.103:80       0.0.0.0:*               LISTEN      
tcp        0      0 192.168.100.2:80        0.0.0.0:*               LISTEN      
tcp        0      0 127.0.0.1:10002         0.0.0.0:*               LISTEN      
tcp        0      0 127.0.0.1:10003         0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:10004           0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:8180            0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:5500            0.0.0.0:*               LISTEN      
tcp        0      0 127.0.0.1:8188          0.0.0.0:*               LISTEN      
tcp        0      0 192.168.100.2:22        192.168.100.1:51948     ESTABLISHED
tcp        0      0 127.0.0.1:10004         127.0.0.1:54956         ESTABLISHED
tcp        0      0 127.0.0.1:52552         127.0.0.1:10003         ESTABLISHED
tcp        0      0 192.168.100.2:22        192.168.100.1:51952     ESTABLISHED
tcp        0      0 192.168.100.2:988       192.168.100.1:2049      ESTABLISHED
tcp        0      0 127.0.0.1:10003         127.0.0.1:52552         ESTABLISHED
tcp        0      0 127.0.0.1:54956         127.0.0.1:10004         ESTABLISHED
tcp        0      0 :::22                   :::*                    LISTEN      
tcp        0      0 :::23                   :::*                    LISTEN

5.4 Process Memory Layout

One of the goals of EMUX is to create an emulated process memory layout identical to that on the actual hardware. This allows us to guarantee that memory corruption exploits created on the emulated environment work as-is when targeting the actual hardware.

Presented below is a comparision of the memory layout of the httpd process:

attackbox:~$ emuxmaps httpd
00008000-000f7000 r-xp 00000000 00:0f 278680     /emux/TENDA/squashfs-root/bin/httpd
000ff000-00102000 rw-p 000ef000 00:0f 278680     /emux/TENDA/squashfs-root/bin/httpd
00102000-00124000 rw-p 00000000 00:00 0          [heap]
40000000-40005000 r-xp 00000000 00:0f 278830     /emux/TENDA/squashfs-root/lib/ld-uClibc.so.0
40005000-40006000 rw-p 00000000 00:00 0
40007000-40008000 rw-p 00000000 00:00 0
4000c000-4000d000 r--p 00004000 00:0f 278830     /emux/TENDA/squashfs-root/lib/ld-uClibc.so.0
4000d000-4000e000 rw-p 00005000 00:0f 278830     /emux/TENDA/squashfs-root/lib/ld-uClibc.so.0
4000e000-40015000 r-xp 00000000 00:0f 137945     /emux/TENDA/squashfs-root/lib/libnvram-armx.so
40015000-4001c000 ---p 00000000 00:00 0
4001c000-4001d000 rw-p 00006000 00:0f 137945     /emux/TENDA/squashfs-root/lib/libnvram-armx.so
4001d000-40025000 rw-p 00000000 00:00 0
40025000-40026000 r-xp 00000000 00:0f 137935     /emux/TENDA/squashfs-root/lib/tenda_hooks.so
40026000-4002d000 ---p 00000000 00:00 0
4002d000-4002e000 rw-p 00000000 00:0f 137935     /emux/TENDA/squashfs-root/lib/tenda_hooks.so
4002e000-4003d000 r-xp 00000000 00:0f 278831     /emux/TENDA/squashfs-root/lib/libCfm.so
4003d000-40045000 ---p 00000000 00:00 0
40045000-40046000 rw-p 0000f000 00:0f 278831     /emux/TENDA/squashfs-root/lib/libCfm.so
40046000-40061000 rw-p 00000000 00:00 0
40061000-40081000 r-xp 00000000 00:0f 278841     /emux/TENDA/squashfs-root/lib/libcommon.so
40081000-40088000 ---p 00000000 00:00 0
40088000-40089000 rw-p 0001f000 00:0f 278841     /emux/TENDA/squashfs-root/lib/libcommon.so
40089000-40091000 rw-p 00000000 00:00 0
40091000-40099000 r-xp 00000000 00:0f 278832     /emux/TENDA/squashfs-root/lib/libChipApi.so
40099000-400a0000 ---p 00000000 00:00 0
400a0000-400a1000 rw-p 00007000 00:0f 278832     /emux/TENDA/squashfs-root/lib/libChipApi.so
400a1000-400a3000 r-xp 00000000 00:0f 278868     /emux/TENDA/squashfs-root/lib/libvos_util.so
400a3000-400ab000 ---p 00000000 00:00 0
400ab000-400ac000 rw-p 00002000 00:0f 278868     /emux/TENDA/squashfs-root/lib/libvos_util.so
400ac000-400c0000 r-xp 00000000 00:0f 278869     /emux/TENDA/squashfs-root/lib/libz.so
400c0000-400c7000 ---p 00000000 00:00 0
400c7000-400c9000 rw-p 00013000 00:0f 278869     /emux/TENDA/squashfs-root/lib/libz.so
400c9000-400d4000 r-xp 00000000 00:0f 278858     /emux/TENDA/squashfs-root/lib/libpthread.so.0
400d4000-400db000 ---p 00000000 00:00 0
400db000-400dc000 r--p 0000a000 00:0f 278858     /emux/TENDA/squashfs-root/lib/libpthread.so.0
400dc000-400e1000 rw-p 0000b000 00:0f 278858     /emux/TENDA/squashfs-root/lib/libpthread.so.0
400e1000-400e3000 rw-p 00000000 00:00 0
400e3000-400e4000 r-xp 00000000 00:0f 278855     /emux/TENDA/squashfs-root/lib/libnvram.so
400e4000-400eb000 ---p 00000000 00:00 0
400eb000-400ec000 rw-p 00000000 00:0f 278855     /emux/TENDA/squashfs-root/lib/libnvram.so
400ec000-400f9000 r-xp 00000000 00:0f 278861     /emux/TENDA/squashfs-root/lib/libshared.so
400f9000-40101000 ---p 00000000 00:00 0
40101000-40102000 rw-p 0000d000 00:0f 278861     /emux/TENDA/squashfs-root/lib/libshared.so
40102000-40168000 r-xp 00000000 00:0f 278863     /emux/TENDA/squashfs-root/lib/libtpi.so
40168000-4016f000 ---p 00000000 00:00 0
4016f000-40170000 rw-p 00065000 00:0f 278863     /emux/TENDA/squashfs-root/lib/libtpi.so
40170000-401a2000 rw-p 00000000 00:00 0
401a2000-401b1000 r-xp 00000000 00:0f 278852     /emux/TENDA/squashfs-root/lib/libm.so.0
401b1000-401b9000 ---p 00000000 00:00 0
401b9000-401ba000 r--p 0000f000 00:0f 278852     /emux/TENDA/squashfs-root/lib/libm.so.0
401ba000-401bb000 rw-p 00010000 00:0f 278852     /emux/TENDA/squashfs-root/lib/libm.so.0
401bb000-401e4000 r-xp 00000000 00:0f 278864     /emux/TENDA/squashfs-root/lib/libucapi.so
401e4000-401ec000 ---p 00000000 00:00 0
401ec000-401ed000 rw-p 00029000 00:0f 278864     /emux/TENDA/squashfs-root/lib/libucapi.so
401ed000-401ef000 rw-p 00000000 00:00 0
401ef000-401f9000 r-xp 00000000 00:0f 278849     /emux/TENDA/squashfs-root/lib/libgcc_s.so.1
401f9000-40200000 ---p 00000000 00:00 0
40200000-40201000 rw-p 00009000 00:0f 278849     /emux/TENDA/squashfs-root/lib/libgcc_s.so.1
40201000-40266000 r-xp 00000000 00:0f 278839     /emux/TENDA/squashfs-root/lib/libc.so.0
40266000-4026e000 ---p 00000000 00:00 0
4026e000-4026f000 r--p 00065000 00:0f 278839     /emux/TENDA/squashfs-root/lib/libc.so.0
4026f000-40270000 rw-p 00066000 00:0f 278839     /emux/TENDA/squashfs-root/lib/libc.so.0
40270000-40275000 rw-p 00000000 00:00 0
40275000-40277000 r-xp 00000000 00:0f 278845     /emux/TENDA/squashfs-root/lib/libdl.so.0
40277000-4027e000 ---p 00000000 00:00 0
4027e000-4027f000 r--p 00001000 00:0f 278845     /emux/TENDA/squashfs-root/lib/libdl.so.0
4027f000-40280000 rw-p 00000000 00:00 0
40280000-40281000 r-xp 00000000 00:0f 278860     /emux/TENDA/squashfs-root/lib/librt.so.0
40281000-40288000 ---p 00000000 00:00 0
40288000-40289000 r--p 00000000 00:0f 278860     /emux/TENDA/squashfs-root/lib/librt.so.0
befdf000-bf000000 rw-p 00000000 00:00 0          [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0          [vectors]

Comparing it with the process memory layout on the actual hardware (deliberately non-randomized)

~ # cat /proc/4388/maps
00008000-000f7000 r-xp 00000000 1f:03 289        /bin/httpd
000ff000-00102000 rw-p 000ef000 1f:03 289        /bin/httpd
00102000-00124000 rw-p 00000000 00:00 0          [heap]
40000000-40005000 r-xp 00000000 1f:03 124        /lib/ld-uClibc.so.0
40005000-40006000 rw-p 00000000 00:00 0
40007000-40008000 rw-p 00000000 00:00 0
4000c000-4000d000 r--p 00004000 1f:03 124        /lib/ld-uClibc.so.0
4000d000-4000e000 rw-p 00005000 1f:03 124        /lib/ld-uClibc.so.0
4000e000-4001d000 r-xp 00000000 1f:03 192        /lib/libCfm.so
4001d000-40025000 ---p 00000000 00:00 0
40025000-40026000 rw-p 0000f000 1f:03 192        /lib/libCfm.so
40026000-40041000 rw-p 00000000 00:00 0
40041000-40061000 r-xp 00000000 1f:03 205        /lib/libcommon.so
40061000-40068000 ---p 00000000 00:00 0
40068000-40069000 rw-p 0001f000 1f:03 205        /lib/libcommon.so
40069000-40071000 rw-p 00000000 00:00 0
40071000-40079000 r-xp 00000000 1f:03 201        /lib/libChipApi.so
40079000-40080000 ---p 00000000 00:00 0
40080000-40081000 rw-p 00007000 1f:03 201        /lib/libChipApi.so
40081000-40083000 r-xp 00000000 1f:03 209        /lib/libvos_util.so
40083000-4008b000 ---p 00000000 00:00 0
4008b000-4008c000 rw-p 00002000 1f:03 209        /lib/libvos_util.so
4008c000-400a0000 r-xp 00000000 1f:03 211        /lib/libz.so
400a0000-400a7000 ---p 00000000 00:00 0
400a7000-400a9000 rw-p 00013000 1f:03 211        /lib/libz.so
400a9000-400b4000 r-xp 00000000 1f:03 191        /lib/libpthread.so.0
400b4000-400bb000 ---p 00000000 00:00 0
400bb000-400bc000 r--p 0000a000 1f:03 191        /lib/libpthread.so.0
400bc000-400c1000 rw-p 0000b000 1f:03 191        /lib/libpthread.so.0
400c1000-400c3000 rw-p 00000000 00:00 0
400c3000-400c4000 r-xp 00000000 1f:03 121        /lib/libnvram.so
400c4000-400cb000 ---p 00000000 00:00 0
400cb000-400cc000 rw-p 00000000 1f:03 121        /lib/libnvram.so
400cc000-400d9000 r-xp 00000000 1f:03 190        /lib/libshared.so
400d9000-400e1000 ---p 00000000 00:00 0
400e1000-400e2000 rw-p 0000d000 1f:03 190        /lib/libshared.so
400e2000-40148000 r-xp 00000000 1f:03 188        /lib/libtpi.so
40148000-4014f000 ---p 00000000 00:00 0
4014f000-40150000 rw-p 00065000 1f:03 188        /lib/libtpi.so
40150000-40182000 rw-p 00000000 00:00 0
40182000-40191000 r-xp 00000000 1f:03 118        /lib/libm.so.0
40191000-40199000 ---p 00000000 00:00 0
40199000-4019a000 r--p 0000f000 1f:03 118        /lib/libm.so.0
4019a000-4019b000 rw-p 00010000 1f:03 118        /lib/libm.so.0
4019b000-401c4000 r-xp 00000000 1f:03 119        /lib/libucapi.so
401c4000-401cc000 ---p 00000000 00:00 0
401cc000-401cd000 rw-p 00029000 1f:03 119        /lib/libucapi.so
401cd000-401cf000 rw-p 00000000 00:00 0
401cf000-401d9000 r-xp 00000000 1f:03 187        /lib/libgcc_s.so.1
401d9000-401e0000 ---p 00000000 00:00 0
401e0000-401e1000 rw-p 00009000 1f:03 187        /lib/libgcc_s.so.1
401e1000-40246000 r-xp 00000000 1f:03 203        /lib/libc.so.0
40246000-4024e000 ---p 00000000 00:00 0
4024e000-4024f000 r--p 00065000 1f:03 203        /lib/libc.so.0
4024f000-40250000 rw-p 00066000 1f:03 203        /lib/libc.so.0
40250000-40255000 rw-p 00000000 00:00 0
40255000-40256000 r-xp 00000000 1f:03 213        /lib/librt.so.0
40256000-4025d000 ---p 00000000 00:00 0
4025d000-4025e000 r--p 00000000 1f:03 213        /lib/librt.so.0
befdf000-bf000000 rw-p 00000000 00:00 0          [stack]

The addresses of libraries such as libc.so.0 and others in EMUX are slightly different when compared to those on the actual hardware. The reason is because of /lib/libnvram-armx.so and tenda_hooks.so being injected via LD_PRELOAD.

Apart from the preloaded libraries, everything else matches.

5.5 Interacting with the Tenda's web interface

  1. Open the browser and navigate to http://192.168.100.2. We are greeted by the Tenda Quick Setup Wizard

Tenda Setup Wizard

  1. Skip connecting the WAN cable

Tenda Connect WAN cable

  1. Set the login password. The password for the Tenda emulated instance on the EMUX Docker image is ringzer0

Tenda Set Password

  1. Login to the Web Administration Interface with password ringzer0

Tenda Login

  1. Enjoy!

Tenda Running

Concluding thoughts

This article was intended to familiarise you with what it takes to emulate a new device from scratch. I urge you to download the EMUX Docker image and play with the emulated Tenda AC15.

For those of you who want to learn about IoT exploitation, take this as a challenge and discover vulnerabilities on the AC15. Try to reproduce some existing CVE's against the emulated AC15, or find 0-days on your own!

Upcoming articles

Follow me on Twitter @therealsaumil for updates on EMUX, new articles, talks and trainings!

BX LR