新买的小米 BE10000 Pro,原本只是想给它配置主 WiFi 和 IoT WiFi。UCI 配置方案已经准备好了,接下来需要一个能长期使用的 root 登录入口。

用 XMiR-Patcher 开启临时 SSH 很顺利,安装永久 SSH 时却出现了两行互相打架的提示:前一行说安装完成,后一行说内核模块加载失败。继续往下查,涉及到了原厂 OpenWrt 衍生固件的启动逻辑、内核模块 ABI、MTD 分区写保护,以及恢复出厂时配置究竟如何重建。

这次把源码核查、远程操作和备份校验交给 Codex 协助执行,我提出需求,并要求实际做普通重启和恢复出厂验收。下面按设备上的观测、分区回读和测试结果记录整个过程。

最终在这台 RP04、原厂 1.0.70 上验证通过的是:恢复出厂后仍可直接通过 Telnet 登录 root;SSH 重新开启并安装软固化脚本后,可以经受普通重启。恢复出厂会清除 SSH 软固化配置,升级其他固件没有测试。

记录日期:2026 年 10 月 31 日。本文中的分区结构和命令均对应这一次实测,涉及分区写入与恢复出厂,复现前需要核对自己的型号、固件和分区表,并完成本机备份。

为什么需要原厂固件里的 root

我这次的需求是直接操作无线、网络和防火墙配置,为后续的主 WiFi、IoT WiFi 方案准备管理入口。需要用到的配置文件包括:

/etc/config/wireless
/etc/config/network
/etc/config/firewall

这些配置由 UCI 管理。root 是这套 Linux 系统的超级用户,SSH 和 Telnet 则是进入系统的两种方式。能打开路由器的 Web 管理页,与能拿到 root shell,是两件不同的事。

机器全程保持小米原厂固件,没有刷入社区 OpenWrt,也没有更换内核。在这台机器上实际读到的信息是:

项目 实测值
产品 小米 BE10000 Pro
设备型号 RP04
原厂固件 1.0.70 release,CN
CPU 架构 arm64 / aarch64
Linux 内核 6.6.47,SMP PREEMPT
OpenWrt 发行标识 18.06-SNAPSHOT
target ipq54xx/generic
无线配置中的驱动类型 qcawificfg80211

这里的发行标识、内核版本和无线驱动都来自原厂系统。看到 UCI 或 OpenWrt 的名称,并不意味着可以直接套用社区某个发行版的全部教程。具体配置是否被厂商脚本重写,还需要另行验证。

这篇先把 root 入口建立并验收,主 WiFi 和 IoT WiFi 的具体拓扑、隔离规则及配置不在本次测试范围里。

先区分三种状态:临时开启、脚本软固化和 bdata 固化

“开启 SSH”和“永久 root”很容易被混在一起说。实际操作前,我先把目标拆成了三个层次:

层次 主要做什么 需要怎样验证
临时开启 在当前启动周期里修改开关、启动服务,取得 root 新建连接并执行 id
脚本软固化 把恢复 SSH 的脚本和启动入口放到持久配置中 普通重启后,不重跑漏洞即可登录
bdata 固化 将解锁标志写进独立的 bdata 分区,供原厂启动流程读取 实际恢复出厂后,重新连接并取得 root

社区常把第三种叫“硬固化”。在本文里,它具体指修改 bdata 中的环境参数,并不意味着任意固件升级后都保证可用。

还要把 root 入口是否保留 与 SSH 是否自动启动 分开验收。本次最后保留下来的基础入口是 root Telnet,SSH 依然需要相应的服务配置和启动脚本。

第一次操作:SSH 开了,永久安装却报错

使用的工具是 openwrt-xiaomi/xmir-patcher。它检测到 datacenter7,成功启动了 22 端口上的 SSH 服务。

进入后执行 id,得到:

uid=0(root) gid=0(root)

这一步确认了临时 root 可用。接着执行菜单 6,安装永久 SSH,工具先显示 bdata 中的几个开关都没有开启:

bdata: telnet_en = 0, ssh_en = 0, uart_en = 0
CPU arch: arm64
Kernel: Linux XiaoQiang 6.6.47 ... SMP PREEMPT ... aarch64 GNU/Linux

上传脚本和模块、执行安装之后,又出现了:

Ready! The Permanent SSH patch installed.
Patch for bdata result: ERROR: cannot load xmir_patcher.ko!

单看第一行,很容易以为已经结束了。但 bdata 的三个 *_en 标志仍然都是 0,这与“永久安装完成”的直觉明显对不上。

先做对照:普通重启成功,恢复出厂失败

在尝试修复之前,我先要求做一组实际对照。

普通重启后,没有再次运行漏洞,使用原来的 root 凭据可以直接登录 SSH。这说明至少有一部分持久化生效了。

随后执行原厂恢复出厂流程。路由器重新启动后,Web 管理页回到首次设置状态,接口中的 inited 也变成 0。页面恢复可访问后,继续观察了超过 100 秒,22 端口仍没有开启。

结果很明确:

操作 当时的结果
普通重启 root SSH 可直接登录
恢复出厂 SSH 未保留,22 端口关闭

这组测试先证明了工具并非完全没起作用。它已经安装的启动脚本能经受普通重启,而恢复出厂会清掉这部分用户配置。

对照本机挂载和启动脚本,也能看出几个相关位置的区别:

位置 在这台机器上的作用
/etc/init.d/dropbear 位于运行时重建的 /etc 中,普通启动时需要重新修补相关条件
/etc/config 持久配置目录,普通重启保留,恢复出厂会重建
/etc/crontabs 持久保存 cron 和补丁脚本,恢复出厂同样会清理
/etc/shadow 指向数据分区中的账户文件;恢复出厂后 root 密码会回到出厂生成值
bdata 分区 独立保存设备环境参数,本次恢复出厂后修改值仍保留

“放在持久目录里”只能说明普通断电重启后有机会保留。恢复出厂是主动重建配置的流程,需要单独测试。

读源码和 dmesg:模块不兼容,成功提示也不完整

检查 install_ssh.py 后,可以把菜单 6 的执行过程拆成两段。

第一段安装 SSH 补丁:把脚本移到 /etc/crontabs/patches,写入 cron,添加防火墙启动钩子,设置 NVRAM,并执行一次补丁。这与普通重启成功的现象一致。

第二段尝试修改 bdata。它需要先加载 xmir_patcher.ko,再请求模块解除 bdata 写保护,最后执行 bdata set 和 bdata commit。

本机失败在第一步。重置之前读取的内核日志是:

module xmir_patcher: .gnu.linkonce.this_module section size must match the kernel's built struct module size at run time

这条错误比脚本日志中的“cannot load”具体得多。Linux 的模块加载代码会检查模块中 .gnu.linkonce.this_module 段的大小是否与运行时 struct module 一致,本次就在这里被拒绝了。

工具如何适配内核版本也值得看一眼。它根据 6.6 + arm64 + PREEMPT 选到预编译模块,然后修改其中的 vermagic:

原模块:6.6.0-XMiR-Patcher SMP preempt mod_unload aarch64
上传版:6.6.47 SMP preempt mod_unload aarch64

对比这两个本地文件,模块结构段仍都是 640 字节。替换版本字符串并没有改变结构布局,实际内核日志已经证明存在 ABI 不兼容。具体由哪个编译配置或厂商补丁造成,这次没有继续定位,也没有强制加载模块。

源码里还有两个影响诊断的细节:

  1. bdata 步骤在模块加载失败后直接退出,尚未执行后续的 bdata 写入。
  2. “Permanent SSH patch installed”无条件打印;insmod 的输出捕获又没有把标准错误一起保存。

因此,这次应当被报告为“SSH 脚本安装成功,bdata 修改失败”。工具的成功提示不能代替分区回读和重启验收。

社区里的另一条路线:临时进入工厂模式

继续查社区讨论,在 BE10000 Pro 的上游 Issue 154 中找到了两类反馈。

Xierfei提到过 Telnet 固化成功,再手动开启 SSH、安装脚本的经历;qdore 的后续评论给出了通过 crash 标志进入工厂模式、写入 bdata、再清除标志的三次重启步骤。

同一讨论中也有人报告直接 bdata commit 出现 I/O 错误,还有人质疑过硬固化步骤。它提供了一条值得验证的路线,完整的结果仍需要在自己的设备上确认。

这条路线的关键是改变 bdata 的可写状态:

正常模式
  ft_mode=0,bdata 只读
          ↓
写入 crash 工厂模式标志并重启
          ↓
工厂模式
  ft_mode=1,bdata 可写
          ↓
用原厂 bdata 命令提交解锁标志
          ↓
清除 crash 标志并重启
          ↓
回到正常模式,保留 bdata 中的修改

这样就可以通过原厂工具提交 bdata,不必先完成不兼容内核模块的适配。接下来的工作重点,是备份、检查前置条件,以及在每次重启后读取实际状态。

备份:需要保存到电脑,也需要验证能读对

开始写分区之前,先完成了原始镜像和配置备份。重要分区各自独立读取两遍,检查长度、比较内容,并计算 SHA256,确认文件已经保存到电脑。

本次保存的主要内容如下:

内容 本机大小 用途
原始 bdata 512 KiB 保留修改前的完整设备环境
原始 crash 512 KiB 退出工厂模式后核对恢复结果
引导环境 0:APPSBLENV 512 KiB 保存操作前的引导环境
ART 校准数据 1536 KiB 保存本机校准数据,本次不修改它
许可证分区 512 KiB 保存本机许可证数据,本次不修改它
引导配置 1024 KiB 记录最终恢复出厂测试前的状态
配置、账户、SSH 密钥和启动脚本 归档文件 留存操作前后配置,辅助恢复和核对

其中 bdata、crash 和引导环境在硬固化写入前备份;ART、许可证和引导配置在后续补齐。这些镜像是 MTD 分区的逻辑内容,不含 NAND OOB,也不是整机完整固件备份。

bdata 中包含本机环境参数,配置归档还可能包含密码和私钥。原始备份应当私密保存,不适合作为公开教程的附件,更不能直接拿其他设备的 bdata 覆盖本机。

bdata 的 CRC 范围也要核对

本机 bdata 分区有 512 KiB,但环境区是前 64 KiB:开头四字节存放小端 CRC32,校验范围是其后到 64 KiB 末尾的数据。将整个 512 KiB 都拿来计算 CRC,会得到错误的校验结论。

针对本机格式,离线检查可以写成:

from pathlib import Path
import zlib

image = Path("bdata-original.bin").read_bytes()
assert len(image) == 0x80000

stored_crc = int.from_bytes(image[:4], "little")
actual_crc = zlib.crc32(image[4:0x10000]) & 0xffffffff
assert stored_crc == actual_crc

这里的大小和范围来自本次镜像验证,不能据此推定其他小米路由器都采用相同格式。

写入工厂模式标志

先通过 /proc/mtd 确认本机的分区名称和大小。后面的写操作使用分区名,读取 /sys/class/mtd/mtdX/flags 时,mtdX 也必须按自己的分区表替换。

写入前,本机观测到:

ft_mode=0
bdata flags=0x0
crash 整分区均为 0xFF

工厂模式的启动脚本还可能运行硬件测试。检查本机脚本后,确认 auto_flashtest 不为 once,没有满足闪存压力测试的触发条件;取得工厂模式 root 后,也停止了 systestctl 测试服务。

以下各步是本机的执行顺序,每一步成功并回读核对后,才继续下一步。

生成明确的四字节文件

社区使用的标志是:

A5 5A 00 00

本次用八进制转义生成文件,避免依赖 shell 变量保存空字节:

printf '\245\132\000\000' > /tmp/rp04-factory-marker.bin &&
test "$(wc -c < /tmp/rp04-factory-marker.bin)" -eq 4 &&
mtd write /tmp/rp04-factory-marker.bin crash

然后回读整个 crash。本机原始分区全为 0xFF,因此可以明确核对:前四字节为 A5 5A 00 00,其余仍全部为 0xFF。确认后再重启。

如果原始 crash 含有其他数据,就不能假设这个短文件写入方式会保留它们。分区写入包含擦除行为,原始内容需要事先理解和备份。

重连后检查模式和写保护

工厂模式启动后,通过 Telnet 取得了 root,并读取到:

uid=0(root) gid=0(root)
ft_mode=1
bdata flags=0x400

在这台机器上,bdata 此时已可写。确认模式之后执行:

/etc/init.d/systestctl stop

到这里还没有修改 bdata,只确认了进入工厂模式后确实可以解除本次遇到的写保护限制。

用原厂命令修改 bdata,再比较完整镜像

接下来使用原厂 bdata 命令设置四个值:

bdata set telnet_en=1 &&
bdata set ssh_en=1 &&
bdata set uart_en=1 &&
bdata set boot_wait=on &&
bdata commit

没有手工编辑二进制环境区,也没有自行计算新 CRC 后刷回镜像。参数提交由原厂工具完成,提交后的结果则通过完整回读验证。

修改后的分区又读取了两遍,检查了四件事:

  1. 两次读取长度都为 512 KiB,内容完全相同。
  2. 前 64 KiB 环境区的 CRC32 正确。
  3. 解析修改前后的全部环境变量,仅有预期的四项变化。
  4. 64 KiB 环境区之外的数据完全未变。

变量差异如下:

参数 修改前 修改后
ssh_en 0 1
telnet_en 0 1
uart_en 0 1
boot_wait 不存在 on

镜像验证通过后,再把相同的设置写入 NVRAM:

nvram set ssh_en=1 &&
nvram set telnet_en=1 &&
nvram set uart_en=1 &&
nvram set boot_wait=on &&
nvram commit

随后按社区步骤再重启一次,在工厂模式下确认 bdata 中的值仍然保留。

清除临时标志,回到正常模式

完成 bdata 修改之后,需要退出工厂模式:

mtd erase crash

因为本机原始 crash 是全 0xFF,擦除后可以直接将完整回读与原始镜像逐字节比较。两份内容一致,才继续重启。

正常启动后的结果是:

ft_mode=0
bdata flags=0x0

四个 bdata 标志仍然保留,Telnet 可以正常登录 root。这个状态很重要:可写的工厂模式已经退出,原先只读的分区重新受到保护,保留的是之前成功提交的环境参数。

本机 /lib/preinit/31_restore_nvram 的恢复出厂分支会执行 bdata sync,把 bdata 中的参数同步到 NVRAM。这解释了独立保存解锁标志的意义;接下来用真实恢复出厂,验证完整的启动和登录链路。

关键验收:真正恢复出厂,然后直接登录 root

执行前先确认当前已经是 ft_mode=0,bdata 与修改后的镜像一致,crash 与原始备份一致。

然后调用本机原厂的恢复出厂入口:

/usr/sbin/restore_defaults.sh

这个脚本会设置原厂重置标志并重启。测试中没有恢复此前的用户配置,也没有重新利用 datacenter7 或其他漏洞。

启动后依次确认:

检查项 实测结果
管理页面 回到首次设置状态
init_info.inited 0
启动标识 与重置前不同
ft_mode 0
root Telnet 可直接登录,执行 id 得到 UID 0
bdata 完整回读 与修改后镜像逐字节一致
crash 完整回读 与原始全 0xFF 镜像一致

关键输出摘录:

uid=0(root) gid=0(root) groups=0(root)
ft_mode=0
bdata_telnet_en=1
nvram_telnet_en=1
bdata_ssh_en=1
nvram_ssh_en=1
bdata_uart_en=1
nvram_uart_en=1
bdata_boot_wait=on
nvram_boot_wait=on
bdata_flags=0x0

恢复出厂后,root 密码也回到了本机派生的出厂密码。 临时开启 SSH 时设置的密码没有继续保留;使用本机出厂密码成功登录后,又独立复查了一次。设备密码保存在私密备份说明中,文章不公开具体值。

这组结果证明了本次的 root Telnet 入口可以经受真实的恢复出厂。此时 Web 仍是未初始化状态,root 登录验证已经完成。

恢复 SSH,并让主机密钥经受普通重启

有了恢复出厂后仍可使用的 root Telnet,就可以继续处理 SSH。

本机原厂 /etc/init.d/dropbear 还有 release 通道检查。即便 NVRAM 中 ssh_en=1,仍需修补这个服务启动条件。因此通过 Telnet 执行:

nvram set ssh_en=1 && nvram commit
sed -i 's/= "release"/= "XXXXXX"/g' /etc/init.d/dropbear
/etc/init.d/dropbear enable
/etc/init.d/dropbear restart

这段替换针对本机查看过的启动脚本。不同固件先读自己的文件,不能假设条件写法相同。root 密码可以通过 passwd 设置为自己保存的管理口令。

如果旧 Dropbear 与 OpenSSH 客户端协商时提示只提供 ssh-rsa,可以在单次连接中指定:

ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.1

随后恢复基本初始化和 SSH,再运行原工具菜单 6。此时它读到的是:

bdata: telnet_en = 1, ssh_en = 1, uart_en = 1

工具跳过了需要加载内核模块的 bdata 修改步骤,正常安装 SSH 启动补丁、cron 和防火墙钩子。普通重启时,这些脚本负责重新开启 SSH。

同时保存 SSH 主机密钥

这里还处理了一个容易影响长期使用的问题:SSH 服务端的主机密钥位于 /etc/dropbear,需要保存到持久配置目录,让客户端在普通重启后仍能识别同一台机器。

本次将当前密钥复制到补丁目录,再通过 Dropbear 的 rsakeyfile 配置引用:

cp -p /etc/dropbear/dropbear_rsa_host_key \
  /etc/crontabs/patches/dropbear_rsa_host_key
chmod 600 /etc/crontabs/patches/dropbear_rsa_host_key

uci set dropbear.@dropbear[0].rsakeyfile='/etc/crontabs/patches/dropbear_rsa_host_key'
uci commit dropbear

chmod 755 /etc/crontabs/patches/ssh_patch.sh

最后再普通重启一次,直接通过 SSH 登录 root,确认启动标识变化、ft_mode=0、bdata 未变、SSH 主机密钥也未变。防火墙启动钩子、cron 和补丁执行日志均存在。

恢复出厂仍会清除这一层 SSH 配置和密钥备份。 重置后先使用保留的 root Telnet 入口,再恢复 SSH;主机密钥在普通重启和恢复出厂两种场景下的预期也应当分别看待。

这次排障中值得保留的几个细节

要证明真的重启了

本次辅助脚本有一次过早关闭 Telnet 连接,重启命令没有实际执行。旧会话断开、随后还能重新连上,并不能证明系统已经重新启动。

实际验收时比较了:

cat /proc/sys/kernel/random/boot_id
uptime

发现启动标识没变后,修正了命令发送方式,重新完成重启和正常模式检查。公开记录里只保留“前后已变化”的结论,原始标识留在本地验收文件中。

连接超时需要结合链路和设备状态

本次部分启动阶段耗时较长,有线网口也曾暂时显示未连接。检查时分别看了物理链路、管理页是否恢复、22/23 端口是否可达,以及实际登录结果。

开启 Dropbear 后,主机密钥生成也可能需要等待。最终以新连接认证成功、执行 id 得到 root 为准。

密码、启动脚本和 bdata 是不同的持久化对象

这次恢复出厂后,bdata 保留、root Telnet 可用,但 root 密码回到了本机出厂生成值,SSH 启动配置则被清除。

因此,“重置后保留 root 入口”没有包含“用户设置的所有凭据和服务配置都保留”。这也是恢复出厂验收必须真正登录一次的原因。

Telnet 不加密传输,保留该入口后要限制管理网络的访问范围,不向 WAN 暴露它。root 口令、SSH 私钥和设备分区镜像都应私密保存。

最终结果和适用范围

场景 本次结果
通过 datacenter7 临时开启 SSH 成功,root 可登录
最初仅软固化后普通重启 root SSH 可直接登录
最初仅软固化后恢复出厂 SSH 未保留,22 端口关闭
工厂模式下修改 bdata 成功,完整回读与 CRC 校验通过
清除工厂模式标志后启动 ft_mode=0,root Telnet 可用
硬固化后实际恢复出厂 无需漏洞,root Telnet 可直接登录
恢复出厂后的分区验证 bdata 与修改后镜像相同,crash 与原始镜像相同
恢复 SSH 软固化后再次普通重启 root SSH 可登录,SSH 主机密钥不变
升级其他固件、其他型号、UART 通信 未做独立验证

原始镜像、修改后镜像、配置归档、SHA256 清单和机器可读验收记录,最终整理成了本地私密备份包。后续可以继续执行准备好的 UCI 配置;无线方案本身仍需要自己的连通性和重启验收。

这次最有用的排障习惯,是给每种“成功”配一个能直接观测的条件:模块要真的加载,分区要能完整回读,重启要有新的启动标识,恢复出厂要回到首次设置状态,root 入口要用新连接实际认证。

参考与社区反馈

感谢提供方法和讨论记录的 Xierfei、qdore、项目维护者以及其他参与者。这次在本机补上了实际恢复出厂后的 root 登录验证,也把结果反馈回了原讨论。