Skip to content

NTFS EA integer underflow + WNF kernel exploit (CVE-2021-31956)

NtfsQueryEaUserEaList içindeki unsigned-length underflow paged pool'u overflow eder ve bu overflow, _WNF_NAME_INSTANCE / _WNF_STATE_DATA objelerini groom ederek arbitrary kernel read/write'a dönüştürülür.

Mechanism

Underflow neden oluşur

NTFS extended attributes (EAs), bir dosyada saklanan key/value çiftleridir. Query yolu NtQueryEaFile -> NtfsCommonQueryEa -> NtfsQueryEaUserEaList, caller'ın verdiği bir EA list'i dolaşır ve eşleşen her EA'yı bir output buffer'a kopyalayarak entry'leri 4-byte boundary üzerinde paketler.

Her FILE_FULL_EA_INFORMATION entry'si NextEntryOffset, EaNameLength ve EaValueLength taşır. Loop entry'leri yazdıkça output buffer'da kalan yeri takip eder ve bir sonraki entry'nin 32-bit aligned başlaması için bir alignment padding uygular:

padding = ((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size

Remaining-length check'in şekli ea_block_size <= out_buf_length - padding'dir ve unsigned bir değer üzerinde değerlendirilir. Buffer ilk EA tarafından tam olarak sıfıra kadar doldurulduğunda, ikinci bir EA işlenirken out_buf_length - padding, out_buf_length == 0 ve padding sıfırdan farklı iken hesaplanır. Çıkarma unsigned olduğu için 0 - 2 negatif olmaz — 0xFFFFFFFF'e yakın bir değere wrap eder. Bound check bu durumda başarısız olması gerekirken geçer ve ikinci EA'nın byte'ları allocation'ın sonunun ötesine yazılır.

Output buffer paged pool'da yaşar; attacker'ın etkilediği bir size ile ExAllocatePoolWithQuotaTag üzerinden allocate edilir. Sonuç, bir paged-pool chunk'ından bir sonrakine controlled-length lineer bir overflow'dur — klasik bir pool-overflow primitive'i. Exploit'in geri kalan işi pool feng shui'dir: overflow edilen buffer'ın hemen ardına kullanışlı bir victim object yerleştir ve tam olarak bir memory-access primitive'i veren field'ı corrupt et.

O victim, Windows Notification Facility (WNF)'dir. Bir _WNF_STATE_DATA object'i, NtQueryWnfStateData'nın kaç byte döndüreceğini tanımlayan bir length/AllocatedSize saklar ve bir _WNF_NAME_INSTANCE o data'ya bir pointer (StateData) taşır. Size'ı corrupt etmek bir query'yi out-of-bounds read'e çevirir; data pointer'ını corrupt etmek bir update'i arbitrary write'a çevirir. İkisini chain'lemek tek bir pool byte-overflow'undan temiz bir arbitrary kernel R/W verir — hiçbir zaman bir function pointer overwrite etmeden.

Walkthrough

Bug'a tamamen user mode'dan, dokümante edilmiş NT EA syscall'ları üzerinden ulaşılır.

  1. İki entry'li bir EA list oluştur ve onu bir dosyaya set et. İlk entry, kopyalandıktan sonra çalışan out_buf_length tam olarak sıfıra ulaşacak şekilde boyutlandırılır. İkinci entry overflow payload'ını taşır.
// two FILE_FULL_EA_INFORMATION entries chained via NextEntryOffset:
//   entry[0]: consumes the output buffer down to length 0
//   entry[1]: payload that overflows the next pool chunk
NtSetEaFile(hFile, &iosb, eaBuffer, eaBufferLen);
  1. Underflow'u tetiklemek için EA'ları query et. NtQueryEaFile'ı, length'i entry[0] ile eşleşen bir output buffer ile çağır. entry[1] üzerindeki padding çıkarması underflow yapar ve entry[1]'in value'su paged-pool allocation'ının ötesine, komşu chunk'a taşar.
NtQueryEaFile(hFile, &iosb, outBuf, entry0_size,
              TRUE /*ReturnSingleEntry*/, eaList, eaListLen,
              NULL, FALSE);
  1. Paged pool'u WNF state object'leriyle groom et. Çok sayıda WNF state name oluştur (NtCreateWnfStateName) ve data publish et (NtUpdateWnfStateData); böylece seçilen size'daki _WNF_STATE_DATA allocation'ları EA buffer'ına komşu otursun. Overflow'un controlled bir victim üzerine düşmesi için holes'ları free/realloc et.

  2. Overflow'u relative bir read'e çevir. Victim _WNF_STATE_DATA'yı, declare edilen size'ı gerçek allocation'ından büyük olacak şekilde corrupt et. NtQueryWnfStateData artık out-of-bounds byte'lar döndürür — komşu _WNF_NAME_INSTANCE pointer'larını açığa çıkaran ve pool/KASLR'ı kıran bir info leak.

  3. Name instance üzerinden arbitrary R/W'ya yükselt. Leak edilen bir _WNF_NAME_INSTANCE ile onun StateData'sını arbitrary bir kernel adresine forge edebilir/yönlendirebilirsin. NtQueryWnfStateData oradan okur; NtUpdateWnfStateData oraya yazar. Artık bir arbitrary read ve arbitrary write'ın var.

  4. Escalate et. R/W'yı mevcut process'in _EPROCESS'ini bulmak için kullan, ardından ya process'ine bir SYSTEM token kopyala ya da token'ın privilege/integrity field'larını null'la — bkz. access-token-theft-via-arbitrary-kernel-r-w.

Neden bir değil iki EA

Tek bir oversized EA ilk meşru length check'te başarısız olur, çünkü başlangıçtaki out_buf_length sıfırdan farklıdır ve karşılaştırma dürüsttür. Underflow yalnızca counter sıfıra sürüldükten sonra bir sonraki iteration'da ortaya çıkar, bu yüzden wrap'e ulaşmak için chained-EA layout'u gereklidir.

Detection

  • This bug was found exploited in the wild (discovered by Kaspersky) chained with a Chrome renderer 0-day for sandbox escape; EDR telemetry of a sandboxed browser child calling NtSetEaFile/NtQueryEaFile on its own temp files is anomalous.
  • Frequent NtCreate/NtUpdate/NtQueryWnfStateData bursts immediately after EA operations is a grooming signature.
  • Pool corruption may surface as BAD_POOL_HEADER/KERNEL_MODE_HEAP_CORRUPTION bugchecks on imperfect feng shui.

Mitigation

  • Apply the Microsoft June 2021 patch (MSRC CVE-2021-31956); the fixed NtfsQueryEaUserEaList performs the length math without the unsigned wrap.
  • Kernel pool hardening (segment heap / pool zeroing introduced in later Windows 10 builds) raises the cost of the WNF grooming step but does not fix the underlying underflow.

References