Windows 反調試技術——OpenProcess 許可權過濾

本周我有了休息時間,來回顧一下反調試技術。目前,Bug Bounty平台上有大量程序依賴於客戶端應用,而且許多安全產品和遊戲反作弊引擎都採用了這些反調試技術來阻止你調試核心模塊。我想有必要來分享其中一項反調試技術,以及如何繞過它。

本文所述的技術並不是一個安全漏洞,很明顯,如果攻擊者擁有了這個級別的系統訪問許可權,遊戲就已經結束了。他們只需要安裝一個 rookit 就夠了。

文中我將以 AVG 產品為例。儘管我盡量避免過多地討論這一款產品,然而其他的反病毒解決方案和安全產品使用了完全相同的技術,所以相同的原則也同樣適用這些產品。

面臨什麼問題?

如果你以前嘗試過打開 x64dbg,並把它附加到一個 AV(譯者註:AntiVirus) 組件中,通常會看到如下界面:(下圖是GIF動圖1)

調試器基本沒有附加成功,並停在了啟動頁。如果我們在調試器內不採用附加的方式,而是直接啟動剛才的進程:(下圖是GIF動圖2)

還是不行,出現了相同的結果。當進程剛要啟動時,調試程序被踢出了。最後,我們試試 WinDBG,得到了下面的錯誤信息:

為了理解調試器剛才做了什麼,同時發現哪裡出了問題,我們看一下 x64dbg 的源碼(實際上,是 x64dbg 使用的調試引擎 TitanEngine 的源碼)。

__declspec(dllexport) bool TITCALL AttachDebugger(DWORD ProcessId, bool KillOnExit, LPVOID DebugInfo, LPVOID CallBack){...if(ProcessId != NULL && dbgProcessInformation.hProcess == NULL){if(engineEnableDebugPrivilege){EngineSetDebugPrivilege(GetCurrentProcess(), true);DebugRemoveDebugPrivilege = true;}if(DebugActiveProcess(ProcessId)){...}}}

從代碼中發現,x64dbg 使用了一個 KernelBase.dll 提供的 Win32 函數 「DebugActiveProcess」。

DebugActiveProcess 的工作原理

DebugActiveProcess 函數用於在目標進程上開啟一個調試會話。該函數的唯一參數是目標進程的PID。如果在 MSDN 上查閱該函數,可以看到如下的描述:

「The debugger must have appropriate access to the target process, and it must be able to open the process for PROCESS_ALL_ACCESS.

DebugActiveProcess can fail if the target process is created with a security descriptor that grants the debugger anything less than full access.

If the debugging process has the SE_DEBUG_NAME privilege granted and enabled, it can debug any process.」

這裡,我們發現了導致調試會話失敗的端倪。

上述的代碼片段中,調試器調用了 EngineSetDebugPrivilege 函數。那麼,來看看這個函數。

DWORD EngineSetDebugPrivilege(HANDLE hProcess, bool bEnablePrivilege){DWORD dwLastError;HANDLE hToken = 0;if(!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)){...}...if(!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)){...}tokenPrivileges.PrivilegeCount = 1;tokenPrivileges.Privileges[0].Luid = luid;if(bEnablePrivilege)tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;elsetokenPrivileges.Privileges[0].Attributes = 0;AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL);...}

從上述代碼中可以看到,SE_DEBUG_NAME 許可權已經設置到進程令牌(process token)上。

這意味著,調用 DebugActiveProcess 函數的要求已經滿足(譯者註:要求指的是MSDN中描述的 SE_DEBUG_NAME許可權要求)。

接著檢查,是否擁有對於目標進程的 PROCESS_ALL_ACCESS 許可權。

深入 DebugActiveProcess 內部

DebugActiveProcess 接受唯一的參數是「進程ID」。在該函數內部,使用進程ID 調用 ProcessIdToHandle,打開目標進程的句柄:

進入 ProcessIdToHandle 函數內部,可以發現該函數僅僅是對 NtOpenProcess 的封裝:

NftOpenProcess函數中有一個形參叫做「Desired Access」,即所需的訪問權。該參數的實參是 C3Ah。通過微軟官方文檔發現,這個值是以下值的組合:

  • PROCESS_CREATE_THREAD (譯者註:0x0002)
  • PROCESS_VM_OPERATION (0x0008)
  • PROCESS_VM_WRITE (0x0020)
  • PROCESS_VM_READ (0x0010)
  • PROCESS_SUSPEND_RESUME (0x0800)
  • PROCESS_QUERY_INFORMATION (0x0400)

於是,這次調用具備了調試進程所需要的全部授權。

到這裡,調試器具備了 SE_DEBUG_NAME 授權,DebugActiveProcess 調用也給自身賦予了正確的訪問目標進程的許可權。

那麼是什麼阻止了附加過程呢?

ObRegisterCallbacks 簡介

我是在一個遊戲模組社區(譯者註:即遊戲mod社區)中第一次知道 ObRegisterCallbacks 函數的。在繞過反作弊和 DRM 驅動時,該函數被用於阻止修改或注入遊戲功能。

按照微軟官方說法,ObRegisterCallbacks 是「這樣一個函數,它為線程、進程、桌面句柄操作註冊了一系列回調函數。」這是在操作系統內核態完成的。主要是給驅動程序開發者提供一種能力,用於在 OpenProcess 函數被調用時和返回時收到通知。

為什麼這個函數能夠用於阻止調試器訪問 AV 進程呢?阻止 DebugActiveProcess 調用成功的其中一個方法就是,過濾掉 「調用NtOpenProcess「所需要的訪問許可權(譯者註:NtOpenProcess 函數有一個形參 DesiredAccess,這裡指的是,該參數對應的實參被過濾後,就不是所需要的值了)。通過移除調試器「請求目標進程的 PROCESS_ALL_ACCESS 訪問權」的能力,我們就無法調試一個進程。這也解釋了剛剛在 WinDBG看到的錯誤。

怎麼確認這就是問題所在呢?我們接著進入內核調試器,觀察註冊的回調函數是如何在 Ring-0 被處理的。(這裡不會詳細介紹如何使用內核調試器,如果你需要一些資料,可以閱讀我之前的博客)

深入 ObRegisterCallback 內部

當啟動內核調試後,從 nt!ProcessType 開始分析:

kd> dt nt!_OBJECT_TYPE poi(nt!PsProcessType)+0x000 TypeList : _LIST_ENTRY [ 0xffffcb82`dee6cf20 - 0xffffcb82`dee6cf20 ]+0x010 Name : _UNICODE_STRING "Process"+0x020 DefaultObject : (null) +0x028 Index : 0x7 +0x02c TotalNumberOfObjects : 0x26+0x030 TotalNumberOfHandles : 0xe8+0x034 HighWaterNumberOfObjects : 0x26+0x038 HighWaterNumberOfHandles : 0xea+0x040 TypeInfo : _OBJECT_TYPE_INITIALIZER+0x0b8 TypeLock : _EX_PUSH_LOCK+0x0c0 Key : 0x636f7250+0x0c8 CallbackList : _LIST_ENTRY [ 0xffffa002`d31bacd0 - 0xffffa002`d35d2450 ]

這個符號包含了一個指向 _OBJECT_TYPE 類型對象的指針,該對象定義了 「Process」 類型,並包含了一個CallbackList屬性。

這個屬性值得我們注意。該屬性定義了一個回調函數列表,其中存儲了由 ObRegisterCallbacks 註冊的函數。

之後,其中的每個函數都會在獲取進程句柄時由內核調用。基於這個理解,我們將遍歷這個列表,找到阻止成功調用 OpenProcess 函數的回調函數句柄。

CallbackList 是一個 _LIST_ENTRY,指向 CALLBACK_ENTRY_ITEM 結構體。該結構體在微軟的文檔中沒有說明,然而有一篇文章 「DOUGGEM』S GAME HACKING AND REVERSING NOTES」 給出了結構體的定義:

typedef struct _CALLBACK_ENTRY_ITEM {LIST_ENTRY EntryItemList;OB_OPERATION Operations;CALLBACK_ENTRY* CallbackEntry;POBJECT_TYPE ObjectType;POB_PRE_OPERATION_CALLBACK PreOperation;POB_POST_OPERATION_CALLBACK PostOperation;__int64 unk;}CALLBACK_ENTRY_ITEM, *PCALLBACK_ENTRY_ITEM;

結構體中的 PreOperation 引起了我們的注意。

通過如下 WinDBG 命令,遍歷 CALLBACK_ENTRY_ITEM 列表:

!list -x ".if (poi(@$extret+0x28) != 0) { u poi(@$extret+0x28); }" (poi(nt!PsProcessType)+0xc8)

在我的電腦上,有 4 個驅動程序通過 ObRegisterCallbacks 註冊了 PreOperation 回調函數。

接著,我們通過 WinDBG 輸出驅動程序的名字:

!list -x ".if (poi(@$extret+0x28) != 0) { lmv a (poi(@$extret+0x28)) }" (poi(nt!PsProcessType)+0xc8)

這 4 個驅動程序中,其中一個立刻引起了我們關注,很可能它就是問題的關鍵:avgSP.sys。

可以判斷出:就是 「AVG self protection module」 模塊在阻止我們將調試器附加到進程中(更有可能的是,當反病毒引擎阻止惡意軟體時,產生了這樣的副作用)。接著,我們深入分析下這個驅動程序,找出其影響 OpenProcess 調用的痕迹。

首先,找到 ObRegisterCallbacks 函數,它註冊了一個函數句柄:

我們如果檢查這個剛註冊的函數句柄,可以發現:

在反彙編代碼中,出現了一個幻數(Magic Number)A0121410。實際上,它表示以下許可權:

  • PROCESS_VM_READ (譯者註:0x0010)
  • PROCESS_QUERY_INFORMATION (0x0400)
  • PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
  • READ_CONTROL (0x00020000L)
  • SYNCHRONIZE (0x00100000L)

其實,如果只設置這些許可權的話,則沒有進一步的許可權檢查操作,OpenProcess 函數繼續執行。然而,如果請求上述許可權白名單以外的許可權,還要執行一系列的檢查操作,最終在函數返回前,所需要的許可權被過濾掉。

由於本文主要講解「識別和移除」這種鉤子的通用方法,所以我不打算深入驅動程序的細節了。

從上面的分析可知,我們發現有一個驅動程序在攔截和修改 OpenProcess 調用。

現在,已經找到問題根源,接下來就是從內核中拆下這個鉤子。

移除 OpenProcess 許可權過濾

為了移除 OpenProcess 的許可權過濾函數,首先需要找到過濾函數所在的 PreOperation 屬性的地址。輸入 WinDBG 命令:

!list -x ".if (poi(@$extret+0x28) != 0) { .echo handler at; ?? @$extret+0x28; u poi(@$extret+0x28); }" (poi(nt!PsProcessType)+0xc8)

一旦發現了正確的屬性地址,我們使用下面的命令將其置為 NULL,以此來禁止回調句柄:

eq 0xffffa002`d31bacf8 0

此時,再次將調試器附加到被調試程序,可以得到如下界面:

太棒了!看上去我們已經成功了。

嗯,幾乎是……我們稍加操作就可以發現大量錯誤,問題還沒有處理乾淨。

即使在上述界面,我們也可以看到寄存器的值都是0,並且出現了訪問衝突。這一定是漏掉了什麼。

記住還有線程

我們已經知道 ObRegisterCallbacks 函數可以給 OpenProcess 加上鉤子函數,還能做什麼呢?再次查看官方文檔發現,ObRegisterCallbacks 也可以給 OpenThread 加上鉤子。

慶幸的是,很多工作已經完成了,我們只需要找到線程的鉤子函數所在的位置即可。這個位置恰好定義在 nt!PsThreadType 中。

修改一下剛才輸入的命令,觀察驅動程序(譯者註:指的是 avgSP.sys)是否為 OpenThread 函數添加了鉤子:

!list -x ".if (poi(@$extret+0x28) != 0) { .echo handler at; ?? @$extret+0x28; u poi(@$extret+0x28); }" (poi(nt!PsThreadType)+0xc8)

真的有鉤子!和剛才的進程鉤子類似,我們使用 eq 命令移除鉤子:

eq 0xffffc581`89df32e8 0

再次附加調試器到進程:(下圖是GIF動圖3)

大功告成!可以開始正常調試了。

希望本文有助於你了解這項反調試技術。如果感興趣,還有很多 Bug Bounty 程序可供學習,包括 BugCrowd 平台上 AVG 的一個例子(點擊這裡)、Cylance、Sophos等等。(儘管我沒有把這些作為安全漏洞,但是 DKOM 不在討論範圍)(譯者註:DKOM,全稱是 Direct kernel object manipulation)。

參考資料

  • TitanEngine
  • DOUGGEMS GAME HACKING AND REVERSING NOTES
  • AVG Bug Bounty
  • x64Dbg GitHub

本文由看雪翻譯小組 yezhang 編譯,來源xpnsec@Adam 轉載請註明來自看雪社區

更多乾貨請關注看雪學院公眾號:ikanxue!

推薦閱讀:

深入X64架構(翻譯轉載)(4)之參數獲取
調試利器-SSH隧道
ReactNative 知識小集(1)-深入理解 React Native Debugging

TAG:MicrosoftWindows | 軟體調試 |