Skip to content

mq_notify() netlink socket double sock_put UAF (CVE-2017-11176)

Known alias: "ALSA sequencer UAF" — bu, bug'ın dolaştığı bir takma addır; gerçek affected path sys_mq_notify() → netlink socket refcount'tur.

mq_notify(), retry atlamasından önce sock pointer'ını NULL yapmayı unutuyor; bu yüzden race halindeki bir close(), exit path'in netlink socket üzerinde ikinci kez sock_put() çağırmasına yol açıyor — refcount kaynaklı bir use-after-free, arbitrary call'a ve root'a dönüşüyor.

Mechanism

Eksik bir sock = NULL neden çift sock_put() UAF'ına yol açar

sys_mq_notify(), bir POSIX message-queue bildirimini bir netlink socket'ine bağlar. İlgili iskelet şu:

sock = NULL;
retry:
    filp = fget(notification.sigev_signo);
    if (!filp) { ret = -EBADF; goto out; }
    sock = netlink_getsockbyfilp(filp);   // takes a ref on the sock
    fput(filp);
    if (IS_ERR(sock)) { sock = NULL; goto out; }

    timeo = MAX_SCHEDULE_TIMEOUT;
    ret = netlink_attachskb(sock, nc, &timeo, NULL);
    if (ret == 1)
        goto retry;                       // BUG: sock not reset to NULL
    ...
out:
    if (sock)
        netlink_detachskb(sock, nc);      // sock_put() again

Tuzak, netlink_attachskb()'in contract'ında: hedef socket'in receive buffer'ı doluyken kendisine verilen reference'ı bırakır ve caller'a retry yapmasını söylemek için 1 döndürür:

// netlink_attachskb(): receive queue congested
sock_put(sk);   // ref released
return 1;       // "please retry"

Yani ret == 1 durumunda, netlink_getsockbyfilp()'in aldığı reference zaten bırakılmıştır. Doğru retry, onu sıfırdan yeniden almalıdır — sock reset edilmiş olsaydı zaten alırdı. Ama kod, sock hâlâ (artık under-referenced olan) socket'i gösterirken retry'a atlıyor. Eğer ikinci bir thread, race penceresinde netlink fd'sini close()'larsa, retry'ın fget()'i NULL döner ve kod, stale sock non-NULL hâldeyken out:'a düşer; böylece netlink_detachskb(), refcount'un kaldırabileceğinden bir fazla sock_put() çalıştırır.

Kırılan invariant şu: reference'ı geri teslim edilmiş bir pointer, herhangi bir path onu yeniden release edebilmeden önce temizlenmelidir. Fazladan gelen sock_put(), sk_refcnt'i zamanından önce sıfıra düşürür ve ona işaret eden bir dangling pointer hâlâ dururken struct netlink_sock'u serbest bırakır — bir refcount imbalance'tan doğan ders kitabı niteliğinde bir use-after-free. Yoldaş KB notu mq-notify-netlink-sock-double-sock-put-uaf aynı bug'ı yeniden kullanılabilir bir primitive olarak kataloglar. (CVE-2017-11176, netlink/mqueue path'ini etkiler; "ALSA sequencer", bug'ın dolaştığı bir alias'tır.)

Walkthrough

Lexfo'nun dört bölümlük serisi, bir SLAB kernel üzerinde tam bir PoC → root exploit'i inşa eder. Aşamalar:

Adım 1 — double free'yi deterministik biçimde tetikle. İki thread race eder: biri mq_notify()congested bir netlink socket üzerinde döngüde tutar (böylece netlink_attachskb() 1 döner), diğeri ise pencerede fd'yi close()'lar; böylece retry'ın fget()'i başarısız olur ve exit path double-sock_put() yapar:

// thread A: keep the recv queue full so attachskb returns 1, then loop in retry
mq_notify(mqd, &sev_netlink_congested);
// thread B (during the retry window):
close(netlink_fd);     // next fget() -> NULL, out: runs netlink_detachskb()
// -> sk_refcnt hits 0 early; struct netlink_sock is freed but still referenced

Step 2 — reallocate the freed netlink_sock with controlled bytes. The freed object lives in a kmalloc slab; spray a same-sized, user-controlled object to land attacker data over it (the writeup drives the netlink hashtable reallocation / wait-queue path; a generic spray such as sendmsg ancillary data or msg_msg fills the same role):

// occupy the just-freed slab object with attacker-controlled content
spray_same_size_object(fake_netlink_sock_bytes);

Step 3 — turn the UAF into an arbitrary call. The reallocated object's embedded wait queue is the lever: a wait queue entry has a func function pointer invoked by __wake_up_common(). Driving a wake-up on the corrupted socket calls attacker-controlled func:

netlink_setsockopt(...) -> wake_up -> __wake_up_common() walks wait queue
-> entry->func()  with attacker-controlled pointer  ==  arbitrary call

The PoC observed the dispatch as call QWORD PTR [rax+0x10], so the fake object is laid out to control [rax+0x10].

Step 4 — pivot and escalate. The arbitrary call lands on a stack-pivot gadget (xchg eax, esp class), runs a short ROP chain that clears CR4.SMEP (bit 20), then returns to userland code that calls the payload:

commit_creds(prepare_kernel_cred(NULL));   // payload: become root

See commit-creds for this final credential-overwrite step.

Step 5 — recover. The exploit repairs the dangling references / pointers it corrupted so the kernel survives, then drops a root shell:

# id
uid=0(root) gid=0(root) ...
The patch is one line

The upstream fix simply clears the pointer before retrying, restoring the invariant:

if (ret == 1) {
    sock = NULL;    // fix: re-acquire from scratch on retry
    goto retry;
}

With sock NULL'd, the only sock_put() that runs at out: corresponds to a reference the function still legitimately holds, so refcounting stays balanced and the object is never freed early.

Detection

  • KASAN on a debug kernel flags the use-after-free on struct netlink_sock the moment the second sock_put() / dangling use occurs.
  • refcount_t hardening: with CONFIG_REFCOUNT_FULL, an underflow on the socket refcount is detected/saturated, neutralizing the double-put.
  • Anomalous mq_notify() + rapid close() racing on netlink fds from one process is an exploit-shaped pattern.

Mitigation

  • Patch to a kernel ≥ 4.11.9 fix (the one-line sock = NULL commit).
  • Build with CONFIG_REFCOUNT_FULL / hardened refcount_t to convert the refcount underflow into a safe saturation instead of a free.
  • Generic UAF hardening (SLAB freelist randomization, hardened usercopy, init_on_free) raises the cost of the reallocation/spray steps.

References