現代內核級反作弊系統,毫不誇張地說,是運行在消費者 Windows 機器上最複雜的軟體之一。它們在軟體可用的最高權限層級運行,攔截原本為合法安全產品設計的內核回調,掃描大多數程式設計師在其整個職業生涯中都不會觸及的記憶體結構,並且在遊戲運行時以透明的方式完成這一切。如果你曾想知道 BattlEye 是如何抓到作弊的,或者 Vanguard 為何堅持在 Windows 啟動前載入,或者 PCIe DMA 設備繞過所有這些保護意味著什麼,那麼這篇文章就是為你準備的。

這不是一個全面或權威的參考資料。這只是我記錄我所發現的並試圖清楚地解釋它。其中一些內容來自文末連結的公開研究和論文,一些來自閱讀內核原始碼和自行逆向工程驅動程式。如果內容有誤,請隨時聯繫。文章假設讀者對 Windows 內部機制和低階程式設計有一定的熟悉度,但我已盡力在使用每個概念前進行解釋。

僅限使用者模式的反作弊系統面臨的根本問題是信任模型。使用者模式進程運行在 ring 3,受制於內核的完全權威。完全在使用者模式實現的任何保護都可以被任何運行在更高權限層級的程式繞過,在 Windows 中,這意味著 ring 0(內核驅動程式)或更低(虛擬化環境、韌體)。一個調用 ReadProcessMemory 來檢查遊戲記憶體完整性的使用者模式反作弊程式,可以被一個掛鉤了 NtReadVirtualMemory 並返回偽造數據的內核驅動程式擊敗。一個通過 EnumProcessModules 枚舉載入模組的使用者模式反作弊程式,可以被一個修補 PEB 模組列表的驅動程式擊敗。使用者模式進程完全無法感知其上方發生的事情。

作弊開發者在大多數反作弊工程師願意採取行動之前很多年就已經理解了這一點。內核在很長一段時間內一直是作弊程式的專屬領域。內核模式作弊程式可以直接操作遊戲記憶體,而無需經過任何使用者模式反作弊程式可以攔截的 API。它們可以輕易地向使用者模式枚舉 API 隱藏自身的存在。它們可以攔截並偽造使用者模式反作弊程式可能執行的任何檢查結果。

回應是不可避免的:將反作弊程式移入內核。

升級一直是持續不斷的。使用者模式作弊程式讓位給內核作弊程式。內核反作弊程式隨之出現。作弊開發者開始利用合法的、簽名的驅動程式中的漏洞來實現內核執行,而無需載入未簽名的驅動程式(BYOVD 攻擊)。反作弊程式則通過封鎖列表和更嚴格的驅動程式枚舉來回應。作弊開發者轉向虛擬化環境,運行在內核之下並虛擬化整個作業系統。反作弊程式增加了虛擬化環境檢測。作弊開發者開始使用 PCIe DMA 設備直接通過硬體讀取遊戲記憶體,完全不接觸作業系統。對此的回應仍在開發中。

四個系統主導著競技遊戲的格局:

BattlEye 被 PUBG、Rainbow Six Siege、DayZ、Arma 和數十款其他遊戲使用。其內核組件是 BEDaisy.sys,並且一直是詳細公開逆向工程工作的對象,尤其是由 secret.club 研究人員和 back.engineering 部落格進行的。

EasyAntiCheat (EAC) 現由 Epic Games 擁有,並在 Fortnite、Apex Legends、Rust 等遊戲中使用。其架構在三組件設計上與 BattlEye 大致相似,但在實現細節上差異很大。

Vanguard 是 Riot Games 在 Valorant 和 League of Legends 中使用的專有反作弊程式。它以在系統啟動時而非遊戲啟動時載入其內核組件 (vgk.sys),以及其對驅動程式允許列表的積極態度而聞名。

FACEIT AC 用於 Counter-Strike 的 FACEIT 競技平台。它是一個內核級系統,在競技社群中以其有效的作弊檢測而享有良好聲譽,並已成為學術分析的主題,探討了內核反作弊軟體的架構特性。

2024 年的論文「If It Looks Like a Rootkit and Deceives Like a Rootkit」(發表於 ARES 2024)透過 rootkit 分類法的視角分析了 FACEIT AC 和 Vanguard,指出這兩個系統都與該類軟體具有技術特徵:內核級操作、系統範圍的回調註冊以及對作業系統活動的廣泛可見性。作者們謹慎地區分了技術分類和意圖,明確承認這些系統是為防禦目的服務的合法軟體。該論文的貢獻主要是分類學上的,而非指責性的。

根本的觀察是,有效的內核反作弊需要惡意內核軟體使用的相同作業系統原語,因為這些原語提供了檢測作弊所需的能見度。任何足夠強大的內核反作弊程式,在靜態行為分析下都會看起來像 rootkit,因為在內核 API 層級,能力和意圖是正交的。這是 Windows 架構施加的限制,而不是任何特定反作弊供應商的設計選擇。

現代內核級反作弊程式普遍遵循三層架構:

內核驅動程式:運行在 ring 0。註冊回調,攔截系統呼叫,掃描記憶體,強制執行保護。這是真正具有執行任何有意義操作能力的組件。

使用者模式服務:作為 Windows 服務運行,通常具有 SYSTEM 權限。通過 IOCTL 與內核驅動程式通信。處理與後端伺服器的網路通訊,管理封鎖執行,收集並傳輸遙測數據。

遊戲注入 DLL:注入到(或由)遊戲進程載入。執行使用者模式端的檢查,與服務通信,並作為應用於遊戲進程的保護的終端點。

這裡的關注點分離既是架構上的,也是安全驅動的。內核驅動程式可以執行使用者模式組件無法做到的事情,但它無法輕鬆建立網路連接或實現複雜的應用程式邏輯。服務可以做這些事情,但不能直接攔截系統呼叫。遊戲內 DLL 可以直接存取遊戲狀態,但在不可信的 ring-3 上下文中運行。

IOCTL(I/O 控制碼)是使用者模式和內核驅動程式之間的主要通信機制。使用者模式進程打開驅動程式設備對象的句柄,並使用控制碼調用 DeviceIoControl。驅動程式在其 IRP_MJ_DEVICE_CONTROL 分派例程中處理此請求。整個通信由內核調解,這意味著被破壞的使用者模式組件無法偽造任意內核操作——它只能發出驅動程式被程式設計為服務的請求。

命名管道用於服務和遊戲注入 DLL 之間的 IPC。命名管道比通過內核路由所有內容更快、更簡單,並且允許服務將通知推送到遊戲組件,而無需輪詢。

使用 NtCreateSection 創建並通過 NtMapViewOfSection 映射到服務進程和遊戲進程的共享記憶體區段,可以實現高頻寬、低延遲的數據共享。遙測數據(輸入事件、計時數據)可以由遊戲 DLL 寫入共享環形緩衝區,並由服務讀取,而無需每個事件都進行 IPC 開銷。

啟動時載入和運行時載入驅動程式之間的區別比看起來更重要。

BattlEye 和 EAC 在遊戲啟動時載入其內核驅動程式。BEDaisy.sys 及其 EAC 對應物被註冊為按需啟動驅動程式,並在遊戲啟動時由服務通過 ZwLoadDriver 載入。遊戲退出時會卸載它們。

Vanguard 在系統啟動時載入 vgk.sys。該驅動程式被配置為啟動時驅動程式(註冊表中的 SERVICE_BOOT_START),這意味著 Windows 內核在系統大部分初始化之前就載入了它。這給了 Vanguard 一個關鍵優勢:它可以觀察到在其之後載入的每一個驅動程式。任何在 vgk.sys 之後載入的驅動程式,都可以在其代碼被有效執行之前進行檢查。在正常驅動程式初始化階段載入的作弊驅動程式,實際上是載入到一個 Vanguard 已經監控的系統中。

啟動時載入的實際影響也是 Vanguard 需要系統重新啟動才能啟用:驅動程式必須在系統初始化之前到位,這意味著它不能在事後載入而無需重新啟動。

Windows 在 64 位系統上強制執行驅動程式簽章強制 (DSE),這要求內核驅動程式必須使用鏈接到受信任根的憑證進行簽章,並且驅動程式的代碼完整性必須在載入時進行驗證。這通過 ci.dll 中的 CiValidateImageHeader 和相關函數實現。內核還強制要求驅動程式憑證滿足某些擴展金鑰用法 (EKU) 要求。

反作弊程式以顯然的方式處理簽章:它們購買擴展驗證 (EV) 代碼簽章憑證,對某些組件執行 Microsoft 的 WHQL 流程,或使用交叉簽章。憑證要求多年來顯著收緊;Microsoft 現在對新的內核驅動程式要求 EV 憑證,並且對於針對 Windows 10 及更高版本的驅動程式,內核驅動程式簽章入口網站通常要求提交 WHQL。

這對作弊程式很重要的原因是 DSE 是一個重大的障礙。沒有簽名的驅動程式或繞過 DSE 的方法,作弊作者就無法載入任意內核代碼。BYOVD 攻擊(第 7 節介紹)是繞過此限制的主要機制。

BattlEye 的架構通過逆向工程得到了很好的記錄:

BEDaisy.sys 是內核驅動程式。它為進程創建、線程創建、映像載入和對象句柄操作註冊回調。它實現了實際的掃描和保護邏輯。

BEService.exe(或 BEService_x64.exe)是使用者模式服務。它通過驅動程式暴露的設備對象與 BEDaisy.sys 通信。它處理與 BattlEye 後端伺服器的網路通訊,從驅動程式接收檢測結果,並負責封鎖執行(將玩家踢出遊戲伺服器)。

BEClient_x64.dll 被注入到遊戲進程中。BattlEye 不會以傳統意義上的 CreateRemoteThread 注入它——它作為遊戲初始化的一部分載入,並得到遊戲的配合。此 DLL 負責在遊戲進程上下文中執行使用者模式端的檢查:它驗證自身完整性,執行各種環境檢查,並作為內核驅動程式專門應用於遊戲進程的保護的目標。

通信流程是:BEDaisy.sys 檢測到可疑情況,通過 IOCTL 完成或共享記憶體通知信號 BEService.exe,BEService.exe 報告給 BattlEye 的伺服器,伺服器決定操作(踢出/封鎖),然後 BEService.exe 指示遊戲終止連接。

BattlEye 的三組件架構:BEDaisy.sys 在 ring 0 通過 IOCTL 向作為 SYSTEM 服務運行的 BEService.exe 通信,後者管理注入到遊戲進程中的 BEClient_x64.dll。

vgk.sys 在範圍上比 BattlEye 驅動程式更具侵略性。由於它在啟動時載入,它可以攔截驅動程式載入過程本身。Vanguard 維護一個內部允許列表,列出了允許與受保護遊戲共存的驅動程式。任何不在該列表中的驅動程式,或任何未能通過完整性檢查的驅動程式,都可能導致 Vanguard 拒絕啟動遊戲。這是一個允許列表模型,而不是封鎖列表模型,後者在架構上要強大得多。

vgauth.exe 是 Vanguard 服務,負責 vgk.sys 和 Riot 後端基礎設施之間的通信。

這是內核級反作弊程式所做一切的基礎。Windows 內核公開了一套豐富的回調註冊 API,專為安全產品設計,反作弊程式利用了其中的每一個。

ObRegisterCallbacks 可能是進程保護中最重要的一個 API。它允許驅動程式註冊一個回調,該回調在打開或複製指定對象類型的句柄時被調用。對於反作弊目的,感興趣的對象類型是 PsProcessType 和 PsThreadType。

操作前回調接收一個 POB_PRE_OPERATION_INFORMATION 結構。關鍵字段是 Parameters->CreateHandleInformation.DesiredAccess。回調可以通過修改 Parameters->CreateHandleInformation.DesiredAccess 來從所需訪問權限中剝離訪問權限,然後再創建句柄。這就是反作弊程式如何阻止外部進程打開具有 PROCESS_VM_READ 或 PROCESS_VM_WRITE 訪問權限的遊戲進程句柄。

當作弊程式調用 OpenProcess (PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, gameProcessId) 時,反作弊程式的 ObRegisterCallbacks 操作前回調會觸發。回調檢查目標進程是否是受保護的遊戲進程。如果是,它會剝離 PROCESS_VM_READ、PROCESS_VM_WRITE、PROCESS_VM_OPERATION 和 PROCESS_DUP_HANDLE 的所需訪問權限。作弊程式會收到一個句柄,但該句柄對於讀取或寫入遊戲記憶體無效。作弊程式的 ReadProcessMemory 調用將以 ERROR_ACCESS_DENIED 失敗。

ObRegisterCallbacks 操作前回調的 IRQL 是 PASSIVE_LEVEL,這意味著回調可以調用可分頁代碼並執行阻塞操作(在合理範圍內)。

ObCallbackDemo.sys 正在運行。DebugView 顯示驅動程式正在剝離句柄訪問權限,Target.exe 正在運行並在記憶體中存儲秘密數據,而 Verifier.exe 嘗試讀取它時失敗並顯示 Access Denied。

PsSetCreateProcessNotifyRoutineEx 允許驅動程式註冊一個回調,該回調在系統範圍內的每個進程創建和終止事件上觸發。回調接收進程的 PEPROCESS、PID 和一個 PPS_CREATE_NOTIFY_INFO 結構,其中包含有關正在創建的進程的詳細信息(映像名稱、命令行、父 PID)。

值得注意的是,Ex 版本(在 Windows Vista SP1 中引入)提供了映像文件名和命令行,而原始的 PsSetCreateProcessNotifyRoutine 則沒有。回調在系統線程上下文的 PASSIVE_LEVEL 被調用。

反作弊程式使用此回調來檢測系統上出現的作弊工具進程。如果在遊戲運行時創建了已知的作弊啟動器或注入器進程,反作弊程式可以立即標記它。一些實現還將 CreateInfo->CreationStatus 設置為失敗代碼,以完全阻止進程啟動。

PsSetCreateThreadNotifyRoutine 在系統範圍內的每個線程創建和終止時觸發。反作弊程式專門使用它來檢測受保護遊戲進程中的線程創建。當在遊戲進程中創建新線程時,回調會觸發,反作弊程式可以檢查線程的起始地址。

調用 PsLookupThreadByThreadId 以檢索新線程的 ETHREAD 指標。PsGetThreadWin32StartAddress 返回進程看到的 Win32 起始地址,這與內核內部的起始地址不同。完成線程對象後,ObDereferenceObject 會釋放 PsLookupThreadByThreadId 獲取的引用。

在遊戲進程中創建的、其起始地址不在任何載入模組地址範圍內的線程,是注入代碼的強烈指標。合法的線程從模組代碼內部開始。注入的線程通常從 shellcode 或手動映射的 PE 代碼開始,這些代碼沒有模組支持。

PsSetLoadImageNotifyRoutine 在任何映像(DLL 或 EXE)映射到任何進程時觸發。它提供映像文件名和一個 PIMAGE_INFO 結構,其中包含基地址和大小。

這是 IRQL PASSIVE_LEVEL。回調在映像映射後但在其入口點執行之前觸發,這為反作弊程式提供了在任何代碼運行之前掃描映像的機會。

CmRegisterCallbackEx 註冊一個用於註冊表操作的回調。反作弊程式用它來監控可能表明作弊程式正在配置自身或嘗試修改反作弊設定的註冊表修改。

最小篩選器驅動程式位於文件系統篩選器堆棧中,並攔截發往文件系統驅動程式的 IRP 請求。反作弊程式使用最小篩選器來監控作弊文件丟失(將已知的作弊可執行文件或 DLL 寫入磁盤)、檢測對其自身驅動程式文件的讀取(這可能表明在磁盤上的驅動程式二進制文件被驗證之前嘗試修補它),以及強制執行文件訪問限制。

FltGetFileNameInformation 檢索操作目標的正規化文件名。完成後必須調用 FltReleaseFileNameInformation 來釋放引用。最小篩選器回調通常在 APC_LEVEL 或 PASSIVE_LEVEL 運行,具體取決於操作和文件系統。這很重要,因為許多操作(如分配可分頁池或調用可分頁函數)在 DISPATCH_LEVEL 或更高時不安全。

內核驅動程式可以做的遠不止註冊回調。它可以主動掃描遊戲進程的記憶體和系統範圍的記憶體池,以查找作弊程式的痕跡。

如 ObRegisterCallbacks 部分所述,保護遊戲記憶體免受外部讀寫的主要機制是從遊戲進程打開的句柄中剝離 PROCESS_VM_READ 和 PROCESS_VM_WRITE。這對於任何使用標準 Win32 API(ReadProcessMemory、WriteProcessMemory)的作弊程式都有效,因為這些最終會調用 NtReadVirtualMemory 和 NtWriteVirtualMemory,而這些都需要適當的句柄訪問權限。

然而,內核模式作弊程式可以完全繞過這一點。它可以直接調用 MmCopyVirtualMemory(一個未導出但可定位的內核函數)或直接操作頁表條目,而無需經過基於句柄的訪問控制系統即可訪問遊戲記憶體。這就是為什麼僅靠句柄保護是不夠的,以及為什麼內核模式作弊程式需要內核模式反作弊程式的回應。

反作弊程式會定期對遊戲可執行文件及其核心 DLL 的代碼段(.text 段)進行哈希計算。在遊戲開始時計算一個基準哈希,並定期將重新計算的哈希與基準進行比較。如果哈希值發生變化,則表示有人寫入了遊戲代碼,這是代碼修補(常用於通過修補遊戲邏輯來啟用無後坐力、加速或瞄準輔助功能)的強烈指標。

KeStackAttachProcess / KeUnstackDetachProcess 模式用於將調用線程臨時附加到目標進程的地址空間,允許驅動程式讀取映射到遊戲進程中的記憶體,而無需經過基於句柄的訪問控制。RtlImageNtHeader 從記憶體中的映像基址解析 PE 頭。

最有趣的記憶體掃描是手動映射代碼的啟發式檢測。當一個合法的 DLL 加載時,它會出現在進程的 PEB 模組列表中,在 InLoadOrderModuleList 中,並有一個對應的 VAD_NODE 條目,其 MemoryAreaType 表明映射來自文件。手動映射繞過了正常載入器,因此映射的代碼在記憶體中顯示為匿名私有映射或具有可疑特徵的文件後盾映射。

關鍵的啟發式方法是:找到進程中所有可執行記憶體區域,然後將每個區域與載入模組列表進行交叉引用。與任何載入模組都不對應的可執行記憶體是可疑的。

ZwQueryVirtualMemory 遍歷已提交的記憶體區域,為每個區域返回一個 MEMORY_BASIC_INFORMATION 結構。Type 字段區分私有分配 (MEM_PRIVATE) 和文件後盾映射 (MEM_IMAGE, MEM_MAPPED)。BattlEye 的掃描方法,如 secret.club 和 back.engineering 分析所記錄的,涉及掃描受保護進程的所有記憶體區域,並專門標記沒有文件後盾的可執行區域。它還掃描外部進程的記憶體頁面,尋找執行位異常,特別是針對頁面保護標誌被程式設計更改以使原本不可執行的記憶體變得可執行的情況(當 shellcode 被分階段時的常見技術)。

VAD(虛擬地址描述符)樹是內核內部結構,記憶體管理器使用它來跟踪進程中分配的所有記憶體區域。每個 VAD_NODE(實際上是內核術語中的 MMVAD 結構)包含有關區域的信息:其基地址和大小、其保護、它是否是文件後盾(如果是,是哪個文件),以及各種標誌。

反作弊程式直接遍歷 VAD 樹,而不是依賴 ZwQueryVirtualMemory,因為 VAD 樹不像模組列表那樣可以被輕易地從內核模式隱藏。遍歷 VAD:

我們可以通過在具有注入代碼的進程上使用 WinDbg 的 !vad 命令來實際觀察到這種檢測。

第一個條目是一個沒有後盾文件的 Private EXECUTE_READWRITE 區域,由我們的測試工具注入。每個合法模組顯示為 Mapped Exe,並帶有完整的文件路徑。

VAD 遍歷的力量在於,即使作弊程式已經操縱了 PEB 模組列表或 LDR_DATA_TABLE_ENTRY 鏈來隱藏自身,它也能捕獲手動映射的代碼。VAD 是一個使用者模式代碼無法直接修改的內核結構。

經典的注入技術:在目標進程中調用 CreateRemoteThread,將 LoadLibraryA 作為線程起始地址,並將 DLL 路徑作為參數。這可以通過 PsSetCreateThreadNotifyRoutine 輕鬆檢測:新線程的起始地址將是 LoadLibraryA(或者更確切地說是它在 kernel32.dll 中的地址),並且調用者進程不是遊戲本身。

一個更微妙的檢查是創建線程的 CLIENT_ID。當調用 CreateRemoteThread 時,內核會記錄是哪個進程創建了線程。反作弊程式可以檢查遊戲進程中的線程是否由外部進程創建,這是注入的可靠指標。

QueueUserAPC 和底層的 NtQueueApcThread 允許將異步過程調用排隊到任何進程的線程,只要調用者對該線程具有 THREAD_SET_CONTEXT 訪問權限。當目標線程進入可提醒等待狀態時,APC 會觸發並在目標線程的上下文中執行任意代碼。

內核級別的檢測利用 KAPC 結構。每個線程都有一個內核 APC 隊列和一個用戶 APC 隊列。反作弊程式可以檢查遊戲進程線程的掛起 APC 隊列,以檢測可疑的 APC 目標:

一種複雜的注入技術是通過 NtMapViewOfSection 將共享對象(由文件支持或使用 NtCreateSection 創建)映射到目標進程。這繞過了基於 CreateRemoteThread 的檢測啟發式方法,因為最初沒有創建遠程線程。然後,注入的代碼通常通過 APC 或通過修改現有線程的上下文來觸發。

檢測通過 VAD 進行:一個出現在遊戲進程中但由外部進程創建的段映射,在 VAD 中會顯示出獨特的模式。特別是,段後盾(或缺乏後盾)的 MMVAD::u.VadFlags.NoChange 和相關標誌可以揭示這種技術。

反射 DLL 注入將一個反射式加載器嵌入到 DLL 中,當 DLL 被執行時,它會在不使用 LoadLibrary 的情況下將 DLL 映射到記憶體中。DLL 解析自己的 PE 頭,解析導入,應用重定位,並調用 DllMain。結果是在記憶體中一個功能齊全的 DLL,它從未出現在 InLoadOrderModuleList 中。

檢測:具有有效 PE 頭(檢查 DOS 存根的 MZ 魔術字節和 e_lfanew 指定偏移量處的 PE\0\0 簽名)但沒有對應模組列表條目的可執行記憶體。這是可靠的指標。

我們可以通過一個簡單的測試工具來實際觀察到這一點,該工具分配一個 RWX 區域並將一個最小的 PE 頭寫入正在運行的進程:

使用 !vad 遍歷 VAD 樹會立即顯示注入的區域。第一個條目是 0x8A0 處的 Private EXECUTE_READWRITE 區域,沒有後盾文件。與底部的合法 Target.exe 映像相比,後者是 Mapped Exe EXECUTE_WRITECOPY,並帶有完整的文件路徑。轉儲合法模組的基址(使用 db 命令)證實了一個完整的 PE 頭,帶有 DOS 存根:

轉儲 0x008A0000 處的注入區域也顯示了一個有效的 MZ 簽名,但其餘的頭部大部分是零,沒有 DOS 存根。這是手動映射代碼的特徵:

最後,!peb 確認注入的區域沒有出現在任何模組列表中。PEB 只包含 Target.exe、ntdll.dll、kernel32.dll 和 KernelBase.dll。0x008A0000 處的區域對於任何枚舉載入模組的使用者模式 API 來說都是完全不可見的:

當 BEDaisy 想要檢查線程的調用堆棧時,它使用 APC 機制來捕獲用戶模式下的堆棧幀。APC 在遊戲線程的上下文中觸發,並調用 RtlWalkFrameChain 或 RtlCaptureStackBackTrace 來捕獲返回地址鏈。

BEDaisy(以及 Aki2k/BEDaisy GitHub 研究)的 back.engineering 分析專門記錄了這一點:BEDaisy 將內核 APC 排隊到受保護進程的線程中。APC 內核例程在 APC_LEVEL 運行,捕獲線程的堆棧,然後將每個返回地址與載入模組列表進行比較。指向任何載入模組外部的返回地址是堆棧上注入代碼的強烈指標,這表明線程當前正在執行注入代碼或已從其中返回。

掛鉤是使用者模式作弊程式攔截和操縱遊戲與作業系統交互的主要機制。檢測它們是反作弊程式的核心功能。

PE 文件的導入地址表 (IAT) 包含導入函數的地址。當進程載入時,載入器通過查找導出 DLL 中的每個導入函數並將函數地址寫入 IAT 來解析這些地址。IAT 掛鉤會用指向攻擊者控制的代碼的指針覆蓋其中一個條目。

檢測很直接:對於每個 IAT 條目,將解析的地址與正確 DLL 的磁盤導出所說的地址進行比較。

RtlImageDirectoryEntryToData 從 PE 頭定位導入目錄。TRUE 參數指定映像已映射(而不是磁盤上的原始文件),這在處理記憶體中的模組時是正確的。外部循環遍歷 IMAGE_IMPORT_DESCRIPTOR 數組,在 Name 字段為零時終止。內部循環將每個解析的 IAT 條目與預期的導出地址進行比較。

內聯掛鉤通過 JMP(相對近跳的 opcode 0xE9,或通過記憶體指針的間接跳轉的 0xFF 0x25)修補函數的前幾個字節,以將執行重定向到攻擊者代碼,攻擊者代碼通常會執行其修改,然後跳回原始代碼(“跳板”模式)。

檢測涉及讀取每個監視函數的前 16-32 個字節,並檢查:

反作弊程式讀取磁盤上的 PE 文件,並將函數序言的磁盤字節與當前記憶體中的字節進行比較。任何差異都表明修補。

為了演示內聯掛鉤檢測,我們使用一個測試工具,該工具將一個 MOV RAX; JMP RAX 掛鉤修補到運行進程中的 NtReadVirtualMemory:

在修補之前,函數序言顯示一個乾淨的 syscall 存根。mov r10, rcx 保存第一個參數,mov eax, 3Fh 加載 syscall 編號,然後 syscall 轉換到內核模式:

安裝掛鉤後,前 12 個字節被 mov rax, 0xDEADBEEFCAFEBABE; jmp rax 覆蓋,將執行重定向到攻擊者控制的地址。反作弊程式將這些字節與 ntdll 的磁盤副本進行比較,將立即標記不匹配:

系統服務描述符表 (SSDT) 是內核的 syscall 調度表。當使用者模式進程執行 syscall 指令時,內核使用 syscall 編號(放在 EAX 中)索引 SSDT 並調用相應的內核函數。修補 SSDT 會將 syscall 重定向到攻擊者控制的代碼。

SSDT 掛鉤是一種經典技術,在 64 位 Windows 中引入 PatchGuard(內核修補保護,KPP)後變得更加困難。PatchGuard 會監視 SSDT(以及許多其他結構),並在檢測到修改時觸發 CRITICAL_STRUCTURE_CORRUPTION 錯誤檢查 (0x109)。因此,SSDT 掛鉤在 64 位 Windows 中基本上已經失效。然而,反作弊程式仍然將 SSDT 完整性驗證作為縱深防禦措施。

中斷描述符表 (IDT) 將中斷向量映射到其處理程序例程。全局描述符表 (GDT) 定義記憶體段。兩者都是處理器級結構,在所有配置上不能僅由 PatchGuard 輕鬆保護。

在內核級運行的作弊程式可以嘗試替換 IDT 條目以攔截特定中斷,這可用於控制流攔截或作為隱蔽通道。反作弊程式驗證 IDT 條目是否指向預期的內核位置:

一種常見的規避技術是作弊程式直接執行 syscall(使用帶有適當 syscall 編號的 syscall 指令),而不是通過 ntdll.dll 函數。這繞過了 ntdll 中設置的使用者模式掛鉤。反作弊程式通過監控遊戲進程中的線程執行來自意外代碼位置的 syscall 指令,以及檢查本應被調用的 ntdll 函數是否以預期的頻率和模式被調用來檢測這一點。

在具有安全啟動 (Secure Boot) 的正確配置的 Windows 系統上,所有內核驅動程式都必須由 Microsoft 信任的憑證簽名。測試簽名模式(通過 bcdedit /set testsigning on 啟用)允許載入自簽名驅動程式,這是常見的開發和作弊部署技術。

反作弊程式通過讀取 Windows 啟動配置和檢查反映 DSE 當前是否強制執行的內核變量來檢測測試簽名模式。一些反作弊程式在啟用測試簽名時拒絕啟動。

SeValidateImageHeader 和 SeValidateImageData 函數在內核中驗證驅動程式簽名。反作弊程式可以檢查載入的驅動程式對象,並驗證其 IMAGE_INFO_EX ImageSignatureType 和 ImageSignatureLevel 字段是否反映了正確的簽名。

攜帶您自己的易受攻擊驅動程式 (Bring Your Own Vulnerable Driver, BYOVD) 是 2024-2026 年載入未簽名內核代碼的主導技術。該攻擊的工作原理如下:

常見的 BYOVD 目標包括來自 MSI、Gigabyte、ASUS 和各種硬件供應商的驅動程式。這些驅動程式通常具有暴露直接物理記憶體讀寫能力的 IOCTL 處理程序,這是一個攻擊者所需的一切。

對 BYOVD 的主要防禦是已知易受攻擊驅動程式的封鎖列表。Microsoft 易受攻擊驅動程式封鎖列表(在 DriverSiPolicy.p7b 中維護)內置於 Windows 中,並通過 Windows Update 分發。反作弊程式維護自己的、更具侵略性的封鎖列表。

Vanguard 特別以其主動將載入的驅動程式集與其封鎖列表進行比較,並在存在封鎖列表中的驅動程式時拒絕啟動受保護遊戲而聞名。這是強制執行的,因為一些 BYOVD 攻擊涉及載入易受攻擊的驅動程式並立即使用它,然後再卸載它,因此僅在遊戲啟動時進行預掃描可以涵蓋大多數情況。

這是內核作弊開發者和反作弊工程師都非常關心的內部機制之一。

PiDDBCacheTable 是內核內部 AVL 樹,用於緩存先前載入驅動程式的信息。當驅動程式載入時,內核會存儲一個以驅動程式的 TimeDateStamp(來自 PE 頭)和 SizeOfImage 為鍵的條目。此緩存用於快速查找驅動程式是否先前已被看到。該結構是一個 RTL_AVL_TABLE,由 PiDDBLock(一個 ERESOURCE 鎖)保護。

手動映射驅動程式而不經過正常載入路徑的作弊開發者會嘗試刪除或修改對應的 PiDDBCacheTable 條目,以隱藏其驅動程式曾被載入的事實。反作弊程式通過以下方式檢測此行為:

PiDDBCacheTable 未導出,PiDDBCacheEntry 也不在公共符號中。要與緩存交互,我們需要逆向工程條目結構。比較例程是最佳起點,因為它直接訪問用於排序的字段。

反編譯的輸出顯示了結構佈局。該函數接收兩個 PiDDBCacheEntry 指標,首先比較它們的 DriverName 字段(偏移量 0x10 處的 UNICODE_STRING)使用 RtlCompareUnicodeString。如果名稱相等且 TableContext 非零,則認為條目相等。否則,它會繼續比較 TimeDateStamp 字段(偏移量 0x20 處的 ULONG)。這使我們恢復了結構:

PiDDBCacheTable 是一個 RTL_AVL_TABLE,一個自平衡二叉搜索樹。樹中的每個節點都以 _RTL_BALANCED_LINKS 頭開始,包含 Parent、LeftChild 和 RightChild 指針。實際的 PiDDBCacheEntry 數據緊隨在此頭之後。

要枚舉條目,我們首先解析表地址。PiDDBCacheTable 未導出,因此反作弊程式通過在 ntoskrnl.exe 中進行簽名掃描來定位它。在載入符號的 WinDbg 中,我們可以直接解析它:

該表包含 151 個緩存的驅動程式條目,樹深度為 9。CompareRoutine 指向 PiCompareDDBCacheEntries,證實這是正確的表。BalancedRoot 是樹的入口點。其 RightChild 給我們第一個實際節點:

從每個節點開始,條目數據從偏移量 0x20(在 _RTL_BALANCED_LINKS 頭之後)開始。添加我們恢復的偏移量:DriverName 在 node+0x30,TimeDateStamp 在 node+0x40。跟隨 LeftChild 和 RightChild 指針可以讓我們遍歷整個樹:

手動映射內核驅動程式的作弊開發者會嘗試查找並從該樹中刪除其條目以避免檢測。一個反作弊程式通過內核分配掃描或其他方式檢測到記憶體中的驅動程式,但找不到對應的 PiDDBCacheTable 條目,就知道該條目已被清除。

MmUnloadedDrivers 是一個內核數組(也未導出),它維護一個最近 50 個已卸載驅動程式的循環緩衝區,存儲其名稱、起始地址、結束地址和卸載時間戳。此結構允許調試和取證驅動程式活動。

成功載入然後卸載內核驅動程式的作弊開發者通常會嘗試將 MmUnloadedDrivers 中的條目歸零或損壞,以隱藏痕跡。反作弊程式通過以下方式檢測此行為:

當內核分配超過約 4KB 時(更準確地說,當它超過由池分配器管理的閾值時),它被視為“大池分配”,並在 PoolBigPageTable 中進行跟踪。反作弊程式掃描此表以查找由手動映射的驅動程式進行的記憶體分配。手動映射的驅動程式通常為其代碼和數據段進行大分配;這些在 big pool 表中顯示為分配地址,但沒有對應的載入驅動程式。

該技術是枚舉所有大池條目,然後將每個分配的地址與載入驅動程式地址範圍列表進行交叉引用。不在任何驅動程式範圍內的、大小適合作為驅動程式代碼段的分配是可疑的。

反作弊程式碼本身是逆向工程的高價值目標。分析反作弊驅動程式的逆向工程師需要使用內核調試器,而反作弊程式會積極檢測這些調試器。

在使用者模式級別(在遊戲注入 DLL 中),反作弊程式使用 NtQueryInformationProcess 並帶有多種信息類:

內核驅動程式檢查內核導出的變量 KdDebuggerEnabled 和 KdDebuggerNotPresent。在連接了 WinDbg(或任何內核調試器)的系統上,KdDebuggerEnabled 為 TRUE,KdDebuggerNotPresent 為 FALSE。

一些反作弊程式更進一步,直接檢查 KDDEBUGGER_DATA64 結構和共享內核數據頁 (KUSER_SHARED_DATA) 中的調試器相關標誌。

NtSetInformationThread 帶有 ThreadHideFromDebugger (17) 會在線程的 ETHREAD 結構中設置一個標誌(CrossThreadFlags.HideFromDebugger)。設置後,內核將不會向任何連接的調試器傳遞該線程的調試事件。該線程實際上對 WinDbg 變得不可見:線程中的斷點不會觸發調試器通知,異常也不會被轉發。

反作弊程式使用此功能來保護自己的線程。但是,它們也會檢測作弊程式是否使用此功能來隱藏自己的注入線程。檢測方法是通過內核枚舉(而不是通過可能被掛鉤的使用者模式 API)來枚舉系統中的所有線程,並檢查每個線程的 CrossThreadFlags 中的 HideFromDebugger 位。遊戲進程中一個反作弊程式未自行隱藏的隱藏線程是一個危險信號。

為了演示這種檢測,我們使用一個測試工具,該工具在 Target.exe 中創建一個遠程線程,然後為其設置 ThreadHideFromDebugger:

在 WinDbg 中,我們將十進制 TID 轉換為十六進制,在進程中找到線程,並檢查其 CrossThreadFlags。在設置標誌之前,值為 0x5402,位 2(HideFromDebugger)未設置:

調用帶有 ThreadHideFromDebugger 的 NtSetInformationThread 後,值變為 0x5406。位 2 現在已設置,這使得該線程對任何連接的調試器都不可見:

枚舉遊戲進程中線程的反作弊程式會檢查每個線程的這個位。一個反作弊程式未自行創建的隱藏線程是注入作弊代碼的強烈指標。

單步調試(通過 EFLAGS 中的 TF 標誌)和硬件斷點會顯著增加指令執行之間的間隔。反作弊程式使用基於 RDTSC 指令的計時來檢測這一點:

閾值 EXPECTED_MAXIMUM_CYCLES 是根據已知的 CPU 行為校準的。單步調試每條指令可能增加數千個週期(由於調試異常處理),使得計時差異顯而易見。

x86-64 調試寄存器(DR0-DR3 用於斷點地址,DR6 用於狀態,DR7 用於控制)在內核模式下是可訪問的。讀取它們可以檢測調試器設置的硬件斷點:

反作弊程式掃描所有線程的保存的調試寄存器狀態(可通過 KeGetContextThread 獲取的 CONTEXT 結構或直接從 KTHREAD::TrapFrame 獲取)是否存在反作弊程式本身未設置的活動硬件斷點。

類型 1 虛擬化環境的調試器(例如,運行 Windows VM 進行隔離調試的自定義虛擬化環境)非常難以檢測。主要的檢測向量是:

CPUID 檢查:虛擬化環境存在位(執行 CPUID leaf 1 時 ECX 的第 31 位)表明存在虛擬化環境。可以使用 CPUID leaf 0x40000000 查詢虛擬化環境供應商。VMware 返回“VMwareVMware”,VirtualBox 返回“VBoxVBoxVBox”。未知的供應商字符串是可疑的。

MSR 計時:在 VM 中執行 RDMSR 比原生執行會引入額外的開銷。反作弊程式計時 MSR 讀取並標記異常。

CPUID 指令計時:CPUID 指令本身在虛擬化環境中是特權指令,必須由虛擬化環境處理,這會引入可衡量的延遲。

DMA 作弊代表了反作弊軍備競賽的當前前沿,並且僅靠軟體確實難以解決。

PCIe DMA(直接記憶體訪問)作弊使用一個 PCIe 連接的設備——通常是一個開發 FPGA 板——它可以直接讀取主機系統的物理記憶體,通過 PCIe 總線,而無需 CPU 的參與。pcileech 框架及其 LeechCore 庫為這些設備提供了軟體堆棧。該設備物理出現在 PCIe 總線上,通過 PCIe TLP(事務層封包)協議訪問主機的物理記憶體,並通過將虛擬地址轉換為物理地址(使用頁表,頁表也在物理記憶體中,設備可以讀取)來讀取遊戲進程記憶體。

攻擊機器(運行作弊軟體)與受害者機器(運行遊戲)物理分離。所有作弊邏輯都在攻擊者的機器上運行。遊戲機器沒有來自作弊程式的進程、驅動程式或記憶體分配。從純軟體角度來看,遊戲機器是完全乾淨的。

FPGA 位於遊戲 PC 的 PCIe 總線上,通過 TLP 讀取物理記憶體,無需 CPU 介入。作弊軟體完全運行在單獨的機器上,從軟體角度來看,遊戲 PC 是乾淨的。

PCIe 通信圍繞 TLP 結構化。來自 DMA 設備的記憶體讀取 TLP 包含要讀取的物理地址和請求的字節數。PCIe 根複合體通過讀取指定的物理記憶體並在完成 TLP 中返回數據來處理此請求。這完全是硬體級別的,CPU 不參與處理請求。

設備需要配置一個有效的 BAR(基址寄存器),BIOS 在 PCIe 枚舉期間會分配給它。設備還需要目標系統的 IOMMU(如果存在)被禁用,或者設備的 DMA 事務被允許通過它。

IOMMU(Intel VT-d,AMD-Vi)是一個硬體單元,它使用特定於設備的頁表(類似於 CPU 的使用者模式地址轉換頁表)來轉換來自 PCIe 設備的 DMA 地址。如果 IOMMU 已啟用並正確配置,PCIe 設備只能訪問作業系統通過 IOMMU 頁表明確授予它的物理記憶體。

啟用 IOMMU 後,未與任何作業系統驅動程式關聯的 DMA 設備沒有 IOMMU 映射,無法訪問任意物理記憶體。理論上,這是對 DMA 攻擊的硬體級防禦。

實際上,IOMMU 防禦存在顯著的漏洞。許多遊戲主板出廠時 IOMMU 默認禁用。即使啟用,IOMMU 配置也很複雜,許多系統的 IOMMU 策略配置錯誤,導致大範圍的物理記憶體區域可訪問。關鍵是,成功模擬合法 PCIe 設備(例如,作業系統已授予 IOMMU 訪問權限的 USB 控制器或網卡)的 DMA 韌體,可能利用合法設備的授權權限通過 IOMMU 訪問記憶體。

複雜的 DMA 作弊韌體旨在模擬合法設備的 PCIe 設備 ID、供應商 ID、子系統 ID 和 BAR0 配置。運行自定義韌體的 Xilinx FPGA 可以向 BIOS 和作業系統呈現為,例如,一個 USB 主控制器。作業系統載入該設備的合法驅動程式(提供 IOMMU 覆蓋),然後 FPGA 韌體利用該覆蓋執行 DMA 讀取。

反作弊程式試圖通過枚舉所有 PCIe 設備並驗證每個設備報告的特性是否與預期的硬體匹配來檢測這一點。但是,沒有特定的韌體級別證明,很難區分合法硬體和正確模擬的 FPGA。

Epic Games 對 Fortnite 要求安全啟動和 TPM 2.0,這與 DMA 威脅直接相關。安全啟動確保只運行簽名啟動載入器,這可以防止禁用 IOMMU 或安裝韌體級別作弊程式的啟動時攻擊。TPM 2.0 支持測量啟動(其中每個啟動階段的哈希值記錄在 TPM 的 PCR 寄存器中),提供一個證明鏈,證明系統在已知的良好狀態下啟動。使用 TPM 的遠程證明允許伺服器驗證客戶端系統未在韌體級別被篡改。

這並沒有直接解決 DMA 問題(物理連接到 PCIe 插槽的 DMA 攻擊設備繞過了所有這些),但它關閉了一些軟體輔助的 DMA 攻擊路徑。

沒有靜態保護方案是足夠的。基於遊戲遙測數據運行的行為檢測是解決內核保護無法解決的問題的補充層。

內核反作弊驅動程式運行在可以攔截原始輸入並在輸入到達遊戲之前進行處理的級別。用於 HID(人機接口設備)輸入的驅動程式,特別是滑鼠和鍵盤,位於輸入驅動程式堆棧中。通過在 mouclass.sys 或 kbdclass.sys 上方安裝篩選器驅動程式,反作弊程式可以觀察所有輸入事件,其時間戳精度可達系統時鐘(微秒級別)。

瞄準輔助檢測針對滑鼠移動的統計屬性。人類瞄準具有特定屬性:Fitts 定律 go