Skip to content

Double-fetch / user-kernel TOCTOU

The kernel reads the same userspace address twice — once to validate a size, once to use it — and a racing user thread changes the value in between, turning a passed check into an overflow.

Mechanism

Note

Bir double-fetch, kernel tarafında bir time-of-check-to-time-of-use race'tir. Kernel kodu bir userspace adresinden bir değeri (bir length/size/count) get_user / copy_from_user ile okur, doğrular, sonra aynı adresi ikinci kez okur ve o değeri kullanır — değişmediğini varsayarak. İki fetch arasında değeri mutate eden, yarışan bir user thread, doğrulanmış invariant'ı ihlal eder. Bochspwn'un tanımı: kernel kodunun user-mode belleği birden çok kez okuduğu ve erişimler arasında değerlerin sabit kaldığını varsaydığı race condition'lar.

Klasik escalation: fetch #1 count'u okur, bir bounds kontrolünü geçer ve bir kmalloc'u boyutlandırır; fetch #2 artık daha büyük olan count'u tekrar okur ve o kadar byte kopyalar -> kontrollü data ile heap overflow. Attacker struct'ı paylaşılan/mmap'lenmiş belleğe yerleştirir ve değeri "küçük/geçerli" ile "büyük/kötü" arasında flip'leyen ikinci bir thread çalıştırır; page üzerinde register edilen userfaultfd, ikinci fetch'i fault'latır ve kernel'i deterministik olarak stall'a sokar; böylece o devam etmeden önce değer flip'lenebilir.

Walkthrough

Zafiyetli handler kalıbı:

long vuln_ioctl(struct foo __user *arg) {
    u16 len;
    get_user(len, &arg->len);            /* FETCH #1: get size  */
    if (len > MAX) return -EINVAL;        /* validate fetch #1   */
    buf = kmalloc(len, GFP_KERNEL);       /* size from fetch #1  */

    get_user(len, &arg->len);             /* FETCH #2: re-read!  */
    copy_from_user(buf, arg->data, len);  /* uses fetch #2 size  */
    /* attacker enlarged len between fetches -> heap overflow */
}

Somut gerçek bir bug, ioctl_file_dedupe_range() (fs/ioctl.c) içindeki CVE-2016-6516'dır; FIDEDUPERANGE ioctl'i üzerinden erişilir: dest_count alanı iki kez fetch edilir, ilk fetch allocation'ı boyutlandırır ve ikincisi kopyayı sürer; bu da küçük boyutlu bir allocation ve heap overflow verir. Yayımlanan exploit'in flipping thread'i:

void *flipping_thread(void *addr) {
    struct file_dedupe_range *range = addr;
    while (!finish) {
        flipping_ready = 1;
        while (!trigger_ready) {}
        usleep(interval);
        range->dest_count = evil_value;   /* flip count between fetches */
        interval++;
    }
}
/* #define FIDEDUPERANGE _IOWR(0x94, 54, struct file_dedupe_range)
   ret = ioctl(fd1, FIDEDUPERANGE, range);  build: gcc exploit.c -lpthread */

userfaultfd, timing-tabanlı usleep/flip döngüsünün yerini alarak kernel'i ikinci fetch'te deterministik biçimde stall'a sokabilir.

Detection

Static pattern matcher'lar bunları bulur: Wang ve ark. Linux kernel'indeki double-fetch durumlarını işaretlemek için Coccinelle kullandı; Bochspwn (Jurczyk/Coldwind) ve Bochspwn Reloaded tekrarlanan user-memory okumalarını yakalamak için tüm sistemi taklit eden bir emülatörü enstrümante eder; DECAF (Schwarz ve ark.) double fetch'leri runtime'da tespit etmek için Intel TSX kullanır.

Mitigation

User değerini bir kez bir kernel local'e oku, o local'i doğrula ve yalnızca local'i kullan — ya da tüm struct'ı bir kez copy_from_user() edip kernel kopyası üzerinde çalış. Bu single-read prensibi, CVE-2016-6516 upstream fix'inin de biçimidir. Aynı Schwarz ve ark. makalesi (aşağıdaki DECAF / DropIt referansı; "Automated Detection, Exploitation, and Elimination of Double-Fetch Bugs using Modern CPU Features") ayrıca DropIt adlı bir runtime savunma önerir: hardware transactional memory (Intel TSX, XBEGIN/XEND) kullanarak iki fetch arasındaki concurrent modification'ı algılar ve son tutarlı state'e geri döner; böylece double-fetch'i exploit edilemez sıradan bir double-fetch'e indirir.

References