Skip to content

poll_list object spray (CoRJail)

poll()/ppoll()'un slow path'inde allocate edilen değişken boyutlu struct poll_list'i sprayable bir elastic object olarak kötüye kullan — boyutu kontrol edilebilir, lifetime'ı kontrol edilebilir ve next pointer'ı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
  1. kmalloc-4k'yı groom et ki sprayed bir poll_list chunk'ı vulnerable buffer'ın hemen arkasına otursun; başıboş NUL o chunk'ın next pointer'ının low byte'ını sıfırlar.
  2. Bozulmuş next, poll() teardown'unda dereference edilir ve attacker-controlled bir adresi kfree()'ler — bir arbitrary free.
  3. 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.
  4. 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 bir simple_xattr list.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.

References