[node.js]PC端微信小程序包解密

原來發布在掘金,搬過來好了。javascript

微信小程序在PC端是加密存儲的,若是直接打開是看不到什麼有用的信息的,須要通過解密才能夠看到包內具體的內容。本文使用nodejs實現解密算法,主要涉及到crypto, commander, chalk三個包的使用。java

小程序的源碼在哪裏

PC端打開過的小程序會被緩存到本地微信文件的默認保存位置,能夠經過微信PC端=>更多=>設置查看:node

進入默認保存位置下的/WeChat Files/WeChat Files/Applet文件夾,能夠看到該目錄下有一系列前綴爲wx的文件(文件名實際上是小程序的appid),這些就是咱們打開過的小程序啦:git

進入其中某個小程序的文件夾,咱們能夠看到一個名字爲一串數字的文件夾。點進這個文件夾, 就能夠看到一個__APP__.wxapkg文件,也就是小程序對應的代碼啦:es6

然而,當咱們打開這個文件以後卻發現是這樣的:github

WTF 這能看出來個🔨。很明顯,這個文件是通過加密的,須要解密才能看到咱們想看到的東西。算法

 

PC端小程序是怎麼被加密的

這裏參考了一位大佬用Go語言寫的PC端wxapkg解密代碼。整理一下的話,加密流程是這樣的:小程序

首先將明文代碼在第1024字節處一分爲二,前半部分使用CBC模式的AES加密,後半部分則直接進行異或。最後,將加密後的兩節拼接起來,並在最前邊寫入固定的字符串:"V1MMWX"。微信小程序

因此,咱們打開__APP__.wxapkg文件看到的就是加密後的代碼,若是想還原回去的話,須要從後往前逐步推回去。promise

 

解密思路

預處理

咱們使用node.js去寫一個解碼的程序。根據上邊加密的流程,咱們首先讀取加密文件,把前6個字節的固定字符串去除。因爲AES加密和異或先後數據的位數是相同的,咱們能夠據此獲取到加密後的頭部1024字節和加密後的尾部部分:

const fs = require('fs').promises;
...

const buf = await fs.readFile(pkgsrc); // 讀取原始Buffer
const bufHead = buf.slice(6, 1024 + 6);
const bufTail = buf.slice(1024 + 6);

加密後的頭部部分

爲了獲得這1024個字節的明文,咱們須要知道AES加密的初始向量iv,以及一個32位的密鑰。已知16字節的初始向量iv是字符串:「the iv: 16 bytes」,咱們接下來須要計算出這個由pbkdf2算法導出的32位的密鑰。

pbkdf2(Password-Based Key Derivation Function)是一個用來生成密鑰的函數,它使用一個僞隨機函數,將原文密碼和salt做爲輸入,經過不斷的迭代獲得密鑰。在crypto庫中,pbkdf2函數是這樣的:

const crypto = require('crypto');
...

crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)

 

其中參數分別是:原文密碼、鹽值、迭代次數、密鑰長度、散列算法、回調函數。已知salt是"saltiest",原文密碼爲微信小程序的id(也就是wx開頭的那個文件夾名),迭代次數爲1000,散列算法爲sha1。所以,咱們能夠寫出計算密鑰的代碼:

crypto.pbkdf2(wxid, salt, 1000, 32, 'sha1', (err, dk) => {
    if (err) {
        // 錯誤
    }
    // dk即爲計算獲得的密鑰
})

 

密鑰和初始向量iv都有了以後,咱們能夠開始對密文進行解密了。AES加密算法是一種非對稱加密算法,它的密鑰分紅公開的公鑰和只有本身知道的私鑰,任何人均可以使用公鑰進行加密,可是隻有持有私鑰的人解密獲得明文。

小程序使用的加密算法是CBC(Cipher Block Chaining, 密碼分組連接)模式的AES,也就是它在加密的時候,首先把明文進行分塊,而後將每一塊與前一塊加密後的密文進行異或,再使用公鑰進行加密,獲得每一塊的密文。對於第一塊明文,因爲它不存在前一塊明文,所以它會與初始向量iv進行異或,再進行公鑰加密。在實現的時候,咱們只須要調用crypto提供的解密函數就能夠啦。

咱們知道,AES算法根據密鑰長度的不一樣有AES128, AES192和AES256。回顧上邊,咱們的密鑰是32字節,也就是256位的,所以顯然咱們應該使用的是AES256。綜上,咱們能夠寫出來解密的代碼:

const decipher = crypto.createDecipheriv('aes-256-cbc', dk, iv);
const originalHead = Buffer.alloc(1024, decipher.update(bufHead));

 

其中originalHead就是咱們要的前1024字節的明文啦。咱們能夠打印出來看看:

嗯…… 有那麼點意思了。

加密後的尾部部分

這一部分就很簡單啦。因爲異或運算是具備自反性的,所以只須要簡單的判斷一下小程序id的位數得到異或的xorKey,再把它與密文進行異或,就能夠獲得原文了:

const xorKey = wxid.length < 2 ? 0x66 : wxid.charCodeAt(wxid.length - 2);
const tail = [];
for(let i = 0; i < bufTail.length; ++i){
    tail.push(xorKey ^ bufTail[i]);
}
const originalTail = Buffer.from(tail);

 

將頭部部分的明文與尾部部分的明文進行拼接,再以二進制形式寫入文件,就能夠獲得最終的明文啦。

 

再漂亮點

根據上邊的描述,咱們能夠把咱們整個的解密過程封裝成一個黑盒子:

commander

咱們可使用commander庫讓程序直接從命令行讀取小程序的id和密文包。commander是一個nodejs命令行界面的解決方案,能夠很方便的定義本身的cli命令。好比說對於下面這一串代碼:

 

const program = require('commander');
...
program
    .command('decry <wxid> <src> [dst]')
    .description('解碼PC端微信小程序包')
    .action((wxid, src, dst) => {
        wxmd(wxid, src, dst);
    })

program.version('1.0.0')
    .usage("decry <wxid> <src> [dst]")
    .parse(process.argv);

 

我定義了一個"decry <wxid> <src> [dst]"的命令,其中尖括號表明必選參數,方括號表明可選參數。description內是關於這個命令的描述文本,action則是執行這段命令。在控制檯使用node執行代碼以後,能夠看到以下界面:

因而咱們就能夠根據提示,輸入參數進行解密啦。commander.js的中文文檔在這裏

chalk

爲了讓咱們的控制檯多一抹顏色,咱們可使用chalk.js來美化輸出。chalk的基本用法也比較簡單:

const chalk = require('chalk');
...

console.log(chalk.green('綠了'))

這樣咱們就能夠在黑白的控制檯上填上一抹綠色,替大熊貓實現夢想:

除此以外,咱們還可使用es6的字符串標籤模板更方便的使用chalk。具體的參考chalk官方文檔吧。

 

源代碼

代碼發佈到github和gitee啦,能夠給你們參考一下下~

github點這裏, gitee點這裏

相關文章
相關標籤/搜索