原來發布在掘金,搬過來好了,
微信小程式在PC端是加密存盤的,如果直接打開是看不到什么有用的資訊的,需要經過解密才可以看到包內具體的內容,本文使用nodejs實作解密演算法,主要涉及到crypto, commander, chalk三個包的使用,
小程式的原始碼在哪里
PC端打開過的小程式會被快取到本地微信檔案的默認保存位置,可以通過微信PC端=>更多=>設定查看:

進入默認保存位置下的/WeChat Files/WeChat Files/Applet檔案夾,可以看到該目錄下有一系列前綴為wx的檔案(檔案名其實是小程式的appid),這些就是我們打開過的小程式啦:

進入其中某個小程式的檔案夾,我們可以看到一個名字為一串數字的檔案夾,點進這個檔案夾, 就可以看到一個__APP__.wxapkg檔案,也就是小程式對應的代碼啦:

然而,當我們打開這個檔案之后卻發現是這樣的:

WTF 這能看出來個🔨,很明顯,這個檔案是經過加密的,需要解密才能看到我們想看到的東西,
PC端小程式是怎么被加密的
這里參考了一位大佬用Go語言寫的PC端wxapkg解密代碼,整理一下的話,加密流程是這樣的:

首先將明文代碼在第1024位元組處一分為二,前半部分使用CBC模式的AES加密,后半部分則直接進行異或,最后,將加密后的兩節拼接起來,并在最前邊寫入固定的字串:"V1MMWX",
所以,我們打開__APP__.wxapkg檔案看到的就是加密后的代碼,如果想還原回去的話,需要從后往前逐步推回去,
解密思路
預處理
我們使用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點這里
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/233928.html
標籤:區塊鏈
上一篇:常用演算法之驗證回文串
