数据接口分析
数据接口 https://match.yuanrenxue.cn/match/18data
请求参数
{page: 页码, t: 时间戳, v: 加密值}
请求第一页不需要携带 t, v 参数
cookie
只需要携带 sessionid
只要 还原加密字段 v
还原加密参数
查看数据接口对应的 request 栈
下断点,点击页码请求数据,会在对应的位置断住
断住后单步执行到, xml.open 方法是被重写过的,跟进去
代码是压缩成一行的
拿到本地格式化再折叠
可以很明显的看到是一个自执行函数
这个自执行函数传入的参数就是这个 vmp 对应的指令
继续跟栈,可以找到这个 vmp 对应的解释器
将断点放到最后一行
单步进去,这个函数就是对应的解释器
解决 vmp 最有效的办法就是插桩看日志,根据日志的信息 进行合理的猜测 / 还原
插桩调试
插桩应该插在:
运行了方法,同时使用变量接收的位置 xxx = xxx(xxx, xxx) 或 xxx.apply(xxx, [xxx])
又或者是 return 语句后面 进行了计算操作 xxx1 + xxx2 (运算操作)
在这个解释器有很多符合这两个特征的地方
这里只插关键的位置
搜索 27 == __U
这个 return语句后进行了运算操作
这里是三元表达式,如果想知道进行了什么运算操作的花可以将这里的 三元表达式还原成 if
插入条件断点
这里的代码是用 ast 还原后再重新生成的 (ast 真方便 /dog)
对应的代码 ↓
if(33==(v_=vv_)){console.log(27,"V_ instanceof y_;",V_,y_,V_ instanceof y_)V_ instanceof y_;}else{if(23==v_){console.log(27,"V_ in y_;",V_,y_,V_ in y_)V_ in y_;}else{if(19==v_){console.log(27,"V_ + y_;",V_,y_,V_+y_)V_+y_;}else{if(46==v_){console.log(27,"V_ - y_;",V_,y_,V_-y_)V_-y_;}else{if(14==v_){console.log(27,"V_ / y_;",V_,y_,V_/y_)V_/y_;}else{if(31==v_){console.log(27,"V_ * y_;",V_,y_,V_*y_)V_*y_;}else{if(32==v_){console.log(27,"_y_(V_, y_);",V_,y_,_y_(V_,y_))_y_(V_,y_);}else{if(27==v_){console.log(27,"V_ % y_;",V_,y_,V_%y_)V_%y_;}else{if(25==v_){console.log(27,"V_ < y_;",V_,y_,V_<y_)V_<y_;}else{if(2==v_){console.log(27,"V_ <= y_;",V_,y_,V_<=y_)V_<=y_;}else{if(16==v_){console.log(27,"V_ > y_;",V_,y_,V_>y_)V_>y_;}else{if(42==v_){console.log(27,"V_ >= y_;",V_,y_,V_>=y_)V_>=y_;}else{if(40==v_){console.log(27,"V_ & y_;",V_,y_,V_&y_)V_&y_;}else{if(39==v_){console.log(27,"V_ != y_;",V_,y_,V_!=y_)V_!=y_;}else{if(21==v_){console.log(27,"V_ !== y_;",V_,y_,V_!==y_)V_!==y_;}else{if(47==v_){console.log(27,"V_ | y_;",V_,y_,V_|y_)V_|y_;}else{if(50==v_){console.log(27,"V_ ^ y_;",V_,y_,V_^y_)V_^y_;}else{if(26==v_){console.log(27,"V_ == y_;",V_,y_,V_==y_)V_==y_;}else{if(37==v_){console.log(27,"V_ === y_;",V_,y_,V_===y_)V_===y_;}else{if(8==v_){console.log(27,"V_ << y_;",V_,y_,V_<<y_)V_<<y_;}else{if(18==v_){console.log(27,"V_ >> y_;",V_,y_,V_>>y_)V_>>y_;}else{if(1==v_){console.log(27,"V_ >>> y_;",V_,y_,V_>>>y_)V_>>>y_;}else{console.log(27,"void 0;",V_,y_,void 0)void 0;}}}}}}}}}}}}}}}}}}}}}};false
搜索 48 == __U
这个 return 语句后面执行了 apply 方法
插入条件断点
对应的代码 ↓
if(__V(_, ___(u_), 0, 0, _u__).name){
console.log(48, __V(_, ___(u_), 0, 0, _u__).name, 'apply', y__(____));
}else{
console.log(48, 'function(){}', 'apply', y__(____));
};false
搜索 32 == __U
这个 return 语句后面执行了 apply 方法
插入条件断点
对应的代码↓
if(yU_ instanceof Function){
console.log(32, yU_.name, yU_[_v].name, ...__)
}else{
console.log(32, yU_, yU_[_v].name, ...__)
};false
其实还少了两个地方的桩没有插,后面有补充,最好补上
在 目录 日志分析下的 插桩补充目录,点击跳转过去即可
分析日志
插好桩以后,点击页码进行请求 会生成对应的日志(第1, 5页不会进行加密)
生成日志后,将日志保存到本地
在出现密文后的日志全部删除,不需要分析
这里就是生成的密文,和请求字段的密文是一致
替换掉无意义的内容
从头开始分析(这里只分析关键信息)
这一段指令集的目的是生成 鼠标轨迹数组
在页面一直滑动会一直生成这几段日志
910为鼠标在页面上 x 轴的位置
226 为鼠标在页面上 y 轴的位置
滑动鼠标,那么两个坐标的值会与 m 相加 (move)
按下鼠标左键,那么两个坐标的值会与 d 相加 (down)
抬起鼠标左键,那么两个坐标的值会与 u 相加 (up)
在对应数组的后面执行了 shift 方法
那么这个数组的值就为
[‘912m226’, ‘911m226’, ‘910m226’, ‘910d226’, ‘910u226’]
这段指令是判断你是否点击了第一页或第五页(第一页和第五页不执行后续的加密逻辑)
其实…这个和还原加密没有关系 /dog(不皮了,后面只分析和加密相关的日志)
执行了 Date.now()方法获取 时间戳
将这个 时间戳 除以 1000
使用 parseInt 将这个时间戳转化为 整数
生成了 t 参数(请求参数的 t 字段)
使用 toString(16) 将这个数值转化为 十六进制字符串(‘66d9f991’)
将获取的十六进制字符串再相加(‘66d9f991’ + ‘66d9f991’)
使用 slice 方法取索引为 从 0 开始,从 16 结束的字符(‘66d9f99166d9f991’.slice(0,16))
结果为 ‘66d9f99166d9f991’
验证
获取页码与 “|” 相加 (2 + “|”)
获取了轨迹数组并转化为字符串 ([…].toString())
将前面的操作得到的结果再一起相加 (2 + “|” + […].toString())
‘2|912m226,911m226,910m226,910d226,910u226’
这两个操作分别将上面生成的两个参数进行了解码,是接下来用于加密用的
对应 nodejs 的操作
Crypto.enc.Latin1.parse(‘66d9f99166d9f991’)
Crypto.enc.Latin1.parse(‘2|912m226,911m226,910m226,910d226,910u226’)
全局搜索 encrpyt
查看搜索到的第一个 encrypt
加密方式是 AES 加密,加密时需要传入 密文,密钥,iv向量
对应着传入的 3 个参数,可以看打印 32 日志的输出方式
先来查看密文
是 Crypto.enc.Latin1.parse(‘2|912m226,911m226,910m226,910d226,910u226’) 的结果
再来查看密钥
是 Crypto.enc.Latin1.parse(‘66d9f99166d9f991’) 的结果
iv 的值与密钥的值一致
接下来加密验证下结果是否一样
Crypto = require('crypto-js')
let text = Crypto.AES.encrypt(Crypto.enc.Latin1.parse('2|912m226,911m226,910m226,910d226,910u226'), Crypto.enc.Latin1.parse('66d9f99166d9f991'), {
iv: Crypto.enc.Latin1.parse('66d9f99166d9f991'), // iv 向量
// 下面的参数可以填也可以不填,加密模式默认为 cbc, 填充方式默认为 pkcs7
// mode: Crypto.mode.CBC, // 加密模式
// padding: Crypto.pad.Pkcs7, // 填充方式
})
console.log(text.toString())
得到的结果与浏览器的一致
插桩补充
插桩的点其实有问题,比如为什么我知道是 AES 加密,为什么我知道哪个是 key,哪个是 iv
其实少插了两处桩
全局搜索 18 == _U
插入日志点
18, v_, V__
对应的日志
这就是我为什么知道 密文,密钥 和 iv 的原因
还有一处地方插桩后是可以看得到 字符串 AES 的,但是我忘记在哪里了,这也是我为什么是 AES 加密 Latin1解码的原因,感兴趣的可以自己插桩找找
分析验证完之后 就可以编写 python 代码
python 代码
首先获取 2,3,4,5 页对应的鼠标轨迹数组,将最后两个参数中的 m 替换成 d 和 u 即可
第2页:[‘905m206’, ‘905m207’, ‘905m208’, ‘905d209’, ‘905u211’]
第3页:[‘950m203’, ‘950m204’, ‘950m206’, ‘950d208’, ‘950u210’]
第4页:[‘1004m209’, ‘1004m208’, ‘1004m207’, ‘1004d208’, ‘1004u209’]
第5页:[‘1052m197’, ‘1052m199’, ‘1052m200’, ‘1051d201’, ‘1051u203’]
js代码
Crypto = require('crypto-js')
function sdk(page){
let slideArr;
switch (page){
case 2:
slideArr = ['905m206', '905m207', '905m208', '905d209', '905u211']
break;
case 3:
slideArr = ['950m203', '950m204', '950m206', '950d208', '950u210']
break;
case 4:
slideArr = ['1004m209', '1004m208', '1004m207', '1004d208', '1004u209']
break;
case 5:
slideArr = ['1052m197', '1052m199', '1052m200', '1051d201', '1051u203']
break;
}
let encryptStr = Crypto.enc.Latin1.parse(page + "|" + slideArr.toString())
let timeStrap = parseInt(Date.now() / 1000)
let key_iv = Crypto.enc.Latin1.parse(timeStrap.toString(16) + timeStrap.toString(16));
let v = Crypto.AES.encrypt(encryptStr, key_iv, {
iv: key_iv,
mode: Crypto.mode.CBC, // 加密模式
padding: Crypto.pad.Pkcs7, // 填充方式
}).toString()
return {
'page': page,
't': timeStrap,
'v': v
}
}
python 代码
import time
import requests
import execjs
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
}
cookies = {
"sessionid": "你的sessionId",
}
def call_js(file_name, func_name, *args):
with open(file_name, mode='r', encoding='utf-8') as f:
js_code = execjs.compile(f.read())
return js_code.call(func_name, *args)
def get_match18(page_):
url = "https://match.yuanrenxue.cn/match/18data"
params = {}
if page_ > 1:
params = call_js('18.js', 'sdk', page_)
else:
params['page'] = page_
response = requests.get(url, headers=headers, cookies=cookies, params=params)
print(response.json()['data'])
return response.json()['data']
if __name__ == '__main__':
nums = 0
for page in range(1, 6):
nums_list = get_match18(page)
for num in nums_list:
nums += num['value']
print('page: ', page, 'nums: ', nums)