Internet Explorer EPM沙盒跳出漏洞的分析(CVE-2014-6350)

wyzsk發表於2020-08-19
作者: blast · 2014/12/04 9:42

0x00 前言


作者: James Forshaw

原文: 連結

這個月微軟修復了3個不同的IE增強保護模式EPM的沙盒跳出漏洞,這些漏洞由我(原作者,下同)在8月披露。沙盒是Project Zero(我也參加了)中最主要的關注點所在,而且這也是攻擊者是否能實施一個遠端程式碼攻擊的關鍵點所在。

三個BUG都在MS14-065中修復了,你可以在 here here here 讀到文章內容。

CVE-2014-6350也許是最有趣的內容了,不是因為BUG很特別,而是因為要利用這個BUG時,使用的技術點比較非常規。它是一個讀任意記憶體的漏洞,但是透過COM宿主展現出了一個潛在的攻擊方式。這個博文就要深入的介紹一下如何才能實施這個漏洞。

0x01 這次的漏洞是什麼?


漏洞源於增強保護模式下的IE代理程式的許可權問題。這個漏洞並不會影響到舊的保護模式,原因我稍後介紹。EPM沙盒中執行著不可信的Tab程式,因為在Tab程式裡執行著網頁內容。而代理程式則負責在Tab程式需要的時候給它們提供必要的許可權。Tab和代理程式透過基於IPC通訊的DCOM來互動。

enter image description here

知道Windows訪問檢查是如何工作之後,我們應該可以確定你想要從EPM沙盒中的開啟代理程式時要獲得哪些許可權。在AppContainer中程式碼的訪問檢查比Windows用的一套機制更復雜一些。除了通常的訪問檢查之外,還有兩個獨立的用於計算DACL可以提供最大的許可權的額外檢查。第一個檢查是普通針對Token中使用者和組SID的,第二個是基於Compability SID的檢查。這兩組許可權進行按位和運算之後(*譯註:取交集)就是可以給使用者的最大許可權(這裡忽略了ACE因為它跟這個討論並無關係)。

enter image description here

讓我們看看代理程式的DACL,下面是一個簡化表單,第一次的訪問檢查將匹配當前使用者的SID,也就是說會給予完全控制(紅色標記處),第二次檢測則會匹配IE的Compability SID(藍色標記處),這兩個許可權取並集之後,則是隻有“讀、查詢”的許可權了。事實上這次微軟修復的就是讀記憶體的許可權。

enter image description here

我們可以呼叫OpenProcess來把代理程式的PID傳入,並且請求PROCESS_VM_READ許可權,這樣核心會返回給沙盒內程式一個控制程式碼。透過這個控制程式碼就可以用ReadProcessMemory來讀取代理程式的任意記憶體。 不過這個函式會正確處理讀無效的記憶體的操作,所以不會有任何崩潰。

#!c
BOOL ReadMem(DWORD ppid, LPVOID addr, LPVOID result, SIZE_T size) {
    HANDLE hProcess = OpenProcess(PROCESS_VM_READ,
                                      FALSE,
                                      ppid);
    BOOL ret = FALSE;


    if(hProcess) {
        ret = ReadProcessMemory(hProcess,
                 addr,
                 result,
                 size,
                 NULL);
        CloseHandle(hProcess);
    }


    return ret;
}

但是如果你是Win64位系統的話,從32位的Tab程式中執行此漏洞的話,事情會變的有一些複雜,因為此時Wow64(*譯註:64位子系統)會登場,你不能直接使用ReadProcessMemory來讀取64位的代理程式的記憶體。但是你可以使用一些例如wow64ext的模組來繞過這個限制,但是現在我們暫時不管它。

稍等,看一下PM,為什麼這裡不會有問題呢?在PM中只會做一個訪問檢查,所以我們可以獲得完全控制,但是因為微軟Vista之後引入的強制健壯性檢查(IL)特性,我們無法這麼做。當一個程式試圖開啟另一個程式的時候,核心會首先比較訪問者的IL和目標程式的系統ACL。如果訪問程式的IL比目標程式標記的健壯級別還要低,那麼訪問許可權會被限制成一個可用許可權的一個很小的子集(例如PROCESS_QUERY_LIMITED_INFORMATION)。這將會阻止PROCESS_VM_READ或者更危險的許可權,哪怕DACL已經檢查了都是如此。

好的,所以讓我們在Process Explorer中看看這個處於EPM沙盒中執行的程式,我們可以清楚的看到它的Token是處於低健壯級別的(下圖選中部分)。

enter image description here

但是奇怪的是,AppContainer訪問檢檢視起來像是忽略了中以下級別的任何資源。如果一個資源透過了DACL檢查,那麼它就會無視IL而被授予許可權。這看起來像是對包括檔案、登錄檔鍵的任何安全資源都有效。我不知道這個為什麼要這麼設計,但是看起來像是一個弱點之處,如果這裡IL正確檢查了也就沒有這事兒了。

0x02 實施漏洞


Google事件追蹤(https://code.google.com/p/google-security-research/issues/detail?id=97)提供原始的PoC提供了一個透過代理的IPC介面來讀取系統任意檔案的思路。透過讀取各程式的HMAC key,然後PoC因此偽造了一個有效的Token,然後透過CShDocVwBroker::GetFileHandle來開啟檔案。這個對EPM很有用,因為AppContainer會阻止讀取任意檔案。但是,再怎麼說,這個也就只是一個讀取,而不是寫入。理想情況我們應該能完全脫離Sandbox,而不是隻是洩露一些檔案的內容。

看起來是一個困難的工作,但是事實上還有更多的使用各程式秘密值(per-process secrets)的方式來讓自己變的更安全的技術。一個技術就是我最愛的Windows COM(說笑的)。而且最終,只要我們能洩露宿主程式的內容,就有一個可以在許多程式中引入遠端COM服務來執行程式碼的方式。

COM執行緒模型,套間和介面封送處理(Marshaling)


COM被Windows的多個元件使用,比如Explorer Shell,或者本地的許可權服務,例如BITS。每個用例都有不同的要求和限制,例如UI需要所有的程式碼都在一個執行緒裡面跑,否則作業系統會很不爽(譯註:程式設計師也不爽)。另一方面,一個功能類則可能是完全的執行緒安全的。為了支援這些需求,COM支援了一組執行緒模型,這個就解輕了程式設計師頭上的擔子(譯註:並沒有多少)。

套間中的一個物件定義了物件中的方法是如何被呼叫的。有兩類套間:1、單執行緒套間(STA)和多執行緒套間(MTA)(譯註:S他的特點是套間內永遠只有一個執行緒執行,具體參閱《COM本質論》,MTA則如字面意思)。當考慮到這些套間是如何被呼叫時,我們需要定義呼叫者和物件的關係。因此,我們將呼叫者的方法稱為“客戶端”,物件為“服務端”。

客戶端的套間由傳遞給CoInitializeEx(使用CoInitialize則預設為STA)的flag來決定。服務端的套間依賴於Windows登錄檔中COM物件的執行緒模型定義。可以有如下3個設定:Free(多執行緒),Apartment(單執行緒)、Both。如果客戶端和服務端有相容的套間(僅僅當服務端的物件支援兩個執行緒模型時),那麼呼叫該物件的函式呼叫就會透過物件的虛擬函式表直接解引用到對應的函式上。但是,在STA呼叫MTA或者MTA呼叫STA時我們需要透過某些方式來代理呼叫,COM透過封送處理來處理此類操作。我們可以總結成下表。

enter image description here

封送透過程式的序列化方法來呼叫服務端物件。這在STA中尤其重要,因為STA裡所有東西的呼叫都必須在一個執行緒裡面完成。通常這個是由Windows 訊息迴圈來完成,如果你的程式沒有視窗或者訊息迴圈,STA會為你建立一個(譯註:透過“隱藏的視窗”)。當一個客戶端呼叫一個不相容的套間中的物件時,它其實是呼叫一個特別的代理(譯註:proxy,不是上方的broker)物件。這個代理物件知道每個不通的COM介面和方法,包括什麼方法需要什麼引數。

代理接受到引數之後,會把他們透過內建的COM封送程式碼把它們序列化,然後送到服務端。服務端側透過一個呼叫來反封送引數,然後呼叫合適的方法。任何返回值都會透過同樣的方法傳送到客戶端。

enter image description here

結果是,這個模型在程式內執行的就像使用DCOM做程式間操作一樣好。同樣的代理封送技術和派發(dispatcher)可以用在程式或者計算機間。僅有的不同是封送的引數的傳送,他不再透過單個程式的程式內操作,而是透過本地RPC、命名管道甚至基於客戶端和服務端的位置,使用TCP去做。

自由執行緒封送者(free-threaded marshaler)


好吧,這就是如何在記憶體裡洩露資訊的漏洞?要理解這些,我需要再介紹一個稱作“附限制執行緒封送模型”(FTM)的東西,這個是和之前的表格相關的。STA客戶端呼叫一個相容多執行緒的服務端時,看起來客戶端透過這個代理-封送的過程來做通訊是很費效能的。為啥他不直接呼叫物件?這個就是FTM要解決的問題。

當COM物件從一個不相容的套間引用中例項化時,它必需向呼叫者返回一個物件的引用。這個和普通的call時是用的同樣的封送方法。事實上,這個機制同樣適用於呼叫物件的方法時,引數中帶COM物件的情況。使用這個機制傳遞引用的封送者建立了一個獨特的資料流叫OBJREF。這個資料流包含有所有的客戶端需要的,能建立一個代理物件並聯系服務端的資訊。這個例項是COM物件的“按引用傳遞”文法。OBJREF的例子如下:

enter image description here

在一些場景下,儘管可以透過值傳遞內容,比如中止代理。當原始的客戶端套間中指定物件的所有的程式碼都需要重新構造時,OBJREF流可以使用按值傳遞文法傳遞。當解封送者重新構造物件時,它會建立並初始化原始物件的一個全新複製,而不是獲取代理。一個物件可以透過IMarshal介面實現它自己的按值傳遞文法。

透過FRM使用的這個特性可以用來“欺騙”作業系統。即透過傳遞一個OBJREF中的已經在記憶體中序列化的物件,而不是傳遞原始物件資料的指標。當解封送時,這個指標會被反序列化並且返回給呼叫者。它表現得就像是一個“偽造的代理”,而且可以允許直接向原始物件傳送請求。

enter image description here

現在如果你覺得不舒服的話也是可以理解的。因為封送與DCOM有一些不同的地方,那麼程式內COM就是一個重大的安全漏洞嗎?很幸運,不是這樣的。FTM不僅會傳送指標值,還會確保封送的資料的反封送操作僅僅會在同一個程式內執行。它透過生成一個按程式的16位元組隨機值,並且把他附在序列化資料之後。當反序列化時FTM會檢查這個值,看看是不是當前程式儲存的這個值。如果兩個值不一樣,它會拒絕反序列化。這個操作的前提是攻擊者無法猜測或者破解這個值,因此在這個前提下FTM不會反封送任何它覺得不對的指標。但是這個威脅模型在我們可以讀取任意記憶體時其實並沒有用,所以我們才有了這麼一個漏洞。

FTM的這個實現是透過combase.dll的CStaticMarshaler類來完成的。在win7下則是ole32.dll的CFreeMalshaler類。看看CstaticMarshaler::UnmarshallInterface的程式碼,大致如下:

#!c
HRESULT CStaticMarshaler::UnmarshalInterface(IStream* pStm,
                                            REFIID riid, 
                                            void** ppv) {
 DWORD mshlflags;
 BYTE  secret[16];
 ULONGLONG pv;


 if (!CStaticMarshaler::_fSecretInit) {
     return E_UNEXPECTED;
 }


 pStm->Read(&mshlflags, sizeof(mshlflags));
 pStm->Read(&pv, sizeof(p));
 pStm->Read(secret, sizeof(secret));


 if (SecureCompareBuffer(secret, CStaticMarshaler::_SecretBlock)) {
     *ppv = (void*)pv;


     if ((mshlflags == MSHLFLAGS_TABLESTRONG) 
     || (mshlflags == MSHLFLAGS_TABLEWEAK)) {
         ((IUnknown*)*ppv)->AddRef();
     }


     return S_OK;
 } else {
     return E_UNEXPECTED;
 } 
}

注意這個方法會檢測秘密值(secret)是否已經初始化,這可以阻止攻擊者使用一個未初始化的秘密值(也即0)。還有需要注意的是需要使用一個安全的字元比較函式以免受到針對秘密值檢查的時差攻擊。事實上這是一個非向後移植的修復。在win7上,字串比較使用的是repe cmdsd指令,這個並不是線性時間的比較(*譯註:指非原子操作)。因此在win7上你也許可以一次旁路時差攻擊,雖然我覺得這個肯定巨費事。

最後這個結構看起來像是:

enter image description here

為了執行我們的程式碼,我們需要在我們的com物件中呼叫IMarshal介面。特別是我們需要執行兩個函式,Imarshal::GetUnmarshalClass,當重構建程式碼的時候,會用到它返回要使用的COM物件的CLSID。IMarshal::MarshalInterface,用來為漏洞打包合適的指標值。簡單的例子如下:

#!c
GUID CLSID_FreeThreadedMarshaller = 
{ 0x0000033A, 0x0000, 0x0000, 
{ 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, } };


HRESULT STDMETHODCALLTYPE CFakeObject::GetUnmarshalClass(
REFIID riid,
void *pv,
DWORD dwDestContext,
void *pvDestContext,
DWORD mshlflags,
CLSID *pCid)
{
memcpy(pCid, &CLSID_FreeThreadedMarshaller, 
sizeof(CLSID_FreeThreadedMarshaller));


return S_OK;
}


HRESULT STDMETHODCALLTYPE CFakeObject::MarshalInterface(
       IStream *pStm,
       REFIID riid,
       void *pv,
       DWORD dwDestContext,
       void *pvDestContext,
       DWORD mshlflags)
{
  pStm->Write(&_mshlflags, sizeof(_mshlflags), NULL);
  pStm->Write(&_pv, sizeof(_pv), NULL);
  pStm->Write(&_secret, sizeof(_secret), NULL);


  return S_OK;
}

夠簡單了吧,我們看看怎麼用它。

0x03 脫離沙盒


有了上面這些背景知識,也是時間脫離沙盒了。為了讓程式碼脫離沙箱,在代理程式中執行,有三個我們需要做的事情:

  1. 獲取中介程式中FTM各程式秘密值。

  2. 構建一個假虛表和假物件指標。

  3. 封送一個物件到代理程式以執行程式碼。

獲取各程式秘密值


這個很簡單,我們知道這個值存在記憶體的位置,因為combase.dll在沙盒程式和代理程式中的載入地址是一樣的。儘管Vista之後引入了ASLR,系統DLL只是在啟動之後會隨機化一次,因此combase.dll會在每個程式的同一個地方被載入。這是Windows的ASLR的弱點,特別是對本地提權而言就更是如此了。但是如果你從普通的IE操作中讀取這個值的話你會發現一個問題:

enter image description here

很不幸FTM還沒初始化,這意味著再著急我們都利用不了。我們該怎麼讓它在沙盒程式中初始化起來呢?我們只需要讓中介程式做更多的COM操作,特別是一些會引入FTM的操作。

我們可以使用開啟/儲存對話方塊,這個對話方塊是在Explorer Shell中實現的(shell32.dll),當然它是使用了COM的。而且它還是一個UI,因此他肯定會使用一個STA,但是會使用自由執行緒物件最終也會使用FTM。所以讓我們試試看,手動開啟一個對話方塊看看效果。

enter image description here

幹得不錯。選擇它的實際理由是因為我們可以使用IEShowSaveFileDialog API來在沙盒程式中啟動這個對話方塊(這個API通常由多個代理呼叫實現)。顯然這個會顯示一些UI出來,但是並不重要,因為對話方塊顯示的時候,FTM已經初始化過了,使用者已經沒啥要做的了。

現在我們可以硬編碼一些combase.dll的偏移。當然你也可以動態的在沙盒程式中初始化FTM然後透過記憶體搜尋找到它的位置。

構建假的虛表


下一個挑戰是讓我們的假虛表進入代理程式。因為我們可以讀取到代理程式的記憶體,所以我們可以確定的使用代理程式的API做一些例如堆淹沒(heap flooding)的操作,但是有沒有更簡單的方法?IE代理程式和沙盒程式有一個共享的記憶體節,他們用它來傳遞設定和資訊。這些節對沙盒程式來說有部分是可寫入的,因此我們需要做的是找到對應的中介程式的對映,然後修改成我們想要的內容。在這個的例子裡,使用了\Sessions\X\BaseNamed\Objects\URLZones_user (X是Session ID,user是使用者名稱),雖然它對映到了代理程式,對沙盒程式也是可寫入的,但是還需要一些東西。

我們不需要暴力的去找這個節,我們需要使用PROCESS_QUERY_INFORMATION許可權開啟程式,然後使用VirtualQueryEx來列舉所有對映的記憶體節,因為它會返回大小,所以我們可以快速的跳過沒對映的區域。然後我們可以找一個寫入區域的保護值(*譯註:canary value,用於檢測緩衝區溢位的值)來確定釋放地址。

#!c
DWORD_PTR FindSharedSection(LPBYTE section, HANDLE hProcess)
{
  // No point starting at lowest value
  LPBYTE curr = (LPBYTE)0x10000;
  LPBYTE max = (LPBYTE)0x7FFF0000;


  memcpy(&section[0], "ABCD", 4);


  while (curr < max)
  {
    MEMORY_BASIC_INFORMATION basicInfo = { 0 };
    if (VirtualQueryEx(hProcess, curr,
               &basicInfo, sizeof(basicInfo)))
    {
       if ((basicInfo.State == MEM_COMMIT)
          && (basicInfo.Type == MEM_MAPPED)
          && (basicInfo.RegionSize == 4096))
       {
          CHAR buf[4] = { 0 };
          SIZE_T read_len = 0;


          ReadProcessMemory(hProcess, (LPBYTE)basicInfo.BaseAddress, 
                            buf, 4, &read_len);


          if (memcmp(buf, "ABCD", 4) == 0)
          {
             return (DWORD_PTR)basicInfo.BaseAddress;
          }
        }


        curr = (LPBYTE)basicInfo.BaseAddress + basicInfo.RegionSize;
     }
     else
     {
        break;
     }
  }


  return 0;
}

決定了需要在共享記憶體的哪裡建立虛表和假物件之後,我們應該怎麼呼叫這個虛表?你可能會想到使用一個ROP鏈(*譯註:返回導向),但是顯然我們不需要這麼做。因為所有的COM呼叫都使用stdcall,所以所有引數都放在了棧上,所以我們可以幾乎透過this指標來呼叫所有的東西。

有一個用攻擊方式是使用類似LoadLibraryW的函式,然後構建一個會載入指向相對路徑DLL的假物件。因為虛表指標並沒有任何NULLCHAR(這個導致了在64位系統上難以使用這個方式去攻擊),因此我們可以把它(虛表)從路徑中移除,這會導致它載入那個庫。為了解決這個問題,我們可以把低16位設定成任何隨機值,而且因為高16位並不在我們掌控之中,所以它幾乎不會以0結束,因為Windows的空頁保護禁止分配低64KB的地址。最後我們的假物件看起來像是:

enter image description here

當然,如果你檢視它IUnknown介面的定義,你會發現這個物件的虛表中僅僅AddRef和Release有正確的signature。如果代理程式在物件上呼叫QueryInterface的話,這時候signature肯定是不正確的。在64位系統上因為傳參的方式不同,這個倒沒啥問題。但是在32位系統上這個卻會導致棧無法對齊,這不是我想要的結果。但是其實並沒事,如果這是一個問題的話肯定有解決方案,或者乾脆在代理程式中呼叫ExitProcess就好了。但是,注入一個物件時,我們要選擇一個合適的方式,如果物件可能完全不會呼叫它,也就不會出現這個問題了。這就是我們接下來要做的。

封送一個物件到代理程式


最終也是一個簡單的點,因為沙盒中使用的代理程式的所有介面都使用COM,因此我們需要做的就是找到一個只呼叫IUnknown的指標,然後把我們的假封送物件給它。為了達到這個目的,我發現你可以請求Shell Document View的IEBrokerAttach介面,它只有如下一個函式原型:

HRESULT AttachIEFrameToBroker(IUnknown* pFrame);

為了讓我們的指標到達中介程式之前做的更完美,我們會預設好一個frame,因此當不帶pFrame物件時呼叫這個方法會立刻失敗。因此我們並不需要擔心會有QueryInterface會被呼叫,我們的漏洞程式碼會在這個函式被呼叫之前就被執行,所以我們並不關心QueryInterface導致的問題。

所以,我們透過呼叫這個方法來建立我們的假物件。這將導致COM開始封送我們的程式碼到OBJREF物件中。最終在IPC通道的另一頭,也就是COM開始反封送的地方停止。這將呼叫FTM的UnmarshalInterface方法,而且我們已經成功的找到了秘密值,因此我們可以愉快的解包我們的假物件指標。最終,這個方法會呼叫物件上的AddRef,這時我們也可以傳遞mshlflags到MSHLFLAGS_TABLESTRONG。這將呼叫LoadLibraryW,而且它的“路徑”引數是我們的假物件,這將隨機載入一個DLL到代理程式中。所有需要做的就是彈一個calc,現在任務完成。

enter image description here

最終,真實的服務斷函式會被呼叫,但是會立刻返回一個錯誤。一個漂亮的沙盒跳出,儘管它需要大量的程式碼來支援。

0x04 演講結束


所以我在原來的事件追蹤(https://code.google.com/p/google-security-research/issues/detail?id=97)中新增了一個新的PoC,這可以在32位Windows 8.1系統上執行攻擊(顯然你不能打MS14-065補丁)。在64位Windows 8.1上它執行的不是太好,因為中介程式是64位的,儘管Tab程式可能還是32位的。如果你想讓他在64位上執行,你需要再試一試,但是因為你可以控制RIP,所以並不是什麼太難的事情。如果你想要在最新的機器上試驗,PoC中也有一個工具,SetProcessDACL,這可以修改一個程式的DACL,給它重新加一個帶讀許可權的IE Compability SID。

希望這個可以給你一些對待類似漏洞的解決方法。還有,不要因為這個抱怨COM,因為這個跟它沒啥關係。這只是用來演示一個相對無害的記憶體任意讀取最終如何打破層層防守演變成任意程式碼執行和許可權提升的例子。

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

相關文章