记一次逆向破解微信小程序参数签名

article/2025/10/7 11:12:47

前言

在一个平淡无奇的午后,接到一个新的活——对某微信小程序进行渗透测试。老规矩,倒了杯茶,准备开始新一天轰轰烈烈的干活(摸鱼)。

然鹅,BurpSuite 上号不到 2 min 我就发现今天的砖头有点烫手了(晚饭可能都不香了)……事情是这样的:

没错,这货不讲武德,直接加了个时间戳参数timestamp、签名参数signature用来防止数据包重放……那这还玩个锤子!

这我还能说啥,合上电脑准备睡觉!但下一秒还是理智占据了上峰……算了,不能跟 RMB 过不去,还是老实搬砖干活为妙!

小程序逆向

喝口茶冷静想想,既然做了参数签名验证,那么想要继续愉快地渗透测试,必须得解决如何随意替换签名值signature了,那也就是说必须拿到小程序生成签名的源码。

那么问题来了。Web 网站有前端 js 代码负责这活,直接 F12 开发者工具就能查看前端代码逻辑,但是微信小程序的客户端源码上哪找??不装了摊牌了,先前确实还没干过这活,这大概就是要现场表演下传说中的从0到1了……

基础知识储备

打开搜索引擎一通搜索,了解了关于微信小程序源码几个核心的扫盲问题:

1、微信小程序源码如何获得?

微信小程序客户端的源文件由开发者发布后会存放在微信官方的服务器上。用户在微信客户端访问小程序后,会将其客户端源码打包下载到本地(就像APP的客户端程序),所以我们可以在手机本地找到对应小程序客户端的安装包,并进一步进行反编译获得源码。

2、微信小程序安装包存放在哪?

手机本地存放微信小程序安装包的具体目录位置为:

/data/data/com.tencent.mm/MicroMsg/XXXXXXXX(命名很长的文件夹,据说是用户随机码)/appbrand/pkg/

在这个目录下会发现一些xxxx.wxapkg类型的文件,这些就是微信小程序的安装包(二进制文件,还需要进一步进行反编译才能获得源码,类似获得 APP 的 APK 安装包后还需进一步进行反编译)。但是从上面的/data/data路径可以看出,必须 root 环境下的手机才能获取到目标文件。

3、如何反编译小程序安装包?

拿到xxxxx.wxapkg类型的微信小程序安装包以后,如何反编译获得小程序源码?大佬已经给我们写好现成的反编译脚本了,拿来即可食用:wxappUnpacker 。

逆向环境准备

了解完上述知识,顿时觉得这活有盼头了,撸起袖子准备干。获取源码前,先来准备下逆向环境。

1、安装 node.js 运行环境

访问 node 的下载地址,下载安装后设置系统环境变量,成功后如下:

2、下载反编译脚本文件 wxappUnpacker 到本地并解压缩:

然后需要运行以下命令安装对应的依赖:

npm install esprima
npm install css-tree
npm install cssbeautify    
npm install vm2    
npm install uglify-es    
npm install js-beautify

3、使用 DDMS(或者 adb、RE文件管理器)工具,从手机模拟器中找到并导出目标小程序的安装包:

【注意】如果发现 pkg 文件夹下当前存在的xxxxx.wxapkg安装包太多,分不清是哪一个的话,可以提前清空、删除 pkg 文件夹下的xxxxx.wxapkg文件,再重新运行目标小程序;同时注意多点击几下程序,使得手机能够从微信服务器下载完整的安装包(本人目标程序点击后生成了如图所示的4个xxxxx.wxapkg文件)。

4、将目标小程序的安装包导出到本地,完成前期的准备工作:

反编译出源码

准备工作完成,接下来开始运行反编译脚本,对获取到的xxxxx.wxapkg安装包文件进行反编译,由于不知道 4 个安装包文件中哪个包含了我想要的参数签名的源码,那就只能逐个反编译出来看看了。

1、先看第一个,执行命令node wuWxapkg.js + file,运行脚本对目标文件进行反编译:

2、运行结果如下,报错信息提醒当前反编译的包不是程序的主安装包:

3、既然如此,那就接着反编译下一个安装包文件,成功反编译:

4、接着到xxxxx.wxapkg存储路径下查看反编译成功后自动生成的存放源码的文件夹,可以看到已经成功获取到目标小程序的客户端源码:

至此,烫手的砖头搬完一半了,可以准备点个外卖吃晚饭了~

破解签名算法

吃饭先搁一边,继续肝,破解完签名算法,晚饭才能吃得香哈哈(干饭人)。

源码算法分析

1、在 VS Code 打开源码文件夹,搜索sign关键词,发现request.js文件存在疑似签名函数:

2、经过审计分析,该位置确实是要找的目标签名函数,签名大致流程是MD5(固定盐值+时间戳timestamp),核心代码如下:

get_signature_timestamp: function() {var e = new Date().getTime();return {timestamp: e,signature: c("SvZy7GUy5mqWk15l4F3Ivb1IXCWOhnAm" + e)};
}

3、接下来使用 MD5 在线加密验证一下,确认已成功找到签名算法:

既然知道目标小程序如何计算签名参数了,那么接下来,就可以使用 在线生成时间戳的网站结合 MD5 在线转换网站来计算新的签名值,然后手动替换数据包中对应的时间戳、签名值进行重放。实测发现这种做法虽然可以成功重放数据包,然而,这样子测试的话特别折腾!

难以忍受这种测试方式的龟速不说,进一步测试还发现,由于有时从浏览器复制计算出的新签名值到 BurpSuite 进行黏贴的过程手速太慢,会导致签名失效……此处猜测目标小程序的服务端应该校验了发送请求中包含的时间戳与服务器接收到请求时的时间戳之间的时间间隔,间隔太久的话则返回 400 报错。

脚本计算签名

作为 21 世纪的新一代青年,自然不能忍受这种机械式体力活,于是乎,掏出 IDEA,编写脚本自动计算新的时间戳和签名值:

import java.util.Date;
import java.security.MessageDigest;public class MD5Test {public String toMD5(String plainText) {try {//生成实现指定摘要算法的 MessageDigest 对象。MessageDigest md = MessageDigest.getInstance("MD5");plainText="SvZy7GUy5mqWk15l4F3Ivb1IXCWOhnAm"+plainText;//使用指定的字节数组更新摘要。md.update(plainText.getBytes());//通过执行诸如填充之类的最终操作完成哈希计算。byte b[] = md.digest();//生成具体的md5密码到buf数组int i;StringBuffer buf = new StringBuffer("");for (int offset = 0; offset < b.length; offset++) {i = b[offset];if (i < 0)i += 256;if (i < 16)buf.append("0");buf.append(Integer.toHexString(i));}return buf.toString();}catch (Exception e) {e.printStackTrace();}return plainText;}public static void main(String[] arg) {// 获取当前时间戳,精确到毫秒;然后计算对应的签名值long now_time=new Date().getTime();System.out.println("当前新的时间戳 timestamp:"+now_time);String now_sign=new MD5Test().toMD5(String.valueOf(now_time));System.out.println("当前新的签名值 signature:"+now_sign);}
}

运行脚本获得新的时间戳和签名值如下:

成功利用计算所得的新的时间戳和签名值进行数据包重放:

插件自动替换

本来到这里已经可以愉快地次饭去了,但是,作为 21 世纪的新一代青年……理应追求极致效率(说到底上面复制黏贴还是太麻烦了)!

于是乎,继续掏出 IDEA,编写 BurpSuite 插件(关于 BurpSuite 插件编写的基础知识请自行百度……),实现 Repeater 模块重放数据包时,会自动计算新的时间戳、签名值并自动替换,达到全自动的效果。

不废话了,直接放上插件最终的核心源码BurpExtender.java

package burp;import java.io.PrintWriter;
import java.util.List;
import java.util.Date;
import java.security.MessageDigest;public class BurpExtender implements IBurpExtender, IHttpListener
{// implement IBurpExtenderprivate PrintWriter stderr;private PrintWriter stdout;private IExtensionHelpers helpers;@Overridepublic void registerExtenderCallbacks(burp.IBurpExtenderCallbacks callbacks){callbacks.setExtensionName("My Sign Plugin");stdout = new PrintWriter(callbacks.getStdout(), true);stderr = new PrintWriter(callbacks.getStderr(), true);stdout.println("Success!Enjoy it!\n");this.helpers = callbacks.getHelpers();callbacks.registerHttpListener(this);}//processHttpMessage handle requests and responses from HttpListener@Overridepublic void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) {// Process only Repeater, Scanner and Intruder requestsif(toolFlag == IBurpExtenderCallbacks.TOOL_SCANNER ||toolFlag == IBurpExtenderCallbacks.TOOL_REPEATER ||toolFlag == IBurpExtenderCallbacks.TOOL_INTRUDER ) {if(messageIsRequest){//处理请求数据包Handle_Request_Packet(messageInfo);}else {//处理返回数据包Handle_Response_Packet(messageInfo);}}}//处理请求数据包private void Handle_Request_Packet(IHttpRequestResponse messageInfo){//获取请求数据包byte[] request = messageInfo.getRequest();IRequestInfo requestInfo = helpers.analyzeRequest(messageInfo);//String url = requestInfo.getUrl().toString();int bodyOffset = requestInfo.getBodyOffset();//获取所有的请求头List<String> headers = requestInfo.getHeaders();//获取所有的请求body体String body = new String(request).substring(bodyOffset);if(body.indexOf("signature") >= 0) {stdout.println("Before change:\n" + body);//计算当前的时间戳和签名值long new_time = new Date().getTime();String new_sign = new BurpExtender().toMD5(String.valueOf(new_time));//提取原始请求中的时间戳和签名值int time_start = body.indexOf("&timestamp");String oldtimestamp = body.substring(time_start + 11, time_start + 24);int sign_start = body.indexOf("&signature=");String oldsign = body.substring(sign_start + 11, sign_start + 43);//替换原始请求中的时间戳和签名值body = body.replace(oldtimestamp, String.valueOf(new_time));body = body.replace(oldsign, new_sign);//修改后的数据替换原始的请求包String newBody = body;stdout.println("After change:\n" + newBody);//重构数据包的目的是因为修改完请求体body后,需要将请求头head和请求体body重新拼接起来后再发送给服务器byte[] bodyByte = newBody.getBytes();byte[] new_Request = helpers.buildHttpMessage(headers, bodyByte);//stdout.println("After change:\n" + new String(new_Request));messageInfo.setRequest(new_Request);}}//处理返回数据包private void Handle_Response_Packet(IHttpRequestResponse messageInfo){//忽略,无需做任何处理}public String toMD5(String plainText) {try {//生成实现指定摘要算法的 MessageDigest 对象。MessageDigest md = MessageDigest.getInstance("MD5");plainText="SvZy7GUy5mqWk15l4F3Ivb1IXCWOhnAm"+plainText;//使用指定的字节数组更新摘要。md.update(plainText.getBytes());//通过执行诸如填充之类的最终操作完成哈希计算。byte b[] = md.digest();//生成具体的md5密码到buf数组int i;StringBuffer buf = new StringBuffer("");for (int offset = 0; offset < b.length; offset++) {i = b[offset];if (i < 0)i += 256;if (i < 16)buf.append("0");buf.append(Integer.toHexString(i));}return buf.toString();}catch (Exception e) {e.printStackTrace();}return plainText;}
}

来看看不使用插件的情况下,直接重放上面的数据包的结果:

最后上大招,从 IDEA 导出、生成 jar 插件文件并导入 BurpSuite:

下面就是见证奇迹的时候了!来看看这时候重放数据包的效果:

成功重放hh,同时可以在插件的输出日志里查看到时间戳和签名值的自动替换记录:

至此,就可以愉快地继续进行渗透测试啦!

总结

本次测试过程从0到1接触了微信小程序的逆向,首先通过审计分析出计算参数签名的源码逻辑,接着编写了自动计算的时间戳和签名值的脚本,再到最后开发 BurpSuite 自动化插件,这过程也算小有收获了。

最后总结下进一步思考的几个问题:

1、实际上很多进行参数签名校验的系统的会采用对整个数据包的参数进行签名的方式,而非像本文所述的案例(只是对时间戳进行 MD5 哈希加盐),具体的签名算法破解需要逆向分析不同系统的源码;

2、时间戳和参数签名确实是防止数据篡改、重放的有力措施,而这个过程安全性的保障的核心在于防止签名算法中的加密密钥 secret (即本文案例中的硬编码盐值)泄露;

3、开发人员可通过对微信小程序客户端进行安全加固(如代码混淆)的方式来增加攻击者分析、获取加密密钥的难度。

从本次测试也可以看出,开发人员不应该过度依赖客户端参数签名机制抵御网络攻击,应该着重于重视、强化服务端代码自身业务逻辑的安全性!

本文作者:Tr0e


http://chatgpt.dhexx.cn/article/wKURUlV4.shtml

相关文章

Flowable多实例加签减签功能正式上线

文章的最下面有学习视频与源代码 拒绝重复造轮子了&#xff0c;有这个流程平台就够了 概念 什么是加签 在一个多实例会签流程中&#xff0c;会签节点审批人有A、B两个人。当A领导登录的时候&#xff0c;看到当前节点只有两个人&#xff0c;责任承担比较大。 然后我们就想把C…

flowable6.4 并行网关 驳回 跳转 回退 多实例加签减签

目录 前言flowable6.4.2 不足之处并行网关跳转驳回BUG多实例加签减签BUG 解决之法说明用法 前言 flowable 优秀的工作流流程引擎框架&#xff0c;前身Activity 为什么要用工作流&#xff1f; 主要是应对&#xff1a; 流程可能随时变更步骤处理人可能变更并行任务处理分支任务…

工作流后加签实现

1、流程重新定义 动态改变流程指向&#xff0c;创建新的节点 2、后加签环节及后加签属性设置 流程持久化、克隆活动、独立CMD 3、原始流程操作及后加签环节操作 4、流程场景遍历 5、流程图元体现 BPM后加签V1.0版说明。 效果图示&#xff1a; 功能说明&#xff1a; 1、…

前后端RSA互相加解密、加签验签、密钥对生成(Java)

目录 一、序言二、关于PKCS#1和PKCS#8格式密钥1、简介2、区别 二、关于JSEncrypt三、关于jsrsasign四、前端RSA加解密、加验签示例1、相关依赖2、cryptoUtils工具类封装3、测试用例 五、Java后端RSA加解密、加验签1、CryptoUtils工具类封装2、测试用例 六、前后端加解密、加验签…

国密:SM2公私钥加签验签

一、工具类 POM中增加hutool <dependency><groupId>cn.hutool</groupId><artifactId>hutool-core</artifactId><version>5.6.5</version></dependency> package cn.test.encrypt.utils.sm2; /*** SM2签名所计算的值 可以根据…

RSA加签解签方法

一、RSA签名的过程 &#xff08;1&#xff09;A生成一对密钥&#xff08;公钥和私钥&#xff09;&#xff0c;私钥不公开&#xff0c;A自己保留。公钥为公开的&#xff0c;任何人可以获取。 &#xff08;2&#xff09;A用自己的私钥对消息加签&#xff0c;形成签名&#xff0…

安全对外的接口--加签验签

前言 我们在求职面试中,经常会被问到,如何设计一个安全对外的接口呢? 其实可以回答这一点,加签和验签,这将让你的接口更加有安全。接下来,本文将和大家一起来学习加签和验签。从理论到实战,加油哦~ 密码学相关概念加签验签概念为什么需要加签、验签加密算法简介加签验签…

程序员必备基础:加签验签

来源&#xff1a;捡田螺的小男孩 我们在求职面试中&#xff0c;经常会被问到&#xff0c;如何设计一个安全对外的接口呢? 其实可以回答这一点&#xff0c;加签和验签&#xff0c;这将让你的接口更加有安全。接下来&#xff0c;本文将和大家一起来学习加签和验签。从理论到实战…

RSA公私钥加密加签顺序

RSA是一种非对称加密的机制&#xff0c;是一对密钥对&#xff08;公钥和私钥&#xff09;。 一、加密&#xff1a; 1.公钥加密&#xff0c;私钥解密。可以多人持有公钥进行数据加密&#xff0c;仅一人持有私钥进行数据解密&#xff1b; 2.可以确保数据传输的安全性。 二、加签…

【flowable】十一、flowable加签和转签

flowable加签和转签 加签 同时让多人处理任务&#xff0c;但是不同于网关操作。 1、向前加签 任务在A这里&#xff0c;A这个时候需要BCD核对一下&#xff0c;等BCD核对之后又回到A这里 2、向后加签 任务在A这里&#xff0c;A这个时候需要BCD处理这个事情&#xff0c;处理完毕…

加解密,加签、验签也就这肥事

前言&#xff1a; ps:上图有一处错误&#xff0c;右下角有一条线是N&#xff0c;指向的是不读取内容 上图省略了CA认证过程&#xff0c;直接提取到了密钥对&#xff0c;Base64的过程已省略(关于Base64的底层原理&#xff0c;可参考标题3)。全文1.6w字&#xff0c;如果想全部…

前后端分离技术之加签,验签,防篡改

上一篇讲解了加密解密&#xff0c;这次来个加签验签&#xff0c;实际项目里&#xff0c;我们采用的是react 和nodejs 来进行加签验签&#xff0c;用的jsrsasign库&#xff0c;下面贴点核心代码 react加签 nodejs验签 实际应用在nodejs层可以将时间戳和sign签名验证通过剔除掉&…

深入探究Camunda加签问题

开源项目介绍&#xff1a;点击直达 前言 这里我们先抛出两个大问题&#xff0c;整篇文章针对这两个大问题再详细解析。 首先我们在设计流程定义时&#xff0c;流程节点可能是或签也可能是会签 会签&#xff1a;指同一个审批节点设置多个人&#xff0c;如ABC三人&#xff0c…

activity多实例任务加签

前言 加签是减签的相反的操作,加签与减签的思路刚好相反,减签是删数据;加签则是添加数据。在一些特殊场景下,需要在某个多实例节点上面动态新增一个审批人员或任务时,就需使用到activity多实例任务加签的功能 加签思路1 根据一级流程实例查找二级流程执行实例判断二级执…

Activiti7工作流引擎:进阶篇(十二) 加签和转签

知识传送门 》》》》》》》》》》》》》》》》》》》 加签就是委派任务delegateTask&#xff0c;然后去解决任务resolveTask&#xff08;并不是去真正的去完成任务&#xff09;。转签完成后才能完成任务complete。 一&#xff1a;委派任务 A由于某些原因不能处理该任务可以把任…

必备基础:加签验签

必备基础&#xff1a;加签验签 1 密码学相关概念1.1 明文、密文、密钥、加密、解密1.2 对称加密、非对称加密1.3 什么是公钥私钥&#xff1f; 2 加签验签概念3 为什么需要加签验签4 常见加密相关算法简介4.1 消息摘要算法4.1.1 MD家族算法4.1.2 ShA家族算法4.1.3 MAC算法家族 4…

Flowable工作流之加签(委派)、转签(转办)

目录 1. 加签1.1. 向前加签1.2. 向后加签 2. 或签3. 委派和转办的区别2.1. 委派2.2. 转办 4. 向前加签4.1. 流程图4.2. 部署并启动4.3. 完成任务4.4. 向前加签实现4.4.1. 添加加签&#xff08;委派&#xff09;功能任务4.4.1. 加签&#xff08;委派&#xff09;任务的完成 5. 转…

加密、解密、加签、验签专题

首先明确几个名词&#xff1a; 加密&#xff1a;发送方利用接收方的公钥对要发送的明文进行加密。 解密&#xff1a;接受方利用自己的私钥进行解密。 公钥和私钥配对的&#xff0c;用公钥加密的文件&#xff0c;只有对应的私钥才能解密。当然也可以反过来&#xff0c;用私钥…

@ControllerAdvice 之 @InitBinder和@ModelAttribute

InitBinder 准备 InitBinder 在整个 HandlerAdapter 调用过程中所处的位置 收获&#x1f4a1; RequestMappingHandlerAdapter 初始化时会解析 ControllerAdvice 中的 InitBinder 方法&#xff08;可以定义个性化数据转化器&#xff09;RequestMappingHandlerAdapter 会以类…

SpringBoot @InitBinder注解实现Bean国际化校验

参考资料 参考&#xff1a; 妥当性チェックのエラーメッセージ出力方法 (需翻墙)springMVC之InitBinder的用法1springMVC之InitBinder的用法2springMVC之InitBinder 和 ValidatorSpring MVCにおけるフォームバリデーションの適用事例【後編】 目录 一. 前期准备1.1 自定义校验…