Windows RDP协议 Fuzzing 漏洞挖掘研究
基本介紹
本文描述了我在Fuzzing Windows RDP 客戶端和服務端方面所做的一些嘗試和挑戰,以及Crsahs分析。
Microsoft 的遠程桌面協議 (RDP) 持續受到安全社區的關注。從 2019 年發現的幾個可能危及數百萬面向互聯網的服務端的關鍵漏洞,到RDP 被攻擊者用作主要的初始訪問向量之一。
我對這個目標最初的興趣是與 VM 相關的。因為連接到 Azure Windows 機器或 Hyper-V 虛擬機的默認方式是 RDP,所以我認為 RDP 是比較重要的目標。
我很快就發現了 Park、Jang、Kim 和 Lee在 BlackHat Europe 2019 上發表的關于 RDP Fuzzing的精彩分享。演講者在短短幾個小時內使用一個并不高效的 fuzzer 發現了幾個漏洞,因此我決定在他們的工作基礎上進行構建新的挖掘工具,將 fuzzing 能力擴展到其他方面,改進其性能并挖掘到我自己的 RDP 遠程代碼執行 (RCE)漏洞。
不幸的是,并不是所有的Fuzzing工具都能挖到嚴重的漏洞。我目前還沒有挖到 RDP 的RCE漏洞,但我確實設法找到了一些bugs,并更好地了解了協議、其組件以及Fuzzing過程和工具。我開發的Fuzzing框架足夠通用,有助于對其他目標進行Fuzzing。
在這篇文章中,我將分享我執行上述所有操作的過程。首先,我將概述 RDP 和開發的Fuzzing設置,然后將分享我臨的挑戰以及如何處理的方法,最后,我將回顧在此過程中發現的幾個漏洞。
1.RDP 協議
遠程桌面協議是用于遠程訪問 Windows 計算機的通用協議,Malwaretech 最近將其述為“協議的協議”。RDP 允許在每個連接中運行多個通道,并且每個通道都有不同的用途。這意味著每個通道都有自己的代碼來處理其數據、自己的結構定義和數據流。這實質上意味著 RDP 中確實存在多種協議。
RDP 連接中的通道
RDP 通道可以是靜態的,也可以是動態的,但對于我們的目的而言,兩者之間的區別并不重要。如果你想了解更多關于RDP連接的內部運作原理,我建議你閱讀下文。
2.RDP Fuzz的挑戰
在“標準”Fuzzing場景中,有一個程序讀取由Fuzzer控制的輸入,輸入可以是文件或任何類型的數據流。然后程序處理數據,同時Fuzzer監視生成的數據的代碼覆蓋率。基于該覆蓋范圍,Fuzzer對輸入進行變異,再次將變異的輸入發送到程序,然后重復該過程。
RDP Fuzzing是不同的,因為我必須始終有一個 RDP 連接處于活動狀態。Fuzzer可以輸入到程序中的數據需要作為協議數據單元 (PDU) 發送到特定通道(在Fuzzing期間也應處于活動狀態)的頂部,并在開放連接中進行。如前所述,每個通道都有自己的協議,因此需要逐個通道進行Fuzzing。這給Fuzzing過程帶來了以下挑戰(這也可能適用于其他協議/網絡相關的Fuzzing):
客戶端-服務端架構——在傳統的Fuzzing中,Fuzzer可以簡單地運行目標應用程序并提供其輸入。在客戶端-服務端場景中,目標應用程序在連接的一側運行,而輸入從另一側發送。在RDP的情況下,雙方通常在不同的機器上。
狀態性- RDP 是一種有狀態協議,這意味著你必須在對測試用例進行Fuzzing時考慮連接的狀態。這會嚴重影響Fuzzing的穩定性。
多輸入Fuzzing——當對接受文件作為輸入的目標進行Fuzzing(文件格式Fuzzing)時,Fuzzer對目標的所有輸入都包含在單個文件中。相反,當你對協議進行Fuzzing時,你可能需要發送一些連續的消息才能到達有效的代碼路徑。
尋找目標代碼——當你使用覆蓋率引導的Fuzzing時,你通常需要向Fuzzer表明它需要在什么時候開始監控代碼覆蓋率(即,處理你輸入的目標函數是什么?)。RDP 有許多負責其操作的組件,在某些情況下,找到正確的位置可能是一項艱巨的任務。
這四個挑戰是我預計Fuzzing RDP的主要挑戰。在文中,我將討論如何克服這些挑戰,以及在工作后期出現的其他挑戰。
Fuzzing技術細節
在本節中,我將介紹技術細節,以及在工作過程中遇到的挑戰。還將解釋如何解決它們以實現有效的Fuzzing設置。我在 GitHub 上創建了一個工作存儲庫,其中包含我在此工作中編寫的所有代碼。
1.客戶端-服務端架構
在這個工作中,我想要Fuzzing Windows 的 RDP 服務端及其 RDP 客戶端。對 RDP 服務端進行Fuzzing的動機很明顯:攻擊者可以使用它來遠程入侵 Windows 服務端并獲得對其的訪問權限。對 RDP 客戶端進行Fuzzing的動機是不同的,如果攻擊者已經獲取了 RDP 服務端權限的場景,然后等待受害 RDP 客戶端連接過來。一旦受害者連接上,攻擊者也可以通過 RDP 客戶端獲取受害者的機器權限。當管理員連接到他們管理的服務端時可能會發生這種情況,由于 Hyper-V 利用 RDP 訪問其虛擬機,甚至可以用作 VM 逃逸。
RDP 的 fuzzer 需要具有以下基本組件:
?檢測引擎跟蹤代碼覆蓋并檢測崩潰
?變異引擎產生新輸入
?輸入饋送器通過適當的目標通道發送fuzzer的測試用例
?目標二進制文件狀態由檢測引擎跟蹤
客戶端和服務端的Fuzzing配置不同,但也有一些相似之處。
基本Fuzzing配置
在目標方面,有以下內容:
?Fuzzer – 定制的 afl-fuzz.exe
?檢測引擎——定制的 winafl.dll,使用定制的 DynamoRIO 和in_app檢測模式
?輸入 ——被寫入目標和輸入發送者共享目錄中的中間文件的變異 PDU
在輸入發送方,有一個組件:
?代理——讀取中間文件并在目標通道上發送輸入
將輸入的生成與向目標的傳輸分離,允許輸入在被目標處理之前從 RDP 連接的一側移動到另一側。此外,我使用 WinAFL 的應用內檢測模式來不中斷正常的執行流程。
為了使覆蓋引導的Fuzzing工作,輸入和它們觸發的代碼路徑之間必須存在一一對應的關系。為了實現這一點,我開發了“background fuzzing”,它將Fuzzer PDU 與常規 PDU 區分開來,并且只跟蹤前者的代碼路徑。這是必不可少的,因為我只希望Fuzzer跟蹤我自己測試用例的覆蓋范圍,而不是通過連接發送的隨機 PDU。
為了說明這一點,讓我看看在Fuzzing將音頻從 RDP 服務端重定向到客戶端的RDPSND虛擬通道時,類似的事情會是什么樣子。根據官方文檔,每個 PDU 的第一個字節表示發送的消息類型。
資料來源:[ MS-RDPEA ]
msgType字段支持的值為0x01到0x0D。在這種情況下,可以通過以下方式使用第一個字節的最高有效位作為fuzz標記:
?在發送 PDU 之前,代理會轉換第一個字節的最高有效位。
?WinAFL 在處理消息之前檢查第一個字節的最高有效位。如果該位打開,WinAFL 將關閉該位并跟蹤此消息的代碼覆蓋率。如果該位關閉,WinAFL 將忽略該消息并且不跟蹤任何覆蓋范圍。
在了解了客戶端設置和服務端設置之間的相似之處之后,讓我看看它們之間的區別,先從客戶端開始。
2.客戶端Fuzz配置
Windows RDP 客戶端是mstsc.exe,但處理虛擬通道數據的大部分邏輯都在客戶端加載的mstscax.dll中。
Hyper-V 虛擬機的遠程訪問客戶端vmconnect.exe也使用mstscax.dll作為其核心功能。
Windows RDP 客戶端
為了簡單和高效,我在同一臺機器上同時執行客戶端和服務端(目標和代理)(使用客戶端連接到 localhost/127.0.0)。為了允許并行Fuzzing,我還使用了mimikatz來修補服務端,使其允許并發 RDP 連接。
這些是對客戶端進行Fuzzing時的設置組件:
?目標 - mstsc.exe和mstscax.dll的目標模塊
?檢測引擎 – 創建客戶端進程的 DynamoRIO 和 winafl.dll,報告代碼覆蓋率的 DynamoRIO 客戶端
?變異引擎——在同一臺機器上運行的 AFL-Fuzz 并將新的測試用例寫入文件
?輸入饋送器——我的 RDPFuzzAgent,它打開服務端的句柄并在選定的虛擬通道上發送 PDU。代理從 AFL-Fuzz 創建的文件中獲取每個測試用例,并將它們發送到目標通道上。
使用這些組件,我能夠實現大約每秒 50-100 次執行的執行速度。
3.服務端Fuzz配置
為了找到包含 RDP 服務端主要邏輯的目標二進制文件,可以簡單地查看遠程桌面服務服務。
PS C:\> gci HKLM:\SYSTEM\CurrentControlSet\Services\$((Get-Service -Name "Remote Desktop Services").Name)Hive: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TermService Name Property ---- -------- Parameters ServiceDll : C:\Windows\System32\termsrv.dll . . .RDP 服務端的主要邏輯確實在termrv.dll 中,它通過以下命令加載到svchost.exe進程中:C:\Windows\System32\svchost.exe -k NetworkService -s TermService。
最初對服務端進行Fuzzing的計劃與客戶端類似,即在需要運行多個TermService實例的同一臺機器上并行地對目標的幾個實例進行Fuzzing。結果證明這是一項非常具有挑戰性的任務,因為 Windows 默認不支持此操作。當我嘗試手動執行此操作時,我在termrv.dll中看到了一些指向TermService及其注冊表項的硬編碼字符串,因此我決定將精力集中在其他地方,只使用多個 VM 并行fuzz服務端。
在客戶端Fuzzing設置中,我使用服務端API 調用WTSVirtualChannelWrite()將Fuzzing輸入發送到目標模塊。不幸的是,我找不到類似的 API 來讓通過 RDP 連接向服務端發送輸入。
因此,我選擇使用 Ubuntu 機器的定制FreeRDP,是一個開源 RDP 客戶端工具,將輸入發送到fuzz服務端。請注意,這不是Fuzzing的理想配置,這些限制導致服務端Fuzzing的速度大約是客戶端Fuzzing的 1/10。
服務端fuzz統計
這些是對服務端進行Fuzzing時的設置組件:
?目標文件——取決于被Fuzzing的通道。可以是termrv.dll、audiodg.exe、rdpinput.exe等。
?檢測引擎 - 在此設置中,Fuzzer無法控制目標進程的初始化。因此,為了跟蹤代碼覆蓋率,必須檢測一個正在運行的目標。WinAFL Fuzzer 有幾個可以使用的檢測引擎,我選擇 DynamoRIO 是因為它的可擴展性和穩健性。
?DynamoRIO(包括WinAFL)不支持直接連接到活動進程。
?值得注意的是,此附加功能為之前使用 WinAFL 無法fuzz的進程打開了大門——用戶無法控制其創建的進程。特別是,它允許對 Windows 服務進行Fuzzing。
?變異引擎——AFL-Fuzz 在 Windows 機器上運行,并將新的測試用例寫入共享文件夾中的文件。
?輸入饋送器——我們構建的FreeRDP連接到Windows計算機上的服務器,監視共享文件夾中的新測試用例,并在目標RDP通道上發送每個測試用例。
4.狀態性
在運行第一個通道(對客戶端進行Fuzzing)時,我遇到了一個問題:只要在客戶端檢測到兩條無效消息,它就會立即終止連接。
為了避免這個問題,我在代理的邏輯中引入了一些強制協議語法,即限制允許輸入的空間。
?消息大小限制
-最小值、最大值
-分片(例如,如果消息包含一個由 4 字節元素組成的可變長度數組,則它應該可以被 4 整除)
?數據值限制
-只允許特定值
-最小值和最大值
-值必須是 PDU 大小
-每個 PDU 中的值必須不同
我從 Microsoft 的RDP 及其擴展文檔中提取了一些 RDP 語法,一些從相關二進制文件的逆向中提取,其余從跟蹤失敗的目標執行中提取。
例如,此邏輯可用于僅允許以受支持的msgTypes之一開頭的消息,后跟 PDU 的大小和唯一標識符,并且其總大小介于 22 和 122 之間,其余為 2 模 4 .
值得一提的是,通過執行這些強制措施,實際上限制了變異引擎隨心所欲更改測試用例的能力,因此可能會遺漏某些變異。出于這個原因,我嘗試盡可能少地強制執行,同時仍然確保連接不會經常關閉。
這里的另一個重點是,在處理此類問題時,這些語法強制并不是唯一的選擇。在一個特定通道 (GFX) 的情況下,我轉而patch正在Fuzzing的實際目標函數,以便在一組無效消息的情況下它不會關閉連接。這使我能夠繼續fuzz無效消息并始終保持連接打開。在這里,你也有可能發現不會在原始代碼中重現的crashs。這是一個很好的例子,說明Fuzzing需要在確保獲得足夠的執行速度的同時仍然保持目標程序的原始功能以及變異引擎的自由之間的微妙平衡。
5.多輸入Fuzzing
大多數bug都依賴于一系列消息而不是單個消息。我發現的大多數bugs都涉及至少兩條消息,這并非巧合。
為了發現這些bugs,我引入了多輸入Fuzzing。我使用了一個Fuzzer字典來識別新消息的開始及其類型。然后,代理將根據這些字典單詞將輸入拆分為多個 PDU,并一個接一個地發送。
因此,多輸入輸入可能如下所示:
___cmd07 < 1st PDU data> ___cmd02 < 2nd PDU data> ___cmd03 < 3rd PDU data>代理將其轉換為msgType 7、2和 3 的三個消息,以及它們各自的內容。
為了保持Fuzzer創建的輸入與其觸發的代碼覆蓋率之間的一一對應關系,我引入了第二個標記來標識序列中的最后一條消息。只有當 WinAFL 識別出帶有“最后一個序列”標記的調用結束時,它才會完成循環并創建下一個輸入。
雖然多輸入Fuzzing對當前工作至關重要并且富有成效,但我還發現有必要限制每個測試用例的 PDU 數量。因為Fuzzer重復相同的消息 100 次會導致與發送一次不同的代碼序列。
6.Crashs重現問題
經過大約一周的Fuzzing,第一次崩潰出現了。但是,當我再次嘗試運行相同的輸入時,崩潰并沒有重現。這種情況經常發生,很可能是由于協議的狀態性質。換句話說,一個測試用例讓客戶端進入一個特定的狀態,然后被后續的測試用例“利用”來使目標崩潰。
為了理解不可重現的崩潰,我修改了 WinAFL 以在檢測到崩潰時創建目標進程的內存轉儲。
7.Crashs自動化分析
在崩潰時創建轉儲解決了一個問題,但造成了另一個問題:一旦發現崩潰,很可能會重復遇到。通常,WinAFL 會嘗試檢測相同的崩潰并僅通知“特殊的崩潰”,但是我的多消息Fuzzing使這種檢測變得非常困難。考慮單個消息導致目標崩潰的情況。fuzzer 可以在最后創建任何一組消息。這些消息集中的每一個都會使目標崩潰,并且還會導致不同的覆蓋位圖,因為消息及其處理方式不同,這將導致 WinAFL 每次都報告一個特殊的崩潰。
出于兩個原因,我必須自動分析崩潰。首先,手動分析每個崩潰的工作很繁瑣,其次,磁盤很快被內存轉儲填滿。
為了克服這個問題,我編寫了一個 WinDBG 腳本來分析崩潰并從中提取崩潰堆棧。然后,我運行了一個 PowerShell 腳本,該腳本定期分析崩潰并僅保留那些包含新堆棧的崩潰,并通過電子郵件向我發送消息。
在客戶端Fuzzing設置中,從Fuzzer創建目標(mstsc.exe)的那一刻,到建立連接并可以發送第一條消息的那一刻,花費了 10 多秒鐘。因此,在不重新啟動目標的情況下執行盡可能多的迭代至關重要。我通過使用AFL-Fuzz的-fuzz_iterations參數并提供盡可能多的迭代來實現這一點。
8.多通道Fuzzing
與多輸入Fuzzing一樣,某些邏輯需要不同通道上的一系列消息。例如,如下面文檔中所述,使用多個通道支持從客戶端向服務端發送相機數據。
資料來源:[ MS-RDPECAM ]
因此,如果希望通過發送輸入來fuzz服務端,必須至少在兩個不同的通道上這樣做。
我的解決方案也很相似:fuzzer 字典確定了要發送消息的通道。
9.定位相關代碼
由于 RDP 在 Windows 中有許多不同的組件,甚至定位需要Fuzzing的目標函數都具有挑戰性。
PS C:\> gci -Include *.exe, *.dll, *.sys -Recurse C:\Windows\ -ErrorAction SilentlyContinue | ?{[System.Diagnostics.FileVersionInfo]::GetVersionInfo($_).FileDescription -match "RDP|Remote Desktop"} | Measure-Object | select count Count -----191為了快速做到這一點,我創建了一個小型數據庫,其中包含可能與我的工作相關的所有符號文件。
我的想法是下載與我的 Windows 版本相關的所有 PDB,從中提取所有函數名稱并將它們轉儲到一個文件中(鏈接回 exe/sys/dll),以便可以快速搜索函數名稱和定位與我當前目標通道相關的功能。
由于幾乎所有動態通道接收函數都匹配以下模式C< class-name >::OnDataReceived,我們可以快速查看這些函數的列表并找出可能與我們所針對的通道相關的內容。
Crashs漏洞分析
在本節中,我將分享我在該工作中發現的兩個bugs的技術細節。
1.AUDIO_PLAYBACK 通道(服務端→客戶端)
該AUDIO_PLAYBACK_DVC虛擬通道用于輸出從客戶端上輸入的服務端數據。它的正常流程包括兩個序列:初始化和數據傳輸。在協議的正常使用中,初始化序列在開始時發生一次,然后是許多數據傳輸序列。
?初始化序列——用于建立以下數據序列中使用的版本和格式
資料來源:[ MS-RDPEA ]
?數據傳輸序列——來自服務端的數據將在客戶端輸出
資料來源:[ MS-RDPEA ]
Wave和WaveInfo PDU包含初始化序列中交換的格式數組的索引,用于確定傳輸音頻數據的格式。
資料來源:[ MS-RDPEA ]
當格式發生變化時ーー例如,Wave 或 WaveInfo PDU 的索引與上次使用的索引不同,客戶端會驗證新索引是否有效。
// in mstscax!CRdpAudioController::OnNewFormat if ( (unsigned int)new_format_index >= this->formatArray_size )但是,只要格式索引保持不變,就會跳過此驗證,下面是相關部分的偽代碼。
// in mstscax!CRdpAudioController::OnWaveData last_format_index = this->last_format_index; format_index_from_pdu = *((_WORD *)pdu + 3); //pdu is controlled by the server if ( last_format_index != format_index_from_pdu ) {CRdpAudioController::OnNewFormat(this, (__int64 *)format_index_from_pdu); // this is where the bound check is being made// but only if the format index is different than the last indexlast_format_index = *((unsigned __int16 *)pdu + 3);this->last_format_index = last_format_index; } formats_array = (AUDIO_FORMAT **)this->formatArray; current_format = formats_array[last_format_index]->wFormatTag; // crashes here在客戶端觸發此漏洞的流程如下:
漏洞流程
?服務端向客戶端發送具有0x1A格式的服務端格式 PDU,客戶端分配具有此大小的格式數組。
?服務端向客戶端發送一個Wave2 PDU,該PDU使用數組中的格式0x5和數據。
-客戶端檢查此格式是否與上次發送的格式相同。
-如果是,它使用最后一個解碼器;如果不是,它將從格式數組加載新的解碼器函數指針。
?服務端再次向客戶端發送服務端音頻格式 PDU——這次只有0x2,導致客戶端釋放之前的格式數組并分配一個具有新大小的新格式。
?服務端最終使用最后使用的格式0x5發送另一個Wave2 PDU。
-由于格式沒有改變,客戶端不執行任何有效性檢查。
-然后,客戶端執行越界讀取,嘗試從 2 格式數組中讀取第6種格式造成崩潰。
這允許攻擊者使用額外的服務端音頻格式 PDU 重新分配格式數組,然后指定以前有效和使用的無效索引,導致客戶端讀取格式數組的邊界并崩潰。
請注意,此bugs在很大程度上依賴于多輸入Fuzzing,如果沒有此功能,我們將無法找到它。
2.AUDIO_INPUT 通道(客戶端→服務端)
所述AUDIO_INPUT虛擬信道用于從所述客戶端發送的聲音輸入到服務端。在服務端端,音頻輸入數據由提升的audiodg.exe進程處理。
在AUDIO_PLAYBACK_DVC通道中,客戶端和服務端首先交換它們支持的一系列聲音格式。
資料來源:[ MS-RDPEAI ]
所述聲音格式的PDU開始的9個字節的報頭,其包括指令,格式編號,和該分組的大小,接著格式的陣列,每個可變長度,加上附加數據的一個可選的字段。
資料來源:[ MS-RDPEAI ]
處理聲音格式 PDU的代碼在rdpendp.dll 中。它首先驗證數據包大小至少為九個字節,然后讀取頭部并驗證頭部的大小不大于數據包的大小。
// in rdpendp!CAudioInputHandler::OnFormatsReceivedif ( size < 9 ){// ...}// ...size_from_msg = *(_DWORD *)(data + 5);if ( size_from_msg > size ){// ...}然后相同的函數從header讀取的大小中減去 9,并讀取header中指定的格式數,只要剩余長度足夠大。header的大小不受整數下溢保護,這可能會導致減法溢出,并導致程序從數據包末尾讀取“formats”。
// in rdpendp!CAudioInputHandler::OnFormatsReceived underflowed_size = size_from_pdu - 9; format_definition_offset = (unsigned __int16 *)(pdu + 9); if ( num_formats ) {while ( underflowed_size >= 0x12 ){format_definition_size = format_definition_offset[8];total_format_size = format_definition_size + 18;if ( underflowed_size < (unsigned __int64)(format_definition_size + 18) )break;(*class_fomats_array)[format_index] = (struct SNDFORMATITEM *)operator new[](total_format_size);local_format = (*class_fomats_array)[format_index];if ( !local_format ){status = E_OUTOFMEMORY;goto CLEAN_AND_RETURN;}memcpy_0(local_format, format_definition_offset, total_format_size);format_definition_offset = (unsigned __int16 *)((char *)format_definition_offset + total_format_size);underflowed_size -= total_format_size;if ( ++format_index >= num_formats )goto LABEL_50;}goto INVALID_ARG_EXIT; }研究總結
在這篇文章中,我記錄了我嘗試解決具有挑戰性的Fuzzing目標的過程:Windows 的 RDP 客戶端和服務端。我想分享我的過程有幾個原因。
首先,我認為即使你無法實現最初的目標(例如 RCE),分享流程也很重要。這可以幫助你反思自己的流程——哪些看起來運作良好,哪些可以改進。
其次,盡管設置Fuzzing環境可能是一個復雜的過程,但我認為這是一個值得追求的目標——即使是像我在這里提出的更具挑戰性的目標。RDP 是一個非常復雜的協議,具有許多組件和不同的代碼庫。Shodan.io搜索超過 400 萬臺使其成為攻擊者非常有利可圖的目標。
最后
為了展示我的Fuzzing的通用性,我已經將其應用于幾個 RPC 服務端,并取得了一些初步成果。
【網絡安全學習攻略】
總結
以上是生活随笔為你收集整理的Windows RDP协议 Fuzzing 漏洞挖掘研究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 常见的安全应用识别技术有哪些?
- 下一篇: java信息管理系统总结_java实现科