前言
需求
模拟小程序打卡,结束打卡,上传轨迹文件的过程,实现全自动打卡
完整流程抓包分析
发现关键接口:
- starRun
- runPunchCard
- uploadFile
- stopRun
分析请求
以startRun为例:
传递的参数
除了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.笔者能力不足,无法再继续分析