vivo U1 Root方案——用8年前的0day圆8年前的梦
2018年搞安卓的时候,小米、华为、oppo均有官方解锁和Root方案,唯独vivo从始至终不提供bootloader解锁,也就无法root。
随着大解锁时代的到来,我也翻出了抽屉里这台 vivo U1 ,时隔八年,我终于成功将它解锁和root了。
AI用来干重体力活儿真是太好了 其实并未用到最新的技术,全是2018年本身就存在的技术
技术路线
- 拆机进入9008,下载泄露的firehose,写入devinfo失败
- 现挖firehose漏洞,写入devinfo成功,解锁bootloader
- 清空数据
- 刷magisk失败,刷魔改过的magisk-suu失败,卡logo或反复重启
- patch kernel的反root魔改,刷magisk成功,获得root
- 适配各个版本的magisk找到全部patch点
未尝试的技术路线:
- 通过kernel-exp读写devinfo进行解锁
设备信息
- 名称:vivo U1
- 型号:V1818A
- Android版本:8.1.0
- Android安全补丁程序级别:2021-03-01
- vivo ROM:Funtouch OS_9
- 软件版本号:PD1818G_A_5.10.45
- 内核版本:4.9.82-perf+
- 编译时间:2025-09-03

小小吐槽一下,这手机2018年发布的,ROM早已在2021年停更,内核居然维护到2025年9月,本文在2026年写作,传奇耐更王。
它有另一个马甲,叫vivo Y93,只是名字不一样。
EDL 9008 写入devinfo失败
参考:记一次vivo Y93刷机清除密码、获取Root权限的历程
该文章并未实现Root,根据网络上的资料,这款机型短接测试点即可进入9008,在主板右侧有两个非常明显的触点。
进入9008要点:
必须拆开螺丝,断开电池排线,如图所示。

操作步骤:插USB,短接触点然后松开触点,自动进入9008;短接触点,插USB,自动进入9008,然后松开触点;
Windows10 + QFILTools 读分区表会卡死,原因未知
使用macbook 和 https://github.com/bkerler/edl ,一次成功
一些背景知识(含AI生成内容)
firehose是什么:手机进入救砖状态后,Android 通常没有运行,不能依靠系统帮你读写存储。这时,电脑就发送一个专门的程序到手机,让它负责接收指令、操作存储芯片,这就是大家说的 Firehose 文件。严格说,Firehose 是通信协议的名字,这个文件是实现该协议的程序,也叫 programmer 或 loader。它的入口由芯片里固化的最初级启动代码PBL提供,也就是 Boot ROM,因此即使闪存里的后续启动程序损坏,也可能仍能进入这个模式。
firehose是有合法性的:手机生产时,厂商会在芯片的 eFuse/QFPROM,也就是一次性可编程的硬件区域里,预先写入一个根证书的哈希值。可以把它理解成“认可的签发机构的指纹”。BootROM会校验firehose的签名,也就是说,只能用泄露出来的文件,调用它的接口。
如何判定bootloader已解锁:在骁龙439设备上,实际分析的是可下载的 5.10.18 全量 OTA 中的 emmc_appsboot.mbn,包内升级脚本明确将它写入 aboot,所以它可以用于分析该版本的解锁实现。aboot读取devinfo里的数据,可以进行逆向和验证,如果标志位是1,则跳过boot.img是否属于官方签名,只关注boot.img的基础完整性。
以下偏移相对于本样本使用的 devinfo 分区开头:
| 偏移 | 类型 | 含义 |
|---|---|---|
0x00 |
13 字节 | ANDROID-BOOT!\0 |
0x10 |
32 位小端整数 | bootloader unlocked |
0x14 |
32 位小端整数 | tampered |
0x18 |
32 位小端整数 | critical unlocked |
devinfo读取成功,写入失败
这部分全程是我让AI干的,使用edl这款python工具,我做了如下动作
- 连接手机 [ok]
- 列举分区表 [ok]
- dump boot.img, devinfo.img [ok]
- 编辑一个空白的bit,刷入手机 [fail]
日志是:
1 | result is=0, Unauthorized tag '' |
在该facebook里也有人提到这个现象:https://www.facebook.com/groups/246863286031193/posts/887923035258545/
搜遍全网,从各处下载到的 firehose 文件的md5清一色都是:2f37225263d8b7824aceb88e85f55fc0,然后意外发现 edl 官方仓库也收录了一个,叫:Loaders/vivo/000bf0e100000000_60ba997fef6da9f0_fhprg_peek.bin,md5是 aa7a973783e5944efddcfbed7540a94a。
二者的签名都非常完整,均可以和手机里的数据进行交互,但都会遇到无法写入的问题,经过逆向,并未有特别大的差异。
要点:loader的写入是一次性的,如果切换loader,需要断电重新进入9008,然后再使用新的loader。
写入失败原因分析(含AI生成内容)
最初失败,是 loader 的写入授权状态为 0,在接收分区数据前就拒绝了请求。
把 Y93.elf 的反汇编还原成伪代码,就是:
1 | // auth 位于手机 RAM:0x08062F04 |
parse_sig 的作用是验证授权签名,然后更新这个变量:
1 | M = "emmcid=" + 设备标识 + "&key=elk&op=qfil" |
edl客户端将sigchar发送给firehose,由于客户端没有私钥,所以签名检查失败,写入分区的行为被拒绝。
现挖漏洞,写入devinfo成功
之前失败的API叫 program,意思是编程和写入,既然 program 这个API有签名检查,那么其他API呢?于是让AI逆向firehose,列出全部的API,尝试使用其他API进行写入。
| XML 命令 | 处理函数地址 | 用途 | 此前验证情况 |
|---|---|---|---|
| configure | 0x08029968 | 配置存储类型、传输参数等 | 初始化成功 |
| query_auth_id_info | 0x0802c474 | 获取设备绑定的待签名授权消息 | 返回过 emmcid=…&key=elk&op=qfil |
| parse_sig | 0x0802ab28 | 接收签名并更新授权状态 | 静态分析;未发送 |
| program | 0x0802bcf4 | 按扇区写存储 | 实测在分发层被 Unauthorized/NAK 拒绝 |
| firmwarewrite | 0x0802a118 | 接收固件更新数据并调用底层固件更新路径 | 未实测;未完整审计底层目标和限制 |
| patch | 0x0802b0cc | 修改存储扇区内指定字节,也支持 CRC 等表达式 | 未实测 |
| setbootablestoragedrive | 0x0802ca28 | 设置可启动的存储硬件分区 | 未实测 |
| ufs | 0x0802cb94 | 设置 UFS 存储额外参数 | 未实测;本机存储为 eMMC |
| emmc | 0x0802cb94 | 设置 eMMC 存储额外参数 | 未实测 |
| power | 0x0802bbb8 | 电源/重启控制 | 未实测 |
| benchmark | 0x08029574 | 存储性能测试 | 未实测 |
| read | 0x0802c5e0 | 按扇区读取存储 | GPT 和 devinfo 读取成功 |
| getstorageinfo | 0x0802a9f0 | 获取容量、块大小、厂商等存储信息 | 初始化过程中成功取得;省略参数的独立请求曾 NAK |
| getcrc16digest | 0x0802a4cc(参数 1) | 计算指定存储范围 CRC16 | 未实测 |
| getsha256digest | 0x0802a4cc(参数 0) | 计算指定存储范围 SHA-256 | 未实测 |
| erase | 0x08029e70 | 擦除指定存储范围 | 未实测 |
| peek | 0x0802b768 | 读取设备内存地址 | 指定代码/状态地址读取成功 |
| poke | 0x0802b960 | 向设备内存地址写值 | 未实测 |
| nop | 0x0802ab1c | 空操作 | 本清单未将其标记为实测成功 |
肉眼发现了叫 poke 和 patch 的 API,于是让 AI 尝试去调用一下,调用 patch 就直接成功了。
那么,patch 路径缺少签名校验,这应该是个0day吧。
解锁教程
命令:edl xml set-unlocked-and-critical.xml
819456 是此次核对的这台手机上 devinfo 的起始扇区;16、24 分别对应 0x10、0x18。size_in_bytes="4" 配合 value="1",写入的是小端字节 01 00 00 00。
⚠️警告:扇区编号 819456 不一定对其他机型通用,建议交给AI进行分析
set-unlocked-and-critical.xml 文件:(如果失败了,就拆成2个xml去执行)
1 |
|
bootloader解锁结算画面
1 | ➜ edl git:(master) ✗ fastboot getvar unlocked |
解锁了好像不会自动弹OrangeState,刷了修改过的boot.img,就会出现下面这张图:

内核的反root检测和绕过方式
含AI生成内容,本人表达可能不清楚,看不懂就去让AI逆向内核
反复重启和卡logo时,进入fastboot的按键是:插着USB,音量加、音量减、电源键全部一起按住,长按断电,松开再长按进入fastboot。
由于 https://github.com/4accccc/vivo-4.x-kernel-autopatch 搜索的特征无法命中 4.9.82 的关键代码,所以只能根据前人的经验,手动进行patch。
vivo-4.x-kernel-autopatch 的特征搜索和patch方案比较粗暴,建议作为 AI 分析和验证的参考
其中 P1~P4 已经在 https://github.com/topjohnwu/Magisk/issues/5148 有明确的记载,P5和P6是本次新发现的检测点。
P1:检查用户请求的执行路径,限制传统 su 入口
在公共 exec 流程中,内核取得待执行文件的路径字符串后,先调用厂商前置检查,再调用 do_open_execat 打开文件;通过以后才会继续准备程序映像、交给 search_binary_handler。
本机调用关系为:
1 | exec 公共路径:0xffffff800823c6b0 |
P2:检查实际打开的文件,补上路径解析后的名称限制
为什么 P1 后面还需要 P2?用户提交的路径文字与内核最终打开的文件对象不是同一层信息。路径可能包含符号链接,解析后获得的是另一个目录项。
P2 位于 do_open_execat 成功返回之后。它接收真正的 struct file *,从 file->f_path.dentry 取得文件名指针和长度,不再使用最初那段路径字符串。
这两处检查形成“请求名称”和“解析所得名称”的两次核对。例如,在身份和厂商策略均满足限制条件时,一个末尾不叫 su、但解析后指向名为 su 的文件的符号链接,可以避开 P1 的字符串匹配,却仍被 P2 识别。
P3:禁止对 /system 这个路径进行挂载,防止“把受保护挂载伪装成普通挂载”
P3 的有效数据位于 0xffffff80095a19f8,原始 7 字节为 92 cf c2 c9 cd dd da。本机比较函数按下面的逐字节关系解释它:
1 | plaintext[i] = encoded[i] XOR ((-66 - (seed & 0x2f) - i) & 0xff) |
历史补丁把这份内容改为解码后的 /syswxl。
P4:mount 函数执行时,do_mount_check
挂载前的完整调用顺序
1 | 解析目标路径 |
P5:检查syscall,kernel SID 的非 init 进程,必须来自允许的执行位置,只影响magisk>=25
Magisk v25.0 开始重写的 MagiskInit / SELinux 策略注入机制,Magisk 先创建一个早期助手进程,再让原厂 init 继续运行,助手通过 FIFO 同步接管策略加载。
vivo 的 syscall 包装器会审查助手仍持有的初始 kernel SID 和可执行文件位置,检查失败后直接结束助手,原厂 init 因等不到 FIFO 的另一端而停住。
本机原始内核在部分 syscall 前安装了包装器,ARM64 native 和 ARM32 compat 都先进入共同检查函数 0xffffff800809bf74,检查通过才执行原 syscall。它沿当前 exe_file 的目录项向上遍历,并在跨挂载边界时继续沿父挂载的挂载点遍历,得到一个用于检查的 selected 目录项。在正常对象关系下,路径子检查的关键放行条件是:
selected就是原始文件目录项,或没有选出目录项;普通根目录直接文件/init可落入这个例外。- 否则,
selected的名字必须恰好为六个字节的system;例如通常的/system/bin/init。
| 对象拓扑 | selected | 路径子检查 |
|---|---|---|
根目录普通文件 /init |
orig 本身 | 放行 |
根目录普通文件 /arbitrary |
orig 本身 | 放行 |
/sbin/magiskinit |
sbin | 拒绝 |
/system/bin/init |
system | 放行 |
/data/local/tmp/helper |
data | 拒绝 |
/system_ext/bin/helper |
system_ext | 拒绝 |
P6:检查syscall 221 execve,只影响64位的magiskinit
arm32的execve调用号是11,所以不受影响。
从实现推断,P6 要求真正的 PID 1 在一次特定的用户态 native exec 之后,不应继续以初始 kernel SID 使用这些敏感接口,防止额外启动代码一直沿用早期特殊身份。
各个版本的patch需求一览表
| 版本 | 位数 | P1~P4 | P5 | P6 |
|---|---|---|---|---|
| magisk <= 24 | 32 | 需要 | 无需 | 无需 |
| magisk >= 25 | 32 | 需要 | 需要 | 无需 |
| magisk == 24 | 64 | 需要 | 无需 | 需要 |
| magisk >= 25 | 64 | 需要 | 需要 | 需要 |
Magisk从24开始才提供64位的magiskinit程序,但其实magiskinit32和magisk64在启动过程中的作用没有区别,没有强迫症的话可以一直用magiskinit32来引导系统,示意如下:
- A: 64 位内核 → 32 位 magiskinit → 64 位原厂 init → Android 与 64 位 Magisk 服务
- B: 64 位内核 → 64 位 magiskinit → 64 位原厂 init → Android 与 64 位 Magisk 服务
整体规则:
- 推荐:懒得思考就把P1~P6全部都patch掉
- 全部需要P1~P4
- 大于等于magisk-25的需要P5
- 64位的magiskinit需要P6
Patch前后的字节变化一览
设备固件:PD1818_A_5.10.45,Linux 4.9.82-perf+。offset 相对于 gzip 解压后的 raw kernel,不是 boot.img 偏移或运行时虚拟地址。
| 编号 | raw kernel offset | 原字节 | 补丁后字节 | 修改说明 |
|---|---|---|---|---|
| P1 | 0x1dc8fc |
1f cc 01 71 |
1f dc 01 71 |
请求路径 basename 的 su 匹配改为 wu。 |
| P2 | 0x1e46d0 |
5f cc 01 71 |
5f dc 01 71 |
实际 dentry 名称的 su 匹配改为 wu。 |
| P3 | 0x15219f8 |
92 cf c2 c9 cd dd da |
92 cf c2 c9 ce c0 db |
共享编码字符串 /system → /syswxl。 |
| P4 | 0xf19474 |
b6 04 00 35 |
16 00 80 52 |
cbnz w22 → mov w22,#0,清零 vivo 内部 check_flags。 |
| P5 | 0x1c1bc |
7f 00 07 eb |
ff 03 1f eb |
cmp x3,x7 → cmp xzr,xzr,进入检查通过的分支。 |
| P6 | 0x1c238 |
a6 60 1c 39 |
bf 60 1c 39 |
strb w6 → strb wzr,使 PID 1 native exec 标志写入 0。 |
成功截图

成功运行了:magisk + zygisk + lsposed + busybox + Systemless Hosts ,感觉没什么问题
原始的内核和修改后的内核文件在:https://github.com/4accccc/vivo-4.x-kernel-autopatch/discussions/1#discussioncomment-18412672