IQOO Z9 Turbo+ / VIVO 系设备 REC 检测 - 去除 Root 标识

本文最后更新于:1 小时前

IQOO Z9 Turbo+ / VIVO 系设备 REC 检测 - 去除 Root 标识

来源:IDA Pro7.7分析
设备:IQOO Z9 Turbo+(蓝厂 VIVO 系设备)
目标:去除 REC(恢复模式)中的 Root 检测标识,恢复未 Root 状态
核心原理:修改 backup 分区 NV 数据结构中的 is_root 字段,并同步更新 MD5 校验值
本文由openclaw整理生成


一、背景与原理概述

1.1 存储分区分析

VIVO/IQOO 设备中,Root 标识可能存储在以下位置:

分区/路径 说明 是否直接存储 root 标识
/dev/block/by-name/nvram NVRAM 原始分区 ❓ 未确定
/dev/block/by-name/nvdata NVRAM 数据分区 ❓ 未确定
/dev/block/by-name/nvcfg NVRAM 配置分区 ❓ 未确定
/dev/block/by-name/persist persist 分区(ext4) ⚠️ 可能相关
/dev/block/by-name/backup 备份分区 已确认为主要存储位置
/data/vendor/engineermode/nvram_item 明文备份文件 记录日志,非 root 标识存储
/persist/opporeserve/iqoo_nv.bin IQOO 定制 NV 文件 ⚠️ 可能相关
/persist/security/root_flag Root 标志文件 ⚠️ 可能相关

1.2 核心发现

经过代码分析,确认:

  1. Root 标识 is_root 存储在 backup 分区的 NV 数据结构中
  2. 该数据结构同时被备份到明文文件 /data/vendor/engineermode/nvram_item
  3. MD5 校验保护:前 524 字节的 MD5 值存储在偏移 524539(0x20C0x21B)
  4. 修改后必须重新计算并写入 MD5,否则系统校验失败,可能拒绝启动或使用默认值

二、关键函数分析

以下函数来自逆向工程(ARM64 伪代码):

2.1 sub_2B69B0 — 从 backup 读取 NV 数据

1
2
3
4
5
6
7
8
9
10
11
12
功能:从 backup 分区读取 IQOO NV 数据
路径尝试顺序:
1. /dev/block/by-name/backup
2. /dev/block/bootdevice/by-name/backup
3. /dev/block/platform/bootdevice/by-name/backup

读取参数:
- 块偏移:a2 << 12(即 a2 * 4096
- 每次读取 4 块,每块 0x200 = 512 字节,共 2048 字节
- 有效 NV 数据大小:0x7AC = 1964 字节

返回值:成功返回 1,失败返回 0

2.2 sub_2B6210 — 写入 backup 分区

1
2
3
4
5
6
7
8
功能:向 backup 分区写入 NV 数据(与 sub_2B69B0 对称)
写入参数:同读取
写入模式:O_RDWR (1052674)
写入后调用 fdatasync() 强制刷盘

特殊处理:
- 写入前计算 MD5(第 0~523 字节,共 524 字节)
- 将 MD5 结果写入偏移 524~539

2.3 sub_2B5290 — NV 数据写入 nvram_item 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
功能:将 NV 数据同时写入明文备份文件
文件路径:/data/vendor/engineermode/nvram_item

写入内容(完整 NV 结构体解析):
- offset 0 (4B): gsensor_calibration_flag
- offset 4 (4B): master_clear_flag
- offset 8 (4B): master_clear_result
- offset 12 (4B): adb_flag
- offset 16 (16B): iqoo_sv (字符串)
- offset 27*4 (4B): ★ is_root ★ ← 核心目标字段
- offset 28*4 (4B): is_bksn_ok
- offset 29*4 (4B): is_first_boot
...
- offset 540 (16B): MD5 校验值(低 8 字节)
- offset 524 (16B): MD5 校验值(高 8 字节)

注意:写入 nvram_item 仅作日志记录,不影响实际使用。
但 backup 分区才是系统真正读取的存储位置。

2.4 sub_2B6BB0 — NV 数据 MD5 校验

1
2
3
4
5
6
7
8
功能:验证 NV 数据结构完整性
校验算法:
1. 取偏移 0~523 共 524 字节
2. 计算 MD5(结果为 16 字节 = 128bit)
3. 前 8 字节存入偏移 524
4. 后 8 字节存入偏移 532

返回值:校验通过返回 1,失败返回 0

三、NV 数据结构速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
整体结构大小:0x7AC = 1964 字节

偏移 大小 字段名
─────────────────────────────────────────────
0x000 4B gsensor_calibration_flag
0x004 4B master_clear_flag
0x008 4B master_clear_result
0x00C 4B adb_flag
0x010 16B iqoo_sv (版本字符串)
...
0x06C ★ 4B is_root ← 核心修改目标!
0x00000000 = 未 Root
0x00000001 = 已 Root
...
0x20C MD5[0:8] (前8字节)
0x214 MD5[8:16] (后8字节)
─────────────────────────────────────────────

关键偏移计算is_root 在 offset 27,27 × 4 = 0x6C(108 字节)


四、修改方法(手动 Hex 编辑)

⚠️ 操作有风险,请提前备份原始文件!

4.1 工具准备

工具 说明
HxD Windows 平台推荐,免费十六进制编辑器
010 Editor 专业级,支持模板
Android ADB 用于刷入修改后的镜像

4.2 获取 backup 分区镜像

1
2
3
4
5
6
7
8
9
10
# 方法1:设备上直接提取
adb shell
su
dd if=/dev/block/by-name/backup of=/sdcard/backup.img

# 方法2:若路径不同,尝试
dd if=/dev/block/bootdevice/by-name/backup of=/sdcard/backup.img

# 拉取到电脑
adb pull /sdcard/backup.img C:\backup\backup.img

4.3 定位并修改 is_root 字段

① 备份原文件

1
backup.img 复制一份为 backup_original.img

② 打开 HxD,跳转到偏移 0x6C

HxD 操作:

  • 菜单 → 搜索 → 跳转到 → 输入 6C → 选择”十六进制”
  • 或按 Ctrl+G 输入 6C

③ 确认当前值

  • 当前 Root:01 00 00 00(小端序,值为 1)
  • 目标值: 00 00 00 00(小端序,值为 0)

④ 修改

选中 4 个字节,直接输入 00 00 00 00,保存文件。

4.4 重新计算 MD5

① 提取前 524 字节用于计算

在 HxD 中:

  • 选中偏移 0x000 ~ 0x20B(共 524 字节)
  • 右键 → 复制选块 → 另存为 chunk_524.bin

② 计算 MD5(Windows certutil)

1
2
# 方法1:certutil(Windows 自带)
certutil -hashfile chunk_524.bin MD5

输出示例:

1
2
3
MD5 哈希(适合 chunk_524.bin):
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
CertUtil: -hashfile 命令成功完成。

③ 将 MD5 写入偏移 0x20C

在 HxD 中:

  • Ctrl+G → 输入 20C → 跳转
  • 将 32 位十六进制字符转换为字节写入(共 16 字节)

例如:a1 b2 c3 d4 e5 f6 a7 b8 c9 d0 e1 f2 a3 b4 c5 d6

偏移 写入值
0x20C a1
0x20D b2
0x20E c3
0x20F d4
0x210 e5
0x211 f6
0x212 a7
0x213 b8
0x214 c9
0x215 d0
0x216 e1
0x217 f2
0x218 a3
0x219 b4
0x21A c5
0x21B d6

④ 保存修改后的文件

4.5 刷入设备

1
2
3
4
5
6
7
8
9
10
# 推送修改后的镜像到设备
adb push backup_modified.img /sdcard/backup.img

# 进入 root shell 并刷入
adb shell
su
dd if=/sdcard/backup.img of=/dev/block/by-name/backup

# 同步
sync

五、修改方法(Python 自动化脚本)

推荐使用此方法,自动处理 MD5 重新计算

5.1 修改脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env python3
"""
IQOO Z9 Turbo+ / VIVO 系设备
backup 分区 is_root 字段修改工具

功能:
1. 读取 backup.img
2. 将 is_root 字段(偏移 108)置为 0
3. 重新计算前 524 字节的 MD5
4. 将新 MD5 写入偏移 524~539
5. 输出修改后的镜像

用法:
python modify_backup.py backup.img backup_modified.img
"""

import hashlib
import sys
import os

# ===================== 配置区 =====================
# is_root 字段在 NV 数据结构中的偏移(字节)
IS_ROOT_OFFSET = 108

# MD5 计算范围:前 524 字节
MD5_RANGE = 524

# MD5 存储偏移(紧接在数据区之后)
MD5_OFFSET = 524
# ===========================================


def modify_backup_img(input_file: str, output_file: str) -> bool:
"""
修改 backup.img 的 is_root 字段并更新 MD5

参数:
input_file : 原始 backup.img 路径
output_file : 修改后输出路径

返回:
True 成功,False 失败
"""

# 读取原始文件
with open(input_file, 'rb') as f:
data = bytearray(f.read())

print(f"[+] 文件大小: {len(data)} 字节")

# 验证文件大小
if len(data) < MD5_RANGE + 16:
print(f"[✗] 文件太小 ({len(data)} 字节),不是有效的 backup 镜像")
return False

# 读取原始 is_root 值(偏移 108,共 4 字节,uint32 小端序)
original_is_root = int.from_bytes(
data[IS_ROOT_OFFSET:IS_ROOT_OFFSET + 4], 'little'
)
print(f"[*] 原始 is_root 值: {original_is_root} "
f"({'已 Root' if original_is_root else '未 Root'})")

if original_is_root == 0:
print("[*] is_root 已经是 0,无需修改")
# 仍重新计算 MD5(以防文件损坏)
else:
# ========== 步骤1:修改 is_root 字段 ==========
data[IS_ROOT_OFFSET:IS_ROOT_OFFSET + 4] = b'\x00\x00\x00\x00'
print(f"[+] 已将 is_root 置为 0")

# ========== 步骤2:重新计算 MD5 ==========
chunk_to_hash = bytes(data[:MD5_RANGE])
new_md5 = hashlib.md5(chunk_to_hash).digest()

print(f"[*] 新 MD5 (16 字节): {new_md5.hex()}")

# ========== 步骤3:写入新 MD5 ==========
data[MD5_OFFSET:MD5_OFFSET + 16] = new_md5
print(f"[+] 新 MD5 已写入偏移 0x{MD5_OFFSET:X} ~ 0x{MD5_OFFSET + 15:X}")

# ========== 步骤4:验证 MD5 ==========
recalc_md5 = hashlib.md5(bytes(data[:MD5_RANGE])).digest()
if recalc_md5 == new_md5:
print("[✓] MD5 验证通过")
else:
print("[✗] MD5 验证失败!")
return False

# ========== 步骤5:写入输出文件 ==========
with open(output_file, 'wb') as f:
f.write(data)

size_kb = os.path.getsize(output_file) / 1024
print(f"[✓] 已保存修改后的镜像: {output_file} ({size_kb:.1f} KB)")

return True


def verify_backup(img_path: str) -> dict:
"""
验证 backup.img 的 is_root 状态和 MD5

返回:
dict,包含 is_root 值、MD5 状态等
"""
with open(img_path, 'rb') as f:
data = f.read()

if len(data) < MD5_RANGE + 16:
return {"valid": False, "error": "文件太小"}

is_root = int.from_bytes(data[IS_ROOT_OFFSET:IS_ROOT_OFFSET + 4], 'little')

stored_md5 = data[MD5_OFFSET:MD5_OFFSET + 16]
calc_md5 = hashlib.md5(data[:MD5_RANGE]).digest()

return {
"valid": True,
"is_root": is_root,
"is_root_str": "已 Root" if is_root else "未 Root",
"md5_match": stored_md5 == calc_md5,
"stored_md5": stored_md5.hex(),
"calc_md5": calc_md5.hex(),
"file_size": len(data),
}


def main():
if len(sys.argv) < 2:
print("用法:")
print(" 验证: python modify_backup.py verify backup.img")
print(" 修改: python modify_backup.py backup.img output.img")
sys.exit(1)

if sys.argv[1] == "verify":
if len(sys.argv) < 3:
print("用法: python modify_backup.py verify backup.img")
sys.exit(1)
result = verify_backup(sys.argv[2])
print("=" * 45)
print(f" 文件: {sys.argv[2]}")
print(f" 大小: {result.get('file_size', 'N/A')} 字节")
print(f" is_root: {result.get('is_root_str', 'N/A')}")
print(f" MD5 校验: {'✓ 通过' if result.get('md5_match') else '✗ 失败'}")
print(f" 存储 MD5: {result.get('stored_md5', 'N/A')}")
print(f" 计算 MD5: {result.get('calc_md5', 'N/A')}")
print("=" * 45)
return

# 修改模式
if len(sys.argv) < 3:
print("用法: python modify_backup.py backup.img output.img")
sys.exit(1)

input_file = sys.argv[1]
output_file = sys.argv[2]

if not os.path.exists(input_file):
print(f"[✗] 文件不存在: {input_file}")
sys.exit(1)

print("=" * 45)
print(" IQOO Z9 Turbo+ / VIVO Root 标识修改工具")
print("=" * 45)

success = modify_backup_img(input_file, output_file)

if success:
print("\n[*] 下一步:")
print(" 1. 将 backup_modified.img 推送到设备")
print(" 2. 在 root shell 中执行:")
print(" dd if=/sdcard/backup_modified.img of=/dev/block/by-name/backup")
print(" 3. 重启设备验证")
else:
sys.exit(1)


if __name__ == "__main__":
main()

5.2 使用说明

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 验证当前镜像状态
python modify_backup.py verify backup.img

# 2. 执行修改
python modify_backup.py backup.img backup_fixed.img

# 3. 刷入设备
adb push backup_fixed.img /sdcard/
adb shell
su
dd if=/sdcard/backup_fixed.img of=/dev/block/by-name/backup
sync
reboot

六、刷入后的验证

6.1 验证 MD5 匹配

1
2
3
4
5
6
# 推送验证脚本到设备
adb push modify_backup.py /sdcard/
adb shell
su
cd /sdcard
python3 modify_backup.py verify backup_fixed.img

6.2 检查 root 属性

1
getprop | grep root
  • is_root 字段修改成功且 MD5 校验通过,Root 检测应失效。

6.3 刷入后系统无反应的应对

若 MD5 校验不通过导致系统无法启动:

  • 方案 A:在 recovery 或 fastboot 模式刷入原版 backup 镜像恢复
  • 方案 B:将 MD5 字段全部写 0,让系统使用默认值(is_root=0
    1
    2
    # 紧急恢复:MD5 全部写 0
    data[524:540] = b'\x00' * 16

七、其他可能涉及的存储位置

根据分析,以下路径也可能存储 root 相关标志,建议同步检查:

路径 说明
/persist/security/root_flag 直接以文件名存储 root 标志
/persist/opporeserve/iqoo_nv.bin IQOO 定制 NV 文件
/persist/vivo/emergent_mode 应急模式标志
/data/vendor/engineermode/nvram_item 明文备份(仅记录,不影响校验)

八、注意事项与风险提示

⚠️ 重要声明

  • 操作前务必备份原始 backup.img
  • backup 分区同时存储了大量校准数据(gsensor、gyro、光感等),修改后可能影响校准状态
  • MD5 校验失败时系统会使用内存中的默认值,短期内可能正常,但不稳定
  • 若设备已加密或使用动态分区,部分路径可能不存在
  • 修改系统存储涉及安全机制,仅供学习研究使用


IQOO Z9 Turbo+ / VIVO 系设备 REC 检测 - 去除 Root 标识
https://moranwp.eu.org/IQOO_Z9Turbo_RootFlag_Remove_Guide.html
作者
墨染_nlx
发布于
2026年3月27日
许可协议