建指南:Ubuntu 20.04打造Android NDK混淆工具鏈)
搞Android Native層開(kāi)發(fā)的人大概率都遇到過(guò)這種尷尬場(chǎng)景自己辛辛苦苦寫(xiě)的核心邏輯編譯成so扔進(jìn)APK里別人拖出來(lái)用反編譯工具一看函數(shù)名、調(diào)用關(guān)系、常量字符串清清楚楚核心算法幾乎是裸奔狀態(tài)。如果你正在做的是SDK保護(hù)、游戲反外掛、或者是收費(fèi)算法防破解這種裸奔基本等于送人頭。OLLVM就是在這一層做文章的。它是LLVM的一個(gè)衍生分支在編譯器層面加入了幾套代碼混淆Pass可以直接把產(chǎn)出so的控制流、指令形態(tài)、偽代碼結(jié)構(gòu)全部打亂不需要改動(dòng)你的業(yè)務(wù)代碼。這篇實(shí)戰(zhàn)記錄要解決的問(wèn)題很具體在一臺(tái)Ubuntu 20.04上從源碼構(gòu)建一套能用于Android NDK的OLLVM工具鏈然后讓項(xiàng)目的CMake構(gòu)建走這套工具鏈最終產(chǎn)出帶混淆效果的so庫(kù)。適合正在做App加固、SDK安全、以及想把代碼混淆工具鏈真正落地到日常構(gòu)建流程里的開(kāi)發(fā)者參考。1. 為什么要折騰OLLVMNative代碼保護(hù)的痛點(diǎn)1.1 so庫(kù)裸奔的現(xiàn)狀很多應(yīng)用的核心邏輯之所以放到Native層本身就是為了增加逆向難度。但實(shí)際上普通的NDK編譯產(chǎn)物并沒(méi)有想象中那么安全。拿一個(gè)未做任何處理的so文件用常見(jiàn)逆向工具打開(kāi)函數(shù)的導(dǎo)入導(dǎo)出表、符號(hào)名、跳轉(zhuǎn)邏輯基本都是直接可讀的。如果開(kāi)發(fā)者在編譯時(shí)沒(méi)加-fvisibilityhidden甚至保留了完整符號(hào)表那基本等于把設(shè)計(jì)圖紙也一并交了出去。另一個(gè)容易被忽視的問(wèn)題是邏輯結(jié)構(gòu)。即便去掉符號(hào)表匯編層面的指令序列仍然是有規(guī)律可循的。熟悉ARM匯編的人可以通過(guò)連續(xù)的條件跳轉(zhuǎn)、循環(huán)結(jié)構(gòu)、函數(shù)調(diào)用關(guān)系反推算法邏輯。換句話說(shuō)普通編譯只是掩蓋了代碼的可讀性并沒(méi)有改變代碼的“形態(tài)”。而代碼混淆要做的就是改變形態(tài)把線性的執(zhí)行流程變成網(wǎng)絡(luò)狀把簡(jiǎn)單的運(yùn)算變成等價(jià)但復(fù)雜的運(yùn)算。1.2 OLLVM到底做了什么OLLVM的全稱是Obfuscator-LLVM最初是某知名安全團(tuán)隊(duì)在LLVM基礎(chǔ)上做的一套開(kāi)源混淆框架。它的核心用法不是給你一個(gè)打包工具而是給你一套“能用混淆Pass的編譯器”。你要做的就是用這套編譯器重新編譯原生代碼它會(huì)在編譯過(guò)程中自動(dòng)完成混淆而不是在編譯后對(duì)二進(jìn)制做patch。它最常用的幾套Pass分別是控制流平坦化FLA函數(shù)體變成一個(gè)大循環(huán)加狀態(tài)分發(fā)器的結(jié)構(gòu)原來(lái)的if/else、循環(huán)邏輯全被拍平。分析的人看到的是一個(gè)巨大的switch-case調(diào)度器。虛假控制流BCF插入大量不會(huì)真實(shí)執(zhí)行的冗余條件分支讓反編譯工具的流程分析直接膨脹甚至卡死。指令替換SUB把簡(jiǎn)單的加減乘除、位運(yùn)算換成等價(jià)但更復(fù)雜的計(jì)算序列從指令層面增加閱讀成本。這三套Pass可以獨(dú)立開(kāi)啟也可以疊加。更關(guān)鍵的是OLLVM保留了LLVM的跨平臺(tái)能力這意味著它不光能編Android的ARM架構(gòu)也可以編iOS、桌面甚至嵌入式目標(biāo)。對(duì)Android安全領(lǐng)域來(lái)說(shuō)它一直是Native層代碼保護(hù)的經(jīng)典方案。1.3 為什么自己構(gòu)建而不是拿現(xiàn)成的市面上很多商業(yè)加固方案都內(nèi)嵌了類似的編譯器混淆能力但如果你有自己的SDK或者專用算法庫(kù)直接用第三方加固平臺(tái)往往意味著把源碼編譯環(huán)境交給別人而且每次出包都要上傳、加密、下載調(diào)試鏈路拉長(zhǎng)。自己構(gòu)建OLLVM工具鏈本質(zhì)上就是把混淆能力固化到本地構(gòu)建流程里一次搭好之后每次出包都是本地編譯產(chǎn)物配合自己的構(gòu)建腳本和簽名方案非常方便。另一方面自建工具鏈也方便二次定制。OLLVM雖然是開(kāi)源框架但原始版本停留在LLVM 4.0時(shí)期很多新ARM指令、新ABI適配和編譯器優(yōu)化都跟不上。如果你后續(xù)想自己加一個(gè)專用Pass比如字符串加密、整數(shù)運(yùn)算編碼、函數(shù)調(diào)用花指令普通的商業(yè)加固平臺(tái)做不到只有自建的編譯器源碼才能讓你加得進(jìn)去。這也是很多安全團(tuán)隊(duì)堅(jiān)持本地工具鏈的根本原因。2. 環(huán)境準(zhǔn)備與工具鏈選型2.1 Ubuntu 20.04上的基礎(chǔ)依賴構(gòu)建OLLVM本質(zhì)上就是在構(gòu)建一個(gè)LLVM編譯器所以對(duì)系統(tǒng)的要求和你編譯普通C/C項(xiàng)目差不多。需要準(zhǔn)備的基礎(chǔ)依賴包括構(gòu)建工具集、CMake和Python。Ubuntu 20.04默認(rèn)源里就有這些包直接通過(guò)apt安裝就行sudo apt update sudo apt install build-essential cmake git python3 ninja-build libffi-dev libedit-dev這里有兩個(gè)細(xì)節(jié)值得說(shuō)。第一個(gè)是CMake版本Ubuntu 20.04自帶的CMake是3.16版本構(gòu)建OLLVM 4.0完全夠用不需要折騰源碼安裝新版CMake。第二個(gè)是內(nèi)存和CPU核心數(shù)。LLVM全家桶編譯非常吃資源如果機(jī)器是8G內(nèi)存建議編譯的時(shí)候限制并行任務(wù)數(shù)不然很容易出現(xiàn)內(nèi)存耗盡被系統(tǒng)殺掉的情況。2.2 OLLVM源碼獲取與分支選擇OLLVM的源碼托管在GitHub它和LLVM官方倉(cāng)庫(kù)最大的區(qū)別就是多了混淆相關(guān)的Pass實(shí)現(xiàn)。拉取源碼時(shí)要注意分支選擇OLLVM官方的混淆特性集中在llvm-4.0分支上git clone -b llvm-4.0 https://github.com/obfuscator-llvm/obfuscator.git拉下來(lái)之后目錄結(jié)構(gòu)看起來(lái)和普通LLVM源碼是一模一樣的但多了一個(gè)Obfuscator相關(guān)目錄里面就是各個(gè)混淆Pass的源碼實(shí)現(xiàn)。不要小看這個(gè)分支選擇如果你直接拉默認(rèn)分支可能拉到的是沒(méi)有任何混淆Pass的版本編譯出來(lái)和官方LLVM沒(méi)區(qū)別。初次接觸的人經(jīng)常有一個(gè)疑問(wèn)為什么不用新版的LLVM因?yàn)镺LLVM的混淆Pass在LLVM 4.0之后沒(méi)有跟隨上游更新社區(qū)里雖然有一些移植到更高版本的fork但穩(wěn)定性普遍不如原始的llvm-4.0分支。對(duì)生產(chǎn)環(huán)境來(lái)說(shuō)穩(wěn)定優(yōu)先我最終選定的是llvm-4.0分支。2.3 NDK版本匹配最容易踩的隱形坑這一節(jié)是整個(gè)工具鏈搭建里最容易被忽略、卻最容易讓人卡住的點(diǎn)。OLLVM是LLVM 4.0的衍生物而Android NDK從r17之后全面切換到更新的Clang并移除了老舊的GCC工具鏈。如果你拿一個(gè)全新的NDK比如r23、r25去對(duì)接OLLVM大概率會(huì)碰到系統(tǒng)頭文件不兼容、鏈接器版本不匹配、內(nèi)建編譯器宏定義缺失等一系列問(wèn)題。我實(shí)測(cè)下來(lái)與OLLVM配合最穩(wěn)的是NDK r15c或r16b。這兩個(gè)版本里的Clang組件和LLVM 4.0同屬一個(gè)時(shí)代sysroot、libc、鏈接器都能對(duì)得上。你可以在Android Studio的SDK Manager里直接安裝指定版本的NDK也可以在項(xiàng)目的build.gradle里顯式鎖定android { ndkVersion 15.2.4203891 }這里寫(xiě)的“15.2.4203891”就是NDK r15c對(duì)應(yīng)的版本號(hào)。版本鎖定很關(guān)鍵否則Android Studio在同步工程時(shí)會(huì)自動(dòng)拉一個(gè)最新版NDK最后編譯階段報(bào)出各種晦澀的找不到頭文件錯(cuò)誤排查起來(lái)非常痛苦。2.4 為什么不直接拿官方LLVM編譯這個(gè)問(wèn)題值得展開(kāi)說(shuō)。LLVM官方主線其實(shí)也帶了一些基礎(chǔ)的混淆相關(guān)工具比如控制流圖分析、指令調(diào)度優(yōu)化等但這些和OLLVM的核心訴求不是一回事。官方LLVM的定位是通用編譯器優(yōu)化它的目標(biāo)是讓代碼跑得更快、體積更小而OLLVM的目標(biāo)是讓代碼更難被讀懂兩者在優(yōu)化方向上甚至是有沖突的。舉個(gè)例子LLVM的優(yōu)化Pass會(huì)盡量消除冗余跳轉(zhuǎn)和合并基本塊而OLLVM的控制流平坦化恰恰要插入大量狀態(tài)變量和分發(fā)跳轉(zhuǎn)這在常規(guī)優(yōu)化看來(lái)完全是負(fù)優(yōu)化。所以你不能用官方LLVM加參數(shù)來(lái)模擬OLLVM必須用內(nèi)置了混淆Pass的編譯器版本。3. 核心構(gòu)建流程從源碼到混淆工具的產(chǎn)出3.1 CMake配置與構(gòu)建參數(shù)源碼拉下來(lái)之后推薦使用獨(dú)立的build目錄進(jìn)行構(gòu)建這樣源目錄不會(huì)混入構(gòu)建產(chǎn)物。進(jìn)入源碼目錄后執(zhí)行mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease -DLLVM_TARGETS_TO_BUILDX86;ARM;AArch64 -DLLVM_ENABLE_ASSERTIONSON ..解釋一下幾個(gè)重點(diǎn)配置項(xiàng)的含義。CMAKE_BUILD_TYPERelease表示編譯LLVM本身時(shí)開(kāi)啟優(yōu)化這樣構(gòu)建出的OLLVM工具鏈運(yùn)行效率更高編譯Android代碼時(shí)速度更快。LLVM_TARGETS_TO_BUILD指定了需要生成的架構(gòu)支持這里填入X86、ARM、AArch64三個(gè)目標(biāo)X86是留給Ubuntu宿主機(jī)運(yùn)行的ARM和AArch64是Android設(shè)備需要的分別對(duì)應(yīng)32位和64位架構(gòu)。如果你明確只做64位應(yīng)用可以只保留X86;AArch64構(gòu)建時(shí)間和產(chǎn)物體積都會(huì)小一些。但做Android安全兼容測(cè)試時(shí)建議還是保留ARM因?yàn)槭忻嫔线€有不少armeabi-v7a的存量設(shè)備。LLVM_ENABLE_ASSERTIONSON這個(gè)選項(xiàng)很多人會(huì)漏掉。它在LLVM內(nèi)部啟用斷言檢查方便調(diào)試編譯器自身的問(wèn)題。發(fā)布工具鏈時(shí)通常建議開(kāi)上一旦混淆Pass內(nèi)部有邏輯錯(cuò)誤編譯階段會(huì)直接暴露而不是編譯出一個(gè)損壞的so到運(yùn)行期才崩潰。3.2 開(kāi)始編譯以及資源控制配置完成之后正式進(jìn)入編譯階段make -j8這里的-j參數(shù)需要根據(jù)機(jī)器配置調(diào)整。我的經(jīng)驗(yàn)是每2G內(nèi)存配一個(gè)并行任務(wù)比較穩(wěn)妥比如8G內(nèi)存開(kāi)-j416G內(nèi)存開(kāi)-j8甚至-j12。OLLVM完整編譯在四核心八線程機(jī)器上大約需要30到45分鐘具體時(shí)間取決于CPU主頻和磁盤(pán)性能。如果機(jī)器內(nèi)存確實(shí)緊張可以考慮降低并行數(shù)但不要試圖用make -j1硬扛那樣等待時(shí)間會(huì)非常漫長(zhǎng)。另外編譯過(guò)程中如果看到某個(gè)編譯任務(wù)出現(xiàn)internal compiler error或者被Killed基本就是內(nèi)存不足要么降低并行數(shù)要么關(guān)掉一些常駐內(nèi)存的進(jìn)程。構(gòu)建完成后重點(diǎn)關(guān)注build/bin目錄下生成的幾個(gè)可執(zhí)行文件。最重要的是clang和clang它們就是帶混淆Pass的編譯器本體。接下來(lái)可以通過(guò)執(zhí)行./bin/clang --version確認(rèn)版本信息如果輸出中包含clang version 4.0相關(guān)字樣說(shuō)明工具鏈本體已經(jīng)構(gòu)建成功。3.3 工具鏈完整性驗(yàn)證拿到clang之后不要急著接入Android工程先做一個(gè)最小驗(yàn)證。比如寫(xiě)一個(gè)簡(jiǎn)單的C文件用OLLVM clang直接編譯成目標(biāo)平臺(tái)的匯編確認(rèn)混淆Pass能正常工作echo int foo(int a, int b) { return a b; } test.c ./build/bin/clang -target aarch64-linux-android -mllvm -sub -c test.c -o test.o執(zhí)行完之后用工具檢查目標(biāo)文件llvm-objdump -d test.o。如果沒(méi)有這個(gè)工具也可以用objdump??吹絽R編里出現(xiàn)多段復(fù)雜的位運(yùn)算替代簡(jiǎn)單的加法指令說(shuō)明指令替換Pass生效。如果直接看到add指令說(shuō)明參數(shù)沒(méi)傳進(jìn)去或者工具鏈有問(wèn)題。這類最小驗(yàn)證非常重要它能幫你區(qū)分兩件事到底是OLLVM沒(méi)構(gòu)建成功還是后面接入Android工程時(shí)參數(shù)配置錯(cuò)了。我見(jiàn)過(guò)太多人一次性把工具鏈接入大工程結(jié)果出現(xiàn)問(wèn)題之后根本不知道去哪里排查最后把NDK、CMake、編譯器全部重來(lái)一遍浪費(fèi)大量時(shí)間。4. Android項(xiàng)目接入CMake集成與混淆參數(shù)實(shí)戰(zhàn)4.1 全局替換與項(xiàng)目級(jí)指定兩個(gè)方案對(duì)比OLLVM工具鏈構(gòu)建好之后接入Android項(xiàng)目有兩種思路。第一種是直接替換NDK里的clang也就是把OLLVM產(chǎn)物放到$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/目錄下覆蓋原有的編譯器。這種方式的好處是Android Studio構(gòu)建時(shí)不需要改任何配置所有C/C代碼自動(dòng)走OLLVM編譯壞處是極其危險(xiǎn)一旦某個(gè)版本出問(wèn)題整個(gè)NDK都被污染而且不同工程想用不同混淆策略就無(wú)法實(shí)現(xiàn)。第二種是項(xiàng)目級(jí)配置在CMakeLists里指定編譯器路徑。我的建議是如果想要一份干凈可控的產(chǎn)物一定要用第二種方案。Android Studio現(xiàn)在原生支持通過(guò)CMake配置外部編譯器不需要全局去動(dòng)NDK目錄。4.2 用CMakeLists鎖定OLLVM編譯器在項(xiàng)目級(jí)的CMakeLists.txt里只需要覆蓋幾個(gè)核心變量就可以把默認(rèn)編譯器切換到OLLVMset(NDK_HOME $ENV{ANDROID_NDK_HOME}) set(OLLVM_HOME /your/path/to/ollvm/build) set(CMAKE_C_COMPILER ${OLLVM_HOME}/bin/clang) set(CMAKE_CXX_COMPILER ${OLLVM_HOME}/bin/clang) set(CMAKE_SYSROOT ${NDK_HOME}/sysroot) set(CMAKE_C_COMPILER_TARGET aarch64-linux-android) set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-android) set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} -gcc-toolchain ${NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64 --sysroot${NDK_HOME}/sysroot) set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -gcc-toolchain ${NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64 --sysroot${NDK_HOME}/sysroot)這段配置里幾個(gè)關(guān)鍵點(diǎn)要解釋清楚。CMAKE_SYSROOT指向的是NDK提供的系統(tǒng)根目錄里面包含了Android平臺(tái)的系統(tǒng)頭文件和庫(kù)文件沒(méi)有它編譯器根本不知道目標(biāo)平臺(tái)的API定義。-gcc-toolchain則是指向NDK自帶的GCC兼容工具鏈主要作用是告訴編譯器去哪里找內(nèi)建的庫(kù)搜索路徑。不同ABI需要改CMAKE_C_COMPILER_TARGET的值對(duì)應(yīng)關(guān)系如下ABICMAKE_C_COMPILER_TARGETarm64-v8aaarch64-linux-androidarmeabi-v7aarmv7a-linux-androideabix86i686-linux-androidx86_64x86_64-linux-android如果你的項(xiàng)目同時(shí)打多個(gè)ABI一般是在build.gradle的externalNativeBuild段通過(guò)targets配置對(duì)應(yīng)的CMake參數(shù)。4.3 混淆Pass怎么開(kāi)梯度策略非常關(guān)鍵編譯器切換到OLLVM之后不會(huì)自動(dòng)混淆。你必須在編譯選項(xiàng)里顯式開(kāi)啟對(duì)應(yīng)的Pass。三種經(jīng)典Pass的命令行參數(shù)如下-mllvm -fla # 控制流平坦化 -mllvm -bcf -mllvm -bcf_prob60 # 虛假控制流概率60% -mllvm -sub -mllvm -sub_loop3 # 指令替換循環(huán)3次在CMake里可以對(duì)特定的本源文件開(kāi)啟混淆而不是一股腦全部加上set_source_files_properties( native-lib.cpp PROPERTIES COMPILE_FLAGS -mllvm -fla -mllvm -bcf )強(qiáng)烈建議用這種按文件粒度開(kāi)啟的方式。原因很簡(jiǎn)單混淆是有代價(jià)的??刂屏髌教够瘯?huì)把一個(gè)簡(jiǎn)單的函數(shù)體擴(kuò)大數(shù)倍甚至十幾倍指令替換會(huì)增加CPU計(jì)算時(shí)間虛假控制流會(huì)讓so體積明顯膨脹。如果全工程無(wú)差別開(kāi)啟最直接的后果就是包體暴漲、性能下降還可能出現(xiàn)個(gè)別函數(shù)混淆后觸發(fā)編譯器后端的問(wèn)題。我的典型做法是先對(duì)核心加解密函數(shù)、關(guān)鍵校驗(yàn)函數(shù)單獨(dú)開(kāi)啟-fla和-sub確認(rèn)穩(wěn)定后再對(duì)一些普通輔助函數(shù)開(kāi)-bcf。業(yè)務(wù)代碼保持不混淆否則每次發(fā)版都會(huì)收到各種莫名其妙的性能客訴。4.4 so產(chǎn)物的符號(hào)處理代碼經(jīng)過(guò)混淆只能讓分析者看不懂邏輯但如果so導(dǎo)出表里還留著函數(shù)名那就等于告訴別人“重點(diǎn)看這里”。所以編譯完成后一定要做符號(hào)表清理常規(guī)做法是加-fvisibilityhidden默認(rèn)隱藏所有符號(hào)只對(duì)JNI導(dǎo)出函數(shù)單獨(dú)標(biāo)注可見(jiàn)性。示例代碼如下__attribute__((visibility(default))) extern C JNIEXPORT jstring JNICALL Java_com_example_app_NativeBridge_getKey(JNIEnv* env, jobject thiz) { // only this function is exported }編譯完成后還可以用NDK自帶的llvm-strip工具去掉剩余符號(hào)信息${NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --strip-unneeded libnative-lib.sostrip之后再檢查導(dǎo)出表應(yīng)該只剩下JNI標(biāo)準(zhǔn)接口和少量動(dòng)態(tài)依賴符號(hào)。到這一步一個(gè)可發(fā)布的混淆so才真正具備防護(hù)價(jià)值。5. 常見(jiàn)問(wèn)題與排查實(shí)錄5.1 編譯期間的高頻報(bào)錯(cuò)對(duì)照整個(gè)搭建過(guò)程中遇到的最典型的幾個(gè)問(wèn)題我做了一個(gè)速查表按現(xiàn)象、原因、解決方式三列給出方便直接對(duì)照現(xiàn)象原因解決方法fatal error: sys/cdefs.h file not found編譯器沒(méi)有正確指定sysroot在CMake或編譯命令里加--sysroot$NDK_HOME/sysrootunable to find target或架構(gòu)解析失敗CMAKE_C_COMPILER_TARGET沒(méi)有配置或拼寫(xiě)錯(cuò)誤按上文ABI對(duì)照表檢查target三元組unknown argument: -mllvm混淆參數(shù)通過(guò)CMAKE_C_FLAGS傳入時(shí)位置不對(duì)被鏈接器解析了確保-mllvm類型的參數(shù)附加在COMPILE_FLAGS而不是LINK_FLAGS編譯過(guò)程中進(jìn)程被Killed內(nèi)存不足默認(rèn)并行任務(wù)太多降低make -j數(shù)量比如從-j8降到-j4鏈接時(shí)找不到libunwind或libc相關(guān)符號(hào)NDK版本和OLLVM不匹配換NDK r15c/r16b并統(tǒng)一版本安裝好的OLLVM clang無(wú)法運(yùn)行依賴庫(kù)缺失或動(dòng)態(tài)鏈接異常執(zhí)行l(wèi)dd bin/clang檢查依賴安裝libffi、libedit等庫(kù)5.2 混淆后的so在真機(jī)上崩潰這是一個(gè)非常讓人頭大的問(wèn)題編譯沒(méi)報(bào)錯(cuò)構(gòu)建也成功一裝到真機(jī)上直接崩潰但是不混淆的版本一切正常。根據(jù)經(jīng)驗(yàn)90%以上這類問(wèn)題原因集中在幾個(gè)地方。第一個(gè)是浮點(diǎn)和內(nèi)聯(lián)匯編。OLLVM在某些Pass組合下會(huì)把浮點(diǎn)運(yùn)算改寫(xiě)而目標(biāo)Android設(shè)備的老舊GPU/CPU對(duì)異常浮點(diǎn)處理不當(dāng)時(shí)會(huì)產(chǎn)生SIGFPE。這種情況下建議先把-sub關(guān)閉逐步添加Pass找到具體哪個(gè)Pass觸發(fā)問(wèn)題。第二個(gè)是??臻g和基本塊爆炸。控制流平坦化在某些大函數(shù)上會(huì)生成巨大的狀態(tài)機(jī)導(dǎo)致棧使用量飆升進(jìn)而觸發(fā)棧溢出。解決方案是排除體積過(guò)于巨大的函數(shù)或者在大函數(shù)上只開(kāi)-bcf不開(kāi)-fla。第三個(gè)是O2優(yōu)化級(jí)別與混淆Pass的相互影響。常見(jiàn)的組合是編譯命令帶了-O2混淆后再優(yōu)化導(dǎo)致某些尾調(diào)用被改寫(xiě)成跳轉(zhuǎn)破壞了混淆的結(jié)構(gòu)。一般做法是先-O0驗(yàn)證功能再開(kāi)-O1或者-O2做最終產(chǎn)物。5.3 性能與安全強(qiáng)度的平衡建議關(guān)于混淆強(qiáng)度這是最容易被忽略的經(jīng)驗(yàn)問(wèn)題。新人往往覺(jué)得Pass開(kāi)得越全越好概率越高越好其實(shí)混淆的收益在超過(guò)一個(gè)閾值后增長(zhǎng)緩慢成本卻直線上升。比如-bcf_prob100和-bcf_prob60對(duì)分析者的困擾程度其實(shí)是差不多的但前者的so體積可能是后者的兩倍以上運(yùn)行耗時(shí)也更高。我的經(jīng)驗(yàn)是核心機(jī)密算法函數(shù)用-fla和-sub普通的協(xié)議封裝函數(shù)用-bcf難度已經(jīng)足夠。只有當(dāng)你確定某個(gè)函數(shù)是攻擊者的主攻目標(biāo)時(shí)才需要把三種Pass全部拉滿。還有一個(gè)細(xì)節(jié)是關(guān)于調(diào)試信息的?;煜蟮陌绻€留著調(diào)試符號(hào)等于一切白做。Release構(gòu)建里一定要設(shè)CMAKE_BUILD_TYPERelease同時(shí)清理DWARF信息和注釋相關(guān)的字符串。5.4 自動(dòng)化打包鏈路的關(guān)鍵一環(huán)工具鏈搭好之后最好把它固化進(jìn)構(gòu)建腳本避免每次手動(dòng)改環(huán)境變量??梢栽陧?xiàng)目的根目錄放一個(gè)ollvm.env里面統(tǒng)一導(dǎo)出路徑。在構(gòu)建腳本里source一下確保CI環(huán)境和本地構(gòu)建環(huán)境一致。如果團(tuán)隊(duì)里多人協(xié)作一定統(tǒng)一NDK版本和OLLVM構(gòu)建版本并把兩個(gè)路徑通過(guò)環(huán)境變量而不是硬編碼的方式傳入項(xiàng)目。否則每個(gè)人的機(jī)器編譯出的so行為可能不一致排查問(wèn)題時(shí)各說(shuō)各話。我個(gè)人在實(shí)際操作中的體會(huì)是這套鏈路最花時(shí)間的從來(lái)不是OLLVM本身的編譯而是NDK版本和編譯器路徑的匹配。很多崩潰問(wèn)題排查到最后發(fā)現(xiàn)源頭只是老版clang和新版sysroot之間的ABI不兼容。如果你也想在自己的SDK里試驗(yàn)建議從arm64-v8a一個(gè)ABI、一個(gè)源文件、一個(gè)混淆Pass開(kāi)始先跑通全流程再逐步鋪開(kāi)。最后想再提醒一句OLLVM解決的是“代碼被人讀懂”的問(wèn)題解決不了“密鑰怎么藏、校驗(yàn)怎么做”這些業(yè)務(wù)層面的安全設(shè)計(jì)兩者是配合關(guān)系不是替代關(guān)系。