知物由學 | SO加固如何提升Android應用的安全性?

網易易盾發表於2021-04-25

傳統Android App的程式碼保護分為DEX加固和SO加固。從反編譯的成本來看彙編程式碼強度要比SMALI程式碼要高,所以DEX保護程式碼一般都使用C/C++實現。DEX加固在從DEX檔案到指令虛擬化都已形成較為成熟的方案,而SO的加固還有很大的提升空間。

SO加固分有原始碼SO加固和無原始碼SO加固。有原始碼SO加固:需要特定的程式碼或者編譯器支援,可對程式碼進行加密,混淆甚至虛擬化保護,開發人員可透過主動插入混淆程式碼到原始碼內或透過。無原始碼SO加固:透過殼對SO進行保護,保護方式以程式碼加密的形式居多,混淆和虛擬化還未成熟,相容性和效能難以保證。

由於Android平臺的效能限制,SO層做虛擬化對效能影響較大,遠不及Windows平臺的成熟,大部分SO加固還是以程式碼加密為主。本文旨在透過講解SO載入流程,分享SO加固的原理,為開發加固功能的技術人員提供一些解決加固問題的思路,也讓開發者更明確加固目的,不會因追求加固效果而盲目加固所有SO檔案。

01 SO載入原理

SO屬於ELF格式檔案,ELF格式檔案提供兩類檢視訪問檔案結構,連結檢視和執行檢視。

知物由學 | SO加固如何提升Android應用的安全性?

連結檢視是以Section為單位訪問各段資料,一般程式編譯連結過程中需要會使用Section資訊去讀取SO資訊,strip工具也會根據Section資訊對ELF檔案進行裁剪。

執行檢視是以程式載入執行過程中的資訊訪問方式,大部分Section在破壞的情況下不會影響SO檔案的載入。SO加固就是根據SO執行流程以及所依賴的資料結構進行拆解,加密和重組的。

SO載入過程簡單描述就是將SO中的程式碼和資料按照編譯時預定的結構,載入到記憶體,然後進行重定位,即修復必要的資料資訊,修復完成後就能保證所有程式碼的正常執行。

知物由學 | SO加固如何提升Android應用的安全性?

SO載入流程如下:

Linker優先從記憶體中查詢已載入的SO,如果SO已載入,則直接返回handle。

如SO沒有載入需要新建soinfo結構體,並將SO載入到記憶體。

執行預載入prelink_image,讀取SO中dynamic欄位中的內容,初始化重定位需要的資料。

執行link_image,修復記憶體中的資料,完成重定位。

執行SO中的Init以及Initarray中的函式。

從載入流程看,可以得出以下幾點:

1.重定位完成之前,SO中的程式碼不會主動執行,程式碼都在Linker中執行。

2.最先執行的程式碼為Init,其次是Init_array。

因存在這一特性,任何做過加密的SO,只要在SO內函式執行函式前對加密資料進行修復,就能達到加固的目的。其中大部分加固都是利用Init或Init_arrray中的函式來進行解密,也可以在任何一個確定的時機完成解密,如Android下可利用JNI_OnLoad函式,但這種方式只適用於Android下特殊的SO。

02 SO加固方法 

2.1 Section加密

知物由學 | SO加固如何提升Android應用的安全性?

Section加密的主要分兩步:

1. 將需加密的程式碼放入特定Section,在加密程式碼被執行前執行解密邏輯。

2. 編譯後的檔案,使用工具對特定Section進行加密,即對Section區間的內容加密。

這種加密方式適用於開發者對自實現的程式碼進行保護,因為Section資訊以及解密程式碼都需要開發者主動新增和編譯,需要原始碼支援。

2.2 UPX以及類UPX的SO加固

開源的SO加固較為常見的是UPX,很多人會根據UPX原始碼修改一些特徵改成自己的版本,避免直接被脫殼。

UPX加固SO的做法是隻加固程式碼段,資料段和重定位相關的結構都保留在檔案中,利用原SO的重定位資訊完成SO重定位,透過插入的INIT節對程式碼進行解密,本質上和Section加密類似,只是放大了加密範圍,並新增修改Init節的操作。透過命令readelf -d sopath,仍可以看到原SO內的資料和原SO一致,但Section已被破壞無法檢視。

知物由學 | SO加固如何提升Android應用的安全性?

2.3 自實現Linker方式加固

自實現Linker的方案目前來說是加固的主要手段。因重定位過程完全自己實現,這樣既可以加密SO中的程式碼,也可以加固SO中的資料。由於自實現Linker,SO結構可以完全破壞和自定義,可以防止被dump出完整的SO。加固後SO靜態分析只能看到殼的結構,對於原SO的結構完全隱藏。

知物由學 | SO加固如何提升Android應用的安全性?

2.4 程式碼混淆

有原始碼的程式碼混淆一般可透過插入花指令或透過帶混淆功能的編譯器進行編譯,對生成的程式碼進行混淆。無原始碼的程式碼混淆需要藉助殼,對原SO的指令進行抽離,然後對抽離的程式碼做混淆轉換。

知物由學 | SO加固如何提升Android應用的安全性?

2.5 VMP

有原始碼的VMP方案一般都是藉助編譯器,在ir層對指令進行虛擬化並插入直譯器。無原始碼的SO加固方案目前還未普及,但原理和有原始碼方案類似,將原指令處理成虛擬資料,並且插入直譯器對虛擬資料解釋執行。由於VMP對效能損耗大,對移動端來說,效能和相容性都需要衡量,實用性不及程式碼混淆。

03 SO加固相容性分析

前面講過SO加固在滿足SO中加密程式碼和資料在被執行使用之前完成解密,理論上相容性就能得到保證。但SO加固並不能完全相容所有情況,只要SO被加密處理,其載入流程和資料格式肯定存在修改,所以在一些特殊流程或者程式碼中有特殊格式校驗的情況下,無法避免地存在相容性問題,但一般正常開發且沒有校驗行為的SO是有辦法做到完美相容的。

在分析載入流程中的相容性問題前需要回顧一下載入流程。載入流程中提到SO最先執行的程式碼為Init,其次是Init_array。這個只是理論層面上的流程,實際情況需要考慮函式過載以及依賴SO的載入流程。Init和Init_array只能說是載入過程中最早能確定執行的程式碼塊。還存在一些可能執行的程式碼,但觸發執行需要特殊條件。以一個簡單的示例程式演示SO中函式在Init前執行的情況。

1. 首先需要一個依賴SO,在Init中呼叫運算子new,將其編譯成libdpend.so。

知物由學 | SO加固如何提升Android應用的安全性?

2. 編寫一個測試SO,構造Init節,過載new運算子,編譯時連結libdepend.so,生成libtest.so。

知物由學 | SO加固如何提升Android應用的安全性?

3. 編寫一個主函式載入libtest.so,編譯後執行結構如下:

知物由學 | SO加固如何提升Android應用的安全性?

可以看到,真實的執行順序先執行過載的new函式,再執行依賴SO的Init函式,最後執行測試SO的Init函式。其根本原因在於重定位的流程是有順序的,符號優先從SO內進行查詢,然後再查詢依賴,這也是函式能實現過載的原因之一。

該類情況在第三方SO中很難預測哪些符號被過載,且執行時間無法確定,類似UPX和一些簡單的程式碼加密方案無法相容這類問題,解決方法可以透過隱藏部分符號繞過過載函式問題,或者藉助延遲載入技術,在函式執行時觸發解密操作。

程式碼校驗格式導致的相容性問題沒有通用的解決方案,因為無法確定校驗的方式是透過讀檔案還是讀記憶體的形式,且校驗內容不確定,一般情況開發者新增校驗的目的就是為了防篡改,和SO加固存在衝突。所以在進行加固時一般建議加固客戶自研的SO,第三方SO的行為存在不確定性,難以保證相容性。

04 總結

以上介紹的SO加固方式各有優劣,下表是對其實現難度、優缺點的總結:

知物由學 | SO加固如何提升Android應用的安全性?

SO加固能很好地保護客戶程式碼,避免被靜態分析。第三方SO加固都是處於無原始碼的環境,加固功能都依賴於殼程式碼的實現。在對抗動態分析時,可配置防除錯功能減少被動態除錯和dump記憶體的可能性。在SO加固時,我們對關鍵程式碼新增一些指令混淆和VMP的處理,即使在攻擊者繞過反除錯的情況下也能最大限度地保護程式碼。


相關文章