小程序逆向:协议分析+sign算法复现

164 阅读3分钟

前言

需求

模拟小程序打卡,结束打卡,上传轨迹文件的过程,实现全自动打卡

完整流程抓包分析

image.png

发现关键接口:

  1. starRun
  2. runPunchCard
  3. uploadFile
  4. stopRun

分析请求

以startRun为例:

传递的参数

image.png

除了sign外都可以获取到固定值,重点是还原sign计算算法.

逆向源码分析

使用工具解包小程序,在源码中搜索startRun:

定位到关键代码.

apiStartRun: function(i, r) {
    var d = this,
      s = {};
    s.openid = n.globalData.userStatus.openid, s.runType = d.data.runType, s.longitude = i, s.latitude = r;
    var u = t.sortKey(s, n.globalData.datakey);
    s.sign = o.hexMD5(u), s.lineId = d.data.lineId ? d.data.lineId : 0, s.userid = n.globalData.userStatus.userid, s.batchNo = d.data.batchNo ? d.data.batchNo : "", s.areaNo = d.data.areaNo, s.brand = n.globalData.brand, s.model = n.globalData.model, s.runEvidence = d.data.runEvidence, s.runSimilarity = d.data.runSimilarity, wx.showLoading({
      title: "加载中...",
      mask: !0
    }), wx.request({
      url: n.globalData.apiurl + "/f/api/startRun",
      method: "POST",
      data: s,
      header: {
        "content-type": "application/x-www-form-urlencoded"
      },

追踪sortKey和hexMD5函数:

sortKey: function(t, r) {
    var e = function(t) {
        var r = [];
        for (var e in t) r.push(e);
        r = r.sort();
        var n = {};
        for (var a in r) {
          var o = r[a];
          n[o] = t[o]
        }
        return n
      }(t),
      n = [];
    for (var a in e) "" != e[a] && "0" != e[a] && n.push(a + "=" + e[a]);
    return n.join("&") + "&key=" + r
  },

实现了根据t的键排序,过滤空字符串("")和字符0("0"),根据指定格式拼接t的值,最后在末尾拼接&key=datakey

hexMD5函数则是计算字符串md5值,输出16进制字符串.

使用python复现

请求头

需要注意的是,小程序使用了特殊的请求头

headers = {
      'User-Agent': user_agent,
      'content-type': 'application/x-www-form-urlencoded',
      'Connection': 'keep-alive',
      'charset': 'utf-8',
      'Accept-Encoding': 'gzip,compress,br,deflate',
      'Referer': 'https://servicewechat.com/wxfxxxxxxx/xx/page-frame.html'
}
生成startRun的sign

直接使用了hashlib库

start_mdfive = 'latitude='+str(start_latitute)+'&longitude='+str(start_longitute)+'&openid='+openid+'&runType=1&key='+key
start_sign = hashlib.md5(start_mdfive.encode(encoding='utf_8')).hexdigest()
解析startRun响应

如果请求成功,服务器会返回各个打卡点的数据

{ "config": { "timeLimit": { "max": "xx.xx", "min": "x.xx" }, "spaceLimit": { "maxTime": "xxxx", "searchDiam": "xxxx" }, "target": { "mileage": "x.xx" } }, "status": { "code": "xxxxxx", "message": "启动成功!", "voice": { "count": "xx", "switch": "xxxx" }, "screen": "xxxx", "upload": { "detailNum": "xxxx", "type": "xxxx" } }, "route": { "id": "xxxxxxxxxxxxxxxxxxxx", "type": "xxxx", "devices": [ { "address": "xx:xx:28:xx:02:xx", "order": "xxxx", "location": { "lat": "xx.xxxxxx", "lng": "xxx.xxxxxx" }, "id": "xx" }, { "address": "xx:xx:28:xx:03:xx", "order": "xxxx", "location": { "lat": "xx.xxxxxx", "lng": "xxx.xxxxxx" }, "id": "xx" }, { "address": "xx:xx:28:xx:03:xx", "order": "xxxx", "location": { "lat": "xx.xxxxxx", "lng": "xxx.xxxxxx" }, "id": "xx" }, { "address": "xx:xx:28:xx:02:xx", "order": "xxxx", "location": { "lat": "xx.xxxxxx", "lng": "xxx.xxxxxx" }, "id": "xx" } ] }, "area": { "status": "xxxx", "points": [ { "lat": "xx.xxxxxx", "lng": "xxx.xxxxxx", "id": "xxxx" }, { "lat": "xx.xxxxxx", "lng": "xxx.xxxxxx", "id": "xxxx" }, { "lat": "xx.xxxxxx", "lng": "xxx.xxxxxx", "id": "xxxx" }, { "lat": "xx.xxxxxx", "lng": "xxx.xxxxxx", "id": "xxxx" } ] } }

解析

detail = res_start_run["detailId"]
line_array = res_start_run["lineArray"]
arr = []
for point in line_array:
      arr.append({
            "deviceId": point["deviceId"],
            "latitude": point["latitude"],
            "longitude": point["longitude"]
      })
      
发送runPunchCard请求,

与上面的分析类似,不再赘述

分析uploadFile请求
处理上传的文件

文件格式: 累计距离,经纬度,1,0,Thu May 15 2025 20:56:05 GMT+0800 (CST)

使用py设置随机轨迹偏离:

try:
    start_long = float(parts[1])+0.00003+random.uniform(0, 0.00002)
    start_lat = float(parts[2]) + random.uniform(-0.00001, 0.00001)
    # 生成指定格式的时间字符串
    current_time = datetime.datetime.now()
    time_str = current_time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)')
    fp.write(str(mark)+','+str(start_long)+','+str(start_lat)+',1,'+str(now_detail)+','+time_str+'\n')
    mark += 1
    now_detail = now_detail + random.uniform(0.002, 0.004)
处理stopRun请求
处理openid

值得注意的是,此处的openid并不是固定的用户身份代码,而是wx提供的登录凭证

stopRun: function() {
    var o = this;
    o.stopLocationUpdate(), wx.login({
      success: function(n) {
        var i = {},
          r = Date.parse(new Date),
          s = wx.getStorageSync("mileage") || 0;
        s = Math.ceil(100 * s) / 100, i.openid = n.code, i.detailId = wx.getStorageSync("detailId"), i.longitude = "", i.latitude = "", i.endTime = r / 1e3, i.
interface LoginSuccessCallbackResult {
        /** 用户登录凭证(有效期五分钟)。开发者需要在开发者服务器后台调用 [code2Session](https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html),使用 code 换取 openid、unionid、session_key 等信息 */
        code: string
        errMsg: string
    }

关于code的获取流程,可参考开放接口 / 登录 / wx.login.笔者能力不足,无法再继续分析