Skip to content

阶段二:恢复机制探索与破解

🕒 Published at:

阶段二:恢复机制探索与破解 ​

2.1 问题发现 ​

现象 ​

每次修改 system 分区后重启,所有修改都被还原。包括:

  • 删除的 APK 会重新出现
  • 修改的配置文件会恢复原样
  • 删除的二进制会重新出现

初步猜测 ​

最初认为是 Android 的恢复机制,可能是:

  1. 运营商 OTA 自动恢复
  2. 系统分区只读保护
  3. 某种写保护机制

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 字节)

关键字段 ​

字段说明
WorkMode0=正常, 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 进恢复模式

关键发现 ​

  1. set_fs_safe_mode 在每次 ext4 错误时都会被调用
  2. 不只是启动时,运行时也会触发
  3. 即使 fstab 设了 errors=continue,驱动仍然拦截
  4. 驱动在 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 分区

恢复模式操作 ​

恢复模式会:

  1. 格式化 /system 分区
  2. 从隐藏备份恢复 /system
  3. 恢复 /boot 分区(原始 boot.img)
  4. 恢复 /data 分区
  5. 所有修改丢失

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 分钟后重启。原因:

  1. 删除 framework 文件
  2. 某些服务尝试访问这些文件
  3. ext4 检测到文件缺失
  4. 触发 ext4 错误处理
  5. set_fs_safe_mode 拦截
  6. SafeErrCode++
  7. 2 分钟后达到阈值
  8. 触发恢复

确认测试 ​

删除当贝桌面(不关键的 APK)→ 系统稳定运行 删除 framework JAR → 2 分钟后重启

2.7 最终解决方案 ​

二进制 Patch 内核 ​

既然无法从配置层面解决,直接 patch 内核二进制。

定位函数 ​

  1. 使用 strings 找到关键字符串
  2. 使用 ADRP 扫描找到引用这些字符串的代码
  3. 定位 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

阶段二完成:恢复机制完全破解