:方法級代碼抽取——PVM1 虛擬化打包到底是什么?)
Android APK 加固原理三方法級代碼抽取——PVM1 虛擬化打包到底是什么系列文章?第一篇《Android APK 加固原理一Native Shell 如何隱藏和恢復 DEX》第二篇《Android APK 加固原理二從 DEX 解密到 ART 加載如何縮短代碼明文暴露窗口》第三篇《Android APK 加固原理三方法級代碼抽取——PVM1 虛擬化打包到底是什么》第四篇《Android APK 加固原理四真正的代碼虛擬化——PVM2 Native Interpreter 技術解析》第五篇《Android APK 加固原理五SO.text段加密、ELF 加載與運行時動態(tài)解密》第六篇《Android APK 加固原理六RASP 運行時安全防護——如何檢測 Frida、Hook 與運行時攻擊》第七篇《從 APK 加密到代碼虛擬化XopProtector 多層 Android 應用保護體系解析》項目地址https://github.com/xopJack/XopProtector一、前言為什么有了 DEX 加密還需要 PVM1在前兩篇文章中我們已經(jīng)分析了 Android APK 加固最基礎的一層防護第一層是把原始 DEX 從 APK 中拿走。第二層是把 DEX 加密讓逆向工具無法直接從 APK 中得到完整的 classes.dex。但是僅僅做到 DEX 加密并不能解決所有問題。因為 Android 應用最終還是需要運行。無論 DEX 在 APK 中如何加密應用啟動以后代碼最終還是需要進入 Android Runtime也就是 ART 的執(zhí)行體系。因此攻擊者真正關心的問題會逐漸從“APK 里面有沒有完整 DEX”轉(zhuǎn)變?yōu)椤斑\行時能不能把 DEX 恢復出來”進一步又會變成“能不能只針對關鍵方法進行分析”這也是方法級保護存在的意義。XopProtector 在整體保護體系中增加了 PVM1原始 DEX ↓ 定位目標方法 ↓ 抽取方法 Dalvik 指令 ↓ PVM1 編碼 ↓ AES-GCM 加密 ↓ 原方法體替換成安全占位 Stub ↓ 生成 code.bin ↓ 運行時 Native Shell 解密 ↓ 恢復 Dalvik 指令 ↓ 寫回 DEX ↓ ART 執(zhí)行因此PVM1 的核心思想并不是“讓代碼永遠不出現(xiàn)”。而是把關鍵方法從正常 DEX 的 code_item 中抽離出來讓靜態(tài) DEX 分析工具看到的只是一個占位方法真正的方法實現(xiàn)被移動到獨立的加固數(shù)據(jù)區(qū)中并在運行時恢復。XopProtector 源碼明確將--vmp-prefix定義為 PVM1并特別說明它是 ?**“decode → write Dalvik”**?而不是 Interpreter。二、先搞清楚PVM1 到底是什么很多人看到“VMP”“Virtual Machine”“虛擬化”這些詞第一反應就是原始代碼 ↓ 虛擬指令 ↓ Virtual Machine ↓ Interpreter這種理解對于 XopProtector 的PVM2才成立。對于 ?PVM1?并不是這樣。XopProtector 的VmCodec.java對 PVM1 的注釋非常直接Lightweight method-level VM packing (PVM1) Dalvik → non-Dalvik image Runtime unpacks inside .bitcode before writing DEX. This is virtualized packing, not a full bytecode interpreter.也就是說PVM1 更準確的技術定義應該是Method-Level Virtualized Packing方法級虛擬化打包而不是True Code Virtualization真正的代碼虛擬化。這兩個概念一定要區(qū)分。三、PVM1 和 PVM2 到底有什么區(qū)別這是整個系列最容易混淆的地方??梢灾苯佑孟旅孢@張表理解特性PVM1PVM2保護粒度方法級方法級是否抽取原始指令是是是否生成虛擬化數(shù)據(jù)是是是否存在 Interpreter否是是否恢復 Dalvik是否是否寫回 DEX是否最終執(zhí)行者ARTNative Interpreter核心目的提高靜態(tài)逆向成本改變代碼執(zhí)行模型對運行時 Hook 的抵抗能力中等更強性能開銷相對較小更高實現(xiàn)復雜度較低很高XopProtector README 對兩者的定義非常明確--vmp-prefix PVM1 unpack → write Dalvik not an interpreter --true-vmp-prefix PVM2 JNI trampoline native interpret因此PVM1 是“代碼搬家 編碼保護 運行時恢復”。而PVM2 是“代碼翻譯 自定義指令集 Native Interpreter”。這也是為什么 XopProtector 把 PVM1 和 PVM2 設計成兩個不同階段。四、PVM1 的核心從“類級保護”下降到“方法級保護”傳統(tǒng) DEX 加密往往是classes.dex ↓ 整體加密運行時整個 DEX ↓ 整體解密 ↓ 交給 ART這種方式的缺點很明顯一旦整個 DEX 被恢復攻擊者就獲得了大量可分析代碼。而 PVM1 改變了保護粒度。它不再簡單地把整個 DEX 看成一個整體而是進一步進入DEX ├── Class A │ ├── method A() │ ├── method B() │ └── method C() │ ├── Class B │ ├── method D() │ └── method E() │ └── Class C └── method F()然后選擇其中關鍵方法method B() method D() method F()進行抽取。最終形成DEX ├── 普通方法 → 正常保留 │ ├── PVM1 方法 → 抽空 │ ↓ │ code.bin │ └── PVM2 方法 → 后續(xù)真正虛擬化因此PVM1 的第一個重要思想就是保護從“DEX 級”進一步下降到了“Method 級”。五、第一步找到需要保護的方法XopProtector 的 Packer 是在構建階段工作的。源碼中的PackerMain會讀取 DEX并遍歷其中的 ClassDef、ClassData 以及 DirectMethods / VirtualMethods。核心流程可以抽象為APK ↓ 解包 ↓ classes.dex ↓ 解析 DEX ↓ 遍歷 ClassDef ↓ 定位目標 Class ↓ 遍歷 DirectMethod ↓ 遍歷 VirtualMethod ↓ 提取 code_item源碼中的walkDexMethods()就承擔了這一職責。它會遍歷dex.classDefs()然后讀取classData.getDirectMethods() classData.getVirtualMethods()再逐個調(diào)用extractOne(...)處理方法。六、PVM1 并不是所有方法都保護這是一個非常重要的設計。XopProtector 并不是無腦把所有方法都轉(zhuǎn)換成 PVM1。源碼中存在vmpPrefixes對應--vmp-prefix也就是說可以按照類描述符前綴選擇需要進行 PVM1 處理的代碼。例如--vmp-prefix Lcom/example/security/那么Lcom/example/security/Foo; Lcom/example/security/Pay; Lcom/example/security/License;等類中的方法就可能進入 PVM1 流程。源碼中明確維護了vmpPrefixes trueVmpPrefixes hollowPrefixes三個不同維度。這實際上構成了普通代碼 ↓ Hollow PVM1 代碼 ↓ PVM1 Packing PVM2 代碼 ↓ True VMP因此可以針對不同代碼價值選擇不同保護等級。七、第二步讀取方法真正的 Dalvik 指令找到目標方法以后PVM1 并不是簡單地復制整個 MethodId。它真正需要保護的是方法的 code_item 中的 Dalvik instruction stream。源碼中com.android.dex.Codecodedex.readCode(method);然后short[]unitscode.getInstructions();再根據(jù)units.length * 2計算實際指令區(qū)域大小。之后通過method.getCodeOffset()16定位到 code_item 中真正的 instructions 區(qū)域。然后raf.seek(insnsOffset);raf.readFully(original);把原始 Dalvik 指令讀取出來。所以這里可以把 PVM1 的第一核心動作總結成Method ↓ CodeItem ↓ instructions ↓ byte[]也就是把原方法的 Dalvik 指令從 DEX 中物理抽取出來。八、第三步PVM1 編碼到底做了什么現(xiàn)在進入 PVM1 最核心的VmCodec。XopProtector 的 PVM1 數(shù)據(jù)擁有一個非常明顯的 MagicPVM1源碼privatestaticfinalbyte[]MAGIC{P,V,M,1};編碼后的數(shù)據(jù)結構可以簡單理解成---------------- | PVM1 | ---------------- | encoded byte 0 | ---------------- | encoded byte 1 | ---------------- | encoded byte 2 | ---------------- | ... | ----------------值得注意的是PVM1 編碼后的數(shù)據(jù)長度基本等于原始 Dalvik 指令長度 4 字節(jié) Magic。它并沒有像真正的虛擬機那樣把一條 Dalvik 指令重新編譯成復雜的 VM 指令流。這也是為什么稱它為Virtualized Packing而不是True Virtualization源碼明確說明 PVM1 編碼結果是same length 4即原始長度加上PVM1四字節(jié)頭。九、PVM1 的第一層編碼基于 Method Index 的 Key StreamPVM1 的編碼并不是簡單byte ^ 0x55它會根據(jù)methodIdx以及byte offset生成一個簡單的動態(tài)字節(jié)流。源碼keystream(methodIdx, i)其核心計算為(methodIdx * 131 i * 17 0xA5) 0xff因此Key f(methodIndex, byteOffset)然后encodedByte originalByte ^ key這樣不同 Method Index 的編碼結果就不會完全相同。十、PVM1 的第二層Nibble Swap除了 XORPVM1 還做了一層非常輕量的字節(jié)變換。源碼中if((i1)0){b((b4)0xf0)|((b4)0x0f);}也就是對偶數(shù)位置的字節(jié)進行高低 4 bit 交換。例如原始 1010 0011經(jīng)過 Nibble Swap0011 1010所以 PVM1 的實際編碼邏輯可以抽象為Dalvik Byte ↓ XOR KeyStream ↓ 偶數(shù)位置 Nibble Swap ↓ PVM1 Blob解碼時反過來PVM1 Blob ↓ Nibble Swap ↓ XOR KeyStream ↓ 原始 Dalvik Byte因此這個過程本質(zhì)上是為了破壞原始 Dalvik 指令的線性特征讓靜態(tài)掃描器無法直接把這一段數(shù)據(jù)當作正常 DEX 指令流解析。十一、但是 PVM1 真正的安全邊界并不在這個 XOR這一點非常重要。如果只看methodIdx i 131 17 0xA5 XOR你會發(fā)現(xiàn)這并不是現(xiàn)代密碼學意義上的強加密。實際上 XopProtector 的真正安全邊界來自后面那一層AES-GCMPVM1 編碼只是Dalvik ↓ PVM1 Transform ↓ AES-GCM源碼中extractOne()的流程非常清楚original Dalvik instructions ↓ VmCodec.encode() ↓ PVM1 blob ↓ CryptoUtils.aesGcmEncrypt() ↓ stored也就是說PVM1 負責改變數(shù)據(jù)形態(tài)AES-GCM 負責真正的數(shù)據(jù)機密性。十二、第四步原始方法代碼被真正“抹掉”這是 PVM1 最關鍵的一步。如果只是復制一份代碼那么原 DEX 里面仍然存在原始代碼。保護就沒有意義。所以 XopProtector 在提取完方法指令以后會直接修改原來的 DEX。流程原始 Method Code ↓ 讀取 ↓ 保存到 PVM1 ↓ 原位置寫入 Stub源碼中writeReturnStub(...)會把原始 instructions 替換成一個與返回類型匹配的占位代碼。例如void → return-void int → const/4 v0, 0 → return v0 object → const/4 v0, 0 → return-object v0剩余空間則用nop類指令填充。十三、為什么不能直接把方法體全部清零這是 Android ART 加載過程中的一個關鍵問題。DEX 并不是Class Method Code隨便寫什么都可以。ART 在加載、驗證以及后續(xù)執(zhí)行過程中會檢查 Method 的結構和 code_item。如果直接code_item 0或者破壞整個 CodeItem很容易造成DEX 驗證失敗 VerifyError Class loading failure Crash所以加固系統(tǒng)常見的思路是不破壞方法結構只替換真正的業(yè)務指令。XopProtector 也是這樣做的。例如原方法intadd(inta,intb){returnab;}原來的 Dalvik 指令可能類似add-int returnPVM1 后變成const/4 v0, 0 return v0 nop nop ...真正的add-int已經(jīng)被拿走。這就是所謂Hollow / Method Hollowing也就是方法空洞化。十四、因此 PVM1 最重要的結構變化是這樣的加固之前classes.dex Method A ↓ CodeItem ↓ 真正業(yè)務 Dalvik 指令加固之后classes.dex Method A ↓ CodeItem ↓ 安全 Stub與此同時assets/protector/code.bin ↓ PVM1 ↓ AES-GCM ↓ 真正的 Dalvik 指令形成┌─────────────────────┐ │ classes.dex │ │ │ │ Method A │ │ ↓ │ │ Stub / Hollow │ └──────────┬──────────┘ │ │ runtime restore ↓ ┌─────────────────────┐ │ code.bin │ │ │ │ Method Index │ │ Plain Size │ │ Flags │ │ AES-GCM Blob │ └─────────────────────┘十五、PVM1 的第五步生成 code.bin方法被抽取以后需要一個地方保存這些方法。XopProtector 使用code.bin作為運行時方法代碼倉庫。源碼中的writeCodeBin()會將不同 DEX 中的保護方法進行組織。當前代碼使用的是code.bin v4。其結構可以抽象為Header ↓ version ↓ dex count ↓ dex offsets ↓ Dex Blob每個方法記錄大致包含methodIndex plainInsnsSize storedInsnsSize flags insns也就是Method Index ↓ 告訴 Runtime “這個代碼屬于哪個方法” Plain Size ↓ 解密以后需要恢復多少字節(jié) Stored Size ↓ 當前加密數(shù)據(jù)長度 Flags ↓ 告訴 Runtime PVM1 / PVM2 / 其他類型 Insns ↓ AES-GCM 加密后的方法數(shù)據(jù)源碼中writeCodeBin()明確寫入了methodIndex plainInsnsSize insns.length flags insns并且支持多 DEX。十六、為什么必須保存 Method Index這是整個方法級保護體系的“索引核心”。DEX 中的方法是通過method_ids進行編號的。例如method_id #100 method_id #101 method_id #102PVM1 不需要在code.bin中保存一套完整的 Java/Kotlin 方法名。它可以直接利用methodIndex關聯(lián)DEX Method ? code.bin Record因此 Runtime 可以實現(xiàn)methodIndex 102 ↓ 找到 code.bin 中 #102 ↓ AES-GCM decrypt ↓ PVM1 decode ↓ 得到真實 Dalvik instructions ↓ 恢復 Method #102這就是 PVM1 的“方法級映射關系”。十七、PVM1 運行時到底發(fā)生了什么這部分是整個機制最值得分析的地方。很多人會誤以為啟動 APP ↓ 整個 code.bin 解密 ↓ 整個 DEX 恢復實際上從源碼結構來看XopProtector 的 Runtime 是圍繞code.bin code_map Method ART Hook組織起來的。Native Shell 啟動以后會先定位dexes.zip code.bin config.json然后加載相關 Key。之后code.bin ↓ read_file() ↓ codeitem::parse() ↓ state.code_mapRuntime 將保護方法建立成內(nèi)部映射。源碼protector::codeitem::parse(...)解析完成以后state.code_map就成為運行時的方法保護索引。十八、Runtime 為什么要 Hook ART這里就涉及 Android 加固真正困難的地方。如果Method A在 DEX 中已經(jīng)被替換成return 0那么 ART 自己執(zhí)行的時候自然只會執(zhí)行return 0它不知道真正代碼在哪里。所以必須在ART 加載 / 定義 Class / Method的關鍵路徑上進行干預。XopProtector 在初始化階段會protector::hook::install_hooks();然后再解析和應用code.bin源碼明確說明在解析 / 應用 code.bin 前安裝 ART hooks以便 DefineClass 時進行 patch。因此整個體系實際上形成DEX ↓ ART ↓ Hook ↓ 識別被保護 Method ↓ 查 code_map ↓ 恢復真實 Dalvik ↓ 交給 ART十九、PVM1 的運行時恢復流程可以把它完整畫成App 啟動 │ ▼ Native Shell 初始化 │ ▼ 讀取 code.bin │ ▼ codeitem::parse │ ▼ 建立 code_map │ ▼ 安裝 ART Hook │ ▼ ART 加載目標 Class │ ▼ 檢查 Method 是否受保護 │ ┌──────┴──────┐ │ │ 否 是 │ │ ▼ ▼ 正常執(zhí)行 查詢 code_map │ ▼ AES-GCM 解密 │ ▼ PVM1 Decode │ ▼ 得到真實 Dalvik 指令 │ ▼ Patch CodeItem │ ▼ ART 執(zhí)行這就是 PVM1 最核心的運行時機制。二十、PVM1 的“虛擬化”到底體現(xiàn)在哪里現(xiàn)在回到最開始的問題PVM1 到底算不算虛擬化答案是算但屬于非常輕量級的“虛擬化打包”而不是完整 VM 虛擬化。因為它確實把原始代碼Dalvik instructions轉(zhuǎn)換成了PVM1 image原始代碼不再直接位于原 Method CodeItem 中。但是它沒有Dalvik opcode ↓ PVM opcode ↓ VM Register ↓ VM Stack ↓ Dispatcher ↓ Handler這一整套機制。因此PVM1 Dalvik → protected image → Dalvik而PVM2 Dalvik → custom VM bytecode → Native Interpreter這才是真正意義上的Code Virtualization二十一、為什么 PVM1 比真正虛擬化簡單很多假設原始代碼intcalc(inta,intb){intxab;returnx*10;}PVM1 的目標只是隱藏 add-int mul-int return然后運行時恢復 add-int mul-int returnART 繼續(xù)執(zhí)行。所以 PVM1 并不需要理解add-int mul-int if goto invoke new-instance monitor try/catch的語義。它只需要保存 ↓ 解碼 ↓ 恢復因此實現(xiàn)成本相對較低。二十二、真正的 PVM2 為什么會復雜得多假設 PVM2 也拿到add-int mul-int return它不會把這些指令恢復到 DEX。而是轉(zhuǎn)換成自己的VM_ADD VM_MUL VM_RETURN然后Native Interpreter switch(opcode) { case VM_ADD: ... case VM_MUL: ... case VM_RETURN: ... }于是ART ↓ JNI Trampoline ↓ Native Interpreter ↓ VM Opcode ↓ Handler整個執(zhí)行模型都發(fā)生改變。XopProtector 當前 PVM2 文檔也明確說明PVM2 方法不會恢復到 Dalvik而是通過 JNI trampoline 進入 Native Interpreter讀取code.bin中的 PVM2 image。這就是下一篇文章真正需要討論的內(nèi)容。二十三、PVM1 對靜態(tài)逆向的意義假設沒有 PVM1classes.dex ↓ jadx ↓ Java/Kotlin-like code攻擊者可以直接看到calculateToken()validateLicense()checkSignature()generateKey()而加入 PVM1 后classes.dex ↓ 目標方法 ↓ Stub靜態(tài)分析工具看到的可能只是return0;或者returnnull;真實邏輯已經(jīng)不在 Method CodeItem 中。所以靜態(tài)反編譯結果 ≠ 真實業(yè)務邏輯這就是 PVM1 最直接的價值。二十四、但是 PVM1 并不是“不可逆”這一點在技術文章中必須客觀說明。PVM1 的最終執(zhí)行路徑仍然是PVM1 ↓ Decode ↓ Dalvik ↓ ART所以從攻擊者角度靜態(tài)分析 ↓ 難度增加 運行時動態(tài)分析 ↓ 仍然可以觀察 最終恢復后的 Dalvik ↓ 仍然存在因此PVM1 的目標不是讓代碼永遠無法獲得而是增加靜態(tài)分析和自動化脫殼的成本。這也符合 XopProtector 項目本身的定位加固的作用是提高逆向成本而不是保證應用絕對不可破解。二十五、PVM1 最大的價值其實是“方法級明文控制”如果把保護過程畫成時間軸APK │ │ 加密狀態(tài) ▼ App 啟動 │ ▼ Runtime 解密 │ ▼ PVM1 Method Decode │ ▼ Dalvik Method 明文 │ ▼ ART 執(zhí)行關鍵區(qū)別是以前整個 DEX ↓ 大量代碼同時明文PVM1Method A ↓ 需要時恢復 Method B ↓ 需要時恢復 Method C ↓ 需要時恢復因此保護對象從“整個代碼包”變成“一個個關鍵方法”。這就是方法級加固最重要的工程價值。二十六、從源碼看PVM1 實際上是“三層保護疊加”如果把 XopProtector 的 PVM1 單獨拆開可以得到第一層 Method Hollowing ↓ 從 DEX 中移除真實實現(xiàn) 第二層 PVM1 Transform ↓ XOR Nibble Swap ↓ 破壞原始 Dalvik 數(shù)據(jù)特征 第三層 AES-GCM ↓ 真正的數(shù)據(jù)機密性所以它并不是PVM1 XOR也不是PVM1 AES而是Method │ ▼ ┌───────────────┐ │ Method Extract│ └───────┬───────┘ ▼ ┌───────────────┐ │ PVM1 Transform │ │ XOR Nibble │ └───────┬───────┘ ▼ ┌───────────────┐ │ AES-GCM │ └───────┬───────┘ ▼ code.bin而 DEX 中只留下Stub二十七、PVM1 和傳統(tǒng)“代碼抽取”有什么區(qū)別如果只說“PVM1 就是把代碼抽出來。”其實不夠準確。因為普通代碼抽取可能只是DEX ↓ 抽取 Method ↓ 保存到其他文件但是 PVM1 還增加了Method Index PVM1 Encoding AES-GCM Method Hollowing ART Runtime Restore所以完整體系是代碼抽取 代碼變形 加密存儲 原位置空洞化 運行時恢復這才構成完整的 PVM1。二十八、PVM1 的完整生命周期把整個源碼實現(xiàn)濃縮成一條鏈【構建階段】 APK │ ▼ 解包 DEX │ ▼ 遍歷 ClassDef │ ▼ 找到目標 Method │ ▼ 讀取 CodeItem │ ▼ 提取 Dalvik Instructions │ ▼ VmCodec.encode │ ▼ PVM1 Blob 生成 │ ▼ AES-GCM 加密 │ ▼ code.bin │ ├───────────────┐ │ │ ▼ ▼ 原 Method 被抹掉 Stub │ ▼ DEX 重寫 │ ▼ APK 重新打包 【運行階段】 App 啟動 │ ▼ Native Shell │ ▼ 讀取 code.bin │ ▼ codeitem::parse │ ▼ 建立 code_map │ ▼ 安裝 ART Hook │ ▼ ART 加載 Class │ ▼ 找到受保護 Method │ ▼ 查詢 code_map │ ▼ AES-GCM 解密 │ ▼ PVM1 Decode │ ▼ 得到真實 Dalvik │ ▼ Patch Method │ ▼ ART │ ▼ 正常執(zhí)行這條鏈基本就是 XopProtector PVM1 的核心原理。二十九、PVM1 真正解決了什么問題可以總結成四個字靜態(tài)不可見。更準確地說1. 靜態(tài) DEX 中不再存在完整方法實現(xiàn)攻擊者拿到 DEX 后目標方法已經(jīng)被替換成 Stub。2. 方法實現(xiàn)被移動到獨立數(shù)據(jù)區(qū)真實代碼進入code.bin而不是繼續(xù)留在原來的 CodeItem 中。3. PVM1 破壞原始 Dalvik 數(shù)據(jù)形態(tài)通過XOR KeyStream Nibble Swap讓數(shù)據(jù)不再表現(xiàn)為正常 Dalvik instruction stream。4. AES-GCM 提供真正的機密性即使攻擊者找到code.bin也不能簡單通過strings dexdump jadx直接獲得原始代碼。三十、但 PVM1 還有一個天然弱點這也是為什么 XopProtector 后面還需要 PVM2。PVM1加密 ↓ 恢復 ↓ Dalvik ↓ ART所以最終真實 Dalvik還是要出現(xiàn)。因此攻擊者如果把分析重點從APK 靜態(tài)分析轉(zhuǎn)向Runtime ↓ ART Hook ↓ Method Restore ↓ Memory Dump就可能重新獲取真實方法。所以 PVM1 的安全模型是提高靜態(tài)分析成本 增加脫殼復雜度 縮短攻擊者直接獲得代碼的路徑 但是 最終仍然回到 ART Dalvik 執(zhí)行體系這也是 PVM1 與 PVM2 最根本的安全邊界。三十一、PVM1 → PVM2是 Android 加固的一次質(zhì)變可以把整個演進過程理解為第一階段 DEX 加密 攻擊者 APK ↓ 找到解密點 ↓ 得到 DEX 第二階段 PVM1 攻擊者 APK ↓ 找到 code.bin ↓ 找到 Runtime Restore ↓ 獲取 Dalvik 第三階段 PVM2 攻擊者 APK ↓ 找到 JNI Trampoline ↓ 找到 Native Interpreter ↓ 理解自定義 VM ↓ 分析 VM Opcode ↓ 恢復原始語義所以難度是逐層提升的。三十二、PVM1 最值得學習的工程設計從工程實現(xiàn)角度看PVM1 并沒有試圖一步做到極端復雜。它采取的是一種非常實用的思路DEX 加密 ↓ 解決“APK 靜態(tài)暴露” PVM1 ↓ 解決“關鍵方法暴露” PVM2 ↓ 解決“恢復后仍然是 Dalvik” SO 加密 ↓ 解決 Native 代碼暴露 RASP ↓ 解決運行時攻擊也就是說不同保護技術解決不同攻擊面。這比單純依賴一種“超級加密算法”更加符合商業(yè) APK 加固系統(tǒng)的工程思路。三十三、源碼層面的關鍵文件如果你準備繼續(xù)深入研究 XopProtector那么 PVM1 最值得看的幾個源碼位置是packer/ └── src/main/java/com/yqsh/protector/packer/ │ ├── PackerMain.java │ ├── walkDexMethods() │ ├── extractOne() │ ├── writeReturnStub() │ └── writeCodeBin() │ └── VmCodec.java ├── encode() └── decode()其中PackerMain.java負責掃描 DEX ↓ 定位 Method ↓ 抽取指令 ↓ 替換 Stub ↓ 生成 code.binVmCodec.java負責PVM1 Encode PVM1 DecodeNative 側(cè)則對應native/src/main/cpp/vm/ └── vm_codec.cpp負責運行時PVM1 DecodeNative Runtimenative/src/main/cpp/runtime/ └── engine.cpp負責初始化 ↓ 加載 code.bin ↓ 建立 code_map ↓ 安裝 Hook ↓ 進入運行時保護流程這些源碼結構可以非常清晰地證明PVM1 并不是一個獨立的 VM而是 Packer Native Runtime ART Hook 三者協(xié)同完成的方法級保護機制。三十四、最終總結一句話理解 XopProtector PVM1如果只用一句話解釋PVM1 就是在構建階段把關鍵方法的 Dalvik 指令從 DEX 中抽出來經(jīng)過 PVM1 變換和 AES-GCM 加密后保存到code.bin原 Method 只留下與返回類型匹配的安全 Stub應用運行時由 Native Shell 解析code.bin通過 ART Hook 找到目標 Method解密并恢復真實 Dalvik 指令再交給 ART 執(zhí)行。整個過程可以最終濃縮成PVM1 ┌───────────────────┐ │ 原始 Method │ └─────────┬─────────┘ │ ▼ 提取 Dalvik Code │ ▼ PVM1 Encode XOR NibbleSwap │ ▼ AES-GCM │ ▼ code.bin │ │ ┌─────────▼─────────┐ │ 原 DEX Method │ │ │ │ 真實 Code 被移除 │ │ ↓ │ │ Stub │ └───────────────────┘ Runtime Native Shell │ ▼ code.bin │ ▼ code_map │ ▼ ART Hook │ ▼ 找到目標 Method │ ▼ AES-GCM Decode │ ▼ PVM1 Decode │ ▼ 恢復 Dalvik │ ▼ Patch Method │ ▼ ART │ ▼ 執(zhí)行代碼所以PVM1 不是“讓代碼不再執(zhí)行”而是讓代碼不再以正常 DEX Method 的形式存在。這就是它和傳統(tǒng) DEX 加密最大的區(qū)別。而下一階段真正值得研究的問題就是如果連“恢復 Dalvik”這一步都不要了能不能讓被保護方法從始至終都不回到 DEX而是直接由 Native 自己解釋執(zhí)行答案就是PVM2。PVM2 不再是加密 → 解密 → 恢復 Dalvik → ART而會變成原始 Dalvik ↓ PVM2 Compiler ↓ 自定義 VM Image ↓ JNI Trampoline ↓ Native Interpreter ↓ VM Opcode ↓ 執(zhí)行XopProtector 當前源碼中的 PVM2 已經(jīng)進一步加入了多 ISA、Opcode Morphing、RASP Gate、解釋執(zhí)行以及 PVM2 Image等機制。PVM2 v3 還會為每個 APK 生成 opcode 映射并根據(jù)isa_id選擇不同 Native dispatch 入口。這才是真正意義上的 Android 代碼虛擬化。下一篇《Android APK 加固原理四真正的代碼虛擬化——PVM2 Native Interpreter 技術解析》將重點拆解Dalvik ↓ PVM2 Compiler ↓ VM Opcode ↓ PVM2 Image ↓ JNI Trampoline ↓ Native Interpreter ↓ Dispatcher ↓ Opcode Handler ↓ 寄存器 / 對象 / Field / Method ↓ 最終執(zhí)行并重點解釋為什么 PVM2 和 PVM1 已經(jīng)不是同一個層級的加固技術。參考源碼本文分析以 XopProtector 當前公開源碼為基礎重點涉及packer/PackerMain.javapacker/VmCodec.javanative/vm/vm_codec.cppnative/runtime/engine.cppPVM2 設計文檔項目公開 README 明確將--vmp-prefix定義為 PVM1將--true-vmp-prefix定義為 PVM2。