Skip to content

n_hdlc TTY race-condition LPE (CVE-2017-2636)

n_hdlc TTY line discipline'inde, herhangi bir unprivileged kullanıcının TIOCSETD ile 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 ise tbuf buffer'ını tekrar tx_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.

  1. 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 */
  1. 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.

  1. Master'ı kapatarak double-free'i tetikleyin; bu, artık bozuk olan free list üzerinde n_hdlc_release()'i çalıştırır:
close(ptmd);                        /* n_hdlc_release() double-frees */
  1. Free edilen 8 KiB'lık chunk'ı reclaim edin. Yayınlanan exploit sk_buff data spray'ler: yerel bir socket'e ~7500 byte'lık UDP datagram'ları gönderir, böylece (callback function pointer'ı taşıyan ubuf_info ile gelen) skb_shared_info kmalloc-8192'de allocate edilir; ayrıca tamamen attacker-controlled 8 KiB payload'ları aynı cache'e yerleştirmek için add_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 TIOCSETD ile yapılan herhangi bir N_HDLC (line discipline 13) set işlemini şüpheli sayın — çoğu fleet HDLC'yi hiç kullanmaz. TIOCSETD'li ioctl'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.tbuf pointer'ı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.d içinde install n_hdlc /bin/true, auto-load attack surface'ini bloklar.
  • Unprivileged ldisc yüklemelerini durdurmak için kernel.modules_disabled=1 set edin (veya sonradan eklenen dev.tty.ldisc_autoload=0 ile 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_key quota'sını kısıtlayın.

References