[iOS 逆向 13] 代碼混淆

背景

經過逆向工程實踐,可以發現靜態分析在整個過程中是不可缺少的,而且靜態分析工具生成的僞代碼極大地提高了分析效率。想象一下如果沒有靜態分析,實現解除會員限制的過程:連接界面調試器 Reveal,找到目標界面,獲取按鈕地址,打印按鈕的響應事件,獲取響應方法的 C 函數指針,連接 LLDB 給該函數打斷點,但是該函數內有大量的分支語句,每個分支都要通過調試才能判斷是不是確定會員權限的函數,分析“一天”後找到目標函數,編寫 hook 代碼。致命問題是,對部分反調試策略無法應對,因爲不知道在哪打斷點,如果一條一條地執行指令來尋找反調試入口,可以視爲找不到。

如果缺少了界面調試工具和動態調試工具,但有靜態分析的彙編代碼和僞代碼在,只會略微增加分析時間。如果有靜態混淆策略,首先在大型項目中應用的可能性就不大,就算有,因爲系統符號名是不能替換的,所以可以從系統類入手。比如某個行爲會彈窗,就在代碼中搜索 UIAlertAction 之類的字眼,縮小目標範圍,然後利用 IDA Pro 給符號重命名爲有意義的字符串,感覺這個功能就是給名稱混淆準備的。

綜上,增加靜態分析的代碼邏輯複雜度纔是保護 App 的關鍵。那麼有沒有辦法讓反編譯得到的代碼難以閱讀呢?這就要從二進制代碼的源頭入手——在編譯過程中替換指令、增加無用控制流、使代碼扁平化等。因此,需要改變編譯器生成代碼的過程,而 LLVM 架構爲這個思路的實現提供了可能。

LLVM

編譯器架構一般分三個模塊,分別是前端、優化器和後端。

源代碼在預處理過程中展開宏定義,導入頭文件,處理條件編譯指令等,然後開始分詞。分詞過程中識別單詞或者符號的類型並記錄位置,比如一個左括號被識別爲 l_paren + 位置。然後按照語言規則構造語法樹,同時檢驗了語法。遍歷語法樹,將各節點翻譯爲一種中間代碼。以上是編譯器前端的工作,最終輸出中間代碼。對於傳統編譯器,每種語言的編譯器前端生成的中間代碼格式都不相同。前端生成的中間代碼作爲優化器的輸入,優化器從時間、空間上優化代碼,然後輸出給後端。後端根據目標 CPU 支持的指令集、寄存器等信息生成目標文件。

LLVM 編譯器架構與前面傳統的編譯過程不同的是,每種語言的前端輸出的都是同種類型的中間代碼,這個中間代碼像是一種彙編,但在函數聲明上又有一些高級語言的樣子。這樣做的好處是,對 IR 優化的工作可以完全複用。LLVM 架構用 Clang 作爲 C 和類 C 語言的前端;優化器由多個流程(Pass)組成,各個流程對中間表示(IR)也就是中間代碼從不同方面進行優化。LLVM 架構中的 IR 即 Bitcode。

蘋果從 Xcode 7 開始默認使用 LLVM 編譯器架構。在 App 準備上傳時,打包 Archive 版本會把 Bitcode 附加到可執行文件中,查看 Mach-O 文件可以看到多出一個 LLVM 段 __LLVM,__bitcode。 蘋果服務器會對 Bitcode 進一步優化,然後從 Bitcode 重新生成目標文件,因此從 App Store 下載下來的可執行文件和上傳時的可執行文件會不一樣。一些比較舊的第三方庫不支持 LLVM 的 Bitcode,整個項目必須用傳統方式編譯,這時下載下來的可執行文件代碼段會和上傳時的相同。

回到正題,代碼混淆中的改變執行流程、添加垃圾指令等操作,就相當於對代碼進行負優化。而各個 Pass 正是負責對中間代碼的處理、優化,因此我們可以添加自定義 Pass 來實現代碼混淆。

Pass Demo

編譯配置

從 GitHub 下載 LLVM 的源碼,在 lib/Transforms/ 目錄下可以看到 Hello 文件夾,這是 LLVM 自帶的 Pass 入門項目,先模仿一下它。在相同目錄下新建一個 MyDemo 文件夾,裏面添加一個 cpp 文件和 CMakeLists 文件。CMakeLists 文件中使用 add_llvm_loadable_module 函數;在 Transforms 目錄下的 CMakeLists 中添加 add_subdirectory(MyDemo)。然後爲了方便寫代碼、讀源碼,用 CMake 生成 Xcode 項目,cmake ../llvm -G "Xcode"。用 Xcode 打開後,即可找到:
在這裏插入圖片描述
然後設置 Xcode 的編譯 Scheme 爲 MyDemo 的 Target,這樣就可以開始寫代碼了。

編寫 Demo

Pass 基類中提供了一些虛函數,不同的子類通過實現這些函數來提供不同的功能,系統默認實現了一些子類。 Demo 中定義一個類繼承 FunctionPass 就可以操作代碼中的函數了,導入相應的頭文件,Demo Pass 聲明如下:
在這裏插入圖片描述
LLVM 用變量 ID 的引用來識別 Pass,可以賦任意值;MyPass 重載了 runOnFunction,這裏只打印函數名。然後指定使用該 Pass 的命令行參數爲 demo:
static RegisterPass<MyPass> X("demo", "use demo pass", false, false);
開始構建,會生成 MyLLVMDemo.dylib 文件,下面使用這個 dylib。寫一段測試代碼,包含 func 函數 和 main 函數,然後使用 clang -emit-llvm -c 生成 Bitcode 文件。然後在 Xcode Scheme 中選擇 opt 並構建。可以在命令行中使用 opt,但使用 Xcode 可以調試,用 Xcode 給 opt 的 Scheme 添加以下參數: 在這裏插入圖片描述
如果添加 -help 參數,會打印出該 Pass 接收的參數;運行時會輸出函數名 func 和 main,用 Xcode 打斷點,可以調試 Pass 過程:
在這裏插入圖片描述
可以在 opt 的 CMakeLists 中添加 MyPassDemo 的依賴,這樣修改 Pass 代碼後可以只構建 opt 也能讓最新代碼生效。

Obfuscator-LLVM

論文主要介紹了三個 Pass 用於混淆。

  • 指令替換:將操作符替換爲一系列的指令,例如a = b ^ c 替換爲a = (b & !c) | (!b & c)
  • 控制流扁平化:把 if-else 自上而下的結構替換爲 switch 扁平的結構,使邏輯變混亂。
  • 添加無用控制流:在原函數入口導向無用代碼塊,額外經歷一系列無用流程。

現在要讓 Clang 編譯程序時加載以上 Pass,需要重新構建 Clang。由於該論文中使用的是比較舊的 LLVM-4.0,我下載了 LLVM-7.0.1 用於移植實驗,需要執行以下操作:

  • 複製舊項目 lib/Transforms/Obfuscation 目錄到相同位置,裏面是 Pass 代碼;
  • 修改 lib/Transforms 目錄下的 CMakeLists,添加子目錄 Obfuscation;
  • 修改 lib/Transforms 目錄下的 LLVMBuild.txt,添加 Obfuscation;
  • 修改 Transforms/IPO 目錄下的 LLVMBuild.txt,添加 Obfuscation;
  • 修改 Transforms/IPO 目錄下的 PassManagerBuilder.cpp,把舊項目中相同位置關於 Obfuscation 的代碼複製過去。

在 Xcode Scheme 中選擇 Clang 並構建。構建完成後,用其編譯下面的測試程序:
在這裏插入圖片描述
先不添加參數,生成可執行文件後用 IDA Pro 打開,生成的僞代碼如下:
在這裏插入圖片描述
然後添加編譯參數:-mllvm -sub 啓用指令替換;-mllvm -bcf 啓用僞造無用流程;-mllvm -bcf_prob=100 啓用僞造流程時,一個基本塊被生成無用代碼的概率爲 100%。
./clang test.c -o test.o -mllvm -sub -mllvm -bcf -mllvm -bcf_prob=100
生成可執行文件後,同樣用 IDA Pro 反編譯生成僞代碼。可以看到,僅 func 函數就非常冗長,邏輯也不明顯,這種混淆方式的效果比較好,可以極大地增加靜態分析的難度。
在這裏插入圖片描述
在替換 Xcode 編譯器之前,需要加一個 Target,執行cmake -DLLVM_CREATE_XCODE_TOOLCHAIN=ON ../llvm -G "Xcode",這樣 Xcode Scheme 中才有 install-xcode-toolchain,選中並構建。注意硬盤要有 40G 左右的空閒空間,因爲最終生成的工具鏈爲 14G,編譯過程中的文件 25G。生成的工具鏈位於 /usr/local/Toolchains,將其移動到 /Library/Developer/ 目錄。打開 Xcode - Preference - Components,選擇剛剛生成的工具鏈。
在這裏插入圖片描述
此時打開 Xcode 也可以看到當前使用的工具鏈:
在這裏插入圖片描述
用新工具鏈編譯 Xcode 項目,報錯unknown argument: '-index-store-path',需要到項目 Build Settings 關閉 Index-While-Building,因爲這個參數在開源的 LLVM 項目中沒有實現。然後編輯項目的 Compiler Flags,添加編譯參數。在這裏插入圖片描述
我在參數中設置對基本塊僞造無用流程的概率爲 40%,那麼項目的核心功能代碼有六成概率不會被混淆,比較危險。但如果將概率設爲 100%,大量無用代碼勢必減慢程序運行速度,因此需要對不同源文件採用不同的編譯參數。前面提到過用命令行手動構建、打包 App,編寫 makefile 確實可以做到精準控制,但是很麻煩。幸好 Xcode 提供了這個功能,在項目 Build Phase 中可以對源文件指定編譯參數。
在這裏插入圖片描述
這樣就可以實現對核心代碼高度混淆、對不重要的代碼部分混淆或不混淆。還有一個問題,項目中使用的第三方靜態庫,我們沒有庫的源碼,如何進行混淆?對於啓用了 Bitcode 的第三方靜態庫,可以先用 ar -x 命令解壓得到其中的目標文件,因爲 Bitcode 是保存在 Mach-O 文件的 LLVM 段 bitcode 節中,使用 segedit 命令提取出 bc 文件,然後用自己編譯的 Clang 給定混淆參數將所有 bc 文件編譯打包成新的靜態庫。這樣做的缺點是新靜態庫沒有 Bitcode,導致主項目也必須關閉 Bitcode 功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章