(翻譯)Attacking Interoperability(攻擊互操作性)in Black Hat 2009 研究報告

前言

在這裏插入圖片描述
在這裏插入圖片描述

內容介紹

  • 近年來,客戶端應用程序增加了定製的動態內容的驅動力。傳統上,這個目標是通過將佈局指令與腳本功能相結合的方式來實現的,腳本功能可以提供數據並以編程方式修改佈局(例如 HTML 和 JavaScript)。此模型已經發展到允許嵌入對象,擴展布局屬性和腳本引擎(包括在嵌入對象中使用的那些,如 Adobe 的 ActionScript)之間更精細的協作。這種新的互操作性浪潮有助於創建跨越多種技術的無縫用戶體驗。
  • 本文旨在探討軟件互操作性層的安全隱患,特別關注幾種突出的 Web 瀏覽器技術。我們將揭示大量未經探索的攻擊面,這些攻擊面是允許這種互操作性的直接結果,並討論了可能存在於其中的獨特類型的漏洞。此外,我們將探討互操作性對主機應用程序中實現的安全功能的影響。具體來說,我們將演示這些安全功能如何經常被可插拔組件破壞,這是信任擴展到所述組件的直接結果。雖然本文主要關注當代 Web 瀏覽器中存在的幾個互操作性層,但很多關於漏洞類和審計策略的討論可以應用於執行某種組件間數據交換的廣泛軟件。此類軟件的一些示例包括其他腳本語言和插件體系結構,RPC 堆棧和虛擬機。

本文的組織

  • 本文分爲三個部分。 首先,本文將在第1節中詳細介紹攻擊面的簡要介紹。具體來說,將檢查通用瀏覽器體系結構,並突出顯示與攻擊互操作性相關的組件。 本文的第二部分將提供技術概述,提供互操作性如何在兩個流行的瀏覽器中工作的背景信息:Microsoft Internet Explorer(IE)和 Mozilla Firefox。 最後,第 3 節將致力於列舉已識別的攻擊面中出現的漏洞類別,並展示揭示這些類型問題的實用策略。 在最後一節中,將對作者發現的一些重要的現實漏洞進行檢查。

第一節:攻擊面

  • 在深入討論目標軟件層之前,從概念層面瞭解攻擊面是非常重要重要的。當代 Web 瀏覽器的高級架構如圖1所示,其中包含與本白皮書相關的組件細分 。
    在這裏插入圖片描述
  • 圖1分爲三個邏輯層。 第一層是瀏覽器核心,它包含幾個組件,這些組件爲插件提供了與瀏覽器交互的環境。首先,插件是通過腳本控制的,但在某些情況下,它們也可以直接與瀏覽器的文檔對象模型(DOM)交互。
  • 第二層代表插件本身,它們本質上是瀏覽器加載的對象,主要通過處理唯一的文檔類型來支持其他功能。瀏覽器策略在瀏覽器環境中明確授予或拒絕信任插件; 但是它們有時會在瀏覽器的單獨進程上下文中運行。例如,Internet Explorer 8(IE8)瀏覽器在 Windows Vista 或 Windows 7 上運行時在限制性“低完整性”上下文中運行,而在限制較少的“中等完整性”上下文中,多個插件會耗盡進程。此策略允許插件在瀏覽器中保持完全信任,但對操作系統的上下文具有較少的信任。
  • 最後,存在第三層隱式受信任對象,這些對象可以加載可加載的對象以增強其自身的功能。由於瀏覽器顯式地將信任擴展到插件(插件 X),並且插件將信任擴展到任意對象(對象 Y),因此我們可以說在瀏覽器和由任意對象加載的任意對象之間建立了可傳遞的信任關係。插件(B -> X,X -> Y ,因此 B - > Y)。我們將在本文的第二部分中展示示例,這種信任的擴展允許攻擊者利用插件及其可信組件來破壞瀏覽器安全模型。第三層的另一個值得注意的部分是一些插件創建自己的腳本功能,在許多情況下可以用來與瀏覽器提供的腳本引擎或 DOM 進行交互。實際上,這種情況就是許多流行插件的情況,包括 Adobe Flash,Sun Java 和 Microsoft Silverlight 。在每種情況下,隱式地從瀏覽器擴展信任以允許攻擊者獲得對每種腳本語言提供的功能的訪問。此外,可以將對象從這些腳本語言導出回瀏覽器中的腳本上下文。由於信任傳遞性,這些對象可能不僅被瀏覽器腳本引擎操縱,而且還被 DOM 函數和其他插件操縱,有時會產生非常意想不到的後果。
  • 信任擴展不是互操作性的唯一安全成本。 從圖1中可以看出,爲了使每個附加組件相互交互,必須在互操作組件之間建立通信橋。這在圖1中用雙向箭頭表示。這些通信橋本身就是一個相當大的攻擊面:它是負責將數據從一個組件編組到下一個組件的代碼。編組層在協作組件本機的數據結構之間隱式執行轉換。由於該層在某種程度上無聲地運行,因此在嘗試發現安全漏洞時經常會被忽略。實際上,目前有大量文獻專門用於評估瀏覽器中的插件對象是否存在安全問題(具有實際結果),但很少有關於檢查互操作性層的信息。缺乏審查是本文試圖解決的一個領域。
  • 互操作性層是各種獨特漏洞類別的溫牀,這些漏洞類別以前基本上未被探索過。由於正在執行的操作,編組基礎設施通常會導致與類型混淆(由於對其類型的誤解而誤用數據)和對象保留(虛假引用計數問題)相關的漏洞,這些問題很少見於其他領域。一個應用程序。雖然過去偶爾會發現這些漏洞,但我們將展示目標軟件中流行的 API 如何特別容易受到攻擊,並將在本文第二部分提供揭示這些類型漏洞的策略。應該注意的是,雖然本文中提到的體系結構是以 Web 瀏覽器爲中心的,但是這些類型的問題在任何軟件中都是系統性的,這些軟件爲具有不同內部數據表示的組件之間的協作提供平臺。

第二節:技術概述

  • 本節概述了將用作案例研究的相關技術,以說明本文以下部分“攻擊互操作性”中提出的概念。我們討論了 Internet Explorer 的 ActiveX 控件架構,以及 Mozilla 的 NPAPI 插件架構(存在於 Firefox,Google Chrome 和其他一些非瀏覽器應用程序中)。我們將探討如何在可用的通用腳本語言中表示對象,如何對它們進行編組並將其導出到插件入口點,以及如何進行 DOM 交互。 最後,我們將爲 ActiveX 和 NPAPI 提供攻擊面摘要,總結每種技術在所述攻擊面的上下文中將扮演的角色。

2.1 微軟 ActiveX 插件

  • ActiveX 是一種源自微軟 COM 技術的技術。它用於創建可以暴露給運行時引擎(例如 JavaScript 和 VBScript)的插件,以便爲宿主應用程序提供額外的功能。瞭解本文第三部分將探討的漏洞類型需要深入瞭解一些 COM / Automation 架構。因此,我們將在本節中概述相關技術。我們還將探索“持久對象”的概念,它是序列化的 COM 對象,可以選擇嵌入到網頁中。第三部分將介紹如何使用持久 COM 對象不僅可以定位各種 COM 編組組件中的漏洞,還可以在某些情況下破壞瀏覽器安全功能。

2.1.1 插件註冊

  • ActiveX 控件是 COM 對象的特化,因此在系統註冊表中有一個描述相關實例化信息的條目。與任何其他 COM 對象一樣,每個 ActiveX 對象都由全局唯一的類 ID(CLSID)標識,並位於 HKEY_CLASSES_ROOT\CLSID{} 的註冊表中。也可以使用註冊表的 HKEY_CURRENT_USER 部分基於每個用戶安裝對象。由於 COM 對象在整個 Windows 操作系統中如此普遍使用,因此 Internet Explorer(IE)需要一種限制允許通過 Web 瀏覽器啓動哪些 COM 對象的方法。隨着時間的推移,安全機制的語義逐漸變得更加細化,這裏將簡要描述。

2.1.1.1 ActiveX 插件之安全控制技術

  • IE 有幾種機制來確定 ActiveX 對象是否具有運行權限。控件的安全權限分爲兩類:初始化和腳本。初始化安全性是指是否允許基於來自持久 COM 流的數據來實例化控件(稍後將深入討論)。腳本安全性是指是否可以通過在運行時公開的腳本 API 來操縱控件。 有關 ActiveX 安全控件的完整概述,請訪問 Microsoft,網址爲 http://msdn.microsoft.com/en-us/library/bb250471(VS.85).aspx ,本節中的大部分逆向工程信息都來源於這裏。
a) 通過註冊表進行安全控制
  • 將控件標記爲安全腳本(SFS)或安全初始化(SFI)的第一個也是最常見的方法是在註冊表中爲控件條目下添加特定子鍵。可以在 “Implemented Categories” 子項下添加兩個值,分別標記控件 SFS 和 SFI 。這些值分別爲 7DD95801-9882-11CF-9FA9-00AA006C42C4(CATID_SafeForScripting)7DD95802- 9882-11CF-9FA9 00AA006C42C4(CATID_SafeForInitialization)。圖2顯示了使用這些類別的控件示例:
    在這裏插入圖片描述
  • 控件可以使用 StdComponentCategoriesMgr 對象以編程方式爲這些類別註冊自己。ICatRegister 接口包含 RegisterClassImplCategories() 方法,該方法可用於處理任何給定 COM 對象的類別註冊信息。在內部,StdComponentCategoriesMgr 使用上述信息更新註冊表。
  • Internet Explorer 也使用 StdComponentCategoriesMgr 對象,但是用於枚舉而不是註冊。ICatInformation 接口提供了一個名爲 IsClassOfCategories() 的函數,IE 可以調用該函數來確定控件是 SFS 還是 SFI 。同樣,此操作在內部查詢上述註冊表位置以確定對象實現的控件。
b) 通過 IObjectSafety 接口進行安全控制
  • 存在將控件標記爲 SFS 或 SFI 的替代方法。ActiveX 控件可以通過實現 IObjectSafety 接口爲這些安全限制提供支持。在這種情況下,可以通過調用 IObjectSafety::GetInterfaceSafetyOptions() 方法獲得控件的安全功能,該方法具有以下原型:
	HRESULT IObjectSafety::GetInterfaceSafetyOptions(
		REFIID riid,
		DWORD *pdwSupportedOptions,
		DWORD *pdwEnabledOptions
	);
  • IE 將調用此函數以確定支持的安全選項集。如果界面似乎支持安全選項,則 IE 將使用它希望對象強制執行的選項調用 IObjectSafety 接口的 SetInterfaceSafetyOptions() 方法。SetInterfaceSafetyOptions 具有以下原型:
	HRESULT IObjectSafety::SetInterfaceSafetyOptions(
		REFIID riid,
		DWORD dwOptionSetMask,
		DWORD dwEnabledOptions
	);
  • 如果 SetInterfaceSafetyOptions() 成功返回,則應用程序可以使用 COM 對象,知道該對象打算使用所請求的安全選項。此 API 在 COM 類別上的附加值是控件可以提供更精細的控制方式,因爲它可以根據在 riid 參數中指定的接口 ID 爲不同的接口指定不同的安全設置以及方法調用。此外,IObjectSafety 接口可以執行本機代碼,以確定創建對象的應用程序是否可以安全地執行此操作。此類功能的一個具體示例是 Microsoft 提供的 SiteLock 模板代碼。此模板代碼允許程序員將 ActiveX 控件限制爲預定的 URL 列表。
c) 通過 ActiveX 中的 Killbits 值進行安全控制
  • IE 還實現了對標準安全功能的覆蓋,允許管理員專門禁止在瀏覽器中實例化所選控件。這是通過在 HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer\ActiveX Compatibility 註冊表位置添加一個子項來實現的。添加的子項必須具有相關控件的 CLSID,幷包含 DWORD 值 “Compatibility Flags”,其中設置了 “killbit”(值0x400)。圖3顯示了使用 killbit 集的控件示例:
    在這裏插入圖片描述
  • 當應用程序希望確定是否設置了 killbit 時,它將調用 CompatFlagsFromClsid() 函數,該函數從 urlmon.dll 導出。 CompatFlagsFromClsid() 具有以下原型:
	HRESULT CompatFlagsFromClsid(
		CLSID *pclsid,
		LPDWORD pdwCompatFlags,
		LPDWORD pdwMiscStatusFlags
	);
  • 當應用程序調用此函數時,它將傳入它感興趣的 COM 對象的 CLSID,以及兩個 DWORD 指針,其值將等於成功返回函數時對象的兼容性和其他 OLE 標誌。然後,應用程序將測試是否設置 0x400 位以確定控件是否具有 killbit 設置。
  • 如果設置了 Killbit,則註冊表中可能會出現一個條目,表示備用類 ID。此替代類 ID 將用於代替 Internet Explorer 中的原始類 ID。圖4顯示了使用備用類 ID 的類 ID 的註冊表項。處理圖4中的控件時,Internet Explorer 將透明地將類 ID 爲 {41B23C28-488E-4E5C-ACE2-BB0BBABE99E8} 的 COM 對象的請求轉換爲類 ID {52A2AAAE-085D-4187-97EA-8C30DB990436}
    在這裏插入圖片描述
d) 通過 Preapproved List 或 ActiveX Opt-In 選擇性的添加控件
  • Microsoft 在 Internet Explorer 7 中引入了一項名爲 ActiveX Opt-In 的功能。ActiveX Opt-In 旨在通過在允許網頁實例化以前未在 Internet Explorer 中加載的對象或用戶未通過 Internet Explorer 安裝的對象之前提示用戶來減少瀏覽器的攻擊面。圖5顯示了註冊表的相關區域:HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Ext\PreApproved
    在這裏插入圖片描述
  • 在 Windows 的基本安裝中,預先批准的列表上已有許多控件。但是,有更多的安全腳本或初始化的控件沒有出現在此列表中。此功能使得更有可能在此列表中找到控件中的缺陷,而不是其他控件中的缺陷。
e) 通過 ActiveX Per - User 將 Killbits 擴展爲域控制
  • IE8 引入了一系列與安全瀏覽相關的附加安全功能,包括對 ActiveX 的一些改進。在添加這些功能之前,可以在每臺計算機上配置可配置的控制權限。新功能將每臺機器的 killbit 擴展到每用戶級別的粒度,並通過允許基於用戶和域的 Opt-In 功能擴展 ActiveX opt-in。
  • 傳統上,killbits 已被用於有效地禁止整個控制系統的實例化。在許多用戶的系統上的單個用戶需要使用特定控件但沒有其他人需要它的情況下,此模型存在問題。微軟通過引入,擴展了 killbits 註冊表項 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Ext\Settings\{CLSID},其中 CLSID 是要限制的 ActiveX 控件的類 ID。通過將此鍵的 Flags 值設置爲 “1”,將限制單個用戶的控件。圖6顯示了在註冊表的這個區域中禁用的表格數據控件:
    在這裏插入圖片描述
  • 將 ActiveX 控件限制到某些域允許用戶對 ActiveX 安全性進行更精細的控制。最初,SiteLock 是唯一允許域限制的方法,最終用戶無法對其進行配置。通過將特定允許域的密鑰添加到 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Ext\Stats\{CLSID}\iexplore\Allowe dDomains,可以在註冊表中管理此新的每域限制。可以使用名稱 “*” 在此處添加所有域的密鑰,而不是特定域。
  • 每個域選擇加入控件通過要求用戶在不熟悉的域的上下文中運行之前批准使用 ActiveX 控件來減少攻擊面。實際上,這需要攻擊者將惡意 Web 內容插入受信任的域,以便偷偷地利用 ActiveX 控件。圖7顯示了配置爲在 microsoft.com 域內運行而沒有提示的表格數據控件:
    在這裏插入圖片描述
f) 基於 IE 圖形界面設置安全控制
  • 除了提供限制功能外,Microsoft 還通過添加一個界面來增強Internet Explorer UI,該界面允許用戶輕鬆配置ActiveX 控件權限,而無需修改註冊表。圖8顯示瞭如何訪問 Add-on Manager 界面,圖9顯示瞭如何在未經許可的情況下查找允許在瀏覽器中運行的 DLL:
    在這裏插入圖片描述
    在這裏插入圖片描述
g) ActiveX Safety Wrap-Up
  • ActiveX 有許多方法可以限制哪些控件可以加載,以及如何在給定的上下文中對它們執行操作。一個原因很可能是,隨着應用程序的互操作性增加,攻擊者也有機會。在這個前提下,ActiveX 安全性以攻擊 - 響應的方式發展,並導致安全體系結構有些破碎。在後面的部分中,我們將展示允許繞過這些限制的攻擊,主要是因爲 Microsoft 以特殊方式向瀏覽器添加安全功能,而不是從一開始就建立了強大的安全架構。

2.1.2 COM 概述

  • COM 是一種架構標準,它要求對象的語言無關表示,並促進這些對象之間的交互。Microsoft 使用 COM 作爲其許多主要技術的基本構建塊。它在旗艦 Windows 操作系統中非常普遍,並且還被許多其他外圍產品廣泛使用,例如 Internet Explorer 和 Office。在標題爲 Variants 的第一部分中,我們將討論 COM 用於通信的基本的,語言不可知的數據類型以及用於操作它們的 API。將探索 variant,以便爲讀者提供本文所關注的漏洞類型的更多背景信息。以下 variant 是一個名爲 COM Automation 的部分,它討論了可以很容易地暴露給腳本運行時環境的 COM 對象子集,統稱爲 ActiveX 控件。最後,在標題爲 COM 持久性概述的部分中,我們將討論持久性的概念 - 序列化 COM 對象的當前狀態並隨後在以後復活該對象的能力。將在潛在的惡意環境中探索持久性的使用,其中序列化對象可能源自不受信任的源(例如惡意網頁或辦公文檔)。

2.1.2.1 Variants 數據結構

  • VARIANTS 是整個 Windows 平臺中用於以標準化格式表示任意數據類型的關鍵數據結構之一。特別是,它們是 COM 的組成部分,用於在兩個或多個通信對象之間交換數據。VARIANT 數據結構相對簡單 - 它由類型和值組成,並在 Windows SDK 中的 OAIdl.h 中定義,如下所示:
struct __tagVARIANT
{
	VARTYPE vt;
	WORD wReserved1;
	WORD wReserved2;
	WORD wReserved3;
	union
	{
		BYTE bVal;
		SHORT iVal;
		FLOAT fltVal;
		DOUBLE dblVal;
		VARIANT_BOOL boolVal;
		
			… more elements …
		
		BSTR bstrVal;
		IUnknown *punkVal;
		IDispatch *pdispVal;
		SAFEARRAY *parray; 
		VARIANT_BOOL *pboolVal;
		_VARIANT_BOOL *pbool;
		SCODE *pscode;
		CY *pcyVal;
		DATE *pdate;
		BSTR *pbstrVal;
		VARIANT *pvarVal;
		PVOID byref;
	} __VARIANT_NAME_1;
} ;
  • VARIANT 包含的值可以是各種不同類型之一,因此只有在 vt 成員給定上下文時纔有意義,這表示類型。 VARIANT 可以表示相當多的基本類型。一些較常見的如表10所示:
    在這裏插入圖片描述
  • 從表10中可以看出,除了各種 COM 接口類型(如 IUnknown 和 IDispatch 接口)之外,所有基本數據類型都可以表示爲 variant 。此外,通過使用 IRecordInfo COM 接口支持用戶定義的類型。此接口提供定義自定義對象大小和 marshallers 的函數,以便可以表示任意數據結構。列出的類型僅是所有受支持的 VARIANT 類型的子集。可以在 Windows SDK 中的 wtypes.h 中找到所有可用類型的完整列表。
  • 除了基本 variant 類型之外,還有幾個修飾符,當與基本類型結合使用時,會更改 __VARIANT_NAME_1 聯合中包含的內容的含義。修飾符不能單獨使用; 它們專門爲基本類型提供附加上下文。它們通過將修飾符值(或多個值)與基本類型的值組合使用。表11總結了它們各自的含義:
    在這裏插入圖片描述
  • 從表中可以看出,基本類型都低於 0x0FFF,修飾符是大於 0x0FFF 的單比特值。因此,通過使用簡單的位掩碼操作使用修飾符擴充基本類型,形成了新的複雜類型。例如,包含字符串數組的 VARIANT 將具有類型 VT_ARRAY | VT_BSTR,值成員將指向 SAFEARRAY,其中每個成員都是 BSTR。(稍後將更深入地檢查SAFEARRAY)VARIANT 可以通過具有類型 VT_BYREF | VT_I4 來表示指向有符號整數的指針 VT_BYREF 修飾符也可以與其他修飾符一起使用,因此 VARIANT 可以具有類型(VT_BYREF | VT_ARRAY |VT_BSTR)。在這種情況下,value 成員將指向 SAFEARRAY 指針,其成員都是 BSTR 類型。
a) Safe Arrays 類型
  • 數組是 COM 使用的常見數據結構,存在於包含 vt 字段中的 VT_ARRAY 修飾符的 VARIANT 中。在這種情況下,SAFEARRAY 用於封裝相同數據類型的一系列元素,並且可以通過 SafeArray API 進行操作,以安全地訪問陣列成員,而無需擔心與陣列訪問相關的邊界和其他管理問題。儘管它們最常用於表示僅具有單個維度的陣列,但 SAFEARRAY 也能夠表示具有可能不同尺寸大小的多維陣列(通常稱爲“鋸齒狀陣列”)。SAFEARRAY 結構定義在 Windows SDK 的 OAIdl.h 中定義,如下所示:
	typedef struct tagSAFEARRAY
	{
		USHORT cDims;
		USHORT fFeatures;
		ULONG cbElements;
		ULONG cLocks;
		PVOID pvData;
		SAFEARRAYBOUND rgsabound[ 1 ];
	} SAFEARRAY;
  • SAFEARRAY 中包含的元素大小爲 cbElements,並且連續存儲在內存區域中,由 pvData 成員指向。 SAFEARRAYBOUND 結構數組跟在內存中的 SAFEARRAY 描述符之後,每個 SAFEARRAYBOUND 結構描述數組的單個維度。SAFEARRAYBOUND 結構構造如下:
	typedef struct tagSAFEARRAYBOUND
	{
		ULONG cElements;
		LONG lLbound;
	} SAFEARRAYBOUND; 
  • 簡單地說,lLbound 成員指示所描述維度的下限,cElements 成員指示該維度中存在多少成員。
  • SAFEARRAY API 相對廣泛,因此我們將考慮操作這些結構所需的最常見的API函數。前兩個函數用於初始化和銷燬,並且是彼此的補充:
	(1) SAFEARRAY *SafeArrayCreate(VARTYPE vt, UINT cDims, SAFEARRAYBOUND *rgsabound);
	(2) HRESULT SafeArrayDestroy(SAFEARRAY * psa);
  • 這些函數分別用於創建和銷燬數組。創建數組時,將指定每個數組成員的數據類型,以及數組的維數。這些屬性都是不可變的; 創建後無法修改 SAFEARRAY 的類型和維數。
  • 有兩種不同的方式來訪問數組中的數據。第一種方法是獲取指向所有元素所在的內存的指針,並使用以下函數完成:
	(1) HRESULT SafeArrayAccessData(SAFEARRAY * psa, void HUGEP** ppvData);
	(2) HRESULT SafeArrayUnaccessData(SAFEARRAY * psa);
  • 當訪問循環中的元素時,這通常是首選方法,格式如下:
	BSTR *pString;
	if(FAILED(SafeArrayAccessData(psa, &pString))
		return -1;
	for(i = 0; i < psa->rgsabound[0].cElements; i++)
	{
		… operate on string …
	}
	SafeArrayUnaccessData(psa);
  • 訪問數據的第二種方法是使用以下函數訪問單個元素:
	(1) SafeArrayGetElement(SAFEARRAY * psa, LONG * rgIndices, void * pv);
	(2) SafeArrayPutElement(SAFEARRAY * psa, LONG * rgIndices, void * pv);
  • 這些函數中的每一個都採用一系列索引,並將返回或存儲有問題的特定值。請注意,在內部,兩個函數都驗證提供的索引的有效性,以確保每個數組訪問都在邊界內。
  • 最後,我們應該提到 SAFEARRAY 具有鎖定機制以確保對陣列數據的獨佔線程訪問,由以下兩個函數訪問:
	(1) HRESULT SafeArrayLock(SAFEARRAY * psa);
	(2) HRESULT SafeArrayUnlock(SAFEARRAY * psa);
b) VARIANT 和 VARIANTARG 類型
  • 許多 VARIANT API 函數採用 VARIANT 或 VARIANTARG。Microsoft 文檔表明,這兩個值之間的區別在於 VARIANT 總是包含直接值(即,它們不能具有修飾符 VT_BYREF),而 VARIANTARG 可以。實際上,您將在 VARIANT API 的討論中進一步注意到大多數 Variant * 函數都採用 VARIANTARG。實際上,儘管文檔另有說明,但這些結構實際上是等效的並且可以互換使用。此外,使用它們時不會生成編譯器錯誤(有關其所謂區別的 Microsoft 文檔,請訪問 http://msdn.microsoft.com/en-us/library/ms221627.aspx )。
c) VARIANT API 函數
  • 用於操作 VARIANT 的 API 非常廣泛,但是隻有少數功能與本文的目的相關,本節將對它們進行討論。
c - 1) Variant 的初始化和銷燬
  • 使用 VariantInit() 函數初始化 VARIANT,該函數具有以下原型:
	HRESULT VariantInit(VARIANTARG *pvarg);
  • 除了將 VARIANT 的類型成員 vt 設置爲 VT_EMPTY 之外,此函數不執行任何操作,指示 VARIANT 不保留任何值。 稍後使用互易函數 VariantClear() 清理 VARIANT :
	HRESULT VariantClear(VARIANTARG *pvarg); 
  • VariantClear() 函數還將清除 vt 成員,以及釋放與VARIANT相關的任何數據。例如,如果 VARIANT 包含 IDispatch 或 IUnknown 接口(分別鍵入 VT_DISPATCH 或 VT_UNKNOWN),則 VariantClear() 將釋放該接口。如果 VARIANT 是一個字符串(VT_BSTR),它將被取消分配,依此類推。
c - 2) 操縱 Variant
  • 可以使用 API 在 VARIANT 上執行的兩種主要操作類型是轉換和複製。VarXXFromYY() 形式有多種特定的轉換函數,其中 XX 是目標 VARIANT 類型,YY 是源類型。還有用於在任何兩種 VARIANT 類型之間進行轉換的通用函數,如下所示。
	(1) HRESULT VariantChangeType(VARIANTARG *pvargDest, VARIANTARG *pvargSrc, unsigned short wFlags, VARTYPE vt);
	(2) HRESULT VariantChangeTypeEx(VARIANTARG *pvargDest, VARIANTARG *pvargSrc, LCID lcid, unsigned short wFlags, VARTYPE vt);
  • 這兩個函數執行的任務基本相同 - 將 pvargSrc 轉換爲 vt 指定的類型,並將結果放在 pvargDest 中。 這些功能將在本文第3節中進一步深入探討。
  • 值得一提的其他函數是那些負責將 VARIANT 值從一個 VARIANT 複製到另一個 VARIANT 的函數:
	(1) HRESULT VariantCopy(VARIANTARG *pvargDest, VARIANTARG *pvargSrc);
	(2) HRESULT VariantCopyInd(VARIANTARG *pvargDest, VARIANTARG *pvargSrc);
  • 這些函數既清除目標 VARIANT,又複製源 VARIANT 。他們做了很深的副本; 也就是說,如果複製了 COM 接口,則引用計數會遞增,依此類推。這兩個函數之間的區別在於 VariantCopyInd() 將遵循副本的間接引用(即,如果 VARIANT 具有 VT_BYREF 修飾符,則該值將被解除引用然後被修改),而 VariantCopy() 則不會。VariantCopyInd() 也是遞歸的; 如果收到具有類型(VT_BYREF | VT_VARIANT)的 VARIANT,則將進一步檢查目標 VARIANT。如果它也是(VT_BYREF | VT_VARIANT),則發出錯誤信號。 如果它具有 VT_BYREF 修飾符但不是 VT_VARIANT,則此 VARIANT 將再次傳遞給 VariantCopyInd(),從而檢索存儲的值。

2.1.2.2 COM 自動化

  • 如前所述,COM Automation 有助於將可插入組件集成到腳本環境中。這主要通過創建實現一個或兩個自動化接口的對象來實現:IDispatch 和 IDispatchEx。IDispatch 接口公開了旨在實現以下指令的函數:
	1. Allow an object to be self-publishing – ie. Advertise its properties and methods
	允許對象自我發佈 - 即公開其屬性和方法
	
	2. Allow methods to be called or properties to be manipulated by name, rather than direct VTable memory manipulation.
	允許通過名稱調用方法或屬性,而不是直接VTable內存操作

	3. Provide a unified marshalling interface for objects being passed to methods or properties, as well as objects being returned to the scripting host.
	爲傳遞給方法或屬性的對象提供統一的編組接口,如以及返回腳本主機的對象
  • 通過實現 IDispatch,對象可以在運行時由主機應用程序加載,並隨後進行操作,而主機不必知道有關對象的任何編譯時詳細信息。此功能對於需要可擴展性的腳本接口特別有用。
  • IDispatch 接口派生自 IUnknown(均在 MSDN 上記錄),添加了四種方法,如下所示:
	 /*** IDispatch methods ***/
	HRESULT (STDMETHODCALLTYPE *GetTypeInfoCount)(
		IDispatch* This,
		UINT* pctinfo);
	HRESULT (STDMETHODCALLTYPE *GetTypeInfo)(
		IDispatch* This,
		UINT iTInfo,
		LCID lcid,
		ITypeInfo** ppTInfo);
	HRESULT (STDMETHODCALLTYPE *GetIDsOfNames)(
		IDispatch* This,
		REFIID riid,
		LPOLESTR* rgszNames,
		UINT cNames,
		LCID lcid,
		DISPID* rgDispId);
	HRESULT (STDMETHODCALLTYPE *Invoke)(
		IDispatch* This,
		DISPID dispIdMember,
		REFIID riid,
		LCID lcid,
		WORD wFlags,
		DISPPARAMS* pDispParams,
		VARIANT* pVarResult,
		EXCEPINFO* pExcepInfo,
		UINT* puArgErr);
  • 如果應用程序想要調用任何方法或修改對象公開的任何屬性,則首先需要確定與其要調用的方法關聯的調度 ID。要確定此信息,應用程序首先需要調用 GetIdsOfNames() 。返回值是一個整數,它映射到將通過 Invoke() 方法執行的實際方法。Invoke() 方法將要執行的成員的 ID,方法的參數以及有關語言環境等的一些其他信息作爲參數。傳遞給 Invoke() 的 wFlags 參數定義了調度 ID 是引用由對象公開的方法還是應該獲取或設置的屬性值。將要執行的方法的參數在 DISPPARAMS 結構中傳遞。DISPPARAMS 結構定義如下:
	typedef struct FARSTRUCT tagDISPPARAMS{
		VARIANTARG FAR* rgvarg;					// Array of arguments(參數數組)
		DISPID FAR* rgdispidNamedArgs; 			// Dispatch IDs of named arguments(已命名參數的調度ID)
		Unsigned int cArgs; 					// Number of arguments(個數參數)
		Unsigned int cNamedArgs; 				// Number of named arguments(命名參數的數目)
	} DISPPARAMS;
  • 如您所見,此結構將參數傳遞給 VARIANT 數組中的方法(有關更多詳細信息,請參閱有關 VARIANT 的部分)。必須通過被調用的方法對此數組進行解組。 在某些情況下,考慮到陣列中可能存在的某些 VARIANT 類型的複雜性,這可能是一項艱鉅的任務。
  • IDispatch 接口對於創建行爲不可變的自動化對象非常有用 - 必須在編譯時知道屬性和方法,並且它們不會更改。但是,在某些情況下,需要具有可在運行時修改其行爲的對象,並且 IDispatchEx 接口擴展 IDispatch 以允許此附加功能。使用 IDispatchEx 對象,可以在運行時添加或刪除屬性或方法。這是更動態的後期綁定語言(例如腳本語言 JavaScript)通常需要的功能。
  • IDispatchEx 也派生自 IUnknown 接口,添加了以下八種方法:
	HRESULT DeleteMemberByDispID(
		DISPID id);
		
	HRESULT DeleteMemberByName(
		BSTR bstrName,
		DWORD grfdex);
		
	HRESULT GetDispID(
		BSTR bstrName,
		DWORD grfdex,
		DISPID *pid);
		
	HRESULT GetMemberName(
		DISPID id,
		BSTR *pbstrName);
		
	HRESULT GetMemberProperties(
		DISPID id,
		DWORD grfdexFetch,
		DWORD *pgrfdex);

	HRESULT GetNameSpaceParent(
		IUnknown **ppunk);
		
	HRESULT GetNextDispID(
		DWORD grfdex,
		DISPID id,
		DISPID *pid);
		
	HRESULT InvokeEx(
		DISPID id,
		LCID lcid,
		WORD wFlags,
		DISPARAMS *pdp,
		VARIANT *pVarRes,
		EXCEPINFO *pei,
		IServiceProvider *pspCaller);
  • 雖然檢索調度 ID 的方式存在一些差異,但 IDispatchEx 的主要更改是允許創建和刪除對象屬性和方法。例如,GetDispID() 與 GetIdsOfNames() 的不同之處在於,它可以被告知爲新屬性或方法創建新名稱和分派 ID。此外,您還可以看到添加了 DeleteMemberByName() 和 DeleteMemberByDispID() 方法。在擴展 IDispatchEx 接口的 ActiveX 控件中,可以通過 JavaScript 對訪問成員進行動態創建和刪除。
  • 有趣的是,JavaScript(用於 Internet Explorer)本身是使用 Microsoft 腳本引擎公開的經過修改 IDispatchEx 接口實現的。從概念上講,這種實現是有意義的,因爲 JavaScript 需要能夠創建對象並添加和刪除所有成員,而不需要任何先入爲主的概念。 因此,例如,當 JavaScript 創建一個新對象時:
	Obj = new Object();
  • Internet Explorer 將首先調用 Obj 的 GetDispID() 方法 - 確保將 fdexNameEnsure 標誌設置爲創建成員。然後它將調用自己的內部版本的 Invoke() 來調用 Object() 方法。然後,調用 Invoke() 返回的值將分配給 Obj 成員。

2.1.2.3 COM 持久性概述

  • COM 提供了兩個主要接口來操作對象的持久性數據。第一個接口 IStream 表示用於存儲單個對象的持久數據的數據流。它支持標準文件操作,包括使用接口方法進行讀取,寫入和搜索。IStream 接口從流的使用者抽象出底層存儲細節。此抽象允許 COM 對象實現序列化功能,而無需明確瞭解底層後備存儲。該抽象在圖12中可視地描繪:
    在這裏插入圖片描述
  • 當程序或 COM 對象需要多個對象的持久性時,使用第二個接口 IStorage 。 IStorage 表示一個存儲文件,它可以使用唯一名稱在單個文件中保存邏輯上獨立的二進制流,以標識每個流。此外,存儲文件可以包含邏輯上獨立的從屬存儲文件,也可以通過唯一名稱訪問,從而允許在需要時進行遞歸。IStorage 接口提供的方法允許程序員訪問每個組成流和從屬存儲文件。圖13描繪了典型存儲文件的示例:
    在這裏插入圖片描述
  • 除了 IStream 和 IStorage 之外,還有幾個其他接口可用於操作 COM 持久性數據,具體取決於包含數據的介質。以下是可以存儲持久對象數據的接口列表:
	(1) IMoniker
	(2) IFile
	(3) IPropertyBag
	(4) IPropertyBag2
  • COM 對象通過實現幾個衆所周知的持久性接口之一來支持序列化。這些持久性接口中的每一個都是 IPersist 接口的特化,具有以下定義:
	MIDL_INTERFACE("0000010c-0000-0000-C000-000000000046")
	IPersist : public IUnknown
	{
	public:
		virtual HRESULT STDMETHODCALLTYPE GetClassID(
		/* [out] */ __RPC__out CLSID *pClassID) = 0;
	}
  • IPersist 的每個子類都有名爲 Load() 和 Save() 的方法,它們分別對數據進行序列化和恢復。這些子類之間的區別是保存持久數據的接口類型。表14列出了持久性接口以及每個相應接口用於保存數據的參數類型。圖15直觀地描述了這些接口的繼承層次結構:
    在這裏插入圖片描述
  • 當主機程序希望序列化對象時,它將查詢該對象以獲得持久性接口。如果成功,應用程序將調用 Save() 方法,將指針傳遞給之前討論的存儲接口之一( IStream、IStorage、IFile 等)。稍後,當主機程序希望從持久狀態恢復對象時,它將再次檢索對象的持久性接口,並調用 Load() 方法。從持久性數據中恢復的對象應該等同於先前保存的對象。

在這裏插入圖片描述

a) 在 ATL 中實現 COM 持久性
  • COM 對象的開發人員可以自由地實現自己的持久性接口。如果這些開發人員選擇爲接口編寫自己的代碼,他們將通過以任意格式讀取和寫入數據來操縱存儲持久性數據的接口。但是,大多數開發人員選擇使用 Microsoft ATL 中提供的模板類,當有模板代碼時,可以避免實現這些接口所需的額外工作。Microsoft ATL 的第 9 版具有以下持久性接口的模板類:
	(1) `IPersist` 
	(2) `IPersistPropertyBag` 
	(3) `IPersistStorage` 
	(4) `IPersistStreamInit`
  • 模板代碼要求程序員定義一系列屬性,稱爲屬性映射,持久性接口將使用該屬性作爲用於序列化和恢復相關對象的樣板。此屬性映射是一個終止的結構數組,列出了必須序列化和恢復的控件的屬性,並且應該足夠明確,以保證對象一旦序列化,將等同於從數據中恢復的對象。 ATL 的第9版包括各種宏,以幫助程序員定義這些屬性幷包括來自以下列表的宏:
	(1) BEGIN_PROPERTY_MAP
	(2) BEGIN_PROP_MAP
	(3) PROP_ENTRY
	(4) PROP_ENTRY_EX
	(5) PROP_ENTRY_TYPE
	(6) PROP_ENTRY_TYPE_EX
	(7) PROP_PAGE
	(8) PROP_DATA_ENTRY
	(9) END_PROPERTY_MAP
	(10) END_PROP_MAP
  • 前面提到的每個宏函數都採用各種參數,並使用它們來定義 ATL_PROPMAP_ENTRY 結構。以下代碼是從 ATL 版本 9 中獲取的結構定義:
	struct ATL_PROPMAP_ENTRY
	{
		LPCOLESTR szDesc;
		DISPID dispid;
		const CLSID* pclsidPropPage;
		const IID* piidDispatch;
		DWORD dwOffsetData;
		DWORD dwSizeData;
		VARTYPE vt;
	};
  • ATL_PROPMAP_ENTRY 結構中的元素對於理解非常重要,並在表16中進行了總結:
    在這裏插入圖片描述
  • 用於定義屬性的宏函數使用提供給函數的參數來設置某些 ATL_PROPMAP_ENTRY 元素,並將其他元素設置爲默認狀態。根據具有非默認值的元素,負責持久性操作的模板代碼在序列化和恢復數據時將使用略有不同的策略。 BEGIN_PROPERTY_MAP 和 BEGIN_PROP_MAP 都將包含開始定義結構的代碼; 但是,前者將自動在屬性映射中包含 X 和 Y 位置信息。 END_PROP_MAP 和 END_PROPERTY_MAP 是宏函數,包括終止 ATL_PROPMAP_ENTRY 元素並結束結構定義。在 BEGIN_PROPERTY_MAP 或 BEGIN_PROP_MAP 和 END_PROP_MAP 或 END_PROPERTY_MAP 之間是 ATL_PROPMAP_ENTRY 實例,它們描述 COM 對象的屬性。
  • PROP_ENTRY 和 PROP_ENTRY_EX 都使用屬性的名稱,顯示 ID 和可用於設置屬性的屬性頁來定義屬性。 PROP_ENTRY_TYPE 和 PROP_ENTRY_TYPE_EX 定義與 PROP_ENTRY 和 PROP_ENTRY_EX 相同的信息;但是,它們還需要在處理屬性時預期的顯式 variant 類型。 “_EX” 後綴表示宏函數還需要在設置時使用顯式調度接口 ID 獲得屬性的價值。 PROP_DATA_ENTRY 宏需要該屬性的唯一字符串標識符,將用於存儲該屬性的類成員的名稱,以及該屬性所需的 variant 類型。在內部,PROP_DATA_ENTRY 宏使用 offsetof 和 sizeof 結構在 ATL_PROPMAP_ENTRY 結構中顯式定義 dwOffsetData 和 dwSizeData。 PROP_PAGE 用於指定提供 GUI 界面的 COM 類 ID,該 GUI 界面可以操縱對象的屬性。
  • 爲了幫助說明在 C 代碼中使用屬性映射以及如何從持久狀態讀取屬性,我們將簡要介紹一個名爲 HelloCom 的 COM 對象示例。 HelloCom 是一個簡單的 ActiveX 控件,可以存儲一個人的名字和姓氏。屬性將具有以下名稱:
	(1) NameFirst
	(2) NameLast
  • 以下 C++ 代碼段顯示了與實現持久性相關的 HelloCom 控件的部分代碼:
	class HelloCom :
		public IPersistStreamInitImpl<HelloCom>,
		public IPersistStorageImpl<HelloCom>,
		public IPersistPropertyBagImpl<HelloCom>,
	{
	public:
	BEGIN_PROP_MAP(HelloCom)
		PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
		PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
		PROP_ENTRY("NameFirst", 1, CLSID_HelloComCtrl)
		PROP_ENTRY_TYPE("NameLast", 2, CLSID_HelloComCtrl, VT_BSTR)
	END_PROP_MAP()
	};
  • 如果應用程序正在從二進制流加載持久性數據,則應用程序將查詢 IPersistStreamInit 接口,並將接收指向 IPersistStreamInitImpl 模板類的 vtable 。接下來,應用程序將調用 Load() 方法,傳入將用於讀取持久性數據的 IStream 對象。在流中的任何序列化數據之前,存儲版本號以便處理向後兼容性問題。因此,流中的前四個字節將是用於編譯控件的 ATL 版本的小端表示。在 Visual Studio 2008 中,此值爲 0x00000900。 只要該值小於或等於用於編譯控件的 ATL 的版本,就可以恢復處理,否則會發出錯誤信號。
  • 在處理版本控制信息之後,可以從流中檢索屬性本身。在這種情況下,流中版本號後面的字節將是 _cx 和 _cy 元素的兩個 4 字節 little-endian 表示。由於這些元素是使用 PROP_DATA_ENTRY 宏聲明的,因此這些 32 位值將直接寫入 m_sizeExtent.cx 和 m_sizeExtent.cy 值所在的類中的內存偏移量。
  • 遵循這些值,我們將遇到 NameFirst 的序列化表示。由於 NameFirst 是使用不包含數據類型的 PROP_ENTRY() 宏在屬性映射中聲明的,因此需要從流中檢索類型信息。因此,流中的前兩個字節將是無符號的 16 位值 0x0008,表示 variant 類型 VT_BSTR。接下來將是一個無符號的 32 位值,指定字符串的長度。如果名稱是 “Example”,那麼指定大小的這個 32 位整數的值將等於 0x10; 七個 2 字節字符加上一個終止空值。接下來的值將是表示名稱的字符,後跟終止的 16 位值 0x0000。接下來的 NameLast 與 NameFirst 相同,除了流中不存在 16 位 variant 類型說明符,因爲使用 PROP_ENTRY_TYPE() 宏在屬性映射中顯式聲明瞭類型。
  • 表 17 顯示了前面段落中描述的流的示例,其中十六進制值表示流中的值,偏移量表示流中值的位置,以及如何解釋值的描述:
    在這裏插入圖片描述
b) Internet Explorer 中的 COM 持久性
  • Microsoft Internet Explorer 在爲ActiveX對象的屬性賦值時使用持久性。Internet Explorer 使用的六個主要接口(按優先順序排列)是 IPersistPropertyBag,IPersistMoniker,IPersistFileIPersistStreamInit,IPersistStream 和 IPersistStorage。瀏覽器將嘗試按順序檢索每個持久性接口的接口指針,直到成功,或者沒有找到接口,此時操作失敗。
  • 第一個也是最熟悉的持久性接口是 IPersistPropertyBag。IPersistPropertyBag 專門設計用於允許將持久對象嵌入 HTML 中。舉例來說,以下 HTML 代碼將 Microsoft Media Player 嵌入到網頁中
	<OBJECT id="VIDEO" CLASSID="CLSID:6BF52A52-394A-11d3-B153-00C04F79FAA6" >
		<PARAM NAME="URL" VALUE="MyVideo.wmv">
		<PARAM NAME="enabled" VALUE="True">
		<PARAM NAME="AutoStart" VALUE="False">
		<PARAM name="PlayCount" value="3">
		<PARAM name="Volume" value="50">
		<PARAM NAME="balance" VALUE="0">
		<PARAM NAME="Rate" VALUE="1.0">
		<PARAM NAME="Mute" VALUE="False">
		<PARAM NAME="fullScreen" VALUE="False">
		<PARAM name="uiMode" value="full">
	</OBJECT>
  • 標記內出現的 標記表示 COM 對象的屬性名稱和持久值。當 Internet Explorer 分析網頁並遇到這些 PARAM 標記時,它首先創建一個 PropertyBag 類並查詢 IPropertyBag 接口。接下來,它將解析 PARAM html 標記的名稱和值參數,並在 IPropertyBag 接口上調用 Write() 方法,提供其已解析的屬性的名稱和字符串表示形式。一旦 Internet Explorer 將所有 PARAM 標記加載到屬性包中,它將查詢 IPersistPropertyBag 接口的 COM 對象(在上面的示例中爲 Media Player 對象)。然後, Internet Explorer 將調用 IPersistPropertyBag 接口的 Load() 方法,並傳遞 PropertyBag 從 HTML 解析。然後,COM 對象的 Load() 方法將屬性從字符串表示轉換爲對象的首選表示,然後將轉換後的表示保存在 COM 對象中。當遇到上述 HTML 時,Internet Explorer 使用此策略從持久狀態恢復對象。
  • 當使用對象的 innerHTML 屬性時,最常遇到恢復操作(序列化)。請考慮以下 JavaScript 代碼,在與上述 HTML 相同的網頁中使用:
	<script language="JavaScript">
		alert(VIDEO.innerHTML);
	</script>
  • 處理完之前的 JavaScript 後,網頁將通過帶有 HTML 格式文本的消息框提醒用戶,類似於以下示例:
	<PARAM NAME="URL" VALUE="./MyVideo.wmv">
	<PARAM NAME="rate" VALUE="1">
	<PARAM NAME="balance" VALUE="0">
	<PARAM NAME="currentPosition" VALUE="0">
	<PARAM NAME="defaultFrame" VALUE="">
	<PARAM NAME="playCount" VALUE="3">
	<PARAM NAME="autoStart" VALUE="0">
	<PARAM NAME="_cx" VALUE="6482">
	<PARAM NAME="_cy" VALUE="6350">
  • 當 Internet Explorer 使用 PropertyBag 序列化對象時,它首先創建 PropertyBag 類的實例。接下來,它查詢要爲 IPersistPropertyBag 接口保留的對象。檢索接口後, Internet Explorer 將調用 Save() 方法,並傳遞 PropertyBag 類實例。最後,Internet Explorer 會將 PropertyBag 類序列化爲與 HTML 標準兼容的格式。
  • 將持久性數據插入 Internet Explorer 控件的第二種不太常見的方法是通過使用 OBJECT 標籤的 data 參數。 這種持久性的一個例子是如下 HTML 所示。
	<OBJECT
		id="VIDEO"
		CLASSID="CLSID:6BF52A52-394A-11d3-B153-00C04F79FAA6"
		data="./persistence_data"
		type="application/x-oleobject"
	/ >
  • 在上面的示例中,不是使用 PARAM 標記,而是通過 object 標記的 data 參數傳遞持久性數據。當 Internet Explorer 遇到此格式的對象標記時,它遵循複雜的策略從序列化數據中恢復對象。
  • Internet Explorer 將首先檢查 data 參數中指定的文件名,以查看文件擴展名是否等於 “.ica” , "stm”或 “.ods” 。如果擴展名是其中之一,則它會創建一個 IStream ,它可以從提供的文件 URL 中讀取二進制數據。然後,Internet Explorer 將創建在文件的前 16 個字節中指定的對象的實例,或者,如果這 16 個字節爲零,則在對象標記中創建 CLASSID 參數並查詢 IPersistStream 接口。如果成功檢索到接口, Internet Explorer 將調用接口的 Load() 方法,並傳入 IStream。接下來,COM 對象將解析流並將二進制數據轉換爲每個屬性的首選表示形式。完成這些操作後,Internet Explorer 將完全擁有恢復的 COM 對象。
  • 如果文件名與其中一個衆所周知的擴展名不匹配,Internet Explorer 會做一些額外的工作來確定用於 COM 對象的持久性接口的類型和相應的持久性數據。首先, Internet Explorer 將在 COM 對象中查詢 IPersistFile 接口。如果成功檢索到接口,它將調用 COM 對象接口的 Load() 方法,並傳入文件路徑。然後, COM 對象負責打開文件並解析數據。
  • 如果對象不支持 IPersistFile 接口,Internet Explorer 將使用數據值中的 URL,並創建一個 IStream 對象。接下來,它將在 COM 對象中查詢 IPersistStreamInit 接口。 如果此操作成功,則 Internet Explorer 將調用 IPersistStreamInit 接口的 load() 方法,並傳入 IStream 對象。如果 COM 對象不支持 IPersistStreamInit 接口,則它將嘗試在對象中查詢 IPersistStream 接口。如果對象實現此接口,則 Internet Explorer 將調用 IPersistStream 接口的 Load() 方法,並傳入 IStream 對象。如果這些操作成功,則 COM 對象的 IPersistStreamInit 或 IPersistStream 接口負責從給定的持久性數據中恢復屬性。
  • 如果 COM 對象未實現 IPersistStreamInit 或 IPersistStream ,或者 Load() 方法返回失敗,則 Internet Explorer 將嘗試通過從 OLE32 調用 StgOpenStorage 將 URL 作爲複合 OLE 文檔加載。如果 StgOpenStorage 返回成功值,則 Internet Explorer 將在 COM 對象中查詢 IPersistStorage 接口。如果 COM 對象確實實現了 IPersistStorage 接口,則 Internet Explorer 將調用接口的 Load() 方法,並傳入 IStorage 對象。從這裏開始, COM 對象也有責任解析 IStorage 對象中包含的數據。

2.1.3 攻擊面

  • COM的攻擊面可以分爲三個方面。 這些方面如下:
	(1) 瀏覽器中的對象公開的方法
	(2) COM對象的序列化
	(3) Web瀏覽器組件之間的編組值
  • 第一個攻擊面實際上是之前已被多次解決的表面。 實際上,有很多針對ActiveX控件的演講,以及爲自動模糊測試暴露的漏洞方法而開發的工具。 (對於感興趣的讀者,最近發佈了一篇關於來自 CERT 的Will Dormann 和 Dan Plakosh 編寫的關於 ActiveX 模糊測試的論文,並提供了一個模糊測試工具,可在 http://www.cert.org/archive/pdf/dranzer.pdf 上找到。 另一個流行的 ActiveX 模糊器 AxMan 由 HD Moore 發佈,可從 http://www.metasploit.com/users/hdm/tools/axman/. 獲得)。
  • COM 對象的序列化(也稱爲持久性)是另一個在安全問題上未被充分探索的領域。 我們將在第3節中廣泛地研究持久性的安全性含義,討論反序列化問題,由於持久對象而輸入混淆漏洞,以及可以過對象實例化破壞的信任邊界。
  • 最後,在第3節中,我們將在安全性的上下文中檢查編組代碼。 這是另一個在很大程度上尚未開發的主要攻擊面,很可能是由於它的隱含性質。 充斥着利用有時不直觀的 API 來以抽象方式跟蹤內存分配,對象使用和類型轉換,編組代碼可能非常難以編寫。 我們打算討論在執行某種程度的編組時經常出現的問題類型。 我們將考慮流行的 API 和接口,以及更廣泛的關於編組代碼中比其他任何地方更常見的問題類別的說法。

2.2 NPAPI 插件

  • Netscape 插件應用程序編程接口(NPAPI)是許多當代Web瀏覽器採用的首選插件架構,包括 Mozilla Firefox , Google Chrome , Apple Safari 和 Opera。該體系結構提供了一個簡單的模型,用於創建插件,通過定義的 API 調用向 Web 瀏覽器公開功能。儘管 NPAPI 在其原始版本中受到某種程度的限制,但隨着時間的推移,主要的修訂允許創建插件,這些插件不僅可以處理嵌入在網頁中的專用對象,還可以使用託管腳本語言(如 JavaScript)將它們暴露給腳本控件。這主要是由於2004年幾家公司(Mozilla,Apple,Macromedia,Opera和Sun)的共同努力,通過添加所謂的 NPRuntime 來擴展 NPAPI , NPRuntime 提供了一個跨平臺標準,用於將對象暴露給瀏覽器 DOM。本節旨在提供有關如何利用 NPAPI 的技術細節;特別關注 NPRuntime 組件,因爲該組件是最相關的功能,將在本文的以下部分中討論

2.2.1 NPAPI 插件註冊

  • 在深入研究 NPAPI 的細節之前,我們將簡要探討插件註冊到瀏覽器的過程。這些知識是能夠枚舉給定安裝的攻擊面所必需的。
  • 插件是最簡單級別的共享庫,它們在瀏覽器中註冊,旨在處理專用對象類型。註冊時,插件處理的對象以 MIME 類型,文件擴展名或兩者的組合形式指定。插件註冊並與 MIME 類型/擴展相關聯的方式因瀏覽器和平臺而異。本節考慮 Mozilla Firefox 的 Windows 安裝,但在其他環境中可以使用類似的過程。
  • 插件以兩種方式之一註冊到 Firefox 瀏覽器。
1.它們被複制到瀏覽器的 plugins 目錄中(通常是C:\Program Files\Mozilla Firefox\plugins)
2.將一個密鑰添加到註冊表以指示插件的位置和其他細節(在HKEY_LOCAL_MACHINE\Software\MozillaPlugins或HKEY_CURRENT_USER\Software\MozillaPlugins中)。插件所需的各個子項的結構記錄在https://developer.mozilla.org/en/Plugins/The_First_Install_Problem中)
  • 有關給定插件的關聯 MIME 類型和文件擴展名的信息位於已編譯 DLL 中的版本信息中。 MIME 類型在一系列管道分隔(’|’) MIME 標識符中指定,如下所示:
	MIMEType: mime/type-1|mime/type-2|mime/type-3
  • 同樣,文件擴展名也按管道分隔列表組織,如下所示:
	FileExtents: ext1|ext2|ext3
  • 能夠檢查給定 Firefox 安裝的可用插件的快速方法是簡單地瀏覽到關於: plugins 的 URL,其提供可用安裝插件的列表以及與每個插件相關聯的 MIME 類型和文件擴展名:
    在這裏插入圖片描述

2.2.2 NPAPI 和插件初始化

  • NPAPI 大致分爲兩組功能:瀏覽器端功能和插件端功能。瀏覽器端函數表示瀏覽器導出到插件的 API。此瀏覽器端 API 包含在名爲 NPNetscapeFuncs 的結構中,該結構在 NPAPI SDK 的 npupp.h 中定義(作爲 Gecko SDK 的一部分提供:https://developer.mozilla.org/En/Gecko_SDK ),並顯示在下面:
	typedef struct _NPNetscapeFuncs {
		uint16 size;
		uint16 version;
		NPN_GetURLUPP geturl;
		NPN_PostURLUPP posturl;
		NPN_RequestReadUPP requestread;
		NPN_NewStreamUPP newstream;
		NPN_WriteUPP write;
		NPN_DestroyStreamUPP destroystream;
		NPN_StatusUPP status;
		NPN_UserAgentUPP uagent;
		NPN_MemAllocUPP memalloc;
		NPN_MemFreeUPP memfree;
		NPN_MemFlushUPP memflush;
		NPN_ReloadPluginsUPP reloadplugins;
		NPN_GetJavaEnvUPP getJavaEnv;
		NPN_GetJavaPeerUPP getJavaPeer;
		NPN_GetURLNotifyUPP geturlnotify;
		NPN_PostURLNotifyUPP posturlnotify;
		NPN_GetValueUPP getvalue;
		NPN_SetValueUPP setvalue;
		NPN_InvalidateRectUPP invalidaterect;
		NPN_InvalidateRegionUPP invalidateregion;
		NPN_ForceRedrawUPP forceredraw;
		NPN_GetStringIdentifierUPP getstringidentifier;
		NPN_GetStringIdentifiersUPP getstringidentifiers;
		NPN_GetIntIdentifierUPP getintidentifier;
		NPN_IdentifierIsStringUPP identifierisstring;
		NPN_UTF8FromIdentifierUPP utf8fromidentifier;
		NPN_IntFromIdentifierUPP intfromidentifier;
		NPN_CreateObjectUPP createobject;
		NPN_RetainObjectUPP retainobject;
		NPN_ReleaseObjectUPP releaseobject;
		NPN_InvokeUPP invoke;
		NPN_InvokeDefaultUPP invokeDefault;
		NPN_EvaluateUPP evaluate;
		NPN_GetPropertyUPP getproperty;
		NPN_SetPropertyUPP setproperty;
		NPN_RemovePropertyUPP removeproperty;
		NPN_HasPropertyUPP hasproperty;
		NPN_HasMethodUPP hasmethod;
		NPN_ReleaseVariantValueUPP releasevariantvalue;
		NPN_SetExceptionUPP setexception;
		NPN_PushPopupsEnabledStateUPP pushpopupsenabledstate;
		NPN_PopPopupsEnabledStateUPP poppopupsenabledstate;
	} NPNetscapeFuncs;
  • 當插件最初加載到內存中時,通過調用插件需要導出的函數 NP_Initialize() 來初始化它。 NPNetscapeFuncs 結構作爲瀏覽器的第一個參數傳遞給此函數,從而將其 API 暴露給插件。讀者應該注意, size 和 version 元素中的信息允許對 API 進行擴展,這確實具有已投入使用。SDK 鼓勵使用前綴 NPN_* 作爲瀏覽器端功能(Netscape Plugin:Navigator),因此本文的其餘部分將引用使用該約定的回調。
  • 插件端函數是插件實現的函數,共同用於定義插件的功能。插件端函數包含在 NPPluginFuncs 結構中,該結構也在 npupp.h 中定義,並顯示:
	typedef struct _NPPluginFuncs {
		uint16 size;
		uint16 version;
		NPP_NewUPP newp;
		NPP_DestroyUPP destroy;
		NPP_SetWindowUPP setwindow;
		NPP_NewStreamUPP newstream;
		NPP_DestroyStreamUPP destroystream;
		NPP_StreamAsFileUPP asfile;
		NPP_WriteReadyUPP writeready;
		NPP_WriteUPP write;
		NPP_PrintUPP print;
		NPP_HandleEventUPP event;
		NPP_URLNotifyUPP urlnotify;
		JRIGlobalRef javaClass;
		NPP_GetValueUPP getvalue;
		NPP_SetValueUPP setvalue;
	} NPPluginFuncs;
  • 插件需要發佈 NP_GetEntryPoints() 函數,該函數使用 NPPluginFuncs 結構在插件初始化時將插件信息傳遞給瀏覽器。瀏覽器調用 NP_GetEntryPoints,將指針傳遞給可以保存 NPPluginFuncs 結構的內存位置。 反過來,NP_GetEntryPoints 使用插件的信息填充結構。按照慣例,插件函數名稱以 NPP_*(Netscape Plugin:Plugin)爲前綴,我們將在本文中嘗試遵循此約定。

2.2.3 NPAPI 插件初始化和銷燬

  • NPAPI 插件有兩個級別的初始化 - 我們已經看到的第一個是瀏覽器加載插件時執行的一次性初始化。如前所述,此加載是通過調用導出函數 NP_Initialize() 來實現的。還有實例初始化,每次插件實例化時都會發生。例如,如果在同一頁面上的兩個不同的 標籤中使用相同的插件,則將執行一次性加載初始化,然後進行兩次實例初始化。實例初始化由插件的 NPP_New() 函數執行,該函數定義如下:
	NPError NPP_New(
		NPMIMEType pluginType, 
		NPP instance, uint16 mode, 
		int16 argc, 
		char *argn[], 
		char *argv[], 
		NPSavedData *saved
	);
  • 這個函數有很多參數可以爲插件提供實例信息以幫助初始化過程。pluginType 參數表示與此插件實例關聯的 MIME 類型。許多插件註冊了幾種 MIME 類型,因此該參數允許每個實例區分它應該處理的 MIME 類型。第二個參數 instance 是一個指向插件對象實例的指針,該插件對象有一個 pdata 成員,插件可以用它來保存特定於當前插件實例的任何私有數據。插件通常用於在此處保存 C++ 對象。下一個參數是一個 mode 參數,它可以取值 NP_EMBED(1)來表示對象嵌入在網頁中,如果插件表示整頁對象,則取 NP_FULL(2)。接下來的三個參數與提供給對象的 值(或 標記中的屬性,如果使用它而不是 )相關。argc 參數表示 argn 和 argv 數組中提供的參數數量。兩個字符串數組 argn 和 argv 的元素計數等於 argc 參數,並分別指定參數名稱和值。最後,保存的參數可用於訪問由插件的先前實例使用 NPP_Destroy() 保存的數據,這是我們將暫時探索的函數。
  • 通過相對的函數 NPP_Destroy() 進行銷燬操作,其具有以下定義:
	NPError NPP_Destroy(NPP instance, NPSavedData **saved);
  • 該函數只需要一個實例指針和一個 NPSavedData **,它可以用來保存下一個插件實例的信息,如前所述。

2.2.4 流(Streams)

  • 流也與典型 NPAPI 插件的攻擊面非常相關。由 NPStream 數據結構表示的流對象表示從瀏覽器發送到插件的不透明數據流,反之亦然。插件實例可以處理多個流,但每個流特定於該插件實例; 他們無法分享。
  • 通過調用插件端函數 NPP_NewStream() 將新流從瀏覽器發送到插件,該函數具有以下原型:
	NPError NPP_NewStream(
		NPP instance, 
		NPMIMEType type, 
		NPStream *stream, 
		NPBool
		seekable, 
		uint16 *stype
	);
  • 大多數這些參數都是不言自明的,除了 stype 參數,插件填充爲以下值之一:
	NP_NORMAL(1- 流數據在拉出時傳送。 這是默認的操作模式。
	NP_ASFILEONLY(2- 首先將數據本地保存在臨時文件中
	NP_ASFILE(3- 數據與NP_NORMAL一樣正常傳送,但也保存到臨時文件
	NP_SEEK(4- 流數據可以隨機訪問而不是順序訪問。
  • 當文件與插件實例關聯時(例如 Adobe Flash 插件的 SWF 文件),或者當插件通過調用 NPN_GetURL() , NPN_GetURLNotify() , NPN_PostURL() 請求流時,流將傳遞到插件, 或 NPN_PostURLNotify() 函數。
  • 稍後通過調用 NPP_DestroyStream() 函數來銷燬流。 該函數具有以下原型:
	NPError NPP_DestroyStream(
		NPP instance, 
		NPStream *stream, 
		NPReason reason
	);
  • 處理流數據發生在 NPP_Write() 或 NPP_AsFile() 中,具體取決於相關流是分別 NP_NORMAL / NP_ASFILE 還是 NP_ASFILEONLY 流。使用流數據的機制超出了本文的範圍,將不再進一步討論。

2.2.5 NPRuntime 基礎知識

  • NPRuntime 是 NPAPI 的補充,它提供了一個統一的接口,允許插件將可編寫腳本的對象暴露給 DOM。在引入 NPRuntime 之前,還有其他方法允許將插件暴露給 Java 和腳本橋 - LiveConnect 和 XPCOM。這兩種技術雖然仍在某種程度上得到支持,但被認爲已被棄用,超出了本文的範圍
  • 希望提供腳本功能的插件通過使用 NPP_GetValue() 函數來實現。實質上,瀏覽器使用此功能來查詢插件以獲取許多衆所周知的屬性。 它有以下原型:
	NPError NPP_GetValue(
		NPP instance, 
		NPPVariable variable, 
		void *ret_value
	);
  • 變量參數指示要從插件檢索的信息類。諸如插件的名稱,描述或實例窗口的句柄之類的信息是可以檢索的可能屬性。引入 NPRuntime 組件時,會將一個變量添加到可查詢的可能變量的枚舉中 - 即 NPPVpluginScriptableNPObject,其數值爲 15。當查詢此值時,插件可以選擇返回指向封裝插件腳本功能的 NPObject 的指針。(稍後將更詳細地探討此對象)這是通過在 ret_value 參數中放置指向 NPObject 的指針來實現的。當 NPPVpluginScriptableNPObject 發生查詢時,ret_value 參數實際上被解釋爲 NPObject **。沒有任何腳本功能的插件只需在調用 NPP_GetValue() 時將返回錯誤,並將變量參數設置爲 NPPVpluginScriptableNPObject 。

2.2.5.1 可編寫腳本的對象

  • 如前所述,通過使用 NPObject 結構公開對象,這些結構在 npruntime.h 中定義如下:
	struct NPObject {
		NPClass *_class;
		uint32_t referenceCount;
		/*
		* 這裏可以通過NPObject的類型分配額外的空間
		* Additional space may be allocated here by types of NPObjects
		*/
	};
  • 可以從封裝的 NPClass 對象訪問實際功能,該對象也在 npruntime.h 中定義,如下所示:
	struct NPClass
	{
		uint32_t structVersion;
		NPAllocateFunctionPtr allocate;
		NPDeallocateFunctionPtr deallocate;
		NPInvalidateFunctionPtr invalidate;
		NPHasMethodFunctionPtr hasMethod;
		NPInvokeFunctionPtr invoke;
		NPInvokeDefaultFunctionPtr invokeDefault;
		NPHasPropertyFunctionPtr hasProperty;
		NPGetPropertyFunctionPtr getProperty;
		NPSetPropertyFunctionPtr setProperty;
		NPRemovePropertyFunctionPtr removeProperty;
		NPEnumerationFunctionPtr enumerate;
		NPConstructFunctionPtr construct;
	};
  • 這些函數中的每一個都實現了 JavaScript 對象操作的重要功能,該 API 的相關部分將在下面討論。
a) NPRuntime 之對象初始化和銷燬
  • 首先,我們將考慮初始化。 通常,通過定義具有所有相關函數的 NPClass 結構,然後調用瀏覽器函數來創建可編寫腳本的對象。NPN_CreateObject() 數具有以下原型:
	NPObject *NPN_CreateObject(
		NPP npp, 
		NPClass *aClass
	
  • 可以看出,NPN_CreateObject() 將實例指針作爲其第一個參數(稍後我們將探討),並將指向 NPClass 結構的指針作爲其第二個參數。 它只是在 NPClass 對象周圍創建一個 NPObject 包裝器並返回它。如果 NPClass 對象定義了 allocate() 回調,那麼將調用它來爲 PN_CreateObject() 函數返回的 NPObject 結構分配內存。此分配回調功能允許開發人員分配條件空間,以在包裝 PObject 的結構中保存有關該對象的任何特定於上下文的信息。標準技術是將對象表示爲 C++ 類,如下所示:
	// MyObject derives from NPObject –
	// It will be exposed as a scriptable object
	class MyObject : public NPObject
	{
	public:
		// Definition of the objects behaviors
		static NPClass myObjectClass =
		{
			NP_CLASS_STRUCT_VERSION,
			Allocate,
			Deallocate,
			Invalidate,
			HasMethod,
			Invoke,
			InvokeDefault,
			HasProperty,
			GetProperty,
			SetProperty,
		};
		// Call this function from NPP_GetValue() to retrieve the
		// scriptable object
		// It will create an NPObject wrapping the myObjectClass NPClass
		// It will also call Allocate() to allocate the NPObject
		static MyObject *Create(NPP npp)
		{
			MyObject *object;
			object = reinterpret_cast<MyObject *> (NPN_CreateObject(npp, &myObjectClass));
		}
		// The Allocate() function creates an instance of MyObject,
		// so we can initialize any private variables for MyObject etc..
		// Note that the Allocate() function needs to be static
		static NPObject *Allocate(NPP npp, NPClass *class)
		{
			return new MyObject(npp);
		}
		.. other methods ..
	};
  • 創建對象的另一個值得注意的細節是 NPObject 結構的引用計數成員將被初始化爲 1,並且每次將對象傳遞給瀏覽器端函數 NPN_RetainObject() 時,成員將遞增,其定義如下:
	NPObject *NPN_RetainObject(
		NPObject *obj
	;
  • 對於 Microsoft COM 對象,此函數可以被視爲 AddRef() 的模擬。
  • 當不再需要某個對象時,將調用瀏覽器端函數 NPN_ReleaseObject(),該函數用於 NPN_CreateObject() 的倒數運算。引用計數變量遞減,如果它達到0,則將取消分配對象。如果正在釋放的 NPObject 中指向的 NPClass 結構包含 deallocate() 回調,則將用於銷燬該對象。否則,默認系統分配器將釋放內存。
b) NPRuntime 之對象行爲
  • 對象最重要的特徵是它暴露的行爲。對象可以公開兩種不同類型的屬性:屬性和方法。已定義屬性是可以設置或檢索的對象的屬性。它在腳本中被操作,就像你期望任何其他 DOM 對象的屬性一樣:
	Plugin.property = setVal; 	// set the property 設置屬性
	retVal = Plugin.property; 	// retrieve the property 檢索屬性
	delete Plugin.property; 	// remove the property 刪除屬性
  • 在內部,在腳本中執行任何這些操作將導致從定義對象的 NPClass 對象調用四個定義的屬性相關函數中的兩個:
	bool HasProperty(NPObject *obj, NPIdentifer name)
	bool GetProperty(NPObject *obj, NPIdentifier name, NPVariant *result)
	bool SetProperty(NPObject *obj, NPIdentifier name, NPVariant *value)
	bool RemoveProperty(NPObject *obj, NPIdentifier name)
  • 無論是設置還是檢索屬性,瀏覽器採取的第一個操作是檢查屬性是否受支持,這是通過使用將被操作的屬性的名稱調用 HasProperty() 方法來完成的。NPIdentifier 數據類型用於解析屬性或方法,幷包含名稱的哈希值而不是名稱的值。如果不支持請求的名稱,則會向腳本運行時返回錯誤。假設 HasProperty() 成功,則調用 GetProperty() 或 SetProperty(),具體取決於是檢索還是設置。在檢索的情況下,指向屬性值的指針放在 GetProperty() 的 result 參數中,該參數將被腳本運行時解釋爲返回值(前一個腳本示例中的 retVal)。相反,在設置屬性時,value 參數將被解釋爲屬性設置爲的值(上一個腳本示例中的 setVal)。最後,可以使用上面提到的刪除語法刪除屬性。實際上,很少實現此功能。請注意,作爲所有這些函數的第一個參數傳遞的 obj 參數是指向對象本身的指針。設置和獲取屬性的過程如圖19所示:
    在這裏插入圖片描述
  • 使用 NPClass 結構中定義的三個方法,以與屬性操作類似的方式實現方法調用:
	bool HasMethod(
		NPObject *obj, 
		NPIdentifier name
	)
	bool Invoke(
		NPObject *obj, 
		NPIdentifier name, 
		const NPVariant *args, 
		uint32_t argCount, 
		NPVariant *result
	)
	bool InvokeDefault(
		NPObject *obj, 
		const NPVariant *args, 
		uint32_t argCount,
		NPVariant *result
	)
  • 與屬性一樣,當調用方法時,瀏覽器首先調用 HasMethod() 以查看插件是否已定義給定方法。假設此調用成功,則調用 Invoke() 函數。 Invoke() 函數獲取在 name 參數中調用的方法名稱,後跟一個參數數組,後跟一個指示參數數組大小的計數,最後一個指向將包含結果的變量的指針 調用。插件對象是使用 InvokeDefault() 函數執行就好像它是一個方法,就像在下面的 JavaScript 代碼片段中一樣:
	var pluginobj = document.getElementById(“plugin”);
	var result = pluginobj(args);
c) NPRuntime 之參數傳遞
  • 正如我們在上一節中看到的,對象可以根據腳本主機可用的屬性和方法來定義行爲。在這兩種情況下,參數都作爲 NPVariants 傳入和傳出 NPAPI 入口點。NPVariants 基本上是一個不透明的數據結構,用於表示可以從腳本引擎(如 JavaScript)輕鬆導入或導出的不同變量。NPVariant 結構定義如下:
	typedef struct _NPVariant {
		NPVariantType type;
		union {
			bool boolValue;
			int32_t intValue;
			double doubleValue;
			NPString stringValue;
			NPObject *objectValue;
		} value;
	} NPVariant;
  • 可以看出,該結構是一種非常簡單的類型/聯合結構,非常類似於 Microsoft Windows 平臺上普遍存在的 VARIANT 數據結構。這裏的所有數據類型都是基本類型或 NPObject(之前討論過),但有一個例外 - NPString 值,定義如下:
	typedef char NPUTF8;
	typedef struct _NPString {
		const NPUTF8 *utf8characters;
		uint32_t utf8length;
	} NPString;
  • 聯合中包含的值由 NPVariant 的類型定義,該類型定義爲以下之一:
	typedef enum {
		PVariantType_Void,
		NPVariantType_Null,
		NPVariantType_Bool,
		NPVariantType_Int32,
		NPVariantType_Double,
		NPVariantType_String,
		NPVariantType_Object
	} NPVariantType;
  • NPAPI 提供了許多用於操作 NPVariant 數據結構的標準化宏。這些宏在 npruntime.h 中定義,分爲三類。第一個類別用於測試 NPVariant 的類型,其格式爲:NPVARIANT_IS_XXX(),其中 XXX 是要檢查的對象類型:
	#define NPVARIANT_IS_VOID(_v) ((_v).type == NPVariantType_Void)
	#define NPVARIANT_IS_NULL(_v) ((_v).type == NPVariantType_Null)
	#define NPVARIANT_IS_BOOLEAN(_v) ((_v).type == NPVariantType_Bool)
	#define NPVARIANT_IS_INT32(_v) ((_v).type == NPVariantType_Int32)
	#define NPVARIANT_IS_DOUBLE(_v) ((_v).type == NPVariantType_Double)
	#define NPVARIANT_IS_STRING(_v) ((_v).type == NPVariantType_String)
	#define NPVARIANT_IS_OBJECT(_v) ((_v).type == NPVariantType_Object)
  • 例如,可以使用 NPVARIANT_IS_STRING() 宏來測試特定 variant 是否爲字符串。第二類是從 NPVariant 中提取值,宏名稱的形式爲 NPVARIANT_TO_XXX():
	#define NPVARIANT_TO_BOOLEAN(_v) ((_v).value.boolValue)
	#define NPVARIANT_TO_INT32(_v) ((_v).value.intValue)
	#define NPVARIANT_TO_DOUBLE(_v) ((_v).value.doubleValue)
	#define NPVARIANT_TO_STRING(_v) ((_v).value.stringValue)
	#define NPVARIANT_TO_OBJECT(_v) ((_v).value.objectValue)
  • 最後,有一些宏用於將數據存儲到 NPVariant 變量中。 這些宏的格式 XXX_TO_NPVARIANT()。最後一個類別主要用於爲 GetProperty() ,Invoke() 和 InvokeDefault() 函數填充結果 NPVariant。
d) NPRuntime 之編組和類型解析
  • 那麼,腳本主機如何將數據傳入插件和從插件傳遞數據? 答案是,需要編組層來解釋腳本主機中的對象,並將它們轉換爲插件可以理解的類型,反之亦然。顯然,該層是依賴於實現的,並且因瀏覽器而異。本節將簡要概述 Mozilla Firefox 編組層,以便於 JavaScript 和可編寫腳本的對象之間的通信
  • NPRuntime 插件所需的轉換實際上非常簡單,因爲在大多數情況下,NPObject 類型精確映射到本機支持的 JavaScript。編組操作都包含在 Firefox 源代碼樹的單個文件中: mozilla/modules/plugin/base/src/nsJSNPRuntime.cpp。爲了實現將 JavaScript 變量轉換爲 NPVariants 的兩個主要目標,反之亦然,採用對象代理方法,這將在下面討論。
  • 在設置屬性或調用方法時,傳遞給插件的 JavaScript 對象需要轉換爲 NPVariants 。對於基本類型,此轉換是將 JavaScript 中的文字值移植到 NPVariant 結構中的簡單過程:
	if (JSVAL_IS_PRIMITIVE(val)) {
		if (val == JSVAL_VOID) {
			VOID_TO_NPVARIANT(*variant);
		} else if (JSVAL_IS_NULL(val)) {
			NULL_TO_NPVARIANT(*variant);
		} else if (JSVAL_IS_BOOLEAN(val)) {
			BOOLEAN_TO_NPVARIANT(JSVAL_TO_BOOLEAN(val), *variant);
		} else if (JSVAL_IS_INT(val)) {
			INT32_TO_NPVARIANT(JSVAL_TO_INT(val), *variant);
		} else if (JSVAL_IS_DOUBLE(val)) {
			OUBLE_TO_NPVARIANT(*JSVAL_TO_DOUBLE(val), *variant);
  • 處理此轉換的代碼位於 JSValToNPVariant() 中。 在字符串的情況下,需要做一些額外的工作來處理 UTF-8 轉換:
	} else if (JSVAL_IS_STRING(val)) {
		JSString *jsstr = JSVAL_TO_STRING(val);
		nsDependentString str((PRUnichar*)::JS_GetStringChars(jsstr),
									::JS_GetStringLength(jsstr));
		PRUint32 len;
		char *p = ToNewUTF8String(str, &len);
		if (!p) {
			return false;
	}
 STRINGN_TO_NPVARIANT(p, len, *variant);
  • 最後,還有 JavaScript 對象。 當這些作爲參數傳遞時,會創建一個包裝 JavaScript 對象的 NPObject 結構。包裝器對象的功能在 NPClass 結構 sJSObjWrapperClass 中定義,該結構包含將請求代理到 JavaScript 引擎的方法。 例如,如果在包裝器對象上調用 NPP_GetProperty(),它將檢索被包裝的 JavaScript 對象的實例,並允許 JavaScript 引擎在內部處理特定內容。該過程如圖20所示。
    在這裏插入圖片描述
  • 類似地,當對象從 NPVariants 轉換回 JavaScript 時,NPVariantToJSVal() 函數會將立即值複製回 JavaScript 對象,或者創建一個代理調用 NPObject 公開的功能的 JavaScript 對象。此代理類使用 sNPObjectJSWrapperClass 結構實現。

2.2.6 攻擊面

  • 在 NPAPI 的背景下,攻擊面可以分爲大致三個關鍵區域,即:
	(1) 標準插件入口點
	(2) 入口指向暴露的可編寫腳本的對象,以及
	(3) 在瀏覽器本身內編組圖層
  • 標準插件入口點可以概括爲 “ Netscape 插件” 結構(即 NPP_ * 函數)公開的入口點。我們已經檢查了標準入口點的相對較大的攻擊面,特別是通過 NPP_New() 向插件實例提供參數,或者在 URL 流中檢索的數據(當收到 NP_NORMAL 流時,這些數據主要由 NPP_Write() 函數處理,以及收到 NP_ASFILEONLY 流時的 NPP_StreamAsFile())。雖然這是一個有點大的攻擊面,但它也是大多數安全研究在此之前所關注的明確的攻擊面。因此,本文不會涉及這些切入點,除非它們爲將要討論的互操作性攻擊提供一些上下文。
  • 可腳本化對象暴露的入口點可能是過去幾乎沒有處理過的最廣泛的攻擊面。實現對象的腳本交互的函數將是最明顯的攻擊向量,例如給定 NPObject 的 Invoke(),InvokeDefault() 、GetProperty() 和 SetProperty()。我們還必須在此攻擊面中考慮互操作性的不太明顯的入口點 - 主要是插件使用 NPN_GetProperty() 函數訪問 DOM 層次結構的位置。
  • 最後,每個實現 NPAPI 運行時的瀏覽器都必須提供一些編組層,用於將對象從腳本語言運行時轉換爲 NPVariants,反之亦然。此綁定層還爲攻擊者提供了大量機會,以便發現漏洞。

第三節:對互操作性的攻擊

  • 旨在實現互操作性的架構很難實現。開發代碼時必須避免一系列新問題,因此,這種情況會給攻擊者帶來破壞系統安全性的新機會。以下小節將列舉在通過互操作性層管理數據時特別出現的漏洞類。
    正在討論的課程是:
	1. Object Retention vulnerabilities.	對象保留漏洞
	2. Type Confusion vulnerabilities.		鍵入Confusion漏洞
	3. Transitive Trust vulnerabilities. 	傳遞信任漏洞
  • 在類型混淆和傳遞信任類中提供的研究顯着擴展了這些術語的任何先前使用 - 在一定程度上保證將它們視爲新的漏洞類。當然,在數據編組的上下文中也存在標準類型的漏洞,例如整數寬度問題和緩衝區溢出,但本文不會討論這些漏洞,因爲它們已被充分理解,並且已有大量文獻充分闡明瞭這些漏洞話題。

3.1 互操作性攻擊 I:對象保留漏洞

  • 在內聚模塊之間傳遞的數據可以是簡單的文字值(例如 ntegers 或 Booleans),也可以是複雜的數據結構(例如 COM 對象)。對於後一種情況,運行時必須有一個方法來管理對象的生命週期。管理對象生命週期的一般策略是使用引用計數原語。這樣的策略將對象的消費者的責任放在他們需要時以及完成它時的信號。接下來,如果消費者無法正確報告對象使用情況,則存在內存管理漏洞的潛在機會。一般來說,對象生命週期的錯誤管理髮生在兩種情況下:
	1.在需要時不保留對對象的引用,因此存在過早釋放內存的風險
	2.在需要時不釋放對象的引用,導致內存泄漏以及潛在的可利用場景(稍後討論)
  • 本節將描述這兩種代碼結構通常如何在兩種不同的 realworld 插件體系結構中顯示。讀者應該注意,本節中易受攻擊的代碼構造很可能在插件對象和編組層本身中找到,因爲這些接口通常需要保留對對象的引用並在強制過程中執行轉換時生成新對象。

3.1.1 Microsoft 對象保留漏洞

  • Microsoft 插件體系結構廣泛使用 COM 對象和 VARIANT 來定義和傳遞瀏覽器中各個組件之間的對象。實際上,JavaScript 對象在語言運行時本身表示爲 COM 對象,而 VBScript 對象表示爲 VARIANT。通過調用對象的 IDispatch::Invoke() 方法來訪問 ActiveX 對象公開的方法或屬性,該方法將參數作爲 VARIANT 數組接收到目標函數。(請注意,對於 ActiveX 控件,屬性實際上是作爲一對方法調用公開的,其名稱格式爲 get_XXX() 和 put_XXX(),其中 XXX 是屬性的名稱。這兩個函數分別檢索和設置屬性) VARIANT 中包含的對象實際上可以是任何類型和值,但最常見的是它們是基本類型(如整數或字符串)或表示複雜對象的 COM 接口。由於 JavaScript 在內部將對象表示爲 IDispatch(或更準確地說, IDispatchEx)COM 接口,因此 VT_DISPATCH VARIANT 將是在瀏覽器上下文中傳遞給典型控件的最常見的基於 COM 的 VARIANT 。
  • COM 對象保持內部引用計數,並由 IUnknown::AddRef() 和 IUnknown::Release() 方法在外部進行操作,這些方法分別遞增和遞減引用計數。一旦引用計數達到 0,對象將從內存中刪除自己。 ActiveX 控件中的對象保留錯誤是對象引用計數錯誤管理的結果。本節描述開發人員在處理生命週期大於單個函數調用範圍的對象時所犯的典型錯誤。

3.1.1.2 ActiveX 對象保留攻擊 I:沒有保留

  • 控件在對象保留方面可能犯的最明顯的錯誤是忽略添加它想要保留的 COM 對象的引用計數。當 ActiveX 函數將 COM 對象作爲參數時,編組層已經在接收到的對象上調用了 IUnknown::AddRef() ,以確保它不會被競爭線程刪除。 但是, marshaller 還會在插件函數返回後釋放該口。因此,希望保留超出方法範圍的 COM 對象實例的插件對象必須在方法返回之前調用 IUnknown::AddRef() 函數。 調用 IUnknown::QueryInterface() 也足夠了,因爲這個函數也會(或者至少應該)爲對象調用 IUnknown::AddRef()。未能調用這些函數中的任何一個都可能導致潛在的過時指針漏洞。下面的代碼顯示了這樣一個問題的一個例子:
	HRESULT CMyObject::put_MyProperty(IDispatch *pCallback)
	{
		m_pCallback = pCallback;
		return S_OK;
	}
	HRESULT CMyObject::get_MyProperty(IDispatch **out)
	{
		if(out == NULL || *out == NULL || m_pCallback == NULL)
			return E_INVALIDARG;
		*out = m_pCallback;
		return S_OK;
	}
  • 此代碼中的 put_MyProperty() 函數存儲一個 IDispatch 指針,稍後可以使用 get_MyProperty() 函數由客戶端應用程序檢索該指針。 但是,由於從不使用 AddRef(),因此無法保證在客戶端回讀屬性時 pCallback 函數仍然存在。如果刪除了對該對象的每個其他引用,則將取消分配該對象,使 m_pCallback 指向過時的內存。
a) VARIANT 淺拷貝
  • 當複製 VARIANT 對象時,通常使用 VariantCopy() 完成,但在許多情況下也只使用簡單的 memcpy()。 VariantCopy() 是首選方法,因爲它將執行對象感知複製 - 如果要複製的 VARIANT 是字符串,它將複製內存。如果要複製的對象是對象,則會添加引用計數。 相比之下,memcpy() 顯然執行淺拷貝 - 如果 VARIANT 包含任何類型的複雜對象,例如 IDispatch,則將複製和使用指向該對象的指針,而不添加對該對象的附加引用。如果保留此重複 VARIANT 的結果,則如果釋放該對象的每個其他實例,則可以刪除指向的對象。以下代碼演示了此易受攻擊的構造:
HRESULT CMyObject::put_MyProperty(VARIANT src)
{
	HRESULT hr;
	memcpy((void *)&m_MyProperty, (void *)&src, sizeof(VARIANT));
	return S_OK;
}
HRESULT CMyObject::get_MyProperty(VARIANT *out)
{
	HRESULT hr;
	if(out == NULL)
		return E_FAIL;
	VariantInit(out);
	memcpy(out, (void *)&m_MyProperty, sizeof(VARIANT));
	return S_OK;
}
  • 攻擊還有一個更微妙的變化 - 這次使用 VariantCopy()。在某些方面,VariantCopy() 也可以被認爲是淺拷貝操作,因爲任何具有 VT_BYREF 修飾符的 VARIANT 都不會被深度複製; 只是指針將被複制。請考慮以下代碼:
	HRESULT CMyObject::put_MyProperty(VARIANT src)
	{
		HRESULT hr;
		VariantInit(&m_MyProperty);
		hr = VariantCopy(&m_MyProperty, &src);
		if(FAILED(hr))
			return hr;
		return S_OK;
	}
	HRESULT CMyObject::get_MyProperty(VARIANT *out)
	{
		HRESULT hr;
		if(out == NULL)
		return E_FAIL;
		VariantInit(out);
		hr = VariantCopy(out, &m_MyProperty);
		if(FAILED(hr))
			return hr;
		return S_OK;
	}
  • 此示例顯示了一個示例 ActiveX 屬性,它只接受 VARIANT 並將其存儲,並可選擇將其返回給用戶。此代碼的問題是使用 VariantCopy() 而不是 VariantCopyInd()。如果提供具有類型(VT_BYREF | VT_DISPATCH)的 VARIANT,則執行簡單的指針複製。如果隨後刪除了指向的 VT_DISPATCH 對象,則會留下指向不再存在的 IDispatch 對象的 VARIANT。如果隨後嘗試獲取此屬性,則用戶將檢索帶有過時指針的 VARIANT,從而導致內存損壞。
b) The ActiveX Marshaller
  • 爲了瞭解對象作爲參數傳遞給 ActiveX 控件時發生的事件的確切語義,您需要特別注意目標函數所期望的類型。當 ActiveX 函數期望 VARIANT 作爲參數時,編組代碼不會執行任何類型的深層複製 - 它既不使用 VariantCopy() 也不使用 VariantCopyInd()。因此,如果接收 VARIANT 包含超出方法範圍操作的 COM 接口,則接收 VARIANT 會特別危險。此外,如果 ActiveX 函數允許將 COM 對象的間接指針作爲參數 - 即(VT_BYREF | VT_DISPATCH)或等效參數,則被引用的對象將使其引用計數由 marshaller 遞增(並在函數返回時釋放)。因此,如果將 VARIANT 值傳遞給類型爲(VT_BYREF | VT_DISPATCH)的 ActiveX 控件,則如果函數採用 VARIANT,則不會增加其引用計數,但如果函數採用 IDispatch **,則其引用計數會增加(甚至是 IDispatch *)。該算法有點違反直覺,這增加了錯誤發生的可能性。

3.1.1.2 ActiveX 對象保留攻擊 II:釋放失敗

  • 未能釋放對象基本上構成內存泄漏。當通過 IUnknown::AddRef() 或 IUnknown::QueryInterface() 引用 COM 接口時會發生這些故障,並且稍後在不調用相應的 IUnknown::Release() 函數的情況下將其丟棄。觸發以這種方式操作的代碼路徑可以允許攻擊者消耗任意數量的內存,但更有用的是無限次地增加對象的引用計數。在32位機器上,通過執行易受攻擊的代碼路徑 0xFFFFFFFF 次數,可以在對象的觸發器中觸發整數溢出引用計數。在此之後,對 IUnknown::Release() 的任何調用都將導致對象被釋放,這又會導致過時的指針問題。下代碼基於我們之前使用的示例; 但是,它已被修改以證明無法釋放對象的問題:
	HRESULT CMyObject::put_MyProperty(IDispatch *pCallback)
	{
		if(pCallback == NULL)
			return E_INVALIDARG;
		pCallback->AddRef();
		m_pCallback = pCallback;
		return S_OK;
	}
	HRESULT CMyObject::get_MyProperty(IDispatch **out)
	{
		if(out == NULL || *out == NULL || m_pCallback == NULL)
			return E_INVALIDARG;
		*out = m_pCallback;
		return S_OK;
	}
  • 此示例在設置時正確添加對新回調對象的引用。但是,m_pCallback 中保存的先前值(如果存在)將被覆蓋而不會被釋放。因此,攻擊者可以多次設置此屬性,並最終在引用計數變量中觸發整數溢出。讓我們嘗試在以下示例中修復它:
	HRESULT CMyObject::put_MyProperty(IDispatch *pCallback)
	{
		if(pCallback == NULL)
			return E_INVALIDARG;
		pCallback->AddRef();
		if(m_pCallback != NULL)
			m_pCallback->Release();
		m_pCallback = pCallback;
		return S_OK;
	}
	HRESULT CMyObject::get_MyProperty(IDispatch **out)
	{
		if(out == NULL || *out == NULL || m_pCallback == NULL)
			return E_INVALIDARG;
		*out = m_pCallback;
		return S_OK;
	}
  • 上面的示例添加了一個 Release() 調用來正確釋放以前保存的任何對象,因此不會發生內存泄漏。精明的讀者會注意到這段代碼實際上仍然存在陳舊的指針問題。get_MyProperty() 函數不會向正在分發回腳本引擎的接口添加引用。如果插件只保留對該接口的引用,並且插件將其釋放,則可能會出現問題。請考慮以下 JavaScript 代碼段:
	axObject.MyProperty = new Object();
	var x = axObject.MyProperty();
	axObject.MyProperty = new Object();
  • 此JavaScript代碼會導致執行以下操作:
	1. put_MyProperty保留對我們創建的對象的唯一引用。
	2. 'x'變量接收IDispatch指針,但它只有一個副本
	3. 設置MyProperty會導致舊對象被刪除,即使'x'仍然指向它!

3.1.2 Mozilla 對象保留漏洞

  • 與 COM 體系結構相比,NPAPI 具有更簡單的對象編組模型。如本文的技術概述部分所述, JavaScript 對象不能直接傳遞給插件,而是以 NPAPI(NPObject)理解的對象格式包裝。回想一下, NPObject 結構有一個引用計數,它由 NPN_RetainObject() 和 NPN_ReleaseObject() 操縱。基於 NPAPI 的瀏覽器中的對象保留漏洞源於對這兩個函數的誤用,如下所述:

3.1.2.1 NPAPI 對象保留攻擊 I:沒有保留

  • 與 ActiveX 控件一樣,NPAPI 模塊需要維護對作爲輸入參數接收的對象的引用,只要這些對象將被存儲很長一段時間。如技術概述中所述,NPObjects 由編組層創建以包裝 JavaScript 對象。如果過去某個特定的 JavaScript 對象已被 NPObject 包裝,那麼將重用相同的 NPObject。 此外,插件可以使用 NPN_CreateObject() 創建 NPObject,然後可以在某個時刻將其傳遞給用戶。在任何一種情況下,如果插件需要維護指向對象的指針,則需要調用 NPN_RetainObject(),將指向相關 NPObject 的指針作爲參數傳遞。如果不這樣做,可能會導致插件中存在潛在的過時指針漏洞。 以下代碼是使用 NPAPI API 的對象保留漏洞的示例:
	bool SetProperty(NPObject *obj, NPIdentifier name, const NPVariant *variant)
	{
		if(name == kTestIdent)
		{
			if(!NPVARIANT_IS_OBJECT(*variant))
				return false;
			
			gTestObject = NPVARIANT_TO_OBJECT(*variant);
			return true;
		}
		return false;
	}
	bool GetProperty(NPObject *obj, NPIdentifier name, NPVariant *result)
	{
		VOID_TO_NPVARIANT(*result)
		if(name == kTestIdent)
		{
			if(!NPVARIANT_IS_OBJECT(*result))
				return false;
			if(gTestObject == NULL)
				NULL_TO_NPVARIANT(*result);
			else
				OBJECT_TO_NPVARIANT(*result, gTestObject);
			return true;
		}
		return false;
	}
  • 可以看出,SetProperty() 方法保留了指向對象的指針,但無法調用 NPN_RetainObject()。
    惡意用戶可以通過執行以下步驟來利用此問題:
	1. Create an object of some kind	創建某種對象
	2. Set the vulnerable property using that object	使用該對象設置易受攻擊的屬性
	3. Delete the object	刪除對象
	4. Get the vulnerable property	再次獲取屬性

3.1.2.2 NPAPI 對象保留攻擊 II:發佈失敗

  • 與 ActiveX 控件一樣,NPAPI 中也可能出現發佈失敗問題。當使用 NPN_RetainObject() 保留對象但使用 NPN_ReleaseObject() 從未釋放時,會發生這種情況。同樣,通過多次觸發此代碼路徑,將有可能溢出引用計數器,從而可能導致過時的指針問題。以下代碼是前一個示例的略微修改版本,用於演示此問題:
	bool SetProperty(NPObject *obj, NPIdentifier name, const NPVariant *variant)
	{
		if(name == kTestIdent)
		{
			if(!NPVARIANT_IS_OBJECT(*variant))
				return false;
			gTestObject = NPN_RetainObject(NPVARIANT_TO_OBJECT(*variant));
			return true;
		}
		return false;
	}
  • 在上面的代碼中,正在從用戶檢索的對象上正確調用 NPN_RetainObject()。但是,請注意,永遠不會檢查 gTestObject 以查看它是否先前已設置過。 之前存儲在此處的任何 NPObject 都不會被釋放,因此代碼包含引用計數泄漏。攻擊者可以使用以下步驟利用此機會:
	1. Create an NPObject, either by wrapping one particular JavaScript object or by using another
	   NPObject created by the plugin
	   通過包裝一個特定的JavaScript對象或創建一個NPObject 或通過使用插件創建的另一個 NPObject

	2. Create a second reference to the same object by assigning it to more than one variable in
	   JavaScript (let’s call them objX and objY).
	   通過將其分配給JavaScript中的多個變量(讓我們稱之爲objX和objY),創建對同一對象的第二個引用

	3. Call SetProperty() 0xFFFFFFFF times to take the reference count of the NPObject from 2 to 1
	   (due to the integer overflow)
	   調用SetProperty() 0xFFFFFFFF次,將NPObject的引用計數從2增加到1 (由於整數溢出)

	4. Delete one of the variables, say, objX. This will take the reference count to 0 and destroy the
	   NPObject.
	   刪除其中一個變量,比如objX。 這將引用計數爲0並銷燬NPObject

	5. objY will now contain a stale NPObject reference
	   objY現在將包含一個陳舊的NPObject引用
  • 有關引用計數的具體示例是瀏覽器和平臺特定的。但是,這些類型的問題是互操作性複雜性的症狀。通常,允許通過引用傳遞值並允許維護這些引用的互操作性體系結構將經常遇到此問題。因此,對象保留是尋求在提供互操作性的應用程序中查找漏洞的攻擊者的肥沃目標。

3.2 互操作性攻擊 II:類型混淆漏洞

  • 顧名思義,類型混淆漏洞是當一種數據類型被誤認爲另一種數據類型時發生的漏洞。它們通常是聯合數據類型管理不善的結果,但也可能源於類型通配符,並導致攻擊者能夠從目標應用程序讀取敏感數據(即信息泄漏),或者實現意外執行。類型混淆漏洞出現在負責解碼以語言無關格式表示的任意類型的複雜對象的軟件組件中的可能性更高。這種可能性更高的原因在於,當代碼的預期效果是在人爲和基本類型之間進行轉換時,編譯器的錯誤檢查會變得無能爲力。漏洞類普遍存在的一些情況包括:
	(1) 從持久存儲(例如文件)反序列化對象
	(2) 從聯網應用程序(例如ASN.1編碼對象)反序列化對象
	(3) 語言綁定層負責在兩種語言之間編組數據,這兩種語言的本機表示不同
  • 本節介紹類型混淆漏洞,它們是如何發生的,以及它們對應用程序安全性的影響。還將討論用於查找此類漏洞的審計,使用一些流行的 API 作爲案例研究,以及作者發現的漏洞的真實世界示例。

3.2.1 基礎知識:輸入通配符

  • 從根本上說,類型混淆漏洞是由一段代碼產生的,該代碼在存儲區域類型的錯誤假設下對存儲區域執行操作。例如,以下代碼:
	int ReadFromConnection(int sock)
	{
		unsigned char *Data;
		int total_size;
		int msg_size;
		 
		total_size = 1024;
		Data = (unsigned char *)malloc(total_size);
		
		msg_size = recv(sock, &Data, total_size, 0);
		return(1);
	}
  • recv 函數希望能夠將 total_size 字節寫入 Data 指定的內存區域。但是,在此示例中,代碼錯誤地引用了參數的類型 - 它將指針傳遞給指向可以容納 total_size 字節的內存區域的指針。在32位機器上,存儲區只能容納4個字節的數據,導致堆棧溢出。編譯器將允許發生此錯誤,因爲 recv 函數指定參數2應爲 void * 類型,該類型指定函數將接受指向任何類型的內存的指針,包括指向指針的指針。
  • 其中一位作者(Ryan Smith)在微軟內部版本的 ATL 中發現了這種類型的漏洞。從持久流中讀取類(VT_ARRAY | VT_UI1)的 VARIANT 時會觸發有問題的代碼。 以下代碼粗略表示易受攻擊的功能:
	inline HRESULT CComVariant::ReadFromStream(IStream *pStream)
	{
		…
		hr = pStream->Read(&vtRead, sizeof(VARTYPE), NULL);switch(vtRead)
		{
			case VT_ARRAY|VT_UI1:
				SAFEARRAYBOUND rgsaInBounds;
				SAFEARRAYBOUND rgsaBounds;
				SAFEARRAY *saBytes;
				void *pvData;
				
				hr=pStream->Read(&saInBounds, sizeof(saInBounds), NULL);
				if(hr<0||hr==1)
					return(hr);
					
				rgsaBounds.cElements = rgsaInBounds.cElements;
				rgsaBounds.lLbound = 0;
				saBytes = SafeArrayCreate(VT_UI1, 1, rgsaBounds);
				if(saBytes == NULL)
					return(E_OUTOFMEMORY);
					
				hr = SafeArrayAccessData(saBytes, &pvData);
				if(hr < ERROR_SUCCESS)
					return(hr);
					
				hr=pStream->Read(&pvData, rgsaInBounds.cElements, NULL);
				...
		}
	}
  • 上面的代碼從 IStream 讀取數據,錯誤地將指針傳遞給指向目標緩衝區的指針,而不是指向目標緩衝區的指針(也就是說,它傳遞 &pvData 作爲緩衝區參數而不是 pvData)。在 32 位系統上,如果讀取的數據量大於 4 個字節,則會發生堆棧損壞。該過程在圖21中可視地描繪。鑑於此代碼已經存在了很長時間並且已經分佈在大量 COM 組件中,很明顯類型混淆錯誤(例如前面的示例)很少受到關注,並且非常微妙。
    在這裏插入圖片描述
  • 作者評論:在編寫此漏洞的示例代碼時,作者不小心將錯誤的值寫入 pStream-> Read() 的參數 - 另一種類型的混淆錯誤! 當在同行評審中發現時,另一位作者糾正了它,提出了不同但同樣錯誤的價值觀! 我想這段代碼從來都不是安全的。

3.2.2 基礎知識:構造 Union

  • 正如引言中所提到的,大多數類型的混淆漏洞主要是由於濫用聯合數據類型而引起的。 在 C 和 C++ 中,union 數據類型類似於 struct 數據類型 - 它由許多不同名稱和類型的成員組成,每個成員都可以單獨引用。但是,與 struct 類型不同,union 成員都佔用內存中的相同位置,從而使它們的用法互斥。因此,這些類型的存在錯誤地引用無效的並集的成員的可能性,例如在以下示例中:
	struct VariantType
	{
		union {
			char *string_member;
			int int_member;
		};
	};
	
	int Respond(int clientSock, struct VariantType *pVar);
	int HandleNetworkMsg(int clientSock);
	
	int Respond(int clientSock, struct VariantType *pVar)
	{
		int len;
		int sentLen;
		
		if(pVar == NULL)
			return(0);
		len = strlen(pVar->string_member); 
		sentLen = send(clientSock, pVar->string_member, len+1, 0);
		if(sentLen != len+1)
			return(0);
		return(1);
	}
	
	int HandleNetworkMsg(int clientSock)
	{
		struct VariantType myData;
		char inBuf[1024];
		int msgSize;
		int respCode;
		
		memset(inBuf, 0x00, sizeof(inBuf));
		msgSize = recv(clientSock, inBuf, sizeof(inBuf), 0);
		if(msgSize < sizeof(int))
			return(0);
		memcpy(&myData.int_member, inBuf, sizeof(int)); // *
		respCode = Respond(clientSock, &myData);
		return(respCode);
	}

  • 從這裏可以看出,整數存儲在並集 - 即 int_member 中。 隨後,訪問 string_member 變量,其類型爲 char * 。 顯然,將整數視爲字符串是無效的。 此代碼構造將導致存儲在 int_member 中的整數被錯誤地解釋爲 char *,從而導致應用程序作用於攻擊者選擇的內存的任意部分,就像它是一個字符串一樣。編譯器允許此代碼在沒有警告的情況下進行編譯,因爲聯合類型旨在便於使用不同的基本數據類型訪問內存部分,併爲程序員提供動力以跟蹤哪個聯合成員適合在任何給定時訪問點。
  • 當然,在上面的例子中看到的代碼構造在實際的代碼中很少發生。但是,當 Union 被指定爲值時,Union 的使用者如何知道聯合中包含哪些數據類型的數據?答案是他們沒有; 沒有內在的語言設施確定此信息。 相反,程序員必須外在地指出聯合中包含的數據類型。程序員通常利用下面的數據結構來完成這項任務。
	struct VariantType
	{
		unsigned long TypeValueBits;
		union {
			char *str_member;
			int *pint_member;
			class *class_member;
			unsigned long ulong_member;
		};
	};
  • 此結構具有類型成員 TypeValueBits ,它指示聯合中包含的數據類型。實際上,整個 Windows 中普遍存在的 VARIANT 數據類型正是這種格式,稍後將重新討論。類型混淆漏洞的本質是要麼將指示哪個聯合成員適合訪問的成員與聯合中包含的內容進行去同步,要麼找到錯誤解釋類型字段的代碼。

3.2.3 Microsoft Type 混淆漏洞:VARIANTs

  • 正如我們之前在本文的技術概述中看到的那樣,VARIANT 數據結構在 Microsoft 代碼中被廣泛使用,作爲表示各種數據類型的標準化,語言無關的方法。用於操作 VARIANT 數據結構的 API 已在本文的概述部分中介紹。我們現在將探討如何直接或通過定義良好的 API 對 VARIANT 結構的錯誤管理導致許多微妙的類型混淆場景。

3.2.3.1 VARIANT 類型混淆攻擊 I:Permissive 屬性映射

  • 如前所述, Microsoft 的 ATL 通過爲一組接口分發模板代碼,幫助開發人員快速開發 COM 組件。 Microsoft 以抽象方式編寫了模板代碼,允許模板代碼在各種情況下使用; 但是,利用一些可用的代碼也會產生微妙的後果。具體而言,開發人員使用屬性映射指定 COM 對象屬性的方式具有一些微妙的細微差別,可能會導致攻擊者執行類型混淆攻擊的機會。
  • 請考慮 Microsoft ATL 版本9中可用的以下宏,這些宏可用於指定屬性映射中的各個屬性:
	struct ATL_PROPMAP_ENTRY
	{
		LPCOLESTR szDesc;
		DISPID dispid;
		const CLSID* pclsidPropPage;
		const IID* piidDispatch;
		DWORD dwOffsetData;
		DWORD dwSizeData;
		VARTYPE vt;
	};
	
	#define PROP_DATA_ENTRY(szDesc, member, vt) \
		{OLESTR(szDesc), 0, &CLSID_NULL, NULL, offsetof(_PropMapClass, member), sizeof(((_PropMapClass*)0)->member), vt},
		
	#define PROP_ENTRY(szDesc, dispid, clsid) \
		{OLESTR(szDesc), dispid, &clsid, &__uuidof(IDispatch),0, 0, VT_EMPTY},
	
	#define PROP_ENTRY_EX(szDesc, dispid, clsid, iidDispatch) \
		{OLESTR(szDesc), dispid, &clsid, &iidDispatch, 0, 0, VT_EMPTY},
	
	#define PROP_ENTRY_TYPE(szDesc, dispid, clsid, vt) \
		{OLESTR(szDesc), dispid, &clsid, &__uuidof(IDispatch), 0, 0, vt},
	
	#define PROP_ENTRY_TYPE_EX(szDesc, dispid, clsid, iidDispatch, vt) \
		{OLESTR(szDesc), dispid, &clsid, &iidDispatch, 0, 0, vt},
  • 值得注意的是,PROP_ENTRY 和 PROP_ENTRY_EX 都不需要參數來指定 VARIANT 類型。回想一下我們之前關於持久性的討論,當使用這些函數時,持久性流將包含兩個字節,用於標識序列化數據之前的序列化類型。一旦被描述的成員被反序列化,ATL 代碼將調用屬性映射指定的 IDispatch 接口的 put 屬性方法,以便將數據寫入 COM 對象。總之,利用這些宏提供了一種機會,可以將任何類型的 VARIANT 提供給 IDispatch 接口的 put 方法,而不必強制強制轉換爲特定的數據類型。如果開發人員沒有考慮 put 方法可能提供任意 VARIANT 類型,那麼使用這種類型的屬性聲明可能會導致類型混淆問題。在 Internet Explorer 中未使用的對象中更有可能發現此類漏洞,或者在實現 IDispatch 的接口中,這些接口在屬性映射中指定,但無法從 Internet Explorer 訪問。
  • 開發人員也可以選擇使用 PROP_DATA_ENTRY() 而不是 PROP_ENTRY() 。PROP_DATA_ENTRY 宏是唯一的,因爲該屬性的數據不會被 IDispatch 接口過濾。相反,它直接寫入保存屬性數據的類內存中的偏移量。如果提供給宏的變量類型是 VT_EMPTY,則持久性代碼將讀取類中屬性可用的字節數。解壓縮 PROP_DATA_ENTRY 屬性與 PROP_ENTRY 宏的過程如圖22所示:
    在這裏插入圖片描述
  • 因此,使用 PROP_DATA_ENTRY() 宏爲攻擊者提供了兩個有趣的機會:
	1. The ability to create a property directly in the destination object’s memory possibly without having any typing requirements
	能夠直接在目標對象的內存中創建屬性,而不需要任何類型要求

	2. The ability to provide properties that have undergone absolutely no validation
	提供絕對沒有驗證的屬性的能力
  • 如果在無類型管理器中指定 PROP_DATA_ENTRY 宏,則這些屬性非常危險。如果使用指定爲 VT_EMPTY 的類型構造它們,則隨後使用此類屬性的代碼幾乎肯定會包含類型混淆漏洞,因爲它無法驗證它正在操作的數據類型。例如,考慮一種情況,其中 PROP_DATA_ENTRY 屬性旨在成爲指向字符串或其他更復雜對象的指針。通過指定整數類型而不是預期對象,將觸發類型混淆漏洞,最終結果很可能是任意執行。相反,可能存在這樣的情況:屬性成員應該是整數,但攻擊者指定指針(通過指定字符串或其他內容)。此示例類型混淆漏洞很可能會導致信息泄露,並最終泄露指針的值。當試圖繞過當代 Windows 操作系統中的內存保護機制時,這些類型的問題變得越來越有用。
  • 此外,值得考慮的是 PROP_DATA_ENTRY 屬性是直接設置的,因此可以繞過 IDispatch 接口的 put 屬性可以強制執行的任何驗證級別。這意味着可能存在直接設置這些屬性可能在某種程度上繞過檢測過程的情況,因爲它可能在 put 屬性方法中執行。因此,當在以某種方式對其進行檢測的脆弱假設下使用該屬性時,攻擊者有可能利用該對象繞過檢測。

3.2.3.2 VARIANT 類型混淆攻擊 II:錯誤解釋類型

  • 在處理 VARIANT 數據結構時容易出現潛在問題的一個方面是正確解釋 vt 成員。與 NPAPI variant 數據結構相反,回想一下 VARIANT 中的類型參數可以是基本類型,也可以是由表示基本類型和修飾符的位組成的複雜類型(或者兩個修飾符,如果其中一個是 VT_BYREF)。當錯誤地執行位掩碼時,可能會發生對 vt 成員的錯誤解釋,從而導致細微的漏洞,其中 VARIANT 的值在實際上是另一種時被用作一種類型。
  • 爲了說明這一點,請考慮以下代碼:
	#ifndef VT_TYPEMASK
	#define VT_TYPEMASK 0xfff
	#endif
	WXDLLEXPORT bool wxConvertOleToVariant(const VARIANTARG& oleVariant, wxVariant& variant)
	{
		switch (oleVariant.vt & VT_TYPEMASK)
		{
		case VT_BSTR:
			{
				wxString str(wxConvertStringFromOle(oleVariant.bstrVal));
				variant = str;
				break;
			}
  • 精明的讀者會注意到這段代碼有一個非常明顯的缺陷:使用掩碼執行類型檢查以獲得 VARIANT 的基本類型。在 BSTR 的情況下,字符串被傳遞複製它的函數。 這裏的問題是如果使用修飾符, VARIANT 將不包含 BSTR 作爲其值參數。如果此函數的調用者提供類型爲(VT_BYREF | VT_BSTR)的 VARIANT,則會導致指向 BSTR 的指針放在 VARIANT 而不是 BSTR 中。(BSTR 實際上是 WCHAR *,其前面有 32 位長度,因此 BSTR * 是 WCHAR **)因此,在傳遞給此函數的 VARIANT 上使用任何修飾符都會導致類型混淆漏洞。

  • 考慮這個稍微更微妙的例子:

	SAFEARRAY *psa;
	ULONG *pValue
	// Test if object is an array of integers
	VARTYPE baseType = pVarSrc->vt & VT_TYPEMASK;
	if( (baseType != VT_I4 && baseType != VT_UI4) || ((pVarSrc->vt & VT_ARRAY) == 0) )
		return -1;
	psa = pVarSrc->parray;
	// operate on SAFEARRAY
	SafeArrayAccessData(psa, &pValues);
	...
  • 此代碼執行一些檢查以確保輸入類型是有符號或無符號整數的數組。如果不是,則通過返回值 -1 來發出錯誤信號。但是,此代碼中也存在問題 - 檢查變量類型時未考慮該類型可以設置 VT_BYREF 位。由於 VT_ARRAY 修飾符與 VT_BYREF 不相互排斥,因此在處理類型爲(VT_BYREF | VT_ARRAY | VT_I4)的 VARIANT 時,上述代碼存在類型混淆漏洞。在這種情況下,SAFEARRAY** 將被錯誤地解釋爲 SAFEARRAY *,導致超出內存訪問。

  • 以下代碼是從 IE(所有當前版本)獲取的真實示例。 此示例是 DOM 的核心編組代碼的一部分。有問題的代碼負責驗證從插入 DOM 的腳本主機接收的 VARIANT 參數是否正確,如有必要,將這些參數轉換爲預期類型。儘管每個 DOM 函數都使用不同類型的參數,但大多數編組例程在其核心使用相同的函數 VARIANTArgToCVar(),該函數接受單個 VARIANT 並嘗試將其轉換爲期望的類型。易受攻擊的代碼如下所示:

	int VARIANTARGToCVar(VARIANT *pSrcVar, int *res, VARTYPE vt, PVOID outVar, IServiceProvider *pProvider, BOOL bAllocString)
	{
		VARIANT var;
		VariantInit(&var);
		if(!(vt & VT_BYREF))
		{
			// Type mismatch - attempt conversion
			if( (pSrcVar->vt & (VT_BYREF|VT_TYPEMASK)) != vt && vt != VT_VARIANT)
			{
				hr = VariantChangeTypeSpecial(&var, pSrcVar, vt, pProvider, 0);
				if(FAILED(hr))
					return hr;
				... more stuff ...
				return hr;
			}
			switch(vt)
			{
			case VT_I2:
				*(PSHORT)outVar = pSrcVar->iVal; break;
			case VT_I4:
				*(PLONG)outVar = pSrcVar->lVal; break;
			case VT_DISPATCH:
				*(PDISPATCH)outVar = pSrcVar->pdispVal; break;
			... more cases ...
			}
		}
	}
  • 有問題的代碼嘗試檢索輸入參數 pSrcVar 的值,如果接收的 VARIANT 不是 vt 參數中給出的預期類型,則執行類型轉換。將接收到的輸入 VARIANT 類型與預期類型進行比較時,會出現此代碼中的問題。具體來說,在輸入類型掩碼爲(VT_BYREF | VT_TYPEMASK)或 0x4FFF 之後,通過比較期望的類型和輸入類型來完成測試。執行此掩碼會丟失重要信息,在本例中爲 VT_ARRAY(0x2000)和 VT_VECTOR(0x1000)修飾符。爲了說明該問題,請考慮此函數期望 VT_DISPATCH 輸入類型(0x0009)並且輸入 VARIANT 是 VT_DISPATCH 類型(VT_ARRAY | VT_DISPATCH 0x2009)的數組的情況。由於(0x2009 和 0x4FFF)產生結果 0x0009 或 VT_DISPATCH,因此該代碼將錯誤地認爲它接收到 IDispatch 對象而不是 IDispatch 對象數組。結果?此函數表示成功並返回指向 SAFEARRAY 的指針,該指針已錯誤地評估爲指向 IDispatch 接口的指針。因此,此代碼最終導致類型混淆漏洞。
  • 評估者在使用 VARIANT 類型掩碼時對漏洞進行審計時必須密切關注如何操縱 VARIANT 的 vt 成員。 具體來說,掩碼輸入 VARIANT 類型。
  • 需要謹慎執行,以確保在執行任何驗證步驟時不會忽略信息。

3.2.3.3 VARIANT 類型混淆攻擊 III:直接類型操縱

  • 另一種可能導致類型混淆漏洞的構造是直接操縱 VARIANT 的 vt 成員,而不是使用 API 函數。雖然理論上這應該是一項相當簡單的任務,但是如果沒有正確實施數據類型,或者沒有正確確保類型轉換成功,則可能會引入微妙的漏洞。例如,以下代碼取自 Microsoft 的 ATL 內部版本。從持久性流執行 COM 對象的反序列化時,將調用此代碼。請注意,在此特定示例中, VARIANT 數據結構包裝在 C++ 對象 CComVariant 中。此代碼中的類成員 vt 對應於 VARIANT 結構中的 vt 類型變量。
  • 上面列舉的例子是人爲的; 然而,本文的作者已經確定了發生此類錯誤的真實場景。Microsoft 的內部版本的 ATL 具有特殊代碼來處理持久性流中的 variant,類似於以下示例:
	inline HRESULT CComVariant::ReadFromStream(IStream* pStream)
	{
		ATLASSERT(pStream != NULL);
		HRESULT hr;
		hr = VariantClear(this);
		if (FAILED(hr))
			return hr;
		VARTYPE vtRead;
		hr = pStream->Read(&vtRead, sizeof(VARTYPE), NULL);
		if (hr == S_FALSE)
			hr = E_FAIL;
		if (FAILED(hr))
			return hr;
		vt = vtRead;
		
		 //Attempts to read fixed width data types here
		 
		CComBSTR bstrRead;
		hr = bstrRead.ReadFromStream(pStream);
		if (FAILED(hr))
			return hr;
		vt = VT_BSTR;
		bstrVal = bstrRead.Detach();
		if (vtRead != VT_BSTR)
		{
			hr = ChangeType(vtRead);
			vt = vtRead;
		}
		return hr;
	}
  • 上述代碼的問題是在手動設置 variant 類型之前不會檢查 ChangeType() 函數的返回值。此錯誤允許攻擊者使程序相信攻擊者提供的 BSTR 值是固定寬度數據類型處理程序中未處理的任何類型。在一種情況下,攻擊者可以指定他提供的字符串應被視爲 VT_DISPATCH 對象的數組。當此函數返回錯誤時,調用者將嘗試使用 VariantClear() 函數釋放該字符串。這最終導致程序將攻擊者提供的字符串視爲一個 vtable 數組,一個明確的類型混淆錯誤,最終允許任意代碼執行。

3.2.3.4 VARIANT 類型混淆攻擊 IV:初始化錯誤

  • 儘管是一個相對簡單的操作數據結構,但 VARIANT 在某些情況下由於 API 部分的欺騙性而容易被濫用。在研究本文的 VARIANT 用法時,作者發現的一個關鍵錯誤是 VarintInit() 和 VariantClear() 調用的不匹配。正如我們在本文前面提到的,VariantInit() 函數用於通過將 vt 成員設置爲 VT_EMPTY 來初始化 VARIANT 結構。相反,VariantClear() 將釋放與 VARIANT 相關的數據,同時考慮存儲在那裏的數據類型。隨後它將 VARIANT 的類型值設置爲 VT_EMPTY。
  • 這裏需要注意的重要一點是,在 VARIANT 上調用 VariantClear() 時尚未正確初始化的任何代碼路徑都可能導致潛在的安全問題。爲什麼?因爲 VariantClear() 將讀取 VARIANT 的未初始化的 vt 成員,並使用它來決定如何操作未初始化的 VARIANT 值。例如,如果 vt 成員是 VT_DISPATCH (0x0009),VariantClear() 將從 VARIANT 獲取數據成員並取消引用它以進行間接調用,因爲刪除 IDispatch 對象的過程涉及調用 IDispatch::Release() 功能。VariantInit() 函數的省略創建了一個條件,與沒有先分配它的釋放內存塊的內存管理模擬不同,有兩個主要區別:
1.Double VariantClear()double free()不同 - 由於 VariantClear() 將 VARIANT 類型設置爲 VT_EMPTY,因此對同一 VARIANT 的 VariantClear() 的任何後續調用都將無效
2. 沒有 malloc()VariantInit() 的省略比 free() 更有可能,因爲代碼在大多數情況下仍然看似正常工作,即使行使了易受攻擊的代碼
  • 這類錯誤實際上是一個未初始化的變量問題,但是包含在本節中,因爲它會導致類型混淆的形式,另外需要注意的是攻擊者需要使用有用的數據來填充適當的內存區域,而不是直接指定它。也就是說,這些問題的可利用性非常依賴於分配 VARIANT 的存儲器中包含的殘留數據。在某些情況下,此數據受攻擊者控制,而在其他情況下,攻擊者需要一些運氣。
  • VariantInit() 遺漏漏洞的示例如下所示:
HRESULT MyFunc(IStream* pStream)
{
	VARIANT var;
	IDispatch* pDisp;
	HRESULT hr;
	var.vt = VT_DISPATCH;
	
	hr = pStream->Read(pDisp, sizeof(IDispatch *), NULL);
	if(FAILED(hr)) {
		VariantClear(&var);
		return hr;
	}
	. . .
	return hr;
}
  • 可以看出,位於堆棧上的 VARIANT 是使用 VT_DISPATCH 類型手動初始化的,並且可能在從源流中成功讀取數據後填充了指向 IDispatch 接口的指針。但是,如果 IStream::Read() 操作失敗,則清除 VARIANT,導致操作未初始化的堆棧數據,就像它指向 IDispatch 接口一樣。
  • 儘管這似乎是一個相對不太可能的錯誤,但有時候易受攻擊的代碼路徑的變化會稍微微妙一些。使用 VariantCopy() 函數在 VARIANT 之間複製數據時會出現一個這樣的示例。VariantCopy() 函數在複製任何內容之前清除目標 VARIANT 參數。因此,必須首先清除傳遞給 VariantCopy() 的目標參數。下面的代碼演示了一個易受攻擊的情況,其具有與前一個示例相同的可利用性限制:
	HRESULT MyFunc(IStream* pStream)
	{
		VARIANT srcVar;
		VARIANT dstVar;
		IDispatch* pDisp;
		HRESULT hr;
		srcVar.vt = VT_DISPATCH;
		dstVar.vt = VT_DISPATCH;
		
		hr = pStream->Read(pDisp, sizeof(IDispatch *), NULL);
		if(FAILED(hr)) {
			//VariantClear(&var);
			return hr;
		}
		else {
			srcVar.pdispVal = pDisp;
			hr = VariantCopy(&dstVar, &srcVar);
		}
		return hr;
	}
  • 其他 VARIANT API 函數中也存在類似問題,最值得注意的是 VariantChangeType() / VariantChangeTypeEx() 函數。這些函數將在一些但不是所有轉換情況下使用 VariantClear()。在目標值上調用 VariantClear() 的規則在很大程度上是直觀的; 它們發生在:
  • 未遇到無效的轉換嘗試(即,不在兩個不兼容的類型之間進行轉換),並且當源和目標 VARIANT 相同時,VariantClear() 不會導致問題,例如從 VT_UNKNOWN - > VT_DISPATCH 的轉換
  • 在審覈漏洞方面,應該嚴格查看目標參數未初始化的任何轉換。例如,請考慮以下代碼:
	BSTR *ExtractStringFromVariant(VARIANT *var)
	{
		VARIANT dstVar;
		HRESULT hr;
		BSTR *res;
		
		if(var->vt == VT_BSTR)
			return SysAllocString(var->bstrVal);
		else {
			hr = VariantChangeType(&dstVar, var, 0, VT_BSTR);
			if(FAILED(hr))
				return NULL; 
		}
		
		res = SysAllocString(dstVar.bstrVal);
		VariantClear(&dstVar);
		return res;
	}
  • 這裏我們看到與前面示例類似的構造,除了這次使用 VariantChangeType()。存在以下開發要求:
	1.目的地變量未被初始化
	2.從常規類型到VT_BSTR的轉換將導致目標 VARIANT 上的 VariantClear() (例如 VT_I4 -> VT_BSTR)
  • 如前所述,成功利用上述漏洞需要攻擊者能夠影響堆棧,以便未初始化的目標 VARIANT 中包含有用數據,例如具有 VT_DISPATCH 類型和某種有效指針作爲值。

3.2.4 Mozilla 類型混淆漏洞:NPAPI

  • 大多數非IE瀏覽器爲插件交互實現 NPAPI,後者又利用 NPRuntime 將腳本化對象暴露給腳本語言。用於向插件傳遞變量和從插件傳遞變量的 API 比 COM 和 IE 使用的 API 簡單得多,從而減少了攻擊面。然而,NPRuntime 仍然爲攻擊者提供了有趣的機會,因爲它會導致濫用,導致類型混亂漏洞類似於我們在 VARIANT 中看到的漏洞。本節探討如何在 NPRuntime 腳本化對象的上下文中發生類型混淆漏洞。此討論適用於實現 NPAPI 並將 NPRuntime 功能公開給 Web 內容的所有瀏覽器。

3.2.4.1 NPAPI 類型混淆攻擊 I:類型驗證

  • 我們已經看過的 NPRuntime 和 COM VARIANT 傳遞之間的主要區別之一是 NPRuntime 不對從腳本主機收到的 NPVariant 執行任何類型強制或驗證。回想一下我們之前對 NPRuntime 的討論,插件如何訪問 NPVariant; 通過使用 NPVARIANT_TO_XXX() 宏之一。這些宏除了訪問 NPVariant 中包含的 union 數據結構的成員之外什麼都不做 - 插件開發人員有責任通過使用相應的 NPVARIANT_IS_XXX() 宏來確保 variant 的類型正確。正確處理 NPVariant 參數的插件可能如下所示:
	bool SetProperty(NPObject *obj, NPIdentifier name, const NPVariant *variant)
	{
		if(name == kTestIdent)
		{
			if(!NPVARIANT_IS_INT32(*variant))
				return false;
			gTest = NPVARIANT_TO_INT32(*variant);
			return true;
		}
		return false;
	}
  • 此示例是操作 NPVariants 的預期算法 - 檢查正確的類型,然後檢查數據。每次函數接收 NPVariant 時,必須在處理其數據之前執行此類型檢查。缺少初始檢查會使代碼容易出現類型混淆問題。爲了說明未能執行初始檢查的問題,請考慮以下從 Google 的 "Native Client” 插件中獲取的代碼:
	bool Plugin::SetProperty(NPObject* obj, NPIdentifier name, const NPVariant* variant) 
	{
	Plugin* plugin = reinterpret_cast<Plugin*>(obj);
	if (kHeightIdent == name) {
		plugin->height_ = NPVARIANT_TO_INT32(*variant);
		return true;
  • 此函數設置有問題對象的 “height” 屬性,但無法確保被操作的 NPVariant 是整數。攻擊者可能會傳遞一個字符串或對象 in 作爲 height 參數而不是整數,導致指針混淆爲整數。此代碼很可能導致信息泄露漏洞,當攻擊者稍後讀回高度屬性時,漏洞會泄露指針。顯然,相反的情況可能更危險 - 攻擊者可以提供一個整數來代替指針。根據指針的操作方式,這種情況可能導致更廣泛的信息泄漏或內存損壞漏洞。
  • 此攻擊的一個稍微微妙的變化是 NPVariant 被驗證爲 NPObject,並且插件嘗試將通用 NPObject 強制轉換爲特定類型的對象。NPAPI 運行時缺少 API 函數,允許您確定執行此對象轉換是否安全,因此此構造幾乎總是有助於利用。 返回 Native Client,請考慮以下代碼:
	static bool GetHandle(struct NaClDesc** v, NPVariant var) {
		if (NPVARIANT_IS_OBJECT(var)) {
			NPObject* obj = NPVARIANT_TO_OBJECT(var);
			UnknownHandle* handle = reinterpret_cast<UnknownHandle*>(obj);
			*v = handle->desc();
			return true;
		} else {
			return false;
		}
	}
  • 此代碼負責從 JavaScript 接收“句柄”對象。句柄對象是可編寫腳本的對象的特定專用,由 Native Client 實現,用於與其後端進行通信。代碼使用 NPVARIANT_IS_OBJECT() 宏正確驗證收到的 NPVariant 確實是一個 JavaScript 對象。但是,他們隨後將收到的 NPObject 指針強制轉換爲 UnknownHandle 指針。由於攻擊者可能在此處提供任意 JavaScript 對象,因此可以執行類型混淆攻擊,其中任何隨機 NPObject 與 UnknownHandle 對象混淆。這種類型混淆漏洞的最可能結果是任意代碼執行。
  • 這裏值得一提的是,NPObject 函數的輸入不一定是提供可能錯誤類型對象的唯一方法。如技術概述中所述,NPN_GetProperty() 函數用於從 DOM 層次結構中檢索對象。由於這些對象受腳本控制,因此操縱 DOM 中可見的對象可以是執行與此處描述的類似攻擊的入口點。

3.2.4.2 NPAPI 類型混淆攻擊 II:參數計數驗證

  • NPObject 公開的 Invoke() 和 InvokeDefault() 方法需要根據 NPIdentifier 參數標識的方法的正確數量和類型參數來驗證傳遞給它們的參數的數量和類型。對於這兩個函數,只需確保 argc 參數包含正確的值即可驗證參數的數量。雖然這是一個不太常見的錯誤,但插件開發人員需要驗證 Invoke() 和 InvokeDefault() 中每個可調用函數的 argc 參數 - 它不會自動驗證。無法驗證它可能導致無效數組索引用於檢索參數參數的情況。一些易受攻擊的示例代碼如下所示:
	bool Invoke(NPObject *obj, NPIdentifier name, const NPVariant *args, uint32_targCount, NPVariant *result)
	{
		if(name == kTestFuncName)
		{
			if(argCount != 2 && (!NPVARIANT_IS_INT32(args[0]) || !NPVARIANT_IS_STRING(args[1])))
				return false;
			unsigned int length = NPVARIANT_TO_INT32(args[0]);
			char *buffer = ExtractString(args[1]);
			... more code ...
		}
	}
  • 上面的代碼是出於好意 - 它試圖檢查參數計數以及每個參數的類型。但是,檢查中存在一個問題:邏輯and(&&)運算符用於邏輯 OR(||)應該在的位置。這樣,可以通過驗證並使用與預期數量不同的多個參數執行處理代碼。如果只傳遞一個參數,則對於args數組的第二個元素的任何操作,將訪問越界內存。
  • 前面的代碼構造導致使用未初始化的變量,並且可以認爲它更適當地歸類爲未初始化的變量問題; 然而,這種錯誤行爲源於 NPVariant 參數與原生變量類型相比的相對模糊性。因此,它包含在本節中,因爲它源於類型歧義,並且因爲此問題的語義類似於上一節中描述的語義。

3.3 互操作性攻擊 III:信任可執行模塊

  • 互操作性對執行環境提出了獨特的要求。首先,應用程序需要確保它實例化的組件符合應用程序的安全要求。確保這一事實很困難,因爲爲互操作性而編寫的組件不需要特定的環境; 因此,他們很大程度上不了解可能需要的任何環境特定的安全標準。實際上,在回顧微軟圍繞 COM 的安全性時,很容易形成脆弱的安全架構由於複雜性的結果。
  • 進一步複雜化該問題的事實是互操作性組件可能需要使用一個或多個子組件。假設應用程序有一種方法可以完全確保互操作性組件在應用程序的上下文中運行是安全的,那麼應用程序可能仍然完全不知道經過審查的組件將哪些子組件帶入執行環境。應用程序環境或父組件必須負責確保子組件對於執行環境是可信的。
  • 可傳遞信任是我們用來表示組件能夠將主機應用程序授予的信任擴展到組件可能依賴的對象的條件的一個術語,由組件自行決定。 在Web瀏覽器的上下文中,實踐中使用的授權模型是平坦的;也就是說,只有父組件經過主機應用程序的顯式授權檢查。圖23描繪了一個示例信任鏈:
    在這裏插入圖片描述
  • 如圖所示,強制執行安全模型的動力完全放在父組件上。父對象可能依賴於子對象的這種模型創建了一個信任鏈,其中鏈中的每個鏈接都由來自不同代碼庫的對象驗證,使用可能不同的策略,可能擁有他們繼承的信任模型的有限概念。因此,他們可能無法以完全保真的方式強制執行該模型。此外,隨着時間的推移添加到主機應用程序中的改裝安全功能通常會被最初設計爲不符合新限制的插件或組件破壞。因此,創造性地利用插件功能的攻擊者通常可以方便地繞過新的安全功能。本節將介紹作者發現的一些屬於可傳遞信任類別的攻擊 - 利用插件或組件功能來破壞內置於 Web 瀏覽器的安全功能。

3.3.1 傳遞信任漏洞 I - 持久化對象

  • 本文已經詳細討論了持久 COM 對象的實現和使用,以及它們在安全性方面提出的一些挑戰。除了已經討論過的問題之外,持久對象還爲攻擊者提供了使對象加載屬性值的能力,有時甚至是任意類型的值。以下部分將探討此功能對傳遞信任漏洞的影響,最終概述繞過 Internet Explorer 依賴的安全功能以安全傳遞 Web 內容的方法。

3.3.1.1 傳遞信任漏洞: 繞過控制授權(Highlander Bit)

  • 正如本文第二部分所討論的,IE 實現了各種控件來限制可以在瀏覽器的上下文中實例化哪些 ActiveX 對象,以及在控件被授權用於瀏覽器上下文之前用戶呈現的警告類型。正如我們之前所描述的,對於在 Internet Explorer 的執行環境中被認爲是安全加載的對象,需要將其標記爲安全的腳本和/或安全的初始化,不能設置有問題的對象的 killbit,最後 ,必須批准控件在域中運行。
  • 首次安裝 Internet Explorer 時會填充預先批准的列表,並且此列表的任何其他更改都將來自用戶自定義。此列表限制了攻擊者在沒有 Internet Explorer 通知目標內容可能會破壞瀏覽器安全性的情況下可以利用的控制數量。此外,Microsoft 一直在分發累積 killbit 設置作爲其每月安全捆綁包的一部分,以確保在 IE 的上下文中無法加載大量可利用的控件。實際上,在幾個實例中,Microsoft 選擇通過添加 killbits 來禁用控件,而不是在發現它們時嘗試修復底層漏洞。 很容易看出,從攻擊者的角度來看,繞過這些授權是非常可取的,而對象持久性有助於實現這一目標。
  • 在典型的Windows機器上有幾個可用的控件,可以在不提示用戶的情況下進行初始化。由於大多數自動化控件使用 IPersistStream 的默認 ATL 實現來將對象復活到內存中,因此我們將考慮此實現中的 Load() 方法。 恢復對象的大部分工作實際上是由 CComVariant::ReadFromStream() 執行的,其實現部分顯示:

	HRESULT VariantCopy(VARIANTARG *pvargDest, VARIANTARG *pvargSrc);
	HRESULT VariantCopyInd(VARIANTARG *pvargDest, VARIANTARG *pvargSrc);
	inline HRESULT CComVariant::ReadFromStream(IStream* pStream)
	{
		ATLASSERT(pStream != NULL);
		if(pStream == NULL)
			return E_INVALIDARG;
			
		HRESULT hr;
		hr = VariantClear(this);
		if (FAILED(hr))
			return hr;
			
		VARTYPE vtRead = VT_EMPTY;
		ULONG cbRead = 0;
		hr = pStream->Read(&vtRead, sizeof(VARTYPE), &cbRead);
		if (hr == S_FALSE || (cbRead != sizeof(VARTYPE) && hr == S_OK))
			hr = E_FAIL;
			
		if (FAILED(hr))
			return hr;
			
		vt = vtRead;
		cbRead = 0;
		switch (vtRead)
		{
			case VT_UNKNOWN:
			case VT_DISPATCH:
			{
			punkVal = NULL;
				hr = OleLoadFromStream(pStream, (vtRead == VT_UNKNOWN) ? __uuidof(IUnknown) : __uuidof(IDispatch), (void**)&punkVal);
				// If IPictureDisp or IFontDisp property is not set,
				// OleLoadFromStream() will
				// return REGDB_E_CLASSNOTREG.
				if (hr == REGDB_E_CLASSNOTREG)
					hr = S_OK;
				return hr;
			}
			case VT_UI1:
			case VT_I1: cbRead = sizeof(BYTE); break;
			... more object types ...
			default:
				break;
		}
		... more code ...
	}

  • 通過查看上面的代碼可以看出,當讀取 VT_DISPATCH 或 VT_UNKNOWN 對象時,IStream 將傳遞給 OleLoadFromStream() 以將下級對象讀入內存。從 ole32.dll 導出的 OleLoadFromStream() 的僞代碼如下所示:
	HRESULT __stdcall OleLoadFromStream(LPSTREAM pStm, const IID *const iidInterface, LPVOID *ppvObj)
	{
		IPersistStream *pIPersistStream;
		IUnknown *pIUnknown;
		CLSID clsidControl;
		HRESULT hrValue;
		*ppbObj = NULL;
		
		hrValue = ReadClassStm(pStm, &clsidControl);
		if(hrValue != ERROR_SUCCESS)
			return(hrValue);
			
		hrValue = CoCreateInstance(&clsidControl, NULL, CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER | CLSCTX_NO_CODE_DOWNLOAD, iidInterface, &pIUnknown);
		if(hrValue != ERROR_SUCCESS)
			return(hrValue);
			
		hrValue = pIUnknown->QueryInterface(CLSID_IPersistStream, &pIPersistStream);
		if(hrValue != ERROR_SUCCESS)
			goto CleanupIUnknown;
			
		hrValue = pIPersistStream->Load(pStm);
		pIPersistStream->Release();
		if(hrValue != ERROR_SUCCESS)
			goto CleanupIUnknown;
			
		hrValue = pIUnknown->QueryInterface(iidInterface, ppvObj);
		
	CleanupIUnknown:
		pIUnknown->Release();
		return(hrValue);
	}
  • 如上所示,OleLoadFromStream() 函數將使用 IStream 中提供的 CLSID 調用 CoCreateInstance(),然後使用持久性數據初始化控件。如果攻擊者能夠提供此持久性數據,那麼他們可以使用此代碼加載任意 COM 對象併爲該對象提供持久性數據。最重要的是,沒有功能可以確定從屬控件是否滿足主機應用程序的安全要求 - 包括控件的 killbit 狀態,以及可能請求用戶批准的任何邏輯。應該注意的是,在撰寫本文時,使用此方法似乎只提供對從持久性數據加載的控件的 IPersistStream 接口中的 Load() 方法的訪問。但是,此功能完全足以提供一個不受安全限制影響的向量,允許訪問持久性例程中存在的漏洞以及許多以前公開的漏洞。表24列出了一小部分控件,這些控件可以到達並且已報告僅在對象實例化時觸發漏洞,或者通過處理持久性數據。
    在這裏插入圖片描述
  • 從隨 Visual Studio 97 一起發佈的 ATL 第2版(包括 ATL 8.0 版)開始,與 Visual Studio 2005 一起分發,沒有任何機制可以對從任何宏的流中讀取的屬性類型進行粒度控制。不同於 PROP_DATA_ENTRY ;因此,從流中讀取的控件的大多數屬性可以讀作 VT_DISPATCH 或 VT_UNKNOWN variant 。在與 Visual Studio 2008 一起分發的 ATL 9.0 版中,未聲明類型的屬性條目宏被聲明爲不推薦使用,CComVariant::ReadFromStream() 要求從流中讀取的類型等同於宏中指定的類型,除非類型指定等於 VT_EMPTY。但是,若干第三方控件(最值得注意的是 Macromedia 的 Flash 控件)具有指定 VT_DISPATCH 類型的屬性條目,並且仍將允許此向量。此外,實現自定義 Load() 方法的幾個 Microsoft 控件也爲攻擊者提供了加載任意對象的能力。以下示例代碼是 Microsoft 的 ComponentTypes 控件的 IPersistStream 實現的一部分。
HRESULT __stdcall CComponentTypes::Load(struct IStream *pStm)
{
	HRESULT hrVal;
	ULONG ulRead;
	long lCntComponents;
	long lIndexComponent;
	hrVal = pStm->Read(&lCntComponents, sizeof(lCntComponents), &ulRead);
	if(hrVal < ERROR_SUCCESS)
		return(hrVal);
		
	if(ulRead != sizeof(lCntComponents))
		return(E_UNEXPECTED);
		
	for( lIndexComponent = 0; lIndexComponent < lCntComponents; lIndexComponent++)
	{
		GUID2 ReadGuid;
		hrVal = pStm->Read(&ReadGuid, sizeof(ReadGuid), &ulRead);
		if(hrVal < ERROR_SUCCESS)
			return(hrVal);
			
		CComQIPtr <IPropertyBag,  &__s_GUID const_GUID_55272a00_42cb_11ce_8135_00aa004bb851> myControl;
		hrVal = CoCreateInstance(&ReadGuid, NULL, CLSCTX_INPROC_SERVER| CLSCTX_INPROC_HANDLER,  IID_IPersistStreamInit, &myControl);
		if(hrVal < ERROR_SUCCESS)
			return(hrVal);
			
		hrVal = myControl.Load(pStm);
		if(hrVal < ERROR_SUCCESS)
			return(hrVal);
	...
  • 代碼讀取一個整數,指定流中的控件數。 接下來,它將讀入類ID並嘗試從持久流加載控件。它將重複最後一步,直到遇到錯誤,或者它已經讀取了許多等於流中第一個整數值的控件。 同樣,攻擊者可以指定此控件從中讀取的流,並且控件在使用攻擊者提供的持久性數據加載之前不會對此控件執行任何授權檢查。

第四節:結論

  • 互操作性爲應用程序提供了利用可插拔組件提供更高靈活性的優勢。然而,從安全角度來看,這種靈活性的成本在很大程度上經常被忽視。我們提出了針對互操作性功能本身的攻擊 - 從跨模塊邊界的數據對象的編組和管理到利用對插件或核心組件的信任擴展。此外,我們已經證明,這些區域更容易受到影響,過去已經收到適度或沒有真正關注的獨特錯誤類。希望針對使用互操作性在不相關組件之間進行通信的應用程序的攻擊者可以使用此類技術來發現數據操作代碼中的細微缺陷,或者通過利用寬鬆的信任邊界來破壞旨在阻止安全漏洞的反措施。對互操作性的進一步研究可能會產生進一步獨特的開發方案,特別是在傳遞信任領域。這是由於安全障礙以及不斷添加到 Web 應用程序等豐富應用程序的新組件。

  • Sun, Java and JavaScript is a trademark of Sun Microsystems, Inc. in the United States and other countries.

  • Adobe, Flash, and ActionScript are trademarks of Adobe Systems Incorporated in the United States and/or other countries.

  • Macromedia is a trademark of Marcomedia, Inc in the United States and/or other countries.

  • Microsoft, Windows, Windows Vista, ActiveX, Silverlight, Microsoft Media Player, Visual Studio, VBScript, and Internet Explorer are trademarks of the Microsoft Corporation in the United States and other countries.

  • Mozilla and Firefox are trademarks of the Mozilla Foundation in the United States and other countries.

  • Google and Google Chrome are trademarks of Google, Inc in the United States and other countries.

  • CERT is a trademark of Carnegie Mellon University in the United States and other countries.

  • Apple and Safari are trademarks of Apple, Inc. in the United States and other countries.

  • Opera is a trademark of Opera Software ASA in the United States and other countries.

  • Netscape is a trademark of AOL, Inc. in the United States and other countries.

  • 本文的翻譯到此結束,鑑於本文信息量龐大,所以難免翻譯會出現錯誤,如有錯誤,歡迎指正
  • 由於本研究報告種類爲計算機類型,所以有一些專業術語可能會難以理解,日後會加以修改並逐漸完善
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章