Android Binder ret2bpf UAF (CVE-2021-0399)¶
Android'in
xt_qtaguidsocket-tagging kodundaki on yıllık bir race, biruid_tag_data/tag_refnesnesini hala bir dangling reference yaşarken free eder;CONFIG_ARM64_UAObulunan ARM64 kernel'larda klasikaddr_limitoverwrite engellendiği için exploit ret2bpf'e pivot eder — kontrol edilebilir kernel payload olarak JIT-compiled bir eBPF program kullanır — ve arbitrary R/W ile root elde eder.
Mechanism¶
qtaguid spinlock race neden UAF'a dönüşür, UAO neden ret2bpf'i zorlar
xt_qtaguid, socket'leri tagging ederek network trafiğini UID başına
hesaplayan Android'e özgü netfilter modülüdür. Control device
/dev/xt_qtaguid, üç reference-counted yapıyı manipüle eden text command'lar
(ctrl_cmd_tag / ctrl_cmd_untag) kabul eder: sock_tag (tag'lenmiş bir
socket), tag_ref / tag_ref_tree (tag başına refcount) ve uid_tag_data
(tag_ref entry'lerinin sahibi olan, UID başına accounting).
Bug (NVD: "In qtaguid_untag of xt_qtaguid.c, there is a possible
memory corruption due to a use after free", CWE-416) bir locking
race'tir: fix commit'i ctrl_cmd_tag için "doesn't correctly grab the
spinlocks when tagging a socket," der; bu da bir socket'in tag_ref
entry'sinin, başka bir thread'in eşzamanlı olarak free ettiği bir
uid_tag_data entry'sini point etmeye devam etmesine izin verir. Aynı
socket'i tag/untag eden iki thread race eder; biri uid_tag_data'yı free
ederken diğeri hala onun (artık dangling olan) tag_ref_entry'sini reference
eder — bir use-after-free. Kırılan
invariant şu: shared bir refcounted node'un her reader/freer'ı, lookup ve
mutate penceresinin tamamı boyunca yapının spinlock'unu tutmalıdır; kısmi
lock coverage bir free'nin bir use ile overlap etmesine izin verir.
Bu UAF'ı 64-bit bir Android Pie cihazında root'a çevirmek bir mitigation
duvarına çarpar. Ucuz klasik primitive — free edilen nesneyi reallocate et,
thread_info->addr_limit'i ~0'a overwrite et ve sonra sıradan
copy_to/from_user üzerinden tüm kernel memory'sini read/write et —
CONFIG_ARM64_UAO (User Access Override) tarafından defeat edilir.
UAO ile, addr_limit'e uyması beklenen uaccess rutinleri bunun yerine
page table'lara karşı kontrol edilen LDTR/STTR user-access
instruction'larını çalıştırır; dolayısıyla forge edilmiş bir kernel
addr_limit artık bir usercopy'den kernel erişimi sağlamaz.
addr_limit-overwrite primitive'i etkisiz hale gelir.
ret2bpf UAO'yu by-pass eder. addr_limit'i corrupt etmek yerine exploit,
control flow'u (UAF üzerinden) bir JIT-compiled eBPF program'a hijack
eder. eBPF program'lar kernel BPF JIT tarafından executable kernel page'lerde
yaşayan native koda derlenir ve attacker, program'ın instruction'larını
(verifier modulo) tamamen kontrol eder. O BPF koduna — kernel-resident,
attacker-authored bir payload — "return ederek" exploit, read ve write
işlemlerini UAO'nun block etmediği gerçek kernel-mode instruction'larla yapar
ve addr_limit'e hiç dokunmadan arbitrary kernel R/W elde eder. Bu,
bpf-jit-spray / ebpf-jit-spray'de de
görülen BPF-JIT-as-payload fikridir; burada sprey edilen bir landing pad
yerine post-hijack kod hedefi olarak kullanılır.
Walkthrough¶
The research (Xingyu Jin & Richard Neal, Google Android Security; HITB SecConf 2021) demonstrated the chain on a Xiaomi Mi 9 running Android Pie.
Step 1 — open the control device and learn the command grammar:
int fd = open("/dev/xt_qtaguid", O_RDONLY);
// ctrl commands are written as text, e.g. "t <sock_fd> <tag> <uid>" (tag)
// "u <sock_fd>" (untag)
Step 2 — trigger the UAF by racing tag/untag on the same socket from multiple
threads so the missing-spinlock window frees uid_tag_data while a tag_ref
still references it:
// thread A: repeatedly tag the socket -> ctrl_cmd_tag (incomplete locking)
write(fd, tag_cmd, len);
// thread B: repeatedly untag/delete -> frees uid_tag_data
write(fd, untag_cmd, len);
// race -> dangling tag_ref_entry -> use-after-free on the freed uid_tag_data
Step 3 — reclaim the freed object with a controlled allocation of the same kmalloc size, planting a fake structure whose fields steer a later dereference into attacker-chosen code (heap grooming / spray of a same-size object).
Step 4 — prepare the ret2bpf payload. Load an eBPF program; the kernel BPF JIT emits it as native code in an executable kernel page. The program body is the read/write engine the exploit will run in kernel context:
struct bpf_insn prog[] = { /* attacker-authored R/W gadget body */ };
int prog_fd = bpf(BPF_PROG_LOAD, &attr, sizeof(attr)); // JIT -> kernel exec page
Step 5 — drive the dangling reference to hijack control flow into the JIT'd BPF code (ret2bpf). Because the payload executes as real kernel instructions, UAO is irrelevant and the program reads/writes arbitrary kernel memory:
UAF dereference -> hijacked indirect transfer -> JIT'd eBPF program
-> arbitrary kernel read/write (UAO-immune)
Step 6 — escalate. With kernel R/W, overwrite the current task's credentials to UID 0 and clear SELinux enforcement, then verify:
Why UAO breaks addr_limit but not ret2bpf
Pre-UAO, get_fs()/set_fs() and addr_limit gate every copy_to_user /
copy_from_user: forge addr_limit = KERNEL_DS and a usercopy will happily
read kernel memory into your buffer. CONFIG_ARM64_UAO rewrites those
usercopy paths to unprivileged-load/store (LDTR/STTR) instructions whose
access is decided by the page-table permission bits for the supplied
address, ignoring the software addr_limit. So a forged addr_limit buys
nothing. ret2bpf does not rely on usercopy semantics at all — it executes an
attacker-written program as kernel code, where ordinary kernel loads and
stores already have kernel privilege, so the UAO transformation never
applies.
Detection¶
- SELinux / seccomp: restricting unprivileged
bpf()(e.g. denyingBPF_PROG_LOADto apps) removes the ret2bpf payload primitive. - KASAN test kernels flag the
uid_tag_datause-after-free directly. - Racing bursts of tag/untag writes to
/dev/xt_qtaguidfrom one app is an exploit-shaped access pattern; the device is privileged on modern Android.
Mitigation¶
- Apply the Android Security Bulletin fix for CVE-2021-0399 (correct spinlock
acquisition in
ctrl_cmd_tag); newer Android replacedxt_qtaguidwith eBPF traffic monitoring entirely. - Unprivileged BPF disabled (
kernel.unprivileged_bpf_disabled=1) and SELinux denial ofbpf()to app domains break ret2bpf. - BPF JIT hardening (
net.core.bpf_jit_harden, constant blinding) andCONFIG_BPF_JIT_ALWAYS_ON=nchoices reduce JIT-payload controllability. - Generic UAF hardening (slab freelist randomization,
init_on_free, hardened usercopy) raises the cost of the reclaim step.