微信小程式包解析
零、背景
由於需要找出一些小程式內部的邏輯,我對小程式的理解是一個類似於react native的東西,用java script寫的程式碼,通過一個解釋引擎將其解析成安卓原生的控制元件,所以核心的邏輯應該是寫在某個js檔案內,因此就需要想辦法拿到小程式的執行包以及得到執行包內的js程式碼。
一、尋找小程式包
1.1 初步分析
要拿到程式包,最簡單的辦法就是寄希望於小程式是一個下載在本地的包,這樣只需要找到這個包,再對包進行分析。所以第一步就是需要確認小程式確實是一個本地可執行的包。首先隨便找一個小程式,安裝執行,在斷網並且kill掉微信重啟後發現小程式依然能執行,這樣就排除掉了網路載入和記憶體執行兩種非持久化方案,因此可以確認小程式應該是從一個本地包執行起來的了。接下就是找到整個小程式的包安裝在哪裡。
殺死所有微信相關程序的命令如下
[email protected]:/ # ps |grep tencent
u0_a98 2752 375 2100724 190148 ffffffff b6e58784 S com.tencent.mm
u0_a98 2834 375 1643756 79344 ffffffff b6e58784 S com.tencent.mm:exdevice
u0_a98 2905 375 1654692 78280 ffffffff b6e58784 S com.tencent.mm:push
u0_a98 3001 375 1952576 109060 ffffffff b6e58784 S com.tencent.mm:appbrand0
u0_a98 3086 375 1947448 114104 ffffffff b6e58784 S com.tencent.mm:support
u0_a98 3234 375 1951704 108988 ffffffff b6e58784 S com.tencent.mm:tools
[email protected]:/ # kill -9 2752 2834 2905 3001 3086 3234
1.2 變化確認
這裡用到了BeyondCompare,通過對比安裝前和安裝後的/data/data/com.tencent.mm
cp -r /data/data/com.tencent.mm/ /sdcard/
adb pull /sdcard/com.tencent.mm ~/Desktop
在得到安裝前和安裝後兩個目錄後使用Beyond Compare比較
發現MicroMsg和cache兩個資料夾大小變化比較明顯,cache應該不太可能。直接進入MicroMsg,在一路查詢
發現.wxapkg
這樣字尾的包新增了,由此確定這個包就是剛剛安裝的“小程式示例”這個小程式了。
1.3 提取包
通過上面的分析,可以發現微信的小程式包是一個.wxapkg
的包,放在/data/data/com.tencent.mm/MicroMsg/賬號標識/appbrand/pkg/
下,直接用下面兩行命令即可拉出對應的小程式包。
cp /data/data/com.tencent.mm/MicroMsg/賬號標識/appbrand/pkg/小程式包名.wxapkg /sdcard/
adb pull /sdcard/小程式包名.wxapkg ~/Desktop/
二、解析小程式包
拿到小程式包,算是比較順利了,接下來就是對這個包進行分析了。
2.1 初步分析
一般拿到檔案,直接拖到hex編輯器檢視,我這裡用到的是hex fiend這款軟體,具體用哪個可以自己百度都大同小異。
首先一眼就可以看到,微信的小程式包應該是沒有加密的,至少是某些值是沒有加密的,可以直接看到一些檔案的路徑和名字。在對比了幾個wxapkg檔案之後,發現他們的前28位的開頭都是OxBE ~ OxED,猜測這一段應該是有標識著檔案的頭資訊。
大膽的猜測,微信的小程式包應該就是將圖片,js,json檔案全部壓在一起的結構,這樣的結構應該是有三部分,一段是頭資訊,根據頭資訊確定索引,最後根據索引確定檔案Body體。
在大膽的猜測後,下面我們根據一些特殊檔案和猜測一步一步求證找出微信小程式的大致結構。
2.2 分析格式
在反覆對比幾個檔案後,基本確定OxBE OxED應該是類似掩碼,用來標識檔案的,他們之間應該是頭資訊
拿出一個檔案數量相對比較少的wxapkg如下圖:
可以發現紅框內標識數字五,而剛好此檔案在/WAWidget.js後就是資料段了,正好只有五個檔案,因此猜測OxED後8位標識為此包含有的檔案數量,在查看了幾個wxapkg檔案後確認。再接下來就是確認OxBE和OxED內的表示了,根據思維慣性,將他們之間切割為8位一組,發現正好分為三組,就拿上面那個只有五個檔案的wxapkg來看,分別是Ox00000000, Ox0000007E, Ox000F5D8A, 0x00000000很可能是標識該檔案的某些屬性不好確定,因為查看了好幾個檔案發現值都為0。0x0000007E這個數字比較小,為126,很有可能表示索引的長度單位應該是byte,轉為化十六進位制表示就是252位,正好對應著檔案路徑後面幾位,所以這個長度應該能確定是索引的長度了,那剩下的應該也就是Body的長度了。
在確定了索引的長度和Body的長度後,在特殊分析這個檔案,發現就是一個一個檔案的名字和後面加上兩組,類似於Ox0000000A /WAPerf.js Ox0000008C Ox00001FE9,前面Ox0000000A表示長度10byte, 正好20位Ox2F574150 6572662E 6A73表示了/WAPerf.js。後面的也就很有可能是WAPerf.js這個檔案的開始位置和檔案長度了。這裡只做一個猜測,寫完程式再驗證。
2.3 輸出規則
根據上面的分析後列出下面這樣的一個表
欄位 | 屬性 | 長度 | 說明 |
---|---|---|---|
頭資訊 | 首掩碼 | 1 bytes | OxBE的頭掩碼 |
頭資訊 | 未知 | 4 bytes | 未知屬性,所有拉出來的包值都為Ox00000000 |
頭資訊 | 索引長度 | 4 bytes | 索引的長度 |
頭資訊 | 檔案長度 | 4 bytes | Body的長度,其實相當於檔案的長度 |
頭資訊 | 尾掩碼 | 1 bytes | OxED的尾掩碼 |
頭資訊 | 檔案數量 | 4 bytes | 本來以為這個是屬於索引的,但是通過索引長度計算後,發現索引長度是從檔案數量後開始計算的 |
索引 | 檔名長度 | 4 bytes | 檔名長度 |
索引 | 檔名 | 根據上一個欄位確定 | |
索引 | 檔案開始位置 | 4 bytes | |
索引 | 檔案結束位置 | 4 bytes | |
索引 | 迴圈檔案數量次後索引端結束 | ||
Body | 根據檔案開始位置和檔案結束位置算出 |
其實整個過程並沒有2.2分析的那樣順利,猜測也是有很多次錯誤的地方,大膽猜測,小心求證,最後得出了上面的表格。
三、輸出程式
核心程式碼如下
public WXAPPPackage parse(String path) throws IOException, InvalidWXPackageException {
FileInputStream fileInputStream = new FileInputStream(path);
fileInputStream.skip(1);
int edition = getEdition(fileInputStream);
System.out.println("Edition: " + edition);
int indexLength = getIndexLength(fileInputStream);
System.out.println("Index Length: " + indexLength);
int bodyLength = getBodyLength(fileInputStream);
System.out.println("Body Length: " + bodyLength);
fileInputStream.skip(1);
int fileCount = getFileCount(fileInputStream);
System.out.println("File Count: " + fileCount);
ArrayList<WXAPPFile> wxappFiles = new ArrayList<>();
for (int i = 0; i < fileCount; i++) {
int fileNameLength = getFileNameLength(fileInputStream);
String fileName = getFileName(fileInputStream, fileNameLength);
int fileOffset = getFileOffset(fileInputStream);
int fileSize = getFileSize(fileInputStream);
System.out.println("File Name: " + fileName + ", File offset: " + fileOffset + ", File size: " + fileSize);
WXAPPFile file = new WXAPPFile();
file.setFileNameLength(fileNameLength);
file.setFileName(fileName);
file.setFileSize(fileSize);
file.setFileStart(fileOffset);
wxappFiles.add(file);
}
WXAPPPackage wxappPackage = new WXAPPPackage();
wxappPackage.setEdition(edition);
wxappPackage.setIndexLength(indexLength);
wxappPackage.setBodyLength(bodyLength);
wxappPackage.setFileCount(fileCount);
wxappPackage.setFiles(wxappFiles);
return wxappPackage;
}
可以發現,就是根據上面總結的表格,一個一個欄位的遍歷,最後得到包結構體對應的物件。
具體程式碼可以檢視
3.1 測試程式
可以匯入到IDEA中執行測試,具體方法就不提了,我測試了10來個微信小程式包,暫時沒有發現大的問題,小問題可能還有,歡迎指出提issue。
四、總結
整個過程這麼順利也出乎了我的意料之外,首先是微信居然沒有對小程式包進行加密或者壓縮,如果進行了加密至少不會這麼順利連dump記憶體和反編譯的手段都沒有用上。其次就是解壓後發現js程式碼居然沒有任何的保護,可以很輕易的看清楚小程式的邏輯,請求url,資料加密方式,填充方式,很容易的被攻擊者模仿出請求進行攻擊。
這是我第一次進行這種檔案格式的破解,也有一點心得。
- 仔細觀察多個檔案之間的差異
- 進行大膽的猜測,並用特例法針對猜測進行求證。
- 寫出程式,然後再根據程式執行的結果調整規律,最終接近最終答案
最後再貼出一次程式碼位置: