mq_notify netlink sock double sock_put UAF (CVE-2017-11176)¶
sys_mq_notify()retry path'inde eksik birsock = NULL, cleanup kodunun zaten serbest bırakılmış bir netlink socket üzerindesock_put()'ı ikinci kez çağırmasına yol açar; bu da refcount'u erkenden sıfıra düşürür ve geride reclaim edilip yeniden kullanılacak dangling birstruct sockbırakır.
Mechanism¶
mq_notify(2), bir process'in boş bir POSIX queue'ya mesaj geldiğinde (bir signal ya da bir netlink mesajı üzerinden) haberdar edilmek için kayıt olmasını sağlar. Notification bir netlink socket üzerinden teslim edildiğinde, syscall ona bir control skb attach ederken o socket'i pin'lemek zorundadır. Bug, error/retry path'indeki bir reference-count dengesizliğidir: bir reference iki kez drop edilir.
Note
Invariant her zamanki gibidir — her sock_hold() tam olarak bir sock_put() ile eşleşmeli ve reference'ı drop edilmiş bir pointer yeniden kullanılmamalıdır. Açık barındıran sys_mq_notify() şöyle görünür (Lexfo, 4.11.9'a kadarki kernel'ler):
retry:
filp = fget(notification.sigev_signo);
if (!filp) { ret = -EBADF; goto out; }
sock = netlink_getsockbyfilp(filp); /* does sock_hold(sock) */
fput(filp);
if (IS_ERR(sock)) { ret = PTR_ERR(sock); sock = NULL; goto out; }
ret = netlink_attachskb(sock, nc, &timeo, NULL);
if (ret == 1)
goto retry; /* BUG: sock still set, ref already dropped */
if (ret) { ... goto out; }
...
out:
...
if (sock)
netlink_detachskb(sock, nc); /* second sock_put() on a stale sock */
Reference muhasebesi:
netlink_getsockbyfilp(),sock_hold()ile bir reference alır.netlink_attachskb(), socket'in receive buffer'ı dolu olup sleep/wake yapmak zorunda kaldığında1döner — ve o return path'inde reference'ı zaten drop eder (sock_put()), caller'ın retry'da yeniden almasını bekler.- Açık barındıran kod,
sock'u temizlemedengoto retryyapar. Eğer ardındanfget()başarısız olursa (örneğin attacker iterasyonlar arasında fd'yi kapatırsa), kontrol artık reference'sız olan socket'i hâlâ gösterensockileout:'a atlar venetlink_detachskb()ikinci birsock_put()gerçekleştirir.
Bir sock_hold()'a karşılık iki sock_put(), refcount'u bir adım erken sıfıra düşürür; struct sock, meşru bir kullanıcı hâlâ onu tutarken free edilir. Sonuç, bir netlink struct sock üzerinde klasik bir use-after-free / dangling-pointer'dır (bu bir refcount-imbalance-uaf'tir). Resmî fix tek satırdır:
sock'u temizlemek, out:'taki if (sock) netlink_detachskb(...)'ı retry'da bir no-op hâline getirir ve 1:1 dengesini geri kurar.
Walkthrough¶
Bug'a tamamen unprivileged userspace'ten ulaşılır; zor kısım, netlink_attachskb()'yi 1 döndürmeye zorlayan ve sonra bir sonraki fget()'i başarısız kılan race'i kazanmaktır.
-
Bir netlink socket oluştur ve receive buffer'ını doldur ki
netlink_attachskb()block etsin ve0yerine retry değeri1'i döndürsün. Lexfo bunu, küçücük birSO_RCVBUFayarlayıp socket'i flood ederek yapar; böylece bir sonraki attach sleep etmek zorunda kalır. -
Notification'ı bir POSIX mqueue üzerine register et,
sigev_signo'yu netlink socket fd'sine yönlendir:
struct sigevent sigev = {0};
sigev.sigev_notify = SIGEV_THREAD; /* netlink-backed delivery */
sigev.sigev_signo = netlink_fd; /* fd of the full netlink sock */
/* ...sigev_value carries the netlink cookie... */
mq_notify(mqdes, &sigev); /* enters the vulnerable retry loop */
- İkinci bir thread'den, birincisi
netlink_attachskb()içinde park hâlindeyken (ki1dönecektir),netlink_fd'yi kapat kigoto retrysonrasındakifget()-EBADFile başarısız olsun. Bu, ilk thread'i stale birsockileout:'a gönderir ve ikincisock_put()'ı tetikler.
!!! warning
Bu sıkı bir race'tir. Lexfo bunu, scheduler'a duyarlı pencereyi single-step'leyerek (attach yapan thread'in ne zaman uyanacağını kontrol ederek) ve netlink buffer üzerinde bir blocking-then-unblock pattern kullanarak stabilize eder. Bu kontrol olmadan ikinci fget() genelde başarılı olur ve bug ateşlenmez. Trigger'ın kendisi değil, reliability işi exploit'in büyük kısmını oluşturur.
- Free edilmiş
struct sockartık dangling'dir. Aynıkmallocboyut ve şekline sahip kontrol edilebilir bir object ile slab slot'unu reclaim et (Lexfo socket'in cache'ini groom eder ve forged birsock/netlink_sockoverlay'ler), sonra dangling pointer üzerinden bir operation sürerek bir arbitrary-call primitive elde et ve oradan ring-0 code execution'a geç. Bu size class için pratik spray object'leri arasında mqueue mesajları yer alır — bkz. mqueue-object-spray ve setxattr-userfaultfd-universal-heap-spray.
??? example "Refcount timeline (one hold, two puts)"
netlink_getsockbyfilp() : sock_hold() refcount 1 -> 2
netlink_attachskb() == 1 : sock_put() refcount 2 -> 1 (buffer full, retry)
goto retry; fget() fails : (sock NOT cleared)
out: netlink_detachskb() : sock_put() refcount 1 -> 0 -> FREED
...later legitimate use : use-after-free on struct sock
Detection¶
- Fix, tek satırlık bir refcount düzeltmesidir; build/patch scanner'ları
ipc/mqueue.c:sys_mq_notifyiçindegoto retry'dan öncesock = NULL;varlığını anahtar olarak kullanabilir. - Runtime'da,
CONFIG_REFCOUNT_FULL/ saturatingrefcount_tdönüşümü, erken drop-to-zero'yu ve ardından gelen inc-from-zero'yu birWARN'a çevirir ve sessizce free etmek yerine dengesizliği yüzeye çıkarır. - KASAN, netlink
struct sockslab object'i üzerindeki use-after-free read/write'ı işaretler.
Mitigation¶
- Upstream fix'i uygula (
ret == 1retry path'inesock = NULL;ekleyen commit); bakımı yapılan tüm distro kernel'leri bunu 2017 ortasından beri taşır. - Generic hardening, ortaya çıkan UAF'ın exploitability'sini azaltır:
SLAB_FREELIST_RANDOM/HARDENED,init_on_freeve refcount saturation (refcount_t), dangling-sockyeniden kullanımını çok daha az deterministik kılar. - Sınıf fix'i, el yapımı
atomic_tsocket refcounting'i checkedrefcount_tile değiştirmektir; bu, off-by-one'ı bir free yerine tespit edilebilir bir saturation'a çevirir.