poll_list object spray (CoRJail)¶
poll()/ppoll()'un slow path'inde allocate edilen değişken boyutlustruct poll_list'i sprayable bir elastic object olarak kötüye kullan — boyutu kontrol edilebilir, lifetime'ı kontrol edilebilir venextpointer'ının corruption'ı bir arbitrary-free primitive sağlar (CoRJail Docker escape'i).
Mechanism¶
Note
Bir poll()/ppoll() çağrısı, kernel'in on-stack fast path'ine sığandan daha fazla
file descriptor izlediğinde do_sys_poll() heap allocation'a düşer: her biri
struct_size(walk, entries, len) ile kmalloc'lanan struct poll_list chunk'larından
oluşan singly linked bir list kurar. İki özellik bunu bir exploitation primitive yapar.
(1) Elastic size — FD count'unu attacker seçer, dolayısıyla allocation kmalloc-32'den
kmalloc-4k'ya kadar herhangi bir cache'e yönlendirilebilir. (2) Attacker-held lifetime —
objeler bloklayan poll()'un tüm süresi boyunca yaşar (timeout ile veya izlenen bir FD'yi
un-ready tutarak kontrol edilir), yani bir spraying thread istediği kadar uzun süre bir
slab slot'unu pin'leyebilir. Kritik olarak, chunk'lar teardown sırasında traverse edilip
kfree()'lenen bir list oluşturur: bir chunk'ın next pointer'ını corrupt etmek kernel'in
attacker'ın seçtiği bir adresi free etmesini sağlar — bir arbitrary free.
struct poll_list {
struct poll_list *next; /* off 0x00 <-- corrupt -> arbitrary kfree() */
int len; /* off 0x08 number of pollfd entries */
struct pollfd entries[]; /* off 0x10 8 bytes each */
};
Walkthrough¶
Slow path allocate eder ve tamamlanınca her node'u free ederek list'i walk eder:
/* do_sys_poll: slow-path allocation */
len = min(todo, POLLFD_PER_PAGE); /* <= 510 pollfd per page-sized chunk */
walk = walk->next = kmalloc(struct_size(walk, entries, len), GFP_KERNEL);
/* teardown: free the whole chain */
walk = head->next;
while (walk) {
struct poll_list *pos = walk;
walk = walk->next; /* attacker-controlled if corrupted */
kfree(pos);
}
Spraying sadece her biri seçilmiş bir FD count ile poll() çağıran çok sayıda thread'tir:
struct pollfd pfds[N]; /* N tunes the kmalloc size class */
for (int i = 0; i < N; i++) { pfds[i].fd = quiet_fd; pfds[i].events = POLLIN; }
/* each thread pins one poll_list chunk for `timeout` ms */
poll(pfds, N, timeout_ms);
CoRJail'de (corCTF 2022), bug bir procfs modülündeki off-by-null'dır: bir page'i kmalloc'lar
ve input tam olarak PAGE_SIZE olduğunda sonu bir byte geçen yere sonlandırıcı bir '\0' yazar:
len = count > PAGE_SIZE ? PAGE_SIZE - 1 : count;
syscalls = kmalloc(PAGE_SIZE, GFP_ATOMIC); /* kmalloc-4k */
copy_from_user(syscalls, ubuf, len);
syscalls[len] = '\x00'; /* NUL at [PAGE_SIZE] when len==4096 */
Off-by-null → arbitrary free → Docker escape chain
- kmalloc-4k'yı groom et ki sprayed bir
poll_listchunk'ı vulnerable buffer'ın hemen arkasına otursun; başıboş NUL o chunk'ınnextpointer'ının low byte'ını sıfırlar. - Bozulmuş
next,poll()teardown'unda dereference edilir ve attacker-controlled bir adresikfree()'ler — bir arbitrary free. - Seçilmiş bir victim object'i free et, kernel/heap pointer'larını leak etmek için onu
controlled bir object ile reclaim et (KASLR'ı yenerek), sonra bir
pipe_buffer/cred primitive'ine pivot et. - Namespace'leri değiştirerek (
switch_task_namespaces/ yeni credential'lar) container'dan escape et ki process seccomp+namespace jail'inden çıksın. Sadece bir takım çözdü; first blood, bug'ı kmalloc-192'deki birsimple_xattrlist.nextüzerine cross-cache bir NUL overflow olarak yeniden çerçeveledi.
Warning
poll_list, unprivileged caller'ın tamamen sürdüğü bir path üzerinde GFP_KERNEL ile
allocate edilir, dolayısıyla birçok cache boyutu genelinde security-relevant object'lerle
serbestçe co-locate olur — ama aynı esneklik, başarısız bir arbitrary-free'nin freelist'i
desync etmesi ve reclaim race kaybedilirse bir sonraki allocation'da panic atması anlamına
gelir.
Mitigation¶
Containment defense'leri (poll/ppoll'un seccomp ile allow-list'lenmesi yardımcı olmaz —
bunlar benign syscall'lardır) etkisizdir; ilgili hardening, diğer
elastic-objects için olanla aynıdır: freelist randomization/hardening,
cache isolation (kmalloc-cg/usercopy separation) ve köken aldığı heap overflow'u düzeltmek.
CoRJail kaynak bug sınıfı klasik bir slab-oob-write-via-mount-option-parsing-tarzı
off-by-one'dır (CVE-2022-0185 dönemi), dolayısıyla allocation-size validation onu kökünden kapatır.