Refcount overflow use-after-free¶
32-bit'lik bir reference counter'ı, 2^32'yi aşıp sıfıra doğru wrap edene kadar tekrar tekrar increment et; ardından meşru bir
put, canlı reference'lar dururken objeyi free eder ve bir use-after-free verir — klasik CVE-2016-0728 keyring bug'ı.
Mechanism¶
Note
refcount_t var olmadan önce kernel, reference'ları düz bir atomic_t ile sayıyordu — overflow kontrolü olmayan 32-bit bir integer. Lifetime invariant'ı ("count sıfıra ulaştığında free et") sessizce, counter'ın gerçek release'ler dışında asla sıfıra ulaşamayacağını varsayar. Bir atomic_t counter bu varsayımı bozar: wrap eder. Eğer bir attacker count'u 2^32 kez yukarı sürebilirse, sıfırın içinden geri overflow eder. Hardening tartışmasının dediği gibi: "önce counter overflow edilir, sonra sıfıra geri decrement edilir ve objenin erken free edilmesine yol açar."
Tehlike wrap'in kendisi değil, neyi desenkronize ettiğidir: count küçük bir değere wrap ettikten sonra, canlı reference'ların gerçek sayısı counter'ın iddia ettiğinden çok daha büyüktür. Bir sonraki bir avuç meşru put, sahte count'u sıfıra sürer ve objeyi free eder — birçok gerçek reference hâlâ ona işaret ederken. Onların her biri bir dangling pointer olur: reclaim edilen belleği kontrol eden bir attacker'dan ulaşılabilir bir use-after-free. Bu, refcount-imbalance-uaf'nin overflow kardeşidir (o, wrap-around yerine aritmetik imbalance ile erken sıfıra ulaşır).
Walkthrough¶
CVE-2016-0728, security/keys/process_keys.c'deki join_session_keyring()'de yaşıyordu. Bir process zaten ait olduğu bir session keyring'e katılmak istediğinde, bir error/short-circuit path, almış olduğu fazladan reference'ı drop etmeden return ediyordu — çağrı başına keyring'in usage count'unun bir increment'ini leak ediyordu.
/* struct key carries the reference count in an atomic_t */
struct key {
atomic_t usage; /* number of references -- no overflow check */
...
};
/* join_session_keyring: the "already a member" path leaked a usage increment */
if (PTR_ERR(keyring) == -EDEADLK) {
/* ... returns without the matching key_put() -> usage leaks by 1 */
}
Exploitation o leak'i tam bir wrap'e sürer:
- Refcount'u leak et — buggy syscall path'ini tekrar tekrar çağırarak
2^32 - 1kez, böylece keyring hâlâ ziyadesiyle kullanımdaykenusageoverflow eder ve küçük bir değere wrap eder.
/* repeatedly invoke the leaking path ~4 billion times */
for (i = 0; i < 0xfffffffd; i++)
keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leak-keyring");
-
Count'u sıfıra sür: birkaç increment/decrement daha, artık minik olan
usage'ı sıfırın içinden iter. Key garbage collectorusage == 0'ı görür ve normalkey_put()→ free path'i üzerindenstruct key'i free eder — process hâlâ ona geçerli bir reference tutuyor olsa bile. -
Reclaim ve kullan: free edilen slot'u attacker-controlled objelerle spray et (public PoC, kernel message objelerini kullanarak, örn.
msgsndüzerinden, sahte keyring structure'ları spray etti), sonra hâlâ "geçerli" key reference'ı üzerinde işlem yap (keyctl(KEYCTL_REVOKE, ...)) ki kernel attacker-controlled function pointer'lar üzerinden çağrı yapsın — RIP kontrolü ve privilege escalation.
2^32 increment neden uygulanabilir
usage 32-bit bir counter'dır, dolayısıyla overflow ~4.29 milyar increment gerektirir. Disclosure dönemi donanımında Perception Point bunun on dakikalar mertebesinde sürdüğünü ölçtü — yavaş ama yalnızca bir kez çalışması gereken bir local privilege-escalation exploit'i için tamamen pratik. Bug 2012'den (kernel 3.8) beri mevcuttu ve 4.4.1 öncesi tüm kernel'leri etkiledi.
Detection¶
KASAN, stale key reference'ının erken free'den sonra dereference edildiği noktada use-after-free'yi yakalar. Runtime'da refcount_t saturation'ı (aşağıda) overflow'u bir free yerine kernel log'unda bir WARN splat'ına çevirir. Tek bir syscall'ın anormal, sürekli dönüşü (milyarlarca özdeş keyctl çağrısı) devam eden bir overflow girişiminin davranışsal sinyalidir.
Mitigation¶
CVE-2016-0728 için doğrudan fix, error path'e eksik key_put()'u ekledi, böylece count artık leak etmez. Sistemik mitigation, 4.11'de tanıtılan refcount_t'dir: hızlı atomic_t path'lerini çoğaltır ama count'un asla negatife gitmediğine dair bir check ekler ve overflow eden ya da wrap etmek üzere olan bir counter'ı pinlenmiş yüksek bir değere saturate eder (implementasyon INT_MIN/2 civarında pinler — unsigned bit pattern olarak 0xC0000000, yani sıfırdan uzak, wrap-to-zero'yu imkânsız kılan bir değer) ki asla sıfıra geri wrap edemesin. Saturate olduğunda sonraki get/put no-op olur, böylece overflow imkânsızlaşır ve sahte use-after-free yapısal olarak önlenir. Reference counter'ları atomic_t'den refcount_t'ye çevirmek kernel genelinde süregelen remediation'dır; network-stack ve kref dönüşümlerine bak.