TL-WR886N路由器uart调试及VxWorks固件提取和分析

TL-WR886N路由器uart调试及VxWorks固件提取和分析

咸鱼买的二手路由器,用来练习。这篇文章对该路由器的拆解以及元器件有简单的介绍,外壳只有两颗螺丝能卸下,剩下的只能暴力拆解。另外,这类网络设备的螺丝很多会藏在贴纸下面,比如下图右侧那颗。

拆解后找到闪存和uart口,笔者最近接触到的两个老路由器和一个无线ap的flash都是8脚的,uart一般是3-5个孔。

背面

用到的工具和软件

1
2
3
4
5
6
7
ch341A编程器
SOP8测试夹+转接板
好易通测试夹
ch340 usb转ttl模块
Asprogrammer
SecureCRT
螺丝刀

编程器提取固件

参考z1r0大佬写的文章

路由器不接电源,flash上最靠近圆圈的一角引脚编号为1,逆时针分别为2,3,4,5,6,7,8。测试夹的红线对应编号1的引脚。

连接正确时,路由器的电源灯和ch340模块的电源灯都会亮。

然后就是安装ch340驱动,照着参考文章做就行,需要注意的是在点击设置,windows更新,点击查看所有可选更新,找到驱动程序更新里的wch.cn这一步如果找不到该更新,可以重启电脑。

安装完驱动和软件后插入ch340,read id,选择型号,read ic,save file即可

uart调试+固件转储

参考文章

分辨引脚

辨别方法参考,顺序:GND->Vcc->TXD->RXD

从背面看,自左往右分别编号为1,2,3,4,由于这里是垂直翻转的,从正面看和从背面看孔的顺序相同

  • GND

孔2是GND。万用表调至蜂鸣档,黑表笔接电源引脚,红表笔依次尝试接4个uart孔,有蜂鸣红灯亮的就是GND。

  • Vcc

路由器接电源,万用表调整至测电压20v档。黑表笔接GND,红表笔分别接其它孔,电压为3.3V或5V的为Vcc,如果有多个孔符合,则分别和GND短接,观察是否电源灯灭,短接后电源灯灭的是Vcc。

笔者测出来1号孔3.3V,3号孔0V,4号孔2.59V,那么显然1号孔是Vcc,短接1和2GND后确实发现电源灯灭。

  • TXD

电源和万用表同Vcc,黑笔接GND,红笔分别尝试剩下的两个孔,连接后按重启按钮(有的需要长按),重启时电压发生跳动的是TXD。这里显然4号孔是TXD。

  • RXD

剩下的3号孔就是RXD。如果是5孔的板子,剩下的两个孔需要分别尝试接入usb转ttl设备。

连接USB转TTL设备

分辨完引脚,需要链接usb转ttl设备和uart接口,可以焊一排引脚或使用测试夹,测试夹更方便,这里使用测试夹。

连接时按照文章中的对应关系,GND接TTL设备GND,TXD接TTL设备RXD,RXD接TTL设备TXD,Vcc不用接。

本文设备的对应关系:灰-1-Vcc、紫-2-GND、蓝-3-RXD、绿-4-TXD

正确连接后应如下图所示,POW灯亮,TXD和RXD灯灭。

完成接线后就是使用终端软件来接收终端,终端软件有很多,这里使用SecureCRT,使用PuTTY或者在linux上使用Picocom也可以。

SecureCRT下载和破解

可以在这里下载软件和keygen,官网下很麻烦。

软件的具体安装和破解可以参考该博客

如果不想安装到c盘,可以在以下步骤选择custom,点next后只修改存放路径继续next

SecureCRT使用

USB转TTL设备插入后Port中会出现ch340的选项。波特率因设备而异,9600-115200居多,设置不正确会导致和设备交互时出现乱码,本设备通过该文章得知波特率为117500,一般来说如果常见波特率尝试后无效,可能需要在官方手册中查找串口参数并计算。

参考文章中有一些该步骤常见问题排查及波特率标准。

需要注意的是,有时断电重新接通路由器电源后,再使用117500波特率连接会乱码,需要短接一次Vcc和GND后才恢复正常

如下图设置连接后可获取路由器的shell

GetShell之后 - flash转储

uart转储固件参考文章1

uart转储固件参考文章2

可使用的命令如下,能dump内存、读写闪存、通过tftp协议上传和下载文件、查看文件系统信息、连接云服务端等。

QQ截图20240220211242

flash命令

QQ截图20240221130511

flash -layout查看flash布局,FIRMWARE就是固件,偏移是0x32000,大小为1848k

QQ截图20240220224905

使用flash -read时,系统奔溃了,询问gpt可以了解每条报错的含义,这里大概是因为虚拟内存中0x00000000地址不可写。用flash命令提取固件不太方便。

fs -stat发现内存中rom的基址是BF000000,mem命令可以dump内存,那么或许可以从内存中dump闪存

使用mem -dump查看BF000000-BF200000的内存的16进制,和编程器提取的flash比较,发现是一致的,那么通过该命令即可转储闪存。

但是一次最多dump 16k的数据也就是0x1000,那么需要dump两百次,写脚本进行dump。

两篇参考文章中都使用了python的serial库,这是一个用于串口(serial)通信的库,可以用来和uart串口交互提取固件。参考1中使用JTagulator连接uart转储闪存的脚本放在了github可以参考。搜索引擎能找到一些教程,也有pySerial官方文档。下面是基于该库写的闪存dump脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import serial
import serial.tools.list_ports
import os

def ser_readall(): #读取并打印所有数据,去除回车并转义输出
c = ser.read().decode('utf-8')
while (c):
if (c == '\r'):
c = ''
print(c, end='')
c = ser.read(1).decode('utf-8')

port_list = list(serial.tools.list_ports.comports()) #搜索可用串口
print(port_list)
if len(port_list) == 0:
print('无可用串口')
else:
for i in range(0,len(port_list)):
print(port_list[i])

ser = serial.Serial()
ser.port = 'COM9'
ser.baudrate = 117500
ser.timeout = 0.5
ser.open()
print('Port ' + ser.port + ' open state: ' + str(ser.isOpen()))
start = 0xbf000000
len = 0x1000
f = open('TL-WR886N.bin','wb')
for i in range(0x200):
ser.write(b'mem -dump ' + hex(start).encode('utf-8') + b' ' + hex(len).encode('utf-8') + b'\r')
start += len
line = ser.readline().decode('utf-8').replace('\r','').replace('\n','')

while(line):
if (line[0:2] == 'BF'):
#print(line)
data = line.split(' ')[1].replace(' -','')
#print(data)
byte = data.split(' ')
#print(byte)
for c in byte:
word = bytes.fromhex(c)
f.write(word)
line = ser.readline().decode('utf-8').replace('\r','').replace('\n','')
print(str(i) + '/512')

固件分析

直接binwalk -Me解包出一堆压缩包、图片、网站源码和资源文件,但是没有文件系统,通过查资料发现该固件并非linux系统,而是VxWorks系统,属于RTOS系统的一种。VxWorks系统的固件和Linux系统的固件有较大差异。分析VxWorks固件的关键是正确反编译固件中的VxWorks系统映像,查看其中的函数,一般步骤为: 获取设备CPU架构和端序 -> 获取flash中的uboot、固件和系统映像 -> 获取固件在内存中的加载地址 -> 恢复系统映像符号表 -> 反编译系统映像进行代码审计。

VxWorks固件初步判断依据

参考文章 - VxWorks固件加载地址分析方法研究

1.MyFirmware字符串

1
2
$ hexdump -C TL-WR886N.bin | grep MyFirmware
00031ec0 4d 79 46 69 72 6d 77 61 72 65 00 00 00 00 00 01 |MyFirmware......|

2.MINIFS文件系统

1
2
3
4
5
$ hexdump -C TL-WR886N.bin | grep MINIFS
00021000 4d 49 4e 49 46 53 00 00 00 00 00 00 00 00 00 00 |MINIFS..........|
000e7000 4d 49 4e 49 46 53 00 00 00 00 00 00 00 00 00 00 |MINIFS..........|
00178000 4d 49 4e 49 46 53 00 00 00 00 00 00 00 00 00 00 |MINIFS..........|
001c3000 4d 49 4e 49 46 53 00 00 00 00 00 00 00 00 00 00 |MINIFS..........|

确定架构及端序

binwalk -Y得知为CPU架构为32位,MIPS,端序为大端序

1
2
3
4
5
$ binwalk -Y TL-WR886N.bin 

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
60 0x3C MIPS executable code, 32/64-bit, big endian, at least 1250 valid instructions

VxWorks系统映像概述及引导启动过程

补充一些基础知识

参考文章1

参考文章2

VxWorks大致启动流程:

上电

-> 执行rom首地址的bootstrap进行系统的基本初始化、寄存器设置、存储器映射等

-> bootstrap将u-bootimage加载(并解压)到RAM(内存)的RAM_HIGH_ADRS地址并跳转到该地址执行

-> ubootimage构建最小可运行系统

-> ubootimage将rom中的VxWork映像加载(并解压)到RAM的RAM_LOW_ADRS地址,跳转至该地址执行

-> 硬件环境重新初始化,VxWorks启动

  • bootloader由bootstrapbootimg组成。bootstrap是最初级的引导,旨在初始化CPU、内存控制器、时钟、堆栈,目标是让CPU正常运作起来。boot image往往初始化最小OS内核,搭建网络下载通道,提供一个可以交互的命令行,以便自我更新(update boot)或下载更新系统映像(update vxWorks)。

  • CPU上电后,指令指针指向一个设定好的地址,ROM/Flash被映射到该地址,CPU从该地址开始执行bootloader。因此bootloader的基址并不是0,而是CPU中预先设定的地址,本文中该地址可通过逆向bootstrap获得。

  • 整个ROM(Flash)在bootstrap进行初始化时被映射到内存中。本设备该映射地址即flash转储步骤中用到的0xBF000000(在ram中物理地址为0xBF000000 & 0x1FFFFFFF = 0x1F000000)。

  • Boot Rom即存放bootloader和系统映像的ROM/Flash芯片,其中还会存放应用程序资源、用户配置数据等信息。

获取uboot、固件和Vxworks系统映像

dump出的是整个flash,根据flash布局,使用dd命令进行提取出FACUBOOT、BOOTIMG和FIRMWARE,两份uboot和一份固件。

1
2
3
dd if=TL-WR886N.bin of=facuboot bs=1 skip=0 count=131072  #0x20000=131072
dd if=TL-WR886N.bin of=bootimg bs=1 skip=163840 count= 40448 #0x28000=163840 0x31e00-0x28000=40448
dd if=TL-WR886N.bin of=firmware bs=1 skip=204800 count=1892352 #0x32000=204800 0x200000-0x32000=1892352

提取出来后分别binwalk查看,发现如下对应关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ binwalk TL-WR886N.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
#FACUBOOT
13840 0x3610 U-Boot version string, "U-Boot 1.1.4 (Apr 1 2016 - 17:30:08)"
13888 0x3640 CRC32 polynomial table, big endian
15136 0x3B20 uImage header, header size: 64 bytes, header CRC: 0xDBBCB4A2, created: 2016-04-01 09:30:09, image size: 57104 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0xF4C2D0C3, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: "u-boot image"
15200 0x3B60 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 140620 bytes
#BOOTIMG
176432 0x2B130 U-Boot version string, "U-Boot 1.1.4 (Apr 1 2016 - 17:29:18)"
176480 0x2B160 CRC32 polynomial table, big endian
177708 0x2B62C uImage header, header size: 64 bytes, header CRC: 0x5FA1A7B0, created: 2016-04-01 09:29:19, image size: 20973 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0xF51A98FC, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: "u-boot image"
177772 0x2B66C LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 52596 bytes
#FIRMWARE
204800 0x32000 LZMA compressed data, properties: 0x6E, dictionary size: 8388608 bytes, uncompressed size: 2354144 bytes
946240 0xE7040 LZMA compressed data, properties: 0x5A, dictiona ry size: 8388608 bytes, uncompressed size: 6020 bytes
......................
一堆LZMA compressed data

uboot-bootstrap

对uboot的处理参考该文章,uboot分为三部分:bootstrap代码、0x40字节的uboot image的头部信息、lzma加密的ubootimage主体,通过binwalk信息就能大致分辨各部分的偏移和大小。

binwalk得到的uboot信息中,Data Address: 0x80010000, Entry Point: 0x80010000表示设备启动后,会把后续ubootimg通过lzma解压出来的数据存入内存地址0x80010000,然后把$pc设置为: 0x80010000。

这里只处理FACUBOOT,BOOTIMG同理

使用dd命令提取出bootstrap,用ida打开,由于基址未知,这里暂时和ubootimg一样设定为0x80010000

1
dd if=facuboot of=facbootstrap bs=1 skip=0 count=15136 #15136=0x3B20

在第一个跳转地址0x80010400的下方找到move $gp,$ra,mips通过gp寄存器相对寻址GOT表,即gp会指向got表,所以可以通过gp指针的数值结合bootstrap的大小推测bootstrap的基址

分析汇编,

  • bal,跳转指令,跳转到目标地址,同时将当前指令的返回地址保存到 RA 寄存器中。同时,mips CPU中还有分支延时槽机制,参考该文章,bal指令的下一条指令(此处是nop)为分支延时槽,跳转到目标地址前会先执行分支延时槽指令,所以这里保存到RA的返回地址为0x80010504。

  • lw $t1, 0($ra),相当于将$ra+0地址处的一个word(4字节)存入t1寄存器

这些代码最终把0x9F003A70存入了gp寄存器,然后通过offset($gp)寻址got表中的函数。结合bootstrap文件大小0x3B20可推测基址为0x9F000000,这也是一个比较常见的bootstrap基址。

1
2
3
4
5
6
7
8
9
10
11
12
13
ROM:800104FC 04 11 00 02                   bal     sub_80010508
ROM:80010500 00 00 00 00 nop
ROM:80010500
ROM:80010500 # -----------------------------------
ROM:80010504 9F 00 3A 70 .word 0x9F003A70
ROM:80010508
ROM:80010508 # =============== S U B R O U T I N E
ROM:80010508
ROM:80010508
ROM:80010508 sub_80010508: # CODE XREF: ROM:800104FC↑p
ROM:80010508 03 E0 E0 21 move $gp, $ra
ROM:8001050C 8F E9 00 00 lw $t1, 0($ra)
ROM:80010510 01 20 E0 21 move $gp, $t1

Edit -> Segments -> Rebase Program重新设置基址为0x9f000000。

Options -> general -> Processer specific analysis options ->$gp value设置为0x9F003A70

Edit -> Select All重新反汇编,多分析出了很多函数,got表也恢复正常,处理完成。

通过逆向ubootimage也能获取bootstrap基址,在下文确定固件加载地址->(通用)逆向ubootimage找加载地址中介绍.

uboot主体部分

提取facuboot的主体部分,偏移为0x3B60,尾部位于下图光标前(不包含0xFF),因为需要对齐填充了很多0xFF,这些对齐字节并不属于压缩文件的一部分。

使用dd命令提取出主体部分的lzma压缩文件

1
$ dd if=facuboot of=facbody bs=1 skip=15200 count=57104 #0x3B60=15200   0x11A70-0x3B60=0xDF10=57104

更改后缀名为.xz并解压缩

1
2
$ mv facbody facbody.xz
$ xz -k -d facbody.xz

前文由binwalk信息已知ubootimg基址为0x80010000。使用ida正确设置cpu、端序及加载地址后打开,简单处理后如下说明解压成功

另一份名为为BOOTIMG的uboot同理,命令如下

1
2
3
dd if=bootimg of=bootbody bs=1 skip=13932 count=20973
mv bootbody bootbody.xz
xz -k -d bootbody.xz

FIRMWARE-提取系统映像

FIRMWARE中几乎都是LZMA加密的数据,其中0地址的数据块大小为2354144,明显比其它数据块大很多。binwalk -Me得到解密后的数据,发现除了0,其它都是图片或文本文件。

QQ截图20240224173704

binwalk查看0确认为VxWorks系统映像,其余都是文件系统中的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ binwalk 0

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
1834812 0x1BFF3C Certificate in DER format (x509 v3), header length: 4, sequence length: 4
1842100 0x1C1BB4 Certificate in DER format (x509 v3), header length: 4, sequence length: 4
1887200 0x1CCBE0 VxWorks operating system version "5.5.1" , compiled: "Aug 23 2019, 11:35:29"
1955356 0x1DD61C Copyright string: "Copyright(C) 2001-2011 by TP-LINK TECHNOLOGIES CO., LTD."
1985044 0x1E4A14 VxWorks WIND kernel version "2.6"
2028656 0x1EF470 HTML document header
2028721 0x1EF4B1 HTML document footer
2048248 0x1F40F8 PEM certificate
2048304 0x1F4130 PEM RSA private key
2062120 0x1F7728 Base64 standard index table
2097416 0x200108 CRC32 polynomial table, big endian
2098440 0x200508 CRC32 polynomial table, big endian
2099464 0x200908 CRC32 polynomial table, big endian
2100488 0x200D08 CRC32 polynomial table, big endian
2120928 0x205CE0 XML document, version: "1.0"
2140268 0x20A86C SHA256 hash constants, big endian
2237001 0x222249 StuffIt Deluxe Segment (data): f
2237032 0x222268 StuffIt Deluxe Segment (data): fError
2237113 0x2222B9 StuffIt Deluxe Segment (data): f

确定固件加载地址

通过MyFirmware指纹找加载地址

部分flash中存在“MyFirmware”字符串,定位到该字符串。

往前找到上一个段的末尾,当前段开始的位置,一般上个段末尾会使用0xFF或0x00补齐。当前段开头偏移0x18处的两个4字节就是VxWorks系统映像的加载地址0x80001000。

逆向ubootimage找加载地址

找到可疑字符串vxWorks.bin from =0x%x, len=0x%x\n,查看引用它的函数.

猜测sub_8001DA90函数的作用是将上电后映射到内存的rom中的firmware复制至RAM_LOW_ADRS

可得ubootimg基址为0x80001000,上电后bootstrap的加载基址0x9F000000.

恢复VxWorks系统映像符号表

参考文章

VxWorks 6.0 BSP手册

VxWorks Architecture Supplement, 6.2

ida中查看importsExports都是空的,说明文件没有符号表.

VxWorks使用外部符号文件,只要能找到符号文件,导入符号表就能恢复原本的函数名,更方便逆向.

寻找外部符号文件

符号文件中包含各种函数名字符串,VxWorks BSP手册中可以看到一些必定出现的重要函数名如usrInitbzerobfill等,因此可以根据是否包含这些字符串查找符号文件。

binwalk -Me提取固件,在提取后的目录中搜索包含usrInit的文件,可以确定B65E2就是系统映像的外部符号文件。

1
2
3
$ grep -r usrInit .
Binary file ./B65E2 matches
Binary file ./_B65E2.extracted/1056C.sit matches

如果搜索包含bzero的文件,发现1481D0也符合要求,其为有符号表的ELF可执行文件,用ida进行简单逆向发现其包含Blowfish加密算法及网络相关函数。

1
2
3
$ grep -r bzero .
Binary file ./B65E2 matches
Binary file ./1481D0 matches

符号文件格式

每个外部符号文件的开头4字节表示当前符号文件大小,例如该符号文件大小为126801=0x1EF51。

紧跟的四字节表示符号条目数,每8字节一个条目,该条目数乘8再加8的偏移处开始为函数名字符串区块,例如该符号文件中0x1372*8+8=0x9B98,如下图。

开头8字节后每8字节表示一个条目信息,分为三部分:符号类型(1字节)、该函数名在函数名字符串区块中的偏移(3字节)、该函数在VxWorks系统映像中加载后的基址(4字节),用结构体表示如下:

1
2
3
4
5
struct sym_info{
char type;
char offset[3];
unsigned int address;
};

恢复符号表

使用ida python编写脚本,类似的脚本网上有很多,改一改即可

ida python手册

python2参考

python3参考

这里使用python3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# encoding:utf-8
#symfile_path: 刚刚搜索到的符号文件的路径
#symbols_table_start : 符号表起始偏移,从16进制编辑器看出来是8(前8字节作用不清楚,也不重要)
#strings_table_start : 字符串起始偏移,也是从16进制编辑器看出来
import binascii
import idautils
import idc
import idaapi
import ida_funcs

symfile_path = r"D:\系统默认\桌面\网安\笔记\iot\Firmwares\TL-WR886N/B65E2"
symbols_table_start = 8
strings_table_start = 0x9B98

with open(symfile_path, 'rb') as f:
symfile_contents = f.read()

symbols_table = symfile_contents[symbols_table_start:strings_table_start]
strings_table = symfile_contents[strings_table_start:]

def get_string_by_offset(offset):
index = 0
while True:
if strings_table[offset+index] != 0:
index += 1
else:
break
return strings_table[offset:offset+index]


def get_symbols_metadata():
symbols = []
for offset in range(0, len(symbols_table),8):
symbol_item = symbols_table[offset:offset+8]
flag = symbol_item[0]
string_offset = int(binascii.b2a_hex((symbol_item[1:4])).decode("ascii"), 16)
string_name = get_string_by_offset(string_offset)
target_address = int(binascii.b2a_hex((symbol_item[-4:])).decode("ascii"), 16)
symbols.append((flag, string_name, target_address))
return symbols


def add_symbols(symbols_meta_data):
for flag, string_name, target_address in symbols_meta_data:
idc.set_name(target_address, string_name.decode("utf8")) #命令目标地址的数据,数据类型由flag而定
if flag == 0x54: #类型T,表示是text段的符号,text段的符号一般是函数符号,所以在命名后还要将目标地址设置为函数
idc.create_insn(target_address) #确保target_address有指令
ida_funcs.add_func(target_address) #将target_address定义为函数并自动寻找边界

if __name__ == "__main__":
symbols_metadata = get_symbols_metadata()
add_symbols(symbols_metadata)

修复成功


TL-WR886N路由器uart调试及VxWorks固件提取和分析
https://lkliki.github.io/2024/02/25/TL-WR886N路由器uart调试及VxWorks固件提取和分析/
作者
0P1N
发布于
2024年2月25日
许可协议