文章目录

  • 前言
  • 小程序逆向
    • 基础知识储备
    • 逆向环境准备
    • 反编译出源码
  • 破解签名算法
    • 源码算法分析
    • 脚本计算签名
    • 插件自动替换
  • 总结

前言

本文首发于 Freebuf 安全社区,未经许可谢绝转载!

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

然鹅,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 插件,实现 Repeater 模块重放数据包时,会自动计算新的时间戳、签名值并自动替换,达到全自动的效果。

不废话了,直接放上插件最终的核心源码BurpExtender.java(关于 BurpSuite 插件编写的基础知识请自行百度……):

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

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

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

  1. 逆向某微信小程序参数签名算法

    获取微信小程序压缩包 某小程序请求中有sign参数,包含在url或header中..... 打开微信小程序时,微信会把小程序压缩包(后缀名.wxapkg)下载到本地: 从目录 /data/data/c ...

  2. 破解微信小游戏-动物餐厅之无限小鱼干

    女票最近在玩微信小游戏-动物餐厅,说小鱼干获取的太慢了,后来经过尝试,找到了可以无限添加小鱼干的方法,分享一下. 先上效果图: 1.第一步 逆向砸壳 不会的请参考 之前这篇文章 iOS逆向-支付宝基金 ...

  3. 逆向获取微信小程序源码教程

    最近看上了一个小程序,想着走走捷径,以下是我的步骤. 一.获取小程序包 1.安装安卓模拟器,我用的是夜神 2.拿包 打开微信,运行微信小程序,然后打开文件管理器,根据时间顺序定位到小程序目录 /dat ...

  4. 微信小程序参数二维码生成朋友圈分享图片

    前言 小程序目前无法分享到微信朋友圈,可朋友圈是一个非常重要的传播途径,所以得想办法把这个资源利用起来 微信小程序支持通过扫描/长按识别二维码或小程序码图片的方式进入一个小程序首页或小程序中某个特定页 ...

  5. 微信小程序参数二维码的8大使用场景

    一.小程序参数二维码的8大使用场景 1 地推.     使用小程序参数二维码组织地推,考核地推人员,评估渠道效果.   2 广告投放     使用小程序参数二维码统计广告效果   3 门店运营     ...

  6. nodejs+koa2实现微信小程序签名和请求支付(二)

    废话不多说直接上代码: const getTradeNo = function() {let date = new Date();let arr = [date.getFullYear(),((dat ...

  7. 微信支付V3 微信小程序签名失败问题

    微信支付的V3版本使用的是RSA加密,从前的V2版本使用的是MD5加密.今天在调试微信小程序的时候,始终无法调起微信支付,提示"支付签名验证失败" 问题排查思路: 1. 先调起其它 ...

  8. 微信小程序签名(横屏+竖屏)

    横屏 wxml <view class="container"><canvas canvas-id="firstCanvas" id=&quo ...

  9. 微信小程序 - 签名

    签名并获取签名图片上传至服务器 wxml:<button class="sign-btn" bind:tap="confirmSign">确认< ...

最新文章

  1. 【MyBatis学习01】宏观上把握MyBatis框架
  2. 【建站系列教程】1、前言
  3. C#中常用的经典文件操作方法
  4. 关于 OneAPM Cloud Test DNS 监控的几个重要问题
  5. 如何得到通过GetOpenFileName选择的多个文件的文件名
  6. Map集合HashMap TreeMap的输出方法
  7. docker学习笔记(四)docker数据持久化volume
  8. 前端学习(751):Javascript作用域
  9. 数据库系统原理笔记:关系数据库设计
  10. 卡方分布分位数_数理统计第五讲(三大分布)
  11. 工作文档化升级为工作列表化
  12. 中国城市统计年鉴1985-2021中国城市年鉴面板数据(完美Excel版)
  13. 大数据离线分析之企业实战分享
  14. Linux里怎么打开pt文件,在 Linux 上安装 transmission 进行远程 PT 下载
  15. 广告策划书的一般模式
  16. PCIE实现PIO模式寄存器读写调试记录
  17. Windows常见基本进程三:dumprep or dumprep进程(Dump Reporting Tool启动项)
  18. ggplot2-散点图的边框与填充色问题
  19. 利用R语言制作好看的Meta分析文献偏倚风险图
  20. vue.js 表格表单序号

热门文章

  1. 关于jmeter body Data 传参报错message“:“\u7528\u6237\u540d \u4e0d\u80fd\u4e3a\u7a7a\u3002“的解决方法
  2. SQL注入之order by注入与limit注入
  3. Angular4+ng2-ckeditor踩坑
  4. centos7默认字体_CentOS7安装字体库
  5. 周伯通的空明拳,米诺斯的星尘傀儡线,SAP Kyma的Serverless
  6. 解决Unknown column ‘xxx‘ in ‘where clause‘问题!!(泪的教训!!)
  7. 服务器虚拟化和网络虚拟化关系,数据中心网络如何应对服务器虚拟化?
  8. Win10怎么通过ip添加网络打印机?
  9. Python3,区区5行代码,让黑白老照片变成华丽的彩色照,被吸粉了。
  10. 华为云云筑·开发者年度盛典精彩回顾