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(),retryatlamasından öncesockpointer'ını NULL yapmayı unutuyor; bu yüzden race halindeki birclose(), exit path'in netlink socket üzerinde ikinci kezsock_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:
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:
The patch is one line
The upstream fix simply clears the pointer before retrying, restoring the invariant:
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_sockthe moment the secondsock_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()+ rapidclose()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 = NULLcommit). - Build with
CONFIG_REFCOUNT_FULL/ hardenedrefcount_tto 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¶
- Lexfo — CVE-2017-11176: A step-by-step Linux Kernel exploitation (part 1/4)
- Lexfo — CVE-2017-11176: A step-by-step Linux Kernel exploitation (part 2/4)
- Lexfo — CVE-2017-11176: A step-by-step Linux Kernel exploitation (part 3/4)
- Lexfo — CVE-2017-11176: A step-by-step Linux Kernel exploitation (part 4/4)
- Red Hat Bugzilla 1470659 — CVE-2017-11176 UAF in sys_mq_notify()
- CVE Details — CVE-2017-11176