Skip to content

Android Binder ret2bpf UAF (CVE-2021-0399)

Android'in xt_qtaguid socket-tagging kodundaki on yıllık bir race, bir uid_tag_data/tag_ref nesnesini hala bir dangling reference yaşarken free eder; CONFIG_ARM64_UAO bulunan ARM64 kernel'larda klasik addr_limit overwrite 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:

# id
uid=0(root) ...  context: ... (permissive)
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. denying BPF_PROG_LOAD to apps) removes the ret2bpf payload primitive.
  • KASAN test kernels flag the uid_tag_data use-after-free directly.
  • Racing bursts of tag/untag writes to /dev/xt_qtaguid from 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 replaced xt_qtaguid with eBPF traffic monitoring entirely.
  • Unprivileged BPF disabled (kernel.unprivileged_bpf_disabled=1) and SELinux denial of bpf() to app domains break ret2bpf.
  • BPF JIT hardening (net.core.bpf_jit_harden, constant blinding) and CONFIG_BPF_JIT_ALWAYS_ON=n choices reduce JIT-payload controllability.
  • Generic UAF hardening (slab freelist randomization, init_on_free, hardened usercopy) raises the cost of the reclaim step.

References