lixiaolevae 发表于 2021-7-12 22:56

某商超小程序加密算法解析

**​**

**​**

# 初入道途
## 抓包分析
### 工具
#### charles-网络抓包
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210706151936.png)

下载地址:https://www.charlesproxy.com/

(前提:手机和电脑均安装好charles证书)

证书安装及支持抓包https设置指引请参考: https://blog.csdn.net/victory0943/article/details/106332095/

#### postman   -接口调试工具

!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712171621.png)

下载地址:https://www.postman.com/

支持导入cURL,便捷高效,导入操作如下图

!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712171745.png)

#### RE文件管理器-android文件导出工具(需要root权限)

!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712174731.png)

下载地址:https://m-k73-com.sm-tc.cn/c/m.k73.com/mipw/574951.html

### 运行环境
华为p9 android 6.0

(android7.0以上版本抓包工具默认抓不到https请求,因为7.0以上只信任系统级别证书,而charles证书是安装到用户级目录的。

解决方式:可将charles证书升级为系统证书,即安装证书到系统证书目录下。

具体操作可参考连接:https://www.pianshen.com/article/97291182754/ )

### 抓包接口分析

#### 抓取通过经纬度获取门店的接口

手机上操作该小程序,找到可以进行重新定位的地方点击来触发请求以获取附近的门店,随后charles捕捉到相关接口请求

!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712223517.png)

选中相关请求右键复制其cURL格式数据 ,导入到postman进行调试分析

!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712173322.png)

#### cURL数据分析:

观察发现是个post请求, 请求体是URL编码后的,不易阅读,我们进行url解码

(注意这里获取的cURL接口数据和图例所示的不是同一个请求,图例所示的抓包接口被笔者不小心清除了,于是重新抓了一次请求~)

```bash
curl -H 'Host: yx.feiniu.com' -H 'content-type: application/x-www-form-urlencoded' -H 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.7(0x18000731) NetType/WIFI Language/zh_CN' -H 'Referer: https://servicewechat.com/wx08cc6bd15fabfa53/83/page-frame.html' --data-binary "data=%7B%22apiVersion%22%3A%22t141%22%2C%22appVersion%22%3A%221.5.1%22%2C%22areaCode%22%3A%22CS000016%22%2C%22channel%22%3A%22online%22%2C%22clientid%22%3A%22a7ea53059fc868e2e3e2dd7c04027035%22%2C%22device_id%22%3A%22tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE%22%2C%22time%22%3A1626080760465%2C%22reRule%22%3A%224%22%2C%22token%22%3A%227ae362df162da5ffbfc408ed8e3d4ff3%22%2C%22viewSize%22%3A%22720x1184%22%2C%22networkType%22%3A%22wifi%22%2C%22isSimulator%22%3Afalse%2C%22osType%22%3A%224%22%2C%22scopeType%22%3A1%2C%22businessType%22%3A2%2C%22businessId%22%3A%2217210001%22%2C%22deliveryCircleType%22%3A%221%22%2C%22body%22%3A%7B%22longitude%22%3A%22MTIwLjE1NDc3NQ%3D%3D%22%2C%22latitude%22%3A%22MzAuMzA1ODIy%22%7D%7D&h5=yx_touch&paramsMD5=iOWz8O%2BxL9r9GX4k5Te%2F2U5HGTRk1GQ6YqLnMErWrAI%3D" --compressed 'https://yx.feiniu.com/member-yxapp/location/homeStoreList/t141'
```

如下为url解码后的cURL接口数据,这下好看多了~

```shell
curl -H 'Host: yx.feiniu.com' -H 'content-type: application/x-www-form-urlencoded' -H 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.7(0x18000731) NetType/WIFI Language/zh_CN' -H 'Referer: https://servicewechat.com/wx08cc6bd15fabfa53/83/page-frame.html' --data-binary "data={"apiVersion":"t141","appVersion":"1.5.1","areaCode":"CS000016","channel":"online","clientid":"a7ea53059fc868e2e3e2dd7c04027035","device_id":"tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE","time":1626080760465,"reRule":"4","token":"7ae362df162da5ffbfc408ed8e3d4ff3","viewSize":"720x1184","networkType":"wifi","isSimulator":false,"osType":"4","scopeType":1,"businessType":2,"businessId":"17210001","deliveryCircleType":"1","body":{"longitude":"MTIwLjE1NDc3NQ==","latitude":"MzAuMzA1ODIy"}}&h5=yx_touch&paramsMD5=iOWz8O+xL9r9GX4k5Te/2U5HGTRk1GQ6YqLnMErWrAI=" --compressed 'https://yx.feiniu.com/member-yxapp/location/homeStoreList/t141'
```
观察可知有data、h5、paramsMD5三个参数,整理如下:

```yaml
data: {"apiVersion":"t141","appVersion":"1.5.1","areaCode":"CS000016","channel":"online","clientid":"a7ea53059fc868e2e3e2dd7c04027035","device_id":"tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE","time":1626080760465,"reRule":"4","token":"7ae362df162da5ffbfc408ed8e3d4ff3","viewSize":"720x1184","networkType":"wifi","isSimulator":false,"osType":"4","scopeType":1,"businessType":2,"businessId":"17210001","deliveryCircleType":"1","body":{"longitude":"MTIwLjE1NDc3NQ==","latitude":"MzAuMzA1ODIy"}}
h5: yx_touch
paramsMD5: iOWz8O+xL9r9GX4k5Te/2U5HGTRk1GQ6YqLnMErWrAI=
```

手机重复操作,经多次调用抓包该接口后对比发现:

`h5` 这个值是固定的`yx_touch`

`paramsMD5` 通过字面意思判断为加密参数,但其数据格式不像MD5,猜测是用了MD5后又进行了其他的编码加密

观察可知获取门店要传入的经纬度入参也是加密的,正常来说经纬度均是数字
`{"longitude":"MTIwLjE1NDc3NQ==","latitude":"MzAuMzA1ODIy"}`

### 破解目标

1. **paramsMD5加密逻辑**
1. **经纬度加密逻辑**

**​**

# 初领妙道


## 逆向之旅
### 获取微信小程序包wxapkg
#### 所需工具
前述提到的RE文件管理器app

#### 小程序主子包判断依据

如今微信小程序单包体积不能超过4M(小程序基础依赖包除外),如果项目内容过大,开发者会使用分包模式

拿下图举例来说(下图所示小程序包是其他应用的,非本文要分析的case)

!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712164946.png)

其中:

_2124598774_821.wxapkg      3.3M 主包

_-588782754_76.wxapkg            1.5M 子包

_152740959_13.wxapkg            89k子包

_1123949441_552.wxapkg      14M基础依赖包

#### 操作

打开小程序一顿操作后,会在小程序包存放目录下自动下载生成对应的包

通过re文件管理器直捣微信小程序包路径:
/data/data/com.tencent.mm/MicroMsg/"$用户MD5"/appbrand/pkg/_*_xxx.wxapkg
通过re文件管理器打成zip包发送到个人钉钉或者QQ、微信等,电脑完成文件接收

提示:若在之前打开过多个小程序,可以先进入目录全部删除,这样好区分小程序包的归属

####

### 反编译
#### 工具

##### wxUnpacker

下载地址:https://gitee.com/guo492273770/wxappUnpacker
运行前提需要安装node环境
该工具运行需要一些node依赖库,安装指引在链接中README.md文档中有

#### 原理
等我弄明白了~, 有基础的同学可以参考这个 https://mp.weixin.qq.com/s/4BerA1Ij3BfMeg2LA0cm5g

笔者太菜,看的不太懂~

#### 具体命令
```shell
# 主包反编译
node wxWxapkg.js ../../wxapkg/xxxx/_-2094256841_77.wxapkg
# 子包反编译
node wxWxapkg.js-s=/Users/toretto/crack/wxapkg/xxxx/_-2094256841_77../../wxapkg/xxxx/_571009734_77.wxapkg
....
#部分子包反编译可能会报错,但没关系,不影响后续的加密分析过程
```

# 渐入佳境
## 加密分析
### 工具
#### 微信开发者工具

下载地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html

用于阅读代码,代码跳转追踪

### 分析环境预备

1. 导入反编译主包目录

!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712165335.png)

2. 起个名字,点击测试号生成个AppID,创建小程序
   !(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712165410.png)

### 静态分析

#### paramsMD5分析

是骡子是马拉出来溜溜,不是有个加密叫paramsMD5吗,全局搜索试试看:

1. 关键位置定位

好嘛,定位到2处代码,直觉告诉我选request.js中的,直接定位到一个函数getHmacSha256(n)。
再看看这个data结构,含data、h5、paramMD5,和之前接口分析的结论一致,通用格式没的说~

!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712165507.png)


2. getHmacSha256(n)点进去
```javascript
function getHmacsha256(e) {
    var n = JSON.stringify(e) + e.isSimulator + e.viewSize + e.networkType + e.time, t = _common2.default.environment === _config.ENVIRONMENTS.BETA ? "@yx789*&^DKJ##CC" : "@653yx#*^&HrTy99";
    console.log("request.js@32 n: " + n);
    return _encBase2.default.stringify((0, _hmacSha2.default)(n, t));
}
```
变量n:
通过JSON.stringify(e)可以猜测,参数e是接口数据中的data,为json对象。n的逻辑基本上可以锁定是data本身的字符串再拼接上data的几个key对应的value值

变量t:
三目运算表达式,为true时貌似表示是BETA版本运行,那么正常使用的版本应该是false,所以猜测t="@653yx#*^&HrTy99",是一个固定盐值。



将前述接口分析中的参数data进行复制,粘贴进来改一下代码再加一个打印语句执行调试看看结果:

```javascript
var a = {"apiVersion":"t141","appVersion":"1.5.1","areaCode":"CS000016","channel":"online","clientid":"a7ea53059fc868e2e3e2dd7c04027035","device_id":"tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE","time":1626080760465,"reRule":"4","token":"7ae362df162da5ffbfc408ed8e3d4ff3","viewSize":"720x1184","networkType":"wifi","isSimulator":false,"osType":"4","scopeType":1,"businessType":2,"businessId":"17210001","deliveryCircleType":"1","body":{"longitude":"MTIwLjE1NDc3NQ==","latitude":"MzAuMzA1ODIy"}};


function getHmacsha256(e) {
    // var n = JSON.stringify(e) + e.isSimulator + e.viewSize + e.networkType + e.time, t = _common2.default.environment === _config.ENVIRONMENTS.BETA ? "@yx789*&^DKJ##CC" : "@653yx#*^&HrTy99";
    var n = JSON.stringify(e) + e.isSimulator + e.viewSize + e.networkType + e.time, t = "@653yx#*^&HrTy99";
    console.log("request.js@32 n: " + n);
    return _encBase2.default.stringify((0, _hmacSha2.default)(n, t));
}

console.log(getHmacsha256(a));
```
```javascript
#运行
node request.js
# 运行结果报错:
regeneratorRuntime is not defined   在 comment.js中
```
针对这个错误,百度了一番找到个解决方案:
(https://blog.csdn.net/sinat_33184880/article/details/85533095)

小程序使用async出现regeneratorRuntime is not defined错误说是少个依赖库,下载之

```bash
#生成package.json
npm init
#下载缺少的包
npm install regenerator@0.13.1
# 将所缺文缺runtime.js移动到项目中
cd node_modules/regenerator-runtime/
#与common.js同目录
cp runtime.js /Users/toretto/crack/wxapkg/darunfa/_-2094256841_77/service/
```
修改common.js代码引入该包:
```javascript
// 最上方添加
import regeneratorRuntime from './runtime.js'
```
```javascript
#再次运行
node request.js
#还报错:
can not import modules from outside
#不能从外部导入文件,没有js基础的我盲猜可能是微信小程序无此语法(因为代码全局搜索import关键字后没有任何匹配项)。
```
**解决思路**
我看到小程序代码中有这样的片段:

```javascript
var _wepy = require("./../npm/wepy/lib/wepy.js"),
```
看起来就是引入库的方式,于是我学了下写了这样一段:
```javascript
var regeneratorRuntime = require('./runtime.js');
```
再次运行:
```javascript
node reuqest.js
iOWz8O+xL9r9GX4k5Te/2U5HGTRk1GQ6YqLnMErWrAI=
//呵,可以运行了,照猫画虎成功~
//打印输出的加密和接口获取的一致
```
继续分析它的生成逻辑,追到相关代码,添加打印语句:
```javascript
f.Base64 = {
            stringify: function(r) {
                console.log("\nenc-base64@21 function(r):" + r);
                console.log("\nenc-base64@22 rObj:" + JSON.stringify(r));
                var e = r.words, t = r.sigBytes, o = this._map;
                r.clamp();
                for (var n = [], f = 0; f < t; f += 3) for (var i = e >>> 24 - f % 4 * 8 & 255, a = e >>> 24 - (f + 1) % 4 * 8 & 255, c = e >>> 24 - (f + 2) % 4 * 8 & 255, p = i << 16 | a << 8 | c, s = 0; s < 4 && f + .75 * s < t; s++) n.push(o.charAt(p >>> 6 * (3 - s) & 63));
                var u = o.charAt(64);
                if (u) for (;n.length % 4; ) n.push(u);
                const ret = n.join("");
                console.log('\nenc-base64.js@27 n.join(""): ' + ret);
                return ret;
            }
```
上述r是个base64对象,且用到了它的words和sigBytes两个属性:
```javascript
toString(r):
88e5b3f0efb12fdafd197e24e537bfd94e47193464d4643a62a2e7304ad6ac02

rObj:
{"words":[-1998212112,-273600550,-48660956,-449331239,1313282356,1691640890,1654843184,1255582722],"sigBytes":32}
```
继续分析r的生成逻辑, 添加几行打印语句:
```javascript
_createHmacHelper: function(t) {
                return function(n, e) {
                  console.log("libs/core.js@155 function(n, e) n:" + n);
                  console.log("libs/core.js@156 function(n, e) e:" + e);
                  console.log("libs/core.js@156 t:" + JSON.stringify(t));
                  const ret = new h.HMAC.init(t, e).finalize(n);
                  console.log("\nlibs/core.js@159 h.HMAC.init(t, e).finalize(n): " + JSON.stringify(ret))
                  return ret;
                };
            }
```
加了几句打印语句执行看看:
```javascript
//参数n:
n:{"apiVersion":"t141","appVersion":"1.5.1","areaCode":"CS000016","channel":"online","clientid":"a7ea53059fc868e2e3e2dd7c04027035","device_id":"tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE","time":1626080760465,"reRule":"4","token":"7ae362df162da5ffbfc408ed8e3d4ff3","viewSize":"720x1184","networkType":"wifi","isSimulator":false,"osType":"4","scopeType":1,"businessType":2,"businessId":"17210001","deliveryCircleType":"1","body":{"longitude":"MTIwLjE1NDc3NQ==","latitude":"MzAuMzA1ODIy"}}false720x1184wifi1626080760465
//参数e:
e:
@653yx#*^&HrTy99
```
很明显了,是用“@653yx#*^&HrTy99”作为key种子初始化加密对象,然后将拼接的字符串n传入进行加密
百科了一下,该方法背后调用了著名的加密Hmac-Sha256

看来关于密码学笔者也需要系统地学一学~

# 元神初具

## 加密翻译

前端能加密,后端一定有对应的解密。梳理一下上述分析的加密逻辑后,用java或者python写个测试demo验证一下

!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20210712190729.png)

```java
//执行一下发现与前述js源码执行的结果一致:
88e5b3f0efb12fdafd197e24e537bfd94e47193464d4643a62a2e7304ad6ac02
iOWz8O+xL9r9GX4k5Te/2U5HGTRk1GQ6YqLnMErWrAI=
```
### 另一种思路

混淆代码阅读性差,且代码量也繁杂,实现加密翻译或许有点吃力;那么我们转换思路,由“破译”转为“利用”
重新梳理一下上述加密流程,将涉及加密的代码整理出来,拷贝到一个js文件作为一个工具库来拿到最后的加密结果:
具体过程:

1. 代码中搜索hmac-sha256,发现它来自crypto-js文件

crypto-js 为前端一个加密库 介绍参见:(https://blog.csdn.net/caoyan0829/article/details/88886635)

```bash
# 下载
npm install crypto-js
# 复制该库
cp 'node_modules/crypto-js/crypto-js'crypto-js

```

2. 分析小程序中加密后的处理代码,在新的crypto-js文件中最后面添加下面这段逻辑
```bash
function stringify (r) {
    var e = r.words, t = r.sigBytes, o = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    r.clamp();
    for (var n = [], f = 0; f < t; f += 3) for (var i = e >>> 24 - f % 4 * 8 & 255, a = e >>> 24 - (f + 1) % 4 * 8 & 255, c = e >>> 24 - (f + 2) % 4 * 8 & 255, p = i << 16 | a << 8 | c, s = 0; s < 4 && f + .75 * s < t; s++) n.push(o.charAt(p >>> 6 * (3 - s) & 63));
    var u = o.charAt(64);
    if (u) for (;n.length % 4; ) n.push(u);
    //const常量不能用 Expected an operand but found const
    // console.log("\nenc-base64.js@40 n.join(\"\"): " + ret);
    return n.join("");
}

function getSignStr (str) {
    var hash = CryptoJS.HmacSHA256(str, key);
    // let hashInHex= CryptoJS.enc.Hex.stringify(hash);   //base64_str
    return stringify(hash);
}
```
拿接口中的data数据测试一下,没问题的话这个文件就可以作为获取加密签名的工具库了。适合前端开发没有后端基础的同学使用。



当然了后端同学用java或其他语言实现加密翻译较为吃力的话,也可以直接使用此js文件,下面说下思路:

就当做一个外部js资源文件就做好了,相关参考:(https://blog.csdn.net/u014174048/article/details/80899776)
让java通过调用脚本引擎的方式执行js代码得到加密结果:


1. crypto-js文件放到resource/js/下
1. 实现java串通js脚本的接口:
```java
public interface JavaScriptInterface {
    public String getSignStr(String str);
}

```

3. 功能测试
```java
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.FileReader;

public class ExecuteScript {

    public static void main(String[] args) {
      ScriptEngineManager manager = new ScriptEngineManager();
      ScriptEngine engine = manager.getEngineByName("js");
      String key = "{\"apiVersion\":\"t141\",\"appVersion\":\"1.5.1\",\"areaCode\":\"CS000016\",\"channel\":\"online\",\"clientid\":\"a7ea53059fc868e2e3e2dd7c04027035\",\"device_id\":\"tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE\",\"time\":1626080760465,\"reRule\":\"4\",\"token\":\"7ae362df162da5ffbfc408ed8e3d4ff3\",\"viewSize\":\"720x1184\",\"networkType\":\"wifi\",\"isSimulator\":false,\"osType\":\"4\",\"scopeType\":1,\"businessType\":2,\"businessId\":\"17210001\",\"deliveryCircleType\":\"1\",\"body\":{\"longitude\":\"MTIwLjE1NDc3NQ==\",\"latitude\":\"MzAuMzA1ODIy\"}}";
      try {
            String path = Thread.currentThread().getContextClassLoader().getResource("").getPath(); // 获取targe路径
            System.out.println(path);
            // FileReader的参数为所要执行的js文件的路径
            engine.eval(new FileReader(path+ "js/crypto-js.js"));
            if (engine instanceof Invocable) {
                Invocable invocable = (Invocable) engine;
                JavaScriptInterface executeMethod = invocable.getInterface(JavaScriptInterface.class);
                System.out.println(executeMethod.getSignStr(key));
            }
      } catch (Exception e) {
            e.printStackTrace();
      }
    }
}

```
```java
//执行结果:一致的
iOWz8O+xL9r9GX4k5Te/2U5HGTRk1GQ6YqLnMErWrAI=
```

那就这样吧,也不失为一种解决策略;倘若不是为了爬虫,完全复刻出java版的加密逻辑工作量太大没必要


### 经纬度加密分析

{\"longitude\":\"MTIwLjE1NDc3NQ==\",\"latitude\":\"MzAuMzA1ODIy\"}


经纬度的加密相比paramsMD5来说简单太多,这个不难就不展开了,大体说一下思路:

##### 静态分析


1. 全局所搜latitude,发现有个_base2.default.decode(e.latitude) 方法
1. 观察加密结果格式及结合代码上下文发现有base64相关的代码,由此猜测可能是base64加密
1. 相关位置加几句打印语句后运行验证下,可以成功还原为数字,MzAuMzA1ODIy->30.305822, 是6位小数位,其取值符合我国的维度取值范围,应该是正确的
1. 直接用java写段demo反向验证,用base64加密尝试下
```java
public static void main(String[] args) {
      String ret = Base64.getEncoder().encodeToString("30.305822".getBytes());
      System.out.println(ret);
    }
```
```java
#结果一直
MzAuMzA1ODIy
```

5. 结论:就是单纯的base64加密





## 妙领天机

心得:

1. 逆向需要耐心也需要大胆的猜想和假设去不断尝试,像文本所讲述的一些打印调试都是结合代码做了大胆的猜想后去验证得出的
2. 逆向工作会用到的很多好用的工具,平时注意多收集一些好用的工具或博文以事半功倍,本文所用到的工具和相关扩展知识点均贴出了链接,方便读者收藏~
3. 本文旨在分享一些逆向技巧和思路,本文所举case相关敏感已打码略去,读者不可利用本文所述内容进行非法商业获取利益,若执意带来的法律责任由读者自行承担。

yuqc 发表于 2021-7-13 21:39

学习了大佬! 之前一直以为小程序很安全, 今天才知道, 感谢大佬!

hzzheyang 发表于 2021-7-13 15:46

Cannot read property 'default' of undefined这怎么解决呢,楼主
_encBase2.default.stringify((0, a.default)(t, n));这行报错了

hong_sun 发表于 2021-7-13 08:58

学习了,谢谢分享!

殊_途 发表于 2021-7-13 09:19

楼主技术真厉害

hxp.china.sh 发表于 2021-7-13 09:39

完全看不懂,不过阁下的技术确实佩服

海蓝浪花 发表于 2021-7-13 09:41

楼主思路非常清晰,很棒!

huiwuwu 发表于 2021-7-13 09:43

这样的话我就有个大胆的想法。买东西自由了?

licoke2 发表于 2021-7-13 10:34

学习了。支持一波

萤火 发表于 2021-7-13 11:12

学习了,思路清晰,感谢!

wsliangj 发表于 2021-7-13 11:41

高手,学习了

/│\云。 发表于 2021-7-13 11:50

思路很好!不错
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 某商超小程序加密算法解析