實(shí)戰(zhàn):從字符串加密到控制流平坦化)
接手過(guò)一個(gè)被逆向得幾乎底褲不剩的 C 客戶端項(xiàng)目之后我對(duì) C 代碼混淆與保護(hù)的態(tài)度徹底變了。對(duì)方從內(nèi)存里取走解密后的核心密鑰繞過(guò)全部授權(quán)校驗(yàn)整個(gè)過(guò)程不到一周。從那以后只要交付物里帶核心算法、授權(quán)邏輯或通信協(xié)議我都會(huì)把代碼保護(hù)當(dāng)成和編譯優(yōu)化同等重要的一道工序而不是以后再說(shuō)的加分項(xiàng)。這篇東西不打算把程序講成誰(shuí)也看不懂的天書(shū)而是圍繞真實(shí)項(xiàng)目的威脅模型、符號(hào)與字符串處理、控制流混淆、反調(diào)試與完整性校驗(yàn)、性能取舍以及混淆上線后怎么定位崩潰把我驗(yàn)證過(guò)有效的做法和踩過(guò)的坑完整記錄下來(lái)。適合正在做 C 桌面應(yīng)用、SDK、游戲客戶端或者內(nèi)部工具且核心邏輯不想被人一眼還原的開(kāi)發(fā)者參考。1. 逆向視角下的C程序你以為的壁壘其實(shí)是紙糊的1.1 攻擊者拿到的第一手資料先把立場(chǎng)擺正發(fā)布出去的二進(jìn)制在反匯編器面前沒(méi)有秘密。靜態(tài)分析階段攻擊者可以拿到導(dǎo)入導(dǎo)出表、字符串字面量、RTTI 類型名、異常處理相關(guān)的展開(kāi)元數(shù)據(jù)以及編譯器生成的符號(hào)信息如果沒(méi)做處理的話。這些信息比大多數(shù)人想象的值錢。你辛苦寫(xiě)的 C 類可能因?yàn)橐痪鋝td::runtime_error(Invalid license)直接被字符串交叉引用定位到授權(quán)校驗(yàn)函數(shù)你的核心算法可能因?yàn)槟硞€(gè)全局變量的名字沒(méi)刪干凈讓分析者順著符號(hào)追到了關(guān)鍵分支。某個(gè)項(xiàng)目里我們自以為把校驗(yàn)邏輯藏得很好結(jié)果攻擊者只搜了一下提示語(yǔ)字符串五分鐘不到就定位到了校驗(yàn)入口。動(dòng)態(tài)階段更直接。用調(diào)試器下斷點(diǎn)觀察內(nèi)存讀寫(xiě)、跟蹤函數(shù)調(diào)用、查看?;厮莨粽吣芾@過(guò)你設(shè)想的黑盒直接看運(yùn)行時(shí)行為。當(dāng)年那個(gè)項(xiàng)目就是這么被擊穿的授權(quán)校驗(yàn)的返回值在內(nèi)存里被改寫(xiě)客戶端一直以為自己是正版。這里我想強(qiáng)調(diào)一件事也是整篇文章的前提代碼混淆與保護(hù)的目標(biāo)不是讓逆向分析變成不可能而是把分析成本抬高到超過(guò)攻擊者愿意投入的時(shí)間預(yù)算。任何宣稱絕對(duì)無(wú)法破解的方案基本都可以直接劃掉。1.2 C程序尤其容易成為靶子的原因C 生成的二進(jìn)制和托管語(yǔ)言不太一樣。你沒(méi)有一個(gè)統(tǒng)一虛擬機(jī)替你藏起執(zhí)行邏輯機(jī)器碼直接映射到真實(shí)硬件指令理論上任何一段代碼都可以被反匯編出來(lái)逐條讀懂。與此同時(shí)C 面向性能的設(shè)計(jì)又意味著大量結(jié)構(gòu)在編譯后就消失了——聽(tīng)起來(lái)像是好事但代價(jià)是真正敏感的東西比如授權(quán)分支、算法常量、協(xié)議密鑰也會(huì)以非常直白的形式留在二進(jìn)制里。沒(méi)有額外保護(hù)時(shí)搜索字符串、查引用、下斷點(diǎn)這三板斧就能解決絕大部分問(wèn)題。另外C 的老項(xiàng)目普遍長(zhǎng)年累積很多模塊的邊界早已不清。攻擊者不一定需要看懂整個(gè)程序只要找到那個(gè)判斷是否合法的葉子函數(shù)改掉一兩個(gè)字節(jié)就能達(dá)成目的。這也是為什么后面要專門(mén)聊完整性校驗(yàn)如果只混淆不校驗(yàn)攻擊者可以直接在你混淆好的代碼上打補(bǔ)丁你的混淆等于白做。1.3 先建立正確的保護(hù)邊界動(dòng)手之前我建議先花點(diǎn)時(shí)間把保護(hù)邊界畫(huà)清楚哪些函數(shù)是真正的要害哪些只是普通業(yè)務(wù)代碼。通常值得保護(hù)的包括授權(quán)與付費(fèi)校驗(yàn)、核心算法推導(dǎo)、私有協(xié)議編解碼、許可證生成與驗(yàn)證以及任何涉及密鑰材料的代碼。業(yè)務(wù)代碼、界面邏輯、日志體系反而不用過(guò)度處理——它們被讀懂也無(wú)所謂而且混淆只會(huì)拖慢開(kāi)發(fā)和排查節(jié)奏。邊界清楚了后面的混淆強(qiáng)度、性能預(yù)算都好定。否則上來(lái)就對(duì)全體代碼開(kāi)最高檔混淆項(xiàng)目維護(hù)會(huì)變成一場(chǎng)災(zāi)難這一點(diǎn)后面性能那節(jié)我會(huì)給出具體數(shù)據(jù)。2. 分層布置從編譯開(kāi)關(guān)到符號(hào)與字符串處理的實(shí)操細(xì)節(jié)2.1 編譯期與鏈接期的基礎(chǔ)動(dòng)作最便宜、最不折騰人的保護(hù)是讓二進(jìn)制里沒(méi)有多余的說(shuō)明書(shū)。Linux 上編譯核心模塊時(shí)用-fvisibilityhidden加鏈接期stripWindows 下發(fā)布剝離后的 PDB 版本并檢查導(dǎo)出表這些動(dòng)作能立刻消掉一大類符號(hào)級(jí)線索。配合鏈接器的 strip 開(kāi)關(guān)二進(jìn)制里不會(huì)殘留編譯器生成的函數(shù)名和類型名。成本幾乎為零收益卻實(shí)實(shí)在在。在此基礎(chǔ)上可以再進(jìn)一步用鏈接腳本或自定義段把代碼和數(shù)據(jù)重新編排比如把關(guān)鍵函數(shù)挪到新的段里、打亂段順序。這類手段會(huì)增大靜態(tài)分析時(shí)的人工檢索成本但要注意別和操作系統(tǒng)要求的標(biāo)準(zhǔn)段搞混否則啟動(dòng)就可能直接掛掉。我見(jiàn)過(guò)一個(gè)項(xiàng)目把.text段拆得亂七八糟結(jié)果 Windows 上加載器不認(rèn)藍(lán)屏級(jí)別的崩潰差點(diǎn)讓整個(gè)版本回滾。2.2 符號(hào)層面的手工布置編譯開(kāi)關(guān)只能刪掉帶符號(hào)表的情況。內(nèi)聯(lián)、RTTI、異常展開(kāi)表里仍可能泄露類名或方法名尤其是用了強(qiáng)類型轉(zhuǎn)換和異常的項(xiàng)目。我的做法是發(fā)布前專門(mén)跑一遍腳本掃描二進(jìn)制里的可讀字符串凡是跟內(nèi)部類名、文件名、變量名相關(guān)的都列入清理清單。核心 SDK 模塊甚至可以完全關(guān)閉 RTTI-fno-rtti并避免在熱點(diǎn)路徑使用dynamic_cast從源頭減少類型信息的殘留。清理之后還要保證本地留存一份帶完整符號(hào)和映射文件的版本并歸檔對(duì)應(yīng)的 commit ID。這直接關(guān)系到后面第 6 節(jié)講的崩潰定位很多人忽略這一步等線上出事才發(fā)現(xiàn)手頭根本沒(méi)有能對(duì)得上號(hào)的符號(hào)文件。2.3 字符串加密的實(shí)用寫(xiě)法字符串是重災(zāi)區(qū)。不管是日志還是提示語(yǔ)都是靜態(tài)分析定位關(guān)鍵函數(shù)的路標(biāo)。我常用的做法是一套基于 constexpr 的編譯期異或加密模板密鑰由編譯期表達(dá)式參與生成明文不出現(xiàn)在二進(jìn)制里運(yùn)行時(shí)只在需要時(shí)解密到棧上、用完即棄。代碼大致長(zhǎng)這樣template size_t N class CipherText { public: constexpr CipherText(const char (plain)[N], char key) : data{}, len(N - 1) { for (size_t i 0; i N; i) { data[i] plain[i] ^ (key len); } } std::string str() const { std::string out(len, \0); for (size_t i 0; i len; i) { out[i] static_castchar(data[i] ^ (key len)); } return out; } private: char data[N]; size_t len; }; // 使用方式需開(kāi)啟 C14 及以上標(biāo)準(zhǔn) static constexpr CipherText kNotice(License check failed, 0x4D); // 運(yùn)行時(shí)調(diào)用 kNotice.str() 才還原明文注意幾個(gè)細(xì)節(jié)。第一密鑰要配合長(zhǎng)度參與運(yùn)算避免所有密文都被同一個(gè)字節(jié)異或形成一眼可識(shí)別的規(guī)律。第二不要把解密結(jié)果長(zhǎng)期留在全局變量里解密到局部、用完清零。第三繞開(kāi)編譯器對(duì)字符串常量的自動(dòng)提取必要時(shí)在構(gòu)建腳本里對(duì)二進(jìn)制再做一次字符串掃描驗(yàn)證。我最初實(shí)現(xiàn)時(shí)漏了第三點(diǎn)結(jié)果優(yōu)化器把常量折疊后的明文直接塞進(jìn)了只讀段字符串加密形同虛設(shè)。后來(lái)又補(bǔ)了一版基于構(gòu)建產(chǎn)物的掃描檢查才把這類問(wèn)題堵住。3. 控制流混淆的核心手法平坦化、不透明謂詞與組合順序3.1 控制流平坦化的原理與落地單純隱藏字符串和符號(hào)遠(yuǎn)遠(yuǎn)不夠因?yàn)楣粽呷匀荒苎刂瘮?shù)調(diào)用關(guān)系追蹤關(guān)鍵邏輯??刂屏髌教够亲钪髁鞯囊徽邪言居蒳f/else、while、switch表達(dá)的自然分支邏輯改造成一個(gè)主分發(fā)器加若干狀態(tài)塊的結(jié)構(gòu)。程序運(yùn)行時(shí)不再直接跳轉(zhuǎn)到下一個(gè)應(yīng)該執(zhí)行的塊而是先回到分發(fā)器靠一個(gè)狀態(tài)變量決定下一個(gè)塊是誰(shuí)。用一段極簡(jiǎn)示例來(lái)說(shuō)明。假設(shè)原始邏輯是int check(int code) { int result 0; if (code 100) { result verify_key(code); } else { result verify_mac(code); } return result; }經(jīng)過(guò)平坦化改造視覺(jué)上會(huì)變成類似分發(fā)器 狀態(tài)變量的結(jié)構(gòu)int check(int code) { int state 0; int result 0; while (true) { switch (state) { case 0: state (code 100) ? 1 : 2; break; case 1: result verify_key(code); state 3; break; case 2: result verify_mac(code); state 3; break; case 3: return result; } } }真實(shí)的平坦化由混淆工具或編譯器層面的自動(dòng)化 Pass 完成塊的數(shù)量、分發(fā)器的形態(tài)遠(yuǎn)比這個(gè)復(fù)雜但原理一致靜態(tài)分析時(shí)分支關(guān)系被徹底打散動(dòng)態(tài)調(diào)試時(shí)單步跟蹤也會(huì)陷入大量分發(fā)跳轉(zhuǎn)。代價(jià)是代碼膨脹和局部性能損耗所以一般只對(duì)關(guān)鍵函數(shù)開(kāi)啟別指望全項(xiàng)目都來(lái)一遍。3.2 不透明謂詞給分析者挖的必然陷阱另一個(gè)常用手段是不透明謂詞。用一個(gè)恒真或恒假的表達(dá)式嵌入原本的業(yè)務(wù)判斷讓分析者以為這里有雙分支實(shí)際運(yùn)行時(shí)永遠(yuǎn)走同一條路。標(biāo)準(zhǔn)例子像(n * n n 41) % 2 0這類基于簡(jiǎn)單數(shù)學(xué)性質(zhì)的式子或者利用整數(shù)溢出特征的表達(dá)式。靜態(tài)分析很難一眼判斷它恒成立于是會(huì)浪費(fèi)大量時(shí)間在看起來(lái)存在其實(shí)不存在的分支上。不透明謂詞單個(gè)用意義不大最好疊加在平坦化、函數(shù)內(nèi)聯(lián)混合之后讓整個(gè)控制流圖呈現(xiàn)一種既繞又多岔路的狀態(tài)。務(wù)必注意表達(dá)式不能依賴未定義行為也不能在特定編譯器優(yōu)化下被常量折疊成單一分支否則反而幫對(duì)手簡(jiǎn)化了代碼。我一般都會(huì)在不透明謂詞外面包一層基于運(yùn)行期輸入的計(jì)算讓優(yōu)化器無(wú)法在編譯期完全折疊。3.3 組合順序與自動(dòng)化工具的選擇如果條件允許優(yōu)先使用現(xiàn)成的自動(dòng)化方案開(kāi)源的編譯器中端混淆 Pass或者商業(yè)級(jí)混淆器。它們能穩(wěn)定輸出平坦化、虛假控制流、函數(shù)拆分合并、指令替換等組合效果并且能配合構(gòu)建系統(tǒng)一鍵集成。手寫(xiě)混淆只適合個(gè)別熱點(diǎn)小函數(shù)因?yàn)橥绞指目刂屏骷热菀壮鲥e(cuò)又難以維護(hù)。我自己的經(jīng)驗(yàn)是組合順序會(huì)影響最終效果。推薦的流水線是先做字符串加密和符號(hào)清理再做函數(shù)內(nèi)聯(lián)與拆分之后跑平坦化和不透明謂詞最后做節(jié)區(qū)重排。順序反了比如說(shuō)先平坦化再加密字符串加密工具對(duì)復(fù)雜控制流的處理會(huì)變得很不穩(wěn)定構(gòu)建時(shí)間也會(huì)顯著拉長(zhǎng)。這套順序在多個(gè)項(xiàng)目上驗(yàn)證過(guò)崩潰率最低、混淆效果最穩(wěn)定。4. 動(dòng)態(tài)防線怎么搭反調(diào)試、完整性校驗(yàn)與誤報(bào)控制4.1 反調(diào)試手段要組合拳而不是獨(dú)門(mén)絕技靜態(tài)混淆解決的是讀懂的問(wèn)題動(dòng)態(tài)防線解決的是觀察的問(wèn)題。反調(diào)試是其中最常見(jiàn)的一類。Linux 下可以通過(guò)ptrace自附加檢測(cè)調(diào)試器Windows 下可以檢查進(jìn)程環(huán)境塊中的調(diào)試標(biāo)志也可以利用時(shí)間差法測(cè)量關(guān)鍵代碼段的執(zhí)行耗時(shí)明顯異常就認(rèn)定被跟蹤。但這些手段單打獨(dú)斗都容易被繞過(guò)而且很多時(shí)候只是把調(diào)試過(guò)程變慢不能完全阻止。所以我會(huì)把它們組合使用并且把檢測(cè)點(diǎn)散落在多個(gè)位置而不是集中在某個(gè)專門(mén)的反調(diào)試函數(shù)里——集中式檢測(cè)等于給攻擊者標(biāo)了一個(gè) patch 目標(biāo)。另外反調(diào)試邏輯本身也要做混淆否則攻擊者找到入口直接改跳轉(zhuǎn)檢測(cè)就失效了。4.2 完整性校驗(yàn)防打補(bǔ)丁的關(guān)鍵前面提過(guò)只混淆不校驗(yàn)攻擊者可以直接修改二進(jìn)制跳過(guò)授權(quán)判斷。完整性校驗(yàn)就是把代碼區(qū)、關(guān)鍵數(shù)據(jù)區(qū)算出一個(gè)摘要在運(yùn)行時(shí)定期重新計(jì)算并比對(duì)。核心授權(quán)函數(shù)一旦被修改摘要立刻對(duì)不上程序可以走隱蔽的降級(jí)路徑而不是直接崩潰——直接崩潰等于告訴攻擊者這里就是校驗(yàn)點(diǎn)。完整性校驗(yàn)的代碼也有講究。最好拆成多個(gè)小函數(shù)散布在各處校驗(yàn)結(jié)果不要單一布爾返回而是融入后續(xù)的業(yè)務(wù)計(jì)算增加攻擊者的分析成本。我遇到過(guò)一個(gè)比較有效的組合把校驗(yàn)結(jié)果作為解密密鑰的一部分校驗(yàn)不過(guò)密鑰就錯(cuò)解密出來(lái)的邏輯就是亂的。這種情況下攻擊者想繞過(guò)至少得把完整的數(shù)據(jù)流分析清楚。4.3 運(yùn)行時(shí)自守護(hù)的誤報(bào)控制動(dòng)態(tài)防線最讓人頭疼的不是實(shí)現(xiàn)而是誤報(bào)。合法用戶的殺毒軟件、系統(tǒng)更新、調(diào)試器附加都可能觸發(fā)檢測(cè)。我在項(xiàng)目里遇到的真實(shí)案例是某版本的完整性校驗(yàn)把節(jié)區(qū)重排后的大小計(jì)算錯(cuò)了導(dǎo)致一部分合法安裝包運(yùn)行時(shí)報(bào)錯(cuò)用戶側(cè)崩潰率直接飆到一個(gè)讓人失眠的數(shù)字。誤報(bào)控制的關(guān)鍵是分層策略高置信度的檢測(cè)比如校驗(yàn)和嚴(yán)重不匹配走降級(jí)或終止低置信度的檢測(cè)比如執(zhí)行時(shí)間異常只記錄日志不立刻動(dòng)作。同時(shí)要給校驗(yàn)代碼留一個(gè)灰度開(kāi)關(guān)通過(guò)配置中心遠(yuǎn)程調(diào)整閾值避免出現(xiàn)問(wèn)題還要等緊急發(fā)版。這套思路我后來(lái)一直用效果比單純追求檢測(cè)得越狠越好穩(wěn)妥得多。5. 保護(hù)方案的賬本實(shí)測(cè)數(shù)據(jù)、三層強(qiáng)度對(duì)照與兼容性風(fēng)險(xiǎn)5.1 實(shí)測(cè)數(shù)據(jù)比感覺(jué)更可靠說(shuō)了這么多手段每一樣都有代價(jià)。以我常用的中等強(qiáng)度組合字符串加密 符號(hào)清理 關(guān)鍵函數(shù)平坦化 輕量反調(diào)試為例在一套 C 桌面應(yīng)用上實(shí)測(cè)大概是二進(jìn)制體積增加 15% 到 25%冷啟動(dòng)時(shí)間增加 30% 左右核心算法路徑吞吐下降約 5% 到 10%非關(guān)鍵路徑幾乎無(wú)感知。如果上最強(qiáng)檔把全部代碼做重度混淆體積可能翻倍啟動(dòng)慢得明顯老機(jī)器上甚至?xí)粴⒍拒浖`判。所以我強(qiáng)烈建議先量再優(yōu)化而不是拍腦袋。保護(hù)強(qiáng)度這個(gè)事跟性能預(yù)算必須一起定而且要用發(fā)布配置、Release 優(yōu)化檔來(lái)測(cè)別拿 Debug 數(shù)據(jù)騙自己。混淆對(duì)優(yōu)化器的影響在 O2/O3 下和 Debug 下完全不同同一個(gè)函數(shù)在不同優(yōu)化檔下混淆出的結(jié)構(gòu)差異很大。5.2 三層保護(hù)強(qiáng)度對(duì)照我習(xí)慣把保護(hù)分成三檔按需選擇檔位手段組合適用場(chǎng)景體積影響維護(hù)成本基礎(chǔ)檔符號(hào)清理、strip、字符串加密、關(guān) RTTI內(nèi)部工具、普通 SDK約 5%低標(biāo)準(zhǔn)檔基礎(chǔ)檔 關(guān)鍵函數(shù)平坦化 不透明謂詞商業(yè)客戶端、授權(quán)邏輯15% 到 25%中強(qiáng)護(hù)檔標(biāo)準(zhǔn)檔 全函數(shù)混淆 反調(diào)試組合 分段完整性校驗(yàn)高價(jià)值算法庫(kù)、游戲核心30% 以上高一句話總結(jié)保護(hù)強(qiáng)度應(yīng)該跟著資產(chǎn)價(jià)值走。花一個(gè)月保護(hù)一個(gè)復(fù)制粘貼價(jià)值為 0 的小工具是賠本的。把預(yù)算集中在授權(quán)校驗(yàn)、核心算法、協(xié)議密鑰這幾塊其余代碼保持清爽可維護(hù)這才是大多數(shù)項(xiàng)目的正確姿勢(shì)。5.3 兼容性風(fēng)險(xiǎn)清單混淆和保護(hù)對(duì)編譯器版本、操作系統(tǒng)、安全軟件都有微妙影響。我踩過(guò)的坑包括Windows 上開(kāi)啟控制流守衛(wèi)與自定義節(jié)區(qū)沖突導(dǎo)致啟動(dòng)崩潰Linux 上關(guān)閉 RTTI 后某些第三方頭文件里dynamic_cast編譯失敗殺毒軟件對(duì)加殼或虛擬化特征明顯的二進(jìn)制直接隔離。這些都需要在發(fā)布矩陣?yán)锾崆案采w。建議至少準(zhǔn)備主流 Windows 版本與常見(jiàn) Linux 發(fā)行版各一套測(cè)試環(huán)境開(kāi)啟安全軟件掃描后跑一遍完整功能回歸別把兼容性問(wèn)題留給用戶去發(fā)現(xiàn)。6. 混淆之后最怕的事崩潰定位的完整排查鏈路6.1 崩潰信息不可讀時(shí)的第一反應(yīng)混淆后的代碼崩潰起來(lái)是真的讓人頭大。?;厮堇锶且欢押翢o(wú)意義的地址別名異常上下文里也看不到原始函數(shù)。第一次遇到時(shí)我差點(diǎn)把鍵盤(pán)摔了因?yàn)楸罎⑼耆豢蓮?fù)現(xiàn)線上日志只能告訴我某模塊掛在你完全看不懂的地址上。正確的第一反應(yīng)不是去翻混淆后的反匯編而是立刻打開(kāi)本地歸檔的、帶完整符號(hào)的未混淆版本和線上發(fā)布的混淆版本用同一個(gè) commit 構(gòu)建對(duì)比。只要本地符號(hào)版本能復(fù)現(xiàn)崩潰直接用調(diào)試器從出問(wèn)題的邏輯地址對(duì)照映射文件反向解析到原始函數(shù)。這一步能解決絕大部分的定位問(wèn)題。6.2 符號(hào)映射與構(gòu)建歸檔的正確姿勢(shì)構(gòu)建系統(tǒng)里要約定一條鐵律每次發(fā)布混淆版本的同時(shí)歸檔未混淆的符號(hào)版本、映射文件、編譯參數(shù)、commit 號(hào)。這個(gè)歸檔本身要訪問(wèn)隔離別跟公開(kāi)發(fā)布放一起。定位崩潰時(shí)用二進(jìn)制符號(hào)解析工具把崩潰地址換算成源文件和行號(hào)。還有個(gè)小技巧崩潰上報(bào)時(shí)不僅記棧地址還要記模塊基址和版本號(hào)。因?yàn)椴煌瑯?gòu)建的基址不同只有把偏移算出來(lái)才能正確對(duì)到歸檔符號(hào)上。這些字段在正式日志里都要有別圖省事只記一個(gè)棧字符串。我之前就因?yàn)槿罩纠锶绷四K基址線上崩潰堆了一堆愣是沒(méi)法精確還原最后只能靠概率學(xué)猜位置極其痛苦。6.3 我在實(shí)際項(xiàng)目中遇到的三個(gè)經(jīng)典問(wèn)題第一個(gè)是異常展開(kāi)在平坦化代碼里的問(wèn)題。某版本用了較高檔的平坦化結(jié)果 C 異常在跨越狀態(tài)塊時(shí)報(bào)錯(cuò)或直接終止查下來(lái)是異常展開(kāi)表與改造后的棧布局不匹配。最終方案是給異常處理路徑上的函數(shù)降低混淆檔位并關(guān)閉對(duì)該函數(shù)的分段。第二個(gè)是字符串加密把崩潰日志也加密了。日志系統(tǒng)本身用了加密模板崩潰時(shí)解不出來(lái)上下文排查瞬間變瞎。后來(lái)我規(guī)定日志、崩潰上報(bào)、配置讀取這類代碼一律不加字符串加密寧可多泄露一點(diǎn)內(nèi)部信息也要保住可觀測(cè)性。第三個(gè)是發(fā)布周期拖長(zhǎng)。全量混淆的構(gòu)建時(shí)間比普通版本多出好幾倍迭代快的時(shí)期特別難受。后來(lái)我把保護(hù)切成分層增量日常構(gòu)建只跑基礎(chǔ)檔只有發(fā)版前才跑標(biāo)準(zhǔn)檔以上配合自動(dòng)化流水線問(wèn)題才算壓下來(lái)。6.4 測(cè)試矩陣混淆不該破壞功能語(yǔ)義混淆的膽子再大有一條底線不能碰語(yǔ)義必須等價(jià)。任何保護(hù)方案上線前都要在主流的編譯器版本、優(yōu)化檔、C 標(biāo)準(zhǔn)、操作系統(tǒng)組合下跑一遍完整的測(cè)試用例。特別是混沌測(cè)試和模糊測(cè)試用來(lái)驗(yàn)證不透明謂詞、平坦化沒(méi)有改變執(zhí)行路徑。我的做法是建立一張發(fā)布矩陣每一檔保護(hù)強(qiáng)度對(duì)應(yīng)一列測(cè)試任務(wù)發(fā)版流程里強(qiáng)制勾選少一項(xiàng)都不允許發(fā)布。真正確保質(zhì)量的辦法從來(lái)不是靠某一次細(xì)心而是靠流程把風(fēng)險(xiǎn)兜住。最后再分享一個(gè)我踩過(guò)最深坑后總結(jié)下來(lái)的經(jīng)驗(yàn)不要把所有保護(hù)都?jí)涸趩我皇跈?quán)點(diǎn)上。很多項(xiàng)目把授權(quán)校驗(yàn)做成一個(gè)獨(dú)立函數(shù)混淆、加密、反調(diào)試全部堆上去結(jié)果攻擊者只需要把調(diào)用點(diǎn)改成強(qiáng)制跳轉(zhuǎn)就完事了。正確的做法是讓授權(quán)結(jié)果參與多個(gè)業(yè)務(wù)環(huán)節(jié)比如作為運(yùn)行期密鑰的一部分或者混合進(jìn)核心算法的初始向量。保護(hù)的價(jià)值從來(lái)不在于某一段代碼有多難讀而在于攻擊者改完你代碼之后程序還能不能按他預(yù)期的邏輯跑起來(lái)。這個(gè)思路比任何單一混淆技巧都更重要。