python爬虫 - js逆向之扣出某平台的_signature加密字段
前言
好久没有做逆向案例分析了,最近都在看同行朋友写好的案例,感觉学到很多,算是取长补短了
不多bb,机缘巧合下,拿到个目标网站
aHR0c{请删除大括号及其内容,防搜索}HM6Ly93d{请删除大括号及其内容,防搜索}3cudG91dG{请删除大括号及其内容,防搜索}lhby5jb20v
分析
首先抓个包,就看到请求参数里带着这几个参数
主要就是_signature了,其他的参数都不重要得想必你一看就知道啥意思了。
行,开始分析_signature了
先,全局搜一下,很好只有一个结果:
点进去看到主要就这三个地方:
三个地方都打上断点:
继续滑动下拉,发现值已经有了:
鼠标放到变量上面,看到我们需要得_signature其实就是变量a了,而a由函数I(n,e)生成:
其中,n就是V.getUrl生成,e就是传进去的参数:
那取消刚才的断点,重新打上断点看看:
此时,可能你在调试的时候,有时候会出现滑块验证:
这个暂时不管,以后再系统的搞滑块哈
此时发现断上了:
跟进去,发现V.getUri进入到如下,感觉是在处理参数,不知道有没有我们要的sign
怎么确定有没有,放开断点往下走,如果出现了那就是了,如果没有出现,那多半就不是了,如下:
这个n的值明显没有sign,那就不是了
当然这个理论在这里行得通,但不绝对哈
好,接着看I,跟进去,发现如下,这里的e就是上面生成的n,t就是上面传进来的参数e,就是而且看到里面有sign相关的变量,90%是了
单步调试,一点一点跟着走看看:
找到关键点
发现上面都是些没有什么用的配置,就到了倒数第二行,貌似才有关键的东西,而且sign也在这一行里,先在这一行打个断点,然后继续走,一点放行,发现走到一个新的js里:
大概的看了一眼,应该是个加密算法,那是不是就是我们要的sign呢?不好说啊,直接点这个跳出去吧:
跳出去之后发现,变量o出现了一个很长的字段
拿着跟之前浏览器抓包看的_signature对比下长度,发现一致,那就是这个了,而实际的加密逻辑就是刚才进入的新的js文件alcrawler.js里
关键点找到了,那怎么把核心的加密逻辑扣出来呢?
我大概看了下,这里好像还不好单独把加密的逻辑抠出来,因为有很长的参数调用,而且我抠了下,不好扣,索性整个拿出来吧,结果发现整个也不多,就几百行,复制到本地吧
代码调试
放在本地,取名crawler,用node执行下看看:
发现报了个这个:
什么referrer,那我们搞爬虫的,再熟悉不过了,但是这个是在js里面啊,这就是涉及到补环境了,那么referrer属于哪个js对象里的呢,这个没法展开说了,我就直接说,referrer是document对象里的,js的全局对象有,window,doucument,navigator,global,location
行,这里补齐如下,地址给个主站的地址就行了:
继续执行,报错了:
但其实,懂js的朋友应该知道,其实window.document可以简写成document,这个就你们自己去研究为什么了,如下写,执行立马不报错了:
当然也可以把window也补一下:
就不会报错了:
那现在我们要把那个生成sign的逻辑拿出来用下,怎么用呢?回到上面这个关键的有sign字段的那一步:
var o = (null === (n = window.byted_acrawler) || void 0 === n ? void 0 : null === (a = n.sign) || void 0 === a ? void 0 : a.call(n, i)) || "";
这个懂点js的都不陌生,我们拆开来看:
先看最外层,最外层的括号,如果括号里没有值,那就给o一个空字符串
那根据上面的断点逻辑,肯定不会是空的,直接省略下,变成如下:
先把var o 删了:
再通过||符号拆下:
第一行,null肯定不会全等于(n=window.byted_acrawler)的,所以会走后面的逻辑,但是这一步,同时把window.byted_acrawler复制给了变量n
再看第二行,第二行是个三目运算,首先,0 ==== n肯定是不成立的,直接走后面的null=== (a = n.sgin),这个逻辑就跟第一行类似了,反正最后会把n.sign复制给变量a
再看第三行,这个跟第二行类似,同样的,会走到最后a.call(n,i)
不信可以看看这个下面:
先做简单的替换,本质的运算逻辑是没有区别的
意思就是,那么长一句,最后会直接执行:window.byted_acrawler.sign.call(n, i)
那么,这两个参数,n,i是啥,先看看n:
再看看i,i就是个url的路径,没有带参数的那种
ok,i变量好说,这个n变量就有点不好搞了,这样,直接在控制台看下需要啥参数:
这个报错就很有价值了,它只需要一个url就行了,根本不需要那个什么n变量,那就好说了
如下测试,发现不行,需要一个带有url属性的object对象
那行,整一个:
芜湖,出来了,行的,就是这么调用,放到本地掉就完了
结果一执行,完蛋,卧槽
报错的意思就是,这个对象没有sign属性,很奇怪啊,在控制台都可以用的,在node里不能用,那说明有检测环境的,把那个检测环境的部分改下试试,先把代码缩一下:
先看那个三目运算,复制到控制台执行看看:
实际就还是window,那就改成window:
卧槽,这里才看到是jsvmp啊,这他妈,大名鼎鼎啊,说实话我有点慌了,本篇博文到此结束?
不不不,还是要挣扎下的,先把它当作普通的函数看待,先看,上面主要的两段代码,第一段是定义,第二段是调用,最后的console打印是我自己加的
那行,那看看参数有没有问题:
定义的时候用的b,e,f
看看下面穿的参数是啥,好家伙,不看不知道,一看传了这么多的东西,用sublime 打开看到:
上面一大段全是b变量,后面的中括号里的值,最后会变成e和f变量,而这里面又有三目运算符
把三目运算符整理下,先看第一个:
那说明,这个三目运算符就是void 0了,把这相关的都替换成void 0,搜索看,只有一个,
替换之后再执行,貌似刚才那个sign属性解决了,但是又出现了新的错
这个一看还是补环境的问题了,把这个补了,href是location对象里的,补完又发现新的报错
到这一步的时候,因为报length的话,大概率是补环境除了问题,那么说明刚才的href没有补对,那我们直接再目标网站的控制台copy一下,
回车即可,一定要在目标网站的控制台里copy,copy完执行,至少当前的问题解决了,再搞新的问题
这个userAgent就再熟悉不过了,咋办呢?也直接copy吧,因为userAgent属于navigator,直接如下copy:
再次强调,目标网站控制台里执行
copy完放到代码里执行看看:
发现,卧槽,终于tmd没报错,而且有结果了
但是,这个长度好像不大对劲,短这么多,好像差点啥,到底差什么东西呢
仔细推敲,网上也查了相关的,有说补齐cookie的,我补齐之后执行的结果还是很短,所以,应该还有什么东西没有注意到的
在目标网站的控制台里执行,就是可以拿到很长的字段,这就很骚了
那我觉得应该还是环境的问题,应该有个我们忽视了的地方
先打印window看看:
location基本没有太大区别:
不一样的主要是window.document和window.navigator,以及window.localStorage,但是恰恰这三个对象是没法直接copy的:
因为你发现,粘贴出来的要嘛是undefined,要嘛是{}:
这他妈就很秀了,难道这就是jsvm的威力吗?
我另开一个浏览器标签,把刚才抠出来的代码放到控制台执行,然后测试看看:
首先,至少说明,补的基础环境没问题,就差一些特征值了
再看,目标控制台里的这个arguments,
新开的控制台的这个arguments的值:
所以这里就看出区别了,目标控制台里多了个这个:
但是就不知道是不是这里不同导致的原因了。
跟着断点接着走,新开控制台:
看这个c值
目标的控制台里的:
看这个c值:
差距也太大了,而且c就是window.document对象,也就是上面没法copy对象其一
而且,新开标签页,走到后面进入到了这里:
目标控制台,进入到了这里:
能走不一样的原因就是,这里的B[e]
目标控制台的B[e]是空的,所以,G穿的最后一个值是0,而新开控制台的B[e]有值,所以传的最后一个参数是1,也就导致上面走了不同的逻辑
那么这个B到底是啥:
我去,这他妈的,最后经过我的调试,发现,主要是穿的这个参数的不同:
导致取值不同,上面是目标控制台的,下面是新标签页控制台的:
但是这一个值的变化,不是我们能控制的,写死也是没用的,唉,这就是jsvmp的强大吗?唉,想想后怕了。
但是中途放弃不是我的作风,我就不信了,我开始在漫无目的的找特征,回到最底部调用部分看这个window对象,突然的看到localStorage部分,我激动了,这个是目标网站里的:
新标签的控制台,window部分,明显感觉有问题对吧,
但是,刚才我们分析的,就是那个this里的window对象不一样,那么我们尽量的去贴靠原网站的window里需要的值,我们给赋值下localstorage,用copy看看呢:
哎,发现这个倒是可以复制哈
把这段封装成一个自执行函数:
放到新标签的控制台里,然后放到控制台里执行,回车,再访问下验证是否成功了,发现可行的
好,现在再在控制台执行下sign:
把localStorage部分放进去之后,再次执行:
卧槽,说实话,有点小激动,至少这个长度看着很像了,就不知道能不能用了,在代码里看看呢?
直接复制刚才的sign生成好的字段,执行测试,卧槽,牛逼啊,数据结构终于有了
还没完哈,现在要搞一个在本地能够直接生成的,而不是每次生成需要去浏览器的控制台执行再复制出来的
当把那段自治性函数放到node环境里执行的时候:
很奇怪,提示的是没有setItem这个属性,这咋办呢?
尝试jsdom
理一下思路,目前就差一个localstorage的赋值了,但是上面的代码的setItem无法用,因为localstorage对象是window对象里的,那么我们直接用node伪造一个window吧,咋伪造呢?用jsdom,安装nodejs就不说了,网上一堆教程
在本地搞一个node项目,npm init命令初始化后,然后执行命令npm install jsdom安装jsdom,具体过程也省略了,网上教程同样一堆
现在创建一个js文件,把window对象引入,这里注意一下,因为我们要用localstorage,new JSDOM的时候必须要给个主域名,不然没法用localstorage
测试下现在setItem成功没有:
ok了,现在把刚才抠出来的代码整合到一起,结果出现了这个,卧槽,心累啊
说明这段代码还验证了其他很多东西导致这个sign属性没有正常赋值,换路子吧
本地html文件生成
把抠出来的代码放到一个html文件里,同时要注意的,url里的时间戳,必须要跟sign生成时传进去的url里的时间戳保持一致,不然用不了
ok,用pycharm自带的轻量服务器执行查看:
点击那个谷歌浏览器图标,自动打开并展示如下页面:
拿到这两个值去请求测试,哭了,这他妈终于有数据了
那会过头想想,这个没法运用到实际啊,这个第一,还是用了浏览器自带的window对象,第二,这个服务端是pycharm,还不好控制,就算把这一部换成flask,用requests去请求这个接口,拿结果?也不行啊,为啥,因为请求拿到的是源码,这里的时间戳和sign是js生成的
要走这条路的话,只有用浏览器驱动,puppeter或者selenium了,那这里就会有人说了,这都上浏览器驱动了,那还抠啥代码啊,直接一开始就用浏览器驱动了呗,是的,所以这套路也不是我喜欢的
怎么办
构造localStorage对象
上面试了两个路子都不行,那究其原因就还是那个setItem没法用,没法用的原因是我们构造的window对象里的localStorage不是正确的对象,这里构造一个出来,行不行呢?理论上是可以,试下:
window.localStorage = {
removeItem: function (key) {
delete this[key]
},
getItem: function (key) {
return this[key] ? this[key]: null;
},
setItem: function (key, value) {
this[key] = "" + value; // 将数字转为字符串
},
};
把这段代码放到最开始抠出来的代码里,并在执行setItem前面,代码如下:
执行看看,激动万分啊,这个长度,看着就跟目标网站出来的sign长度一致了
放到程序里执行测试:
执行,哇的一声就哭出来了,ok
相关代码:
js:
window = global;
window.document = {
"referrer": "https://www.xxx.com/"
}
window.location = {
"ancestorOrigins": {},
"href": "https://www.xxxxx.com/?wid=时间戳",
"origin": "https://www.xxxx.com",
"protocol": "https:",
"host": "www.xxxx.com",
"hostname": "www.xxxx.com",
"port": "",
"pathname": "/",
"search": "?wid=时间戳",
"hash": ""
}
window.navigator = {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
};
window.localStorage = {
removeItem: function (key) {
delete this[key]
},
getItem: function (key) {
return this[key] ? this[key]: null;
},
setItem: function (key, value) {
this[key] = "" + value; // 将数字转为字符串
},
};
(function () {
var localStorage = {
"__tea_cache_tokens_2018": "{\"user_unique_id\":\"xxxxxxxx\",\"web_id\":\"7024414886298617379\",\"timestamp\":1635928539118}",
"__tea_cache_tokens_24": "{\"web_id\":\"7024409476728423973\",\"user_unique_id\":\"7024409476728423973\",\"timestamp\":1635907441035,\"_type_\":\"default\"}",
"__tea_cache_first_24": "1",
"tt_scid": "qzapI-RUipcVl.K1CCJHv1H1h5OgJIY8XzMvPoF2aVdVW3ZvjvLViXEDvIDwHXPtfa04",
"__tea_cache_first_2018": "1",
"_byted_param_sw": "tmXeQzPoDDcIho6jKG8=",
"ttcid": "xxxxxxxx"
}
for (var p in localStorage) {
window.localStorage.setItem(p, localStorage[p])
}
})()
// 抠的js部分,自己补充了
py:
import execjs
import requests
import time
def get_sign():
tt = int(time.time())
f = open('crawler_end.js', encoding='utf-8') # crawler_end的内容就是上面的代码
js = f.read()
f.close()
js_obj = execjs.compile(js)
sign = js_obj.call('window.byted_acrawler.sign', {
"url": f"https://www.xxxxxxx.com/api/pc/list/feed?channel_id=0&max_behot_time={tt}&category=pc_profile_recommend"
})
return tt, sign
headers = {
'accept': 'application/json, text/plain, */*',
'accept-encoding': 'gzip, deflate',
'accept-language': 'zh-CN,zh;q=0.9',
'cookie': '你自己的cookie',
'referer': 'https://www.xxxxx.com/',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36',
}
tt, sign = get_sign()
url = f"https://www.xxxxxxx.com/api/pc/list/feed?channel_id=0&max_behot_time={tt}&category=pc_profile_recommend&_signature={sign}"
req = requests.get(url, headers=headers)
print(req)
res = req.content.decode('utf-8')
print(res)
2021-11-05更新
以上方案已经不行了,大概9月份的时候还能用,从大概10月的时候开始,必须要去抠jsvmp了,目前jsvmp的资料不多,为数不多的有这个,大家可以参考下
https://mp.weixin.qq.com/s/mH_9FpJsHLSJj6-APn_54w
但是这个大佬说的没有太详细,不过方法都有了,以后有空再搞吧,jsvmp,确实强,现在搞起来确实需要花很多时间研究了