MS05-055 是一個經典的 Windows 內核漏洞,EEYE 的公告已對其有詳細描述。本文將從源碼層面深入剖析其成因與利用過程。
當線程退出時,PspExitThread 會從 ETHREAD.ApcState.ApcListHead[0] 和 ApcListHead[1] 分離線程的 APC 隊列,使每個隊列成為一個被摘除鏈表頭的循環雙向鏈表。若某 APC 從配額池分配,則佔用分配進程內核對象一個引用。在處理配額池中的 APC 時,若分配進程已終止且該引用是進程對象的最後一個引用,PspExitThread 在調用 ExFreePool 釋放 APC 過程中會進一步調用 PspProcessDelete 銷毀進程對象。
漏洞關鍵在於銷毀進程對象時會調用 KeStackAttachProcess 和 KeUnstackDetachProcess,這兩個函數均會調用 KiMoveApcState 分別保存和恢復 APC 鏈表。問題出在第二次調用 KiMoveApcState 時,將先前已被摘除的鏈表頭重新接回 APC 雙向鏈表,導致後續處理 APC 隊列時發生 ExFreePool(ETHREAD+0x30),其中 ETHREAD 為正在退出的線程內核對象。
PspExitThread
KeFlushQueueApc
(分離 APC 隊列)
(APC 釋放循環開始)
ExFreePool(1st_APC -- 由已退出進程排隊)
ExFreePoolWithTag(1st_APC)
ObfDereferenceObject(exited_process)
ObpRemoveObjectRoutine
PspProcessDelete
KeStackAttachProcess(exited_process)
KiAttachProcess
KiMoveApcState(ETHREAD.ApcState --> duplicate)
KiSwapProcess
PspExitProcess(0)
KeUnstackDetachProcess
KiMoveApcState(duplicate --> ETHREAD.ApcState)
KiSwapProcess
ExFreePool(2nd_APC)
在 KeFlushQueueApc 中,摘除鏈表頭的代碼:
RemoveEntryList(&Thread->ApcState.ApcListHead[ApcMode]);
NextEntry = FirstEntry;
#define RemoveEntryList(Entry) {
PLIST_ENTRY _EX_Blink;
PLIST_ENTRY _EX_Flink;
_EX_Flink = (Entry)->Flink;
_EX_Blink = (Entry)->Blink;
_EX_Blink->Flink = _EX_Flink;
_EX_Flink->Blink = _EX_Blink;
}
這將以 ApcListHead 為頭的雙向鏈表進行摘除操作,原鏈表頭指向第一項。
問題代碼在第二個 KiMoveApcState,恢復 APC 狀態時:
First = Source->ApcListHead[KernelMode].Flink; Last = Source->ApcListHead[KernelMode].Blink; Destination->ApcListHead[KernelMode].Flink = First; Destination->ApcListHead[KernelMode].Blink = Last; First->Blink = &Destination->ApcListHead[KernelMode]; Last->Flink = &Destination->ApcListHead[KernelMode];
這兩句將鏈表頭重新接回雙向鏈表,導致後續循環釋放 APC 結構時,循環到了 ETHREAD+0x3c(UserMode 的 APC),將其誤認為 KAPC+0xc (LIST_ENTRY) 處理,調用 ExFreePool(Apc) 時,該 "Apc" 地址實為 ETHREAD+0x30,實際池頭結構從 ETHREAD+0x28 KernelStack 開始。
當 KernelStack 可作為合法頭結構時,會將頭結構中的 ProcessBilled 視為 EPROCESS 進行釋放,此時該 "EPROCESS" 為用戶態地址,通常為 0x200(因 State=2)。因該地址處於地址空間第一頁,不可訪問,故需使 ETHREAD 中 State 之後的成員不為 0,即 Alerted 數組中某項不為 0。Alerted 分別記錄線程在用戶態和內核態是否被提醒,各佔一字節。在用戶態下置內核態 Alerted 為 1 較難,但可置用戶態 Alerted 為 1。掛起目標線程後,在其被插入 APC 前調用 ZwAlertThread 觸發 KeAlertThread,可使 Alerted[UserMode]=1(前提:線程非 Alertable 狀態)。此時偽造的 EPROCESS 地址為 0x1000200。
KeAlertThread 中置位 Alerted 的邏輯:
if (Alerted == FALSE) {
if ((Thread->State == Waiting) && (Thread->Alertable == TRUE) &&
(AlertMode <= Thread->WaitMode)) {
KiUnwaitThread(Thread, STATUS_ALERTED, ALERT_INCREMENT);
} else {
Thread->Alerted[AlertMode] = TRUE;
}
}
在該用戶態地址 0x1000200 映射偽造數據,後續 ObfDereferenceObject 處理中會減少 EPROCESS 對象頭引用,引用歸零時銷毀結構,並調用對象頭(OBJECT_HEADER)中對象類型(OBJECT_TYPE) 的函數指針,從而獲取控制權。