n_hdlc TTY race-condition LPE (CVE-2017-2636)¶
n_hdlcTTY line discipline'inde, herhangi bir unprivileged kullanıcınınTIOCSETDile ulaşabildiği bir double-free; bir SMEP bypass ile kernel code execution'a dönüştürüldü.
Mechanism¶
Note
drivers/tty/n_hdlc.c tek bir "carry-over" pointer'ı, n_hdlc.tbuf'ı tutar;
bu, alttaki TTY'ye write'ı başarısız olan son transmit buffer'ı
(struct n_hdlc_buf) saklar. Amaç şu: bir sonraki n_hdlc_send_frames()
çağrısında, tx_buf_list'ten yeni frame'ler çekilmeden önce tbuf yeniden
gönderilsin.
Bug, o tek pointer'a synchronize edilmeyen erişimdir. İki code path ona eşzamanlı olarak dokunur:
n_hdlc_send_frames()— TTY write/wakeup path'inden çalışır; başarısız bir send'de buffer'ıtbuf'a kaydeder, bir sonraki geçişte isetbufbuffer'ını tekrartx_free_buf_list'e koyar.flush_tx_queue()—TCFLSH/flush'tan çalışır; o da queue'yu dolaşır ve aynı buffer'ıtx_free_buf_list'e taşıyabilir.
Hiçbir path tbuf'ı diğerine karşı koruyan bir lock tutmadığı için, tbuf'ın
işaret ettiği buffer tx_free_buf_list'e iki kez linklenebilir. Bu kopya,
n_hdlc_release() close anında free list'i dolaşıp her entry'yi free ettiğinde
klasik bir double-free olarak ortaya çıkar.
Buffer'lar büyüktür: struct n_hdlc_buf, kmalloc-8192 slab'ından allocate
edilir; bu da free edilen object'i aynı cache class'ından ikinci bir
attacker-controlled object ile reclaim etmeyi kolaylaştırır. 8 KiB'lık bir
chunk'ın double-free'i, attacker'a tek bir slab object'e iki reference verir —
exploit'in kontrollü bir overlap'e ve nihayetinde hijack edilmiş bir function
pointer'a çevirdiği primitive budur.
Kritik olarak, line discipline otomatik yüklenir: /dev/ptmx açıp
ioctl(fd, TIOCSETD, &N_HDLC) çağırmak, bir unprivileged kullanıcının
n_hdlc modülünü çekmesi için yeterlidir (CONFIG_N_HDLC=m
RHEL/Fedora/SUSE/Debian/Ubuntu üzerinde), yani özel bir donanım ya da
capability gerekmez.
Walkthrough¶
PoC, race'i bir pthread_barrier üzerinde senkronize edilmiş iki thread'den
sürer, ardından object'i free edip sprayed bir payload ile reclaim eder.
- Bir pseudoterminal master açın ve HDLC line discipline'ını set edin:
int ldisc = N_HDLC; /* 13 */
int ptmd = open("/dev/ptmx", O_RDWR);
ioctl(ptmd, TIOCSETD, &ldisc); /* auto-loads n_hdlc.ko */
- Bir buffer'ın
n_hdlc.tbuf'a düşmesi için başarısız bir transmit zorlayın. PoC, output'u suspend eder, bir frame yazar (drain olamaz), sonra bir flush'ı resume'a karşı race'e sokar:
ioctl(ptmd, TCXONC, TCOOFF); /* suspend output -> send will fail */
write(ptmd, buf, len); /* buffer parked in tbuf */
/* thread A */ ioctl(ptmd, TCFLSH, TCIOFLUSH); /* flush_tx_queue() */
/* thread B */ ioctl(ptmd, TCXONC, TCOON); /* resume -> send_frames */
İkisi de aynı anda çalıştığında, flush_tx_queue() ve n_hdlc_send_frames()
ikisi de tbuf buffer'ını tx_free_buf_list'e linkler.
- Master'ı kapatarak double-free'i tetikleyin; bu, artık bozuk olan free list
üzerinde
n_hdlc_release()'i çalıştırır:
- Free edilen 8 KiB'lık chunk'ı reclaim edin. Yayınlanan exploit
sk_buffdata spray'ler: yerel bir socket'e ~7500 byte'lık UDP datagram'ları gönderir, böylece (callbackfunction pointer'ı taşıyanubuf_infoile gelen)skb_shared_infokmalloc-8192'de allocate edilir; ayrıca tamamen attacker-controlled 8 KiB payload'ları aynı cache'e yerleştirmek içinadd_key()kullanır. Free-list sıralamasına bakıldığında, "12, 13, 14 ve 15 numaralı packet'lar muhtemelen exploit edilebilir."
ubuf_info callback üzerinden SMEP bypass
Tam bir ROP chain kurmak yerine exploit, skb_shared_info'yu öyle overwrite
eder ki ubuf_info.callback, kernel symbol'ü native_write_cr4()'e işaret
eder. skb release edildiğinde skb_release_data() callback'i çağırır ve ilk
argüman olarak kontrollü bir değer geçirir — yani SMEP bit'i temizlenmiş
halde native_write_cr4(value) çağrısı yapar. SMEP devre dışıyken bir
sonraki iteration, kontrolü commit_creds(prepare_kernel_cred(0)) çalıştıran
bir userspace payload'a yönlendirir. Race iki kez kazanılmalıdır: bir kez
SMEP'i devre dışı bırakmak için, bir kez de userland payload'u execute etmek
için.
Beklenen sonuç: bir unprivileged process bir root shell açar:
$ ./pwn
[+] Setting N_HDLC line discipline...
[+] Racing flush vs send_frames...
[+] Double free triggered, reclaiming chunk...
[+] Hijacked ubuf_info.callback -> native_write_cr4 (SMEP off)
[+] commit_creds(prepare_kernel_cred(0))
# id
uid=0(root) gid=0(root)
Detection¶
- Bir pseudoterminal üzerinde non-serial bir workload tarafından
TIOCSETDile yapılan herhangi birN_HDLC(line discipline 13) set işlemini şüpheli sayın — çoğu fleet HDLC'yi hiç kullanmaz.TIOCSETD'liioctl'leri audit edin (örneğin auditd veya bir LSM/eBPF hook üzerinden). - Unprivileged process'ler tarafından
n_hdlc.ko'nun on-demand yüklenmesine dikkat edin; beklenmedik bir ldisc isteğinden gelen modül auto-load güçlü bir sinyaldir. n_hdlc_releaseçevresinde slab double-free / list corruption splat'leri gösteren kernel log'ları, patch'lenmemiş kernel'lerde exploitation girişimlerine işaret eder.
Mitigation¶
- Upstream fix'i uygulayın: racy
n_hdlc.tbufpointer'ını tamamen kaldırır ve bir spinlock ile korunan standart bir kernel linked list kullanır, böylece hatalı buffer double-link edilmek yerine güvenli biçimde yeniden queue'lanır. - HDLC kullanılmayan yerlerde modülü blacklist'leyin:
modprobe.diçindeinstall n_hdlc /bin/true, auto-load attack surface'ini bloklar. - Unprivileged ldisc yüklemelerini durdurmak için
kernel.modules_disabled=1set edin (veya sonradan eklenendev.tty.ldisc_autoload=0ile ldisc auto-loading'i kısıtlayın). - Exploit chain'i körelten genel hardening: SMEP/SMAP'i enable tutun, hardened
usercopy ve
slab_nomerge'i enable edin,add_keyquota'sını kısıtlayın.