微信小程序渗透

小程序开启F12调试

WeChatOpenDevTools-Python-微信小程序强制开启开发者工具

KillWxapkg-自动化反编译微信小程序,小程序安全评估工具,发现小程序安全问题,自动解密,解包,可还原工程目录,支持Hook,小程序修改

如果微信版本相同小程序版本不同,就删除小程序版本目录并重启微信,直到刷出支持的小程序版本目录

微信历史版本 Windws

微信历史版本 MacOS

Windows 查看当前运行版本

学习笔记/Hacker/assets/Pasted image 20250102221256.png

启动一次后设置文件夹(RadiumWMPF)权限为只读,这样就能一直保持小程序版本不变了

学习笔记/Hacker/assets/Pasted image 20250102223251.png

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) 源码运行

运行前先启动微信(建议小号,有被封号风险)

学习笔记/Hacker/assets/Pasted image 20250102224015.png

使用KillWxapkg开启F12调试(目前仅支持Windows,且仅支持小程序)

# 克隆项目
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

学习笔记/Hacker/assets/Pasted image 20250102222200.png

小程序解密和反编译

e0e1-wx-微信小程序自动化反编译

wxapkg-微信小程序反编译工具,.wxapkg 文件扫描 + 解密 + 解包工具

KillWxapkg-自动化反编译微信小程序,小程序安全评估工具,发现小程序安全问题,自动解密,解包,可还原工程目录,支持Hook,小程序修改

使用e0e1-wx反编译

安装依赖(pip install -r requirements.txt)后即可之际使用

python3 e0e1-wx.py

学习笔记/Hacker/assets/Pasted image 20250102225731.png

使用wxapkg反编译

目前只支持 windows 系统

学习笔记/Hacker/assets/Pasted image 20250102230220.png

学习笔记/Hacker/assets/Pasted image 20250102230333.png

如果小程序的包不在默认路径

 .\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"

学习笔记/Hacker/assets/Pasted image 20250102231519.png

使用KillWxapkg反编译

-id=<输入AppID> -in=<输入文件1,输入文件2> 或 -in=<输入目录> -out=<输出目录> [-ext=<文件后缀>] [-restore] [-pretty] [-noClean] [-help] [-hook] [-save] [-repack=<输入目录>] [-watch] [-sensitive]

参数说明

.\KillWxapkg_2.4.1_windows_amd64.exe -id wx2f0xxxxxxxxx -in .\__APP__.wxapkg -out testoutdir -restore

学习笔记/Hacker/assets/Pasted image 20250102234415.png

定位加密算法

定位方式就是上面两种,通过开启F12调试去追堆栈和搜索进行定位,或者通过反编译出js文件后通过搜索分析去定位。

通常都是结合来识别定位的,单一的使用一种方式来定位有可能会出现偏差。

搜索关键字

搜索关键字有两条思路

F12搜索

加密相关的关键字

js审计和逆向相关tips

审计

appkey
secret
encrypt
crypt
baseUrl
publicKey
register

在 F12 中使用 Ctrl+Shift+F 全局搜索关键字,然后再一一排查判断定位,一般这里搜索到的内容会很多不能快速定位,这时就需要使用数据包中的关键字来辅助定位了。

学习笔记/Hacker/assets/Pasted image 20250103102559.png

搜索数据包的关键字就是通过抓包工具对数据包中明文出现的一些关键字进行搜索,或搜索发送到那个接口,就搜索该接口的字段从而实现定位。

如下,在数据包中找一下可识别的字段,然后到 F12 中去搜索定位

学习笔记/Hacker/assets/Pasted image 20250103103043.png

学习笔记/Hacker/assets/Pasted image 20250103103229.png

通过上面的步骤大致就能确定加密函数的位置了。当然我的图算是上帝视角了,若遇到搜索关键字结果太多的情况定位就会麻烦很多,需要一个个的点进去分析。当然也不是每个都点进去,从搜索的预览结果中也能判断出这些没必要看。

- 一般搜索时由于没格式化过js内容,会导致搜索的结果中也不能直观的看到搜索内容,这时就有一个小技巧,先搜索一下结果,然后把每个js都点开格式化一下,然后再搜索,这样就能直观的看到搜索到的内容了

学习笔记/Hacker/assets/Pasted image 20250103105727.png
格式化后搜索到的结果就会是如下,一个单行显示,一个多行显示
学习笔记/Hacker/assets/Pasted image 20250103110011.png

类似于如下的,根据关键字通常的属性判断该关键字是使用于:赋值方法名类名字段定义 等等,从而判断是否是需要寻找的内容。
如下红框内容搜索到的关键字就不符合要求,绿框内的就有可能是符合要的,这就需要点进去详细分析了
学习笔记/Hacker/assets/Pasted image 20250103104813.png
学习笔记/Hacker/assets/Pasted image 20250103105210.png
学习笔记/Hacker/assets/Pasted image 20250103110415.png

搜索反编译后的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

学习笔记/Hacker/assets/Pasted image 20250103111229.png
学习笔记/Hacker/assets/Pasted image 20250103111704.png
学习笔记/Hacker/assets/Pasted image 20250103111753.png

学习笔记/Hacker/assets/Pasted image 20250103113403.png

搜索接口

通过搜索数据包中的接口去定位,这个最好是使用F12打断点,在js中若做了混淆那搜索到的结果就不能快速定位加密函数的位置。搜索接口的方式不一定能定位到,但有些应用还是能通过 XHT/fetch 断点追栈信息来快速定位函数位置。

F12搜索

学习笔记/Hacker/assets/Pasted image 20250103114107.png

可以使用搜索定位到接口打断点,或通过添加 XHR/fetch来断点,XHT/fetch 断点就是将接口URL添加到检测中,当向这个URL发数据时就断点

学习笔记/Hacker/assets/Pasted image 20250103114818.png

通过断点拦截,就能在这时去追信息,往前或往后推出数据是在哪被加密,加密前是什么数据,加密后是什么
学习笔记/Hacker/assets/Pasted image 20250103115936.png
学习笔记/Hacker/assets/Pasted image 20250103120137.png

通过回退栈往回定位,然后再通过作用于中的数据来判断是否定位到了加密函数附近

学习笔记/Hacker/assets/Pasted image 20250103143930.png

学习笔记/Hacker/assets/Pasted image 20250103144128.png

站内的异步(async)是没法定位过去的,所有遇到追栈时到最下面的栈还没追到明文数据处理的位置,那就可以断定当前的断点打晚了,数据已经被处理了。
学习笔记/Hacker/assets/Pasted image 20250103150612.png

遇到栈数据已经被处理过的情况只能是从新找断点,把断点定在栈数据被处理前

使用断点调试中的使用调试来控制函数的执行
5个按钮依次是:

通过反编译的js内容进行搜索

学习笔记/Hacker/assets/Pasted image 20250103152804.png

F12断点的艺术

打断点,在js内容的函数定义行左边显示行数的位置点一下即可打下断点,有时会遇到该行能打多个断点的情况。

学习笔记/Hacker/assets/Pasted image 20250103153615.png

断点是鼠标悬停在作用域中的值上就能查看对于的值

学习笔记/Hacker/assets/Pasted image 20250103154055.png

在控制台输入变量也可查看对于的值

学习笔记/Hacker/assets/Pasted image 20250103154509.png

学习笔记/Hacker/assets/Pasted image 20250103155003.png

https://www.cnblogs.com/xiaowenshu/p/10450848.html
https://blog.csdn.net/weixin_69761220/article/details/125430348
https://juejin.cn/post/7043719715994386446

一个不太完整的加密定位过程

学习笔记/Hacker/assets/Pasted image 20250103163448.png

学习笔记/Hacker/assets/Pasted image 20250103163737.png

如在下面的第一个断点处脚本被暂停了,点击步出跳过当前函数,开始下一个函数,这样就能执行下一步,这个过程中就会出现对于的变量值

学习笔记/Hacker/assets/Pasted image 20250103164705.png

通过这种一步一步的找出加密前的所有数据,同时也分析出加密过程。

描述一下这个加密的过程吧,和常规的加密还有些不同,通过阅读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生成的信息、需要加密和加密后的数据输出出来,这样就在发包时对于修改数据即可实现调用加密算法改包发包。

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实现数据处理。

注意避坑的地方是,一定要确定好加密前的数据格式是否正确,加密后也不能实现数据交互。

学习笔记/Hacker/assets/Pasted image 20250103181336.png

收到返回包后可以使用这个站点来实现对数据的加解密