vivo U1 Root方案——用8年前的0day圆8年前的梦

2018年搞安卓的时候,小米、华为、oppo均有官方解锁和Root方案,唯独vivo从始至终不提供bootloader解锁,也就无法root。

随着大解锁时代的到来,我也翻出了抽屉里这台 vivo U1 ,时隔八年,我终于成功将它解锁和root了。

AI用来干重体力活儿真是太好了 其实并未用到最新的技术,全是2018年本身就存在的技术

技术路线

  1. 拆机进入9008,下载泄露的firehose,写入devinfo失败
  2. 现挖firehose漏洞,写入devinfo成功,解锁bootloader
  3. 清空数据
  4. 刷magisk失败,刷魔改过的magisk-suu失败,卡logo或反复重启
  5. patch kernel的反root魔改,刷magisk成功,获得root
  6. 适配各个版本的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

vivo U1 手机后盖

小小吐槽一下,这手机2018年发布的,ROM早已在2021年停更,内核居然维护到2025年9月,本文在2026年写作,传奇耐更王。

它有另一个马甲,叫vivo Y93,只是名字不一样。

EDL 9008 写入devinfo失败

参考:记一次vivo Y93刷机清除密码、获取Root权限的历程

该文章并未实现Root,根据网络上的资料,这款机型短接测试点即可进入9008,在主板右侧有两个非常明显的触点。

进入9008要点:

  • 必须拆开螺丝,断开电池排线,如图所示。

    vivo U1 主板与排线位置

  • 操作步骤:插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工具,我做了如下动作

  1. 连接手机 [ok]
  2. 列举分区表 [ok]
  3. dump boot.img, devinfo.img [ok]
  4. 编辑一个空白的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
2
3
4
5
6
7
8
// auth 位于手机 RAM:0x08062F04
handle_program(...) {
if (auth == 0) {
log("Unauthorized");
return NAK;
}
return write_storage(...);
}

parse_sig 的作用是验证授权签名,然后更新这个变量:

1
2
3
4
5
6
7
8
M = "emmcid=" + 设备标识 + "&key=elk&op=qfil"
H = SHA256(M)

S = hex_decode(sigchar0 + ... + sigchar7) // 256 字节
EM = RSA_公钥运算(内置公钥, S)

auth = (EM 的前 224 字节 == 固定 PKCS#1 编码前缀
且 EM 的后 32 字节 == H) ? 1 : 0

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 空操作 本清单未将其标记为实测成功

肉眼发现了叫 pokepatch 的 API,于是让 AI 尝试去调用一下,调用 patch 就直接成功了。

那么,patch 路径缺少签名校验,这应该是个0day吧。

解锁教程

命令:edl xml set-unlocked-and-critical.xml

819456 是此次核对的这台手机上 devinfo 的起始扇区;1624 分别对应 0x100x18size_in_bytes="4" 配合 value="1",写入的是小端字节 01 00 00 00

⚠️警告:扇区编号 819456 不一定对其他机型通用,建议交给AI进行分析

set-unlocked-and-critical.xml 文件:(如果失败了,就拆成2个xml去执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0"?>
<data>
<patch SECTOR_SIZE_IN_BYTES="512"
byte_offset="16"
filename="DISK"
physical_partition_number="0"
size_in_bytes="4"
start_sector="819456"
value="1" />
<patch SECTOR_SIZE_IN_BYTES="512"
byte_offset="24"
filename="DISK"
physical_partition_number="0"
size_in_bytes="4"
start_sector="819456"
value="1" />
</data>

bootloader解锁结算画面

1
2
3
4
5
6
7
8
9
10
11
➜ edl git:(master) ✗ fastboot getvar unlocked
unlocked: yes Finished.
Total time: 0.001s

➜ edl git:(master) ✗ fastboot oem device-info
(bootloader) Device tampered: false
(bootloader) Device unlocked: true
(bootloader) Device critical unlocked: true
(bootloader) Charger screen enabled: false
(bootloader) Display panel: OKAY
[ 0.006s] Finished. Total time: 0.006s

解锁了好像不会自动弹OrangeState,刷了修改过的boot.img,就会出现下面这张图:

vivo U1 bootloader 解锁后的启动提示

内核的反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
2
3
4
5
6
7
exec 公共路径:0xffffff800823c6b0
├─ 路径前置检查:0xffffff80082645d0
│ └─ P1 basename 检查:0xffffff800825c8d4
│ └─ 共享 su 策略:0xffffff800825c67c
├─ do_open_execat
├─ P2 文件对象检查
└─ search_binary_handler

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
2
3
plaintext[i] = encoded[i] XOR ((-66 - (seed & 0x2f) - i) & 0xff)
seed = 1
结果 = /system

历史补丁把这份内容改为解码后的 /syswxl

P4:mount 函数执行时,do_mount_check

挂载前的完整调用顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
解析目标路径
→ security_sb_mount(标准 LSM)
→ namespace / CAP_SYS_ADMIN 检查
→ 计算 Linux 内部挂载参数
→ P4:check_flags 是否非零
→ 重挂载时优先取得现有 mount 的真实设备名
→ 建立本次函数校验 bitmap 上下文
→ B:检查启动进度
→ M:必要时取得解析后的目标路径
→ do_mount_check
→ 对关键检查函数做完整性校验
→ Linux 实际 remount / bind / move / new mount
→ 成功且 check_flags 非零时,进入 P3 所在的后处理
→ 清理上下文和任务信息

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 目录项。在正常对象关系下,路径子检查的关键放行条件是:

  1. selected 就是原始文件目录项,或没有选出目录项;普通根目录直接文件 /init 可落入这个例外。
  2. 否则,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。

成功截图

vivo U1 Root 成功截图

成功运行了:magisk + zygisk + lsposed + busybox + Systemless Hosts ,感觉没什么问题

原始的内核和修改后的内核文件在:https://github.com/4accccc/vivo-4.x-kernel-autopatch/discussions/1#discussioncomment-18412672