微信小程序渗透
小程序开启F12调试
WeChatOpenDevTools-Python-微信小程序强制开启开发者工具
KillWxapkg-自动化反编译微信小程序,小程序安全评估工具,发现小程序安全问题,自动解密,解包,可还原工程目录,支持Hook,小程序修改
如果微信版本相同小程序版本不同,就删除小程序版本目录并重启微信,直到刷出支持的小程序版本目录
Windows 查看当前运行版本
启动一次后设置文件夹(RadiumWMPF
)权限为只读,这样就能一直保持小程序版本不变了
macOS 查看当前运行版本
ps aux | grep 'WeChatAppEx' | grep -v 'grep' | grep "wmpf-mojo-handle"
使用WeChatOpenDevTools-Python开启F12
支持版本列表
Windows 微信版本 | 小程序版本 |
---|---|
11275_x64 | |
11253_x64 | |
11205_x64 | |
11159_x64 | |
3.9.10.19_x64 | 9129_x64 |
3.9.10.19_x64 | 9115_x64 |
3.9.10.19_x64 | 8555_x64 |
3.9.10.19_x64 | 9105_x64 |
3.9.9.43_x64 | 8555_x64 |
3.9.9.43_x64 | 9079_x64 |
3.9.8.25_x64 | 8531_x64 |
3.9.8.25_x64 | 8529_x64 |
3.9.8.25_x64 | 8519_x64 |
3.9.8.25_x64 | 8501_x64 |
3.9.8.25_x64 | 8461_x64 |
3.9.8.25_x64 | 8447_x64 |
Mac x64微信版本 | x |
---|---|
MacWechat/3.8.8(0x13080811) | 源码运行 |
MacWechat/3.8.8(0x13080812) | 源码运行 |
运行前先启动微信(建议小号,有被封号风险)
- 安装python3
- 下载WeChatOpenDevTools-Python或直接下载 编译好的
- 安装依赖
pip3 install -r requirements.txt
- 运行
python3 main.py -x
开启小程序F12 - 运行
python3 main.py -c
开启内置浏览器F12 - 运行
python3 main.py -all
开启内置浏览器F12与小程序F12
使用KillWxapkg开启F12调试(目前仅支持Windows,且仅支持小程序)
- 下载最新版本的release包
- 自行编译
# 克隆项目
git clone https://github.com/Ackites/KillWxapkg.git
# 进入项目目录
cd ./KillWxapkg
# 下载依赖
go mod tidy
# 编译
go build
Hook支持版本列表
小程序版本 |
---|
11275_x64 |
11253_x64 |
11205_x64 |
9193_x64 |
11159_x64 |
9185_x64 |
9129_x64 |
9115_x64 |
8555_x64 |
9105_x64 |
8555_x64 |
9079_x64 |
8531_x64 |
8529_x64 |
8519_x64 |
8501_x64 |
8461_x64 |
8447_x64 |
运行前先启动微信(建议小号,有被封号风险)
目前仅支持Windows
KillWxapkg.exe -hook
小程序解密和反编译
wxapkg-微信小程序反编译工具,.wxapkg 文件扫描 + 解密 + 解包工具
KillWxapkg-自动化反编译微信小程序,小程序安全评估工具,发现小程序安全问题,自动解密,解包,可还原工程目录,支持Hook,小程序修改
使用e0e1-wx反编译
安装依赖(pip install -r requirements.txt
)后即可之际使用
python3 e0e1-wx.py
使用wxapkg反编译
目前只支持 windows 系统:
- 打开小程序让微信下载小程序
- 使用
wxapkg.exe scan
命令来扫描所有小程序。需要联网获取小程序的名称、路径、wxid(用于后续解密)等信息 - 使用键盘上下键选中想要处理的小程序,然后按回车来执行解密+解包
如果小程序的包不在默认路径
.\wxapkg.exe scan -r "D:\Documents\WeChat Files\Applet"
如果想手动来解密指定小程序,可以使用 wxapkg.exe unpack
命令,需要指定小程序 wxapkg 文件路径,会自动从路径中使用正则表达式匹配获取小程序的id
.\wxapkg.exe unpack -o outdir -r "C:\Users\username\Documents\WeChat Files\Applet\wx2f0xxxxxxxxxx"
使用KillWxapkg反编译
-id=<输入AppID> -in=<输入文件1,输入文件2> 或 -in=<输入目录> -out=<输出目录> [-ext=<文件后缀>] [-restore] [-pretty] [-noClean] [-help] [-hook] [-save] [-repack=<输入目录>] [-watch] [-sensitive]
参数说明
-id string
- 微信小程序的AppID
- 包已解密,可不指定
- 例:-id=wx7627e1630485288d
-in string
- 输入文件路径(多个文件用逗号分隔)或输入目录路径
- 自动检测,已解密的包,自动解包,未解密的包,自动解密后解包
- 解密后的包会保存到输入目录下以AppID命名的文件夹
- 例:-in="app.wxpkg,app1.wxapkg"
- 例:-in="C:\Users\mi\Desktop\Applet\64"
-out string
- 输出目录路径(如果未指定,则默认保存到输入目录下以AppID命名的文件夹)
-restore
- 是否还原源代码工程目录结构,默认不还原
-pretty
- 是否美化输出,默认不美化,美化需较长时间
-ext string
- 处理的文件后缀 (default ".wxapkg")
- 例:-ext=.wxapkg
-noClean
- 是否清理反编译的中间文件,默认清理
-hook
- 是否Hook小程序,动态调试,开启F12,默认不Hook
- 注意:目前仅支持Windows,建议小号,有封号风险
-save
- 是否保存解密后的文件,默认不保存
-repack string
- 重新打包目录路径
- 例:-repack="C:\Users\mi\Desktop\Applet\64"
- 注意:目前仅支持一次打包一个文件,同时仅支持未被解析的源文件(未使用-restore)
-watch
- 是否监听将要打包的文件夹,并自动打包,默认不监听
-sensitive
- 是否导出敏感数据,默认不导出,导出后会在工具目录下生成sensitive_data.json文件,支持自定义规则
-help
- 显示帮助信息
.\KillWxapkg_2.4.1_windows_amd64.exe -id wx2f0xxxxxxxxx -in .\__APP__.wxapkg -out testoutdir -restore
定位加密算法
定位方式就是上面两种,通过开启F12调试去追堆栈和搜索进行定位,或者通过反编译出js文件后通过搜索分析去定位。
通常都是结合来识别定位的,单一的使用一种方式来定位有可能会出现偏差。
搜索关键字
搜索关键字有两条思路
- 搜索加密相关的关键字来到定位
- 搜索数据包中的关键字来定位
F12搜索
加密相关的关键字
在 F12 中使用 Ctrl+Shift+F
全局搜索关键字,然后再一一排查判断定位,一般这里搜索到的内容会很多不能快速定位,这时就需要使用数据包中的关键字来辅助定位了。
搜索数据包的关键字就是通过抓包工具对数据包中明文出现的一些关键字进行搜索,或搜索发送到那个接口,就搜索该接口的字段从而实现定位。
如下,在数据包中找一下可识别的字段,然后到 F12 中去搜索定位
通过上面的步骤大致就能确定加密函数的位置了。当然我的图算是上帝视角了,若遇到搜索关键字结果太多的情况定位就会麻烦很多,需要一个个的点进去分析。当然也不是每个都点进去,从搜索的预览结果中也能判断出这些没必要看。
格式化后搜索到的结果就会是如下,一个单行显示,一个多行显示
类似于如下的,根据关键字通常的属性判断该关键字是使用于:赋值
、方法名
、类名
、字段定义
等等,从而判断是否是需要寻找的内容。
如下红框内容搜索到的关键字就不符合要求,绿框内的就有可能是符合要的,这就需要点进去详细分析了
搜索反编译后的js内容
同样的通过搜索加密相关的关键字和数据包中出现的关键字来定位
linux 和 macOS 可以使用grep来搜索递归搜索-r
,且忽略大小写-i
:
grep encrypt ./* -ri
Windows 可以使用PowerShell命令来搜索,默认是大小写不敏感的,可以使用 -CaseSensitive
启用区分大小写。也可以去安装Windows版本的grep去实现或使用其他工具来搜索。
Get-ChildItem -Recurse | Select-String -Pattern "Factor"
Get-ChildItem -Recurse | Select-String -Pattern "Factor" -CaseSensitive
et-ChildItem -Recurse
:获取当前目录及所有子目录中的所有文件。Select-String -Pattern "搜索的内容"
:搜索指定的字符串模式。
搜索接口
通过搜索数据包中的接口去定位,这个最好是使用F12打断点,在js中若做了混淆那搜索到的结果就不能快速定位加密函数的位置。搜索接口的方式不一定能定位到,但有些应用还是能通过 XHT/fetch
断点追栈信息来快速定位函数位置。
F12搜索
可以使用搜索定位到接口打断点,或通过添加 XHR/fetch
来断点,XHT/fetch
断点就是将接口URL添加到检测中,当向这个URL发数据时就断点
通过断点拦截,就能在这时去追栈信息,往前或往后推出数据是在哪被加密,加密前是什么数据,加密后是什么
通过回退栈往回定位,然后再通过作用于中的数据来判断是否定位到了加密函数附近
站内的异步(async
)是没法定位过去的,所有遇到追栈时到最下面的栈还没追到明文数据处理的位置,那就可以断定当前的断点打晚了,数据已经被处理了。
遇到栈数据已经被处理过的情况只能是从新找断点,把断点定在栈数据被处理前
使用断点调试中的使用调试来控制函数的执行
5个按钮依次是:
恢复脚本执行
跳过当前断点,恢复所有脚本的执行,遇到下一个断点会再次停止跳过下一个函数调用
步过,不进入当前代码的内部,直接执行下一行代码进入下一个函数调用
步入,进入当前代码的内部执行跳出当前函数
步出,跳出当前函数,回到进入的位置,继续执行后面的代码下一步
当前函数的下一步骤
通过反编译的js内容进行搜索
F12断点的艺术
打断点,在js内容的函数定义行左边显示行数的位置点一下即可打下断点,有时会遇到该行能打多个断点的情况。
断点是鼠标悬停在作用域中的值上就能查看对于的值
在控制台输入变量也可查看对于的值
https://www.cnblogs.com/xiaowenshu/p/10450848.html
https://blog.csdn.net/weixin_69761220/article/details/125430348
https://juejin.cn/post/7043719715994386446
一个不太完整的加密定位过程
- 开启F12调试
- 抓包获取到数据包中的关键字,然后到F12中搜索
通过搜索排查大致搜索到加密函数位置(既有数据包关键字,由于加密函数关键字,一般没错了,运气不好的话遇到混淆过的需要读一下js判断一下)
- 直接在涉及的加密函数位置进行断点
- 点一下功能点,在断点出通过追栈、步入、步出在断点附近找到加密前的数据格式、数据内容,根据信息判断哪些是固定不变的,哪些是随机的,哪些是需要修改传递的数据。
如在下面的第一个断点处脚本被暂停了,点击步出跳过当前函数,开始下一个函数,这样就能执行下一步,这个过程中就会出现对于的变量值
通过这种一步一步的找出加密前的所有数据,同时也分析出加密过程。
描述一下这个加密的过程吧,和常规的加密还有些不同,通过阅读js代码
Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;var e=require("../../@babel/runtime/helpers/classCallCheck"),t=require("../../@babel/runtime/helpers/createClass"),r=require("./sm/sm"),s=r.sm2,i=r.sm3,n=r.sm4;exports.default=function(){function r(){e(this,r);var t=function(){for(var e="",t=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"],r=0;r<18;r++){var s=Math.round(Math.random()*(t.length-1));e+=t[s]}return e}(),n=i(t);this.iv=n.substring(0,32),this.secretKey=n.substring(n.length-32),this.factor="04"+s.doEncrypt(t,"0483F7D971BBxxxxxxxxxxx0F1B8E1D3230919",1)}return t(r,[{key:"encrypt",value:function(e,t,r){var i=n.encrypt(t,this.secretKey,{iv:this.iv,mode:"cbc"});return e.Factor=this.factor,e.SignData=s.doSignature(i,"5AF91E74xxxxxxxxxxxxxE9A3262",{hash:!0,userId:e.AppCode+"-"+r}),{head:JSON.stringify(e),body:i}}},{key:"decrypt",value:function(e){return e&&"{}"!=e?n.decrypt(e,this.secretKey,{iv:this.iv,mode:"cbc"}):""}}]),r}();
这个加密过程是先生成一个随机字符串,然后将随机字符串使用SM3算法及栓一个hash值,将hash值的前32位置作为SM4加密算法的iv偏移量,末尾32位作为加密密钥。将hash值使用SM2服务端的公钥进行加密并拼接一个 40
后作为 Factor 发送给服务器,相当于将SM4的偏移量和密钥加密后发送给了服务端。
数据通过SM4加密后,使用SM2的本地的私钥计算出一个值作为SignData,用于服务端对数据完整性校验。同时解密是在收到服务端的数据后使用前面的 iv和密钥进行解密
通过格式化并插入调试信息,将js生成的信息、需要加密和加密后的数据输出出来,这样就在发包时对于修改数据即可实现调用加密算法改包发包。
使用VSCode这类IDE打开js文件,然后遇到括号()
、大括号{}
、中括号[]
、分号;
都来一个回车,IDE会自动完成缩进,这样就能更直观的阅读js内容了。
当然现在也可以交给AI完成格式化,但需要注意AI给你输出的内容是否是改动过的,AI也不是万能的,AI的输出结果可能会出现偏差。
Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;
var e=require("../../@babel/runtime/helpers/classCallCheck"),
t=require("../../@babel/runtime/helpers/createClass"),r=require("./sm/sm"),
s=r.sm2,
i=r.sm3,
n=r.sm4;
exports.default=function(){
function r(){
e(this,r);
// 生成随机字符串 t
var t=function(){
for(var e="",
t=[
"0","1","2","3","4","5","6","7","8","9",
"a","b","c","d","e","f","g","h","i","j",
"k","l","m","n","o","p","q","r","s","t",
"u","v","w","x","y","z","A","B","C","D",
"E","F","G","H","I","J","K","L","M","N",
"O","P","Q","R","S","T","U","V","W","X",
"Y","Z"
],
r=0;
r<18;r++){
var s=Math.round(Math.random()*(t.length-1));
e+=t[s]
}
return e
}(),
n=i(t); // 使用 sm3 生成随机字符串的hash值
console.log("随机字符串:", t); // 打印随机字符串
this.iv=n.substring(0,32), // 获取hash值的前32位作为偏移量iv
this.secretKey=n.substring(n.length-32), // 获取hash值的后32位作为密钥
// 使用sm2将hash值进行加密,并进行处理,用于传送给服务器
this.factor="04"+s.doEncrypt(
t,
"0483F7D971BBxxxxxxxxxxx0F1B8E1D3230919",
1
);
console.log("IV:", this.iv); // 打印偏移量iv
console.log("Secret Key:", this.secretKey); // 打印密钥
console.log("Factor:", this.factor); // 打印加密因子
}
return t(
r,
[
{
key:"encrypt",value:function(e,t,r){
var i=n.encrypt(t, this.secretKey, {iv:this.iv,mode:"cbc"});
console.log("加密后的数据:", i); // 打印加密后的数据
return e.Factor=this.factor,e.SignData=s.doSignature(
i,
"5AF91E74xxxxxxxxxxxxxE9A3262",
{hash:!0,userId:e.AppCode+"-"+r}
),
console.log("SignData:", e.SignData),
{head:JSON.stringify(e),body:i}
}
},
{
key:"decrypt",value:function(e){
return e&&"{}"!=e?n.decrypt(e,this.secretKey,{iv:this.iv,mode:"cbc"}):""
}
}
]
),
r
}();
const SmCryptor = require("./smCryptor.js").default;
// 要加密的数据
const dataInfo = '{"paramIds":"loanAdvertisementLimit","sourceChannel":{"userCode":"","userTokenId":"","userMobileNo":"","userType":"","inviterCode":"","inviterMobileNo":"","inviterUrl":"","inviterType":"","appType":"","sceneCode":"","prodCode":""}}'
console.log("加密前的数据:", dataInfo) // 打印加密前的数据
console.log("WaterMark:", "1735890142345") // 打印加密前的数据
const smCr = new SmCryptor();
smCr.encrypt({AppCode: 'loanXcx', WaterMark: 1735890142345}, dataInfo , "");
由于微信小程序的js是符合nodejs规范的,所有可以直接使用nodejs调用js实现数据处理。
收到返回包后可以使用这个站点来实现对数据的加解密