by Saumil Shah @therealsaumil
June 2020 (revised October 2021)
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.
The emulation process was an great learning experience for me, which I present here. Read on!
The emulated Tenda AC15 is included in the EMUX Docker Image. You're welcome.
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
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
.
/dev/console
, hopefully
find a shell and dump the /dev/mtdblock
s. This is my
preferred technique.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.
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
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.
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 &
:
ramfs
and mount points/etc_ro
to /etc
/webroot_ro
to /webroot
The startup process is summarised in the diagram below:
To create a new EMUX device, follow these steps:
nvram.ini
untouched for now, since we
don't have the nvram contents.config
file./emux/devices
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
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
.
Move the squashfs-root
directory extracted by
binwalk
(Section
1.2) into /emux/TENDA/squashfs-root
.
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.
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"
TENDA,qemu-system-arm-6.0.0,vexpress-a9,,,256M,zImage-2.6.39.4-vexpress,VEXPRESS2,Tenda AC15 Wi-Fi Router
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
From the EMUX Docker shell, 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:
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:
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
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)
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:
bcm_nvram_get
seems to be the way to access
nvram
data. This function needs to be mapped to the
equivalent nvram access functions in libnvram-armx.so
.RestoreNvram
looks interestingdoSystemCmd("reboot")
this is where the reboot is
triggeredchar * 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
.
To proceed with the emulation, it is necessary to:
bcm_nvram_set
to nvram_get
bcm_nvram_get
to nvram_set
bcm_nvram_unset
to nvram_unset
get_flash_type
to always return
4
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/
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"
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 &
:
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.
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
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.
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:
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
).
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
.
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:
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
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.
http://192.168.100.2
.
We are greeted by the Tenda Quick Setup Wizardringzer0
ringzer0
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!
Follow me on Twitter @therealsaumil for updates on EMUX, new articles, talks and trainings!