前言
在平时,我们前端日常工作中,几乎不会接触到去逆向分析 javascript ,因为通常会使用 Webpack , Parcel 等打包工具来完成前端工程化。去看自己的工程即可。但是,如果反过来对自己感兴趣的网站进行学习和抓取,那么就会导致这项技能是必会的,本文带领大家了解其中逆向的一些技巧和分析思路。
准备工作
首先,对大家来说是有一些门槛的。你首先要掌握以下的知识,才能一步一步听懂我说的东西:
chrome调试工具断点熟悉
牢固的 Javascript 基础
需求背景
目前,我们假定有一个这样的需求,某网站采用了以下方式进行加密接口,导致爬虫无法进行抓取数据,因此我们需要逆向分析加密方式来完成抓取。这里为了便于理解,我画了一张图:
原理其实非常简单,如果你是爬虫程序,那么不带这个指定算法完成后的key返回给服务器,那么服务器就会拒绝这次请求。非常轻松的区分了人机。
目标网站分析
首先打开 Chrome DevTools 我们在 Network 面板里面,我们发现了这个有意思的接口,但是情况比我们之前预想的要复杂(真正实战中,你也会碰到各种奇葩的变异套路)我这里只是遇到了这一种,看起来貌似它校验了三个不同的参数。
根据响应报文,我们看到这里的重点是分析 s 和 p 变量以及这个ture的长参数是怎么计算出来的。
听到这里,我想有大家可能会有疑惑:
问:为什么你这么确定这三个 s p ture 一定是前端加密算出来的呢?
答:非常简单,在DevTools里面 对所有的资源进行一次搜索这些对应的key值,看看能不能找到,如果能找到,说明服务器那边传来的,如果找不到,必定是前端根据某种规则生成的。另外,如果是服务器本身传来的,也不需要逆向了,直接抓取对应的js/html拿出来即可。
问:我用上次生成的这三个随机参数去访问不行吗?
答:这需要根据网站本身来看,一般来说是会失效的,每次服务器接收到了上次的随机key。那么就会挂为过期已经访问,如果你再次拿出同样的三个随机数,尽管符合规则。但是服务器本身目的就是防爬虫,肯定要过期掉,否则做这个没有意义,当然,也不排除有些网站它就是在做这种无意义的事情(曾经遇到过)。
回到正题,我们接下来需要对网站进行断点调试,这里需要大家跟着我的思路走一遍,最后我会把整个流程再分析出来。
我们先去看下堆栈信息,看看这个请求是怎么来的:
url之间的关系简单看看即可,这里重点是函数的堆栈关系,这里往往决定了我们调试的第一个断点关键位置(准不准就看这些里面对函数调用的准确分析了,否则绕很大弯子)
题外话:如果你确实无法确定这里的堆栈信息,那么你也可以尝试着全局搜索你需要的关键字 s p ture 这三个key的定义位置,根据代码位置再去做断点。
第一个断点位置其实没必要非常精确,因为目前的网站做的混淆非常厉害,有时候DevTools也无法准确的精准定位。
我这里找到的第一个断点就非常不准,因为我在request那块堆栈信息点进去的,但其实这块并没有非常准确的到那个我想要的位置。当然也可以到了里面,去搜索这个函数的定义位置。
但是没有关系,我们只要遵循一个原则:这个请求发出前断点。只要确定了这点,我们就可以通过一步一步的debug找到离它最近的关键调用位置,当你打完第一个断点以后,一定切记:步进一下,回过去看一下network里面的反应,如果我们的目标请求出现,先在这里打一个断点备用,此时这个请求大概率都是pending状态的,因为还没从服务器返回数据。当pending结束,我们第二个断点位置也就找到了。
通过对堆栈信息的反复查看,我看到了一个可疑的地方,successed函数的定义,一般来说发送请求这个函数往往代表着成功后执行的语句,打完断点,network里的表现跟我预想到的是一样的,此时这个目标接口的状态是 pending 意味着这附近执行后,但是有三个请求都是这个pending状态,虽然就要被返回了,但是还是得观察。此时断点调试的时候,我们步子迈的小一些,一点一点走。
事情往往并不如人愿,这里的断点很明显,离我们目标请求还有些远,再一步锁定,发现这里的时候,就剩它一个了:
并且附近的代码给我们很强的提示,应该就是在这里附近核心区域。鼠标放上去,也验证了我们的猜想,就是这个url,那么核心断点找到了,接下来我们开始分析。
核心算法
当我们抓到了这块之后,我们就可以在编辑器里面开始构建(copy)我们的代码了。我们将这个函数体全部拿出来,然后分析里面的各种缺失的变量,顺藤摸瓜。
这个函数很明显是一个Object.assign方法,这里我们直接进行替换,由此,第一部分逆向代码就被创建出来了,我把这个函数命名为:createKeyCore
function createKeyCore(t, e, i){
var n = this;
if (!this.lock) {
this.lock = !0,
this._setParams(t);
var a = function(){
n.lock = !1,
i && i()
}
, o = (0,
p.calcture)(this.url, this.params);
(0,
d.default)({
url: this.url,
data: (0,
Object.assign)({}, this.params, {
ture: o
}),
success: function(s){
var o = s || {}
, r = o.data || []
, l = r.length
, u = o.next && o.next.max_behot_time;
"success" === o.message && l && (n._qihuAdInsert(r),
r = n._dataPreHandle(r),
"refresh" === t ? (n._refreshItem = {
refresh_mode: !0,
behot_time: u,
time_ago: (0,
p.timeAgo)(u),
_index: r.length
},
n.list = r.concat(n.list)) : n.list = n.list.concat(r),
e && e(n.getList(), l)),
n._handleCaptcha(s, {
successCb: function(){
n.lock = !1,
n._getData(t, e, i)
},
closeCb: function(){
a()
}
}) || a()
},
error: function(){
a()
}
})
}
}
复制代码
同时在这里,我们又找到了我们需要的 s 和 p 变量。
那么问题来了, s 和 p 变量究竟来自于哪里呢?继续顺藤摸瓜:
根据我们已经具备的 js 知识,我们知道,一个函数这样的情况下被调用,this的指向肯定不是全局,也不是它自身,而是调用它的那个函数,那么我们需要去找到它是在哪里被调用的?
根据堆栈信息,我们很轻松的找到了,但是发现。。。。this的this的this的this的this的this的this的this的this的this。。。天,行吧,咱们就继续一步一步往上走,继续回溯,回溯到最外面发现不对,其实我们漏掉了最重要的一步,回来我们现在构建的这个代码里面,我们可以看到了一个叫做 _setParams 方法这个方法我们进去看,发现里面就有我们需要的a变量和c变量
这个时候又发现:
那么简单了 这个函数就直接传入 "refresh" 字符串即可,然后之前那个createKeyCore 其实就没必要有success和error函数,我们可以精简下。
这里发现了一个新的a函数,继续加入进去。
然后根据目前已有的函数,我们再优化下代码。
但是以上代码执行还是会报错,所以其实核心代码并不在这里,真正的算法环境在整个框外。
这些函数互相调用,为了方便,我这里直接将这些函数整个大的闭包拿出来构造。
最后我们成功的构造完成了。但是发现全局还缺少了一个:byted_acrawler 变量
我们全局搜索这个变量 最后在html里面找到了他的init方法,说明这个是一个库,引入库就没有问题了。
至此,整个数据的逆向生成就完成了。
详细代码就不贴了,这里在空白的网页里面演示一下我构造的执行结果:
再来一个
这样的话,我就可以通过这套逻辑,不断的生成无数种符合服务器要求的key,从而逆向接口成功。
总结与技巧
其实很多人看到上面应该都会晕,但我们的核心是讲方法,因为我没有把目标网址挂出来(这样的做法不太好)。本文也仅供交流技术而已。不过没有关系,大家需要理解的是整个的调用逻辑和套路,其实就是通过堆栈信息不断的去debugger,然后根据已有的js经验去构造自己的函数。
当然这里也推荐大家使用chrome自带的override或者fidder,查理等工具去映射本地调试。
如有更加便捷的办法,也欢迎下方留言。