备而后动-勿使有变
一、前言
一晃又一个月没写博客了,最近集中攻坚了一下,历经了无数次失败,终于把第十四题整出来了。总的来说,这题确实比较复杂,而且有很多坑,体现在调试、解析等诸多方面,着实考验爬虫工程师的逆向水平。不过这题做完收获还是挺大,尤其是逆向过程中的一些调试环境检测,涨了不少知识。
二、总体逻辑分析
首先进入这题主页,跟之前的题要求基本一致,都是抓取当前页数字:
这里打开fiddler以及浏览器开发者工具直接观察一下,发现总的请求逻辑如下:
在获取数据时,首先会发起一个异步请求,就是这个sp()函数,对应的就是 https://match.yuanrenxue.com/static/match/match14/m.js 这个js文件的请求结果,主体混淆代码
之后在m.js内会再次发起一个新的请求,也就是这个地址: https://match.yuanrenxue.com/api/match/14/m,返回的也是一段混淆代码,里面包含两个最终加密中的关键参数。
最后,在m.js内完成加密参数生成以后,带上cookie访问数据api页,得到返回的数据结果。可以看出,此时cookie内是包含:m、mz和sessionid三个参数的,其中前两个是重点。
至此,已经明确了这道题要破解的关键目标,即找到这几个参数的生成逻辑。
三、关键参数定位
3.1 找sessionid
首先sessionid是比较好确定的,在请求 https://match.yuanrenxue.com/api/match/14/m这段混淆代码时,响应头里会设置sessionid的值,之后访问的时候cookie里会一直带上这个值。
3.2 找mz
mz这个字符串看起来像一个base64编码结果,直接在线解码一下,发现还真是,原始字符串放了些指纹信息
一般cookie赋值都是通过document[cookie]实现的,于是在源代码里搜索关键字document,没想到并没有混淆,一下就找到了。而且在上一行出现了比较可疑的b64_zw这个变量,查看一下,果然这个就是mz的值。而且经过多次测试,发现这个值是固定的,并不会变,可以直接复制出来用。
现在mz的问题解决了,看起来已经完成了一大半工作,就剩最后一个参数m了,眼看着胜利在望。
3.3 找m
m的值可以说是这道题真正加密的地方,其位置也不难找,也是通过搜索document关键字,在代码最底部的一个catch语句里
在这里打上断点,控制台输出一下这行代码,发现m果然就是在这里生成的
接下来主要就是找m的生成逻辑了,从这行与距离可以看到有gee, e, d, c, bb, aa, b64_zw这几个变量,所以不难推测m的生成是跟其那面几行代码的赋值语句有关的。
肉眼看着这段混淆代码,比较难看出什么,所以用解混淆工具把底下这段代码解混淆看看:
a = Date["parse"](new Date()) * 8;
b = Date["parse"](new Date());
c = window["v14"];//源于eval执行返回值
d = z["toString"]();//指纹信息,值固定不变
e = window["v142"];//源于eval执行返回值
p = E(parseInt(a / 8));//d(k,k)
aa = m5(p);
bb = G["tlpBr"](m5, b);
window["n"] += 1;
document["cookie"] = G["fixAu"](G["OoepU"](G["OoepU"](G["MQOJb"](G["JEyfp"](G["JEyfp"](G["LnlnO"]("m=", G["tlpBr"](m5, G["CdSNM"](gee, aa, bb, c, d, e, b64_zw))), "|"), b), "|") + a, "|"), window["n"]), G["ZDKbv"]);
可以发现,这里a、b都是时间戳c和e是两个window全局变量,其值源于前面的这个请求,这个请求最后返回了一大段混淆代码,但有用的就只有对v14和v142的赋值,当然这里也是通过解混淆以后才能比较容易看出来。
变量d其实就是没有经过base64编码的原始指纹信息,p是调用E函数生成,aa和bb都是调用m5函数生成,所以接下来主要就是找到E函数和m5函数的内容。
在不通过ast还原混淆代码的前提下,想要跟着调用栈一步步找代码分析逻辑是比较困难的。这里我本来是想把所有代码全复制下来一次解混淆,但代码量太大,用ob混淆专解工具解失败了,于是后来尝试一段一段解,但又带来了新的问题,就是还原后的代码报了很多变量未定义之类的错误,所以最后只顺利还原了sp函数内的主要代码。
总的来说这段看起来很长的代码实际上关键有用的代码就那么十几二十行,E函数的核心内容内部的这个d函数,从这几行内容可以看出来,这里的加密跟第六题一样,也是rsa加密。
function d(h, b) {
//还原后的真实逻辑
var D = b;
var I = _n("jsencrypt");
var u = new I();
var Q = u["encode"](h, D);
return Q;
}
return result = d(K, K), result;
}
在核心加密逻辑还原出来后,剩下的就是扣代码了,直接把sp函数往上的代码全复制出来,开始调试。需要注意的是,代码里隐藏了很多环境检测的坑,比如delete window、document这种藏在eval函数里的语句,总的来说,主要检测的内容包含了这五个关键字:document,navigator,global,eval,window,该删的删、该补环境的补环境,一个个填完坑以后代码就能顺利跑起来了。
当然,这题还有最后一个坑,让我苦思冥想了好久才发现。。。虽然调试完底阿妈后,可以顺利跑出结果,测试跟源代码对比后跑出的结果也是一样的。但是我编写好python开始请求的时候,发现只有第一页的数据能顺利返回,第二页开始就返回token无效。
按道理我第一页的值都能顺利生成,后面的不该出问题,于是我尝试了检查请求头、请求时间间隔等手段发现都无果后,决定把连续三页请求加密的参数原始变量都复制出来,看看我自己代码生成的结果,跟网站的有无不同。结果发现果然从第二页开始,我的加密参数就不对了。
这时我才注意到E函数,也就是内部调用的rsa加密,在每次函数调用过后,下次加密调用都会受到前一次的影响,也就是说加密函数是会变的。在网站上体现出来就是每次加密请求都不是独立的,在请求第n+1页内容时,必须已经请求了前n页。而我用python调用解析过的js时,相当于每次都是第一次调用rsa加密,所以从第二页开始往后加密的值都不对。
最后我的解决办法办法就是,在请求第n+1页数据时,在js内先模拟调用n次E函数,这样rsa的加密状态就对应到我的当次请求了,最终就能正确生成加密参数m的值。
比如同样的参数,作为第五页请求时,先请求四次,最后去第五次的加密结果:
m=70cecf78ce878a69c516c225c2eab743|1631364061000|13050912488000|5;path=/
m=94f11ac8f60f80f320d261c8ba5e4822|1631364062000|13050912496000|5;path=/
m=ea1c4cab6474a0e0255e3066153c47d8|1631364062000|13050912496000|5;path=/
m=c3739cb931bb501e3f18ac84c1c62cba|1631364062000|13050912496000|5;path=/
m=70158fdb9a432521346138069798fb2e|1631364062000|13050912496000|5;path=/
m=70158fdb9a432521346138069798fb2e|1631364062000|13050912496000|5;path=/
到此,这道题的解析就基本结束。
四、代码实现
还原后的js代码在这里:github
最终模拟请求的python代码如下:
import requests
import re
import subprocess
import time
session = requests.session()
headers = {
'authority': 'match.yuanrenxue.com',
'pragma': 'no-cache',
'cache-control': 'no-cache',
'sec-ch-ua': '"Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"',
'accept': 'application/json, text/javascript, */*; q=0.01',
'x-requested-with': 'XMLHttpRequest',
'sec-ch-ua-mobile': '?0',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36',
# 'user-agent': 'yuanrenxue.project',
'sec-fetch-site': 'same-origin',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
'referer': 'https://match.yuanrenxue.com/match/14',
'accept-language': 'zh-CN,zh;q=0.9',
}
# 在线解混淆生成window["v14"]、window["v142"]两个变量的那段代码,并返回提取出的变量值
def ob_decode(data):
data = {
'm': data
}
response = requests.post('http://tool.yuanrenxue.com/api/ob2', data=data,
verify=False)
decode_result = response.json()['result']
# print(decode_result)
v14 = re.findall('window\["v14"\] = "(.*?)";', decode_result, re.S)[0]
v142 = re.findall('window\["v142"\] = "(.*?)";', decode_result, re.S)[0]
return v14, v142
session.headers = headers
cookies = {
'mz': 'TW96aWxsYSxOZXRzY2FwZSw1LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzkyLjAuNDUxNS4xNTkgU2FmYXJpLzUzNy4zNixbb2JqZWN0IE5ldHdvcmtJbmZvcm1hdGlvbl0sdHJ1ZSwsW29iamVjdCBHZW9sb2NhdGlvbl0sOCx6aC1DTix6aC1DTix6aCwyLFtvYmplY3QgTWVkaWFDYXBhYmlsaXRpZXNdLFtvYmplY3QgTWVkaWFTZXNzaW9uXSxbb2JqZWN0IE1pbWVUeXBlQXJyYXldLHRydWUsW29iamVjdCBQZXJtaXNzaW9uc10sV2luMzIsW29iamVjdCBQbHVnaW5BcnJheV0sR2Vja28sMjAwMzAxMDcsW29iamVjdCBVc2VyQWN0aXZhdGlvbl0sTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzkyLjAuNDUxNS4xNTkgU2FmYXJpLzUzNy4zNixHb29nbGUgSW5jLiwsW29iamVjdCBEZXByZWNhdGVkU3RvcmFnZVF1b3RhXSxbb2JqZWN0IERlcHJlY2F0ZWRTdG9yYWdlUXVvdGFdLDgyNCwwLDAsMTUzNiwyNCw4NjQsW29iamVjdCBTY3JlZW5PcmllbnRhdGlvbl0sMjQsMTUzNixbb2JqZWN0IERPTVN0cmluZ0xpc3RdLGZ1bmN0aW9uIGFzc2lnbigpIHsgW25hdGl2ZSBjb2RlXSB9LCxtYXRjaC55dWFucmVueHVlLmNvbSxtYXRjaC55dWFucmVueHVlLmNvbSxodHRwczovL21hdGNoLnl1YW5yZW54dWUuY29tL21hdGNoLzE0LGh0dHBzOi8vbWF0Y2gueXVhbnJlbnh1ZS5jb20sL21hdGNoLzE0LCxodHRwczosZnVuY3Rpb24gcmVsb2FkKCkgeyBbbmF0aXZlIGNvZGVdIH0sZnVuY3Rpb24gcmVwbGFjZSgpIHsgW25hdGl2ZSBjb2RlXSB9LCxmdW5jdGlvbiB0b1N0cmluZygpIHsgW25hdGl2ZSBjb2RlXSB9LGZ1bmN0aW9uIHZhbHVlT2YoKSB7IFtuYXRpdmUgY29kZV0gfQ==',
}
for page in range(1, 6):
#解析window[v14]和window[v142]的值
response = session.get('https://match.yuanrenxue.com/api/match/14/m',cookies=cookies)
win_value = ob_decode(response.text)
# 执行m.js,得到加密后的m值
p = subprocess.Popen(['node', './14_备而后动,勿使有变.js', win_value[0], win_value[1], str(page)], stdout=subprocess.PIPE)
m = p.stdout.read().split(';')[0].split('=')[-1]
cookies['m']=m
print(m)
# session.cookies.update(cookies)
#带上cookie信息请求数据页api
url = 'https://match.yuanrenxue.com/api/match/14?page=' + str(page)
response = session.get(url,cookies=cookies)
print(response.text)
请求结果如下:
4547b81f1515519afc9176ff79231df7|1631364474000|13050915792000|1
{"status": "1", "state": "success", "data": [{"value": 132}, {"value": 478}, {"value": 1962}, {"value": 4723}, {"value": 2419}, {"value": 3535}, {"value": 580}, {"value": 7955}, {"value": 350}, {"value": 3306}]}
b06a7282da43b78813eef0785042ddd0|1631364476000|13050915808000|2
{"status": "1", "state": "success", "data": [{"value": 6231}, {"value": 3101}, {"value": 1823}, {"value": 5823}, {"value": 4293}, {"value": 9009}, {"value": 8344}, {"value": 5615}, {"value": 3761}, {"value": 3091}]}
70e7088571aec22742541f3352df9b80|1631364479000|13050915832000|3
{"status": "1", "state": "success", "data": [{"value": 7722}, {"value": 4547}, {"value": 4563}, {"value": 9150}, {"value": 66}, {"value": 4062}, {"value": 8758}, {"value": 4588}, {"value": 1317}, {"value": 7142}]}
cf41f3c7f246d3950087df1f6bb52b95|1631364481000|13050915848000|4
{"status": "1", "state": "success", "data": [{"value": 9331}, {"value": 5978}, {"value": 5396}, {"value": 4096}, {"value": 4434}, {"value": 4330}, {"value": 1946}, {"value": 6056}, {"value": 6120}, {"value": 2544}]}
941d8ad2eab2f5b16ba1d6da8850a478|1631364484000|13050915872000|5
{"status": "1", "state": "success", "data": [{"value": 9299}, {"value": 3888}, {"value": 7732}, {"value": 8488}, {"value": 9127}, {"value": 8156}, {"value": 8491}, {"value": 6981}, {"value": 2559}, {"value": 4588}]}
五、结语
高度混淆的代码,直接逆向起来是真的耗时又耗力,所以学习ast真的是非常有必要的,这道题因为自己没法完全靠工具还原,所以最后js文件写了有一万多行,比较臃肿。还有就是浏览器端对运行环境的检测是非常需要关注的,往往可能就因为一个小的变量检测,就导致结果不对,不过说到底,这些还是体现出js与逆向基本功不扎实才导致的,所以基础真的很重要。