:wxappUnpacker 解包與源碼還原)
簡介面向微信小程序逆向分析與安全審計場景wxappUnpacker 是一套可將小程序包還原為可讀源碼的工具合集適用于希望研究主包與分包結(jié)構(gòu)、排查邏輯或開展合規(guī)安全檢測的開發(fā)者和安全研究人員。壓縮包共780個文件以374個 JavaScript 源碼、83個 JSON 配置、70個 TypeScript 文件及71個 Markdown 文檔為主另含依賴庫、命令行腳本和許可說明整體約1.97MB便于在本地按模塊查閱與運(yùn)行。已有269人學(xué)習(xí)說明其在相關(guān)學(xué)習(xí)群體中具備一定參考價值。借助這套工具讀者可圍繞 WXML、WXSS、JS 與 JSON 的還原結(jié)果理解頁面結(jié)構(gòu)、樣式組織、業(yè)務(wù)邏輯和配置關(guān)系并進(jìn)一步分析組件使用、事件處理與網(wǎng)絡(luò)請求等實現(xiàn)細(xì)節(jié)配套文檔與腳本也有助于復(fù)現(xiàn)處理流程、梳理依賴和定位常見問題適合需要做代碼閱讀、性能優(yōu)化或安全審計的中高級開發(fā)者參考。1. 從一堆 .wxapkg 說起為什么我要把反編譯流程跑通去年做一次內(nèi)部代碼審計對方給過來的東西很尷尬一個跑了三年的小程序源碼倉庫因為人員流動丟了分支線上還在正常跑本地只剩幾個 .wxapkg 緩存包。最壞打算是重寫但真重寫一個三年迭代的業(yè)務(wù)成本比想象中高得多。抱著試試看的心態(tài)用 wxappUnpacker 折騰了一晚上小程序反編譯出來的 wxml、wxss、js 拼回了七八成配合日志和接口文檔業(yè)務(wù)邏輯基本能梳理清楚。這篇就把這條鏈路完整攤開.wxapkg 里面到底裝了什么、四個腳本誰管哪一段、命令行參數(shù)怎么給、哪一步最容易翻車。寫給兩類人——手里有授權(quán)資產(chǎn)要做審計或找回源碼的以及想搞懂小程序編譯產(chǎn)物到底長什么樣的。2. 看懂 .wxapkg 再動手容器結(jié)構(gòu)、版本差異與授權(quán)邊界反編譯這件事九成失敗不是因為工具不行是因為根本沒搞清楚手里這個東西是什么。我見過太多人上來就node wuWxapkg.js xxx.wxapkg報一堆錯才發(fā)現(xiàn)拿到的是個分包、是個沒下載完的殘包、或者根本就是加過殼的產(chǎn)物。2.1 一個 .wxapkg 里到底裝了什么.wxapkg 不是 zip也不是 tar它是一套自定義的線性容器格式。整體結(jié)構(gòu)大致分三段區(qū)段內(nèi)容說明文件頭固定首字節(jié) 4 字節(jié)大端序索引長度用來判斷這到底是不是合法包索引段文件數(shù)量、每個文件的名稱長度、名稱、數(shù)據(jù)偏移、數(shù)據(jù)大小相當(dāng)于一張目錄表數(shù)據(jù)段按索引順序緊密排列的原始文件字節(jié)真正的 wxml、wxss、js、json、圖片索引段里的偏移量是相對數(shù)據(jù)段起點的所以只要索引能正確讀出來理論上每個文件都能原樣切出來。這部分是純字節(jié)操作不涉及任何破解它就是一個自定義打包格式的逆運(yùn)算。真正麻煩的在后面切出來的 js 不是你能直接讀的 jswxml 不是手寫的 wxmlwxss 也被壓過。容器解包只是第一步格式還原才是主線。一個判斷技巧解開后如果app-service.js只有幾 KB而包里索引顯示有幾十個頁面那基本可以確定這是個分包主包的邏輯不在這里。分包單獨(dú)解出來的 js 是殘缺的缺主包的模塊注冊表跑起來會一路報模塊未定義。2.2 編譯產(chǎn)物的代際差異決定你能還原到幾成不同時期、不同基礎(chǔ)庫版本編譯出來的產(chǎn)物可還原程度差別很大。我一般按三個維度去比第一個維度是模塊化寫法。早期編譯產(chǎn)物是自定義的define/require加模塊 ID 映射模塊之間的依賴關(guān)系寫在數(shù)組里還原后能看出清晰的文件邊界。近一些的產(chǎn)物更接近 CommonJS 風(fēng)格甚至帶上了打包器的運(yùn)行時。前者還原出來可讀性更好后者需要把運(yùn)行時那段殼剝掉才看得清業(yè)務(wù)代碼。第二個維度是模板壓縮程度。wxml 編譯后屬性會被壓短、注釋被清空、部分標(biāo)簽做名稱映射。壓縮得越狠還原出來的結(jié)構(gòu)越需要人工對回原語義。有些產(chǎn)物里wx:if被改成了短屬性名映射表不在包里只能靠上下文推斷。第三個維度是樣式處理。wxss 編譯時會把import內(nèi)聯(lián)展開單位處理也可能被重寫。內(nèi)聯(lián)展開是不可逆的你只能還原出合并后的結(jié)果還原不出原本拆成了哪幾個文件。這一點必須先接受別指望完美。所以我在動手前會先做一次抽樣判斷隨便切兩三個文件出來看看頭部特征估一下還原上限。如果目標(biāo)是找回業(yè)務(wù)邏輯六七成就夠用如果目標(biāo)是逐字節(jié)還原原始倉庫那大概率要失望。這個預(yù)期差如果一開始不擺正后面每一步都會覺得工具有 bug。2.3 先把授權(quán)邊界劃出來哪些包能碰哪些碰都別碰這段必須放在前面講不是什么免責(zé)套話。能碰的自己公司或自己開發(fā)的、有明確書面授權(quán)的資產(chǎn)自己設(shè)備上自己賬號產(chǎn)生的小程序緩存包教學(xué)或研究場景下官方提供的樣例包。這幾類的共同點是——你對這份代碼有處置權(quán)。不能碰的任何第三方的小程序包。哪怕它就在你手機(jī)緩存目錄里躺著哪怕它沒有任何加密。反編譯他人代碼并用于分析接口、復(fù)制邏輯、尋找漏洞在很多地方的司法實踐里都是有明確判例的不要拿我只是學(xué)習(xí)當(dāng)擋箭牌。我的習(xí)慣是每次開工前先寫一行記錄——包來自哪臺設(shè)備、哪個賬號、誰授權(quán)的、用途是什么。聽著很形式主義但真出事的時候這一行記錄比什么都管用。血淚經(jīng)驗談不上算是給自己留的后悔藥。3. wxappUnpacker 的四個腳本誰解容器誰還原代碼這套工具的設(shè)計思路很樸素一個主入口負(fù)責(zé)解容器和調(diào)度三個子腳本各管一種文件類型的還原。理解了分工報錯信息基本就能自己對上號。3.1 環(huán)境這一關(guān)Node 版本與依賴別在這翻車倉庫 clone 下來第一件事不是跑是看package.json。這類工具大多寫得比較早依賴的解析庫、字符串處理庫版本偏舊直接npm install在新版 Node 上經(jīng)常出問題。我一般的處理順序是先用node -v看當(dāng)前版本如果報錯里有Cannot find module之外的語法錯誤大概率是 Node 太新。裝一個 LTS 版本用版本管理器切過去跑一次。npm install如果卡在某個包編譯上先看是不是需要本地編譯工具鏈能跳過的可選依賴就跳過。依賴裝完目錄里應(yīng)該能看到這幾個文件文件職責(zé)wuWxapkg.js主入口解容器、建目錄、按類型調(diào)用子腳本wuJs.jsjs 還原處理模塊包裝與依賴映射wuWxml.jswxml 還原處理標(biāo)簽與屬性壓縮wuWxss.jswxss 還原處理 import 內(nèi)聯(lián)與格式化wuConfig.js映射表與配置很多還原細(xì)節(jié)靠它wuConfig.js經(jīng)常被忽略但它決定了標(biāo)簽名映射、屬性名映射這些細(xì)節(jié)。如果還原出來的 wxml 里出現(xiàn)一堆看不懂的短標(biāo)簽先去這個文件里對一下映射表是不是缺項。3.2 wuWxapkg.js 主干流程解容器、建目錄、分發(fā)任務(wù)主腳本干的事可以拆成四步我在調(diào)試時習(xí)慣按這四步去定位問題第一步讀文件頭做合法性校驗。首字節(jié)不對直接退出。這一步失敗通常意味著文件被截斷或者根本不是這個格式。第二步讀索引段把文件表解析成內(nèi)存里的數(shù)組。每項含名稱、偏移、大小。這一步失敗一般是索引長度字段被讀錯常見于下載不完整的包。第三步按偏移切數(shù)據(jù)段把文件寫到輸出目錄。文件名的路徑分隔符在包里可能是平臺相關(guān)的寫盤前要做一次規(guī)范化否則在類 Unix 系統(tǒng)上會出現(xiàn)文件名里帶反斜杠的情況。第四步遍歷輸出目錄按擴(kuò)展名分發(fā).js交給 js 腳本.wxml交給 wxml 腳本.wxss交給 wxss 腳本。分發(fā)的順序有講究——js 先還原因為它產(chǎn)出的模塊映射會影響 wxml 里表達(dá)式的可讀性。想加日志的話最省事的位置是第三步和第四步之間把切出來的文件按大小排個序打出來。哪一類文件數(shù)量明顯偏少就說明那一路還原會出問題。3.3 wuJs.js / wuWxml.js / wuWxss.js三路還原與各自的失敗特征三個腳本的失敗表現(xiàn)完全不同記住特征能省很多時間。js 這一路的典型失敗是文件生成了但是空的或者只還原出殼。原因是模塊格式識別失敗——腳本按預(yù)期格式去找注冊表找不到就跳過了。這種情況下打開原始切出來的app-service.js看頭部如果開頭不是預(yù)期的模塊定義結(jié)構(gòu)說明產(chǎn)物代際和工具預(yù)期不一致需要手動改一下匹配規(guī)則。wxml 這一路失敗通常是結(jié)構(gòu)對但屬性全丟。這不是 bug是映射表缺項需要往wuConfig.js里補(bǔ)。另一種失敗是嵌套層級錯亂多半出在自閉合標(biāo)簽的處理上。wxss 這一路最輕多數(shù)情況只是格式?jīng)]排好內(nèi)容基本完整。真出問題一般是 import 展開時路徑解析失敗看日志里的路徑拼接結(jié)果就能定位。4. 一次完整復(fù)現(xiàn)從包文件到可讀源碼的實操路徑前三章講的都是判斷依據(jù)這一章走一遍完整流程。我用一個虛構(gòu)的內(nèi)部審計場景來演示——某公司自家小程序的歷史版本源碼丟失只剩測試機(jī)上的一份緩存包。4.1 定位你自己的包主包、分包與緩存目錄包一般在自己設(shè)備的宿主 App 緩存目錄下按小程序的 AppID 分文件夾存放。目錄里通常能看到兩類文件一類是主包文件名帶__APP__之類的標(biāo)識另一類是分包按分包名命名。找到之后先把整個目錄拷出來一份別在原位置操作??匠鰜碇笙茸鰞杉碌谝挥梦募笮∨艂€序。主包通常最大幾十 KB 到幾 MB 不等。有幾個包大小接近說明是多分包結(jié)構(gòu)。第二逐個看文件頭。首字節(jié)應(yīng)該是統(tǒng)一的固定值不是的話這個包大概率沒下載完整需要重新觸發(fā)一次加載。分包的處理順序是先解主包得到輸出目錄再用-d參數(shù)解各個分包輸出到主包目錄下。順序反了會缺模塊注冊表。4.2 參數(shù)怎么給-o、-s、-d 的作用與組合主入口的參數(shù)不多但每個都有明確的用途# -o 指定輸出目錄建議每個包單獨(dú)一個目錄方便對比 # -s 傳入 AppID部分版本靠它定位分包歸屬路徑 # -d 標(biāo)記當(dāng)前是分包不再重復(fù)解主包直接復(fù)用已有輸出目錄 node wuWxapkg.js -o./out/main ./pkg/__APP__.wxapkg node wuWxapkg.js -d -swx0000000000000000 \ -o./out/main ./pkg/sub_home.wxapkg-o是最常用的一定要顯式給默認(rèn)輸出到當(dāng)前目錄會把工作區(qū)搞亂。-s在主包解完之后可以省略但解分包時建議帶上能避免路徑推斷出錯。-d是分包專用開關(guān)漏了它會導(dǎo)致重復(fù)解主包、輸出目錄被覆蓋。參數(shù)給錯的典型癥狀輸出目錄里只有一層空文件夾或者解出來的文件數(shù)明顯少于預(yù)期。這時候別急著懷疑工具先把命令回顯打出來核對一遍。4.3 跑通之后先看哪幾個文件解包成功不等于還原成功先掃一遍輸出目錄再決定要不要深挖。我的順序是先看根目錄的app.json之類的配置確認(rèn)頁面列表、分包配置、tabBar 結(jié)構(gòu)。這份文件通常還原得最完整能直接告訴你這個應(yīng)用有多少個頁面、路由怎么組織的。再看app-service.js還原后的主邏輯文件確認(rèn)模塊注冊表在不在。如果模塊列表是完整的說明 js 這一路基本成功了后面逐個頁面讀就行。如果只有一層空殼說明格式不匹配得回頭改匹配規(guī)則。最后抽查兩三個頁面的 wxml看標(biāo)簽和屬性是不是人能讀的。這一步是判斷 wxml 還原質(zhì)量最直接的辦法抽到看不懂的短標(biāo)簽就說明映射表有缺項。4.4 還原不完整時的三級降級策略理想情況是一把過實際很少。我準(zhǔn)備好三級降級第一級補(bǔ)映射表。針對 wxml 的短標(biāo)簽、短屬性往配置文件里補(bǔ)映射重跑 wxml 那一路。這一步成本最低能解決大部分能讀但讀不順的問題。第二級手工修 js 模塊格式。把原始的app-service.js切出來對照工具預(yù)期的格式改匹配規(guī)則或者干脆自己寫個小腳本按模塊 ID 切分。這一步需要一點正則功底但對新版產(chǎn)物的效果最好。第三級放棄全量還原轉(zhuǎn)成按需還原。只解自己關(guān)心的那幾個頁面其余不管。審計場景下這往往是最理性的選擇——你要的是邏輯鏈不是一份能編譯的工程。降級不是失敗是控制成本。我見過有人為了把一張圖的 base64 還原出來耗了兩天最后發(fā)現(xiàn)那張圖跟審計目標(biāo)毫無關(guān)系。5. 避坑與排查五條現(xiàn)場踩出來的記錄這一章全是真出過的問題按現(xiàn)象、原因、解決三段寫。遇到卡住的時候直接對一遍。5.1 第一坑解出來的 js 全是空的現(xiàn)象命令跑完沒有報錯輸出目錄結(jié)構(gòu)也對但每個 js 文件都是 0 字節(jié)或者只有幾十個字節(jié)的殼。原因模塊格式識別失敗。工具按固定的模塊定義結(jié)構(gòu)去匹配產(chǎn)物代際不同注冊表長得不一樣匹配不上就直接跳過寫空文件了。解決先把原始app-service.js單獨(dú)切出來用任何十六進(jìn)制工具按索引偏移切看它的頭部結(jié)構(gòu)。把工具里負(fù)責(zé)匹配的那段正則對照著改或者寫個小腳本按模塊 ID 手動切分。改完只跑 js 這一路別每次都全量重跑。5.2 第二坑分包解出來缺一半頁面現(xiàn)象主包解得挺完整分包解完發(fā)現(xiàn)頁面數(shù)量對不上路由里有但目錄里沒有。原因多半是解分包時沒帶-d工具以為你在解主包把輸出目錄重新初始化了一遍把之前的結(jié)果覆蓋掉了。解決確認(rèn)命令里帶了-d并且-o指向的是主包輸出目錄而不是新建目錄。如果不放心先把主包輸出目錄備份一份再解分包出問題還能回滾。5.3 第三坑wxml 里全是看不懂的短標(biāo)簽現(xiàn)象結(jié)構(gòu)能看出來但標(biāo)簽名是_c、_d這類短標(biāo)識屬性名也對不上讀起來像亂碼。原因標(biāo)簽和屬性在編譯期被做了名稱映射映射表不在包里工具的配置文件中缺項。解決對著上下文猜。列表類結(jié)構(gòu)大概率是view加wx:for可點擊的容器大概率是view加bindtap。把這些推斷補(bǔ)進(jìn)配置文件重跑 wxml 那一路能解決大部分。實在猜不出的先放著優(yōu)先保證業(yè)務(wù)邏輯可讀。5.4 第四坑wxss 還原后樣式全亂現(xiàn)象文件切出來了內(nèi)容也有但import全被展開成一大坨原本分文件的樣式混在一起改都沒法改。原因import內(nèi)聯(lián)是編譯期行為不可逆。工具只能還原合并后的結(jié)果還原不出原本拆成了哪幾個文件。解決接受這個限制別想著完美還原。真要拆就按選擇器和注釋痕跡去人工分組?;蛘吒纱嘀魂P(guān)注關(guān)鍵頁面的樣式其余不動。這個坑沒有技術(shù)解法只有預(yù)期管理。5.5 第五坑跑一半進(jìn)程被殺輸出目錄半殘現(xiàn)象解一個比較大的包跑到一半進(jìn)程沒了輸出目錄里有一半文件再跑一次又從頭覆蓋。原因大包解包時內(nèi)存占用高加上寫盤是逐個文件進(jìn)行的中斷后狀態(tài)不一致。解決分兩步走。先只解容器不還原把分發(fā)那一步注釋掉確認(rèn)所有文件都切出來了再單獨(dú)跑還原。這樣中斷了也不用重來切出來的原始文件一直都在。6. 把流程固化成腳本一條命令跑完解包與校驗手動跑幾次之后必然會煩——每次都要記參數(shù)、分辨主包分包、解完還要數(shù)文件。我的做法是把它固化成一個小腳本輸入包目錄輸出解包結(jié)果和一份完整性報告。#!/usr/bin/env bash # batch-unpack.sh —— 批量解包并對結(jié)果做一次完整性打分 # 用法: ./batch-unpack.sh 包目錄 輸出根目錄 set -uo pipefail PKG_DIR${1:?請給出 .wxapkg 所在目錄} OUT_ROOT${2:?請給出輸出根目錄} TOOL./wuWxapkg.js mkdir -p $OUT_ROOT for pkg in $PKG_DIR/*.wxapkg; do name$(basename $pkg .wxapkg) out$OUT_ROOT/$name mkdir -p $out # 主包先解-o 顯式指定輸出避免污染工作區(qū) if ! node $TOOL -o$out $pkg; then echo [FAIL] $name 解包異常跳過 continue fi # 三類產(chǎn)物的數(shù)量就是最直觀的完整性指標(biāo) js$(find $out -name *.js | wc -l) wxml$(find $out -name *.wxml | wc -l) wxss$(find $out -name *.wxss | wc -l) # 空文件也算失敗單獨(dú)統(tǒng)計一次 empty$(find $out -type f -size 0 | wc -l) echo [OK] $name js$js wxml$wxml wxss$wxss empty$empty done腳本邏輯就三件事遍歷、解包、計數(shù)。empty那一項是我后來加的因為空文件是最容易被忽略的失敗信號——目錄結(jié)構(gòu)齊全、命令沒有報錯但內(nèi)容全是空的光看文件數(shù)量根本發(fā)現(xiàn)不了。參數(shù)上有一點要注意set -uo pipefail里我沒有加-e。原因是解包過程中單個文件失敗很常見加了-e會讓整個批次中斷反而不好排查。改成用if !單獨(dú)捕獲每次調(diào)用的退出碼失敗了就跳過繼續(xù)下一個日志里留痕。跑完對照一下app.json里聲明的頁面數(shù)量和實際還原出的 wxml 數(shù)量做比值。低于六成說明這一路有明顯問題得回去看 js 或 wxml 的失敗特征八成以上基本可以直接進(jìn)入人工閱讀階段了。從那以后我每次做這類審計第一步都不是打開工具而是先跑一遍這個腳本拿到完整性報告再決定這次要投多少時間進(jìn)去。工具給的是上限腳本告訴你實際能拿到多少。希望幫到你。本文還有配套的精品資源點擊獲取