Skip to content

Named pipe (PipeQueueEntry) non-paged pool grooming

Bir named pipe'a okunmamış data yazmak, NPFS.SYS'in non-paged pool'da attacker-sized, attacker-filled bir DATA_ENTRY allocate etmesine yol açar — Windows kernel pool belleğini spray'lemenin ve hole-punch'lamanın kanonik primitive'i.

Mechanism

Note

Bir process bir named pipe'a yazdığında ve diğer ucu hiçbir şey okumadığında, Named Pipe File System (NPFS.SYS) o data'yı kernel içinde buffer'lamak zorundadır. Inline threshold'u (kabaca bir page) aşan write'lar için NPFS, bekleyen byte'ları tutmak üzere non-paged pool'dan özel bir record — tarihsel olarak DATA_ENTRY / DATA_QUEUE_ENTRY ("PipeQueueEntry") denen — allocate eder.

Bu, attacker'a named pipe'ları Windows kernel pool'larında ilk başvurulan grooming primitive'i yapan üç özellik verir:

  • Kontrollü size. Toplam pool allocation'ı header + payload'dur. NPFS, buffer'ın başına kendi DATA_ENTRY header'ını ekler (header size'ı Windows build'leri arasında biraz değişir), yani write uzunluğunu seçerek allocation'ınızın hangi pool block / segment'e düştüğünü seçersiniz. 64-bit Windows'ta pool header 0x10'dur ve writeup'lar kesin bir bucket'a (örneğin bir 0x70 block'a) isabet etmek için yaygın olarak payload = target_alloc_size - header_overhead biçiminde bir ilişki kullanır.
  • Kontrollü content. DATA_ENTRY header'ından sonraki her şey WriteFile'a geçirdiğiniz ham byte'lardır — yani free-then-reclaimed object'in gövdesi tamamen attacker-controlled'dır. (Header'ın kendisi değil.)
  • Kontrollü lifetime. Allocation, data okunana ya da pipe handle kapanana kadar yaşar. ReadFile entry'yi drain edip free eder; CloseHandle onu yıkar. O on-demand free, sprayed bir bölgede "delik açmanıza" ve bir victim object'i tam istediğiniz yere yerleştirmenize izin verir.

Alex Ionescu bunu kamuya kernel heap feng shui olarak formalize etti ("Sheep Year Kernel Heap Fengshui: Spraying in the Big Kids' Pool"): named pipe'lar non-paged pool'u arbitrary-size, arbitrary-content chunk'larla spray'lemenize ve sonra onları seçici olarak free etmenize izin verir, bir overflow ya da use-after-free tetiklenmeden önce allocator'ı deterministik biçimde şekillendirir.

Walkthrough

Pattern şudur: spray → delik aç → bug'ı bir deliğe tetikle.

  1. Spray: birçok pipe oluşturun ve her birine okunmamış, boyutlandırılmış bir buffer yazın. Write uzunluğu pool bucket'ını seçer.
// Choose payload so total alloc lands in the target pool bucket.
// e.g. target 0x70-byte block, subtract NPFS DATA_ENTRY + pool header overhead.
#define BUFSIZE 0x28           // tune per Windows build / target bucket
BYTE payload[BUFSIZE];
memset(payload, 0x41, sizeof(payload));

HANDLE rd[N], wr[N];
for (int i = 0; i < N; i++) {
    CreatePipe(&rd[i], &wr[i], NULL, sizeof(payload));   // anonymous pipe = NPFS
    DWORD done;
    WriteFile(wr[i], payload, sizeof(payload), &done, NULL);  // allocates DATA_ENTRY
}

Özellikle bir named pipe için CreateNamedPipe / CreateFile(\\.\pipe\...) kullanırsınız; grooming için anonymous pipe'lar (CreatePipe, bunlar da NPFS tarafından desteklenir) daha basit ve eşdeğerdir.

  1. Delik aç: her ikincil allocation'ı free edin, tam olarak bucket size'ında free block'lardan oluşan bir dama tahtası bırakın. O size'daki bir sonraki victim allocation, free edilmiş slot'lardan birine düşer.
for (int i = 0; i < N; i += 2) {
    CloseHandle(wr[i]);   // frees the DATA_ENTRY for pipe i
    CloseHandle(rd[i]);
}
// Now: [USED][free][USED][free]... at the target bucket granularity.
  1. Trigger: vulnerable driver path'ini çağırın, böylece bug'ın object'i free edilmiş bir slot'a, kontrol ettiğiniz hâlâ sprayed bir pipe buffer'ına komşu (ya da onunla overlapping) biçimde allocate edilsin. Bir overflow için victim'i kontrollü bir pipe buffer'ının önüne yerleştirirsiniz; bir UAF için free edilmiş victim slot'unu kontrollü bir pipe write ile reclaim edersiniz.
Spray'i geri okuma / doğrulama

Spray, NPFS pool tag'i sayesinde bir kernel debugger'da gözlemlenebilir. NPFS data entry'leri tag'lenir (örneğin build'e bağlı olarak NpFr / pipe ile ilgili tag'ler), yani spray'ledikten sonra yerleşimi !poolfind/!poolused ile doğrulayabilir ve bir chunk'ı inceleyerek DATA_ENTRY header'ından hemen sonra 0x41 filler'ınızı görebilirsiniz:

kd> !poolfind NpFr 0          // locate non-paged pool data-entry chunks
kd> dd <chunk+headerlen>      // your attacker bytes (0x41414141...)

Write handle'ı kapatıp yeniden kontrol etmek, chunk'ın free list'e geri döndüğünü gösterir — victim object ile reclaim edeceğiniz delik.

Detection

  • Teknik bir building block'tur, bir vulnerability değil, dolayısıyla detection anormal hacime odaklanır: tek bir low-privileged process'in binlerce pipe oluşturup okunmamış, aynı boyutta buffer'lar yazması güçlü bir grooming sinyalidir. ETW (Microsoft-Windows-Kernel-File / NPFS create + write event'leri) ve handle-count telemetrisi bunu ortaya çıkarır.
  • Bir driver crash'ettiği sıralarda (pool corruption bugcheck 0x19/0xC2) NPFS pool tag'lerine atfedilebilen non-paged pool kullanımındaki ani büyüme, bir pool-overflow exploit'inin pipe'larla groom edildiğini düşündürür.

Mitigation

  • Savunma çoğunlukla pipe'ları bloklamak yerine exploit-mitigation'dır (pipe'lar çekirdek bir IPC primitive'idir). Modern Windows kernel pool hardening determinism'i azaltır: pool allocation'ları randomize edilir, segment heap / kLFH per-bucket randomize edilmiş free list'ler ekler ve NX non-paged pool eski executable spray target'ını ortadan kaldırır.
  • Driver yazarları size'ları valide etmeli ve güvenli allocation pattern'leri kullanmalıdır, böylece komşu kontrollü bir pipe buffer'ına bir overflow ile ulaşılamasın.
  • Bu groom'ların etkinleştirdiği overflow'u shipping öncesi yakalamak için test'te kernel pool corruption detection'ı (verifier / special pool) enable edin.

References