原文:https://www.virusbtn.com/virusbulletin/archive/2015/03/vb201503-dylib-hijacking
作者:Patrick Wardleku
翻譯水平有限,請理解請指正
DLL劫持是一項廣為人知的攻擊技術,一直以來被認為只會影響到Windows系統。然而本文將會介紹OS X系統同樣存在著動態鏈接庫劫持。通過利用OS X動態庫loader的未文檔化的技術和幾種特性,攻擊者將精心制作的包含惡意代碼的動態鏈接庫加載進存在漏洞的程序中。通過這種方法,攻擊者可以實現多種惡意行為,包括:隱秘駐留,進程加載時注入,繞過安全防護軟件,當然還包括Gatekeeper的繞過(提供遠程注入的機會)。因為攻擊者利用的是操作系統提供的合法函數調用,所以只能盡力防御,不可能被完全修補。本文將要提供用于發現存在漏洞二進制文件的技術和工具,同時也可以用來探測是否有劫持已經發生。
在介紹OS X平臺下的dynamic library(dylib)劫持攻擊細節之前,我們先介紹下Windows平臺下的dynamic link library(DLL)劫持。因為這兩種攻擊在概念上很相似。通過學習成熟的Windows平臺攻擊方法可以幫助理解OS X平臺下的方法。
微軟給出的DLL劫持定義是: “當一個程序動態加載一個DLL時,如果沒有給出完整的路徑,Windows系統會試著在默認的一些目錄下搜索該DLL。如果一個攻擊者獲得了其中一個目錄的控制權,他就可以讓程序加載一個惡意的DLL文件來代替原來的DLL。”【1】
需要指出的是,Windows加載器的默認搜索路徑先搜索一些特定目錄(如程序所在目錄或當前工作目錄),再搜索Windows系統目錄。這種處理策略導致了漏洞的產生。比如,一個程序試圖加載一個沒有完整路徑的系統庫,只給出了名字。在這時,一個攻擊者在之前的搜索目錄中植入了一個惡意同名DLL。這樣Windows加載器會先發現惡意的DLL而不是原來的合法DLL,并且將其加載到漏洞程序中。
下面的圖1和圖2進行了說明,一個存在漏洞的程序被一個惡意DLL劫持了。該惡意DLL放在當前工作目錄中。
圖1 ?加載正常系統DLL
圖2 加載攻擊者的惡意DLL
DLL劫持攻擊最初形成廣泛影響是在2010年,然后迅速的得到了媒體和黑客的關注。起了一些如“二進制植入”,“不安全庫加載”,“DLL預制攻擊”等名字。發現該漏洞的人被廣泛認為是H.D.Moore【2】,【3】。然而NSA是最先發現這個漏洞的,早在Moore發現的12年前,1998年,在NSA的非機密文件‘Windows NT Security Guidelines'中,NSA就描述DLL劫持漏洞并提出了警告:
'攻擊者無法插入一個虛假的同名DLL在搜索器搜索到合法的DLL之前,這是非常重要的’【4】
對于一個攻擊者來說,DLL劫持可以使用于多種場合。比如,攻擊者可以讓惡意庫安靜的啟動加載(不改變任何注冊信息和其他系統組件),權限可以得到提升,甚至進行遠程感染。
惡意軟件作者迅速的認識到了DLL劫持的優勢。在一篇博客文章”What the fxsst?“【5】里面,Mandiant公司的研究者描述了他們怎么發現了大量不同不相關的惡意軟件樣本都叫‘fxsst.dll’。通過進一步的研究,他們發現這些樣本都是利用了一個存在于Windows shell(Explorer.exe)里面的DLL劫持漏洞。該漏洞提供了一個隱秘駐留系統的方法。具體原因是,因為Explorer.exe是安裝在C:\Windows目錄下,在這個目錄下植入一個名為fxsst.dll的惡意動態庫文件,將會成功讓惡意dll駐留在系統,因為加載器會在搜索真正fxsst.dll存在的系統目錄之前搜索這個程序目錄。
另一個惡意軟件利用DLL劫持技術的例子可以在泄露的銀行木馬‘Carberp’]【6】中找到。源代碼顯示該惡意軟件是通過sysprep.exe(圖3)的DLL劫持來繞過UAC的。這個程序是一個自動提權的進程,即不需要任何UAC請求就可以獲得更高的權限。不幸的是,它存在著DLL劫持漏洞,被攻擊者利用來加載惡意的DLL(cryptbase.dll)【7】。
圖3 Carberp利用DLL劫持來繞過UAC
最近,DLL劫持在Windows平臺上已經很少見了。微軟迅速采取應對攻擊,修補易受攻擊的應用程序,并詳細說明如何避免這個問題(例如,對需要加載的DLL指定一個絕對路徑)【8】。然而,操作系統級的防御措施還是需要的,比如通過safedllsearchmode或cwdillegalindllsearch注冊表項啟用,防止大多數DLL劫持。
曾經以為動態庫劫持只一個Windows平臺獨有的問題。但是,在2010年,一個機敏的StackOverflow用戶指出來,‘任何允許動態鏈接外部庫的操作系統在理論上都是有漏洞的’【9】。直到2015年他才證明了這個觀點的正確性。本文將揭示在OS X平臺下具有毀滅性的動態庫劫持攻擊技術。
本文研究的目的是揭示OS X平臺是否存在動態庫攻擊的漏洞。進一步說就是,本研究需要回答如下的問題:在OS X平臺下,是否攻擊者可以植入一個可以自動被加載器加載到漏洞程序的惡意動態庫。假設OS X平臺下的劫持攻擊可以和Windows平臺下的DLL劫持一樣,能夠讓攻擊者實現大量的攻擊目的。比如:隱秘駐留,加載時注入,安全軟件繞過,也許還有可以遠程感染。
需要指出的是,本研究是基于一些限制條件的。第一,成功被限制在不允許對系統做出任何修改情況下,除了創建文件(或者是文件夾)。換一個方式說,本研究忽略了那些要求對特殊二進制文件進行修改(例如補丁)或修改系統配置文件(比如‘auto-run’plists等)的攻擊環境要求。這樣的攻擊都被廣為人知,容易去防止和探測,所以被忽略掉。本研究試圖尋找到一個獨立于用戶環境的劫持方法。OS X也提供了各種合法的方式加載動態庫,可以讓加載器強制自動加載惡意庫到目標進程。這些方法,比如設置DYLD_INSERT_LIBRARIES環境變量,是用戶特定的,也是眾所周知的,容易被檢測到。所以我們也不進行研究,直接忽略了。
本研究從研究分析OS X的動態連接器和加載器開始:dyld。這個二進制文件在/usr/bin目錄下,提供標準的加載器和連接器的功能,尋找,加載,連接動態庫。
因為蘋果公司曾讓dyld開源過【10】,所以分析起來就很簡單直接。比如,通過閱讀源代碼可以很好的理解dyld作為一個可執行程序被加載的過程,包括它所依賴的庫加載和連接的過程。下面幾點簡要的總結了dyld開始的步驟(主要關注與本文研究的相關的)
理解了基礎的dyld初始加載邏輯,研究重心就轉移到尋找可以進行dylib劫持的邏輯上來了。特別的,研究組感興趣的是加載器在有沒有在沒有找到一個dylib時,卻沒有報錯的代碼,還包括在多個位置尋找dylib的代碼。如果任何一個場景在加載器中發現了,我們就有希望來進行OS X的dylib劫持攻擊。
我們先研究了第一個方案,在此方案中,我們假設,一個加載器可以處理dylib沒有找到的情況,一個攻擊者(可以發現這種情況)可以在該位置放置一個惡意的dylib。從而,加載器就會找到放置的dylib并且不經檢驗的加載攻擊者的惡意代碼。
回顧之前,加載器調用ImageLoader類的recursiveLoadLibraries()方法來尋找和加載所有的需求的庫。如圖4所示,加載代碼中處理dylib加載失敗的代碼是被try/catch塊包含的。
圖4 dyilb加載失敗時的處理邏輯
不出所料,處理邏輯會在加載庫失敗的時候拋出一個異常(含有一條信息)。有趣的是,這只有當一個名叫‘required’的變量被設置為true時,才會拋出異常。此外,源代碼的注釋中說明了在加載‘weak’庫失敗是可以的。這就說明在一些情況下,加載器加載不了一些庫也是會繼續正常工作的---||太棒了!
深入分析加載器程序的源代碼,找到是在哪里進行‘required’變量的設置。得到結果為,ImageLoaderMacho類的doGetDependentLibraries()方法對加載命令(下面會進行描述)進行語法解析,并且通過加載命令的LC_LOAD_WEAK_DYLIB標識位來給該變量進行賦值。
加載命令是Mach-O文件格式(OS X的原生二進制文件格式)必有的組成部分。在文件中緊接著Mach-O頭,它提供了不同的命令給加載器。例如,加載命令可以用來說明二進制文
圖5 設置required變量
件在內存中的布局形式,主線程的初始執行狀態,和所需動態庫的具體信息。可以通過工具查看編譯好二進制文件的加載命令信息。比如MachOView【11】,或者/usr/bin/otool(使用-l參數)。(參見圖6)
圖5中的代碼顯示了加載器依次處理所有加載命令的過程,尋找所有聲明倒入的動態庫。這些加載命令的定義可以在mach-o/loader.h文件中找到。
圖6 通過MachOView查看Calculator.app的加載命令
圖7 LC_LOAD_*加載命令的格式
對應可執行程序需要每個動態鏈接庫,程序頭都包含一個LC_LOAD_*加載命令(LC_LOAD_DYLIB,LC_LOAD_WEAK_DYLIB等)。像圖4,圖5中加載代碼顯示的一樣,LC_LOAD_DYLIB加載命令聲明了一個所需的動態庫,通過LC_LOAD_WEAK_DYLIB聲明的庫就是可選的(weak)。前面一種情況(LC_LOAD_DYLIB),如果所需庫沒有被找到就會拋出一個異常,加載器就會放棄并結束該進程。但是如果是后面的情況(LC_LOAD_WEAK_DYLIB),動態庫是可選的,如果沒有發現也并沒有聲明影響。主程序將會繼續執行。
圖8 嘗試加載弱(weak)庫
該加載器邏輯上滿足了第一個假設劫持場景的條件,因此,可以對OS X平臺進行動態庫劫持攻擊。換句話說,如圖9所示,如果一個聲明的弱引用庫沒有找到,攻擊者就可以在該位置上放置一個惡意的動態庫文件。然后,加載器就會找到攻擊者的動態庫并且加載惡意代碼到漏洞程序的進程空間。
圖9 通過惡意的‘weak’動態庫進行劫持
此前說的另外一種劫持攻擊是假設一個加載器在多個地方尋找動態庫。在這種情況下,假設攻擊者可以將一個惡意動態庫放置在其中一個搜尋目錄(合法的動態庫在其他地方)。讓加載器先找到攻擊者的惡意動態庫,并且不經檢查直接加載攻擊者的惡意動態庫。
在OS X平臺上,像LC_LOAD_DYLIB之類的加載命令總是將動態庫的路徑給出(而Windows平臺只是給出動態庫的名字)。正因為給出了路徑,dyld加載器就不需要搜索不同的目錄來尋找動態庫,而是直接到指定的目錄加載dylib。然而在對dyld源代碼進行分析之后發現,dyld在其中一種情況下并沒有進行如此的處理。
如圖10所示,在dyld.cpp中的loadPhase3()函數中,有一些有趣的處理邏輯。
圖10 加載依賴‘rpath’的庫
Dyld會循環迭代rp->paths向量來動態構建路徑(存貯在‘newpath’變量中),之后調用loadPhase4()函數。這樣的做法就滿足了第二種劫持場景的要求(dyld在多個位置尋找同一dylib),當然還需要進行一下路徑順序檢查。
圖10中,[email protected]??果文檔,這是一個特別的加載關鍵字(在OS X 105,Leopard中有介紹),用來定義一個動態庫為‘run-path-dependent library’【12】。蘋果解釋run-path-denpendent library是一種在創建時完整安裝路徑并不知道的依賴庫。其他文檔【13】和【14】等提供了更多的細節,解釋了這種庫所起到的作用,[email protected]:‘frameworks and dynamic libraries to finally be built only once and be used for both system-wide installation and embedding without changes to their install names, and allowing applications to provide alternate locations for a given library, or even override the location specified for a deeply embedded library’【14】。
通過這種特性,軟件開發者可以更為簡單的部署復雜的程序,但同時也為動態庫劫持提供了方便。一個可執行程序為了使用run-path-dependent library,需要提供給加載器運行時搜索路徑列表,加載器在加載時再來尋找這些庫【12】。在dyld的代碼的很多地方都發現了這樣的代碼。包括圖10里面給出的代碼片段。
因為run-path-dependent library是相對新的概念,有些不為人所知,提供一個例子來說明應該是很有必要的,例子包含了run-path-dependent library和使用該庫的例子程序。
一個run-path-dependent [email protected]ylib。如圖11所示,在Xcode中創建這樣一個動態庫只需簡單的將dylib的安[email protected]
圖11 建立一個 run-path-dependent library?
當run-path-dependent library編譯成功之后,檢查LC_ID_DYLIB(包含了該dylib的標識信息)加載命令顯示的dylib運行時路徑。特別是,LC_ID_DYLIB加載命令中‘name’項,顯示了該dylib的文件名(rpathLib.framework/ Versions/A/rpathLib)[email protected]
構建一個加載run-path-dependent library的程序也是非常直接簡單的。首先,將run-path-dependent library添加到Xcode的Libraries列表里面。然后,將run-path搜尋路徑添加到‘Runpath Search Paths’列表。最后,這些搜尋目錄將會在動態加載器加載庫時被搜索到,以確定run-path-dependent library的具體目錄。
圖13 @rpath庫的鏈接設置和聲明run path搜索路徑
一旦應用程序被建立,dumping該程序的加載命令會顯示一些與run-path依賴庫相關的各種命令。一個標準的LC_LOAD_DYLIB加載命令會為需要加載的run-path-dependent dylib的關聯依賴提供信息,如圖14所示。
圖14 @rpath庫的依賴信息
在圖14中,注意到安裝名name項指向run-path-dependent [email protected],并和圖12中的run-path-dependent dylib的LC_ID_DYLIB命令的name值是一樣的。該程序包含的與run-path-dependent dylib相關的LC_LOAD_DYLIB加載命令告訴加載器:‘我需要rapthLib dylib,但是在組建時,我不知道它的具體安裝位置。請用我包含的run-path搜索路徑找到并加載它。’
我們之前在Xcode中將run-path搜索路徑添加進‘Runpath Search Paths’表單中。這些搜索路徑會在程序中生成LC_RPATH加載命令,每條路徑對應一個加載命令。查看編譯好的程序可以發現包含的LC_RPATH加載命令,如圖15所示。
圖15 加載命令中的run-path搜索路徑
通過對run-path-dependent dylib和加載它的程序的理解,我們就可以更加簡單的去理解dyld的源代碼中負責加載動態庫的那部分代碼。
當一個程序啟動時,dyld將會解析程序的LC_LOAD_*加載命令,加載和連接所有依賴的dylib。針對處理run-path-dependent libraries,dyld分為兩個步驟完成:先提取所有包含的run-path搜索路徑,然后再通過搜索列表里的路徑來尋找和加載所有的run-path-dependent libraries。
為了提取所有的run-path搜索路徑,dyld調用ImageLoader類的getRpaths()方法。該方法(被recursiveLoadLibraries()方法調用)簡單的解析程序中所有的LC_RPATH加載命令。對應每個這種加載命令,dyld提取出run-path搜索路徑并添加到一個向量中(例如:一個表),如圖16所示。
圖16 提取并保存所有內置的run-path搜索路徑
有了run-path搜索路徑列表,dyld就可以找到所有依賴的run-path-dependent libraries了。這部分邏輯代碼在dyld.cpp的loadPhase3()函數中。如圖17所示,[email protected]?字。如果有,dyld就循環迭代run-path搜索表,[email protected],然后嘗試從新生成的路徑加載dylib。
圖17 搜索run-path搜索目錄,[email protected]
重要的一點是dyld搜索的路徑順序是確定的,是符合LC_RPATH加載命令的順序的。如圖17中顯示的代碼片段顯示,搜索循環會不停尋找,直到找到目標dylib或者是所有的路徑都搜索了。
圖18,圖解了搜索過程。可以看到dyld搜索了不同的run-path搜索路徑,為了找到需要的run-path-denpendent dylib。注意在這個例子中,目標dylib是在第二個搜索目錄中找到的。
圖18 Dyld搜索多個run-path搜索目錄
總結一下到此的發現:一個OS X系統是可以被劫持攻擊的,只要任何程序存在以下的任意條件: 1,包含一個LC_LOAD_WEAK_DYLIB加載命令,但是相關的dylib并不存在。 2,同時包含一個LC_LOAD*_DYLIB加載命令指向一個run-path-denpendent library([email protected]')和多個LC_RPATH加載命令。并且run-path-denpendent library沒有在第一個run-path搜索目錄中。
本文的余下部分會先講述一個完整的dylib劫持攻擊,然后給出幾個不同的攻擊(駐留,加載時劫持,遠程注入等),最后總結下如何防御此類攻擊。
為了幫助讀者更好的理解dylib劫持攻擊,我們會盡量給出劫持攻擊的細節,包括嘗試攻擊,遇到的錯誤,到最后的成功。有了這些知識的幫助,就可以更容易的理解自動攻擊,攻擊場景識別,和如何防御。
回顧之前描述的例子程序('rPathApp.app')。我們用來解釋連接run-path-denpendent dylib的。這個程序將會是我們劫持攻擊的目標。
dylib劫持攻擊的對象只能是存在漏洞的程序(滿足前面講述的兩個劫持條件之一的程序)。因為本例子程序(rPathApp.app)需要連接一個run-path-dependent dylib,它也許就滿足上面第二個條件。最簡單的檢測方式就是開啟加載器的debug logging功能,然后在命令行簡單的運行該程序。為了開啟這種logging,需要設置DYLD_PRINT_[email protected][email protected]?現漏洞(例如:第一個擴展指向了不存在的dylib)如圖20所示。
圖20 存在漏洞的測試程序rPathApp
圖20展示了加載器第一次尋找目標dylib時,在指定位置沒有發現。和圖19中顯示的一樣,在這種情況下,攻擊者可以部署一個惡意的dylib到剛才第一次搜索的路徑,之后加載器會加載惡意庫。
我們創建了一個簡單的dylib來扮演惡意的劫持庫。為了能夠在加載時自動執行,該dylib實現了一個構造函數。該構造函數在dylib成功加載后會自動執行。這是一個很好的特性,因為一般的dylib代碼不會執行,直到主程序調用它的某個導出函數。
圖21 一個dylib的構造函數將會自動執行
編譯組建完成后,將dylib重命名,改成目標庫的名字:rpathlib。接下來,創建需要的目錄結構(Library/One/rpathLib.framework/Versions/A/)并將惡意的dylib拷貝進該目錄。這就保證了無論何時程序啟動,dyld在搜索run-path-denpendent dylib時會找到劫持dylib。
圖22 惡意dylib被放置在第一個run-path搜索目錄中
不幸的是,這一次劫持嘗試失敗了。程序意外的崩潰了。見圖23。
圖23 成功解析路徑,然后崩潰
雖然失敗了,但是好消息就是,加載器找到了并嘗試加載劫持dylib(看圖23中的'RPATH successful expansion…'日志消息)。雖然程序崩潰了,但是加載器還是拋出了一條詳細的異常信息。這條異常看起來是自解釋的:劫持庫的版本和要求的版本不同。重新研究加載器的源代碼,找到了拋出這條異常的代碼。見圖24。
圖24 Dyld提取和比較合適的版本號
可以看到,加載器會調用doGetLibraryInfo()方法從被加載庫中的LC_ID_DYLIB加載命令中提取兼容和當前的版本號。提取出來的兼容版本號('minVersion')然后在和程序要求的版本進行對比。如果版本號太低,一個不兼容的異常就會被拋出。
解決此兼容問題也不難,只需要通過在Xcode中更新版本號,重新編譯下就行。見圖25。
圖25 設置兼容和當前版本號
檢查重新編譯的劫持dylib的LC_ID_DYLIB加載命令。確認已經更新版本號。見圖26
圖26 兼容版本號和當前版本號
更新版本號后的劫持dylib又被拷貝進程序的第一個run-path搜索目錄。重啟漏洞程序,顯示加載器找到了劫持dylib并且嘗試加載。可是雖然現在dylib版本兼容。但是一個新的異常被拋出,程序又一次崩潰。見圖27。
圖27 ‘符號沒有找到’異常
又一次,異常給出的解釋清晰說明了加載器為什么拋出異常。程序連接動態庫的目的就是獲得動態庫導出的功能(比如:函數,對象等)。一旦被需求的dylib被加載進內存,加載器就會嘗試解析(通過導出符號)依賴庫試圖導出的功能對象。如果功能對象未發現就會連接失敗,連接進程就會終止,導致主程序崩潰。
有幾種方法可以確保劫持dylib導出正確的符號表,這樣才能完整的進行連接。一個簡單的方法就是劫持dylib直接仿造目標dylib的導出信息。也許這樣就可以成功了,看起來有點復雜并且不同的dylib各有特點(比如,攻擊另外一個dylib需要另外的導出信息)。一個更優雅的辦法是簡單的讓連接器去別的地方尋則它要求的符號。當然別的地方就是指合法的dylib。在這個場景中,劫持dylib將簡單的扮演一個代理或者是一個‘re-exporter’dylib,加載器將會跟隨它的重導出指令,沒有連接錯誤會被拋出。
圖28 重導出合法的dylib
需要付出一些努力,才能讓重導出的庫完美工作。第一步是回到Xcode,添加多個鏈接的flags到劫持dylib項目。這些flags包括“-Xlinker ',' reexport_library ',然后還有到包含漏洞程序真正需要導出接口的目標庫的路徑。
圖29 要求的鏈接flag,來實現re-exporting
這些鏈接flags會生成一個內置的LC_REEXPORT_DYLIB加載命令。其中包含到目標dylib的路徑。見圖30。
圖30 內置的LC_REEXPORT_DYLIB加載命令
然而,事情并非如此簡單。因為劫持dylib重導出目標是一個run-path-denpendent library。LC_REEXPORT_DYLIB(從合法的dylib的LC_ID_DYLIB加載命令中導出)[email protected],因為不像LC_LOAD*_DYLIB加載命令,dyld不會解析LC_REEXPORT_DYLIB加載命令中的run-path-denpendent路徑。換句話說,[email protected][email protected]?的。
[email protected],提供目標庫的完整路徑給LC_REEXPORT_DYLIB加載命令。這需要借助一款蘋果開發者工具:install_name_tool。來更新LC_REEXPORT_DYLIB加載命令中的install name。這個工具執行時,用-change選項,隨后是現存的name(LC_REEXPORT_DYLIB中的),新的name,劫持dylib的路徑。見圖31。
圖31 使用installl_tool_name來更新內置的name
在LC_REEXPORT_DYLIB加載命令正確更新后,劫持dylib被重新拷貝到主程序的第一個run-path搜索目錄,重啟程序。如圖32,最終成功執行。
圖32 成功劫持又漏洞的程序
總結一下:因為rPathApp程序連接一個run-path-denpendent庫,而這個庫在第一個run-path搜索目錄中沒有找到,所有存在了dylib劫持漏洞。植入一個特殊兼容的惡意dylib在第一個搜索目錄中會導致在每一次程序執行時,加載器都會盲目的加載這個惡意dylib。因為這個惡意dylib擁有正確的版本信息,同時重導出了合法目標dylib的所有符號,所有需要的符號都能解決,因此保證了程序的功能不會受損。