Skip to content

Android Binder spinlock UAF (CVE-2022-20421)

Bir binder node refcount dengesizliği, embedded spinlock'u hâlâ canlı olan bir binder_proc'u free eder; böylece free edilmiş bellek üzerinde tutulan bir lock, Pixel sınıfı cihazlarda kernel R/W primitive'ine dönüşür.

Mechanism

Free edilmiş bellekteki tutulan bir spinlock neden exploit edilebilir

Binder her IPC endpoint'ini bir binder_node olarak temsil eder ve başka kaç process'in ona referans verdiğini strong/weak ref count'lar üzerinden takip eder. BINDER_TYPE_HANDLE object'i taşıyan bir transaction işlendiğinde, binder_inc_ref_for_node()'un node referansını tam olarak bir kez artırması gerekir; bu, transaction buffer release edildiğinde gelen bir decrement ile dengelenir. CVE-2022-20421 işte bu muhasebedeki bir uyumsuzluktur: bir saldırgan decrement path'inin eşleşen increment olmadan çalışmasını sağlayabilir, böylece reference count underflow eder ve kernel sahip object'i — nihayetinde binder_proc'u — olması gerekenden erken düşürür.

Bu bug'ı sıra dışı yapan şey, geride bıraktığı primitive'dir. Çoğu kernel UAF'i sana hemen yeniden allocate edeceğin dangling bir object pointer'ı verir. Burada ise dangling olan şey, free edilmiş binder_proc içindeki embedded bir spinlock_t'tir. Binder code path'leri bu lock'u almaya ve bırakmaya devam eder (spin_lock(&proc->...)), dolayısıyla kernel — lock'un val/owner word'lerini — zaten page allocator'a iade edilmiş ve muhtemelen yeniden kullanılmış belleğe write eder. Bir spinlock'un tek başına hijack edilecek yararlı bir data alanı yoktur, bu yüzden exploit'in tamamı o page'i artık neyin beslediği ile ilgilidir.

İhlal edilen invariant lifetime ⊇ all uses'tir: proc'u dereference eden her code path (lock dahil) object'in lifetime'ı içinde olmalıdır. Refcount underflow, canlı kod hâlâ bir pointer tutarken lifetime'ı kısaltır. Lock operasyonları sabit bir offset'te küçük, deterministik write'lar olduğundan, free edilmiş page (bir page-level cross-cache attack ile) groom edilebilir; böylece kontrol edilebilir bir structure — örn. pipe buffer page'leri — bayatlamış lock'un altına yerleşir. İki least-significant bit'i temizlenmiş bir pointer (spinlock word'ü yeniden kullanılan bir pointer'ın alt kısmına alias yapar), exploit'in bir arbitrary read/write'a ve anon_pipe_buf_ops gibi bir kernel .text pointer'ını leak ederek bir KASLR break'ine çevirdiği bir type confusion ortaya çıkarır.

Walkthrough

Bu, savunma/eğitim amaçlı bir yeniden kurgudur. Public exploit, Zhenpeng Lin (0xkol) tarafından yazılan badspin'dir; aşağıdaki adımlar onun şeklini tarif eder, çalıştırılabilir bir drop'u değil.

  1. Binder device'ını aç ve node/ref API yüzeyini öğren:
int fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
/* Drive transactions via BINDER_WRITE_READ with BC_TRANSACTION commands
   carrying flat_binder_object entries of type BINDER_TYPE_HANDLE. */
  1. Refcount dengesizliğini tetikle. Transaction'ları öyle kur ki binder_transaction_buffer_release() üzerinden geçen failure / cleanup path'i, binder_inc_ref_for_node()'un hiç artırmadığı bir node referansını decrement etsin. Underflow, strong ref'i erkenden sıfıra düşürür ve sahip binder_proc'u free eder.

  2. Spinlock hâlâ kullanımdayken page'i free et. Free edilmiş binder_proc bir kmalloc/page-backed allocation içinde yaşar; binder hâlâ embedded lock üzerinde spin_lock/spin_unlock yaparken backing page'in tamamının page allocator'a iade edilmesini sağla.

  3. Page'i, bir page-level cross-cache spray kullanarak saldırgan kontrollü içerikle reclaim et (pipe page'leri temiz bir seçimdir — bkz. pipe-buffer-hijack). Bayatlamış lock write'ları artık saldırganın geri okuyabildiği structure'ların içine düşer.

  4. Type confusion kur. Lock word'ü, reclaim edilmiş object'teki iki LSB'si temizlenmiş bir pointer ile çakışır; o object'i normal bir read path üzerinden geri okumak kısmen değiştirilmiş pointer'ı açığa çıkarır ve onun üzerinden write yapmak kısıtlı bir kernel write verir.

Beklenen son durum (badspin writeup'ından)

[*] binder_proc freed, spinlock still hot
[*] page reclaimed by pipe buffer spray
[+] leaked kernel text ptr (anon_pipe_buf_ops): 0xffffffc00xxxxxxx
[+] KASLR base resolved
[+] arbitrary kernel read/write established
[+] creds overwritten -> uid=0, SELinux permissive
Google Pixel 6'da sonuç: untrusted_app SELinux domain'inden root + SELinux bypass; başka bir memory-corruption primitive'i gerektirmez.

Arbitrary R/W'den root'a geçiş, alışılmış commit-creds / credential-overwrite pattern'ini izler.

Detection

  • dmesg'te binder ref accounting'ten gelen kernel BUG/WARN'ları (binder_dec_node, binder_node ref underflow), denenen bir kötüye kullanımın güçlü bir işaretidir.
  • KASAN build'leri, spinlock erişiminde binder_proc use-after-free'ini yakalar.
  • Yoğun binder transaction churn'ü artı çok sayıda pipe()/page-spray allocation yapan SELinux untrusted_app process'leri, işaretlenmeye değer anormal bir pattern'dir.

Mitigation

  • 2022-10-05 Android security patch level'ı veya sonrasını uygula; fix, binder.c içindeki node reference accounting'i düzeltir.
  • Çıtayı yükselten genel hardening: SLAB freelist randomization, init_on_free ve page-level cross-cache-attack reuse adımını zorlaştıran page-allocator hardening.

References