如何藉助COM對Windows受保護程序進行程式碼注入
概述
在Recon Montreal 2018上,我與Alex Ionescu共同發表了“已知DLL和其他程式碼完整性信任繞過的新方法”的演講。我們描述了Microsoft Windows中的程式碼完整性機制,以及Microsoft是如何實現程序保護(Protected Processes,PP)的。在演講中,我展示瞭如何繞過Protected Process Light(PPL)保護機制,有些繞過方法需要具有管理員許可權,有些則不需要。
在這篇部落格中,將主要探討在Windows 10 1803上,如何向PPL進行程式碼注入。由於Microsoft表示存在於防禦安全邊界的唯一漏洞已經被修復,因此我可以更詳細的講解一下利用方法。
漏洞描述 ofollow,noindex">點選這裡 。
Microsoft關於這一 CVE-2018-8449" target="_blank" rel="nofollow,noindex">漏洞的公告 。
關於Windows受保護程序
Windows受保護程序(Windows Protected Process)模型可以追溯到Windows Vista,它是為了保護DRM程序而引入的。受保護程序模型受到嚴格限制,將其需要載入的DLL限制為隨作業系統安裝的程式碼集。此外,只有使用特定Microsoft證書進行簽名,並將其嵌入到二進位制檔案中的可執行檔案,才有許可權啟動保護。其中,有一項由核心強制執行的保護,使未受保護的程序無法開啟受保護程序的控制代碼,因此沒有足夠許可權來注入任意程式碼或讀取記憶體。
在Windows 8.1中,引入了一種新的機制,稱為Protected Process Light(PPL),該機制使保護變得更加通用。PPL放寬了可以受到保護的DLL範圍,併為主要可執行檔案引入了不同的簽名要求。另外,還有一個重大變化,就是它加入了“簽名級別”的概念,以此來區分不同型別的受保護程序。某個級別的PPL,對同級別或更低級別的任何程序擁有完全訪問的許可權,對更高級別的許可權只有受限的訪問許可權。這一簽名級別的概念,也同樣擴充套件到舊的PP模式上,同一級的PP可以開啟相同級別或更低級別的所有PP和PPL,但反過來就不行,PPL在任何級別的情況下都無法擁有對PP的完全訪問許可權。下圖表示了其中的一些級別和相互關係:
由於引入了簽名級別的概念,於是Microsoft向第三方開放了受保護程序。然而目前,第三方唯一可以建立的受保護程序是反惡意軟體PPL。反惡意軟體所具有的級別是特殊的,因為它允許第三方通過Early Launch Anti-Malware(ELAM,https://msdn.microsoft.com/en-us/library/windows/desktop/dn313124%28v=vs.85%29.aspx)來新增其他允許的簽名金鑰。此外,還有Microsoft的TruePlay,這是一個用於遊戲的反作弊技術,但與本次分析無關,我們在此不做討論。
在這裡,我強烈推薦大家閱讀Alex Ionescu的部落格文章(分為三部分): 第一部分 、 第二部分 、 第三部分 。
儘管文章是主要基於Windows 8.1進行講解的,但其中的大多數概念在Windows 10中未發生重大變化。
我之前也寫過一篇 關於受保護程序的文章 ,主要講解了Oracle如何在Windows環境下的VirtualBox虛擬化平臺中實現。在文章中,我展示瞭如何使用多種不同的技術繞過程序保護。但是,我在當時的文章中沒有提到過的是,我描述的第一種技術,將Jscript程式碼注入到程序中,也違背了Microsoft的PPL實現。我向Microsoft 反映了這一問題 ,證明我可以向PPL中注入任意程式碼,但Microsoft決定不將其修復方式作為一個安全補丁釋出。隨後,Microsoft將下面的程式碼新增到核心程式碼完整性庫CI.DLL中,從而修復了這一問題:
UNICODE_STRING g_BlockedDllsForPPL[] = { DECLARE_USTR("scrobj.dll"), DECLARE_USTR("scrrun.dll"), DECLARE_USTR("jscript.dll"), DECLARE_USTR("jscript9.dll"), DECLARE_USTR("vbscript.dll") }; NTSTATUS CipMitigatePPLBypassThroughInterpreters(PEPROCESS Process, LPBYTE Image, SIZE_T ImageSize) { if (!PsIsProtectedProcess(Process)) return STATUS_SUCCESS; UNICODE_STRING OriginalImageName; // Get the original filename from the image resources. SIPolicyGetOriginalFilenameAndVersionFromImageBase( Image, ImageSize, &OriginalImageName); for(int i = 0; i < _countof(g_BlockedDllsForPPL); ++i) { if (RtlEqualUnicodeString(g_BlockedDllsForPPL[i], &OriginalImageName, TRUE)) { return STATUS_DYNAMIC_CODE_BLOCKED; } } return STATUS_SUCCESS; }
這段修復程式碼會根據黑名單中的5個DLL,檢查正在載入的映像資源段的原始檔名。黑名單包括例如JSCRIPT.DLL這類實現原始Jscript指令碼引擎的DLL,以及例如SCROBJ.DLL這類實現scriptlet物件的DLL。如果核心檢測到PP或者PPL正在載入其中一個DLL,會立即使用STATUS_DYNAMIC_CODE_BLOCKED拒絕映像載入。這樣一來,我的漏洞利用方法就失效了。如果我們嘗試修改其中一個DLL的資源段,那麼該映像的簽名隨即無效,映像載入前進行的加密雜湊值校驗也將失敗,因此無法載入映像。實際上,這種修復方式與Oracle修復VirtualBox中漏洞的方法相同,唯一的區別可能就在於這一次是在使用者模式下實現的。
尋找新目標
在漏洞修復前,我們使用指令碼程式碼進行注入,實際上是一種通用的技術,適用於載入COM物件的任何PPL。隨著這一漏洞被修復,我決定再回顧一下有哪些可執行檔案可以作為PPL載入,並分析其是否具有可以利用獲取任意程式碼的明顯漏洞。我選擇了看起來似乎更容易找到問題的PPL開始研究。如果我們能夠獲得管理員許可權,其實就有很多方法可以注入到PPL中,最簡單的是通過載入核心驅動程式。但是,這種條件並不實際,因此我將目標限定為必須要從普通使用者賬戶實現漏洞利用。此外,我們必須還要考慮簽名級別的問題,普通使用者能夠獲得的最高簽名級別應該是Windows TCB級別,因此,這也將作為我們的另外一個限定條件。
第一步,需要識別作為受保護程序執行的可執行檔案,這樣我們就能具有最大的攻擊面,從而發現漏洞。根據Alex的部落格文章,為了作為PP或PPL載入,簽名證書需要一個特殊的物件識別符號(OID),該識別符號位於證書的增強金鑰用法(Enhanced Key Usage,EKU)擴充套件中。PP和PPL都各自有單獨的OID。如下圖所示,我們可以發現WERFAULTSECURE.EXE(可以作為PP或PPL執行)與CSRSS.EXE(只能作為PP執行)之間的區別。
我決定深入看看這些帶有EKU OID嵌入式簽名的可執行檔案,並且我認為可以通過它來獲得所有可執行檔案的列表,從而查詢到可以進行漏洞利用的行為。我為 我的NtObjectManager Shell/">PowerShell模組 編寫了Get-EmbeddedAuthenticodeSignature命令列指令碼,用於提取此資訊。
在這時,我意識到依賴證書籤名的方法存在問題。有很多二進位制檔案都是允許作為PP或PPL執行的,但它們並沒有在列表中出現。由於PP最初是為DRM涉及的,因此它並沒有可執行檔案來處理 受保護的媒體路徑 ,例如AUDIODG.EXE。另外,根據我之前對Device Guard和Windows 10S的研究,我瞭解.NET框架中必須有一個可執行檔案,可以作為PPL執行,用於為NGEN生成的二進位制檔案新增快取的簽名級別資訊(NGEN用於將.NET程式及轉換為Native程式碼)。我決定執行動態分析,而不是靜態分析,我們只需要啟動每一個受保護的可執行檔案,並查詢其具有的保護級別。我編寫了一個指令碼,來測試單個可執行檔案:
Import-Module NtObjectManager function Test-ProtectedProcess { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$FullName, [NtApiDotNet.PsProtectedType]$ProtectedType = 0, [NtApiDotNet.PsProtectedSigner]$ProtectedSigner = 0 ) BEGIN { $config = New-NtProcessConfig abc -ProcessFlags ProtectedProcess ` -ThreadFlags Suspended -TerminateOnDispose ` -ProtectedType $ProtectedType ` -ProtectedSigner $ProtectedSigner } PROCESS { $path = Get-NtFilePath $FullName Write-Host $path try { Use-NtObject($p = New-NtProcess $path -Config $config) { $prot = $p.Process.Protection $props = @{ Path=$path; Type=$prot.Type; Signer=$prot.Signer; Level=$prot.Level.ToString("X"); } $obj = New-Object –TypeName PSObject –Prop $props Write-Output $obj } } catch { } } }
在該指令碼中,定義了一個函式Test-ProtectedProcess。該函式將獲取可執行檔案的路徑,以特定的保護級別啟動該可執行檔案,並檢查是否成功啟動。如果ProtectedType和ProtectedSigner引數設定為0,那麼核心會決定一個“最佳”的程序級別。這會導致出現一些問題。例如,SVCHOST.EXE將會被明確標記為PPL,並且是在PPL-Windows級別執行,但實際上,它也是一個經過簽名的作業系統元件,其最高的級別應該是PP-Authenticode。另外一個有趣的問題是,使用本地程序建立API,可以將DLL作為主要可執行映像啟動。在大量的系統DLL中,實際上都嵌入了Microsoft的簽名,所以它們就都可以作為PP-Authenticode啟動,即使這樣的許可權毫無作用。下面是作為PPL執行的二進位制檔案列表,以及它們的最大簽名級別:
C:\windows\Microsoft.Net\Framework\v4.0.30319\mscorsvw.exe(CodeGen) C:\windows\Microsoft.Net\Framework64\v4.0.30319\mscorsvw.exe(CodeGen) C:\windows\system32\SecurityHealthService.exe(Windows) C:\windows\system32\svchost.exe(Windows) C:\windows\system32\xbgmsvc.exe(Windows) C:\windows\system32\csrss.exe(Windows TCB) C:\windows\system32\services.exe(Windows TCB) C:\windows\system32\smss.exe(Windows TCB) C:\windows\system32\werfaultsecure.exe(Windows TCB) C:\windows\system32\wininit.exe(Windows TCB)
將任意程式碼注入NGEN
經過仔細檢視作為PPL執行的可執行檔案列表,我確定了要進行漏洞利用的目標,就是前面提到的.NET NGEN二進位制檔案MSCORSVW.EXE。之所以選擇NGEN二進位制檔案,原因在於:
1、大多數其他二進位制檔案都是服務類,可能需要管理員許可權才能正確啟動;
2、二進位制檔案可能會載入複雜的功能(例如.NET框架)或者具有多個COM互動;
3、在一些特定情況下,它可能仍會產生Device Guard繞過問題,因為它作為PPL執行,被授予了快取的簽名級別,同時具有訪問核心API的許可權。即使我們無法在PPL中實現程式碼執行,這個二進位制檔案操作中的任何一個漏洞也可能會被利用。
但是,NGEN二進位制檔案也存在著它的問題。特別是,它不符合我自己制定的限制條件,也就是所能獲得最高的級別是Windows TCB。但是,我聽說Microsoft在修復之前漏洞的過程中,留下了一個後門,針對PPL作為呼叫程序的情況,在簽名程序中保留了一個可寫的控制代碼,如下所示:
NTSTATUS CiSetFileCache(HANDLE Handle, ...) { PFILE_OBJECT FileObject; ObReferenceObjectByHandle(Handle, &FileObject); if (FileObject->SharedWrite || (FileObject->WriteAccess && PsGetProcessProtection().Type != PROTECTED_LIGHT)) { return STATUS_SHARING_VIOLATION; } // Continue setting file cache. }
如果我可以在NGEN二進位制檔案中獲得程式碼執行,那麼就可以重用這一後門,來快取載入到任意PPL的任意檔案。隨後,我就可以劫持一個完整的PPL Windows TCB程序,從而實現我們的目標。
首先需要做的事,是確定如何使用MSCORSVW可執行檔案。Microsoft沒有在任何地方給出關於MSCORSVW的文件或說明,因此還需要我們自行研究。首先,這個二進位制檔案不應直接執行,而是應該在建立NGEN的二進位制檔案過程中由NGEN呼叫。因此,我們可以執行NGEN二進位制檔案,並使用Process Monitor等工具來捕獲MSCORSVW程序使用的命令列。執行命令:
C:\> NGEN install c:\some\binary.dll
在命令列中,以下內容將被執行:
MSCORSVW -StartupEvent A -InterruptEvent B -NGENProcess C -Pipe D
其中,A、B、C和D都是NGEN確保在啟動之前能繼承到新程序的控制代碼。由於我們沒有看到任何原始的NGEN命令列引數,所以推測它們可能是通過IPC機制傳遞。“Pipe”引數用於指示命名管道,以供IPC使用。通過深入研究MSCORSVW的程式碼,我們找到了NGenWorkerEmbedding方法,如下所示:
void NGenWorkerEmbedding(HANDLE hPipe) { CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); CorSvcBindToWorkerClassFactory factory; // Marshal class factory. IStream* pStm; CreateStreamOnHGlobal(nullptr, TRUE, &pStm); CoMarshalInterface(pStm, &IID_IClassFactory, &factory, MSHCTX_LOCAL, nullptr, MSHLFLAGS_NORMAL); // Read marshaled object and write to pipe. DWORD length; char* buffer = ReadEntireIStream(pStm, &length); WriteFile(hPipe, &length, sizeof(length)); WriteFile(hPipe, buffer, length); CloseHandle(hPipe); // Set event to synchronize with parent. SetEvent(hStartupEvent); // Pump message loop to handle COM calls. MessageLoop(); // ... }
這段程式碼,其實不是我想要的。因為這部分程式碼並不是將命名管道用於整個通訊通道,而是僅用於將封裝的COM物件傳回給呼叫程序。COM物件是一個類工廠例項(Class Factory Instance),通常會使用CoRegisterClassObject註冊工廠,但是這會使得所有相同安全級別的程序都有許可權訪問它。因此,通過使用了封裝,可以實現其私密性。由於我之前寫過一篇文章,描述如何在.NET中實現COM物件,因此在這裡,我對於使用COM的.NET相關程序非常感興趣。接下來,我希望能確認這個COM物件是不是在.NET中實現的,可以通過查詢其介面來確定。例如,我們在OleViewDotNet PowerShell模組中使用Get-ComInterface命令,結果如下所示。
我們的運氣不太好,這個物件並沒有在.NET中實現。在這裡,只有一個介面ICorSvcBindToWorker,我們接下來深入瞭解該介面,看看是否有任何可以利用的地方。
突然,有一些東西引起了我的注意。在螢幕截圖中,有一個HasTypeLib列,我們看到對於ICorSvcBindToWorker這列設定為True。HasTypeLib表明,這裡介面代理程式碼的實現,並不是使用預定義的NDR位元組流,而是從型別庫中動態生成的。我之前濫用過這種自動生成代理的機制,併成功實現了到SYSTEM的全線提升,並將其報告為 編號1112的問題 。在這個問題中,我使用了系統執行物件表(ROT)中一些有趣的行為,來強制系統COM服務中產生型別混淆。儘管現在,Microsoft已經解決了到SYSTEM的許可權提升漏洞,但並不能阻止我們使用型別混淆技巧,對作為PPL執行的相同許可權級別的MSCORSVW程序進行漏洞利用,並獲得任意程式碼執行。使用型別庫的另一個優點是:普通代理只會作為DLL載入,這意味著它必須滿足PPL簽名級別要求。但型別庫只是資料,它可以載入到PPL中,不會產生任何簽名級別的錯誤。
那麼,在這裡怎麼實現型別混淆呢?我們檢視型別庫中的ICorSvcBindToWorker介面:
interface ICorSvcBindToWorker : IUnknown { HRESULT BindToRuntimeWorker( [in] BSTR pRuntimeVersion, [in] unsigned long ParentProcessID, [in] BSTR pInterruptEventName, [in] ICorSvcLogger* pCorSvcLogger, [out] ICorSvcWorker** pCorSvcWorker); };
單個BindToRuntimeWorker需要5個引數,4個負責入站,1個負責出站。當我們從不受信任的程序通過DCOM訪問該方法時,系統會自動為其生成代理和存根(Stub)。該過程包括:將COM介面封裝到緩衝區、將緩衝區內容傳送到遠端程序、在呼叫實際函式前對指標進行解封裝。例如,我們可以想象一個更簡單的函式,名稱為DoSomething,它接受一個IUnknown指標。其封裝過程如下所示:
方法呼叫的具體操作如下:
1、不受信任的程序在介面呼叫DoSomething,該介面實際上是指向DoSomethingProxy的指標,該指標是從傳遞IUnknown指標引數的型別庫自動生成的。
2、DoSomethingProxy將IUnknown指標引數封裝到緩衝區中,並通過RPC呼叫受保護程序中的Stub。
3、COM執行時呼叫DoSomethingStub方法來處理呼叫。該方法將從緩衝區解封裝介面指標。需要注意的是,這裡所說的指標並非第一步中的原始指標,它可能是一個回撥到不受信任程序的新代理。
4、Stub呼叫伺服器內部的實際實現方法,傳遞解封裝的介面指標。
5、DoSomething使用介面指標,例如通過物件的VTable呼叫AddRef。
那麼,我們如何進行漏洞利用?需要做的就是修改型別庫,這樣一來其傳遞的就是一個介面指標,而非其他內容。儘管型別庫檔案位於一個系統位置,我們無法修改,但我們可以在當前使用者的登錄檔配置單元中替換相應的內容,或者使用與編號為1112漏洞相同的ROT技巧。舉例來說,如果我們修改型別庫,使其傳遞一個整數,而非介面指標,我們將得到如下內容:
現在的封裝過程,變為如下操作:
1、不受信任的程序在介面上呼叫DoSomething,該介面實際上是指向DoSomethingProxy的指標,該指標是從傳遞任意整數引數的型別庫自動生成的。
2、DoSomethingProxy將整數引數封裝到緩衝區中,並通過RPC呼叫受保護程序中的Stub。
3、COM執行時呼叫DoSomethingStub方法來處理呼叫。此方法將從緩衝區解封裝整數。
4、Stub呼叫伺服器內部的實際工具方法,傳遞整數作為引數。但是DoSomething沒有改變,它仍然是接受介面指標的相同方法。由於COM執行時此時沒有更多型別資訊,因此整數與介面指標的型別發生混淆。
5、DoSomething使用介面指標,例如通過物件的VTable呼叫AddRef。由於此指標完全受不受信任程序的控制,因此可能導致任意程式碼執行。
通過將引數型別由介面指標改為整數,就會引發型別混淆,這使得我們可以取消對任意指標的引用,從而導致任意程式碼執行。我們甚至可以通過向型別庫新增以下結構,來簡化攻擊過程:
struct FakeObject { BSTR FakeVTable; };
如果我們將指標傳遞給FakeObject而非介面指標,那麼自動生成的代理將會封裝結構以及BSTR,並在Stub的另一端重新建立。由於BSTR是一個計數字符串,所以它可以包含NULL,這樣一來就會建立一個指向物件的指標,該物件包含指向任意位元組陣列的指標,而該陣列可以作為VTable。如果將已知函式指標放在這一BSTR中,就可以輕鬆的重定向執行,無需猜測VTable緩衝區的位置。
為了充分利用這一方法,我們需要選擇一個合適的方法進行呼叫,可能需要一個ROP鏈,並且可能還需要繞過CFG。目前看起來,這一過程並不簡單,因此我們打算嘗試採用不同的方法,即通過濫用KnownDlls來獲得PPL二進位制檔案中執行的任意程式碼。
KnownDlls和受保護的程序
在我之前發表過的部落格文章中,說明過一種許可權提升的技術,可以通過在KnownDlls目錄中新增一個項,從而將任意DLL載入到特權程序中,進而實現將任意物件目錄的許可權提升到SYSTEM。我發現,這也是管理員PPL程式碼注入的一種,因為PPL也會從系統的KnownDlls位置載入DLL。由於程式碼簽名檢查是在建立期間進行的,而不是在對映期間,所以只要能將特定項放入KnownDlls,就能將任意內容載入到PPL中,甚至是未簽名的程式碼中。
現在還不能立即行動,因為要寫入KnownDlls還需要管理員的許可權。在此之前,我們首先研究一下過程中是如何載入已知DLL的,以及是如何實現濫用的。內部NTDLL載入工具(LDR)的函式程式碼如下,這一部分用於確定是否存在預先存在的Known DLL。
NTSTATUS LdrpFindKnownDll(PUNICODE_STRING DllName, HANDLE *SectionHandle) { // If KnownDll directory handle not open then return error. if (!LdrpKnownDllDirectoryHandle) return STATUS_DLL_NOT_FOUND; OBJECT_ATTRIBUTES ObjectAttributes; InitializeObjectAttributes(&ObjectAttributes, &DllName, OBJ_CASE_INSENSITIVE, LdrpKnownDllDirectoryHandle, nullptr); return NtOpenSection(SectionHandle, SECTION_ALL_ACCESS, &ObjectAttributes); }
LdrpFindKnownDll函式呼叫NtOpenSection,以開啟已知DLL的Named Section物件。這一過程中不會使用絕對路徑,而是通過本地系統呼叫的功能,根據OBJECT_ATTRIBUTES結構中的物件名稱來查詢指定根目錄。根目錄記錄在全域性變數LdrpKnownDllDirectoryHandle中。通過這種方式實現呼叫,能夠允許載入工具僅對檔名進行指定(例如EXAMPLE.DLL),而不必將絕對路徑重新構建為相對於現有目錄的查詢。我們跟蹤對LdrpKnownDllDirectoryHandle的引用,就可以發現它在LdrpInitializeProcess中進行初始化,如下所示:
NTSTATUS LdrpInitializeProcess() { // ... PPEB peb = // ... // If a full protected process don't use KnownDlls. if (peb->IsProtectedProcess && !peb->IsProtectedProcessLight) { LdrpKnownDllDirectoryHandle = nullptr; } else { OBJECT_ATTRIBUTES ObjectAttributes; UNICODE_STRING DirName; RtlInitUnicodeString(&DirName, L"\\KnownDlls"); InitializeObjectAttributes(&ObjectAttributes, &DirName, OBJ_CASE_INSENSITIVE, nullptr, nullptr); // Open KnownDlls directory. NtOpenDirectoryObject(&LdrpKnownDllDirectoryHandle, DIRECTORY_QUERY | DIRECTORY_TRAVERSE, &ObjectAttributes); }
上述程式碼實現了對NtOpenDirectoryObject的呼叫,將絕對路徑作為物件名稱,傳遞給KnownDlls目錄。開啟的控制代碼將會儲存在LdrpKnownDllDirectoryHandle全域性變數中,供以後使用。值得注意的是,此程式碼還會檢查PEB,以確定當前程序是否為完全受保護的程序。在完全受保護的程序模式下,禁用對載入已知DLL的支援,這也就是為什麼即使具有管理員許可權也只能攻破PPL而非PP的原因。
瞭解上述知識後,我們就可以不再嘗試程式碼劫持,而是使用COM型別混淆技巧,將值寫入任意記憶體位置,從而實現對資料的攻擊。由於可以繼承任意控制代碼到新的PPL程序中,所以我們可以使用Named Section來設定物件目錄,然後使用型別混淆的方法,將LdrpKnownDllDirectoryHandle的值更改為繼承控制代碼的值。如果我們使用一個從System32載入的已知名稱DLL,LDR將檢查偽目錄來查詢Named Section,並將無符號程式碼對映到記憶體中,甚至還會為我們呼叫DllMain。這個過程中,無需執行緒注入,無需ROP,也無需繞過CFG。
現在需要的,是一個合適的原語,用來寫入任意值。不幸的是,儘管我找到了能產生任意寫入的方法,但是卻不能充分控制寫入的內容。最終,我使用了以下介面和方法,該介面是在ICorSvcBindToWorker :: BindToRuntimeWorker返回的物件上實現的。
interface ICorSvcPooledWorker : IUnknown { HRESULT CanReuseProcess( [in] OptimizationScenario scenario, [in] ICorSvcLogger* pCorSvcLogger, [out] long* pCanContinue); }; };
在CanReuseProcess的實現過程中,pCanContinue的目標值總是初始化為0。因此,通過用[in] long替換型別庫定義中的[out] long*,就可以將0寫入我們指定的任何記憶體位置。通過使用到假KnownDlls目錄的控制代碼,將其預先填充到新程序控制代碼表的低16位,就可以確定真實KnownDlls的別名,這些KnownDlls將在程序啟動時開啟。而針對假冒的這些,只需修改高16位控制代碼為0即可。如下圖所示:
一旦我們使用0覆蓋了前16位(寫入是32位,但控制代碼實際是64位,並且在64位模式下執行,因此不會覆蓋任何重要的內容),LdrpKnownDllDirectoryHandle就會指向假KnownDlls控制代碼中的一個。然後,可以通過向同一個方法傳送自定義封裝物件,來引起DLL載入,從而實現在PPL內部執行任意程式碼。
將PPL提升到Windows TCB簽名級別
我們不能止步於此,對MSCORSVW的攻擊,只能讓我們在CodeGen級別獲得PPL,而非Windows TCB。通過對Windows TCB級別的WERFAULTSECURE.EXE進行DLL劫持,我們應該能夠獲得Windows TCB簽名級別的程式碼執行。該方法適用於Windows 10 1709以及更早版本,但對1803及以上版本無效。
在與Alex Ionescu進行討論後,我決定使用一個簡單的解析器,用於檢視檔案上的快取簽名資料。在NtObjectManager中,其作為Get-NtCachedSigningLevel命令公開。針對一個帶有假簽名的二進位制檔案和系統二進位制檔案執行此命令,我們發現該二進位制檔案的簽名也同樣被快取,其區別如下:
對於偽造的簽名檔案,Flags為TrustedSignature (0x02)。但是對於系統二進位制檔案,由於PowerShell無法解碼,因此只輸出整數值66(十六進位制為0x42)。0x40值是位於原始可信簽名上的額外標誌,如果沒有這個標誌,DLL似乎就不會被載入到PPL程序中。於是,我決定嘗試將一個不含額外標誌的有效簽名DLL載入到PPL程序。通過Process Monitor的監控,我得到了答案:
Process Monitor的跟蹤結果顯示,首先核心從DLL中查詢擴充套件屬性(EA)。快取的簽名級別資料儲存在檔案的EA中,因此我們可以斷定,這一過程就是在讀取快取的簽名級別。實際上,這一過程還會檢查許多內容,為簡潔起見,我從截圖中刪去了一部分不需要關注的內容。在EA設定完成後,如果我再次檢查檔案的快取簽名級別,就可以發現它現在包含了額外標誌。那麼,我們需要關注的就是,快取簽名級別是如何自動設定的?通過對棧進行跟蹤,我們可以發現它的工作原理:
檢視棧跟蹤的中間部分,能看到對CipSetFileCache的呼叫源自對NtCreateSection的呼叫。在需要時(例如PPL),核心將會自動快取簽名,這樣一來後續對映過程中就不需要再重新檢查簽名。如果從一個具有寫入許可權的檔案對映一個Image Section,那麼就可以重複編號1332的漏洞利用過程,使用NtCreateSection替換對NtSetCachedSigningLevel的呼叫,從而可以偽造任何DLL。事實證明,在對1332進行修復後,會在寫入檢查之後發生設定檔案快取的呼叫,並且可以藉助該呼叫繞過Device Guard。考慮到以上情況,我將這一漏洞進行上報,其編號為1597,CVE編號為CVE-2018-8449。與1332一樣,PPL的後門仍然存在,因此即使修復程式解決了Device Guard繞過問題,仍然可以利用該漏洞實現從PPL-CodeGen到PPL-Windows TCB的提升。
總結
本文主要說明了如何在不需要管理員許可權的情況下,將任意程式碼注入PPL。在普通使用者許可權下,能實現的操作其實並不多,無法對作業系統的某些重要部分進行修改。但是,如果成功將許可權提升到管理員,然後注入PPL,那麼就能得到更多的攻擊點,例如CSRSS(通過對其進行攻擊來獲得核心程式碼執行)和Windows Defender。隨著時間的推移,我相信PPL的大多數用例都會被虛擬安全模式(VSM)和隔離使用者模式(IUM)的應用程式所取代,這些模式將具有更高的安全保障。
在我向Microsoft報告漏洞後,他們明確表示,不會修復僅影響PP和PPL的漏洞。因此,該漏洞並不會在當前版本的Windows中修復,但很可能會在下一個主要版本中實現修復。此外,Microsoft還明確列出了針對一些Windows中所使用的技術,是否會進行修復並向提交漏洞者支付賞金,我們明確看到,PPL不在此列。
因此,Microsoft僅修復了我報告的一個漏洞問題。但仔細考慮一下,只修復Device Guard實際上是不充分的,我們仍然可以通過注入PPL並設定快取簽名級別,從而繞過Device Guard。這樣看來,Microsoft如此堅定的拒絕修復PPL問題是有些武斷的。安全功能很少是獨立存在的,都是通過彼此聯絡、環環相扣,來共同保障整個系統的安全。
後續,我還會發表一篇文章,討論如何使用COM的另一個功能來實現完整PP-Windows TCB程序注入,敬請期待。