對 Windows 10 UAC 保護實作細節逆向工程 (Windows 10 Enterprise 17763)

- 前言

啊啊,真的蠻久沒更新的,部落格荒廢一陣子。🥴 身為一個碩一升碩二,畢業即失業的我最近開始想動手玩一些有趣的東西(不然畢業後投履歷沒東西寫可能真的要餓死街頭了怕爆 QQ)

年初看了這一篇挺有趣的文章由 David Wells 所發表的部落格文  UAC Bypass by Mocking Trusted Directories 裡面提及了在 Windows 10 Build 17134 版本中出現的 Path Normalization 在 WinNT 跟實際 DOS Path 絕對路徑產生的歧異所引發安全上的缺陷允許繞過 UAC 設計。



不過這一篇文章只講到了其中一個利用點、經過完整逆向後發現一些有趣的資訊有其他打法可以利用,身為只會刷水稿的我,運氣不錯今年刷刷水稿有丟上 HITCON (。・ω・。)ノ 喔拜託,如果有打算到現場的不要噓我 QQ 怕爆.jpg

而 整體 UAC(User Account Control)保護設計在網路上也未被多加著墨(至少當前沒有太多逆向工程細節 XD)所以興致來了寫了一篇廢文解釋一下整體逆向工程完畢後對 Windows 10 當前最新企業版設計做出一些解釋。

// 附註

整體 UAC(User Account Control)實作其實相當精細,其中包含了本地端 RPC 消息傳遞、微軟官方未公布的一堆MACRO標記與結構體、Windows特權令牌分發細節 等。礙於本人逆向功力不足、與網路上可爬到的未公布資料實在太少,底下許多提到的數字細節都是我根據當下程式碼結構上臆測的結果而無法考證微軟內部程式碼,若有錯誤麻煩再回覆告知。

此外,本文所描述的逆向分析結果都在於認證機制層面上、但許多程式碼跳轉部分其實 RPC 消息傳遞影響的成分相當重、但本文為了簡化整體架構方便釐清、那些特殊影響邏輯跳轉的 RPC 消息邏輯暫且不提。

- Environment 

截至今日  2019/07/27 目前分析環境為 Windows 10, 17763 企業版,更新補丁上到最新版本。

- SVCHOST

UAC 保護設計在 Windows 中是以系統服務形式被安裝在作業系統之上,在這邊可以看到 UAC 服務名稱對應是 AppInfo,這也代表著系統開機後會有一個 Process 施作 UAC 細節。

在 WinInit 服務叫起的 Services.exe 在每次系統重啟後負責喚醒所有系統服務,而負責施作 UAC 保護的 Process 可以看到以 C:\WINDOWS\system32\svchost.exe -k netsvcs -p -s Appinfo 的方式叫起了一個 SVCHOST Process 其內部加載了 AppInfo.dll 負責整個 UAC 實作、而此一 UAC Process 在權限級別上被分配著相當高的 System 級別權限。(後續都以 UAC Process 表示這一支 SVCHOST Process)

而當使用者欲以「右鍵→以管理員權限執行」時,便會觸發 UAC Process喚醒 consent.exe、而仔細觀察此一 Process 即是彈出詢問使用者是否授權提示畫面的程式。而 consent.exe 由 UAC Process 做分發,因此也擁有對應的系統級別高權限。

這正意味著若有攻擊者意圖達成本地提權,較低權限的 Process 無法以跨Process方式注入、也受限 User Interface Privilege Isolation(UIPI)無法以視窗消息類型方式做注入藉以干擾 consent.exe 授權判斷行為。

- RAiLaunchAdminProcess

在 Windows 體制下無論是 system、WinExec、ShellExecute 系列函數,最終都會走入三本家系列函數——即 CreateProcess* 家族函數:CreateProcess/CreateProcessAsUser/CreateProcessWithToken,此三者函數用過的應該都知道,前兩個參數便分別是指名喚醒 子Process的絕對路徑命令參數;此外的差異在:由 CreateProcess 發起的子Process其Process等級與父Process一致、而後兩者則父Process可以設下遮罩使子Process權限變得更低、甚至使 子Process產生為其他Process之下成為子Process

而有趣的事情就在於:UAC 保護設計的重點在於正確的處置子Process與父Process其權限不一致時該如何認證的過程,因此可想而知 CreateProcessA/W 函數發起請求後,基本上 Kernel 會直接按照父Process需求逕創建新子Process而不會經由 UAC 路由分發;然而仍有低權限Process需要喚醒高權限Process的需求面,好比:從桌面環境(Explorer)喚醒 UAC 等級面板必然需要高權限,此時會採用 ShellExecute 搭配 runas(背後採 LoginAsUser 拿令牌)或是走 UAC 權限自動提升(Auto Elevated)路線來獲得高權限令牌(好比剛剛說的使用者喚醒 UAC 面板的例子)

而這個負責處理認證的機制便在於 RAiLaunchAdminProcess 函數實作內部:

當有權限不一致需要向上提升權限時 ——亦即父Process權限不夠,因此需要向 UAC Process借權限做分發:此時就會走入 RAiLaunchAdminProcess 內部做 UAC 路由分發。

不過一開始分析這邊最令人訝異的是,一般認為 CreateProcess* 函數其第一參數放的是子Process絕對路徑、第二參數擺放給予「子Process的命令參數」然而在上圖可以看到,實務上 Windows 內部實作是允許你直接在命令列參數上(第二個參數)逕擺上「程式名 + 命令參數」這樣的組合,意味著第一個參數(子Process絕對路徑)是允許為 NULL 的,這點在後續程式碼分析上也可顯而易見。


在 RAiLaunchAdminProcess 函數頭走入後會經過一系列 RPC 通訊,通訊完成後,先調用 I_RpcBindingInqLocalClientPID() 對傳遞進來的 RPC binding handler 獲取當前父Process的 Process Id,接著以 NtOpenProcess 嘗試存取父Process、藉此確認父Process還存活著(若父Process已死亡則無需繼續創建子Process)

接著會以 NtDuplicateToken 對當前 UAC 服務的令牌(System 等級)clone 出一把同樣是系統等級的高權令牌,後續用以創建 consent 或是高權子服務使用。

- File Mapping


由於 CreateProcess 家族函數使用上特殊性都擁有兩個參數,一個是指定程式字串絕對路徑、第二個是字串命令,二者可以擇一傳入,所以在這邊 UAC 服務內部設計上,主要以傳入創建子Process路徑為主、若父Process未傳入絕對路徑則以命令字串作為目標。然後以 CreateFileW 搭以 R+X mode 將子Process的靜態資料做 File Mapping 一塊到 UAC Process的內存中。

- CheckElevation


接著調用了 CheckElevation 取出 Windows 系統設定中,使用者在系統設定中配置的通知時機以數值表示。由最底下開始為 1(不要通知)、應用程式嘗試變更時通知(此值為2、即上圖所展示的內存狀況)依此類推。

- Auto Elevated Double Authentication

喔,先說,這命名是我自己亂取的 XD。僅是我逆向分析出來的心得——從程式碼結構跟函數編排上、我猜測架構上應該設計時候就是兩層了,畢竟也沒有官方文件可以考證這件事情 🤤(若有麻煩再來信告知或者留言在底下)

UAC 機制在 Vista 剛被推出階段時,所有的提權請求被發起後經由 RAiLaunchAdminProcess 處理至分發高權限給低權用戶時,就「必須」彈出 consent.exe 顯示出是否要提權的畫面、接著才會創建高權子Process。然而這個機制過於惱人,於是在 Windows 7 推出後的 UAC 版本加入了「雙層信任提權認證」亦即有兩段認證,若兩段認證皆通過的「提權請求」,那麼在 cosent.exe 被喚起後就不會彈出 是否 使用者同意的視窗,而自動同意。(意思是可信任的Process被喚起時,仍然會叫醒 consent.exe 只是不會彈出使用者同意請求視窗)

- Authentication A

信任提權機制A 主要基於對 子Process的路徑上匹配做驗證是否可信任。由於 Windows 10 系統(此份研究分析當下為 Windows 10 企業版最新版)支持長路徑,亦即路徑字符可以超過 256 個字符以上(舊版本 Windows 對路徑所支持的極限長度)所以先以 GetLongPathNameW 將傳入路徑轉為 wchar_t* 的長路徑指標。

而這個過程也帶來了安全上的問題,由於 GetLongPathNameW 為 Windows NT 檔案系統函數、因此諸如像是空白或者符號等字符若在正確的位置上插入,那麼就能在這邊產生驗證層跟實際路徑為兩個不同路徑,引發認證缺陷,細節可以參考至微軟文章 Path Normalization – Jeremy Kuhne's Blog

接著再輔以 RtlDosPathNameToRelativeNtPathName_U_WithStatus 函數將上述 GetLongPathNameW 所返還的路徑,轉譯為 Windows 底層的 DOS 模式全局映像路徑。例如:子Process路徑傳入為 L"C:\a.exe"、經由上述兩階段處置後,將被轉譯為 L"\??\C:\a.exe"


接著將以 RtlPrefixUnicodeString 直接由左而右比對上述轉譯路徑「前幾個字符」是否符合在白名單內(例如:C:\Windows、Program Files 雙目錄中)並且不再黑名單目錄內(通常會是小算盤、Windows Edge 等系統額外特色小工具的目錄)

如果是在 C:\Windows\ 開頭的目錄,那麼 trustedFlag 就會被設為 0x2000,這是第一層信任、可參考的信任但還無法完全信任的數值。


接著倘若程式路徑是在 Program Files 雙目錄中,接著是比對是否目錄在 Windows Defender、Journal、Media Player 或是 Multipoint Server中,若是,則把 trustedFlag 設為 0x2000 | 0x4000,0x4000 是指隸屬 Windows 的外部應用服務(C:\Program Files 下的客製化安裝程序)


接著向下執行,若子Process路徑開頭為 C:\Windows 開頭,那麼必須確認兩種狀況:

(一)若子Process目錄為 C:\Windows\System32 開頭,代表其為最極度敏感、並且是原生的系統高權服務(即 32bit Windows 對應著的唯一 32bit 系統服務目錄、64bit 就必須對應 System32 而非 SysWoW64 一樣)將 trustedFlag 設為 0x6000

(二)若子Process目錄開頭雖然是 C:\Windows、但不是 System32 目錄,比方說:ehome、Adam、SysWoW64 等狀況,那麼維持 trustedFlag 為 0x4000。


接著會去區分若是 C:\Windows\System32\ 開頭的子Process,那麼  \??\C:\Windows\System32\Sysprep\sysprep.exe 與 \??\C:\Windows\System32\inetsrv\InetMgr.exe 則必須特別擁有更高的權限。

在這邊會先以 AipMatchesOriginalFileName 函數將程式做 File Mapping 映射到記憶體中、確認 version.txt 中記錄著編譯時的檔案名稱是否與當前子Process的檔名是否相符(藉此避免檔案替換的劫持手段)

若上述驗證通過,將額外以 or 運算給予 trustedFlag 0x400000 或是 0x800000 的標籤,這個標誌即是可以通過後續第二層自動提升驗證的重要標記。


若上述狀況皆非,那麼必須確認喚醒的子Process是在 C:\Windows\System32 還是 C:\Windows\SysWoW64 中,這兩個都是系統敏感關鍵程序的路徑,因此以 or 運算給 0x200000,這是最後一個能夠通過後續第二層自動提升驗證的重要標記。

到此,便是 信任提權機制A 的完整認證過程、主體上以根據路徑做匹配驗證是否可信任,並將結果寫入至 trustedFlag 中做紀錄。

- Authentication B

接著進入 AiIsEXESafeToAutoApprove 函數中即是整體 UAC 提權「自動提升」(不彈使用者授權視窗)的重點驗證。

進入 AiIsEXESafeToAutoApprove 函數內部,第一件事情是確認子Process提權請求經過前面 信任提權機制A 之後,trustedFlag 是否 > 0x200000,如果沒有大於 0x200000、將直接放棄自動提權的後續驗證、離開此函數。

接著,先根據程式路徑做裁切、拿出純執行程式名稱。然後繼續利用先前所述 CreateFile 所得的 File Handle,以 MapViewOfFile 讀入 Mapping 進來的執行程式靜態內容。

接著就是確認這隻程式靜態內容中的 manifest.xml 是否有配置好 autoElevate 為 true(這是微軟官方記載資訊清單中程序想被自動提權所必備的欄位,可參考 Application Manifests - Windows applications | Microsoft Docs

- Double Chances To Get Elevated without Prompt

若 autoElevate 以配置為 true,那麼 trustedFlag 將以 or 運算刷新 0x1010000 此標記上去,有了這個標記即可認定為無需使用者彈窗授權的完美標記之一。

倘若子程序未有 autoElevate 的請求,但是 子程序檔案名 在白名單清單(共十項)之一,並且能通過 AipIsValidAutoApprovalEXE 的驗證,那麼就能獲得無需使用者彈窗授權的完美標記之二。

而 AipIsValidAutoApprovalEXE 驗證需求如下:
會以 WTGetSignatureInfo 驗證子程序之簽章是否有效、並且同樣的以 AipMatchesOriginalFileName 驗證當前子程序檔案名是否如同編譯階段一樣、未被修改過。

若上述兩項檢測皆通過,那麼則允許放行。

- Launching Consent

接著做的事情便是呼叫 AiLaunchConsentUI 嘗試喚起 consent.exe 彈出使用者授權視窗來詢問使用者是否同意釋出高權限給子Process。

接著在堆疊上分配出一個結構體用來記憶要傳達給 consent.exe 的參數(諸如權限怎麼發、子程序的路徑、資訊清單等細節)

接著以 sprintf 組裝為命令,在參數中傳入剛才的結構體指標、UAC 服務自身的 Process Id 等。

為了讓 consent.exe 能夠有權限能跨Process讀取 UAC 服務中的結構體內容,因此以 NtDuplicateToken 分發了一組一樣高權的系統級令牌。

接著,以剛剛 clone 出來的系統級高權限孵化出 consent.exe 的Process,但是是以暫停的狀態喚醒、接著以 AipVerifyConsent 函數確認 consent.exe 之程式碼內容未被外部替換修改過,再以 ResumeThread 喚醒 consent.exe 子Process,consent.exe 開始執行運作、UAC 服務這一端開始進入等待。

此時會彈出使用者授權視窗,但若前面完全通過信任驗證A與 B 兩項者,則不會彈出此視窗。

接著 consent.exe 的視窗被使用者按下「是」時、或者子Process本身是可信任的,consent.exe Process本身將以 ExitProcess 離開並回應 ExitCode 為 0;反之,若彈出視窗按下「否」或者打叉離開,ExitCode 將會回應非 0 之值。

在 AiLaunchConsentUI 函數末,將會把 consent.exe 的 ExitCode 當作返回值返還,同意下放高權的情況應為 0、反之則應該會是 1223 這個數值。

接著若 ExitCode 返回值為 0,那麼就會開始進入把高權令牌下放給子Process的程序。

接著就會以 AiLaunchProcess 函數將子Process路徑還有先前 NtDuplicateToken 生成好的高權權杖傳入,進入孵化高權子Process程序:
最後在 AiLaunchProcess 函數內部以 CreateProcessAsUserW 根據父Process生成的令牌創建出其高權子Process,完成完整的 UAC 分發流程。

- Takeout

如果大神正在看這一篇,麻煩鞭小力一點啊... 🙏🏻。

全部分析完可以發現整個 權限自動提升 重點就在於認證層的 AiIsEXESafeToAutoApprove 內有三種不同狀況映射的 code block 有機會拿到 > 0x200000 的標誌可以跳入到第二層認證;而 C:\Program Files\ 的狀況基本上 UAC 設計上會在標誌中紀錄信任(像是 Defender 系列)、但預設是不給予自動提升權限。

而在第二層認證中,基本上這層認證就在確認「程式本身想不想被提權」了。有兩次機會拿到最終信任標誌:

  1. 自身 manifest.xml 清單有將 autoElevated 設為 true 
  2. 若自身沒有想被提權、但名字清單是在十項預設白名單成員其一,將會驗證數位簽章有效性並確認編譯階段名字與執行階段相符(避免劫持)
網路上有看到一些針對 RPC 方式去打跳轉的也是蠻有趣 XD,希望有大神能指點一下,還沒看完、看完了再來寫一些廢文吧 🤤

留言

這個網誌中的熱門文章

[C#] Lambda花式應用噁爛寫法(跨UI委派秒幹、多線程處理...etc)

[Windows] 逆向工程 C++ 中入口函數參數 main(argc, argv) 與如何正確的進行參數劫持

重建天堂之門:從 32 位元地獄一路打回天堂聖地(上)深度逆向工程 WOW64 設計