折腾笔记

折腾笔记

小米 17 Ultra 掉 Root 修复记|不下载 10.4GB 线刷包,只从里面抠出 5MB

2026-09-18

事情是怎么开始的

起因说起来很朴素:更新完系统,忘了处理槽位就顺手重启了。

HyperOS 的 OTA 走的是 A/B 双分区机制——更新的时候,新系统不会覆盖当前正在运行的那个槽,而是先写进另一个空闲的槽位,然后打个标记:下次启动从这个新槽进。重启之后,系统才真正切过去生效。

而这台机器上,root 是挂在 init_boot 分区上的,init_boot 每个槽位各有一份init_boot_a / init_boot_b),两份互不共享。之前打好的 KernelSU 补丁只活在原来那个槽里;更新时写进新槽的那份 init_boot 是官方原版,干干净净,没有内核模块。

所以 A/B 机型更新系统的正经姿势是:在重启生效之前先处理槽位,把 root 补丁同步到即将启用的那个新槽(SukiSU / KernelSU 里的「安装到未使用的槽位(OTA 后)」就是专门干这事的)。这一步做了,更新完重启,新系统带着 root 一起起来,什么都没发生。

这次就是漏了这一步。更新包下完、进度条走完、系统提示「重启以完成更新」,然后就直接点了重启,压根没想起来槽位这回事。

(顺便说一句,这台机器跑的是内测版本,这一点在后面会变成一个不小的麻烦。)

重启完再打开 SukiSU Ultra,赫然一行字:未安装。root 掉了。

SukiSU Ultra 显示未安装

顺手确认一下系统落在哪个槽:

adb shell "getprop ro.boot.slot_suffix"
# _a        ← 已经切到新槽了

💡 先说明一件事:这不是变砖。系统一切正常——能开机、能联网、数据全在、版本也没错,只是正在运行的那份 init_boot 里没有 KernelSU 而已。丢的是 root,不是系统。

截图里那串系统指纹(CP2A.260605.016 / OS4.0.0.27.XPACNXM)后面会变成整个修复过程的关键。

root 掉了最难受的地方在于——想修 root,得先有 root

KernelSU / SukiSU 是把内核模块注入到 init_boot 分区的 ramdisk 里的,所以正确的修复姿势是把手机上原始的那份 init_boot.img 导出来重新打补丁。而导出分区最直接的办法是 dd

adb shell "dd if=/dev/block/by-name/init_boot_a of=/sdcard/init_boot.img bs=4096"
# dd: /dev/block/by-name/init_boot_a: Permission denied

读块设备需要 root,现在没 root,读不了;读不了就修不了 root。标准的死循环。

所以只剩一条路可走:从官方固件包里把这一版的原版 init_boot 提取出来

第一步:先确认版本,这是生死线

刷 init_boot 这件事,版本必须和手机上正在跑的系统完全一致。差一个小版本都可能刷完直接卡 fastboot 开不了机,因为 init_boot 里装的就是这一版系统的 ramdisk,和内核、系统镜像对不上就起不来。

所以第一件事不是去下载,而是先把手机上的版本号读准确,一个字都不能靠记忆:

adb shell "getprop ro.product.device"
adb shell "getprop ro.build.display.id"
adb shell "getprop ro.build.version.incremental"

实测结果:

项目 实测值
机型 / 代号 小米 17 Ultra / nezha
系统版本 OS4.0.0.27.XPACNXM
Android 构建号 CP2A.260605.016
Android 版本 17(SDK 37)
当前槽位 _a
Bootloader 已解锁

这里有个特别容易踩的坑:HyperOS 的 OS4.0.0.27.XPACNXM 和小米的构建号 CP2A.260605.016两套编号,指的是同一个版本。找固件的时候两个都要能对上,不然很容易下错包。

第二步:翻了一圈,是个坏消息

按常规思路,去下这个版本的线刷包(fastboot 包,tgz 格式),解压进 images/ 文件夹就能直接拿到原版 init_boot.img,这是最省事的办法。

结果全网翻了个遍,OS4.0.0.27.XPACNXM 的线刷包根本不存在

原因其实很简单:这台机器跑的是内测版本。

小米的线刷包是跟着正式版 / 稳定版走的,内测版本不进公开渠道,自然也不会发对应的线刷包——在公开镜像站上翻到天荒地老也不会有的。内测版能拿到的,只有通过内测通道推下来的 OTA 卡刷包。

版本类型 官方是否提供线刷包
稳定版 / 正式版 ✅ 有 fastboot 线刷包(.tgz
内测版 / Beta ❌ 通常只有 OTA 卡刷包(.zip

能翻到的公开线刷包里,最新的只到 OS3.0.309.0.WPACNXM(Android 16 正式版),而手上这台跑的是 Android 17 的内测版——版本号和 Android 大版本都对不上。

⚠️ 随手抓一个 OS3.0.309.0.WPACNXM(Android 16)的 init_boot 往这台机器上刷,那就是标准的"刷完卡 fastboot 变砖"。

所以内测版掉 root,从一开始就注定走不了"下个线刷包、解压完事"这条路——它没有线刷包可用,只能从卡刷包里精确提取

好消息是,内测版的 OTA 卡刷包和机器上跑的版本是完全一致的:

nezha-ota_full-OS4.0.0.27.XPACNXM-user-17.0-8965156a6f.zip    (约 10.4 GB)

要的东西就在里面,只是这个包有点大。

第三步:能偷懒的路,全被堵死了

在老老实实下载 10.4GB 之前,先试了几条捷径:

思路 结果
fastboot fetch init_boot_a 直接从设备导出分区 Device does not support fetch command
重启进 fastbootd(用户空间 fastboot)再 fetch fetch not supported on user builds
免 root 直接 dd 读分区 ❌ Permission denied

fetch 是 Android 平台专门为"从设备导出分区镜像"设计的命令,理论上不用下整个固件包就能把分区拿出来——可惜小米的 bootloader 压根没实现它。

然后实测了一下这个包的下载速度:150 ~ 300 KB/s。10.4GB 得下十几个小时,不能接受。

那就换个思路。

第四步:只下载真正需要的那几 MB

既然下载慢,那就别下整个包

两个关键发现:

  • 小米的 CDN 支持 HTTP Range,也就是可以只请求文件里的某一段字节;
  • 这个 zip 里的 payload.bin未压缩存储的(compression method = 0)。

第二点才是重点。zip 内的文件如果没压缩,它的字节偏移就是可以直接算出来的,意味着我可以精确定位到包里任意一段数据,只把它拉下来。

整个包的结构大致是这样:

nezha-ota_full-OS4.0.0.27.XPACNXM-xxx.zip        (10,430,488,299 bytes)
├── payload.bin                       @ offset 5149
│   ├── manifest (protobuf)   377,921 bytes   ← 分区清单:每个分区的数据块位置 + SHA-256
│   └── data blobs                            ← 真正的镜像数据(XZ / BZ2 分块)
├── payload_properties.txt            @ 10430746828
└── ... 其他 OTA 元数据

所以整个流程是这样的:

① 拉 zip 尾部 256KB    → 解析 ZIP 中央目录,定位 payload.bin 偏移与大小
② 拉 payload 头部 3MB  → 拿到 protobuf manifest
③ 解析 manifest        → 找到 init_boot 的 4 个数据块(2×XZ + 2×BZ2,共约 2MB)
④ 按块拉取这 2MB       → 每一块用 manifest 里的 SHA-256 立刻校验
⑤ 解压 + 按序拼接      → 得到完整的 init_boot.img

第 ④ 步是整个过程里最舒服的地方:manifest 给每个数据块都附了 SHA-256,所以拉回来的每一块都能当场验证真伪,不用担心网络传输出错。

最后 4 个块 4/4 全部 SHA-256 匹配,重组出来的镜像:

校验项 结果
文件大小 8,388,608 字节(正好 8 MiB)
magic ANDROID!
kernel_size / ramdisk_size 0 / 2,588,280
header_version 4

这里 kernel_size = 0 这个细节要单独说一下:它是 GKI 机型的典型特征——内核在 boot 分区里,ramdisk 在 init_boot 分区里。所以这类机器修 root 只能修补 init_boot,绝对不能碰 boot.img(网上很多教程还在教人修补 boot,刷进去直接黑屏)。

整个过程累计只从 CDN 拉取了不到 7MB 数据,对比 10.4GB 的完整包,省掉了 99.9% 的流量。

第五步:用 SukiSU Ultra 打补丁

把提取出来的原版镜像推回手机:

adb push init_boot_stock.img /sdcard/Download/

然后打开 SukiSU Ultra → 点【未安装】→【安装】:

安装面板,提示建议选择 init_boot 分区镜像

注意看,SukiSU 自己在这里就写了「建议选择 init_boot 分区镜像」,跟前面对 GKI 的判断完全一致。

点【选择一个文件】,选中刚推过去的 init_boot_stock.img,下一步会弹出 KMI 选择框:

选择 KMI

KMI(Kernel Module Interface)必须选当前设备的版本,这里自动选中的是 android16-6.12,对应本机内核 6.12.69-android16-...。列表里从 android12-5.10 一路排下来,选错任何一个,模块都加载不起来。

确认之后就是自动修补:

修补完成日志

日志里能清楚看到它做了什么:

- Unpacking boot image
- KMI: android16-6.12
- Adding KernelSU LKM
- Removing spoof_release config
- Removing spoof_version config
- Repacking boot image
- Output file is written to
- /storage/emulated/0/Download/kernelsu_patched_20260918_020239.img
- Done!

把修补前后的镜像拉回电脑对比一下:

项目 原版 修补后
文件大小 8,388,608 8,388,608
ramdisk_size 2,588,280 2,974,909(+386 KB)
ksud / kernelsu 字符串 0 处 4 处

ramdisk 变大了一圈、多出 KernelSU 相关字符串——说明 LKM 确实被注入进去了。

第六步:刷入并验证

adb reboot bootloader
fastboot flash init_boot kernelsu_patched.img
fastboot reboot

刷入返回 OKAY,重启后系统版本没有任何变化(仍是 OS4.0.0.27.XPACNXM),内核模块也正常挂载了:

$ adb shell cat /proc/modules | grep -i ksu
kernelsu 200704 1 - Live

打开 SukiSU Ultra,主页显示 工作中 <LKM>

Root 恢复,SukiSU 显示工作中

超级用户、模块列表都回来了,LSPosed 和 Zygisk 也恢复正常。root 到手。

中途踩的两个坑

坑一:HyperOS 把 adb shell input 封死了

SukiSU 那几步点击(点卡片 → 选文件 → 选 KMI → 确认)本来想用 adb shell input tap 全自动跑完,结果:

SecurityException: Injecting to another application requires
INJECT_EVENTS permission

tapkeyevent 全部被拒,连 settings putpm grant 也一样被拦——这是 HyperOS 的安全策略,必须在开发者选项里打开「USB 调试(安全设置)」才会放开模拟点击。

绕过办法是 monkey 脚本注入input 被拦,但 monkey -f 允许执行事件脚本文件。

type= raw events
count= 2
speed= 1.0
start data >>-
DispatchPointer(0,0,0,600,620,1,1,0,1,1,0,0)
UserWait(120)
DispatchPointer(0,0,1,600,620,1,1,0,1,1,0,0)

有个很容易忽略的细节:按下和抬起之间必须加 UserWait。按下时长为 0 的点击在 Compose 写的界面上是不生效的,一开始怎么点都没反应就是这个原因。

坑二:MSYS 的路径转换会搞坏传给 adb / curl 的参数

在 Windows 的 Git Bash 里调用 adb.execurl.exe 时,/sdcard/Download/xxx 这类参数会被 MSYS 自作主张转换成本地路径(变成 C:/Program Files/Git/sdcard/...),表现就是 push 莫名其妙失败、curl 报 client returned ERROR on write

后者尤其坑——报错信息看起来像网络问题,实际上是本地写入路径被改坏了,白白排查了半天。解决办法是用 Windows 风格路径,或者设 MSYS2_ARG_CONV_EXCL='*' 直接关掉转换。

回滚姿势

原版镜像一定要留好,而且建议存两份(电脑一份、手机一份):

# 万一以后出问题,刷回原版即可
fastboot flash init_boot init_boot_stock.img
fastboot reboot

刷回官方原版 init_boot 之后,root 消失但系统完全正常,可以重来一遍。

这份原版镜像对内测版尤其重要——内测版本的包不进公开渠道,过了推送窗口就更难找,线刷包又压根没有。这 8MB 的东西某种意义上比系统本身还金贵,建议电脑和手机各存一份——只放手机里的话,万一哪天手机进不去系统,就真拿不出来了。

下次怎么避免再掉一次

这次的坑完全可以绕开,核心就一句话:OTA 和 root 的处理顺序不能颠倒

时机 该做的事
更新包下载完、还没重启 打开 SukiSU →【安装】→ 选「安装到未使用的槽位(OTA 后)」,把补丁预写进那个还没启用的槽
重启之后 什么都不用管,root 跟着新系统一起起来

SukiSU 的安装界面里本来就摆着这个选项,只是平时用不上,很容易被忽略。另外还有三条保险:

  • 别把「重启以完成更新」当成随手一点的按钮。看到这句提示先停三秒,想一下槽位处理了没有——系统更新提示是不会替你考虑 root 的。
  • 两个镜像都留着:原版 init_boot_stock.img(出事能刷回去)+ 修补好的 kernelsu_patched.img(同版本直接重刷一次就恢复 root)。只要系统版本不变,这两个文件随时能救命。
  • 跑内测版的,更要把这一步做扎实:内测版没有公开线刷包兜底,一旦掉了 root,只能像本文这样从 OTA 卡刷包里一点点抠出来,费时费力。正式版掉 root 叫"麻烦",内测版掉 root 叫"真麻烦"。

如果已经掉到无 root 状态了,那就只能回到本文开头,把「提取原版镜像 → SukiSU 打补丁 → fastboot 刷入」这套流程重走一遍。

最后

回头看,这次翻车其实分成两段:掉 root 是更新时漏了槽位那一步,修 root 则卡在版本匹配上。

  1. 预防靠顺序——A/B 机型 OTA 之前把补丁装进「未使用的槽位」,root 根本不会掉,这一步花不了一分钟;
  2. 修复靠版本——修 init_boot 之前,版本必须和手机上正在运行的系统一模一样,这是唯一的硬约束。跑内测版的话还要多一层认知:官方不会给内测版发线刷包,别在镜像站上白费功夫,直接从 OTA 卡刷包里提取才是正路。如果当时图省事抓了 Android 16 的线刷包,刷进去就是卡 fastboot;
  3. 省流量靠巧劲——遇到包太大、下载太慢,不一定非得全量下载。HTTP Range + zip 未压缩存储这组合,是可以直接从远端把需要的那几 MB "抠"出来的。

顺带提醒一句:手里如果存着别的版本的 init_boot.img(比如从搞机工具箱里翻出来的旧包),千万别拿来混用。这次就翻出一个内嵌 OS4.0.0.24.XPACN 的 boot.img,看着像这个机器的,实际是上一个版本的——刷下去就是变砖。