0x00 摘要
2018年8月27日名為sandboxescaper的網友上傳了一份win10本地提權的0day利用代碼(后被微軟修復并分配CVE編號CVE-2018-8440),我通過對歷史漏洞進行研究,對Windows系統中RPC(Remote Procedure Call,遠程過程調用)漏洞挖掘進行了簡單的探索,和大家分享一下探索的過程。理解這種攻擊的工作方式將極大地幫助其他研究人員發現類似于sandboxescaper在Windows任務計劃程序中發現的漏洞,這篇文章中首先回顧sandboxescaper和google project zero發現的歷史漏洞原理,接著介紹對類似漏洞挖掘的嘗試和一些成果,最后提出一些繼續挖掘類似漏洞的方法。
0x01 歷史漏洞回顧
簡單回顧一下CVE-2018-8440的原理:SchRpcSetSecurity函數在win10中會檢測C:\Windows\Tasks目錄下是否存在后綴為.job的文件,如果存在則會寫入DACL(Discretionary Access Control List,自主訪問控制列表)數據。如果將job文件硬鏈接到特定的dll那么特定的dll就會被寫入DACL數據,本來普通用戶對特定的dll只具有讀權限,這樣就具有了寫權限,接下來向dll寫入漏洞利用代碼并啟動相應的程序就可以提權了。詳細的分析請閱讀參考資料中此前發布的預警通告。
那么首先可以想到的是RPC中是否還有類似的函數存在同樣的問題呢?無獨有偶,在2018年4月google project zero披露過SvcMoveFileInheritSecurity函數中的漏洞。
void SvcMoveFileInheritSecurity(LPCWSTR lpExistingFileName,
LPCWSTR lpNewFileName,
DWORD dwFlags) {
PACL pAcl;
if (!RpcImpersonateClient()) {
// Move file while impersonating.
if (MoveFileEx(lpExistingFileName, lpNewFileName, dwFlags)) {
RpcRevertToSelf();
// Copy inherited DACL while not.
InitializeAcl(&pAcl, 8, ACL_REVISION);
DWORD status = SetNamedSecurityInfo(lpNewFileName, SE_FILE_OBJECT,
UNPROTECTED_DACL_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
nullptr, nullptr, &pAcl, nullptr);
if (status != ERROR_SUCCESS)
MoveFileEx(lpNewFileName, lpExistingFileName, dwFlags);
}
else {
// Copy file instead...
RpcRevertToSelf();
}
}
}
在繼續深入之前先簡單介紹一下Windows中的(Access Control Model,訪問控制模型)。如果一個Windows對象沒有DACL,則系統允許每個人完全訪問。如果對象具有DACL,則系統僅允許DACL中的ACE(Access Control Entry,訪問控制條目)明確允許的訪問。如果DACL中沒有ACE,則系統不允許任何人訪問。下圖是一個拒絕用戶Andrew訪問,允許A組成員寫入,允許所有人讀取和執行的DACL的例子。
回到SvcMoveFileInheritSecurity函數。這個函數的功能應該是移動文件到一個新的位置,然后調用SetNamedSecurityInfo函數將所有繼承的ACE應用于新目錄中的DACL。為了確保這個函數不會以服務的用戶身份運行時(這里是Local System)允許任意用戶移動任意文件,需要模擬一個RPC調用者(caller)。模擬是線程使用與擁有線程的進程不同的安全信息執行的能力。通常服務器應用程序中的線程模擬客戶端,這允許服務器線程代表該客戶端操作以訪問服務器上的對象或驗證對客戶端自己對象的訪問。Windows的RPC服務應用程序可以調用RpcImpersonateClient函數來模擬一個客戶端。對于大多數的模擬,這個模擬線程可以調用RevertToSelf函數恢復到原來的安全描述符。
問題就在于此,當第一次調用MoveFileEx函數后會調用RpcRevertToSelf函數,然后調用SetNamedSecurityInfo函數。如果SetNamedSecurityInfo函數調用失敗會再次調用MoveFileEx函數,嘗試恢復原來的文件移動操作。第一個漏洞是有可能原來文件名所處的位置通過符號鏈接指向了別的地方,因此可以創建任意文件(CVE-2018-0826)。第二個漏洞是如果先硬鏈接像SYSTEM32目錄中那些用戶只有讀取權限的文件,在移動后的硬鏈接文件上調用SetNamedSecurityInfo函數,SetNamedSecurityInfo函數會從新目錄位置中提取繼承的ACE,然后將ACE應用到被硬鏈接的文件上。由于這是作為SYSTEM執行的,這意味著任何文件都可以被賦予任意安全描述符,這將允許用戶修改它(CVE-2018-0983)。
修復也經歷了一些波折,微軟在2018年2月和3月分別發布了兩個補丁之后才徹底解決該問題。修補后和修補前的SvcMoveFileInheritSecurity函數如圖所示,解決方法是第一次調用MoveFileEx函數后不再調用RpcRevertToSelf函數恢復到原來的安全描述符。
0x02 嘗試挖掘類似漏洞
要試圖尋找類似的漏洞,首先需要導出所有的RPC函數。在A view into ALPC-RPC這個talk中提到了RPCview這個工具,這個工具可以用來反編譯并查看RPC interface,界面是用QT寫的。然而當下載下來運行時會出現下面這樣的錯誤。
仔細閱讀README之后發現需要自己添加rpcrt4.dll的版本。對于win10來說,修改RpcCore\RpcCore4_32bits\RpcInternals.h和RpcCore\RpcCore4_64bits\ RpcInternals.h,如下所示。
寫一個bat編譯。
set CMAKE_PREFIX_PATH=C:\Qt\Qt5.9.1\5.9.1\msvc2017_64
cmake ..\.. -G"Visual Studio 15 2017 Win64"
cmake --build . --config release
cd D:\ALPC-fuzz\RpcView\Build\x64\bin\Release
mkdir RpcView64
copy *.dll RpcView64\
copy *.exe RpcView64\
C:\Qt\Qt5.9.1\5.9.1\msvc2017_64\bin\windeployqt.exe --release RpcView64
以管理員身份運行編譯好的RpcView.exe(普通用戶權限反編譯出來的結果較少)。
Decompilation窗口是對指定interface反編譯的結果,其中函數名都是ProcX的形式,為了得到函數名需要正確設置符號路徑。該工具似乎并不支持微軟的符號服務器,所以把c:\windows\system32目錄下所有的dll的符號下載到本地。
symchk /s srv*c:\symbol*https://msdl.microsoft.com/download/symbols c:\windows\system32\*.dll
設置_NT_SYMBOL_PATH環境變量。
現在能夠反編譯出來函數名了,需要對源代碼做適當的修改讓它一次性導出所有反編譯結果,而不用一個一個去點。主要修改了源代碼中下面幾處地方。
在EndpointsWidget.cpp的EndpointsWidget_C::AddEndpoint函數開始時添加了一些代碼導出反編譯的Endpoints。
在InterfacesWidget.cpp的InterfacesWidget_C::AddInterfaces函數返回前增加了調用InterfaceSelected函數的循環。
在InterfacesWidget_C::InterfaceSelected函數中首先檢查uuid是否重復避免陷入死循環。
原來反編譯需要右鍵點擊Decompile,所以注釋掉了這部分代碼使得InterfacesWidget_C::InterfaceSelected函數能夠直接調用SigDecompileInterface函數。
修改了IdlInterface.cpp的IdlInterface::dump函數,把反編譯結果寫到文件中。
此外還有其它一些因為編譯語言環境不同的修改。運行修改版的RPCview效果如下。
之前出過問題的SvcMoveFileInheritSecurity函數和SchRpcSetSecurity函數的函數名都帶有Security,來看看還有沒有函數名中含有Security的函數。
除了SvcMoveFileInheritSecurity函數和SchRpcSetSecurity函數,果然還有一些函數名中含有Security的函數。比如這里的NetrpSetFileSecurity函數,看起來真的很有可能存在類似的問題。在IDA中看看反編譯出的代碼。
如果RtlValidRelativeSecurityDescriptor函數和SetFileSecurityW函數之間調用了RpcRevertToSelf函數,就像之前存在漏洞的SvcMoveFileInheritSecurity函數一樣那么很有可能也能用這個函數提權,不過這個函數中是不存在這種漏洞的。
雖然在初次嘗試挖掘過程中沒有能夠找到類似的問題,但是猜測sandboxescaper所披露的CVE-2018-8440應該是用和文中類似的方法發現的。之后fortinet也對RPCview進行了類似的改造。
0x03 RPC Fuzzing
回到之前的talk:A view into ALPC-RPC,研究人員開源了一個RPC fuzz工具RPCForge,但是這個工具并不能直接使用,因為對方沒有開源如何生成待fuzz的interface的這部分代碼,而只給出了5個示例的interface。
RPCForge的作者之一也是PythonForWindows這個庫的作者,這個庫中提供了一些方便的封裝函數,可以節省開發RPC客戶端的時間。RPCForge也用到了這個庫。其實RPCForge的原理特別簡單,就是不斷去調用這些RPC函數,觀察是否有崩潰或者異常。函數的參數是通過用sulley中提供的原始數據生成的。
雖然現在已經導出了所有interface反編譯的結果,但是在此前修改的RPCview反編譯的格式和RPCForge用的格式并不相同,于是嘗試編寫了簡易的python腳本,通過正則對兩個工具的格式進行轉換。
由于有一些interface并沒有能夠下載到對應的符號,加之一些結構體中數據過于復雜未進行處理,在RPCview反編譯出來的兩百多個interface中能夠fuzz的只有一百多個。
在運行RPCForge一段時間后,就在win10最新版上跑出了兩個BSOD,第一個直到現在仍然能在最新的win10正式版和預覽版上復現,第二個可以在win10 1803上復現,不能在win10 1809上復現(更多的版本沒有測試)。
首先是第一個漏洞:
原理非常簡單,BfeRpcEngineClose函數沒有檢查傳進來的參數,直接訪問了非法的地址。
接著是第二個漏洞:
當一些%s被作為Srv_CreateResourcePolicy函數的參數傳入時Srv_CreateResourcePolicy函數最終調用到vsnwprintf函數,使用棧上的值作為字符串的地址,由于沒有檢查%s個數導致非法地址訪問,多一個%s產生了BSOD。
將這兩個問題報告給MSRC之后,MSRC以”Beyond causing a crash this doesn’t appear to leak any data or escalate privileges in any way”為由拒絕修復。
0x04 一些調試技巧
在確定漏洞的具體成因時需要調試,但此處的調試與常規稍有區別,因為含有漏洞函數的dll是被加載到system的svchost.exe中運行的,不能直接用內核態調試sys的方法,用戶態調試也因為權限問題不太好操作。在此采用了下面的方法進行調試。
搭建好雙機調試環境之后首先確定含有漏洞函數的dll的svchost.exe的進程號,例如30c,首先查看進程信息。
1: kd> !process 30c 0
Searching for Process with Cid == 30c
PROCESS ffffb9010a30b540
SessionId: 0 Cid: 030c Peb: a1e044f000 ParentCid: 024c
DirBase: 21b00002 ObjectTable: ffffd98b2160cb00 HandleCount: 1145.
Image: svchost.exe
使用得到的地址切換到該進程上下文,重新加載用戶態符號之后再次侵入式切換。
1: kd> .process /p ffffb9010a30b540
Implicit process is now ffffb901`0a30b540
.cache forcedecodeuser done
1: kd> .reload /f /user
Loading User Symbols
1: kd> .process /i /p ffffb9010a30b540
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
在存在漏洞函數上下斷點,g之后運行poc即可斷下。
1: kd> bp resourcepolicyserver!Srv_CreateResourcePolicy
1: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff802`085d4980 cc int 3
1: kd> g
Breakpoint 0 hit
resourcepolicyserver!Srv_CreateResourcePolicy:
0033:00007ffa`0d91afb0 4883ec38 sub rsp,38h
如果存在漏洞函數所在的dll默認并沒有被加載可以在poc代碼中先調用該dll中的其它函數并且設置斷點,待該dll被加載之后再在想要下斷點的函數處斷下,繼續運行poc代碼即可。為了方便調試復現,還可以用pyinstaller將python代碼打包成可以運行的exe。
pyinstaller.exe -F D:\20181009\poc\poc.py --hidden-import interfaces.test
0x05 繼續挖掘的方向
時至今日,除了CVE-2018-8440這個能夠直接提權的漏洞外,sandboxescaper還公布了兩個越權刪除任意文件的EXP(CVE-2018-8584)和一個越權讀取任意文件的EXP。12月份公布的后兩個漏洞雖然也是Windows中的邏輯問題,但是與RPC無關。已經被修復的CVE-2018-8584是RpcDSSMoveFromSharedFile函數中的漏洞,同樣也是一個邏輯問題,當任意權限的用戶調用該函數時,該函數會將第三個參數所代表的文件刪除。在此之前,通過CreateMountPoint函數建立兩個文件夾之間的軟鏈接,達到刪除目錄下的特定文件從而將所鏈接目錄下的文件一并刪除的效果。
為了能夠繼續發現RPC中的漏洞,通過研究發現還有以下嘗試方向:
This is more a PoC than a real fuzzer. Its aim was to be able to forge a valid serialized stream reaching RPC methods code without being rejected by the Windows RPC Runtime (because of bad arguments type leading to error: RPC_X_BAD_STUB_DATA).Thus, it doesn’t contain any instrumentation in the server side to improve code coverage.
通過改進原始數據或者增強代碼覆蓋率進行發現更多的漏洞的嘗試時發現,基于代碼覆蓋率統計做驅動反饋的效果不佳。Pin或DynamoRIO類似的工具多用于用戶態,Qemu或Bochs等虛擬化技術多用于內核態,對于system權限的svchost.exe進程似乎都不太方便。另外就是要fuzz的dll太多了,被加載到幾十個進程,每個dll又只有那么幾個函數,輸入數據覆蓋的代碼有限而且分散,效果不會太好。
另外無論是靜態審計還是動態監控也與預期有一定差異。在理解CVE-2018-8440后可能認為發現它的過程比較簡單,但是在調試后會發現設置文件安全描述符的函數代碼是一處虛函數調用,靜態審計無法看到,動態調試才能確定最終調用了taskcomp!SetSDNotification函數,按照傳遞的任務名稱參數和SDDL安全描述字串設置%systemdir%\Tasks\任務名稱.job的安全屬性。
0x06 總結
通過改進完善開源fuzz工具和研究已經公布的漏洞來尋找類似的漏洞仍然是找到漏洞的一種高效的方式。RPC中仍然存在非常有趣的邏輯漏洞等待人們發現,快速找到這些邏輯漏洞可能需要對系統深入的理解以及一些不同于查找內存破壞漏洞的手段。
本文用到的所有代碼開源在https://github.com/houjingyi233/ALPC-fuzz-study,文中提到的兩個BSOD的代碼和打包好的可執行文件在https://github.com/houjingyi233/windows-BSOD。
0x07 參考鏈接
上一篇:llvm的去平坦化