目錄
前言
一、介紹用戶賬戶控制(UAC)
1.1 什么是 UAC ?
2.2 UAC 運行機制的概述
2.3 分析 UAC?提權參數
二、?NdrAsyncServerCall 函數的分析
2.1 函數聲明的解析
2.2 對 Winlogon 的逆向
2.3 對 rpcrt4 的靜態分析
2.4 對 rpcrt4 的動態調試
2.5 分析可行的突破點
三、通過 Detours 掛鉤實現
3.1 Buffer 參數解析代碼
3.2 RpcServerTestCancel 熱補丁代碼
3.3 更多實例代碼未來補充
四、總結
參考文獻
本文出處鏈接:[https://blog.csdn.net/qq_59075481/article/details/135543495]。
前言
本系列包含介紹 Winlogon Message Rpc 的多篇文章,從多個方面詳細分析利用 WMsg-RPC 進行熱鍵屏蔽的方法。上一篇文章(傳送門)側重于修改 Buffer 參數的前序字節,達到攔截 Winlogon 調用,屏蔽熱鍵的目的。接下來,我們將進一步分析??Ndr(64)AsyncServerCall(All) 函數的相關原理,從傳輸語法的角度對本地遠程過程調用進行攔截。
在這篇文章中,討論 winlogon.exe 進程響應應用程序請求提升管理員權限的一部分過程時所采用的 Ncalrpc 通信機制,通過掛鉤技術( Hooking )注入代碼來修改該線路上的關鍵點( Key Points),包括分支判斷條件、緩沖區、函數返回值、指針對象等,來達到對相關通信過程的攔截。該方法可以擴展到所有的 WMsg LRpc 消息處理上。為了方便,文章目前只對 Windows 10/11(截至10.0.22631.3235)?x64 系統下的機制進行研究。
[備注:這是在 x64 系統環境下的案例,x86-32 下則需要掛鉤 NdrAsyncServerCall?函數。注意系統的處理器版本。理論上本文方法適用于 Win7 及以上操作系統,但目前只測試了 Win10/11。]
關鍵詞:Ncalrpc 協議;NDR64 接口;逆向工程;熱鍵屏蔽;掛鉤注入
系列文章:
- 屏蔽系統熱鍵/關機(掛鉤 Winlogon 調用 鍵盤鉤子)
- RPC-Hook 屏蔽系統熱鍵(一)
- RPC-Hook 屏蔽系統熱鍵(二)[本文]
- Windows 攔截系統睡眠
一、介紹用戶賬戶控制(UAC)
1.1 什么是 UAC ?
用戶帳戶控制 (UAC) 是一項 Windows 安全功能,旨在保護操作系統免受未經授權的更改。當對系統的更改需要管理員級權限時,UAC 會通知用戶,從而讓用戶有機會批準或拒絕更改。
UAC 允許所有用戶使用?標準用戶賬戶?登錄到他們的計算機。使用 標準用戶令牌 啟動的進程可能會使用授予標準用戶的訪問權限來執行任務。例如,Windows 資源管理器會自動繼承 標準用戶級別 權限。任何使用 Windows 資源管理器啟動 (例如,通過打開快捷方式) 的應用程序也?使用標準用戶權限集 運行。大多數應用程序,包括操作系統附帶的應用程序,都以這種方式正常工作。
其他應用程序(例如設計時未考慮安全設置的應用程序)可能需要更多權限才能成功運行。 這些類型的應用被稱為 Legacy App。
當用戶嘗試執行需要管理員權限的操作時,UAC 會觸發同意提示。該提示通知用戶即將發生權限更改,向用戶要求獲得繼續操作的權限:
- 如果用戶批準更改,則會使用?最高可用權限?執行該操作;
- 如果用戶未批準更改,則不會執行該操作,并且將?阻止請求更改的應用程序運行;

當應用需要使用超過標準用戶權限運行時,UAC 允許用戶使用其 管理員令牌 (即擁有管理員權限)而不是默認的 標準用戶令牌 來運行應用。用戶將繼續在標準用戶安全上下文中操作,同時允許某些應用在需要時以提升的權限運行。
2.2 UAC 運行機制的概述
UAC 的提示界面默認運行在?安全桌面?下,此桌面名為 “Winlogon” 并與用戶會話隔離。在一般情況下,UAC 處于實時就緒狀態。當 UAC 激活時,用戶對默認桌面的操作控制將被暫時剝奪,系統創建全屏的界面以明確詢問用戶是否批準應用進行權限修改。
UAC 激活時是否切換到安全桌面,是否顯示全局陰影背景,提示級別等由注冊表項控制。系統提供的 “用戶賬戶控制設置” 程序只是操作這些注冊表項的可視化界面。由于歸屬的注冊表子鍵設置了訪問權限,所以依然需要管理員權限才能夠進行修改。
?UAC 提權是相對復雜的過程,已經有很多研究人員分析過它,這里我只籠統概括一些重要環節。一個 UAC 過程主要由四方完成:(1)請求權限的進程(Legacy App)或者提權代理的發起者(Proxy App);(2) AIS 服務(Application Information Service);(3) 系統登陸應用程序(Winlogon);(4)用戶實例

其中研究的相對多一些的就是 “?AIS 服務?” 以及由 “請求方?到 處理方(主要是 AIS)” 的通路。 從上面的介紹來看, UAC 處理時離不開進程通信。研究發現 UAC 的消息傳遞主要通過 LRPC (本地遠程過程調用)來完成。由 COM 途徑代理提權模式下,部分環節又可由 DCOM 處理服務傳遞。LRPC 的部分實現過程還包含命名管道通信。
UAC 運行過程中首先需要檢驗二進制文件的合法性,這依賴于二進制文件的簽名證書驗證、特征驗證(文件路徑、標記段或區塊等)以及內置的白名單。如果一個進程通過了所有自動提權檢驗,則不會彈出 UAC 窗口;否則,將根據注冊表設置的級別選擇是否彈出窗口。彈出的窗口上,驗證的發布者信息就是通過有效的文件簽名證書和根證書頒發機構信息來識別的。驗證的過程主要由 AIS 來完成,這也是 AIS 名稱為 Application Information Service (應用程序信息輔助管理服務) 的原因。
從局部來看, Winlogon 在 LRPC 消息的等待過程中扮演了中轉者的身份,同時他也是 WMsg Server 服務終結點。提權的消息首先會經過 AIS 服務,但是同時會有一份轉發給 Winlogon,用戶處理后會由 Winlogon 返回結果給 AIS 服務,AIS 服務會進行提權進程創建的后續操作。這可能與安全桌面是 Winlogon 創建的有關。在消息的響應階段,AIS 首先拉起 consent 進程。它是 GUI 處理進程,用戶看到的提示畫面就是由它負責繪制的(它們是父子進程,consent.exe 通過運行時命令參數訪問 AIS 進程的特定緩沖區上的數據來獲知需要顯示的信息)。AIS 為多個需要在同一階段提權的進程創建等待隊列,只允許一個進程進入就緒階段,并彈出提示窗口。并且由于 AIS 的處理過程需要 Winlogon 的協助,此過程中有一個或多個死鎖判定算法。例如:AIS 每隔一小段時間發送測試消息,當 Winlogon 進程在 5 分鐘內每次測試均沒有及時響應時,AIS 判斷發生了死鎖,此時自動結束 consent 進程,并回到默認桌面。
2.3 分析 UAC?提權參數
UAC 在提權時候,需要拉起的 consent 進程負責 UI 部分,啟動參數格式為:
consent.exe <AIS 服務進程 PID> <參數緩沖區總大小>?<參數緩沖區的首地址>
consent.exe 8312 372 0000015F3EA20AE0

圈起來的第一個是權限令牌。后面幾個參數分別是:第一個字符串開頭在這段內存中的偏移量,第二個字符串開頭在這段內存中的偏移量,后面的字符串組開頭在這段內存中的偏移量,以及字符串組結尾在這段內存中的偏移量。對于 exe 來說,前兩個字符串在我觀察到的情況中總保持一致,是文件路徑,因此并不能很好地區分。而最后的字符串數組是其參數列表。
對于 dll 來說,第一個字符串有可能是其“描述”,而第二個參數,在我實驗的范圍內,一直是其路徑。而字符串數組,則是引起 dll 加載的進程的參數列表。(其實就是 CreateProcessAsUserW 的參數列表)
早在幾年前,我編寫了一個解析 AIS 進程信息的工具,這是里面的一部分代碼,實現了過濾并攔截特定的進程啟動。代碼可能寫的粗糙(可讀性較差),我也暫時沒去重新寫將就著看。
BOOL IsStrictExePath() {if (IsCSChecked == 1) {// 避免多次檢查return TRUE;}else if (IsCSChecked == 2) {return FALSE;}HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE, GetCurrentProcessId()/*ProcessID*/); // 這里如果不是注入,則需要獲取 AIS 服務進程的 PIDif (hProcess == NULL)return TRUE;TCHAR* pszProcessCmd = GetProcessCommandLine(hProcess);if (pszProcessCmd[0] == L'\0') {return TRUE;}else {WCHAR seps[] = L" ";WCHAR* arg1 = wcstok(pszProcessCmd, seps);// 進程名 consent.exeWCHAR* arg2 = wcstok(NULL, seps);// 父進程 Appinfo 服務進程PIDWCHAR* arg3 = wcstok(NULL, seps);// 長度WCHAR* arg4 = wcstok(NULL, seps);// 要讀取的內存地址起始位置int pid, len;void* addr;int s1 = swscanf(arg2, L"%d", &pid);int s2 = swscanf(arg3, L"%d", &len);int s3 = swscanf(arg4, L"%p", &addr);if (arg3[0] != NULL) {void* Address;HANDLE OldForeign;hProcess = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);if (hProcess == NULL){MessageBoxW(GetForegroundWindow(), L"E1 ", L"CallBackMsg!!", MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);return TRUE;}char* Buffer;Buffer = reinterpret_cast<char*>(malloc(len));SIZE_T r;if (!ReadProcessMemory(hProcess, addr, Buffer, len, &r))ExitProcess(1);if (r != len){MessageBoxW(GetForegroundWindow(), L"E2 ",L"CallBackMsg!!", MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);return TRUE;}INT32 Byte1;memcpy(&Byte1, Buffer, sizeof(DWORD));INT32 Byte2;memcpy(&Byte2, Buffer + sizeof(DWORD), sizeof(DWORD));if (Byte1 != len){MessageBoxW(GetForegroundWindow(), L"E3 ",L"CallBackMsg!!", MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);return TRUE;}memcpy(&OldForeign, Buffer + 2 * sizeof(DWORD) + 2 * sizeof(void*), sizeof(void*));memcpy(&Address, Buffer + 6 * sizeof(DWORD) + 4 * sizeof(void*), sizeof(void*));DWORD BASE = 0;if (Byte2 == 0) {//ExcutableBASE = 6 * sizeof(DWORD) + 6 * sizeof(void*);}else if (Byte2 == 1) {//dllBASE = 6 * sizeof(DWORD) + 5 * sizeof(void*);}else {//Not UnderstoodINT64 t = len;memcpy(&t, Buffer + 6 * sizeof(DWORD) + 8 * sizeof(void*), sizeof(void*));if (t < len && t>0)BASE = 6 * sizeof(DWORD) + 5 * sizeof(void*);memcpy(&t, Buffer + 6 * sizeof(DWORD) + 9 * sizeof(void*), 8);if (t < len && t>0)BASE = 6 * sizeof(DWORD) + 6 * sizeof(void*);}LONG DescriptionAddr;memcpy(&DescriptionAddr, Buffer + BASE, sizeof(void*));LONG FilePathAddr;memcpy(&FilePathAddr, Buffer + BASE + sizeof(void*), sizeof(void*));LONG ParamsAddr;memcpy(&ParamsAddr, Buffer + BASE + 2 * sizeof(void*), sizeof(void*));memcpy(&ParamsAddr, Buffer + BASE + 3 * sizeof(void*), sizeof(void*));wchar_t* Description = reinterpret_cast<wchar_t*>(malloc(FilePathAddr - DescriptionAddr));if (Description != NULL) {memcpy(Description, Buffer + DescriptionAddr, FilePathAddr - DescriptionAddr);if (Description[0] == L'\"') {for (int i = 0; i < wcslen(Description) - 1; i++) {Description[i] = Description[i + 1];}Description[wcslen(Description) - 1] = L'\0';}}else{MessageBoxW(GetForegroundWindow(),L"E4 ",L"CallBackMsg!!", MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);return TRUE;}wchar_t* PathName = reinterpret_cast<wchar_t*>(malloc(static_cast<size_t>(ParamsAddr) - FilePathAddr));memcpy(PathName, Buffer + FilePathAddr, ParamsAddr - FilePathAddr);if (PathName != NULL) {// 中文文本std::string curLocale = setlocale(LC_ALL, "chs"); // curLocale = "C";setlocale(LC_ALL, curLocale.c_str());std::wstring RetStr;BaseFlow::Attribute::GetFileDescription(Description, RetStr);// 忽略大小寫查找if (StrStrIW(RetStr.c_str(), L"Terminal")|| StrStrIW(RetStr.c_str(), L"PowerShell")|| StrStrIW(RetStr.c_str(), L"Command")|| StrStrIW(PathName, L"cmd")|| StrStrIW(PathName, L"WindowsTerminal")|| StrStrIW(PathName, L"OpenConsole")|| StrStrIW(PathName, L"powershell")) {TCHAR szInfo[1024] = { 0 };PTSTR lpInfo = szInfo;time_t nowtime;struct tm pt[40];time(&nowtime);localtime_s(pt, &nowtime);// TODO: 造成卡頓的原因可能就是彈窗處理不應該放在這里!!!!//if (RetStr.c_str()[0] == L'\0')//wsprintf(lpInfo, //L"[Time %d-%d-%d:%02d:%02d:%02d]\n以下程序請求提升權限: [ %s ] \n執行參數: [%s]。\n是否要跳轉到 UAC 界面?\n",//1900 + pt->tm_year, 1 + pt->tm_mon, pt->tm_mday,//pt->tm_hour, pt->tm_min, pt->tm_sec, realFileName, PathName);//elsewsprintf(lpInfo, L"[Time %d-%d-%d:%02d:%02d:%02d]\n以下程序請求提升權限: [ %s ] \n執行參數: [%s]。\n是否要跳轉到 UAC 界面?\n",1900 + pt->tm_year, 1 + pt->tm_mon, pt->tm_mday,pt->tm_hour, pt->tm_min, pt->tm_sec, RetStr.c_str(), PathName);int UserMode = NULL;UserMode = MessageBoxW(NULL, szInfo, L"UAC 攔截器", MB_YESNO | MB_ICONINFORMATION | MB_TASKMODAL);if (UserMode != IDYES) {IsCSChecked = 1;return TRUE;}else {IsCSChecked = 2;return FALSE;}}else {IsCSChecked = 2;return FALSE;}}elsereturn TRUE;}}return TRUE;
}
效果如圖所示:

但是,實話實說,這種方法用于攔截啟動并不靠譜,正確的做法是攔截?CreateProcessAsUser 或者?CreateProcessInternal ,具體可以看文章:https://blog.csdn.net/qq_59075481/article/details/128814911。
其實,解析該參數可以用于修改 consent.exe 我們可以自己構建一個 consent.exe,來美化提升管理員界面。但是這需要更多的代碼邏輯,不過已經有人實現了相關項目:https://github.com/6ziv/CustomUAC。

其實這里的提權參數還可以從其他角度細致地去分析和理解,已經有佬完成了相關工作,見文章:https://www.anquanke.com/post/id/231403。??Winlogon 和 AIS 進程之間的通信通過 LRPC 來完成,調用方進程和它們之間一般也是 LRPC。在這個過程中,有一個重要的函數 Ndr(64)AsyncServerCall(All)?[x86?下是 NdrAsyncServerCall,x64 下是 Ndr64AsyncServerCallAll],作為服務端接收消息的關鍵步驟;而相對應的,客戶端調用 Ndr(64)AsyncClientCall 發送消息。

這個系列那位作者連續寫了三篇文章,都是干貨滿滿,有興趣可以去閱讀閱讀。
當開始分析這兩個函數時,你會發現它并沒有在 MSDN 上文檔化解釋。這些函數不被直接使用,往往是被一些上級的 API 調用,但是這些較為底層的函數被作為 rpcrt4.dll 的導出函數而允許我們輕松訪問。作為 MS-RPC 中關鍵過程的封裝函數,我們的方法將圍繞著它來展開,無論是在第一篇還是這一篇中。(上面安全客博客作者是分析客戶端代碼的,沒有涉及到 Winlogon 這邊,本文將側重于服務端 Winlogon 這邊的分析)
二、?NdrAsyncServerCall 函數的分析
盡管 MS 刻意隱瞞這類函數的聲明和作用,但結合一些逆向工作可以進一步分析出這類函數的原型。隨著一些漏洞利用手法關注于 MS-RPC,一些實現細節逐漸被研究者挖掘出來。經典的如 CVE-2021-26411 遠程代碼執行漏洞,它是一個基于 IE 堆棧緩沖區 UAF(Use After Free) 和 RPC 的 CFG 繞過漏洞。漏洞主要利用了存在于 IE 中的一個?UAF,通過覆蓋 NdrServerCall2 在 CFGBitmap 中的合法指針來繞過 CFG 檢測,從而遠程執行任意代碼(比如使用 LoadLibrary 加載 payload)。這個漏洞涉及到一個?API :NdrServerCall2 函數。它傳遞了與 Ndr(64)AsyncServerCall(All) 類似的指針 ——?pRpcMsg 指向 RPC_MESSAGE 結構,即 RPC 消息結構體。研究者對?RPC_MESSAGE 結構的分析對于本文來說是相當有用的。
2.1 函數聲明的解析
?Ndr64AsyncServerCallAll?函數用于服務端接受 RPC 消息,這個函數在?MSDN?上找不到有用的說明。它只有一個形參為指向?PRC_MESSAGE?結構體的指針,但是?PRC_MESSAGE?結構體的信息文檔中解釋的非常含糊、混亂。關于結構結構體的定義和參數解釋在我之前的多篇文章中也有給出過,但并沒有詳細分析該如何使用。為了便于閱讀本文,下面將再次給出這部分官方文檔缺失的內容:
?RPC_MESSAGE 結構體
定義
typedef struct _RPC_MESSAGE
{LRPC_BINDING_HANDLE Handle;unsigned long DataRepresentation;void __RPC_FAR* Buffer;unsigned int BufferLength;unsigned int ProcNum;LPRPC_SYNTAX_IDENTIFIER TransferSyntax;void __RPC_FAR* RpcInterfaceInformation;void __RPC_FAR* ReservedForRuntime;RPC_MGR_EPV __RPC_FAR* ManagerEpv;void __RPC_FAR* ImportContext;unsigned long RpcFlags;
} RPC_MESSAGE, __RPC_FAR* PRPC_MESSAGE;
參數
- Handle
類型:RPC_BINDING_HANDLE?
服務器綁定句柄,服務器綁定句柄包含客戶端與特定服務器建立關系所需的信息。是一個內存地址,指向包含 RPC 運行時庫用于訪問綁定服務器信息的數據結構。該結構為 遠程過程服務器調用(RPC_SCALL) ,是包含虛函數指針的列表。
- DataRepresentation
類型:unsigned long
NDR 規范定義的網絡緩沖區的數據表示形式。默認值為 0x10,如果值不為 0x10,則?NdrConvert2 被調用。
- Buffer
類型:void *
存儲函數調用中使用的參數的緩沖區(部分參數的序列化存儲結構)。
- BufferLength
類型:unsigned int
Buffer?參數指向的緩沖區的大小(以字節為單位)。?WMsg Server 處理消息包時的值一般為 12,調用完成時值被修改為 0。不同的 RPC 傳遞的方法不同,所需要的參數個數也不一樣,緩沖區的大小就不同。Buffer 指向的緩沖區嚴格按照 4 字節對齊。
- ProcNum
類型:unsigned int
即 Procedure Number,過程號的意思。ProcNum 是指定要調用的過程的數字或索引。每個接口可能有多個過程(函數),而 ProcNum 用于確定調用哪個具體的過程(函數)。
每個遠程過程都有一個唯一的過程號,通過這個過程號,服務器可以確定客戶端希望調用的是哪個過程(函數)。
調用過程的語法中,有多個函數在函數指針列表(DispatchTable)中,這是類似于數組的數據結構,使用?ProcNum 即可作為索引,獲取需要的函數的指針。
- TransferSyntax
類型:LPRPC_SYNTAX_IDENTIFIER
指向將寫入用于編碼數據的接口標識(唯一標識稱作?UUID )的地址的指針。?pInterfaceId?由接口通用唯一標識符?UUID?和版本號組成。(對于測試人員,與 Rpc 有關的信息可以使用 RpcView 等工具獲取)
進一步解釋:這個參數在 RPC 調用中告訴服務器要執行哪個遠程接口的例程。通過查看該接口的信息,服務器可以了解要調用的接口的類型和版本等信息。同時,客戶端也可以通過已知的 UUID 配對需要連接的服務器,這就是唯一標識的作用。
- RpcInterfaceInformation
類型:void *
對于服務器端的非對象 RPC 接口,它指向 RPC 服務器接口結構。 在客戶端,它指向 RPC 客戶端接口結構。 對于對象接口,它為 NULL。
進一步解釋:在服務器端,RpcInterfaceInformation 指針指向 RPC_SERVER_INTERFACE 結構,該結構保存了服務端程序接口信息(后文將進一步分析該結構);在客戶端,則指向?RPC_CLIENT_INTERFACE 結構;對于對象接口,則默認為 NULL。
- ReservedForRuntime
類型:void *
保留用于運行時傳遞額外的擴展數據。(推測為指向結構體的指針,作用尚不明確)
- ManagerEpv
類型:RPC_MGR_EPV
管理器入口點向量 (EPV) 是保存函數指針的數組。數組包含指向 IDL 文件中指定的函數實現的指針。數組中的元素數設置為 IDL 文件中指定的函數數。按照約定,包含接口和類型庫定義的文件稱為 IDL 文件,其文件擴展名為 .idl。接口由關鍵字 (keyword)?接口標識。
進一步解釋:ManagerEpv 是一個指向管理器(Manager)的入口點向量的指針。管理器是客戶端和服務器之間通信的中介,負責將調用分派到相應的例程。
入口點向量是一個函數指針數組,其中包含管理器實現的各個例程(函數)的入口點。這個向量由 MIDL 編譯器生成,它包含有關如何調用管理器函數的信息。
但是在 Vista 及以上系統中,(敲黑板)一般不采用該字段。當 ManagerEpv 設置為 NULL 時使用?RpcInterfaceInformation 中的一個成員作為實際的?ManagerEpv。
- ImportContext
類型:void *
推測為指向?RPC_IMPORT_CONTEXT_P 結構的指針。用于在客戶端和服務器之間傳遞上下文信息,其中包括與名稱服務相關的上下文、客戶端提議的綁定句柄以及一個綁定向量,其中包含了多個綁定句柄。該字段在 Vista 及更高版本系統上似乎不再支持(?),始終設置為 NULL。
- RpcFlags
類型:unsigned long
?RPC 調用的過程狀態碼。返回傳輸語法傳遞過程的狀態信息。?Async RPC (異步 RPC) 過程使用 Buffer 傳遞字符串數據時,如果信息傳輸成功,則返回的標志位應該是 RPC_BUFFER_COMPLETE |?RPC_BUFFER_ASYNC (36864) 的組合。
狀態碼可以是下表所列舉的標志位的組合:
RPC_FLAGS_VALID_BIT | 0x00008000 |
RPC_CONTEXT_HANDLE_DEFAULT_GUARD | ((void*)0xfffff00d) |
RPC_CONTEXT_HANDLE_DEFAULT_FLAGS | 0x00000000 |
RPC_CONTEXT_HANDLE_FLAGS | 0x30000000 |
RPC_CONTEXT_HANDLE_SERIALIZE | 0x10000000 |
RPC_CONTEXT_HANDLE_DONT_SERIALIZE | 0x20000000 |
RPC_TYPE_STRICT_CONTEXT_HANDLE | 0x40000000 |
RPC_NCA_FLAGS_DEFAULT | 0x00000000 |
RPC_NCA_FLAGS_IDEMPOTENT | 0x00000001 |
RPC_NCA_FLAGS_BROADCAST | 0x00000002 |
RPC_NCA_FLAGS_MAYBE | 0x00000004 |
RPC_BUFFER_COMPLETE | 0x00001000 |
RPC_BUFFER_PARTIAL | 0x00002000 |
RPC_BUFFER_EXTRA | 0x00004000 |
RPC_BUFFER_ASYNC | 0x00008000 |
RPC_BUFFER_NONOTIFY | 0x00010000 |
RPCFLG_MESSAGE | 0x01000000 |
RPCFLG_HAS_MULTI_SYNTAXES | 0x02000000 |
RPCFLG_HAS_CALLBACK | 0x04000000 |
RPCFLG_AUTO_COMPLETE | 0x08000000 |
RPCFLG_LOCAL_CALL | 0x10000000 |
RPCFLG_INPUT_SYNCHRONOUS | 0x20000000 |
RPCFLG_ASYNCHRONOUS | 0x40000000 |
RPCFLG_NON_NDR | 0x80000000 |
以上為對 RPC_MESSAGE 結構體各個成員的簡單解釋。
根據相關研究,RPC_MESSAGE 結構體的重要成員已經在下圖中標記出:

注意:圖中的偏移量是在 x86 下獲得的,雖然在 x64 下結構的成員一樣,但因為對齊原因偏移量不同(后文講解如何計算實際的偏移量)。
RpcInterfaceInformation 是指向?RPC_SERVER_INTERFACE 結構體的指針。該結構體是我們下面將研究的一個重點。
2.2 對 Winlogon 的逆向
這一部分是一個很好的開始,他是本文一切研究的起點。在我的另外一篇文章中也講過:
“屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等熱鍵(二)”。現在我將前后工作聯系起來整理一下,以便于讀者對相關機制有一個清晰的認識。
相信讀過本系列第一篇文章[https://blog.csdn.net/qq_59075481/article/details/135415028]的應該知道,在 Ndr64AsyncServerCallAll?函數響應的過程中,內部會調用 RpcServerTestCancel 和 RpcAsyncCompleteCall 兩個導出函數。如下圖所示:

這兩個函數很關鍵哦!服務器調用?RpcServerTestCancel?來查明客戶端是否已請求取消未完成的調用。如果客戶端取消了遠程過程調用,則該函數返回 RPC_S_OK;否則,返回非 0 值。服務器以及客戶端調用?RpcAsyncCompleteCall?函數以完成異步遠程過程調用。服務器通過該調用過程?答復?指向?包含需要發送到客戶端的返回值?的?緩沖區。
通過 IDA 查詢導入表可以看到 WMsg 接口的消息回調會調用?RpcServerTestCancel 函數:

而這些函數是 WMsg 接口消息回調的封裝,比如 I_WMsgSendMessage 內部測試連接并觸發間接調用。實際工作的消息回調函數為 WMsgMessageHandler 。對于實際的消息回調函數,在 Winlogon 初始化時調用 WMsgClntInitialize 注冊它們的實例:
int64_t __fastcall WMsgClntInitialize(WLSM_GLOBAL_CONTEXT *WSMGlobalContext, BOOL bEnableKServer) // int IsPresentKey
{int64_t WMsgHandlerList[9]; // [rsp+30h] [rbp-48h] BYREFmemset(WMsgHandlerList, 0, 0x40);if ( !IsPresentKey )return StartWMsgServer();WMsgHandlerList[0] = (int64_t)WMsgMessageHandler;WMsgHandlerList[1] = (int64_t)WMsgKMessageHandler;WMsgHandlerList[5] = (int64_t)WMsgNotifyHandler;WMsgHandlerList[2] = (int64_t)WMsgPSPHandler;WMsgHandlerList[3] = (int64_t)WMsgReconnectionUpdateHandler;WMsgHandlerList[4] = (int64_t)WMsgGetSwitchUserLogonInfoHandler;RegisterWMsgServer(WMsgHandlerList);return StartWMsgKServer(*WSMGlobalContext + 0xCC);
}
在繼續分析前,我需要先談談 Winlogon 是怎么實現快捷鍵響應的。
WMsgClntInitialize 函數調用了兩個比較重要的函數:StartWMsgServer 和 StartWMsgKServer 函數。
具體執行行為通過參數二來決定,參數二其實是一個 BOOL 類型,表明了是否要執行 WMsg Key Server 初始化。
在 Winlogon 啟動的過程中,會調用兩次?WMsgClntInitialize 函數。第一次調用參數二為 TRUE ,注冊 KServer。

第二次調用在 StartLogonUI(啟動 LogonUI.exe 進程,也就是登陸 UI 進程)和?WlStateMachineInitialize(狀態機初始化)之后,參數為 FALSE,表明注冊 Server。

?StartWMsgServer 和 StartWMsgKServer 都是啟動 RPC 服務器并進行一些初始化工作。
?StartWMsgServer 偽代碼:
__int64 StartWMsgServer()
{unsigned int v0; // ebxCUser *v1; // rcx__int64 v2; // rdxwchar_t pszDest[40]; // [rsp+40h] [rbp-68h] BYREFv0 = RpcServerRegisterIfEx(&unk_1400A3190, 0i64, 0i64, 0x28u, 0x4D2u, (RPC_IF_CALLBACK_FN *)WmsgRpcSecurityCallback);if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 21i64;goto LABEL_13;}}else{dword_1400D0658 = 1;v0 = RpcServerInqBindings(&BindingVector);if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 22i64;goto LABEL_13;}}else{StringCchPrintfW(pszDest, 0x25ui64, L"b08669ee-8cb5-43a5-a017-84fe%08X", NtCurrentPeb()->SessionId);v0 = UuidFromStringW(pszDest, &Uuid);if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 23i64;goto LABEL_13;}}else{UuidVector.Count = 1;UuidVector.Uuid[0] = &Uuid;v0 = WaitForDesiredService(L"RPCSS");if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 24i64;goto LABEL_13;}}else{v0 = RpcEpRegisterW(&unk_1400A3190, BindingVector, &UuidVector, 0i64);if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 25i64;goto LABEL_13;}}else{dword_1400D06B0 = 1;v0 = RpcServerListen(1u, 0x4D2u, 1u);if ( v0 == 1713 )v0 = 0;if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 26i64;
LABEL_13:WPP_SF_d(*((_QWORD *)v1 + 2), v2, &WPP_809920f00406303c4ef054ac00db20a5_Traceguids, v0);}}}}}}}if ( v0 )StopWMsgServer();return v0;
}
?StartWMsgKServer 偽代碼:
__int64 __fastcall StartWMsgKServer(LUID *a1)
{int LocallyUniqueId; // eaxULONG v3; // ebxCUser *v4; // rcx__int64 v5; // rdxwchar_t pszDest[152]; // [rsp+40h] [rbp-148h] BYREFLocallyUniqueId = NtAllocateLocallyUniqueId(a1);if ( LocallyUniqueId < 0 ){v3 = RtlNtStatusToDosError(LocallyUniqueId);}else if ( StringCchPrintfW(pszDest,0x91ui64,L"WMsgKRpc%X%X%X",(unsigned int)a1->HighPart,a1->LowPart,NtCurrentPeb()->SessionId) < 0 ){v3 = 13;}else{v3 = RpcServerUseProtseqEpW((RPC_WSTR)L"ncalrpc", 0xAu, pszDest, 0i64);if ( v3 ){v4 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v5 = 18i64;goto LABEL_12;}}else{v3 = RpcServerRegisterIfEx(&hWmsgkSrvIfHandle, 0i64, 0i64, 0x28u, 0x4D2u, WmsgkRpcSecurityCallback);// unk_1400A3250if ( v3 ){v4 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v5 = 19i64;goto LABEL_12;}}else{dword_1400D06C0 = 1;v3 = RpcServerListen(1u, 0x4D2u, 1u);if ( v3 == 1713 )v3 = 0;if ( v3 ){v4 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v5 = 20i64;
LABEL_12:WPP_SF_d(*((_QWORD *)v4 + 2), v5, &WPP_809920f00406303c4ef054ac00db20a5_Traceguids, v3);}}}}}if ( v3 )StopWMsgServer();return v3;
}
他們使用 ncalrpc 協議工作,關于協議和終結點我簡單解釋一下。
在 RPC(Remote Procedure Call,遠程過程調用)中,協議序列和終結點是兩個關鍵概念,它們定義了客戶端與服務器之間的通信方式。
(1)協議序列(Protocol Sequence)
協議序列定義了通信的協議和傳輸方式,即 RPC 通信時使用的底層網絡協議。常見的協議序列包括:
- ncacn_ip_tcp:基于 TCP/IP 協議的網絡連接。
- ncacn_np:基于命名管道的通信。
- ncalrpc:本地過程調用(Local Procedure Call),用于同一臺機器上的進程間通信。
- ncacn_http:基于 HTTP 協議的通信。
協議序列決定了數據如何在網絡上傳輸,因此選擇合適的協議序列對 RPC 應用程序的性能和安全性非常重要。
(2)終結點(Endpoint)
終結點(也稱為端點)是協議序列的具體實現,它定義了服務器的網絡地址或命名管道名稱,使得客戶端能夠找到并連接到服務器。終結點因協議序列的不同而不同,例如:
- 對于 ncacn_ip_tcp,終結點通常是 IP 地址和端口號,例如 192.168.1.1:135。
- 對于 ncacn_np,終結點是命名管道的名稱,例如 \\.\pipe\mypipe。
- 對于 ncalrpc,終結點是一個唯一的字符串名稱,用于標識本地進程間通信。
下面談談兩個函數的區別:
1. 函數簽名:
- StartWMsgKServer:包含一個 LUID *lpLuid 參數,用于傳遞一個本地唯一標識符。
- StartWMsgServer:不包含參數。
2. 本地唯一標識符:
- StartWMsgKServer:調用 NtAllocateLocallyUniqueId 為 lpLuid 分配一個本地唯一標識符,并基于此唯一標識符創建 pszDest 字符串 (WMsgKRpc%X%X%X)。
- StartWMsgServer:不涉及本地唯一標識符的分配。
3. 字符串格式化:
- StartWMsgKServer:使用 StringCchPrintfW 函數格式化字符串 WMsgKRpc%X%X%X
- StartWMsgServer:使用 StringCchPrintfW 函數格式化字符串 b08669ee-8cb5-43a5-a017-84fe%08X。
4. RPC 協議序列和終結點:
- StartWMsgKServer:使用 RpcServerUseProtseqEpW 函數設置 ncalrpc 協議序列和 pszDest 終結點。
- StartWMsgServer:涉及 RpcServerInqBindings 和 RpcEpRegisterW 函數,用于查詢綁定信息和注冊終結點。
5. 日志記錄:
- 兩個函數在錯誤處理部分都有日志記錄邏輯,但使用的事件 ID 不同。
6. RPC 接口注冊:
RPC_IF_HANDLE 為 RPC 調用中會使用到的接口句柄,其本質為 RpcInterfaceInformation,也就是 RPC 接口信息句柄,在注冊 RPC 調用的時候會用到。這里的?hWmsgkSrvIfHandle 和?hWmsgSrvIfHandle 兩個全局變量?RPC_IF_HANDLE 就是 RPC 接口信息句柄。

- StartWMsgKServer:注冊接口 hWmsgkSrvIfHandle,并使用 WmsgkRpcSecurityCallback 回調函數。
- StartWMsgServer:注冊接口 hWmsgSrvIfHandle,并使用 WmsgRpcSecurityCallback 回調函數。
7. 其他差異:
- StartWMsgServer 包含更多的錯誤處理邏輯和特定的 UUID 操作。
- 在 StartWMsgServer 函數中調用 WaitForDesiredService(L"RPCSS") 是為了確保遠程過程調用?(Remote Procedure call Service) 服務 (RPCSS 服務) 已經啟動并處于可用狀態。這一步是必要的,因為在啟動和注冊 RPC 服務器時,依賴于 RPCSS 服務來處理和管理。
RPCSS 服務的角色
RPCSS服務是?COM 和 DCOM 服務器的服務控制管理器。它執行 COM 和 DCOM 服務器的對象激活請求、對象導出程序解析和分布式垃圾回收。
具體來說,RpcSs 服務主要提供以下功能:
- 進程間通信 (Inter-process Communication, IPC):允許一個進程調用另一個進程中的函數,就像調用本地函數一樣。這在分布式系統中尤為重要,因為它使得不同機器上的進程也能夠通信和協作。
- 客戶端和服務器通信:RPC 服務可以用來實現客戶端和服務器之間的通信,支持分布式計算,增強應用程序的靈活性和擴展性。
- 安全和身份驗證:RPC 服務包含安全特性,確保通信的安全性和數據的完整性,并提供身份驗證機制。
WaitForDesiredService 函數的作用
WaitForDesiredService 函數的作用是確保指定的服務(在這里是 RPCSS 服務)已經啟動并準備好接受請求。函數包含以下邏輯:
- 檢查指定服務的當前狀態。
- 如果服務尚未啟動,則等待服務啟動。
- 如果服務在一段合理的時間內未啟動,則返回錯誤。
總的來說,StartWMsgKServer 主要是針對帶有本地唯一標識符 (LUID) 的 RPC 服務器的啟動,使用了 ncalrpc 協議。StartWMsgServer 則使用 UUID 注冊 RPC 服務器,涉及更多的綁定查詢和 UUID 操作。
在?模擬發送 Ctrl+Alt+Del 快捷鍵 -CSDN博客 一文中,我曾解釋過 WMsg 客戶端部分的代碼,有興趣的可以結合一起看看。
談完了 WMsgClntInitialize 我們還需要知道 winlogon 具體在什么階段注冊 Rpc 服務器和處理消息的:?
?Winlogon 首先會在主線程的 WinMain 函數中調用?WlStateMachineInitialize、WMsgClntInitialize 初始化全部的回調函數和事務處理線程(注意 RPC 信息的響應處理是在新線程中完成的,不是主線程)。

?WlStateMachineInitialize 實際上是對?StateMachineCreate 的封裝,該函數創建并初始化狀態機對象。首先,為狀態機和內部結構分配內存。然后,通過調用創建并初始化信號管理器SignalManager。隨后,通過設置內部狀態和結構完成初始化。如果任何步驟失敗,請清理并返回錯誤代碼。如果成功,則設置指向狀態機的全局指針并返回 0。

SignalManagerCreate 負責創建并初始化信號管理器對象。首先,為關鍵部分分配內存并初始化臨界區。然后,創建一個事件對象,并為內部結構分配額外的內存。隨后,初始化這些內部結構。如果任何步驟失敗,請清理并返回錯誤代碼;如果成功,則設置輸出指針并返回 0。

隨后進入臨界區并調用 StateMachineRun 開始監聽事件。StateMachineRun 函數內部實際通過?SignalManagerWaitForSignal 循環等待同步對象(這里是設置的事件對象)。
[Winlogon 進程通過 SignalManagerWaitForSignal 等函數循環等待系統快捷鍵,而關鍵的消息回調是通過 RPC 完成的]

在這里的代碼中,可以看到混合使用了臨界區(Critical Section)和 WaitForSingleObject 等函數。這種混合使用的情況通常是為了實現更復雜的同步機制,保證線程安全和同步的靈活性。

在臨界區內檢查特定條件,并執行相應的邏輯。這里主要是通過循環檢查特定條件是否滿足,如果滿足則執行相應的操作。如果特定條件滿足并進行了操作,則退出臨界區并返回結果。
如果沒有找到滿足條件的情況,則退出臨界區并使用WaitForSingleObject等待一個同步對象(這里可能是一個事件或信號量)變為有信號狀態。如果等待失敗,則獲取錯誤碼并睡眠一段時間后重試。
例如在提升管理員權限的 UAC 會話中該機制就被用于檢查是否發生死鎖或者超時未響應,以避免忙等干等的情況。
談完了初始化管理器的過程,當然還需要談處理的過程:
為了便于理解接收消息的線程的處理過程,以接收到熱鍵消息(有 K 的函數)為例進行講解。處理流程可以用下面的簡化版理論來總結。
備注:提權等操作過程類似,只不過通過的函數沒有 K 字樣,在第一篇說過兩個函數的功能區別。整理經 WinDbg 和 IDA Pro 的逆向分析結果,并參考了 heiheiabcd 的工作。
首先,客戶端的請求通過?Ndr64AsyncClientCall 最終傳遞進入服務例程,服務例程通過調用?Ndr64AsyncServerCallAll 來完成所有響應過程。
而該過程是通過 rpcrt4!Invoke 函數來派發的間接調用鏈(了解過 MS-RPC 的應該都知道 Invoke )。

隨后,進入關鍵調用過程:

所有任務都通過 I_WMsgkSendMessage 實現,因為此時需要調用的遠程過程函數的參數已經全部在堆棧或寄存器上了。我們將過程劃分為三個階段:(1)測試遠程過程,驗證客戶端信息來確定是否取消后續的調用;(2)驗證通過后調用 WMsgKMessageHandler 也就是正真的事務處理例程,在該例程中通過?WlStateMachineSetSignal 設置事件信號,該事件會通知主線程;(3)I_WMsgkSendMessage 進行最后的處理,通過?RpcAsyncCompleteCall 通知客戶端完成請求。
總的來說,整個多進程跨線程的異步機制為:客戶端(調用方進程)請求某個操作時,服務器(Winlogon)通過特殊的事務處理線程接收消息并驗證身份,然后通過設置事件(SetEvent)釋放正在等待的 WinMain 主線程,最后事務線程通知客戶端請求的操作已經完成,客戶端(如果有)等待到消息后類似服務器,釋放相關執行過程的線程(阻滯/非阻滯過程)。
所以,要想攔截快捷鍵等,一個切入點就是從?I_WMsgSendMessage 以及?I_WMsgkSendMessage 函數下手。下面以對?I_WMsgSendMessage 的逆向為例,它間接調用?WMsgMessageHandler 等函數。(前面寫過的文章則以 WMsgKMessageHandler 和 WMsgMessageHandler 兩個具體的消息處理過程進行攔截,其實效果差不多)
?Winlogon 進程通過 SignalManagerWaitForSignal?函數循環等待系統快捷鍵,關鍵的消息回調是通過 RPC 完成的。
注:下圖中的指針實際指向的函數通過 WinDbg 分析獲得。

首先我們觀察到函數內檢測了客戶端是否取消了調用:

如果返回值為 0 也就是 RPC_S_OK 則表明客戶端已經請求取消遠程調用。微軟文檔說服務器此時可以選擇是中斷調用還是繼續調用,只是客戶端不管它返回了而已。在目前的 winlogon 處理機制下,是會立即中止調用的,因為我們觀察到了關鍵的函數的調用 RpcAsyncAbortCall(pAsync, RPC_S_CALL_CANCELLED),這指示了調用將被服務器中止。
所以,一種思路是依賴欺騙?TestCancel 檢測來繞過調用。

下圖展示了使用 WinDbgX 修改 RpcServerTestCancel 的返回值后的效果,可以看到在請求以管理員身份啟動程序時遠程過程被取消:

所以第一個掛鉤點就是掛鉤?RpcServerTestCancel 并在必要時候返回?RPC_S_OK。例如同時掛鉤 Ndr64AsyncServerCallAll?函數,并根據第一篇文章解析的 Buffer 參數來判斷當前正在進行的操作,根據操作選擇是否要欺騙 WMsg 的 TestCancel 檢測。

接下來我們觀察到了對堆棧上的一個參數的按位校驗,這里應該是判斷一個句柄是否有效的的校驗碼:

如果句柄檢測后的返回值是非 0 值(a5 != 0),則按位校驗會不通過(說明句柄是無效句柄),此時終止調用(走默認執行方法)。
所以,第二個思路是修改 a5 的值為一個不是 0 的值,導致執行默認方法(DefaultWMsgMessageHandler),進程創建失敗(如果是提權,則會提示文件系統錯誤)。
為什么會失敗呢?
因為默認流程不執行任何消息處理操作,只 out 錯誤狀態以及返回完成(return 1):
__int64 __fastcall DefaultWMsgMessageHandler(__int64 a1, __int64 a2, __int64 a3, _DWORD *a4)
{*a4 = 0xC00000AF;return 1i64;
}
RpcServerTestCancel 函數是在 Rpcrt4 里面實現的,我們簡單看一下它的逆向代碼(注釋已經十分詳細了,我就不再詳細展開了):
RPC_STATUS __stdcall RpcServerTestCancel(RPC_BINDING_HANDLE BindingHandle)
{int IsInvalid; // eaxRPC_BINDING_HANDLE *ThreadPointer; // raxTHREAD *lpSourceWrapThread; // raxint dwCreateEvent; // [rsp+30h] [rbp+8h] BYREF// 綁定分為靜態和動態,取決于傳入參數是否為空,為空則動態綁定當前線程if ( !BindingHandle ){ThreadPointer = (RPC_BINDING_HANDLE *)RpcpGetThreadPointer();// LPC/ALPC協議頭。此標頭具有 ClientId 字段,// 該字段同時具有發件人PID和TID。// 在接收到ALPC請求后,服務器進程中的RPC運行時將這些值保存在RPC_BINDING_HANDLE對象中,// 從中可以檢索到這些值。// 向函數傳遞空句柄意味著使用當前線程的活動綁定,// 在這種情況下,這些API通過ReservedForNtRpc字段從當前線程的// TEB、IIRC獲取RPC_BINDING_HANDLE指針。// if ( ThreadPointer ) // 如果在 TEB 的 ClientID.UniqueThread 里面找到了已經傳遞的綁定句柄,// 則直接調用測試函數,而不重復創建動態綁定的句柄。goto LABEL_12;dwCreateEvent = 0;lpSourceWrapThread = (THREAD *)AllocWrapper(0xE8ui64);// 在堆上分配內存if ( lpSourceWrapThread ){ThreadPointer = (RPC_BINDING_HANDLE *)THREAD::THREAD(lpSourceWrapThread, &dwCreateEvent);// 創建綁定線程的事件對象(同步對象)if ( ThreadPointer ) // 對線程的動態綁定到 ReservedForNtRpc 結束后,清理過程中用到的事件對象{if ( !dwCreateEvent ) // 如果創建失敗,進一步檢查返回的 THREAD 對象的指針是否為空goto LABEL_11;THREAD::`scalar deleting destructor'((THREAD *)ThreadPointer);// 析構 THREAD 對象,釋放內存}}ThreadPointer = 0i64;
LABEL_11:if ( !ThreadPointer ) // 創建失敗并且對象指針為空指針,則返回錯誤代碼 1725return 1725;
LABEL_12:BindingHandle = ThreadPointer[4]; // 獲取綁定句柄上的函數指針表if ( BindingHandle ) // 測試客戶端是否取消了遠程過程調用。// 如果客戶端沒有取消調用,返回值為 1791(RPC_S_CALL_IN_PROGRESS);// 取消調用則返回 0return (*(unsigned int (__fastcall **)(RPC_BINDING_HANDLE))(*(_QWORD *)BindingHandle + 192i64))(BindingHandle) == 0? 1791: 0; // 如果指針列表為空,說明綁定失敗,也就是客戶端沒有響應這個動態綁定的句柄,// 此時返回錯誤代碼 1725(RPC_S_NO_CALL_ACTIVE)return 1725;} // 下面是測試靜態綁定的句柄LOBYTE(IsInvalid) = GENERIC_OBJECT::InvalidHandle((GENERIC_OBJECT *)BindingHandle, 2105416);// 判斷綁定句柄是否有效if ( !IsInvalid )return (*(unsigned int (__fastcall **)(RPC_BINDING_HANDLE))(*(_QWORD *)BindingHandle + 192i64))(BindingHandle) == 0? 1791: 0;return 1702;
}
其實就是驗證 RPC 句柄是否有效以及函數指針表的完整性而已。這一部分里面很有趣,較多地使用哨兵值和有效范圍進行句柄和指針的校驗。所以,我們也可以修改傳入的句柄或者指針在堆棧上的值,以便于觸發校驗失敗,這種精心構造的代碼也可以使得調用被取消,且不容易被發現。
2.3 對 rpcrt4 的靜態分析
為了進一步解釋攔截系統快捷鍵的方法的原理,下面將圍繞一直在談的?Ndr64AsyncServerCallAll?函數來分析。RPC 相關調用比較復雜,但是我們只需要抓住關鍵流程即可找到突破口。
【分析】(基于版本為 10.0.22621.3235 的 rpcrt4.dll)
?Ndr64AsyncServerCallAll 函數是導出函數,從解析的代碼中可以看出它是 Ndr64AsyncServerWorker 的封裝。

進入? Ndr64AsyncServerWorker 可以看到這個函數很復雜。下圖是函數開頭的部分:

這里的開頭兩句反匯編語句,是很重要的。但第一次接觸的話會很難理解:
v37 = *((_QWORD *)Message->RpcInterfaceInformation + 10);
v41 = *(_QWORD **)(v37 + 8);
首先這里有個陷阱,Message->RpcInterfaceInformation 是指針地址,IDA 反匯編的指令和偽代碼寫法不一樣,偽代碼中的對數據變量地址 + 10 是要類似于數組的處理方式用數據類型乘以所參與運算的常數的,也就是說這里是(地址 + 10* 8) = (地址 +80),也就是十六進制的 ptr + 0x50。
顯然,這里的 Message 變量是指向 RPC_MESSAGE 結構的指針:

我們說過,前文給出的圖片解析的結構體是 x86 架構下的,我們現在研究的是 x64 下的結構,該怎么辦呢?
別急,我們使用一個技巧可以輕松獲取結構體成員的偏移量:
#include <stdio.h>
#include <Windows.h>
#include <rpc.h>//typedef struct _RPC_SERVER_INTERFACE
//{
// unsigned int Length;
// RPC_SYNTAX_IDENTIFIER InterfaceId;
// RPC_SYNTAX_IDENTIFIER TransferSyntax;
// PRPC_DISPATCH_TABLE DispatchTable;
// unsigned int RpcProtseqEndpointCount;
// PRPC_PROTSEQ_ENDPOINT RpcProtseqEndpoint;
// RPC_MGR_EPV __RPC_FAR* DefaultManagerEpv;
// void const __RPC_FAR* InterpreterInfo;
// unsigned int Flags;
//} RPC_SERVER_INTERFACE, __RPC_FAR* PRPC_SERVER_INTERFACE;//typedef struct _MIDL_SERVER_INFO_
//{
// PMIDL_STUB_DESC pStubDesc;
// const SERVER_ROUTINE* DispatchTable;
// PFORMAT_STRING ProcString;
// const unsigned short* FmtStringOffset;
// const STUB_THUNK* ThunkTable;
// PRPC_SYNTAX_IDENTIFIER pTransferSyntax;
// ULONG_PTR nCount;
// PMIDL_SYNTAX_INFO pSyntaxInfo;
//} MIDL_SERVER_INFO, * PMIDL_SERVER_INFO;int main()
{PRPC_SERVER_INTERFACE rpcSvcInterface = nullptr;printf("RPC_SERVER_INTERFACE.Length: 0x%I64X \n", (UINT64)&rpcSvcInterface->Length);printf("RPC_SERVER_INTERFACE.InterfaceId: 0x%I64X \n", (UINT64)&rpcSvcInterface->InterfaceId);printf("RPC_SERVER_INTERFACE.TransferSyntax: 0x%I64X \n", (UINT64)&rpcSvcInterface->TransferSyntax);printf("RPC_SERVER_INTERFACE.DispatchTable: 0x%I64X \n", (UINT64)&rpcSvcInterface->DispatchTable);printf("RPC_SERVER_INTERFACE.RpcProtseqEndpointCount: 0x%I64X \n", (UINT64)&rpcSvcInterface->RpcProtseqEndpointCount);printf("RPC_SERVER_INTERFACE.RpcProtseqEndpoint: 0x%I64X \n", (UINT64)&rpcSvcInterface->RpcProtseqEndpoint);printf("RPC_SERVER_INTERFACE.DefaultManagerEpv: 0x%I64X \n", (UINT64)&rpcSvcInterface->DefaultManagerEpv);printf("RPC_SERVER_INTERFACE.InterpreterInfo: 0x%I64X \n", (UINT64)&rpcSvcInterface->InterpreterInfo);printf("RPC_SERVER_INTERFACE.Flags: 0x%I64X \n", (UINT64)&rpcSvcInterface->Flags);printf("\n\n");PMIDL_SERVER_INFO svcIdlInfo = nullptr;printf("MIDL_SERVER_INFO.pStubDesc: 0x%I64X \n", (UINT64)&svcIdlInfo->pStubDesc);printf("MIDL_SERVER_INFO.DispatchTable: 0x%I64X \n", (UINT64)&svcIdlInfo->DispatchTable);printf("MIDL_SERVER_INFO.ProcString: 0x%I64X \n", (UINT64)&svcIdlInfo->ProcString);printf("MIDL_SERVER_INFO.FmtStringOffset: 0x%I64X \n", (UINT64)&svcIdlInfo->FmtStringOffset);printf("MIDL_SERVER_INFO.ThunkTable: 0x%I64X \n", (UINT64)&svcIdlInfo->ThunkTable);printf("MIDL_SERVER_INFO.pTransferSyntax: 0x%I64X \n", (UINT64)&svcIdlInfo->pTransferSyntax);printf("MIDL_SERVER_INFO.nCount: 0x%I64X \n", (UINT64)&svcIdlInfo->nCount);printf("MIDL_SERVER_INFO.pSyntaxInfo: 0x%I64X \n", (UINT64)&svcIdlInfo->pSyntaxInfo);system("pause");return 0;
}
以 x64 平臺模式編譯執行程序,得到 x64 架構下結構體的成員偏移如下:

當有了偏移量后,再看匯編代碼就會容易理解里面的一些運算是什么了。例如下面圖片展示的第 48 行的代碼中 RpcInterfaceInformation + 0x50 指向的就是 RPC_SERVER_INTERFACE 結構的?InterpreterInfo 成員。

為了便于對照理解,我們把前面一小節的圖片再搬來一次:

同樣地我們通過計算得到 x64 下的 MIDL_SERVER_INFO 結構體中的成員的偏移:

匯編代碼中的 rcx + 0x28 是 RPC_MESSAGE 的?RpcInterfaceInformation 成員。這里 F5 也自動分析出來了。隨后的 rax + 0x50 是?RpcInterfaceInformation 指向的結構中的成員地址,結合上文解析的 RPC_SERVER_INTERFACE 成員偏移量可知這里的 v37 是?InterpreterInfo 成員。而?InterpreterInfo 成員指向?MIDL_SERVER_INFO 結構,再根據相對于 MIDL_SERVER_INFO 起始地址的偏移量可知?v41 就是 DispatchTable 成員。
為什么我們關注這里的偏移量計算呢?往下看你就知道了,這里的 v41 在后面被賦值給 ManagerEpv,然后根據 ManagerEpv[ProcNum] 獲取要執行的函數的指針,并由 rpcrt4!Invoke 執行函數(激活調用)。

(注釋:剛開始認識 RPC 時,我們并不是一開始就關注到?Invoke 函數,在他之前有很多參數初始化的細節。但是它是 MS-RPC 的關鍵部分,由 Invoke 向上可以輕松找到管理入口向量的解析過程)
下面我們簡單介紹一下全部的過程,如果你想深入了解也可以看文末附加的參考文獻:
服務器 / 客戶端使用名為 RPC_MESSAGE 的結構體傳遞信息,其中包含發送的消息。調用的 Ndr64AsyncServerWorker 上下文只是用于設置嵌入在 AsyncMsg 中的調用。它是在這個異步調用的生命周期中使用的。
在 Ndr64AsyncServerWorker 中,首先通過 I_RpcBCacheAllocate( sizeof( NDR_ASYNC_MESSAGE) )?構造?Async?Msg 緩沖區(0x538 字節)。然后,通過 MulNdrpInitializeContextFromProc 初始化上下文(InitializeContext)。使用 NdrpInitializeAsyncMsg 將堆棧上的信息拷貝到?NDR_ASYNC_MESSAGE 結構中。
隨后進入被 try-finally 包圍的設置存根、上下文管理器句柄、異步句柄,反序列化參數(稱為 UnMarshalling)和過程調用(稱為 Invoke)代碼段。Ndr64ServerInitialize 函數用于設置存根信息,而 NdrpServerUnMarshal 函數用于解析參數(反序列化)。調用的下一個例程是 Invoke,它負責調用處理請求所要求內容的本地函數以及將調用例程的參數列表。當服務器發送回復時,也會調用相同的函數(Invoke)。
上文的分析結合了逆向工程和微軟 Leak 的源代碼。
可以在這個神奇的網站找到 Leak 的代碼:Microsoft leaked source code archive。

下面是節選自?/NT/com/rpc/ndr64/async.c ?中的?Ndr64AsyncServerWorker 函數源代碼(自早期代碼至今,它的實現細節幾乎沒有變化):
void RPC_ENTRY
Ndr64AsyncServerWorker(PRPC_MESSAGE pRpcMsg,ulong SyntaxIndex )
/*++
Routine Description :The server side entry point for regular asynchronous RPC procs.Arguments :pRpcMsg - The RPC message.Return :None.
--*/
{ulong dwStubPhase = STUB_UNMARSHAL;PRPC_SERVER_INTERFACE pServerIfInfo;PMIDL_SERVER_INFO pServerInfo;const SERVER_ROUTINE * DispatchTable;MIDL_SYNTAX_INFO * pSyntaxInfo;RPC_ASYNC_HANDLE AsyncHandle = 0;PNDR_ASYNC_MESSAGE pAsyncMsg;ushort ProcNum;PMIDL_STUB_MESSAGE pStubMsg;uchar * pArgBuffer;uchar * pArg;uchar ** ppArg;NDR64_PROC_FORMAT * pHeader;NDR64_PARAM_FORMAT * Params;long NumberParams;NDR64_PROC_FLAGS * pNdr64Flags;ushort ClientBufferSize;BOOL HasExplicitHandle;long n;// This context is just for setting up the call. embedded one in asyncmsg is the// one to be used during the life of this async call.NDR_PROC_CONTEXT *pContext;RPC_STATUS Status = RPC_S_OK;NDR64_PARAM_FLAGS * pParamFlags;NDR64_BIND_AND_NOTIFY_EXTENSION * pHeaderExts = NULL;pServerIfInfo = (PRPC_SERVER_INTERFACE)pRpcMsg->RpcInterfaceInformation;pServerInfo = (PMIDL_SERVER_INFO)pServerIfInfo->InterpreterInfo;DispatchTable = pServerInfo->DispatchTable;pSyntaxInfo = &pServerInfo->pSyntaxInfo[SyntaxIndex];NDR_ASSERT( XFER_SYNTAX_NDR64 == NdrpGetSyntaxType(&pSyntaxInfo->TransferSyntax)," invalid transfer syntax" );//// In the case of a context handle, the server side manager function has// to be called with NDRSContextValue(ctxthandle). But then we may need to// marshall the handle, so NDRSContextValue(ctxthandle) is put in the// argument buffer and the handle itself is stored in the following array.// When marshalling a context handle, we marshall from this array.//// The handle table is part of the async handle.ProcNum = (ushort) pRpcMsg->ProcNum;NDR_ASSERT( ! ((ULONG_PTR)pRpcMsg->Buffer & 0x7),"marshaling buffer misaligned at server" );AsyncHandle = 0;pAsyncMsg = (NDR_ASYNC_MESSAGE*) I_RpcBCacheAllocate( sizeof( NDR_ASYNC_MESSAGE) );if ( ! pAsyncMsg )Status = RPC_S_OUT_OF_MEMORY;else{memset( pAsyncMsg, 0, sizeof( NDR_ASYNC_MESSAGE ) );NdrServerSetupNDR64TransferSyntax(ProcNum,pSyntaxInfo,&pAsyncMsg->ProcContext );Status = NdrpInitializeAsyncMsg( 0, // StartofStack, serverpAsyncMsg);}if ( Status )RpcRaiseException( Status );pContext = &pAsyncMsg->ProcContext;PFORMAT_STRING pFormat = pContext->pProcFormat;pAsyncMsg->StubPhase = STUB_UNMARSHAL;pStubMsg = & pAsyncMsg->StubMsg; // same in ndr20pStubMsg->RpcMsg = pRpcMsg;// The arg buffer is zeroed out already.pArgBuffer = pContext->StartofStack;pHeader = (NDR64_PROC_FORMAT *) pFormat;pNdr64Flags = (NDR64_PROC_FLAGS *) &pHeader->Flags;HasExplicitHandle = !NDR64MAPHANDLETYPE( NDR64GETHANDLETYPE ( pNdr64Flags ) );if ( pNdr64Flags->HasOtherExtensions )pHeaderExts = (NDR64_BIND_AND_NOTIFY_EXTENSION *) (pFormat + sizeof( NDR64_PROC_FORMAT ) );if ( HasExplicitHandle ){NDR_ASSERT( pHeaderExts, "NULL extension header" );//// For a handle_t parameter we must pass the handle field of// the RPC message to the server manager.//if ( pHeaderExts->Binding.HandleType == FC64_BIND_PRIMITIVE ){pArg = pArgBuffer + pHeaderExts->Binding.StackOffset;if ( NDR64_IS_HANDLE_PTR( pHeaderExts->Binding.Flags ) )pArg = *((uchar **)pArg);*((handle_t *)pArg) = pRpcMsg->Handle;}}//// Get new interpreter info.//NumberParams = pHeader->NumberOfParams;Params = (NDR64_PARAM_FORMAT *)( (uchar *) pFormat + sizeof( NDR64_PROC_FORMAT ) + pHeader->ExtensionSize );//// Wrap the unmarshalling and the invoke call in the try block of// a try-finally. Put the free phase in the associated finally block.//BOOL fManagerCodeInvoked = FALSE;BOOL fErrorInInvoke = FALSE;RPC_STATUS ExceptionCode = 0;// We abstract the level of indirection here.AsyncHandle = pAsyncMsg->AsyncHandle;RpcTryFinally{RpcTryExcept{// Put the async handle on stack.((void **)pArgBuffer)[0] = AsyncHandle; //// Initialize the Stub message.// Note that for pipes we read non-pipe data synchronously,// and so the init routine doesn't need to know about async.//if ( ! pNdr64Flags->UsesPipes ){Ndr64ServerInitialize( pRpcMsg,pStubMsg,pServerInfo->pStubDesc );}elseNdr64ServerInitializePartial( pRpcMsg,pStubMsg,pServerInfo->pStubDesc,pHeader->ConstantClientBufferSize );// We need to set up this flag because the runtime does not know whether// it dispatched a sync or async call to us. same as ndr20pRpcMsg->RpcFlags |= RPC_BUFFER_ASYNC;pStubMsg->pAsyncMsg = pAsyncMsg;pStubMsg->RpcMsg = pRpcMsg;pStubMsg->pContext = &pAsyncMsg->ProcContext;//// Set up for context handle management.//pStubMsg->SavedContextHandles = & pAsyncMsg->CtxtHndl[0];// Raise exceptions after initializing the stub.if ( pNdr64Flags->UsesFullPtrPackage )pStubMsg->FullPtrXlatTables = NdrFullPointerXlatInit( 0, XLAT_SERVER );if ( pNdr64Flags->ServerMustSize & pNdr64Flags->UsesPipes )RpcRaiseException( RPC_X_WRONG_PIPE_VERSION );//// Set StackTop AFTER the initialize call, since it zeros the field// out.//pStubMsg->pCorrMemory = pStubMsg->StackTop; if ( pNdr64Flags->UsesPipes )NdrpPipesInitialize64( pStubMsg,&pContext->AllocateContext,(PFORMAT_STRING) Params,(char*)pArgBuffer,NumberParams );//// We must make this check AFTER the call to ServerInitialize,// since that routine puts the stub descriptor alloc/dealloc routines// into the stub message.//if ( pNdr64Flags->UsesRpcSmPackage )NdrRpcSsEnableAllocate( pStubMsg );// Let runtime associate async handle with the call.NdrpRegisterAsyncHandle( pStubMsg, AsyncHandle );pAsyncMsg->StubPhase = NDR_ASYNC_SET_PHASE;// --------------------------------// Unmarshall all of our parameters.// --------------------------------NDR_ASSERT( pContext->StartofStack == pArgBuffer, "startofstack is not set" );Ndr64pServerUnMarshal( pStubMsg );if ( pRpcMsg->BufferLength <(uint)(pStubMsg->Buffer - (uchar *)pRpcMsg->Buffer) ){RpcRaiseException( RPC_X_BAD_STUB_DATA );} }RpcExcept( NdrServerUnmarshallExceptionFlag(GetExceptionInformation()) ){ExceptionCode = RpcExceptionCode();if( RPC_BAD_STUB_DATA_EXCEPTION_FILTER ){ExceptionCode = RPC_X_BAD_STUB_DATA;}NdrpFreeMemoryList( pStubMsg );pAsyncMsg->Flags.BadStubData = 1;pAsyncMsg->ErrorCode = ExceptionCode;RpcRaiseException( ExceptionCode );}RpcEndExcept// Two separate blocks because the filters are different.// We need to catch exception in the manager code separately// as the model implies that there will be no other call from// the server app to clean up.RpcTryExcept{//// Do [out] initialization before the invoke.//Ndr64pServerOutInit( pStubMsg );//// Unblock the first pipe; this needs to be after unmarshalling// because the buffer may need to be changed to the secondary one.// In the out only pipes case this happens immediately.//if ( pNdr64Flags->UsesPipes )NdrMarkNextActivePipe( pContext->pPipeDesc );pAsyncMsg->StubPhase = STUB_CALL_SERVER;//// Check for a thunk. Compiler does all the setup for us.//if ( pServerInfo->ThunkTable && pServerInfo->ThunkTable[ProcNum] ){pAsyncMsg->Flags.ValidCallPending = 1;InterlockedDecrement( & AsyncHandle->Lock );fManagerCodeInvoked = TRUE;fErrorInInvoke = TRUE;pServerInfo->ThunkTable[ProcNum]( pStubMsg );}else{//// Note that this ArgNum is not the number of arguments declared// in the function we called, but really the number of// REGISTER_TYPEs occupied by the arguments to a function.//long ArgNum;MANAGER_FUNCTION pFunc;REGISTER_TYPE returnValue;if ( pRpcMsg->ManagerEpv )pFunc = ((MANAGER_FUNCTION *)pRpcMsg->ManagerEpv)[ProcNum];elsepFunc = (MANAGER_FUNCTION) DispatchTable[ProcNum];ArgNum = (long) pContext->StackSize / sizeof(REGISTER_TYPE);//// The StackSize includes the size of the return. If we want// just the number of REGISTER_TYPES, then ArgNum must be reduced// by 1 when there is a return value AND the current ArgNum count// is greater than 0.//if ( ArgNum && pNdr64Flags->HasReturn )ArgNum--;// Being here means that we can expect results. Note that the user// can call RpcCompleteCall from inside of the manager code.pAsyncMsg->Flags.ValidCallPending = 1;// Unlock the handle - the app is allowed to call RpCAsyncComplete// or RpcAsyncAbort from the manager code.InterlockedDecrement( & AsyncHandle->Lock );fManagerCodeInvoked = TRUE;fErrorInInvoke = TRUE;returnValue = Invoke( pFunc,(REGISTER_TYPE *)pArgBuffer,#if defined(_IA64_)pHeader->FloatDoubleMask,#endifArgNum);// We are discarding the return value as it is not the real one.// The real return value is passed in the complete call.}fErrorInInvoke = FALSE;}RpcExcept( 1 ){ExceptionCode = RpcExceptionCode();if ( ExceptionCode == 0 )ExceptionCode = ERROR_INVALID_PARAMETER;// We may not have the async message around anymore.RpcRaiseException( ExceptionCode );}RpcEndExcept}RpcFinally{if ( fManagerCodeInvoked && !fErrorInInvoke ){// Success. Just skip everything if the manager code was invoked// and returned successfully.// Note that manager code could have called Complete or Abort by now// and so the async handle may not be valid anymore.}else{// See if we can clean up;Status = RPC_S_OK;if ( fErrorInInvoke ){// After an exception in invoking, let's see if we can get a hold// of the handle. If so, we will be able to clean up.// If not, there may be a leak there that we can do nothing about.// The rule is: after an exception the app cannot call Abort or// Complete. So, we need to force complete if we can.Status = NdrValidateBothAndLockAsyncHandle( AsyncHandle );}if ( Status == RPC_S_OK ){// Something went wrong but we are able to do the cleanup.// Cleanup parameters and async message/handle.// propagate the exception.Ndr64pCleanupServerContextHandles( pStubMsg, NumberParams,Params,pArgBuffer,TRUE ); // fail before/during manager routineif (!pAsyncMsg->Flags.BadStubData){Ndr64pFreeParams( pStubMsg,NumberParams,Params,pArgBuffer );}NdrpFreeAsyncHandleAndMessage( AsyncHandle );}// else manager code invoked and we could not recover.// Exception will be raised by the EndFinally below.}}RpcEndFinally
}
通過 Leak Codes 了解到的 _NDR_ASYNC_MESSAGE 結構如下:
typedef struct _NDR_ASYNC_MESSAGE
{long Version; // 0x0long Signature; // 0x4RPC_ASYNC_HANDLE AsyncHandle; // raw and CAsyncMgr * // 0x8(size:n)NDR_ASYNC_CALL_FLAGS Flags; // 0x8 + n(size:2) ----> 0x68unsigned short StubPhase; // 0x8 + n + 0x2(size:2) == 0x6C - 2// ----> n = 0x60 ----> 0x6Aunsigned long ErrorCode; // 0x6CRPC_MESSAGE RpcMsg; // 0x70MIDL_STUB_MESSAGE StubMsg;NDR_SCONTEXT CtxtHndl[MAX_CONTEXT_HNDL_NUMBER];usigned long * pdwStubPhase;// Note: the correlation cache needs to be sizeof(pointer) alignedNDR_PROC_CONTEXT ProcContext;// guard at the end of the messageunsigned char AsyncGuard[NDR_ASYNC_GUARD_SIZE];
} NDR_ASYNC_MESSAGE, *PNDR_ASYNC_MESSAGE;
IDA 中的 Invoke 代碼如下(具體的分析將在后面的分析中給出):
__int64 __fastcall Invoke(__int64 (__fastcall *a1)(__int64, __int64, __int64, __int64),// pFunconst void *a2, // pArgumentList__int64 a3, // pFloatingPointArgumentListunsigned int a4) // cArguments
{void *v4; // rsp__int64 vars0[4]; // [rsp+0h] [rbp+0h] BYREFv4 = alloca(8 * ((a4 + 1) & 0xFFFFFFFE)); // 當函數為堆棧分配的頁面不夠時,// 調用該例程分配更多頁空間。// 在 X64 下局部變量超過 8K 字節時,// 由編譯器插入該函數// 從緩沖區復制參數并構造參數數組qmemcpy(vars0, a2, 8 * a4); // dst src countRpcInvokeCheckICall(&a1); // 在調度服務例程之前,會調用 CFG // (__guard_check_icall_fptr) // 以確保目標函數指針是 CFG 合法指針。return a1(vars0[0], vars0[1], vars0[2], vars0[3]);// 調用目標服務例程
}
Invoke 的第一個參數是要執行過程(函數)的指針,第二個參數是一般整形參數數組的首地址,第三個參數是浮點數參數數組的首地址,第四個參數是參數個數。x64 上按照?8 字節對齊解析參數列表。在通過 CFG 檢查點后,執行目標過程(函數)。
2.4 對 rpcrt4 的動態調試
下面我們從動態調試角度分析有關參數傳輸的細節(算是對上面源代碼中一些不是很透徹的地方加以分析理解)。
【分析】
首先啟動?WinDbg 調試器并附加到 winlogon.exe 進程上。
然后下幾個斷點:
- rpcrt4!Ndr64AsyncServerWorker
- rpcrt4!I_RpcBCacheAllocate
- rpcrt4!Invoke
- rpcrt4!RpcServerTestCancel
I_RpcBCacheAllocate 是初始化分配?NDR_ASYNC_MESSAGE 的緩沖區,后續 RPC 傳輸語法所需要的信息都會在這個緩沖區上被序列化。
Go 進程,然后按下 Ctrl+Shift+Esc 嘗試啟動任務管理器,這會觸發 RPC 過程。
在命中斷點 0 時,我們打印一下堆棧和寄存器信息:

其中,rcx 的內容為 PPRC_MESSAGE,我們看一下結構的參數:

隨后執行直到命中斷點?I_RpcBCacheAllocate,使用 F8 執行直到函數結束,查看返回值,它是指向 NDR_ASYNC_MESSAGE 結構的指針(在 memset 前,分配的緩沖區上有臟數據)。

緩沖區的分配依賴 LsaAlloc(如果無效則再嘗試使用?HeapAlloc )。其中 LsaAlloc?實際為 lsasrv.dll 導出的 LsaIAllocateHeap 。

然后,我們關注到 NdrpInitializeAsyncMsg 函數,在他里面準備了 _RPC_ASYNC_STATE 結構。這個結構一般用于傳遞 RPC 過程的額外執行結果。

看到了一樣的緩沖區分配過程,大小為 0x70,也就是 sizeof(RPC_ASYNC_STATE):?

在這個函數返回時,將?RPC_ASYNC_STATE 地址保存到 a2 參數也就是?NDR_ASYNC_MESSAGE 結構偏移 0x8 位置上:

接下來就是將 Binding Handle 拷貝到參數列表上:

此時可以通過動態調試驗證結論。在?call? RPCRT4!NdrpInitializeAsyncMsg 后,查看偏移 NDR_ASYNC_MESSAGE 結構 0x238 位置:

跟蹤這個地址被修改的位置:

這里將 PRPC_BINDING_HANDLE(地址)拷貝到了參數列表上第二個參數位置處(解組的參數列表嚴格按照 8 字節對齊,但 Buffer 上序列化的參數似乎是按照 4 字節對齊的)。

RPC_BINDING_HANDLE 其實和 TEB 有關,在 RPC 過程中使用 RpcServerTestCancel 測試鏈接需要此句柄。
例如前面提到的?I_WMsgSendMessage 過程:

第二次修改就是將?RPC_ASYNC_STATE 地址復制到參數列表上:

繼續調試到該指令附近:

這個就是?RPC_ASYNC_STATE 結構的地址(上面說過在 NdrpInitializeAsyncMsg 函數中被初始化)。
最后是一個關鍵函數,執行后續 RPC 參數的解組(反序列化)過程:

繼續調試到參數解組完成時:

主要經過三次修改,最終準備完調用 Invoke 所需要的參數列表。
值得注意的是,參數中前兩個參數跟 RPC 本身有關,而后續反序列化得到的參數跟要調用的函數有關。這些后處理的參數在序列化之前是 RPC_MESSAGE 結構的 Buffer 成員指向的緩沖區內容。
根據前面文章的研究,快捷鍵跟?I_WMsgSendMessage 以及?I_WMsgkSendMessage 有關,所以 Buffer 上的存儲了這兩個函數在各自調用時的部分參數。
此外,根據我的前面寫的文章(屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等熱鍵(二)) 中對?WMsgMessageHandler 等函數的分析。上面 2.2 小節分析的 I_WMsgSendMessage 函數的形參中 a3 和 a4 其實分別對應?WMsgMessageHandler 的前兩個參數。

它們可以理解為 WMsg Rpc 消息的低位和高位。Winlogon 使用回調處理消息并執行相應的操作。所以 Buffer 開頭并不是單純的代碼(Code),而是參數列表。我的前后兩篇文章關系可以聯系起來了[注:是 “屏蔽 CAD 熱鍵(二)”、“Rpc-Hook 屏蔽熱鍵(一)” 這兩篇]。

2.5 分析可行的突破點
這個階段主要有三個值得我們關注的地方,一個就是管理入口向量,第二個是 UUID 在存根中的編號。最后一個是解析?Buffer,這一個也是目前最可行的突破點,方法是?hook 導出的 Ndr64AsyncServerCallAll,然后根據分析出的參數攔截你感興趣的操作。
雖然在研究過程中,我也發現根據管理入口向量可以找到要調用函數的參數以及函數指針,可以替換函數指針或者參數來修改消息處理過程; UUID 的編號有一個規律那就是必須是奇數,如果是偶數,則會導致調用失敗。但是,前者必須要結合調試信息才能直到獲取到的入口向量函數名,后者的話則需要一定的 MS-RPC 的認識,你需要知道 UUID 和哪個接口過程對應,RpcViewer 也許是一個不錯的選擇。
注意:雖然上文提到了 I_WMsgSendMessage? 里面有對一個中間返回值的驗證,如下圖所示。

需要注意的是,I_WMsgkSendMessage 沒有驗證 a5 的過程,也不能像 I_WMsgSendMessage 一樣在調用 Handler 返回失敗后通過?RpcAsyncAbortCall 回滾操作。他只不過是通知客戶端取消 RPC 而已,沒有強制終止客戶端后續執行的能力。

但是,二者都可以通過在調用 RpcServerTestCanel 返回 NULL 之后通過?RpcAsyncAbortCall 回滾操作。
TODO: 完善分析過程,補充分析如何激發 NdrAsyncServerCall 的,看看能不能模擬發送消息。
三、通過 Detours 掛鉤實現
TODO:將通過調試器驗證的理論通過編程來實現,這需要在未來補充。
3.1 Buffer 參數解析代碼
首先,通過 Detours 實現掛鉤 Ndr64AsyncServerCallAll?函數并按照 4 字節對齊解析參數數組(Buffer 指向的緩沖區)。
#include "pch.h"
#include "detours.h"
#include <WtsApi32.h>
#include <rpc.h>
#include <cstdint>
#include <cwchar>
#include <cstdarg>
#include <string>#pragma comment(lib, "WtsApi32.lib")
#pragma comment(lib, "Rpcrt4.lib")
#pragma comment(lib, "detours.lib")PVOID fpNdr64AsyncServerCallAll = NULL;
void StartHookingFunction();
void UnmappHookedFunction();
BOOL SvcMessageBoxW(LPCWSTR lpTitleBuffer, LPCWSTR lpMsgBuffer,DWORD style, BOOL bWait, PDWORD lpdwResponse);#define __RPC_FAR
#define RPC_MGR_EPV void
#define RPC_ENTRY __stdcalltypedef void* LI_RPC_HANDLE;
typedef LI_RPC_HANDLE LRPC_BINDING_HANDLE;typedef struct _LRPC_VERSION {unsigned short MajorVersion;unsigned short MinorVersion;
} LRPC_VERSION;typedef struct _LRPC_SYNTAX_IDENTIFIER {GUID SyntaxGUID;LRPC_VERSION SyntaxVersion;
} LRPC_SYNTAX_IDENTIFIER, __RPC_FAR* LPRPC_SYNTAX_IDENTIFIER;typedef struct _LRPC_MESSAGE
{LRPC_BINDING_HANDLE Handle;unsigned long DataRepresentation;void __RPC_FAR* Buffer;unsigned int BufferLength;unsigned int ProcNum;LPRPC_SYNTAX_IDENTIFIER TransferSyntax;RPC_SERVER_INTERFACE* RpcInterfaceInformation;void __RPC_FAR* ReservedForRuntime;RPC_MGR_EPV __RPC_FAR* ManagerEpv;void __RPC_FAR* ImportContext;unsigned long RpcFlags;
} LRPC_MESSAGE, __RPC_FAR* LPRPC_MESSAGE;//--------------------------------------------------
typedef void (RPC_ENTRY* __Ndr64AsyncServerCallAll)(LPRPC_MESSAGE pRpcMsg);void RPC_ENTRY HookedNdr64AsyncServerCallAll(LPRPC_MESSAGE pRpcMsg
);typedef _Return_type_success_(return >= 0) LONG NTSTATUS;typedef NTSTATUS* PNTSTATUS;BOOL APIENTRY DllMain(HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved
)
{DisableThreadLibraryCalls(hModule);switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:StartHookingFunction();break;case DLL_THREAD_ATTACH:break;case DLL_THREAD_DETACH:break;case DLL_PROCESS_DETACH:UnmappHookedFunction();break;}return TRUE;
}void StartHookingFunction()
{// 開始處理DetourTransactionBegin();// 更新線程信息 DetourUpdateThread(GetCurrentThread());fpNdr64AsyncServerCallAll =DetourFindFunction("rpcrt4.dll","Ndr64AsyncServerCallAll");// 將攔截的函數附加到原函數的地址上,這里可以攔截多個函數。DetourAttach(&(PVOID&)fpNdr64AsyncServerCallAll,HookedNdr64AsyncServerCallAll);// 結束處理DetourTransactionCommit();
}void UnmappHookedFunction()
{// 開始處理DetourTransactionBegin();// 更新線程信息 DetourUpdateThread(GetCurrentThread());//將攔截的函數從原函數的地址上解除,這里可以解除多個函數。DetourDetach(&(PVOID&)fpNdr64AsyncServerCallAll,HookedNdr64AsyncServerCallAll);// 結束處理DetourTransactionCommit();
}std::wstring CoCreateStringBufferW(const wchar_t* wsFormat, ...) {// 使用可變參數列表來處理格式化字符串和參數va_list argsList;va_start(argsList, wsFormat);// 計算格式化后的字符串長度int nLength = _vscwprintf(wsFormat, argsList) + 1; // +1 是為了包含結尾的空字符// 分配內存來存儲格式化后的字符串wchar_t* wsBuffer = new (std::nothrow) wchar_t[nLength];if (wsBuffer == nullptr) {va_end(argsList);return L"";}// 格式化字符串到緩沖區中vswprintf_s(wsBuffer, nLength, wsFormat, argsList);// 釋放可變參數列表va_end(argsList);// 將 wchar_t* 轉換為 std::wstringstd::wstring result(wsBuffer);// 釋放臨時緩沖區內存delete[] wsBuffer;return result;
}BOOL SvcMessageBoxW(LPCWSTR lpTitleBuffer, LPCWSTR lpMsgBuffer, DWORD style, BOOL bWait, PDWORD lpdwResponse)
{if (lpTitleBuffer == nullptr || lpMsgBuffer == nullptr)return FALSE;std::multiplies<size_t> multiply;DWORD dwTitleLength = (DWORD)multiply(wcslen(lpTitleBuffer) + 1u, sizeof(WCHAR));DWORD dwlpMsgLength = (DWORD)multiply(wcslen(lpMsgBuffer) + 1u, sizeof(WCHAR));if (dwTitleLength == (DWORD)-1 || dwlpMsgLength == (DWORD)-1) {OutputDebugStringW(L"Message buffer length too large.\n");return FALSE;}BOOL rStatus = 0;PWCHAR wcsTitle = new (std::nothrow) WCHAR[dwTitleLength];PWCHAR wcsMsg = new (std::nothrow) WCHAR[dwlpMsgLength];if (wcsTitle == nullptr || wcsMsg == nullptr) {OutputDebugStringW(L"Allocate memory failed.\n");return FALSE;}memset(wcsTitle, 0, dwTitleLength);memcpy_s(wcsTitle, dwTitleLength,(LPVOID)lpTitleBuffer, dwTitleLength);memcpy_s(wcsMsg, dwlpMsgLength, (LPVOID)lpMsgBuffer, dwlpMsgLength);DWORD dwCSessionId = WTSGetActiveConsoleSessionId();rStatus = WTSSendMessageW(WTS_CURRENT_SERVER_HANDLE, dwCSessionId,wcsTitle, dwTitleLength, wcsMsg, dwlpMsgLength, style, 0, lpdwResponse, bWait);delete[] wcsTitle;delete[] wcsMsg;return rStatus;
}void RPC_ENTRY HookedNdr64AsyncServerCallAll(LPRPC_MESSAGE pRpcMsg
)
{// 基址uint64_t iBufferBaseAddr = reinterpret_cast<uintptr_t>(pRpcMsg->Buffer);const UINT bufferLength = pRpcMsg->BufferLength;// 忽略零長度緩沖區(安全調用指針)if (bufferLength == 0 || pRpcMsg->Buffer == nullptr){((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);return;}// 按照 BufferLength 解析參數列表const DWORD argsNum = bufferLength / 4u; // 4 字節對齊參數個數// 在緩沖區上復制參數數據PDWORD argsList = (DWORD*)HeapAlloc(GetProcessHeap(), 0, bufferLength);if (argsList == nullptr) {OutputDebugStringW(L"Allocate memory failed.\n");return;}ZeroMemory(argsList, bufferLength);// 復制 buffer 指向的緩沖區memcpy(argsList, reinterpret_cast<PVOID>(iBufferBaseAddr), bufferLength);std::wstring argsDbgMsg(L"Arguments List:\n"); // 格式化輸出信息文本// 遍歷數組(嚴格按照 4 字節對齊的個數)for (UINT index = 0; index < argsNum - 1; index += 2) {argsDbgMsg += CoCreateStringBufferW(L"[%02d]: 0x%08X\t[%02d]: 0x%08X\n", index,argsList[index], index + 1, argsList[index + 1]);}if (argsNum % 2 != 0) { // 奇數個數末尾(似乎末尾是空字節?)argsDbgMsg += CoCreateStringBufferW(L"[%02d]: 0x%08X\n", argsNum - 1,argsList[argsNum - 1]);}// 打印參數信息(非阻滯)DWORD dwResult = 0;SvcMessageBoxW(L"WMsg Information", argsDbgMsg.c_str(), MB_APPLMODAL | MB_ICONINFORMATION| MB_OK, FALSE,&dwResult);HeapFree(GetProcessHeap(), 0, argsList);return ((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);
}
使用通用注入器注入 winlogon 進程:

按下組合鍵 Ctrl + Shift + Esc ,結果如下:

在彈出任務管理器前我們獲取了參數信息,并彈出了提示框。
一個發現是,盡管大多數的 Winlogon Message (WMsg) 在處理時, BufferLength 都等于 12 字節,但依然有例外的情況。例如,在切換用戶時,可以捕獲到多條不同 RPC 處理的 Buffer 參數,其中一條按 4 字節對齊的參數項有 25 個,雖然對齊規則下的參數個數不等于實際的參數個數,但也說明了參數個數遠超過 3 個。

第二點是我認為緩沖區的末尾為占位空字節,但我選擇仍然從開頭輸出到 BufferLength 指定的結尾。
3.2 RpcServerTestCancel 熱補丁代碼
通過對?RpcServerTestCancel 熱補丁【不需要使用 Detours 和 Dll 模塊注入】可以攔截有關 Rpc 操作(這里以不判斷參數,直接攔截所有 WMsg 為例,注意:這會導致所有跟 winlogon 有關的系統操作都被禁止,雖然不影響正常運行,但可能會造成睡眠后出現異常,所以,此代碼需要和 Buffer 參數解析聯合使用)。
主要原理:獲取?RpcServerTestCancel 函數地址,修改其指令字節,使其返回 NULL,即可使得調用取消。如圖所示:

修改的指令是:
Raw Hex:
33C0C3???
String Literal:
"\x33\xC0\xC3"
Array Literal:
{ 0x33, 0xC0, 0xC3 }
Disassembly:
0: ?33 c0 ? ? ? ? ? ? ? ? ? xor ? ?eax,eax
2: ?c3? ? ? ? ? ? ? ? ? ? ? ? ret
就是異或 eax 寄存器的值,使得返回值變為 0,然后 ret?使調用返回。需要注意的是:在 x86-64 下,該函數的調用約定是 __fastcall 所以不需要被調用函數清理堆棧;而在 x86-32 下,則是 __stdcall 則需要清理和平衡堆棧,將第二條指令改為 retn 4,即 {C2, 04, 00}。
效果演示:
神!在 Windows 上屏蔽系統操作
主要代碼(兼容 x32 和 x64 系統,但需要獨立編譯模塊):
#include <windows.h>
#include <tlhelp32.h>
#include <richedit.h>
#include <CommCtrl.h>
#include "CustomDialog.h"
#include <mmsystem.h>
#include <dwmapi.h>
#include <unordered_map>
#include <mutex>
#include <string>
#include "resource.h"
#include <iostream>
#include <string>
#include <sstream>#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "Comctl32.lib")
#pragma comment(lib, "dwmapi.lib")struct ExtStruct { // 進程退出處理線程HWND hwndMain;DWORD dwWaitMillisecond;
};enum LogLevel { INFO, KERROR, WARNING };#pragma comment(linker,"\"/manifestdependency:type='win32' "\"name='Microsoft.Windows.Common-Controls' "\"version='6.0.0.0' processorArchitecture='*' "\"publicKeyToken='6595b64144ccf1df' language='*'\"")#define EXITTHREADDATA L"ExitMainThread"
#define TOOLAPPTITLENAME L"Winlogon RpcServerTestCancel Hook"
#define TOOLAPPCLASSNAME L"WMsgTestCancelHookWindowClass"
#define TOOLABOUTSTRING L"Winlogon RpcServerTestCancel Hook\nAuthor:\tLianYou516\nVersion:\t1.0.0.1"
#define MAX_THREAD_NAME_LENGTH 256HINSTANCE g_hInstance;
HWND hRichEdit;
HWND hButtonStart, hButtonStop, hButtonExit, hStatusbar;
bool IsEnabledHook = false;
std::unordered_map<HANDLE, std::wstring> g_ThreadDescriptions;
std::mutex g_ThreadDescriptionsMutex;void AppendText(HWND hEdit, const std::wstring& text) {// 禁用重繪SendMessageW(hEdit, WM_SETREDRAW, FALSE, 0);CHARRANGE cr = { 0 };cr.cpMin = -1;cr.cpMax = -1;SendMessageW(hEdit, EM_EXSETSEL, 0, (LPARAM)&cr);SendMessageW(hEdit, EM_REPLACESEL, FALSE, (LPARAM)text.c_str());// 滾動到文本框底部SendMessageW(hEdit, WM_VSCROLL, SB_BOTTOM, 0);// 啟用重繪并強制重新繪制SendMessageW(hEdit, WM_SETREDRAW, TRUE, 0);InvalidateRect(hEdit, NULL, TRUE);SetFocus(hEdit); // 防止失焦
}void Log(HWND hEdit, const std::wstring& message, LogLevel level) {std::wstringstream ss;switch (level) {case INFO:ss << L"[INFO] ";break;case KERROR:ss << L"[ERROR] ";break;case WARNING:ss << L"[WARNING] ";break;}ss << message << L"\r\n";AppendText(hEdit, ss.str());
}HRESULT MySetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription) {if (!lpThreadDescription) {return E_POINTER; // Null pointer passed}std::lock_guard<std::mutex> lock(g_ThreadDescriptionsMutex);try {g_ThreadDescriptions[hThread] = lpThreadDescription;}catch (const std::exception& e) {Log(hRichEdit, L"SetThreadDescription Exception: "+ std::wstring(e.what(), e.what() + strlen(e.what())) + L"\n", KERROR);return HRESULT_FROM_WIN32(ERROR_UNHANDLED_EXCEPTION); // Catch all exceptions and convert to HRESULT}return S_OK; // Success
}HRESULT MyGetThreadDescription(HANDLE hThread, PWSTR* ppszThreadDescription) {if (!ppszThreadDescription) {return E_POINTER; // Null pointer passed}std::lock_guard<std::mutex> lock(g_ThreadDescriptionsMutex);auto it = g_ThreadDescriptions.find(hThread);if (it != g_ThreadDescriptions.end()) {// Allocate memory for the thread descriptionsize_t len = (it->second.length() + 1) * sizeof(wchar_t);*ppszThreadDescription = static_cast<PWSTR>(CoTaskMemAlloc(len));if (*ppszThreadDescription) {wcscpy_s(*ppszThreadDescription, len / sizeof(wchar_t), it->second.c_str());return S_OK; // Success}else {return E_OUTOFMEMORY; // Memory allocation failed}}else {return HRESULT_FROM_WIN32(ERROR_NOT_FOUND); // Thread not found}
}void MyFreeThreadDescription(PWSTR pszThreadDescription) {if (pszThreadDescription) {CoTaskMemFree(pszThreadDescription);}
}// 檢查是否以管理員權限啟動
bool IsRunAsAdmin() {BOOL isRunAsAdmin = FALSE;PSID adminGroup = NULL;// 獲取管理員組的 SIDSID_IDENTIFIER_AUTHORITY ntAuthority = SECURITY_NT_AUTHORITY;if (!AllocateAndInitializeSid(&ntAuthority, 2,SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS,0, 0, 0, 0, 0, 0, &adminGroup)) {return false;}// 檢查當前進程的令牌是否包含管理員組的 SIDHANDLE token = NULL;if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {TOKEN_GROUPS* groupInfo = NULL;DWORD size = 0;// 獲取令牌的組信息GetTokenInformation(token, TokenGroups, NULL, 0, &size);groupInfo = (TOKEN_GROUPS*)malloc(size);if (groupInfo && GetTokenInformation(token, TokenGroups, groupInfo, size, &size)) {for (DWORD i = 0; i < groupInfo->GroupCount; i++) {if (EqualSid(groupInfo->Groups[i].Sid, adminGroup)) {isRunAsAdmin = TRUE;break;}}}if (groupInfo) {free(groupInfo);}CloseHandle(token);}if (adminGroup) {FreeSid(adminGroup);}return isRunAsAdmin;
}// 啟用 SE_DEBUG 權限
bool EnableDebugPrivilege() {HANDLE token;LUID luid;TOKEN_PRIVILEGES tp;if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) {return false;}if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {CloseHandle(token);return false;}tp.PrivilegeCount = 1;tp.Privileges[0].Luid = luid;tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;if (!AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {CloseHandle(token);return false;}CloseHandle(token);return GetLastError() == ERROR_SUCCESS;
}bool ModifyFunctionInWinlogon(bool enable, HWND hEdit) {Log(hEdit, enable ? L"Blocking Winlogon Messages..." : L"Enabling Winlogon Messages...", INFO);Log(hEdit, L"Attempting to get module handle of Rpcrt4...", INFO);auto module = GetModuleHandleW(L"rpcrt4.dll");if (!module) {Log(hEdit, L"Failed to get module handle.", KERROR);return false;}WCHAR wsModBuffer[55];ZeroMemory(wsModBuffer, 55 * sizeof(WCHAR));swprintf_s(wsModBuffer, L"Rpcrt4 Module Handle: 0x%p", module);Log(hEdit, wsModBuffer, INFO);Log(hEdit, L"Attempting to get function RpcServerTestCancel's address...", INFO);auto func = GetProcAddress(module, "RpcServerTestCancel");if (!func) {Log(hEdit, L"Failed to get function address.", KERROR);return false;}WCHAR wsAddsBuffer[55];ZeroMemory(wsAddsBuffer, 55 * sizeof(WCHAR));swprintf_s(wsAddsBuffer, L"RpcServerTestCancel's address: 0x%p", func);Log(hEdit, wsAddsBuffer, INFO);auto snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if (snapshot == INVALID_HANDLE_VALUE) {Log(hEdit, L"Failed to create snapshot.", KERROR);return false;}PROCESSENTRY32W pe32 = { sizeof(PROCESSENTRY32W) };bool success = false;if (Process32FirstW(snapshot, &pe32)) {Log(hEdit, L"Scanning processes...", INFO);do {// 跳過系統空閑進程if (pe32.th32ProcessID <= 4u)continue;Log(hEdit, std::wstring(L"Found process: ") + pe32.szExeFile, INFO);if (!wcscmp(pe32.szExeFile, L"winlogon.exe")) {Log(hEdit, L"Target process found: winlogon.exe", INFO);WCHAR wszPID[25];swprintf_s(wszPID, L"Target PID: %u", pe32.th32ProcessID);Log(hEdit, wszPID, INFO);Log(hEdit, L"Attempting to open winlogon.exe process...", INFO);auto hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pe32.th32ProcessID);if (hProcess) {Log(hEdit, L"Opened process handle successfully.", INFO);Log(hEdit, L"Attempting to turn off memory protection...", INFO);DWORD oldProtect;if (VirtualProtectEx(hProcess, (void*)func, 0x5, PAGE_EXECUTE_READWRITE, &oldProtect)){Log(hEdit, L"Successfully turned off memory protection.", INFO);/** 在這里對函數返回值自身異或,將得到 NULL,返回值應該不為空,* 如果為空,則表明連接的一方已終止。* 其他終結點應該調用 RpcAsyncAbortCall 結束調用。* __________________________________________________* / \* | Raw Hex: |* | 33C0C3 |* | |* | String Literal: |* | "\x33\xC0\xC3" |* | |* | Array Literal: |* | { 0x33, 0xC0, 0xC3 } |* | |* | Disassembly: |* | 0 : 33 c0 xor eax, eax |* | 2 : c3 ret |* \ ________________________________________________ /*/#ifdef _WIN32#ifndef _WIN64unsigned char buf[] = { 0x33, 0xc0, 0xc2, 0x04, 0 }; // x86-32 為 stdcall 希望被調用函數清理堆棧#elseunsigned char buf[] = { 0x33, 0xc0, 0xc3 };#endif#endifif (enable) {Log(hEdit, L"Attempting to write instruction bytes for patch...", INFO);success = WriteProcessMemory(hProcess, (void*)func, buf, sizeof(buf), NULL);}else {Log(hEdit, L"Attempting to recover instruction bytes...", INFO);success = WriteProcessMemory(hProcess, (void*)func, (void*)func, sizeof(buf), NULL);}Log(hEdit, L"Attempting to turn on memory protection...", INFO);VirtualProtectEx(hProcess, (void*)func, 0x5, oldProtect, &oldProtect);if (success) {IsEnabledHook = enable;Log(hEdit, L"Memory written successfully.", INFO);}else {IsEnabledHook = false;Log(hEdit, L"Failed to write memory.", KERROR);}}else {success = false;Log(hEdit, L"Failed to change memory protection.", KERROR);}CloseHandle(hProcess);Log(hEdit, L"Closed process handle.", INFO);}else {success = false;Log(hEdit, L"Failed to open process.", KERROR);}break;}} while (Process32NextW(snapshot, &pe32));}else {Log(hEdit, L"Failed to retrieve first process.", KERROR);}CloseHandle(snapshot);return success;
}HBITMAP CreateBitmapFromIcon(HICON hIcon, int width, int height) {HDC hdcScreen = GetDC(NULL);HDC hdcMem = CreateCompatibleDC(hdcScreen);// Create a bitmap with an alpha channelBITMAPINFO bmi = { 0 };bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);bmi.bmiHeader.biWidth = width;bmi.bmiHeader.biHeight = -height; // Negative height to create a top-down DIBbmi.bmiHeader.biPlanes = 1;bmi.bmiHeader.biBitCount = 32;bmi.bmiHeader.biCompression = BI_RGB;void* pBits = NULL;HBITMAP hBitmap = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &pBits, NULL, 0);if (hBitmap){HBITMAP hOldBitmap = (HBITMAP)SelectObject(hdcMem, hBitmap);// Fill background with transparent colorBLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };GdiAlphaBlend(hdcMem, 0, 0, width, height, hdcMem, 0, 0, width, height, bf);// Draw icon onto the bitmapDrawIconEx(hdcMem, 0, 0, hIcon, width, height, 0, NULL, DI_NORMAL);SelectObject(hdcMem, hOldBitmap);DeleteDC(hdcMem);ReleaseDC(NULL, hdcScreen);//DeleteObject(hBitmap);}return hBitmap;
}// 播放背景音樂函數
void PlayBackgroundMusic()
{// 從資源中加載并播放音頻PlaySoundW(MAKEINTRESOURCE(IDR_WAVE1), GetModuleHandleW(NULL), SND_RESOURCE | SND_ASYNC | SND_LOOP);
}LRESULT CALLBACK RichEditSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR, DWORD_PTR) {switch (uMsg) {case WM_KEYDOWN:switch (wParam) {case VK_DELETE:case VK_BACK:// 通過返回0阻止鍵盤刪除操作return 0;case 'A':if (GetKeyState(VK_CONTROL) & 0x8000) {// 按下Ctrl+A時選擇所有文本SendMessageW(hwnd, EM_SETSEL, 0, -1);return 0;}break;}break;case WM_CHAR: // 阻止英文字符輸入case WM_IME_COMPOSITION: // 阻止IME(輸入法編輯器)合成case WM_PASTE:return 0; // 阻止文本輸入(粘貼)case WM_RBUTTONDOWN: { // 創建右鍵菜單HMENU hMenu = CreatePopupMenu();AppendMenuW(hMenu, MF_STRING, ID_SELECT_ALL, L"Select All");AppendMenuW(hMenu, MF_STRING, ID_CANCEL, L"Cancel");AppendMenuW(hMenu, MF_STRING, ID_COPY, L"Copy");AppendMenuW(hMenu, MF_STRING, ID_CLEAR, L"Clean Up");POINT pt;GetCursorPos(&pt);TrackPopupMenu(hMenu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, NULL);DestroyMenu(hMenu);return 0;}case WM_COMMAND: // 添加右鍵菜單響應switch (LOWORD(wParam)) {case ID_SELECT_ALL:SendMessageW(hwnd, EM_SETSEL, 0, -1);break;case ID_CANCEL:SendMessageW(hwnd, EM_SETSEL, -1, 0);break;case ID_COPY:SendMessageW(hwnd, WM_COPY, 0, 0);break;case ID_CLEAR:SetWindowTextW(hwnd, L"");break;}break;}return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}DWORD WINAPI OnExitMainWnd(LPVOID lpThreadParameter)
{MySetThreadDescription(GetCurrentThread(), EXITTHREADDATA);ExtStruct* extSt = (ExtStruct*)lpThreadParameter;Log(hRichEdit, L"Process is closing...", INFO);// 必須先接觸掛鉤才能退出進程if (IsEnabledHook){bool status = ModifyFunctionInWinlogon(false, hRichEdit);if (status)SendMessage(hStatusbar, SB_SETTEXT, 0, (LPARAM)L"Hook Disabled");}Sleep(extSt->dwWaitMillisecond);// 用戶點擊了確定按鈕,關閉窗口if (extSt->hwndMain != NULL)PostMessageW(extSt->hwndMain, WM_DESTROY, GetCurrentThreadId(), 1);return 0;
}LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {static HDC hdcBackBuffer;static HBITMAP hbmBackBuffer;static HBITMAP hbmOldBuffer;static RECT clientRect;static HBITMAP hBackgroundBitmap = NULL;switch (uMsg) {case WM_CREATE: {LoadLibraryW(L"Msftedit.dll");// 創建字體HFONT hFont = CreateFontW(35, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,OUT_OUTLINE_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,VARIABLE_PITCH, L"Segoe UI");// 添加控件窗口hButtonStart = CreateWindowW(L"BUTTON", L"Start", WS_VISIBLE | WS_CHILD,10, 10, 90, 45,hwnd, (HMENU)ID_START,NULL, NULL);hButtonStop = CreateWindowW(L"BUTTON", L"Stop", WS_VISIBLE | WS_CHILD | WS_DISABLED, // 初始狀態是禁止的110, 10, 90, 45,hwnd, (HMENU)ID_STOP,NULL, NULL);hButtonExit = CreateWindowW(L"BUTTON", L"Exit",WS_VISIBLE | WS_CHILD,210, 10, 90, 45,hwnd, (HMENU)ID_EXIT,NULL, NULL);SendMessageW(hButtonStart, WM_SETFONT, (WPARAM)hFont, TRUE);SendMessageW(hButtonStop, WM_SETFONT, (WPARAM)hFont, TRUE);SendMessageW(hButtonExit, WM_SETFONT, (WPARAM)hFont, TRUE);// 創建富文本框hRichEdit = CreateWindowW(MSFTEDIT_CLASS, NULL,WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE |ES_AUTOVSCROLL,10, 60, 260, 200,hwnd, NULL,NULL, NULL);// 設置背景色為暗色SendMessageW(hRichEdit, EM_SETBKGNDCOLOR, FALSE, RGB(30, 30, 30));// 設置文本顏色為亮色CHARFORMATW cf = { 0 };SendMessageW(hRichEdit, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&cf);cf.cbSize = sizeof(CHARFORMATW);cf.dwMask = CFM_COLOR;cf.crTextColor = RGB(255, 255, 255);SendMessageW(hRichEdit, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&cf);// 窗口子類化,便于對富文本框添加右鍵菜單和限制編輯操作SetWindowSubclass(hRichEdit, RichEditSubclassProc, 0, 0);// 添加 “關于”到系統菜單HMENU hSysMenu = GetSystemMenu(hwnd, FALSE);AppendMenuW(hSysMenu, MF_SEPARATOR, 0, NULL);AppendMenuW(hSysMenu, MF_STRING, ID_ABOUT, L"&About ...(A)");// 修改 SetMenuItemInfo 調用以包含快捷鍵和位圖HICON hIcon = LoadIconW(NULL, IDI_ASTERISK);if (hIcon){HBITMAP hBitmap = CreateBitmapFromIcon(hIcon, 20, 20);MENUITEMINFO mii = { sizeof(MENUITEMINFO) };mii.fMask = MIIM_BITMAP | MIIM_ID | MIIM_STATE;mii.wID = ID_ABOUT;mii.hbmpItem = hBitmap;SetMenuItemInfoW(hSysMenu, ID_ABOUT, FALSE, &mii);DestroyIcon(hIcon);}// 創建狀態欄hStatusbar = CreateWindowExW(0, STATUSCLASSNAME, NULL,WS_CHILD | WS_VISIBLE | SBARS_SIZEGRIP,0, 0, 0, 0, hwnd, NULL, NULL, NULL);int parts[] = { 155, -1 };SendMessageW(hStatusbar, SB_SETPARTS, sizeof(parts) / sizeof(int), (LPARAM)parts);SendMessageW(hStatusbar, SB_SETTEXT, 0, (LPARAM)L"Hook Disabled");// 啟用雙緩沖GetClientRect(hwnd, &clientRect);HDC hdc = GetDC(hwnd);hdcBackBuffer = CreateCompatibleDC(hdc);hbmBackBuffer = CreateCompatibleBitmap(hdc, clientRect.right, clientRect.bottom);hbmOldBuffer = (HBITMAP)SelectObject(hdcBackBuffer, hbmBackBuffer);ReleaseDC(hwnd, hdc);// 啟用管理員權限if (!IsRunAsAdmin() || !EnableDebugPrivilege()){Log(hRichEdit, L"The program must be launched as an administrator.", KERROR);EnableWindow(hButtonStart, FALSE);EnableWindow(hButtonStop, FALSE);//EnableWindow(hButtonExit, FALSE);}// 最后啟用背景音樂PlayBackgroundMusic();break;}case WM_GETMINMAXINFO: {MINMAXINFO* lpMinMax = (MINMAXINFO*)lParam;lpMinMax->ptMinTrackSize.x = 580; // 設置窗口的最小寬度lpMinMax->ptMinTrackSize.y = 480; // 設置窗口的最小高度return 0;}case WM_SIZE: { // 當客戶區窗口大小發生變化時,動態調整控件的位置GetClientRect(hwnd, &clientRect);//SetWindowPos(hRichEdit, NULL, 10, 65, clientRect.right - 20, clientRect.bottom - 175, SWP_NOZORDER);// 調整后緩沖區尺寸HDC hdc = GetDC(hwnd);HBITMAP hbmNewBuffer = CreateCompatibleBitmap(hdc, clientRect.right, clientRect.bottom);SelectObject(hdcBackBuffer, hbmNewBuffer);DeleteObject(hbmBackBuffer);hbmBackBuffer = hbmNewBuffer;ReleaseDC(hwnd, hdc);int btnWidth = 90;int btnHeight = 45;int spacing = 20;int totalWidth = btnWidth * 3 + spacing * 2;int startX = (clientRect.right - totalWidth) / 2;int startY = 10;SetWindowPos(hButtonStart, NULL, startX, startY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);SetWindowPos(hButtonStop, NULL, startX + btnWidth + spacing, startY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);SetWindowPos(hButtonExit, NULL, startX + 2 * (btnWidth + spacing), startY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);int richEditHeight = clientRect.bottom - startY - btnHeight - 45;SetWindowPos(hRichEdit, NULL, 10, startY + btnHeight + 10, clientRect.right - 20, richEditHeight, SWP_NOZORDER);SetWindowPos(hStatusbar, NULL, 0, clientRect.bottom - 30, clientRect.right, 25, SWP_NOZORDER);InvalidateRect(hwnd, NULL, FALSE);break;}case WM_KEYDOWN:if (IsWindowVisible((HWND)GetSystemMenu(hwnd, FALSE)) && (wParam == 'A')) {SendMessageW(hwnd, WM_SYSCOMMAND, ID_ABOUT, 0);return 0;}break;case WM_SYSCOMMAND: {if (wParam == ID_ABOUT) {MessageBoxW(hwnd,TOOLABOUTSTRING,L"About",MB_OK | MB_ICONINFORMATION | MB_APPLMODAL);return 0;}return DefWindowProcW(hwnd, uMsg, wParam, lParam);}case WM_COMMAND: {switch (LOWORD(wParam)) {case ID_START:if (!IsEnabledHook){Log(hRichEdit, L"============== Start ==============", INFO);bool status = ModifyFunctionInWinlogon(true, hRichEdit);Log(hRichEdit, L"==================================", INFO);if (status){EnableWindow(hButtonStart, FALSE);EnableWindow(hButtonStop, TRUE);SendMessageW(hStatusbar, SB_SETTEXT, 0, (LPARAM)L"Hook Enabled");}//PlaySoundW(L"MouseClick", NULL, SND_ASYNC);}break;case ID_STOP:if (IsEnabledHook){Log(hRichEdit, L"============== Stop ==============", INFO);bool status = ModifyFunctionInWinlogon(false, hRichEdit);Log(hRichEdit, L"==================================", INFO);if (status){EnableWindow(hButtonStop, FALSE);EnableWindow(hButtonStart, TRUE);SendMessageW(hStatusbar, SB_SETTEXT, 0, (LPARAM)L"Hook Disabled");}//PlaySoundW(L"MouseClick", NULL, SND_ASYNC);}break;case ID_EXIT:PostMessageW(hwnd, WM_CLOSE, 0, 0);break;}break;}case WM_ERASEBKGND:return 1; // 通過繞過默認擦除背景防止閃爍case WM_PAINT: {PAINTSTRUCT ps;HDC hdc = BeginPaint(hwnd, &ps);// 在后緩沖區上繪制背景FillRect(hdcBackBuffer, &clientRect, (HBRUSH)(COLOR_WINDOW + 1));// 畫中間分割控件空間的黑線HPEN hPen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));HPEN hOldPen = (HPEN)SelectObject(hdcBackBuffer, hPen);MoveToEx(hdcBackBuffer, 10, 60, NULL);LineTo(hdcBackBuffer, clientRect.right - 10, 60);SelectObject(hdcBackBuffer, hOldPen);DeleteObject(hPen);// 將后緩沖區覆蓋到前緩沖區BitBlt(hdc, 0, 0, clientRect.right, clientRect.bottom, hdcBackBuffer, 0, 0, SRCCOPY);EndPaint(hwnd, &ps);break;}case WM_CLOSE:{// 首先備份按鈕狀態并禁用按鈕操作bool isEnBTStart = IsWindowEnabled(hButtonStart);bool isEnBTStop = IsWindowEnabled(hButtonStop);EnableWindow(hButtonStart, FALSE);EnableWindow(hButtonStop, FALSE);// 獲取光標位置POINT cursorPos;GetCursorPos(&cursorPos);ScreenToClient(hwnd, &cursorPos);// 獲取窗口矩形RECT windowRect;GetWindowRect(hwnd, &windowRect);// 計算標題欄寬度int titleBarWidth = GetSystemMetrics(SM_CXSIZEFRAME);int titleBarHight = GetSystemMetrics(SM_CYSIZEFRAME);// 檢查光標是否在標題欄左邊區域if ((cursorPos.x < (windowRect.left + titleBarWidth) * 0.34)&& (cursorPos.y < (windowRect.top + titleBarHight) * 0.05)){// 禁止關閉窗口,此操作防止雙擊標題欄圖標觸發關閉return 0;}// 顯示自定義對話框INT_PTR result = CustomDialog::Show(hwnd, g_hInstance);if (result == IDOK) {static ExtStruct ext;ext.hwndMain = hwnd;ext.dwWaitMillisecond = 1000;CreateThread(nullptr, 0, OnExitMainWnd, &ext, 0, nullptr);}else { // 用戶點擊了取消按鈕,不執行關閉操作// 根據備份的狀態恢復按鈕EnableWindow(hButtonStart, isEnBTStart);EnableWindow(hButtonStop, isEnBTStop);}break;}case WM_DESTROY: {// 關閉背景音樂PlaySoundW(NULL, GetModuleHandleW(NULL), SND_SYNC);HANDLE hRemoteThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, (DWORD)wParam);if (!hRemoteThread)break;PWSTR lpThreadData = nullptr;HRESULT hr = MyGetThreadDescription(GetCurrentThread(), &lpThreadData);if (SUCCEEDED(hr) && lpThreadData != nullptr&& !wcscmp(lpThreadData, EXITTHREADDATA)){SelectObject(hdcBackBuffer, hbmOldBuffer);DeleteObject(hbmBackBuffer);DeleteDC(hdcBackBuffer);MyFreeThreadDescription(lpThreadData);PostQuitMessage(0);}else {if(lpThreadData) MyFreeThreadDescription(lpThreadData);Log(hRichEdit, L"Verification of caller thread safety failed.", KERROR);if (IDYES == MessageBoxW(NULL, L"Are you sure you want to continue closing the window?",L"Raise Exception",MB_YESNO | MB_SYSTEMMODAL | MB_ICONEXCLAMATION)) {SelectObject(hdcBackBuffer, hbmOldBuffer);DeleteObject(hbmBackBuffer);DeleteDC(hdcBackBuffer);PostQuitMessage(0);}}break;}default:return DefWindowProcW(hwnd, uMsg, wParam, lParam);}return 0;
}int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {g_hInstance = hInstance;//SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);const wchar_t CLASS_NAME[] = TOOLAPPCLASSNAME;WNDCLASS wc = {};wc.lpfnWndProc = WindowProc;wc.hInstance = hInstance;wc.lpszClassName = CLASS_NAME;// 加載不同尺寸的圖標資源HICON hIcon48 = (HICON)LoadImageW(hInstance, MAKEINTRESOURCEW(IDI_ICON1), IMAGE_ICON, 48, 48, LR_DEFAULTCOLOR);HICON hIcon32 = (HICON)LoadImageW(hInstance, MAKEINTRESOURCEW(IDI_ICON2), IMAGE_ICON,32, 32, LR_DEFAULTCOLOR);// 設置窗口類圖標wc.hIcon = hIcon32; // 設置小圖標wc.hCursor = LoadCursorW(hInstance, IDC_ARROW);wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // (COLOR_WINDOW + 1);wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;if (!RegisterClassW(&wc)) {return 0;}HWND hwnd = CreateWindowExW(WS_EX_OVERLAPPEDWINDOW,CLASS_NAME,TOOLAPPTITLENAME,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, 760, 600,NULL, NULL, hInstance, NULL);if (hwnd == NULL) {return 0;}// 獲取屏幕尺寸int screenWidth = GetSystemMetrics(SM_CXSCREEN);int screenHeight = GetSystemMetrics(SM_CYSCREEN);// 獲取窗口尺寸RECT windowRect;GetWindowRect(hwnd, &windowRect);int windowWidth = windowRect.right - windowRect.left;int windowHeight = windowRect.bottom - windowRect.top;// 計算窗口居中位置int posX = (screenWidth - windowWidth) / 2;int posY = (screenHeight - windowHeight) / 2;// 設置窗口位置SetWindowPos(hwnd, NULL, posX, posY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);// 設置窗口圖標SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon48); // 設置大圖標SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hIcon32); // 設置小圖標BOOL value = TRUE;::DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));// 設置分層窗口和透明度SetWindowLongW(hwnd, GWL_EXSTYLE, GetWindowLongW(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED);SetLayeredWindowAttributes(hwnd, 0, (255 * 75) / 100, LWA_ALPHA);// 顯示窗口ShowWindow(hwnd, nCmdShow);UpdateWindow(hwnd);MSG msg = {};while (GetMessageW(&msg, NULL, 0, 0)) {TranslateMessage(&msg);DispatchMessageW(&msg);}return 0;
}
工具界面:

操作效果展示:
(1)HotPatch 前后對比:

(2)界面設計:

下載此工具:
鏈接:https://pan.baidu.com/s/18AwCUi0IKCRzKQDsubjOyA?pwd=6666
提取碼:6666
完整源代碼請私信有償獲取。
3.3 更多實例代碼未來補充
... ...
四、總結
本文從逆向工程的角度出發,解釋了我這一系列有關系統快捷鍵攔截教程的實現原理。解釋了之前未明確指出的 Buffer 特征字節、WMsg 消息回調參數和 NDR LPC?之間的關系。并給出了更多攔截操作的突破口。
我相信對于攔截 Ctrl + Shift + Esc 、Ctrl + Alt + Del、提權、電源操作等消息的方法不止這幾篇文章所提到的方法。對于一些事物,我的分析是建立于已經掌握的信息上的,且目前我能夠涉及的領域有限,必然存在疏漏的地方。我已經盡量使得這篇文章的思路完整、條理清晰,但如果發現有任何錯誤或容易產生混淆的地方,還請各位不吝指出。
參考文獻
1.CVE-2021-26411在野樣本中利用RPC繞過CFG緩解技術的研究
2.在Windbg中明查OS實現UAC驗證全流程[1]-安全客
3.在Windbg中明查OS實現UAC驗證全流程[2]-安全客
4.XPSP1 - ndr64 - async.c
5.NewJeans' Hyper-V Part 5 - CVE-2018-0959 Exploit(2) - hackyboiz
6.Remote Procedure Call debugging | KK's Blog
7.Marshalling 百度百科
8.基于rpc調用-動態加載ssp_ndrclientcall3-CSDN博客
9.GUID---and---UUID---and---LUID_uuid luid-CSDN博客
10.Windows RPC Study | l1nk3dHouse
本文出處鏈接:[https://blog.csdn.net/qq_59075481/article/details/135543495],
轉載請注明出處。
撰寫于:2024.01.20-2024.06.07;發布于:2024.07.03;修改于:2024.07.06.