一、序章

RPC是一個非常寬泛的概念,其核心理念是讓一個函數可以在不同進程或者不同計算機之間被調用。微軟在windows中實現了RPC功能,并且windows中有大量的功能調用其實是通過RPC來調用的,比如:服務的加載,關機消息的發送,包括DCOM也是基于RPC的二次封裝.關于詳細的說明推薦閱讀微軟官網: https://learn.microsoft.com/en-us/windows/win32/Rpc/rpc-start-page。由于RPC的規范非常復雜,且相關內容很少,所以我也只是根據文檔和調式盡可能地將我的理解貼上來,如果有誤歡迎指正。

有些EDR會通過Hook RPC調用過程中的一些關鍵點來實現RPC調用的過濾.因此我們的目標就是直接通過Syscall指令直接調用NT系列函數來完成RPC調用,從而繞過EDR的Hook點。

所有的源碼已經放在了文章的最后面供讀者下載。

二、一個簡單的RPC Demo程序作為練手

源碼中MyRPCServer是一個最簡單的RPC服務端,它首先注冊自己為一個RPC服務,協議類型是"ncalrpc",這表示它底層使用"LPC&ALPC 本地過程調用"來完成通信,本文討論的所有RPC都是"ncalrpc"協議類型的RPC。Endpoint名稱是"MyFirstRPC"。UUID是"88888888-6808-11cf-b73b-666666666666"。

MyRPCServer向外只導出一個函數,并打印出4個傳入的參數內容.MyRPCClient是一個最簡單的RPC客戶端,它調用MyRPCServer向外導出的HelloProc函數,并傳入4個參數。MyRPCClient代碼片段如下:

......    RPC_WSTR pszStringBinding = NULL;    CHAR pszString[] = {"123456789"};    WCHAR wcsString[] = { L"123443210" };    DWORD dwLen = (DWORD)strlen(pszString);    status = RpcStringBindingComposeW(wcsUuid,        (RPC_WSTR)pszProtocolSequence,        (RPC_WSTR)pszNetworkAddress,        (RPC_WSTR)pszEndpoint,        (RPC_WSTR)pszOptions,        &pszStringBinding);    if (RPC_S_OK != status)    {        goto _FUNC_END;    }    status = RpcBindingFromStringBindingW(pszStringBinding, &TestBindingHandle);    if (RPC_S_OK != status)    {        goto _FUNC_END;    }    __try    {        HelloProc(TestBindingHandle, (unsigned char *)pszString, wcsString, 0x87654321, 0x123456789ABCDEF0);    }    __except (1) {        goto _FUNC_END;    }......

下面是我們的Server端向外導出的函數的定義,注意,第一個參數是IDL_handle,它只作為一個上下文狀態句柄,并不會真正的傳輸到服務端:

void HelloProc(    /* [in] */ handle_t IDL_handle,    /* [string][in] */ unsigned char *pszString,    /* [string][in] */ wchar_t *wcsString,    /* [in] */ __int32 nInt32,    /* [in] */ __int64 nInt64)

好現在正式開始,雖然MyRPCClient是個32位的EXE,但是我們依然使用64位的windbg調試,因為這樣可以斷在真正的64位NT函數上。在MyRPCClient入口處下批量斷點,這樣下斷點的好處是Alpc相關的函數全部被下斷點,不需要一個一個函數去下斷,方便我們分析整個調用流程:

bm ntdll!*alpc*  1: 00007ffb`759497d4 @!"ntdll!TppAllocAlpcCompletion"  2: 00007ffb`759374c0 @!"ntdll!TpCallbackSendAlpcMessageOnCompletion"  3: 00007ffb`7593a4e0 @!"ntdll!TppAlpcpCallbackEpilog"  4:...  ......

F5,程序斷在了NtAlpcConnectPortEx上:

NTSTATUS NTAPI NtAlpcConnectPortEx(    _Out_ PHANDLE PortHandle,    _In_ POBJECT_ATTRIBUTES ConnectionPortObjectAttributes,    _In_opt_ POBJECT_ATTRIBUTES ClientPortObjectAttributes,    _In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes,    _In_ ULONG Flags,    _In_opt_ PSECURITY_DESCRIPTOR ServerSecurityRequirements,    _In_ PPORT_MESSAGE ConnectionMessage,    _Inout_opt_ PSIZE_T BufferLength,    _Inout_opt_ PALPC_MESSAGE_ATTRIBUTES OutMessageAttributes,    _Inout_opt_ PALPC_MESSAGE_ATTRIBUTES InMessageAttributes,    _In_opt_ PLARGE_INTEGER Timeout);

該函數的第二個參數是目標Alpc Port的OBJECT_ATTRIBUTES,我們查看ConnectionPortObjectAttributes的ObjectName的值:

0:000> dq rdx0000001a`f375eca8  00007ffb`00000030 00000000`000000000000001a`f375ecb8  0000001a`f375ec58 00000000`000000000000001a`f375ecc8  00000000`00000000 00000000`0000000000:000> dq 01a`f375ec580000001a`f375ec58  00000000`0030002e 00000202`eb7be5000000001a`f375ec68  00000000`00000030 00000000`000000000:000> du 202`eb7be50000000202`eb7be500  "\RPC Control\MyFirstRPC"

可以看到ALPC的端口名稱是"\RPC Control\MyFirstRPC",假如我們的RPC服務明確指定了端口名稱,那么系統會為我們在"RPC Control"下創建一個同名的ALPC端口,打開winobj同樣可以看到這個ALPC端口:

再次F5,程序斷在了NtAlpcQueryInformation上,這個函數是用來查詢ALPC屬性的,不關鍵,繼續F5。

0:000> gBreakpoint 37 hitntdll!NtAlpcQueryInformation:00007ffb`7596e1c0 4c8bd1          mov     r10,rcx

接著函數斷在了NtAlpcSendWaitReceivePort函數上,

NTSTATUS NTAPI NtAlpcSendWaitReceivePort(    _In_ HANDLE PortHandle,    _In_ ULONG Flags,    _In_ PPORT_MESSAGE SendMessage,    _Inout_opt_ PALPC_MESSAGE_ATTRIBUTES SendMessageAttributes,    _Out_ PPORT_MESSAGE ReceiveMessage,    _Inout_opt_ PSIZE_T BufferLength,    _Inout_opt_ PALPC_MESSAGE_ATTRIBUTES ReceiveMessageAttributes,    _In_opt_ PLARGE_INTEGER Timeout);

這個函數的第三個參數是關鍵點,表示要發送的ALPC消息,我們直接查看數據:

0:000> db r8 l10000000000`00f2afd0  48 00 70 00 00 40 00 00-00 00 00 00 00 00 00 00  H.p..@..........00000000`00f2afe0  00 00 00 00 00 00 00 00-00 00 00 00 0d f0 ad ba  ................00000000`00f2aff0  00 00 00 00 00 00 00 00-01 00 00 00 00 00 00 00  ................00000000`00f2b000  0d f0 ad ba 88 88 88 88-08 68 cf 11 b7 3b 66 66  .........h...;ff00000000`00f2b010  66 66 66 66 01 00 00 00-01 00 00 00 00 00 ad ba  ffff............00000000`00f2b020  0d f0 ad ba 00 00 00 00-00 00 00 00 0d f0 ad ba  ................00000000`00f2b030  00 00 00 00 00 00 00 00-96 60 60 04 0d f0 ad ba  .........``.....00000000`00f2b040  0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba  ................

所有的ALPC消息前0x28個字節的數據是PORT_MESSAGE結構作為頭部,緊跟著后面的是實際負載,也就是我們的PRC PDU(Protocol Data Units),根據消息頭的描述,PDU有0x48的長度。RPC PDU是由PDU Header(RPC請求頭,必須有),PDU body(經過打包的要傳輸的實際參數數據,非必須),身份驗證數據(非必須),這三坨數據構成。

PDU Header的第一個字節表示請求的類型,1表示這次是Bind請求,RPC在函數功能在調用前都需要首先調用Bind請求。第0x0C開始是我的的RPC的UUID。具體Bind請求的每個字段的含義我們其實不用詳細了解,因為對于同樣的RPC請求每次的Bind請求內容都是固定的,我們只要第一次獲取到Bind請求體保存下來即可。

這個函數的第五個參數(0x00f2afd0)是服務端針對本次請求返回的數據。

0:000> dq rsp+800000000`00b4e0e0  00000000`00000000 ffffffff`ffffffff00000000`00b4e0f0  00000000`00002000 00000000`00efe9d000000000`00b4e100  00000000`00f2afd0 00000000`00b4e14800000000`00b4e110  00000000`00f2ac80 00000000`00000000

返回的內容決定了我們的Bind請求是否執行成功,同樣是個PORT_MESSAGE結構的ALPC消息,我們單步運行到NtAlpcSendWaitReceivePort函數執行結束然后查看其內容:

0:000> p 0:000> db 00f2afd000000000`00f2afd0  48 00 70 00 02 40 00 00-e0 33 00 00 00 00 00 00  H.p..@...3......00000000`00f2afe0  1c 4a 00 00 00 00 00 00-b4 3a 00 00 00 00 00 00  .J.......:......00000000`00f2aff0  de 5b 54 00 00 00 00 00-01 00 00 00 00 00 00 00  .[T.............00000000`00f2b000  00 00 00 00 88 88 88 88-08 68 cf 11 b7 3b 66 66  .........h...;ff00000000`00f2b010  66 66 66 66 01 00 00 00-01 00 00 00 00 00 ad ba  ffff............00000000`00f2b020  0d f0 ad ba 00 00 00 00-00 00 00 00 0d f0 ad ba  ................00000000`00f2b030  00 00 00 00 00 00 00 00-96 60 60 04 0d f0 ad ba  .........``.....00000000`00f2b040  0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba  ................

怎么判斷Bind請求是否成功呢?如上,返回的Request Header第一個字節必須為0x01(Bind),Request Header從第0x0C開始是RPC的UUID.這樣可以判定為Bind成功。

這里關于Request Header我需要額外補充一點,即使是同樣的Bind請求,不同的底層協議它的Request Header的結構也可能是不一樣的。同樣是MyRPCServer這份代碼,我只是將協議從"ncalrpc"改成了了"ncacn_ip_tcp",Bind請求的請求頭部結構就發生了變化,下圖為我通過Wireshark抓包獲取的MyRPCServer代碼的"ncacn_ip_tcp"協議版本的RPC Bind請求:

前0x36為ETH&TCPIP協議頭,之后為RPC PDU Header,可以清晰的看到其結構為rpcconn_bind_hdr_t結構(這個結構體是開源且規定好的)。而且在這里我們可以看到0x0B代表了Bind請求。為什么同樣是Bind請求,"ncalrpc"就是0x01,"ncacn_ip_tcp"就是0x0B呢?這個問題我會放在文章末尾討論,這不是整個流程的重點。

繼續F5,再次斷在了NtAlpcSendWaitReceivePort函數上,這次是真正的Request請求,我們查看請求的具體內容:

前0x28個字節是PORT_MESSAGE結構,根據PORT_MESSAGE的描述,消息體RPC PDU長度為0xA0,PDU Header第一個字節為0x00,表示是Request請求,PDU Header 長度為0x40,后面緊跟著的是PDU Body,長度為0x60。PDU Body里存放的是函數參數經過打包之后的數據。

我接下來一個參數一個參數的來講解PDU Body里面的內容以及對應的參數:

(1)紅框內是第一個參數(PDU Body偏移0x00):"pszString"是一個窄字符串數組,這是個不固定長度的數組,所以它的編碼方式應該是如下圖,第一個4字節表示數組的最大長度為0x0A,第二個4字節0x00表示從多少偏移開始為真正有效的數組內容,第三個4字節表示數組實際長度為0x0A,之后緊跟著的是0x0A個單位的實際數組內容,最后一個0x00是字符串結尾。注意,這里所謂的長度并不是字節長度,而是有多少個單位元素長度。

(2)綠框內是第二個參數(PDU Body偏移0x18):"wcsString"是一個寬字符串數組,這是個不固定長度的數組,所以它的編碼方式應該和"pszString"一樣,第一個4字節表示數組的最大長度為0x09,第二個4字節0x00表示從多少偏移開始為真正有效的數組內容,第三個4字節表示數組實際長度為0x09,之后緊跟著的是0x09個單位的實際數組內容,最后一個0x0000是字符串結尾。

(3)藍框內是第三個參數(PDU Body偏移0x38):"nInt32"是一個四字節的int類型。我們可以看到"nInt32"參數的數據并不是緊跟著"wcsString"的數據尾部的,而是存放在了"wcsString"尾部兩個字節之后。這是因為PUD Body的參數編碼是需要進行數據對齊的,對齊方式是根據數據內容的大小進行1,2,4,8最大8字節的對齊。"nInt32"長度為4個字節,所以需要4字節對齊。所以需要在空了兩個字節之后的位置存放。

(4)金框內是第四個參數(PDU Body偏移0x40):"nInt64"是一個八字節的int64類型,它因為需要8字節對齊,所以需要空4個字節之后的位置存放。

至此,所有參數都已經編碼完成,這樣我們就可以通過連續調用兩次NtAlpcSendWaitReceivePort函數來自己構造RPC bind和RPC Request完成PRC的調用。這里要說一點,由于ALPC系列函數微軟只作為內部使用,所以其參數并未公開,調用NtAlpcSendWaitReceivePort函數的參數和屬性的填寫我是通過windbg調試得到的,由于這并不是本文的重點,所以這里不具體展開講解。具體請參考AlpcDoMyRPCClient工程源碼。

三、開始嘗試分析定位系統的RPC調用

我們已經對RPC有了基本認識,開始我們的最終目的:1.確定某個我們感興趣的系統功能是否為RPC方式調用。2.分析定位出這個系統功能是怎么調用的,從而我們可以通過代碼實現該RPC功能的調用。現在以Explorer "以管理員身份運行" 某個EXE這個功能為例子開始分析:

我的具體思路:

1.首先要定位該功能所在的進程,這里我們隨便用一個窗口查看工具就可以定位到右鍵彈出菜單是Explorer進程的窗口。

2.對所有的RPC關鍵函數下特殊的條件斷點(NdrClientCall3,NdrClientCall2,NdrAsyncClientCall,Ndr64AsyncClientCall,NdrDcomAsyncClientCall,Ndr64DcomAsyncClientCall),只要當我們點擊 "以管理員身份運行" 按鈕時該斷點立刻斷下,并且我們F5后立刻彈出UAC窗口,則我們就可以大概率定位斷下來的的。

3.我們通過代碼模仿我們定位到的系統RPC調用,如果同樣能彈出UAC窗口,則反向印證我們分析定位的沒有問題。

開始實戰:

通過工具可定位到該右鍵菜單為Explorer進程,我們直接通過windbg附加到Explorer進程,并對RPC函數下斷。這時候重點來了:我們會發現Explorer進程并不是像我們想象的那樣,只有在點擊 "以管理員身份運行" 按鈕時才會觸發我們下的RPC斷點,Explorer進程無時無刻不在頻繁的進行著RPC調用,我們的斷點會被頻繁的斷下。我的思路就是盡量想辦法排除干擾項,在我沒有點擊 "以管理員身份運行" 按鈕時產生的RPC請求一定都是干擾項。我們只關注點擊后產生的RPC請求。因此我們需要一個條件斷點。

在設置條件斷點之前我們需要先了解一下RPC函數:

CLIENT_CALL_RETURN RPC_VAR_ENTRY Ndr*ClientCall*(  PMIDL_STUB_DESC pStubDescriptor,  PFORMAT_STRING  pFormat,     ...            );
CLIENT_CALL_RETURN RPC_VAR_ENTRY Ndr*ClientCall*(  MIDL_STUBLESS_PROXY_INFO *pProxyInfo,  unsigned long            nProcNum,  void                     *pReturnValue,  ...                     );

所有的RPC函數都可以歸類成上邊這兩種類型,這兩種類型函數的第一個參數都是用來描述遠端服務器信息的,其中就包含了RPC服務的GUID標識符。第二個參數是表示要調用遠端RPC服務器的哪個函數,"pFormat"用于詳細解釋函數的參數的各種屬性,"nProcNum"則表示是RPC服務器導出的第幾個函。兩種函數他們的參數的目的都一樣,只不過參數的類型有所不同。

所以我們需要一個條件斷點,這個條件斷點有兩個功能:1.根據第一個參數里的GUID和第二個參數來決定哪些是"干擾項",從而跳過"干擾項".2.如果斷下來,則打印出:1.RPC服務的GUID.2.pFormat或nProcNum。

bm rpcrt4!Ndr*ClientCall* "    r $t0 = @rcx    r $t1 = @rdx    .if(@rdx > 0x1000)    {        r $t2 = poi(@$t0) + 4        .if( ((dwo(@$t2) == 0xe60c73e6) & (qwo(@$t1) == 0x40000`00006800)) )        {            gc        }        .else        {            .echo Format            db @$t2 l10            dd @$t2 l1            dq rdx l1        }    }    .else    {        r $t2 = poi(poi(@$t0)) + 4        .if(@$t2 == 0x04)        {            gc        }        .else        {            .if( ((dwo(@$t2) == 0xb18fbab6) & (@$t1 == 0x0A)))            {                gc            }            .else            {                .echo nNumber                db @$t2 l10                dd @$t2 l1                r rdx            }        }    }"bc 1-2bc 4-5bc 7bc 9-12bc 16

我編寫好的可以直接使用的已經排除了大部分的"干擾項"的腳本文件MyRPCBreakPoint.txt已經放在了附件里以供直接使用。具體windbg斷點語法不是本文重點,可以通過微軟官網了解,會有詳細的說明。上邊是斷點腳本文件的Demo演示:

第2行和第3行申請了兩個偽寄存器t0 t1,用于存放第一個和第二個參數。

第4行".if(@rdx > 0x1000)"用來判斷RPC函數類型,如果小于等于1000則說明第二個參數是函數序號,如果大于1000則說明是函數格式。

第6行和第21行申請了偽寄存器t2,從t0中取出GUID指針,并將GUID指針放入t2中,由于兩種函數的參數不一樣,一個是PMIDL_STUB_DESC 類型,一個是MIDL_STUBLESS_PROXY_INFO,所以取GUID指針的方式也不一樣。

第7行和第28行是重點,如果滿足條件則說明是"干擾項"則會 "gc" 也就是繼續運行,否則會打印出:1.RPC服務的GUID。2."pFormat函數格式" 或者是 "nProcNum函數序號"。

第42行開始是清理除了(NdrClientCall3,NdrClientCall2,NdrAsyncClientCall,Ndr64AsyncClientCall,NdrDcomAsyncClientCall,Ndr64DcomAsyncClientCall)這幾個RPC函數之外的函數斷點,由于我們下的是批量斷點,所以也會誤下一部分命中我們名字規則的斷點。請注意,這里每個人的機器肯定是不一樣的,所以讀者需要根據自身下斷點情況來自己清理斷點。

接下來我實際演示一遍如何將"干擾項"放入 ".if" 判斷中,從而排除干擾項:

首先Windbg命令行輸入"$$>a

如上圖,輸入腳本文件路徑,然后F5跑起來,windbg立刻會斷下。腳本輸出"Format"表示函數是兩種類型的第一種。0xe60c73e6是RPC服務GUID的前4個字節,為了簡單我的腳本文件并沒有完整判斷GUID的16個字節,只判斷了前4個字節。0x4000000006800是函數PFORMAT_STRING的前8個字節,為了簡單也是只取了前8個字節。將0xe60c73e6和0x4000000006800放入第7行的".if"判斷中,即可排除此"干擾項"。

將"干擾項"加入后繼續F5:

腳本輸出"nNumber"表示函數是兩種類型的第二種,0xb18fbab6是RPC服務GUID的前4個字節,0x0A是函數序號。將上面的0xb18fbab6和0x0A放入第28行的.if判斷中,即可排除此"干擾項"。

按照上邊的方法即可慢慢排除所有干擾項,直到在點擊"以管理員身份運行"按鈕前不會有任何RPC函數斷下.這樣的方法缺點就是比較耗時,但是優點就是方法比較通用,大多數RPC功能都可使用此方法定位,而且可以用來確定一個功能是否是通過RPC調用的。

接下來我們點擊右鍵彈出菜單的"以管理員身份運行",Windbg立刻斷下:

根據我們的腳本輸出我們可以看到GUID為201ef99a開頭:{201ef99a-7fa0-444c-9399-19ba84f12a1a},函數序號為0的的RPC請求斷下。查看堆棧信息,我們可以看到是模塊名稱為windows_storage.dll的AicLaunchAdminProcess函數調用過來的。再隨便翻看一下函數參數,我們發現函數的第6個參數剛好就是我們"以管理員身份運行"的EXE的路徑,再看函數名稱也很像我們的目標函數。那么這個RPC調用被我們列為重點懷疑對象。

先不急去驗證,我們繼續F5,直到彈出UAC窗口才算分析結束,windbg再次斷下:

這次的GUID為{e1af8308-5d1f-11c9-91a4-08002b14a0fa},函數序號為0x08,我們查看函數堆棧,發現是上面的AicLaunchAdminProcess函數調用Ndr64AsyncClientcall函數后其內部又再次調用的RPC請求,這可能會讓人有些迷惑,其實這是RPC協議中規定好的一個基礎RPC服務,名字叫"Endpoint Mapper",它的作用就是保存好已經注冊的其他的RPC服務的端口號。因為RPC服務在注冊時是可以不填寫端口號的,這樣系統會為該RPC服務生成隨機端口號并將其保存在"Endpoint Mapper"中。該RPC服務不是我們本次討論的重點,這個留給讀者作為第一個練手用的RPC服務。

繼續F5,UAC窗口成功彈出,這期間一共調用了兩次RPC請求,已知第二個RPC請求是"Endpoint Mapper"請求,第一個RPC根據函數名字和參數都有很大的可能性是"以管理員身份運行"按鈕對應的RPC請求。

接下來我們需要使用在Github上的一個叫做"RpcView"的開源工具,下載地址是https://github.com/silverf0x/rpcview。它可以將目標RPC服務的所有接口導出到IDL文件中,具體使用方法如圖:

管理員方式運行RpcView.exe,然后CTRL+F搜索GUID是0x201ef99a開頭的RPC服務。右鍵Decompile,此RPC服務的接口就會以文本的方式被導出。接下來我們根據函數聲明和windbg調試,就可以依葫蘆畫瓢的填寫好我們的參數:

RPC_BINDING_HANDLE UacBinding = 0;STARTUPINFOW* StartInfo = 0;WCHAR wcsExePath[] = { L"C:\\Windows\\system32\otepad.exe" };WCHAR wcsParam[] = { L"\"C:\\Windows\\system32\otepad.exe\" aabbcc.txt" };DWORD dwNumber = 0x11;DWORD dwNumber2 = 0x04080414;WCHAR wcsDir[] = L"D:\\data";WCHAR wcsDesktop[] = { L"WinSta0\\Default" };Struct_22_t Struct22;memset(&Struct22, 0, sizeof(Struct_22_t));Struct22.StructMember8 = 1;Struct22.StructMember9 = 5;DWORD64 dwNumber3 = 0;DWORD dwNumber4 = -1;PROCESS_INFORMATION ProcInfo;memset(&ProcInfo, 0, sizeof(PROCESS_INFORMATION));long nNumberOut = 6; status = RpcStringBindingComposeW((RPC_WSTR)L"201ef99a-7fa0-444c-9399-19ba84f12a1a", (RPC_WSTR)L"ncalrpc", 0i64, 0i64, 0i64, &StringBinding); status = RpcBindingFromStringBindingW(StringBinding, &UacBinding); LaunchAdminProcess(UacBinding,    wcsExePath, wcsParam,    dwNumber, dwNumber2,    wcsDir, wcsDesktop,    &Struct22, dwNumber3, dwNumber4,    (Struct_56_t*)&ProcInfo, &nNumberOut);

其中wcsExePath是要運行的EXE路徑,wcsParam是運行參數,ProcInfo是PROCESS_INFORMATION結構,用于返回進程創建信息。具體調用方式請參考附件中的源碼。我們運行測試代碼,成功以管理員權限運行了notepad.exe程序并傳遞參數,反向印證了我們的猜測:第一個RPC請求就是"以管理員身份運行"按鈕對應的RPC請求。

Good!我們的工作已經完成了一大半,接下來我們要通過NTAlpc*函數實現我們的RPC請求:

為了方便起見,就不再調試Explorer來查看NTAlpc函數參數了,我們直接調試只有一個RPC請求以管理員方式運行Notepad.exe的RPCRunAdminProcess工程。像第二章節一樣老樣子,windbg打開RPCRunAdminProcess工程,"bm ntdll!alpc*" 對alpc相關函數批量下斷點,然后F5:

0:000:x86> gBreakpoint 52 hitntdll!NtAlpcConnectPort:00007ffb`0750dfc0 4c8bd1          mov     r10,rcx0:000> dq rdx00000000`0037deb0  00000000`002c002a 00000000`009cbdc800000000`0037dec0  0000000c`02090000 009c0000`000000010:000> du 009cbdc800000000`009cbdc8  "\RPC Control\epmapper"

windbg斷下,查看端口號,是"Endpoint Mapper"服務的端口號,上邊已經講過,對于隨機端口的RPC服務,要先通過RPC請求"Endpoint Mapper"服務來獲取目標RPC服務的端口號。這不是本文的重點,留給讀者用來練手。

我們一路F5,跳過和"Endpoint Mapper"相關的Alpc操作,直到再次斷在AlpcConnect函數:

其端口號是"LRPC-50ddf8142f3561885f" 剛好和我們上面通過RpcView.exe查詢到的RPC服務GUID對應的端口號一致。

F5,函數斷在了NtAlpcQueryInformation上,不重要,繼續F5,斷在函數NtAlpcSendWaitReceivePort上,這次是Bind操作,我們第二章已經分析過,跳過,再次F5,再次斷在函數NtAlpcSendWaitReceivePort上,這次是Request請求:

第二章節已經講過Request請求里面的的參數是如何編碼的了,這里我就不一一講解了,我只說第二章沒有遇到的參數類型。如上圖,wcsExePath和wcsParam兩個字符串數組前邊還有紅色框標注的4個字節的數據。我們查看IDL文件對該函數的聲明:

long LaunchAdminProcess(        [in][unique][string] wchar_t* arg_2,        [in][unique][string] wchar_t* arg_3,        [in]long arg_4,        [in]long arg_5,        [in][string] wchar_t* arg_6,        [in][string] wchar_t* arg_7,        [in]struct Struct_22_t* arg_8,        [in]unsigned __int3264 arg_9,        [in]long arg_10,        [out]struct PROCESS_INFORMATION* ProcInfo,        [out]long* arg_12);

這兩個字符串數組前增加了[unique]標記,該標記表示該參數是"unique pointers",這東西沒有明確的中文翻譯,本文我就稱其為唯一指針。它的作用是表示該指針指向的對象僅有這一個指針指向它。它的好處是可以降低系統對參數編碼時的處理量。紅框中的數據是唯一指針的"Referent Identifier"引用標識.引用標識由兩個WORD構成,第一個WORD是0x4=0,1x4=4,2x4=8...nx4=4n。n表示是函數的第幾個指針。第二個WORD固定為0x0002。

藍色框內的Struct22是個結構體,它的編碼方式其實很簡單,就是內部各個元素的集合,總長度為所有元素加起來的長度,一共占用0x30個字節。

單步運行到NtAlpcSendWaitReceivePort函數返回處,系統會彈出UAC窗口,我們點擊"是",可以看到Notepad進程被創建:

如圖,命令行是我們輸入的命令行,PID是7744(0x1E40),主線程TID是23268(0x5AE4)。這時候我們再查看NtAlpcSendWaitReceivePort函數返回的數據的內容:

我們可以看到0x28偏移開始是PDU header的數據,第一個字節0x03表示的是這是RESPONSE 請求返回數據。從第0x40開始是PDU body的數據,藍框標識的是需要返回的第一個參數ProcInfo,我們可以看到ProcInfo里的PID和TID也剛好和我們通過工具查看的Notepad的PID和TID對應上了。紅色框是需要返回的第二個參數arg_12,這是一個long類型參數。

還沒結束,Notepad被創建的時候被掛起了,通過IDA查看windows.storage.dll的AicLaunchAdminProcess函數是怎么處理的:

PROCESS_INFORMATION 輸出到了this+0x178

然后從0x180處取得主線程的Handle,調用ResumeThread恢復主線程的運行。

四、結尾

關于通用性:以運行管理員進程這個RPC功能為例,我測試了win7和win10最新版本(筆者寫文章時最新),都可以成功調用,winxp系統不存在管理員運行這個功能,也沒有Alpc相關函數,所以肯定不可以。我又看了幾個RPC,在GUID和Version都相同的情況下,向外導出的接口仍然可能一樣也可能會不一樣(微軟還真是任性),這個需要讀者自己去具體RPC具體分析了。

比如"saloyun"同學提出的DNS解析RPC,看下面的截圖,雖然GUID都是{45776b01-5956-4485-9f80-f428f7d60129},Version都是(2.0),但是函數定義完全不一樣:

關于附件的源碼:

RpcDemo解決方案:

MyRPCServer工程(x86編譯)是一個最簡單的RPC服務程序,用于入門學習。

MyRPCClient工程(x86編譯)是一個最簡單的RPC客戶端程序,向MyRPCServer發送HelloProc()請求。

RPC_IDL工程(x86編譯)僅僅用來將IDL文件編譯成STUB文件。

RPCRunAdminProcess工程(x86編譯)是通過RPC彈出UAC窗口創建管理員進程。

AlpcDemo解決方案:

AlpcDoMyRPCClient工程(x64編譯)是通過Alpc的方式模擬RPC請求向MyRPCServer發送HelloProc()請求。

AlpcDoRunAdminProcess工程(x64編譯)是通過Alpc的方式模擬RPC請求彈出UAC窗口創建管理員進程。這里要注意,工程中" #define EXECUAC_ALPC_NAME L"\RPC Control\LRPC-50ddf8142f3561885f "定義的Alpc端口號讀者一定要自己通過RpcView獲取(獲取方式文章中有詳細講解)或者通過Endpoint Mapper這個RPC服務自己動態獲取,這個端口號每次開機都不一樣,是系統隨機生成的。

附件密碼: pediy

看到有人不理解繞過了EDR的什么,我這里額外補充一下:正如第一章中所說,系統中有大量的調用其底層實現是通過RPC實現的,DCOM,服務的加載,關機消息的發送,等等等。以我們已經分析過的"以管理員身份運行"這個功能為例,EDR可以HOOK NdrXXClientCallXX系列函數,解析出第一個參數內的GUID是201ef99a-7fa0-444c-9399-19ba84f12a1a,第二個參數是0或者是對應的函數Format,就可以判斷該進程正調用"以管理員身份運行"這個功能,并從之后的參數中獲得進程的路徑和參數。所以所有這些底層依賴RPC的系統功能理論上都能被EDR過濾到。

所以,我們通過 NtAlpcXXX系列函數封裝RPC調用,從而可以繞過上層的所有EDR針對RPC請求的過濾攔截。