心血来潮有机会把Tenda无线放大器的flash提取出来了,然后在此记录下解包以及安全分析过程

固件提取

flash提取就不多说了,用热风枪+编程器一把梭即可
拿到flash提取的内存bin文件后,使用binwalk查看,发现了ubi文件,但是直接使用github已有的工具ubi_reader以及ubidump.py都无法成功提取。通过跟着ubidump.py一步步调试发现是两个问题:

  • 该固件的ubi block_size过大,为0x20000。而ubidump.py中定义的太小,无法成功识别到下一个块的magic头
  • 该固件每隔0x800字节都会有0x40字节的padding byte,需要删除掉。
    解决以上两个问题后,即可看到squashfs文件,将其dump下来。
    此处贴一下与ubidump.py的diff内容
    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
    base ❯ diff ubidump.py ubidump.py.bak 
    296c296
    < for lnum in range(self.blks.vmap[self.volid].__len__()):
    ---
    > for lnum in range(self.blks.maxlebs):
    358,360c358,359
    <
    < for log_blocksize in range(0x40,0x30000, 4):
    < self.fh.seek(log_blocksize)
    ---
    > for log_blocksize in range(10,20):
    > self.fh.seek(1<<log_blocksize)
    363,364c362
    < return log_blocksize
    <
    ---
    > return 1<<log_blocksize
    383,390d380
    < # patch to handle duplicate lnums in VTBL
    < if vid.vol_id in self.vmap and vid.lnum in self.vmap[vid.vol_id]:
    < if vid.vol_id == VTBL_VOLID:
    < print("WARNING: duplicate VTBL lnum %d, skipping" % vid.lnum)
    < continue
    < else:
    < print("WARNING: duplicate volid/vollnum/lnum vs ori lnum %x/%d/%d vs %d, overwriting" % (vid.vol_id, vid.lnum, lnum, self.vmap[vid.vol_id][vid.lnum]))
    <
    441,444c431,433
    < # check if we at least have a valid VID header
    < if self.nr_named == 0:
    < self.vid = UbiVidHead()
    < self.vtbl = [ UbiVtblRecord() ]
    ---
    >
    > self.vid = UbiVidHead()
    > self.vtbl = [ UbiVtblRecord() ]

然后是每隔0x800字节进行patch的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import os
# replace 0x40 padding bytes to null every 0x840 bytes

firmware_data = b""

os.remove("./target_extracted/patched.ubi")

with open ("./target_extracted/630000.ubi", "rb") as f:
size = f.seek(0, os.SEEK_END)
print(f"target firmware size: {hex(size)}")
block_num = size // 0x840
for i in range(block_num):
f.seek(0x840*i)
if i % 0x100 == 0:
print(f"read block ({i+1}/{block_num})")
firmware_data = f.read(0x800)

with open("./target_extracted/patched.ubi", "ab") as f2:
f2.write(firmware_data)

然后使用binwalk提取squashfs即可拿到文件系统。

漏洞分析

系统初始化服务

首先查看/etc_ro/init.d/rcS文件,发现主要工作为

  1. 创建相关目录文件以及配置环境变量
  2. 加载各种无线有线内核模块
  3. 其他网络配置

passwd文件密码:
$1$nalENqL8$jnRFwb1x5S.ygN.3nwTbG1

httpd 服务分析

直接搜索httpd文件了,文件位于/bin/httpd。使用ida打开应该只是一个bind socket的功能,实际处理逻辑在其他二进制程序中。
查看字符串内容,发现一些uri。

这些字符串位于函数init_route_auth_file中(突然发现甚至没有去符号:-))。函数功能为写入route.txt(默认路径为/var/route.txt)以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
# See route.txt file
route uri=/ handler=url-check
route uri=/action handler=action
route uri=/goform handler=action
route uri=/goform/getModules handler=action
route uri=/goform/setModules handler=action
route uri=/login/ handler=action
route uri=/cgi-bin handler=cgi-bin
route uri=/ methods=OPTIONS|TRACE handler=options
route uri=/ extensions=jst,asp handler=jst
route uri=/secure/ protocol=https
route uri=/

写入auth.txt(默认路径为/var/auth.txt)以下内容:

1
2
3
4
#   auth.txt - Authorization data
role name=user abilities=view,person,
role name=administrator abilities=user,manage,
user name=admin password=admin roles=administrator,purchase

输入点提取

web的处理逻辑主要位于/lib/libgo.so中,其通过j_websDefineAction函数以及j_moduleRegister函数注册了相关的URL.
通过j_websDefineAction注册的URL如下,其实这里都是送入了formModules函数处理,该函数进一步分配给moduleRegister注册的函数

通过j_moduleRegister注册的URL如下:

还有一个位置对于webCgiDoUpload的处理:

可疑漏洞

1. (已验证)fast_setting_timezone函数时区参数没有校验参数

三个地方可能会溢出:sscanf, strcpy, SetValue
formSystemTimeSet函数也有类似的逻辑,当time_type设置为manual时,可以通过timeZone来实现溢出

2. fromSystemPwdSet函数”isQuick”是否可以绕过

感觉不会那么直接,可能workmode会有点含义功能在里面,无法模拟 flashinfo,有点困难

3. formLedCfgGet函数可能存在溢出

内部是否会对SetValue函数存在检查?

SetValue 函数定义位于 libcommmon.so 文件中,发现其会与 cfmd 程序进行通信。
cfmd 程序负责 flash 中的信息读写,由于模拟中无法模拟 flash 相关功能,导致无法对其进行验证

4. (已验证)formSystemTimeSet可能存在溢出
5. (已验证)upgrade函数固件上传漏洞命令注入漏洞

上传固件名没有作校验,可以直接进行命令执行。

构造文件名为’`touch eutopia.txt`.bin’ 即可
然后在下面升级固件中选择上传该固件即可

decry_firm逻辑分析

在upgrade逻辑中使用了decry_firm二进制程序进行解密:

该文件逻辑比较简单,对目标固件进行了签名验证以及解密。其中解密对应了516字节以后的内容。
首先使用openssl_aes_password获取到aes密钥,使用硬编码的signed_key来进行rsa解密:
查看解密函数openssl_aes_decrypt,其中包括了salt,aeskey等解密参数。使用cbc-128方式解密

固件模拟

尝试使用用户模拟加载 http 服务
进入 squash-root 目录,执行以下命令:

1
sudo chroot . ./qemu-arm-static ./bin/httpd

网站根目录为 ./squashfs_rootfs/webroot 不过软链接失效了,可以直接将 ./squashfs_rootfs/webroot_ro/ 移动过来

发现报错:

1
2
3
4
5
6
7
8
9
10
Yes:

****** WeLoveLinux******

****** Welcome to ******

goahead 4.0.1
goahead: 0: setLocalHost error
goahead: 0: Cannot initialize server. Exiting.

怀疑是缺少网卡配置 br 网口,使用以下命令添加:

1
2
3
4
5
6
7
8
sudo apt install uml-utilities bridge-utils
sudo brctl addbr br0
sudo ifconfig br0 192.168.1.129
sudo brctl addif br0 ens160 # need to be your online network interface
sudo ifconfig br0 up
sudo dhclient br0
ping www.baidu.com
ping 192.168.1.129

ip命令(超绝简化版)

1
2
3
sudo brctl addbr br0
sudo ip addr add 192.168.2.1/24 dev br0
sudo ip link set br0 up

添加后即可成功使目标监听在 80 端口,不过发现运行会有日志报错:

1
2
connect: No such file or directory
func:cfms_mib_proc_handle, line:199 connect cfmd is error.

不影响模拟进行
原因是 /bin/cfmd 没有启动,这个应该是控制了 default.cfg 的读取和写入的,然后会开启 socket 与 httpd 进行通信
但是无法成功启动,应该是要有硬件支持才行(mtd)没有也可以模拟

接下来对以上提到的 可疑漏洞点 进行验证 (dokidoki的素捏 QWQ

使用浏览器访问 路由,发现如果访问 index.html 的话会无限循环重定向,问了 gpt 是没有给定合法的 Cookie,需要先访问 login.html 路由,访问该路由发现可以正常返回 登录页面:
逆向固件逻辑可以看到访问路由不能为127.0.0.1,改为br0的网桥ip地址即可。

看到kanxue上也有一位大佬做了相关型号的分析,比我的详细的多,可以参考:
[原创]TD路由器环境模拟与漏洞分析
这位大佬在 mtd 设备实现上采用了编程实现 hook 掉对应 cfm socket 的方法:
我们从 /bin/cfmd 的程序的模拟执行照搬照抄即可成功运行。这样前面的2,3两个洞就可以进行验证了。

参考链接