阶段二:恢复机制探索与破解
2.1 问题发现
现象
每次修改 system 分区后重启,所有修改都被还原。包括:
- 删除的 APK 会重新出现
- 修改的配置文件会恢复原样
- 删除的二进制会重新出现
初步猜测
最初认为是 Android 的恢复机制,可能是:
- 运营商 OTA 自动恢复
- 系统分区只读保护
- 某种写保护机制
2.2 conf 分区发现
发现过程
通过串口查看分区设备:
bash
ls -l /dev/block/发现 /dev/block/conf (mmcblk0p3, 4MB)。
读取 conf 分区
bash
hexdump -C /dev/block/conf | head -50输出:
00000000 ff fe 5b 53 79 73 74 65 6d 5d 0a 57 6f 72 6b 4d |..[System].WorkM|
00000010 6f 64 65 3d 30 0a 53 61 66 65 45 72 72 43 6f 64 |ode=0.SafeErrCod|
00000020 65 3d 32 0a 4d 6f 6e 69 74 6f 72 46 61 69 6c 65 |e=2.MonitorFaile|
00000030 64 4e 75 6d 3d 31 0a 00 00 00 00 00 00 00 00 00 |dNum=1..........|conf 分区格式
格式: INI, append-only
编码: UTF-16LE + BOM (ff fe)
每条大小: 2048 字节
结构:
0x0000 ff fe (BOM)
0x0002 [System]\n (UTF-16LE)
WorkMode=N\n
SafeErrCode=N\n
MonitorFailedNum=N\n
[Common]\n (可选)
KeyControlTime=N\n
[QOS]\n (可选)
ErrorCodeStr=...\n
00 00 ... (零填充到 2048 字节)关键字段
| 字段 | 说明 |
|---|---|
| WorkMode | 0=正常, 1=恢复模式 |
| SafeErrCode | 安全错误计数器 |
| MonitorFailedNum | 触发恢复的阈值 |
2.3 内核驱动分析
发现 set_fs_safe_mode
通过 strings 分析内核:
bash
strings /tmp/ota/boot.img | grep -i "set_fs_safe_mode\|SafeErrCode\|MonitorFailedNum"找到关键字符串:
_____%d > %s goto set_fs_safe_mode
[set_fs_safe_mode %d]
WorkMode=%d
SafeErrCode=%d
MonitorFailedNum=%d
ALERT:File system panic, system will enter safemode...
Restarting system from safe_mode.驱动工作原理
内核启动
↓
set_fs_safe_mode 驱动加载
↓
读取 /dev/block/conf (mmcblk0p3)
↓
检查上次关机是否干净?
├─ 干净 → SafeErrCode 不变
└─ 不干净 → SafeErrCode++
↓
SafeErrCode >= MonitorFailedNum ?
├─ 否 → 正常启动
└─ 是 → WorkMode=1 → bootloader 进恢复模式关键发现
- set_fs_safe_mode 在每次 ext4 错误时都会被调用
- 不只是启动时,运行时也会触发
- 即使 fstab 设了 errors=continue,驱动仍然拦截
- 驱动在 fstab 处理之前就拦截了 ext4 错误
2.4 恢复机制完整流程
从 ext4 错误到恢复
1. ext4 检测到错误(文件缺失、损坏等)
2. ext4 错误处理函数被调用
3. set_fs_safe_mode 拦截错误
4. 检查条件: error_count > threshold
5. 如果条件满足 → 进入 set_fs_safe_mode 函数
6. 读取 conf 分区
7. 递增 SafeErrCode
8. 写回 conf 分区
9. 检查: SafeErrCode >= MonitorFailedNum ?
10. 如果是 → 设置 WorkMode=1
11. 调用 panic/restart
12. 系统重启
13. bootloader 读取 conf → WorkMode=1
14. 进入恢复模式
15. 恢复 system、boot、data 分区恢复模式操作
恢复模式会:
- 格式化 /system 分区
- 从隐藏备份恢复 /system
- 恢复 /boot 分区(原始 boot.img)
- 恢复 /data 分区
- 所有修改丢失
2.5 尝试的解决方案
方案 1: 修改 conf 分区
尝试: 清零 conf 分区
bash
dd if=/dev/zero of=/dev/block/conf bs=4096 count=1024结果: 失败。MonitorFailedNum=0 导致 SafeErrCode(0) >= 0 = TRUE,每次都触发恢复。
教训: conf 是 append-only,清零后 MonitorFailedNum 变成 0,反而更容易触发恢复。
方案 2: 设置 MonitorFailedNum=255
尝试: 写入正确的 conf 值
bash
echo -e "[System]\nWorkMode=0\nSafeErrCode=0\nMonitorFailedNum=255\n" > /tmp/conf_fix.txt
dd if=/tmp/conf_fix.txt of=/dev/block/conf bs=4096结果: 部分成功。阈值提高了,但 ext4 错误仍然触发恢复。
原因: 驱动在每次 ext4 错误时都会递增 SafeErrCode,即使阈值是 255,也会很快达到。
方案 3: 修改 fstab
尝试: 给 /system 分区加 errors=continue
/dev/block/system /system ext4 rw,barrier=1,errors=continue wait结果: 失败。set_fs_safe_mode 在 fstab 处理之前就拦截了 ext4 错误。
方案 4: 修改 init.rc
尝试: 设置 panic_on_oops=0
write /proc/sys/kernel/panic_on_oops 0结果: 失败。set_fs_safe_mode 不依赖 panic_on_oops。
2.6 关键突破
发现真正触发点
通过分析内核字符串,发现:
EXT4-fs error
previous I/O error to superblock detected
I/O error while writing superblock
_____%d > %s goto set_fs_safe_mode触发条件是 ext4 超级块 I/O 错误!
2 分钟重启现象
发现设备总是运行 2 分钟后重启。原因:
- 删除 framework 文件
- 某些服务尝试访问这些文件
- ext4 检测到文件缺失
- 触发 ext4 错误处理
- set_fs_safe_mode 拦截
- SafeErrCode++
- 2 分钟后达到阈值
- 触发恢复
确认测试
删除当贝桌面(不关键的 APK)→ 系统稳定运行 删除 framework JAR → 2 分钟后重启
2.7 最终解决方案
二进制 Patch 内核
既然无法从配置层面解决,直接 patch 内核二进制。
定位函数
- 使用 strings 找到关键字符串
- 使用 ADRP 扫描找到引用这些字符串的代码
- 定位 set_fs_safe_mode 函数入口
Patch 点
偏移 (文件) 原始指令 Patch 后 作用
0x47A0E0 ADRP x0, 0x991000 RET ALERT 函数直接返回
0x47A110 BL 0x37C98 NOP 去掉 panic/restart 调用
0x47A11C STP x29, x30, [sp,-96]!RET set_fs_safe_mode 直接返回Patch 脚本
python
#!/usr/bin/env python3
import struct
RET = 0xD65F03C0 # ARM64 RET 指令
NOP = 0xD503201F # ARM64 NOP 指令
with open('/tmp/boot_cma16.img', 'rb') as f:
boot = bytearray(f.read())
# Patch 1: ALERT 处理函数
struct.pack_into('<I', boot, 0x800 + 0x4798E0, RET)
# Patch 2: panic/restart 调用
struct.pack_into('<I', boot, 0x800 + 0x479910, NOP)
# Patch 3: set_fs_safe_mode 函数
struct.pack_into('<I', boot, 0x800 + 0x47991C, RET)
with open('/tmp/boot_cma16_patched.bin', 'wb') as f:
f.write(boot)验证
刷入 patched boot.img 后:
- 删除 framework JAR → 系统不重启 ✓
- 删除所有 APK → 系统不重启 ✓
- 等待 3 分钟 → 系统稳定运行 ✓
- 删除所有二进制 → 系统不重启 ✓
2.8 conf 分区最终状态
生成正确的 conf 镜像
python
#!/usr/bin/env python3
def make_entry(workmode, safeerrcode, monitorfailednum):
content = f"[System]\nWorkMode={workmode}\nSafeErrCode={safeerrcode}\nMonitorFailedNum={monitorfailednum}\n"
bom = b'\xff\xfe'
text_bytes = content.encode('utf-16-le')
raw = bom + text_bytes
return raw + b'\x00' * (2048 - len(raw))
# 创建 4MB 镜像
image = bytearray(4 * 1024 * 1024)
# 历史条目
entries = [
(0, 2, 1), # 原始
(1, 2, 2), # 恢复尝试
(1, 0, 2), # 恢复成功
(0, 0, 2), # 正常启动
(0, 0, 2), # 正常启动
(0, 0, 2), # 正常启动
(0, 0, 0), # 问题:MonitorFailedNum=0
(0, 0, 255), # 修复:MonitorFailedNum=255
]
for i, (wm, se, mf) in enumerate(entries):
offset = i * 2048
entry = make_entry(wm, se, mf)
image[offset:offset + len(entry)] = entry
with open('/tmp/conf_fixed.bin', 'wb') as f:
f.write(image)刷入
bash
dd if=/storage/usb0/conf_fixed.bin of=/dev/block/conf bs=4096阶段二完成:恢复机制完全破解