戰(zhàn):從虛擬機(jī)原理到插樁還原)
“JSVMP反編譯之JD”入這個(gè)坑的朋友應(yīng)該都明白這幾個(gè)字母的分量。標(biāo)題里的JD不是Java Decompiler而是業(yè)內(nèi)對(duì)某大型電商平臺(tái)前端防護(hù)項(xiàng)目的一個(gè)通用代號(hào)。這家平臺(tái)的登錄、搜索、下單等核心鏈路上跑著一套相當(dāng)完整的JavaScript虛擬機(jī)保護(hù)方案也就是常說的JSVMPJavaScript Virtual Machine Protection。接口請(qǐng)求里那些加密簽名參數(shù)、請(qǐng)求頭里的動(dòng)態(tài)令牌背后基本都是這套VM在處理。這篇文章不是我給某家站點(diǎn)做“脫殼實(shí)錄”而是把JSVMP這類虛擬機(jī)保護(hù)當(dāng)作一個(gè)技術(shù)研究對(duì)象講講它的原理、我從哪里切入分析、實(shí)操時(shí)用了哪些思路、遇到哪些坑。文章里所有目標(biāo)對(duì)象都會(huì)做打碼和抽象處理實(shí)例也是在本地受控環(huán)境、自有測(cè)試賬號(hào)基礎(chǔ)上完成的靜態(tài)分析。適合正在做Web安全研究、風(fēng)控對(duì)抗、爬蟲反爬的研發(fā)同學(xué)也適合對(duì)JS混淆逆向感興趣、想把手藝練扎實(shí)的初學(xué)者。1. 先搞懂JSVMP到底做了什么1.1 它和普通JS混淆不是一回事很長(zhǎng)一段時(shí)間里前端代碼保護(hù)靠的是三類手段變量名混淆、字符串加密、控制流平坦化。這類手段老老實(shí)實(shí)說給分析者添的麻煩有限——變量名_0x3f2a1c看多了拿AST工具還原一下命名、把字符串?dāng)?shù)組解出來邏輯基本能讀。JSVMP不是一個(gè)量級(jí)的。它的思路是干脆不讓你直接看到“源碼”而是把你的業(yè)務(wù)邏輯編譯成一組自定義字節(jié)碼運(yùn)行時(shí)交給一個(gè)內(nèi)置的解釋器逐條執(zhí)行。這個(gè)解釋器本身就是一段正常的JS代碼里面有指令分派循環(huán)、有操作數(shù)棧、有執(zhí)行上下文跟我們要分析的目標(biāo)業(yè)務(wù)邏輯已經(jīng)沒有直接的對(duì)應(yīng)關(guān)系。打個(gè)比方普通的混淆像把菜譜里的字眼換成了別名你拿著字典還是能讀懂。JSVMP是把菜譜直接改成了一套密碼本廚師VM解釋器看著密碼本做菜你光拿到密碼本沒用還得先把廚師的“手藝規(guī)則”搞明白才能推斷出每道菜的做法。一個(gè)標(biāo)準(zhǔn)的JSVMP保護(hù)體系至少包含這幾個(gè)部分字節(jié)碼數(shù)組業(yè)務(wù)邏輯被翻譯成的一串opcode序列通常被編碼或者加密存放執(zhí)行上下文虛擬機(jī)運(yùn)行時(shí)保存中間變量、臨時(shí)結(jié)果的寄存器堆或棧結(jié)構(gòu)指令分派循環(huán)解釋器核心通常是 while switch / if-else 結(jié)構(gòu)逐條讀取opcode并分發(fā)Handler函數(shù)集合每個(gè)opcode對(duì)應(yīng)一個(gè)處理函數(shù)負(fù)責(zé)具體的運(yùn)算、賦值、跳轉(zhuǎn)、函數(shù)調(diào)用等操作字節(jié)碼和解釋器是“綁定關(guān)系”不同的VM方案指令集設(shè)計(jì)差異很大甚至同一套方案在不同版本下opcode含義都會(huì)變化。這也是為什么圈內(nèi)常說“逆向JSVMP沒有通用一鍵脫殼方案”每個(gè)目標(biāo)都得從零開始過一遍。1.2 JD場(chǎng)景下的JSVMP長(zhǎng)什么樣在JD這類大型平臺(tái)場(chǎng)景里JSVMP不是單獨(dú)一個(gè)文件而是拆在若干環(huán)節(jié)里一個(gè)體積很大的入口腳本通常以立即執(zhí)行函數(shù)IIFE包裹內(nèi)部變量名全部被壓縮成極短的英文/數(shù)字組合腳本里能看到 Uint8Array、Uint32Array 這類TypedArray說明字節(jié)碼不是以直觀字符串存在而是數(shù)值數(shù)組入口腳本運(yùn)行時(shí)會(huì)動(dòng)態(tài)加載一段與當(dāng)前請(qǐng)求綁定的字節(jié)碼數(shù)據(jù)。也就是說你請(qǐng)求一次頁面拿到的字節(jié)碼和下一次拿到的不一定相同——?jiǎng)討B(tài)化讓“一次逆向吃遍天下”的想法直接失效字符串、數(shù)字常量不會(huì)明文出現(xiàn)而是密文存儲(chǔ)需要執(zhí)行到特定handler位置才會(huì)被解密還原并立即參與運(yùn)算很多人第一次看到這類代碼第一反應(yīng)是——這么大一串怎么下手其實(shí)克制住“我要把整個(gè)VM還原成源碼”的想法任務(wù)反而清晰你不需要翻譯全部指令只需要找到業(yè)務(wù)邏輯里“輸入到輸出”的對(duì)應(yīng)關(guān)系定位到具體一個(gè)或幾個(gè)handler搞清楚關(guān)鍵算法就夠了。JD這類場(chǎng)景目標(biāo)絕大多數(shù)時(shí)候是簽名算法和加密參數(shù)生成邏輯不是整個(gè)頁面。2. 反編譯切入點(diǎn)怎么從逆向視角拆解VM2.1 先定位再動(dòng)手我對(duì)第一次碰JSVMP的朋友有一個(gè)建議別急著一上來就格式化文件、搜關(guān)鍵字。正確的順序一定是從業(yè)務(wù)出發(fā)從外往里推進(jìn)。具體可以這樣來打開瀏覽器DevTools的Network面板刷新頁面找到帶加密參數(shù)的請(qǐng)求接口把關(guān)鍵參數(shù)列出來例如簽名sign、令牌token、時(shí)間戳ts這些參數(shù)就是逆向分析的“結(jié)果集”在發(fā)起請(qǐng)求前給調(diào)用棧里的JS函數(shù)下事件斷點(diǎn)或者直接在發(fā)請(qǐng)求的入口處斷住順著調(diào)用棧往回追追到某個(gè)函數(shù)內(nèi)部發(fā)現(xiàn)它在執(zhí)行一段無法閱讀的代碼——典型特征就是大循環(huán)數(shù)值數(shù)組神秘函數(shù)調(diào)用那基本就是VM解釋器或外圍封裝我用過的最有效方式是在發(fā)起請(qǐng)求的關(guān)鍵函數(shù)上下斷點(diǎn)后查看“調(diào)用者”。JS的Call Stack會(huì)把從事件處理、業(yè)務(wù)函數(shù)、再到加密模塊、再到VM執(zhí)行器的調(diào)用鏈全部暴露出來。你不懂VM沒關(guān)系先知道它在哪兒、被誰調(diào)用了就已經(jīng)完成了一半。定位時(shí)要關(guān)注這些代碼特征一個(gè)函數(shù)體內(nèi)有大量連續(xù)的數(shù)字字面量例如數(shù)組 [3, 7, 1, 4, 9, ...]元素值范圍接近256以內(nèi)很可能是字節(jié)碼函數(shù)里出現(xiàn) while (true) 或 for(;;)配合大段switch-caseswitch里的 case 分支數(shù)量非常多幾十到上百個(gè)都是正常有大量的位運(yùn)算、數(shù)組下標(biāo)操作一眼看著“不像正常人寫的業(yè)務(wù)代碼”有人習(xí)慣用文件搜索專搜“opcode、dispatch、executor、vm”這些關(guān)鍵詞來釣VM入口。我不太推薦一開始就這么做因?yàn)榛煜^的變量名早就不是這些詞了??刻卣魉阉鬟m合已經(jīng)有了分析基礎(chǔ)、確認(rèn)目標(biāo)一定存在的階段不適合從零開始。2.2 解釋器入口與Handler分發(fā)表定位到疑似解釋器后下一步是把“指令分派循環(huán)”和“handler集合”區(qū)分開。這兩者經(jīng)?;煸谝黄鸬氊?zé)完全不同分派循環(huán)是骨架作用是讀opcode、跳轉(zhuǎn)到對(duì)應(yīng)處理分支。它本身不實(shí)現(xiàn)業(yè)務(wù)運(yùn)算Handler是具體干活的人每類opcode對(duì)應(yīng)一種操作比如棧頂兩個(gè)數(shù)相加、把常量加載到寄存器、調(diào)用某個(gè)內(nèi)置函數(shù)、根據(jù)條件跳轉(zhuǎn)區(qū)分方法很簡(jiǎn)單在可疑函數(shù)第一個(gè)可執(zhí)行行下斷點(diǎn)然后觸發(fā)一次業(yè)務(wù)調(diào)用觀察它是否在從未知入口進(jìn)來后反復(fù)命中同一個(gè)函數(shù)內(nèi)部。如果命中次數(shù)極多、且每次會(huì)有不同的case分支命中那這就是分派循環(huán)了。我習(xí)慣在分派循環(huán)里做一次“標(biāo)記”給case分支數(shù)量、外部入?yún)ⅰ⒀h(huán)退出條件打印一次日志。第一次跑日志會(huì)比較亂但通過對(duì)比不同業(yè)務(wù)輸入下的命中分布能更快找出跟目標(biāo)參數(shù)強(qiáng)相關(guān)的case集合。實(shí)際操作時(shí)還要注意解釋器外通常還套著一層“參數(shù)預(yù)處理”代碼負(fù)責(zé)解碼加密的字節(jié)碼常量表、初始化執(zhí)行上下文、校驗(yàn)運(yùn)行環(huán)境。這一層是JS代碼不是VM內(nèi)部邏輯讀到這塊時(shí)別懵它只是外圍包裝。3. 實(shí)操核心插樁Hook還原字節(jié)碼執(zhí)行邏輯3.1 動(dòng)態(tài)插樁的基本原理搞JSVMP最忌諱的就是純靜態(tài)硬看。字節(jié)碼指令流本身就是給機(jī)器執(zhí)行的人眼盯幾十上百行數(shù)值數(shù)組盯不出業(yè)務(wù)邏輯。正確的姿勢(shì)是“插樁”——在VM解釋器里加輸出把每條指令的執(zhí)行情況打出來生成一份可讀的執(zhí)行軌跡。有人管這叫Hook有人叫插樁本質(zhì)上都一樣在合適的位置插入日志代碼。而對(duì)JSVM場(chǎng)景合適的插入點(diǎn)通常就是分派循環(huán)的入口處——也就是每次要讀取下一個(gè)opcode、準(zhǔn)備進(jìn)入case分支之前。一個(gè)通用層面的演示思路是這樣我簡(jiǎn)化了邏輯重點(diǎn)展示思路不是直接套用某個(gè)真實(shí)目標(biāo)// 原始解釋器大概是這種結(jié)構(gòu) function runBytecode(code, ctx) { let pc 0; while (pc code.length) { const opcode code[pc]; switch (opcode) { case 0x01: // 執(zhí)行加法 ctx.stack.push(ctx.stack.pop() ctx.stack.pop()); break; case 0x02: // 加載變量 ctx.stack.push(ctx.params[code[pc]]); break; // ... 大量case } } }改成帶日志的版本最簡(jiǎn)單的做法是function runBytecode(code, ctx) { let pc 0; while (pc code.length) { const opcode code[pc]; // 插樁點(diǎn)記錄當(dāng)前指令位置、操作碼、以及數(shù)??煺?console.log([PC${pc - 1}] opcode${opcode}, JSON.stringify(ctx.stack)); switch (opcode) { // 分支保持原樣 } } }看著簡(jiǎn)單真實(shí)操起來有幾個(gè)細(xì)節(jié)要注意插樁只加日志不要改動(dòng)原執(zhí)行邏輯。一旦你手滑改了棧順序、跳轉(zhuǎn)條件執(zhí)行結(jié)果就變了日志也沒意義輸出量非常大一個(gè)簽名算法跑下來可能幾萬條日志。不要直接死盯控制臺(tái)把日志收集到數(shù)組里最后一次性導(dǎo)出到本地文件分析如果棧里存的是對(duì)象直接JSON.stringify可能循環(huán)引用要寫個(gè)安全的序列化函數(shù)或者只記錄棧長(zhǎng)度和值類型很多分析者還用過“二分插樁法”先所有handler都打日志跑通了再逐步關(guān)掉無關(guān)handler的日志只保留跟業(yè)務(wù)強(qiáng)相關(guān)的部分。這樣能減少干擾也更利于觀察核心邏輯。3.2 從字節(jié)碼軌跡到業(yè)務(wù)邏輯還原日志拿到了下一步是最花心思的環(huán)節(jié)——從一串路由記錄里還原出業(yè)務(wù)邏輯。我的習(xí)慣是這樣做把同一函數(shù)用不同參數(shù)調(diào)用多次記錄多份軌跡對(duì)比不同輸入下軌跡里哪個(gè)位置的值發(fā)生變化。變化的點(diǎn)就是“參數(shù)進(jìn)入VM的入口”對(duì)應(yīng)的handler負(fù)責(zé)讀取外部參數(shù)找到軌跡末尾、返回值產(chǎn)生的位置回溯這個(gè)值是怎么一步步算出來的把那一段軌跡里的handler逐個(gè)搞懂翻譯成JavaScript偽代碼舉個(gè)簡(jiǎn)化例子假設(shè)一個(gè)簽名函數(shù)輸入是字符串”abc”輸出是密文。軌跡日志里你看到這樣的模式指令0x05把輸入字符串加載到棧指令0x1a從常量表加載一段密文字節(jié)指令0x23執(zhí)行按字節(jié)異或指令0x0e把結(jié)果寫入輸出這樣就初步判斷所謂的簽名算法很可能就是輸入字符串和一個(gè)固定常量表做XOR最后轉(zhuǎn)成十六進(jìn)制。之后再驗(yàn)證幾次輸入輸出如果結(jié)論一致這個(gè)算法就算拿下了。這里有一個(gè)很關(guān)鍵的經(jīng)驗(yàn)不用完全理解每個(gè)handler。一個(gè)VM可能有上百個(gè)handler但一個(gè)具體簽名算法用的可能只有十幾個(gè)。你只需要把這十幾個(gè)的語義吃透其他保持未知一點(diǎn)關(guān)系沒有。這跟“反編譯”字面感覺的“全量還原”差別很大實(shí)際做逆向的人都是“按需還原”。還有個(gè)經(jīng)典技巧叫“差異對(duì)比法”。比如同一個(gè)函數(shù)在參數(shù)只差一個(gè)字符時(shí)執(zhí)行軌跡大部分相同只有局部變化。從變化區(qū)域附近的指令能非常精準(zhǔn)地定位參數(shù)如何參與運(yùn)算遠(yuǎn)比自己從頭到尾通讀字節(jié)碼高效。4. 解密參數(shù)與環(huán)境檢測(cè)JD反編譯里的硬骨頭4.1 字符串與數(shù)字常量加密JD這類場(chǎng)景的JSVMP一個(gè)很讓人頭疼的設(shè)計(jì)是常量加密。字節(jié)碼數(shù)組自己能拿到但代碼里引用的字符串、數(shù)字、甚至是handler跳轉(zhuǎn)表很多都是密文存儲(chǔ)。直接在字節(jié)碼文件里搜字符串是搜不到的。解密的方式通常有幾種簡(jiǎn)單XOR加密密文和固定key逐字節(jié)異或變種RC4偽隨機(jī)密鑰流與密文異或自定義的查表編碼把字節(jié)映射到另一套字符表有點(diǎn)像Base64但自定義了表調(diào)用了內(nèi)置函數(shù)例如把字符數(shù)組拆散到多個(gè)變量運(yùn)行時(shí)再拼接定位解密函數(shù)的方法本質(zhì)上還是靠插樁。在VM環(huán)境中任何明文字符串被使用的瞬間它一定經(jīng)過了“解密——使用”的過程。所以在handler插樁時(shí)特別關(guān)注以下兩類操作把字節(jié)數(shù)據(jù)轉(zhuǎn)換成字符串的指令類似CharCode、join操作加載一個(gè)字符串后立刻參與運(yùn)算的指令在插樁日志里搜“看起來像人話”的字符串往回倒一點(diǎn)就能看到解密操作的位置。經(jīng)驗(yàn)是一個(gè)大型VM的字符串解密通常集中在幾個(gè)固定handler里找到一次之后每次解密都會(huì)從同一條路徑走等于開了一個(gè)穩(wěn)定后門。我提個(gè)小建議插樁日志不要直接打明文因?yàn)榱刻笕菀装褳g覽器跑卡??梢园讶罩鞠葘懙絻?nèi)存數(shù)組等整套流程跑完再統(tǒng)一從控制臺(tái)導(dǎo)出。這樣既能保留完整軌跡又不影響頁面正常運(yùn)行。4.2 環(huán)境檢測(cè)與反調(diào)試大型平臺(tái)的VM里環(huán)境檢測(cè)幾乎是標(biāo)配。核心是防止逆向者在自己搭的Node.js或調(diào)試環(huán)境里跑字節(jié)碼。常見手段有檢測(cè)navigator關(guān)鍵屬性比如webdriver標(biāo)記、userAgent、platform、pluginsCanvas指紋通過渲染特定圖形再讀取像素檢測(cè)是否存在無頭瀏覽器特征時(shí)間戳校驗(yàn)記錄VM初始化時(shí)間二次調(diào)用時(shí)對(duì)比時(shí)間差判斷是否被“你等我算完再放行”式拖慢無限debugger在關(guān)鍵代碼路徑上加入debugger語句開發(fā)者工具一打開就反復(fù)斷住?;厮莼煜屽e(cuò)誤信息的調(diào)用棧變成一堆無意義函數(shù)名應(yīng)對(duì)思路要分清兩個(gè)假期目標(biāo)。如果你只是想分析算法最優(yōu)解不是跟反調(diào)試死磕到底而是創(chuàng)造“干凈的受控環(huán)境”用真實(shí)瀏覽器的同時(shí)對(duì)已知反調(diào)試函數(shù)做定向替換——比如把debugger語句替換成空函數(shù)把環(huán)境檢測(cè)的通路全部走正常值。關(guān)鍵是找到VM啟動(dòng)前的外圍初始化代碼那里的環(huán)境檢測(cè)是最密集的也是最值得Hook的。我自己踩過的坑是剛開始插樁時(shí)為了輸出日志改了一個(gè)handler里的變量賦值結(jié)果后續(xù)運(yùn)算全錯(cuò)。排查了很久才意識(shí)到不是環(huán)境檢測(cè)干擾而是我的插樁無意中破壞了執(zhí)行上下文。從那以后我定了個(gè)規(guī)矩——插樁代碼一律不影響原變量、不改變控制流、不修改棧內(nèi)容。日志用全局?jǐn)?shù)組收集插樁點(diǎn)只做push操作。5. 常見問題與排查技巧把這個(gè)階段常遇到的問題整理成表方便對(duì)號(hào)入座現(xiàn)象可能原因排查思路找不到VM入口入口腳本體積大、文件名隨機(jī)、變量名全混淆從帶加密參數(shù)的請(qǐng)求接口逆推調(diào)用棧在業(yè)務(wù)事件斷點(diǎn)處查看調(diào)用鏈插樁后頁面報(bào)錯(cuò)/白屏插樁改變了執(zhí)行上下文、棧順序或跳轉(zhuǎn)條件只加日志不改邏輯確認(rèn)日志函數(shù)無副作用用數(shù)組收集代替console.log解析出的指令流亂序字節(jié)碼數(shù)據(jù)本身有跳轉(zhuǎn)指令不能從頭線性解讀重點(diǎn)分析跳轉(zhuǎn)類handler以實(shí)際執(zhí)行順序?yàn)闇?zhǔn)而不是數(shù)組順序字節(jié)碼每次請(qǐng)求都不同平臺(tái)做了動(dòng)態(tài)化每次下發(fā)不同VM固定請(qǐng)求參數(shù)、固定環(huán)境、固定會(huì)話縮短分析窗口不要期望一次徹底通吃遇到無限debugger平臺(tái)開啟了反調(diào)試在進(jìn)入VM前替換debugger調(diào)用比如改寫Function構(gòu)造器日志量過大瀏覽器卡死插樁范圍太寬輸出頻繁只保留關(guān)鍵handler的日志用節(jié)流思路抽樣記錄棧長(zhǎng)度而不是每次全量記錄分析出的算法跑不通可能漏掉環(huán)境校驗(yàn)或時(shí)間窗校驗(yàn)檢查VM初始化時(shí)的環(huán)境參數(shù)、當(dāng)前時(shí)間戳是否參與運(yùn)算這些坑里最容易被忽視的是“字節(jié)碼是跳轉(zhuǎn)的”。很多新手以為字節(jié)碼數(shù)組從頭讀到尾就是業(yè)務(wù)邏輯但JSVMP保留了完整的分支、循環(huán)、函數(shù)調(diào)用語義執(zhí)行順序完全受跳轉(zhuǎn)指令控制。單純按順序打印出來的軌跡中間會(huì)穿插大量無用跳轉(zhuǎn)屬于正?,F(xiàn)象。看到這種不要慌重點(diǎn)看它跳去了哪個(gè)handler、跳了多少次。另外一個(gè)比較遲來的經(jīng)驗(yàn)就是一定要固定輸入輸出做對(duì)比測(cè)試。我一般是準(zhǔn)備三個(gè)不同輸入每次插樁分析都跑一遍。如果三組軌跡的關(guān)鍵運(yùn)算區(qū)間呈現(xiàn)可復(fù)現(xiàn)的模式那結(jié)論基本可信如果三次結(jié)果形態(tài)變化很大說明字節(jié)碼本身就是動(dòng)態(tài)變化的得先想辦法鎖住一個(gè)版本。6. 反編譯之后安全線與防御視角做JSVMP分析做到一個(gè)階段后很容易產(chǎn)生一個(gè)錯(cuò)覺——“這保護(hù)沒啥用反正都能分析出來”。說句實(shí)在話從純技術(shù)研究角度JSVMP沒有不可破解的只要運(yùn)行環(huán)境是開放的動(dòng)態(tài)分析總有路可走。但這不代表這類保護(hù)沒有價(jià)值。它的價(jià)值體現(xiàn)在“成本”和“時(shí)效”兩個(gè)維度。對(duì)攻擊者來說花幾天時(shí)間搞清楚一套VM的具體指令語義、還原某個(gè)簽名算法是可行的。但如果平臺(tái)方保持動(dòng)態(tài)更新字節(jié)碼、定期輪換VM結(jié)構(gòu)攻擊者每次都需要重新投入成本。這就是典型的攻防成本博弈。防御方不指望前端永不可破但希望讓你“破一次的成本大于收益”。從防御角度我從這些分析經(jīng)驗(yàn)里總結(jié)了幾條建議服務(wù)端校驗(yàn)永遠(yuǎn)不能缺。前端加密參數(shù)再復(fù)雜到最后都得過服務(wù)端校驗(yàn)服務(wù)端一定要有獨(dú)立的簽名驗(yàn)簽邏輯而不是單純信任前端傳來的參數(shù)JSVMP只保護(hù)關(guān)鍵鏈路。核心的簽名算法、令牌生成、風(fēng)控參數(shù)值得上VM但別把整個(gè)網(wǎng)站邏輯全塞進(jìn)去否則體積爆炸、維護(hù)成本拖垮開發(fā)效果也不一定好動(dòng)態(tài)化迭代比一次最強(qiáng)更重要。定期換字節(jié)碼生成策略、更新常量加密key、調(diào)整handler結(jié)構(gòu)比試圖一次性做到“絕對(duì)不可逆”要實(shí)在風(fēng)控體系要做在VM之外。結(jié)合設(shè)備指紋、行為序列、IP信譽(yù)、頻率檢測(cè)比單純指望JS保護(hù)更可靠安全合規(guī)是繞不開的底線。研究JSVMP、做漏洞分析、評(píng)估防護(hù)效果這些行為都應(yīng)該建立在合法授權(quán)的基礎(chǔ)上。自己的項(xiàng)目、自己負(fù)責(zé)的系統(tǒng)、有授權(quán)的測(cè)試目標(biāo)都是合理的對(duì)象。未經(jīng)授權(quán)分析線上業(yè)務(wù)系統(tǒng)、繞過簽名接口批量拿數(shù)據(jù)這類行為既違背安全研究倫理也可能觸碰法律紅線超出技術(shù)討論的范疇了。當(dāng)你能把一套VM從頭到尾捋順、還能指出它的薄弱環(huán)節(jié)時(shí)你的收獲其實(shí)比“破解某個(gè)算法”更值錢——你成了這套保護(hù)體系真正的理解者。往后的工作里你可以做出更貼合業(yè)務(wù)場(chǎng)景的防護(hù)設(shè)計(jì)也能在對(duì)抗中快速評(píng)估對(duì)方成本在哪、弱點(diǎn)在哪。我在實(shí)際操作中的體會(huì)是練手階段自己寫一個(gè)小型VM和配套的混淆器比直接去破解大型平臺(tái)效率高得多。寫VM的過程就是理解VM的過程把指令分派、常量加密、環(huán)境檢測(cè)都親手實(shí)現(xiàn)一遍再回過頭去分析JSVMP很多以前覺得云里霧里的東西突然就通了。這條路徑我推薦給每個(gè)想入這個(gè)方向的人基礎(chǔ)打得扎實(shí)盛宴才吃得下。