轉載: https://blog.xpnsec.com/anti-debug-openprocess/
看雪翻譯:https://bbs.pediy.com/thread-223857.htm
本周我有了休息時間,來回顧一下反調試技術。目前,Bug Bounty平台上有大量程序依賴於客戶端應用,而且許多安全產品和游戲反作弊引擎都采用了這些反調試技術來阻止你調試核心模塊。我想有必要來分享其中一項反調試技術,以及如何繞過它。
本文所述的技術並不是一個安全漏洞,很明顯,如果攻擊者擁有了這個級別的系統訪問權限,游戲就已經結束了。他們只需要安裝一個 rookit 就夠了。
文中我將以 AVG 產品為例。盡管我盡量避免過多地討論這一款產品,然而其他的反病毒解決方案和安全產品使用了完全相同的技術,所以相同的原則也同樣適用這些產品。
面臨什么問題?
如果你以前嘗試過打開 x64dbg,並把它附加到一個 AV(譯者注:AntiVirus) 組件中,通常會看到如下界面:(下圖是GIF動圖1)
調試器基本沒有附加成功,並停在了啟動頁。如果我們在調試器內不采用附加的方式,而是直接啟動剛才的進程:(下圖是GIF動圖2)
還是不行,出現了相同的結果。當進程剛要啟動時,調試程序被踢出了。最后,我們試試 WinDBG,得到了下面的錯誤信息:
為了理解調試器剛才做了什么,同時發現哪里出了問題,我們看一下 x64dbg 的源碼(實際上,是 x64dbg 使用的調試引擎 TitanEngine 的源碼)。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
__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.”
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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;
else
tokenPrivileges.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 開始分析:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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” 給出了結構體的定義:
|
1
2
3
4
5
6
7
8
9
|
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 列表:
|
1
|
!
list
-
x
".if (poi(@$extret+0x28) != 0) { u poi(@$extret+0x28); }"
(poi(nt!PsProcessType)
+
0xc8
)
|
在我的電腦上,有 4 個驅動程序通過 ObRegisterCallbacks 注冊了 PreOperation 回調函數。
接着,我們通過 WinDBG 輸出驅動程序的名字:
|
1
|
!
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 命令:
|
1
|
!
list
-
x
".if (poi(@$extret+0x28) != 0) { .echo handler at; ?? @$extret+0x28; u poi(@$extret+0x28); }"
(poi(nt!PsProcessType)
+
0xc8
)
|
一旦發現了正確的屬性地址,我們使用下面的命令將其置為 NULL,以此來禁止回調句柄:
|
1
|
eq
0xffffa002
`d31bacf8
0
|
此時,再次將調試器附加到被調試程序,可以得到如下界面:
太棒了!看上去我們已經成功了。
嗯,幾乎是……我們稍加操作就可以發現大量錯誤,問題還沒有處理干凈。
即使在上述界面,我們也可以看到寄存器的值都是0,並且出現了訪問沖突。這一定是漏掉了什么。
記住還有線程
我們已經知道 ObRegisterCallbacks 函數可以給 OpenProcess 加上鈎子函數,還能做什么呢?再次查看官方文檔發現,ObRegisterCallbacks 也可以給 OpenThread 加上鈎子。
慶幸的是,很多工作已經完成了,我們只需要找到線程的鈎子函數所在的位置即可。這個位置恰好定義在 nt!PsThreadType 中。
修改一下剛才輸入的命令,觀察驅動程序(譯者注:指的是 avgSP.sys)是否為 OpenThread 函數添加了鈎子:
|
1
|
!
list
-
x
".if (poi(@$extret+0x28) != 0) { .echo handler at; ?? @$extret+0x28; u poi(@$extret+0x28); }"
(poi(nt!PsThreadType)
+
0xc8
)
|
真的有鈎子!和剛才的進程鈎子類似,我們使用 eq 命令移除鈎子:
|
1
|
eq
0xffffc581
`
89df32e8
0
|
再次附加調試器到進程:(下圖是GIF動圖3)
大功告成!可以開始正常調試了。
希望本文有助於你了解這項反調試技術。如果感興趣,還有很多 Bug Bounty 程序可供學習,包括 BugCrowd 平台上 AVG 的一個例子(點擊這里)、Cylance、Sophos等等。(盡管我沒有把這些作為安全漏洞,但是 DKOM 不在討論范圍)(譯者注:DKOM,全稱是 Direct kernel object manipulation)。
參考資料

