Java沙箱逃逸走過的二十個春秋(五)
原文:ping_the_java_sandbox.html" target="_blank" rel="nofollow,noindex">http://phrack.org/papers/escaping_the_java_sandbox.html
在上一篇文章中,我們為讀者詳細介紹了糊塗的代理人漏洞方面的知識,在本文中,我們將繼續為讀者介紹例項未初始化漏洞。
—-[ 4.2 – 例項未初始化漏洞
——[ 4.2.1 – 背景知識
Java物件的初始化過程中,非常關鍵的一個步驟就是呼叫相應型別的建構函式。在建構函式中,不僅含有初始化變數所需的程式碼,同時,也可能含有執行安全檢查的程式碼。因此,為了保證平臺的安全性和穩定性,必須在完成物件的初始化以及允許其他程式碼呼叫該型別的方法之前強制呼叫建構函式,這一點非常重要。
建構函式呼叫的強制執行是由位元組碼驗證器負責的,它會在載入過程中對所有的類進行相應的檢查,以確保其合法性。
除此之外,位元組碼驗證器還負責(例如)檢查跳轉是否落在有效指令上,而不是落在指令的中間,並檢查控制流是否以return指令結尾。此外,它還檢查指令的操作物件是否為有效型別,這是用來防禦型別混淆攻擊的。關於這類攻擊的介紹,請參考第3.1.1節。
過去,為了檢查型別的有效性,JVM需要通過分析資料流來計算固定點(fix point)。該分析過程可能對同一路徑檢查多次。由於這種檢查方式非常耗時,會拖慢類的載入過程,因此,後來人們開發了一種新型方法,能夠線上性時間內完成型別檢查,其中,每個路徑僅被檢查一次。為此,可以為位元組碼新增稱為堆疊對映幀的元資訊。簡而言之,堆疊對映幀用來描述每個分支目標的可能型別。通常情況下,堆疊對映幀被儲存在一種稱為堆疊對映表[25]的結構中。
如果安全分析人員能夠建立一個例項,但不為其執行<init>(*)
呼叫(即不執行物件的建構函式或超類的建構函式)的話,就會出現例項未初始化漏洞。實際上,該漏洞直接違反了虛擬機器的相關規範[21]。它對JVM安全性的影響是,藉助於例項未初始化漏洞,安全分析人員能夠例項化他原本無權訪問的物件,進而訪問他原本無權訪問的屬性和方法。這樣的話,就可能會導致沙箱逃逸。
——[ 4.2.2 – 示例: CVE-2017-3289
通過閱讀該CVE的描述,會發現“該漏洞的成功攻擊可能導致Java SE、Java SE Embedded被完全接管”[22]。
就像CVE-2017-3272那樣,這意味著能夠利用該漏洞實現Java沙箱的逃逸。
據Redhat的bugzilla稱,“在OpenJDK的Hotspot元件中發現了一個不安全的類構造漏洞,它與異常堆疊幀的錯誤處理方式有關。不受信任的Java應用程式或applet能夠利用這個漏洞繞過Java沙箱的限制”[23]。我們可以從中推斷出兩條有用的資訊:(1)該漏洞出現在C/C++程式碼中(Hotspot是Java VM的名稱),以及(2)該漏洞與非法類構造和異常堆疊幀有關。並且,通過第2條資訊,我們可以進一步推斷出,該漏洞可能位於檢查位元組碼的合法性的相關C/C++程式碼中。此外,該頁面還提供了該漏洞的OpenJDK補丁的連結。
OpenJDK的更新補丁,即“8167104: Additional class construction refinements”可以修復該漏洞,該補丁可線上獲取,具體見參考文獻[24]。該程式對5個C ++檔案進行了更新,它們分別是:“classfile/verifier.cpp”,負責檢查類檔案的結構和合法性的類;“classfile/stackMapTable.{cpp, hpp}”,處理堆疊對映表的檔案;以及“classfile/stackMapFrame.{cpp, hpp}”,描繪堆疊對映幀的檔案。
藉助於diff命令,我們發現,函式StackMapFrame::has_flag_match_exception()已經被刪除,並且更新了一個我們將稱為C1的條件,即刪除了對has_flag_match_exception()函式的呼叫。此外,方法match_stackmap()和is_assignable_to()現在只剩下一個引數了,因為“bool handler”已被刪除。當該驗證程式正在檢查異常處理程式時,唯一的引數,即“handler”將被設為“true”。現在,條件C1已經變成下面的樣子:
--------------------------------------------------------------------------- .... -bool match_flags = (_flags | target->flags()) == target->flags(); -if (match_flags || is_exception_handler && has_flag_match_exception(target)) { +if ((_flags | target->flags()) == target->flags()) { return true; } .... ---------------------------------------------------------------------------
這個條件在函式is_assignable_to()中,用於檢查作為引數傳遞給該函式的當前堆疊對映幀,是否可賦值給目標堆疊對映幀。在打補丁之前,返回“true”的條件是match_flags || is_exception_handler && has_flag_match_exception(target)
。也就是說,要滿足當前堆疊對映幀和目標堆疊對映幀的標誌相同或者當前指令位於異常處理程式中,並且函式“has_flag_match_exception”返回“true”。注意,只有一種叫做“UNINITIALIZED_THIS”(又名FLAG_THIS_UNINIT)的標誌。如果該標誌的值為true,則表示“this”引用的物件還沒有進行初始化操作,即尚未呼叫其建構函式。
在打完補丁之後,條件變為“match_flags”。這意味著,在易受攻擊的版本中,可能存在一種方法能夠構造出這樣的位元組碼,能夠使得:“match_flags”為“false”(即“this”在當前幀中具有未初始化的標誌,但在目標幀中則沒有該標誌)、“is_exception_handler”為“true”(當前指令位於異常處理程式中)以及“has_flag_match_exception(target)”返回“true”。然而,這個函式什麼情況下會返回“true”呢?
函式has_flag_match_exception()的程式碼如下所示。
--------------------------------------------------------------------------- 1: .... 2: bool StackMapFrame::has_flag_match_exception( 3:const StackMapFrame* target) const { 4: 5:assert(max_locals() == target->max_locals() && 6:stack_size() == target->stack_size(), 7:"StackMap sizes must match"); 8: 9:VerificationType top = VerificationType::top_type(); 10:VerificationType this_type = verifier()->current_type(); 11: 12:if (!flag_this_uninit() || target->flags() != 0) { 13:return false; 14:} 15: 16:for (int i = 0; i < target->locals_size(); ++i) { 17:if (locals()[i] == this_type && target->locals()[i] != top) { 18:return false; 19:} 20:} 21: 22:for (int i = 0; i < target->stack_size(); ++i) { 23:if (stack()[i] == this_type && target->stack()[i] != top) { 24:return false; 25:} 26:} 27: 28:return true; 29: } 30: .... ---------------------------------------------------------------------------
為了讓這個函式返回“true”,必須滿足以下所有條件:(1)當前幀和目標幀的最大區域性變數個數與堆疊的最大長度必須相同(第5-7行);(2)當前幀必須將“UNINIT”標誌設為“true”(第12-14行);(3)目標幀中沒有使用未初始化的物件(第16-26行)。
下面是滿足以上述三個條件的位元組碼:
--------------------------------------------------------------------------- <init>() 0: new// class java/lang/Throwable 1: dup 2: invokespecial // Method java/lang/Throwable."<init>":()V 3: athrow 4: new// class java/lang/RuntimeException 5: dup 6: invokespecial // Method java/lang/RuntimeException."<init>":()V 7: athrow 8: return Exception table: fromtotarget type 048Class java/lang/Throwable StackMapTable: number_of_entries = 2 frame at instruction 3 local = [UNINITIALIZED_THIS] stack = [ class java/lang/Throwable ] frame at instruction 8 locals = [TOP] stack = [ class java/lang/Throwable ] ---------------------------------------------------------------------------
我們可以將區域性變數的最大數目和堆疊的最大尺寸都設定為2,以滿足第1個條件。此外,第3行程式碼處,當前幀會將“UNINITIALIZED_THIS”設定為true,以滿足第2個條件。最後,未初始化的區域性變數不會用於“athrow”指令的目標運算元(第8行),因為區域性變數的第一個元素被初始化為“TOP”,這樣第3個條件也能得到滿足。
請注意,這些程式碼位於try/catch語句塊中,以便通過函式is_assignable_to()將“is_exception_handler”設定為“true”。
此外,還需要注意的是,該位元組碼都位於建構函式(位元組碼形式的<init>()
)中。要想將標誌“UNINITIALIZED_THIS”設定為true,必須如此。
我們現在已經知道,安全分析人員能夠構造出返回其自身尚未被初始化的物件的位元組碼了。乍一看,可能很難看出這種物件是如何供安全分析人員使用的。但是,通過仔細觀察就會發現,所需的類可以實現為一個系統類的子類,可以在不呼叫超類的建構函式super.<init>()
的情況下完成初始化。這個類可用於例項化因建構函式是私有的或包含許可權檢查而無法由不受信任的程式碼例項化的那些公共系統類。下一步是尋找含有安全分析人員“感興趣的”功能的類。這樣做的目的是,將所有功能組合在一起,以便能夠在沙箱環境中執行任意程式碼,從而繞過沙箱。然而,尋找有用的類本身就是一項非常複雜的任務。
具體而言,我們面臨著以下挑戰。
挑戰1:到哪裡尋找助手程式碼
JRE提供了許多包含JCL(Java類庫)類的jar檔案。這些類作為_trusted_類進行載入,並且可以在構造漏洞利用程式碼時使用。當前,有越來越多的類被標記為“restricted”,這意味著_untrusted_程式碼將無法直接例項化它們——對於安全分析人員來說,這是非常不幸的;但是對於Java使用者來說,這又是非常幸運的。在1.6.0_01版本中,訪問許可權為restricted的包的數量只有1個,到1.8.0_121版本釋出時,這種型別的包的數量已經變為47個。這意味著安全分析人員在構建漏洞利用程式碼時無法直接使用的程式碼的百分比,從1.6.0_01版本升級到1.8.0_121版本的過程中,已經從20%提升到54%了。
挑戰2:欄位可能未初始化
如果沒有適當的許可權,通常無法例項化新的類載入器。在建構函式中接受檢查的_ClassLoader_類的許可權,看起來似乎是一個不錯的目標。
藉助於CVE-2017-3289漏洞,我們確實可以在沒有相應許可權的情況下例項化新的類載入器,因為建構函式程式碼——以及許可權檢查程式碼——不會被執行。但是,由於繞過了建構函式,因此,這時會使用預設值來初始化各個欄位(例如,對於整數來說,將被初始化為0;對於引用來說,將被初始化為null)。所以,這可能導致某些問題:我們感興趣的方法通常是允許為定義的新類賦予全部許可權的那些方法,但是在這種情況下,這些方法都無法正常執行,因為程式碼將嘗試解除對未正確初始化的欄位的引用。在手動檢查之後,我們發現似乎很難繞過欄位的解引用,因為所有路徑都是通過該指令來解除對非初始化欄位的引用的。這樣看來,利用_ClassLoader_似乎是一個死衚衕。當利用CVE-2017-3289中的漏洞時,非初始化欄位是一個主要挑戰:除了要求目標類的訪問許可權是public、非final和非restricted之外,其感興趣的方法也不應該執行撤銷對未初始化的欄位的引用的方法。
對於Java version 1.8.0 update 112來說,我們還沒有找到有用的助手程式碼。為了闡明CVE-2017-3289漏洞的形成機制,我們將展示用於利用編號為0422和0431的漏洞的助手程式碼。這兩個漏洞依賴於MBeanInstantiator ,該類定義了可以載入任意類的方法,即findClass()。類_MBeanInstantiator_只提供了私有建構函式,因此無法直接進行例項化。
最初,這些漏洞都是通過_JmxMBeanServer_來建立_MBeanInstantiator_的例項。這裡,我們將證明,安全分析人員可以直接子類化MBeanInstantiator ,並利用編號為3289的漏洞來獲取它的例項。
用於例項化_MBeanInstantiator_的原始助手程式碼依賴於JmxMBeanServer ,具體如下所示:
--------------------------------------------------------------------------- 1: JmxMBeanServerBuilder serverBuilder = new JmxMBeanServerBuilder(); 2: JmxMBeanServer server = 3:(JmxMBeanServer) serverBuilder.newMBeanServer("", null, null); 4: MBeanInstantiator instantiator = server.getMBeanInstantiator(); ---------------------------------------------------------------------------
例項化_MBeanInstantiator_的程式碼利用了CVE-2017-3289漏洞:
--------------------------------------------------------------------------- 1: public class PoCMBeanInstantiator extends java.lang.Object { 2:public PoCMBeanInstantiator(ModifiableClassLoaderRepository clr) { 3:throw new RuntimeException(); 4:} 5: 6:public static Object get() { 7:return new PoCMBeanInstantiator(null); 8:} 9: } ---------------------------------------------------------------------------
請注意,由於_MBeanInstantiator_沒有任何公共建構函式,_PoCMBeanInstantiator_必須在原始碼中擴充套件一個虛擬類,在我們的示例中為java.lang.Object
。我們將通過ASM [28]位元組碼操作庫,把_PoCMBeanInstantiator_的超類改為MBeanInstantiator
。此外,我們還將使用ASM來修改建構函式的位元組碼,以繞過對super.<init>(*)
的呼叫。
自Java 1.7.0 update 13版本以來,Oracle已將_com.sun.jmx._新增為受限程式包。類_MBeanInstantiator_就位於這個程式包中,因此,我們無法在更高版本的Java中繼續使用該助手程式碼。
出乎我們意料之外的是,這個漏洞影響了40多個不同的公開發行版本。Java 7的所有版本,包括從update 0到update 80,都含有這個漏洞。從update 5到update 112的所有Java 8版本也會受到該漏洞的影響。不過,Java 6版本並沒有受到該漏洞的影響。
通過檢查Java 6 update 43發行版的位元組碼驗證器與Java 7 update 0發行版的原始碼,我們發現主要的區別對應於上面提供的補丁的逆操作。
這意味著堆疊幀可分配給建構函式中異常處理程式內的目標堆疊幀的條件已被削弱。diff中的註釋表明,這個新程式碼是應7020118號請求[26]而新增的。該請求要求更新位元組碼驗證程式的程式碼,以使NetBeans的分析器(profiler)能夠生成可以覆蓋建構函式的全部程式碼的處理程式。
這個漏洞已經通過收緊約束條件得到了修復,只有滿足了這個加強版的約束條件,當前堆疊幀(位於try/catch程式碼塊中的建構函式中)才可以分配給目標堆疊幀。這樣就能有效地防止位元組碼從建構函式返回未初始化的“this”物件了。
據我們所知,Java至少有三個已經公開的_uninitialized instance_漏洞。其中,第1個漏洞是本文介紹的CVE-2017-3289。第2個漏洞於2002年被發現,具體見參考文獻[29]。同時,該文獻的作者還利用了位元組碼驗證器中的漏洞,該漏洞的作用是讓Java平臺無法呼叫超類的建構函式。但是,利用這些漏洞時,無法開發出能夠實現沙箱的完全逃逸的利用程式碼。但是,它們能夠可以用來訪問網路並將檔案讀寫入磁碟。第3個漏洞是普林斯頓的一個研究小組於1996年發現的,具體見參考文獻[30]。同樣,這個安全問題也是位於位元組碼驗證器中。它允許建構函式捕獲呼叫super()時丟擲的異常,並返回部分初始化的物件。請注意,利用該漏洞發動攻擊時,ClassLoader類沒有任何例項變數。因此,利用該漏洞來例項化類載入器的時候,能夠獲得一個完全初始化的類載入器,可以在其上呼叫任何方法。
——[ 4.2.3 -討論
這個漏洞的根本原因是對C/C++編寫的位元組碼驗證程式碼的修改,而原來驗證程式碼的作用是,保證安全分析人員構造出的Java位元組碼無法繞過對子類建構函式中的super()的呼叫。但是,該漏洞直接違反了虛擬機器的相關規範[21]。
但是,如果沒有合適的_helper_程式碼,這個漏洞將毫無用處。不過,Oracle已經開發了一款靜態分析工具,專門用於查詢危險的gadget,並將其列入黑名單[31]。這使得安全分析人員在開發用於繞過沙箱的漏洞利用程式的時候,難度更大了。實際上,我們只發現了能夠與舊版JVM配套使用的gadget。由於它們已被列入最新版本的黑名單,因此,這種攻擊方法已經失效了。
然而,即使可以使用靜態分析工具進行防禦,但是仍然面臨兩個問題:(1)可能會引發許多假正例,這使得識別真正危險的gadget變得更加困難,並且(2)可能導致許多假負例,因為它無法模擬語言的所有特性,比如反射和JNI,因此,這種防禦方式還不夠健全。
小結
在本文中,我們為讀者詳細介紹了例項未初始化漏洞,在本文中,我們將繼續為讀者介紹。在下一篇文章中,我們將繼續為讀者介紹更多精彩內容,敬請期待。
本文轉載自:先知社群