
簡介wxappUnpacker 是一套面向微信小程序開發(fā)者與安全研究人員的反編譯工具包用于將小程序包還原為可讀的 WXML、WXSS、JavaScript 與 JSON 代碼并支持主包與分包處理便于分析業(yè)務(wù)邏輯、優(yōu)化加載性能及開展安全審計。壓縮包共 780 個文件約 1.97MB以 js、json、md、ts、map 為主包含工具源碼、依賴說明、類型聲明與調(diào)試映射另有 license、cmd/ps1 腳本及配置文件目錄結(jié)構(gòu)較完整便于在本地搭建解析環(huán)境并按模塊查閱。已有 269 人學(xué)習(xí)說明其在逆向分析與小程序機制研究場景中有一定參考價值。讀者可據(jù)此了解反編譯流程與關(guān)鍵文件構(gòu)成對照主包、分包結(jié)構(gòu)梳理頁面配置、樣式與交互實現(xiàn)為代碼復(fù)用、問題排查和漏洞檢測提供線索。使用時需注意僅限合法合規(guī)場景遵守微信開發(fā)者協(xié)議與相關(guān)授權(quán)要求。1. 第一次運行 wxappUnpacker 處理小程序反編譯時我盯著控制臺那行Wrong Header!看了半小時包是我從自己的測試機里導(dǎo)出來的路徑?jīng)]寫錯Node 也沒崩依賴都在。問題出在包體本身首字節(jié)不是0xBE說明它在我動手之前就已經(jīng)被處理過一遍。這件事之后我養(yǎng)成一個習(xí)慣拿到任何一個 wxapkg先花兩分鐘看頭部和索引再決定要不要上 wxappUnpacker。小程序反編譯這件事工具只占三成剩下七成在“你能不能判斷這次還原算成功”。這篇按我實際的操作順序走先講容器里到底裝了什么再走一遍從小程序包到可讀工程的完整鏈路然后把我踩過的五類失敗現(xiàn)象逐條對上原因和解法最后說還原完之后怎么讀代碼才有價值。新手可以照著命令跑通熟手可以看我在參數(shù)和校驗點上的取舍。2. 容器與管線wxappUnpacker 處理的是哪幾層文件2.1 一個 wxapkg 里裝的不是壓縮包是索引表加數(shù)據(jù)區(qū)很多人第一次解包會下意識去找解壓密碼其實 wxapkg 根本沒有壓縮這一層。它是一個自定義二進(jìn)制容器結(jié)構(gòu)大致是一字節(jié)起始標(biāo)記0xBE接著三個四字節(jié)的長度字段一字節(jié)分隔標(biāo)記0xED然后是文件數(shù)量跟著一張索引表每條記錄包含“文件名長度 文件名 數(shù)據(jù)偏移 數(shù)據(jù)長度”索引表之后才是所有文件內(nèi)容首尾相接的數(shù)據(jù)區(qū)。所有多字節(jié)整數(shù)都是大端序這點在你自己寫腳本讀的時候特別容易翻車按小端解析出來會得到一堆荒唐的數(shù)字。位置字段長度說明0起始標(biāo)記1 字節(jié)固定0xBE被改過基本就是加密或二次封裝1保留字段4 字節(jié)大端整數(shù)不同構(gòu)建版本取值不一不用深究5索引區(qū)長度4 字節(jié)索引表總字節(jié)數(shù)9數(shù)據(jù)區(qū)長度4 字節(jié)所有文件內(nèi)容加起來的總長13分隔標(biāo)記1 字節(jié)固定0xED14文件數(shù)量4 字節(jié)索引條目數(shù)18 起索引條目變長文件名長度 / 文件名 / 偏移 / 長度真正的坑在數(shù)據(jù)區(qū)。解出來的東西離“源代碼工程”差得很遠(yuǎn)所有頁面的 JS 邏輯被define(pages/xxx/index.js, function(...){...})這樣的包裹串成一整個app-service.jsWXML 被壓成一行標(biāo)簽屬性引號被吞掉{{}}里的表達(dá)式被做過轉(zhuǎn)義WXSS 的import被內(nèi)聯(lián)展開rpx換算規(guī)則散落在各處。所以嚴(yán)格說這不是“反編譯”是“結(jié)構(gòu)還原”。我在評估一個包的時候從來不用“能不能還原出源碼”當(dāng)標(biāo)準(zhǔn)而是看“能不能還原到可讀、可搜索、可定位邏輯”的程度。還有一個常被忽略的點包里不只有代碼。圖片、字體、音頻這些二進(jìn)制資源是原樣存著的索引表里能拿到準(zhǔn)確的偏移和長度所以資源還原的準(zhǔn)確率接近 100%出問題的一定是代碼那幾層。把這條記住后面排查的時候能省一半時間——資源對得上、代碼對不上問題一定出在解析器不在容器。2.2 四個腳本各管一層串起來才是一次完整還原wxappUnpacker 不是一個單體程序是入口加幾個分工明確的腳本。入口wuWxapkg.js只干容器層的事校驗頭部、讀索引、按偏移把每個文件切出來寫盤然后根據(jù)文件類型決定要不要繼續(xù)往下交給別的腳本。真正的還原邏輯分散在wuJs.js拆app-service.js、wuWxml.js還原 WXML、wuWxss.js還原樣式、wuConfig.js處理app-config.json里。wuJs.js的做法值得單獨說。它用esprima把整個app-service.js解析成 AST然后在語法樹上遍歷找形如define(模塊名, 函數(shù)體)的調(diào)用節(jié)點把模塊名當(dāng)路徑、把函數(shù)體重新生成代碼寫出去。這個思路的成立前提是構(gòu)建產(chǎn)物還保留著可識別的define包裹。一旦構(gòu)建鏈路換成別的打包器、或者基礎(chǔ)庫版本變了把模塊表換了形式AST 里就找不到這些節(jié)點結(jié)果就是只得到一個幾十萬行的巨型 JS——這不是工具壞了是它的識別假設(shè)失效了。wuWxml.js更像一套規(guī)則集而不是編譯器。它做的是逆向壓縮動作把被吞掉的屬性引號補回來、把自閉合標(biāo)簽補全、把被轉(zhuǎn)義的表達(dá)式還原成{{}}、把被壓成一行的結(jié)構(gòu)按縮進(jìn)展開。所以它對“標(biāo)準(zhǔn)寫法”效果很好碰上自定義組件、新語法、或者被內(nèi)聯(lián)成字符串模板的片段還原質(zhì)量會斷崖式下降。wuWxss.js的邏輯類似主要處理import展開后的還原和rpx相關(guān)的換算。理解這條管線之后很多報錯的性質(zhì)就清楚了容器層報錯頭部、長度、索引是硬錯誤必須解決解析層報錯AST 遍歷失敗、正則匹配不上是軟錯誤經(jīng)常是“部分還原”還能繼續(xù)用。我一般會先把容器層跑通拿到全部原始文件再決定要不要花時間調(diào)解析層——因為光靠原始文件里的字符串搜索很多時候已經(jīng)夠定位問題了。2.3 三個開關(guān)決定你要不要返工入口腳本的參數(shù)不多但每一個都直接影響后面要不要重跑。-o表示只解容器、不做代碼還原跑完得到的是原始文件樹這個模式在我只想確認(rèn)包結(jié)構(gòu)和資源清單的時候特別省時間幾十秒出結(jié)果不會卡在 AST 解析上。-d用于處理子包。-s主包目錄指定主包解出來的路徑子包里的公共資源引用要靠它來定位。# 老版本解析依賴在新 Node 上容易拋錯建議固定到 12/14 nvm use 14 npm install # esprima / escodegen / js-beautify / css 等解析依賴 # 只解容器不還原代碼先看目錄結(jié)構(gòu)和資源清單快 node wuWxapkg.js -o ./pkg/__APP__.wxapkg # 完整還原產(chǎn)物輸出到與包同名的目錄 node wuWxapkg.js ./pkg/__APP__.wxapkg # 子包必須帶 -s且主包要先跑完順序反了會找不到公共路徑 node wuWxapkg.js -s./__APP__ ./pkg/sub1.wxapkg-o這個開關(guān)我建議所有人都先用一遍。原因很實際完整還原在大包上可能跑幾分鐘如果包體本身有問題你會在幾分鐘后拿到一個半成品然后再回頭懷疑是解析器的問題。先用-o確認(rèn)容器層干凈把變量一個個排除掉比一次性跑完再猜要快得多。這個習(xí)慣類似調(diào)試時先打日志再下斷點看著笨實際最省時間。3. 復(fù)現(xiàn)鏈路從小程序包到可讀工程的四步實操3.1 先定位包體、校驗頭部別急著上工具包體的位置和命名是有規(guī)律的。主包通常叫__APP__.wxapkg子包按分包名命名同一目錄下平鋪。我一般先把整個目錄拷出來按“主包 子包”分組然后對每個文件跑一遍頭部檢查。這一步的意義不在于“檢查能不能用”而在于“提前知道哪些包需要特殊處理”避免跑到一半才發(fā)現(xiàn)某個子包是加密的。import struct, sys, os def peek(path): with open(path, rb) as f: buf f.read() # 首字節(jié)不是 0xBE說明包體被處理過加密或二次封裝先別往下走 if not buf or buf[0] ! 0xBE: print(f{os.path.basename(path)}: 首字節(jié)異常跳過) return info1, idx_len, body_len struct.unpack(III, buf[1:13]) # 全程大端序 if buf[13] ! 0xED: print(f{os.path.basename(path)}: 分隔標(biāo)記異常索引區(qū)可能被替換) return count struct.unpack(I, buf[14:18])[0] print(f{os.path.basename(path)} 文件數(shù){count} 索引{idx_len} 數(shù)據(jù){body_len}) p 18 for _ in range(min(count, 10)): # 只打印前 10 條夠看結(jié)構(gòu)了 n struct.unpack(I, buf[p:p4])[0]; p 4 name buf[p:pn].decode(utf-8, replace); p n off, size struct.unpack(II, buf[p:p8]); p 8 print(f {name} off{off} size{size}) if __name__ __main__: for f in sys.argv[1:]: peek(f)這段腳本的關(guān)鍵在三個地方III里的是大端序?qū)懗蓵馕龀鐾耆珜Σ簧系臄?shù)字偏移off是相對于數(shù)據(jù)區(qū)起點的不是相對文件頭實際讀取時要加上數(shù)據(jù)區(qū)起始位置文件名用utf-8加replace容錯解碼因為個別包里的路徑是轉(zhuǎn)義過的。跑完這個你對包里有多少文件、有沒有異常條目、資源大概占多少心里就有數(shù)了。3.2 主包先跑、子包后跑順序錯了后面全白干分包機制是這套流程里最容易出錯的一環(huán)。主包里有公共組件、公共樣式、公共工具函數(shù)子包在引用這些資源時用的是相對主包根的路徑。wuWxapkg.js在處理子包時需要靠-s參數(shù)知道主包解到哪兒了才能把這些引用解析成真實文件。如果順序反了或者忘了帶-s你會得到一堆“找不到模塊”的報錯然后誤以為是解析器不支持。我的操作順序固定成三步先跑主包并確認(rèn)產(chǎn)物目錄結(jié)構(gòu)正確再逐個跑子包每個都帶上-s主包產(chǎn)物目錄最后把子包的產(chǎn)物合并回主包目錄沖突的文件按“主包優(yōu)先、子包覆蓋非公共路徑”處理。第三步不是工具做的是手工或者腳本做的因為合并策略取決于你要干什么——如果只是想搜索關(guān)鍵字合不合并無所謂如果要還原完整工程結(jié)構(gòu)就必須合。還有一個細(xì)節(jié)子包之間可能互相引用。這種情況下合并順序會影響結(jié)果我一般先按app.json里的分包聲明順序建一個依賴圖把葉子包先合公共包最后合。聽著麻煩但比事后對著幾十個“模塊未定義”試錯要快。3.3 五個檢查點判斷這次還原算不算成功跑完不知道成不成是最浪費時間的狀態(tài)。我給自己定了五個檢查點從粗到細(xì)依次過任何一個不通過就停下來先解決它不帶病往下走。檢查點怎么查不通過說明什么目錄結(jié)構(gòu)產(chǎn)物里是否有pages/、components/等目錄且與配置文件里的路徑一一對應(yīng)容器層解析有問題索引表讀錯了JS 拆分app-service.js是否被拆成多個文件文件名是否就是模塊路徑define包裹沒被識別AST 遍歷失效WXML 結(jié)構(gòu)標(biāo)簽是否成對閉合{{}}是否成對屬性引號是否補全還原規(guī)則沒有覆蓋到這批寫法WXSS 引用import是否已經(jīng)展開成真實樣式內(nèi)容樣式層還原被跳過或順序錯亂資源完整性抽幾張圖片對比字節(jié)數(shù)與原包索引記錄的長度數(shù)據(jù)區(qū)偏移計算有偏差屬于嚴(yán)重問題第五個檢查點最容易被跳過但它最能說明問題。資源是原樣存儲的索引表里有準(zhǔn)確長度如果產(chǎn)物里圖片字節(jié)數(shù)和索引記錄對不上那一定是“偏移計算”或者“數(shù)據(jù)區(qū)起點”錯了這時候去調(diào) JS 解析器純屬浪費時間。反過來如果資源全對但代碼不對問題就精確定位在解析層可以直接去看wuJs.js的遍歷邏輯有沒有匹配上。4. 排查清單五類常見故障的現(xiàn)象、原因和解法4.1 首字節(jié)不是0xBE入口直接報Wrong Header!現(xiàn)象腳本一啟動就退出提示頭部校驗失敗日志里連文件數(shù)量都沒打印出來。原因包體在你拿到之前已經(jīng)被處理過了常見于從某些環(huán)境導(dǎo)出的包頭部被替換或整體做了加密wuWxapkg.js的第一道校驗就過不去。解決先用 3.1 的腳本確認(rèn)首字節(jié)到底是什么如果是固定偏移的替換可以定位到原始標(biāo)記的位置再看后面結(jié)構(gòu)是否完整如果整個數(shù)據(jù)區(qū)都被加密那容器層工具無能為力得先解決解密再談還原。我一般的判斷是頭部異常但數(shù)據(jù)區(qū)長度字段合理說明只是頭被改連長度字段都是亂的說明整包被重寫過。4.2 只得到一個幾十萬行的app-service.js沒有拆成單文件現(xiàn)象容器層跑通了文件都出來了但 JS 目錄里只有一個巨型文件其余頁面文件一個沒有。原因wuJs.js靠識別define(模塊名, 函數(shù)體)這種調(diào)用形式來拆分模塊構(gòu)建產(chǎn)物一旦換成別的打包器或者模塊表改成了數(shù)組映射AST 里就找不到這個節(jié)點。解決先在這個巨型文件里搜define(和webpackJsonp看它用的是哪種模塊形式如果是define檢查是不是被包在了一層eval或者字符串里這種要先剝一層如果是別家的模塊表就自己寫個腳本按模塊表里的名稱數(shù)組和函數(shù)數(shù)組配對還原思路一樣只是入口節(jié)點不同。4.3 WXML 還原出來一堆殘缺表達(dá)式{{}}對不上現(xiàn)象結(jié)構(gòu)大致在但屬性值變成半截表達(dá)式里的變量名被截斷控制臺還刷了一堆正則匹配失敗的警告。原因這批模板用了自定義組件、新語法或者被壓縮成了字符串拼接形式wuWxml.js的規(guī)則集覆蓋不到。解決先別急著改規(guī)則降低期望——大多數(shù)排查場景里能在這堆文本里做關(guān)鍵字搜索就夠了。如果確實需要結(jié)構(gòu)就針對具體文件手工還原幾個關(guān)鍵模板別想著一次性全救回來。我吃過這個虧花了兩天調(diào)正則最后發(fā)現(xiàn)需要的三個關(guān)鍵字段在原始文本里直接搜就有。4.4 子包報“找不到模塊”路徑解析成一堆../現(xiàn)象子包單獨跑能出文件但引用主包資源的語句全變成了失效路徑報錯集中在公共組件和工具函數(shù)上。原因沒帶-s參數(shù)或者-s指向的目錄不是主包產(chǎn)物根目錄或者跑的順序反了——子包先跑主包目錄還不存在。解決嚴(yán)格按“主包先、子包后、每個子包都帶-s主包產(chǎn)物目錄”的順序重跑一遍如果還不行檢查-s的值是不是寫成了相對路徑且當(dāng)前工作目錄不對改成絕對路徑最穩(wěn)。這個錯誤我犯過不止一次后來干脆在腳本里寫死絕對路徑。4.5 Node 版本太新裝依賴時就崩現(xiàn)象npm install報錯或者跑起來拋SyntaxError: Unexpected token堆棧指向解析庫內(nèi)部。原因這類工具大多寫于 Node 10 到 12 的年代依賴的解析庫版本很老在新版本 Node 上語法或 API 對不上。解決用版本管理器切到 12 或 14重新裝依賴再跑。別試圖升級依賴版本來“修好”它解析庫的主版本一升esprima的 AST 節(jié)點結(jié)構(gòu)就變了wuJs.js里的遍歷邏輯會全部失效那才是真正的麻煩。我的做法是給這個工具單獨建一個環(huán)境目錄鎖死版本不跟其他項目混用。5. 讀懂還原產(chǎn)物把文件樹變成可評估的攻擊面5.1 先掃接口和密鑰再談邏輯還原出來的文件樹有幾百上千個文件從頭讀是不可能的。我的第一遍永遠(yuǎn)是關(guān)鍵字掃不看邏輯只看“面”。這一步的目標(biāo)不是找漏洞是畫出一張“這個包跟外界有多少條通路”的地圖。搜索關(guān)鍵詞想定位的東西后續(xù)動作request、http、api網(wǎng)絡(luò)請求封裝與接口前綴匯總出接口清單看有沒有內(nèi)網(wǎng)地址、測試環(huán)境地址token、sign、encrypt鑒權(quán)與簽名算法判斷簽名是在客戶端算還是服務(wù)端算key、secret、appid硬編碼憑據(jù)單獨列出來評估是否可被直接復(fù)用upload、download文件流轉(zhuǎn)路徑看有沒有未校驗的存儲路徑eval、Function(動態(tài)執(zhí)行點這類點在客戶端腳本里通常是風(fēng)險放大器掃完你會得到兩樣?xùn)|西一張接口表和一份“客戶端自己算出來的安全參數(shù)”列表。后者是重點。簽名算法放在客戶端意味著算法本身對攻擊者完全公開安全性只能靠密鑰而密鑰通常也在這個包里。這不是說一定有問題而是說這條鏈路的安全假設(shè)必須由服務(wù)端的二次校驗來兜底還原產(chǎn)物恰好是驗證“有沒有兜底”的最直接材料。5.2 客戶端校驗只是體驗層重點看服務(wù)端有沒有兜底還原之后最容易產(chǎn)生的錯覺是“這里能改所以有漏洞”。實際上客戶端的所有判斷都可以被繞過這句話在業(yè)內(nèi)是常識但真正做評估的時候還是容易上頭。我的做法是把所有客戶端校驗點列出來然后針對每一個去接口層找對應(yīng)的服務(wù)端校驗證據(jù)。找不到證據(jù)的才寫成風(fēng)險項。典型的幾類客戶端校驗金額在提交前算好并做格式化服務(wù)端如果直接采信這個值就是問題權(quán)限判斷靠本地緩存里的角色字段服務(wù)端如果不重新查就是問題次數(shù)限制在本地做計數(shù)服務(wù)端如果不記賬就是問題。這三類在還原產(chǎn)物里都能找到痕跡——格式化函數(shù)、角色字段的讀取點、計數(shù)變量的自增位置。找到痕跡之后判斷依據(jù)不是代碼寫得好不好看而是“這個值最終有沒有被送到服務(wù)端并被服務(wù)端復(fù)核”這需要結(jié)合接口表一起看。我在寫這類評估記錄時有個固定格式現(xiàn)象哪個文件的哪一行做了什么、影響路徑這個值怎么流到接口、判定依據(jù)服務(wù)端有沒有對應(yīng)校驗有就寫“已兜底”沒有就寫“待確認(rèn)”。這個格式看起來啰嗦但它能避免把“看起來不安全”寫成“確認(rèn)存在風(fēng)險”后者是要負(fù)責(zé)的。5.3 把一次性動作變成可重復(fù)流程單次還原的價值有限真正省時間的是把它變成流程。我現(xiàn)在的做法是按日期建目錄每次拿到新包就跑一遍固定腳本產(chǎn)物按版本歸檔然后用目錄對比看兩個版本之間改了哪些文件。這個動作在跟蹤同一套業(yè)務(wù)的不同版本時特別有效——改動點往往就是新功能或者剛修過的地方重點看那幾個文件比通讀整棵樹高效一個量級。#!/usr/bin/env bash set -e STAMP$(date %Y%m%d) OUT./dump/$STAMP mkdir -p $OUT # 主包先解產(chǎn)物目錄作為后續(xù)子包的 -s 基準(zhǔn) node wuWxapkg.js -o $OUT/__APP__ ./pkg/__APP__.wxapkg /dev/null node wuWxapkg.js $OUT/__APP__ ./pkg/__APP__.wxapkg # 子包按聲明順序逐個處理-s 必須是絕對路徑避免工作目錄漂移 for sub in ./pkg/sub*.wxapkg; do node wuWxapkg.js -s$(realpath $OUT/__APP__) $sub || echo skip: $sub done # 只保留業(yè)務(wù)代碼二進(jìn)制資源另存避免歸檔體積膨脹 find $OUT -type f \( -name *.png -o -name *.jpg -o -name *.ttf \) -delete echo done - $OUT腳本里|| echo skip這一句是刻意的子包處理失敗很常見但我更希望整批跑完再統(tǒng)一看哪些失敗而不是第一個失敗就中斷。歸檔時刪掉二進(jìn)制資源是為了控制體積文本產(chǎn)物才是后續(xù)對比和搜索的對象。這套流程跑順之后一個中等規(guī)模的包從拿到到可搜索大概十分鐘以內(nèi)。6. 一個具體技巧用目錄對比鎖定改動比通讀整棵樹高效得多如果只讓我留一個技巧就是這個用diff -rq對比兩個版本的還原產(chǎn)物只看差異文件列表。# 只列差異文件名不打印內(nèi)容幾千個文件也能一眼看完 diff -rq ./dump/20240110/__APP__ ./dump/20240118/__APP__ | grep -v ^Only in這個命令在跟蹤同一套業(yè)務(wù)的迭代時幾乎是作弊。業(yè)務(wù)方每次發(fā)版改動的文件通常只有幾十個剩下幾百個是原封不動的。你把差異列表拉出來按目錄聚類一眼就能看出這次動的是哪個模塊——如果集中在一個業(yè)務(wù)目錄那就是功能迭代如果散落在網(wǎng)絡(luò)層和工具目錄那就值得多看一眼因為這類改動往往涉及鑒權(quán)、加密或者請求封裝的調(diào)整是整個包里最需要復(fù)核的部分。我最深的一次教訓(xùn)跟這個技巧正好相反。早期我做評估拿到還原產(chǎn)物就一頭扎進(jìn)app-service.js幾百個模塊挨個讀讀了整整兩天寫出來的記錄全是“某某函數(shù)實現(xiàn)了某某功能”一句有用的判斷都沒有。后來帶我的前輩問了一句“你知道這次發(fā)版改了什么嗎”我答不上來。他讓我先做版本對比結(jié)果改動只集中在三個文件其中一個是簽名函數(shù)——把一段本地拼接改成了走服務(wù)端下發(fā)。這一處改動直接推翻了前面兩天的很多結(jié)論。從那以后我每次做這類分析都強制走一遍“先對比、再定點、最后才通讀”的順序不跳步。前面那些準(zhǔn)備工作——頭部校驗、-o先跑一遍、五個檢查點驗收——看著繁瑣本質(zhì)上都是同一個道理先把變量控制住再談結(jié)論。這份 wxappUnpacker 的整理版我放在資源里了入口腳本、依賴清單還有我這些年攢下來的報錯對照備注都在里面幾個典型失敗的現(xiàn)場處理方式也一并寫上去了拿去按第 3 章的順序跑一遍就能出結(jié)果。希望幫到你。本文還有配套的精品資源點擊獲取