前言

好久没有做逆向案例分析了,最近都在看同行朋友写好的案例,感觉学到很多,算是取长补短了

不多bb,机缘巧合下,拿到个目标网站

 

aHR0c{请删除大括号及其内容,防搜索}HM6Ly93d{请删除大括号及其内容,防搜索}3cudG91dG{请删除大括号及其内容,防搜索}lhby5jb20v

 

分析

首先抓个包,就看到请求参数里带着这几个参数

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

主要就是_signature了,其他的参数都不重要得想必你一看就知道啥意思了。

 

行,开始分析_signature了

 

先,全局搜一下,很好只有一个结果:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 点进去看到主要就这三个地方:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

三个地方都打上断点:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

继续滑动下拉,发现值已经有了:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

鼠标放到变量上面,看到我们需要得_signature其实就是变量a了,而a由函数I(n,e)生成:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

其中,n就是V.getUrl生成,e就是传进去的参数:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

那取消刚才的断点,重新打上断点看看:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

 

此时,可能你在调试的时候,有时候会出现滑块验证:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

这个暂时不管,以后再系统的搞滑块哈

 

 

此时发现断上了:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

跟进去,发现V.getUri进入到如下,感觉是在处理参数,不知道有没有我们要的sign

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 怎么确定有没有,放开断点往下走,如果出现了那就是了,如果没有出现,那多半就不是了,如下:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 这个n的值明显没有sign,那就不是了

当然这个理论在这里行得通,但不绝对哈

 

好,接着看I,跟进去,发现如下,这里的e就是上面生成的n,t就是上面传进来的参数e,就是而且看到里面有sign相关的变量,90%是了

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

单步调试,一点一点跟着走看看:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

找到关键点

 

发现上面都是些没有什么用的配置,就到了倒数第二行,貌似才有关键的东西,而且sign也在这一行里,先在这一行打个断点,然后继续走,一点放行,发现走到一个新的js里:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

大概的看了一眼,应该是个加密算法,那是不是就是我们要的sign呢?不好说啊,直接点这个跳出去吧:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

跳出去之后发现,变量o出现了一个很长的字段

 

 

 

 python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

拿着跟之前浏览器抓包看的_signature对比下长度,发现一致,那就是这个了,而实际的加密逻辑就是刚才进入的新的js文件alcrawler.js里

关键点找到了,那怎么把核心的加密逻辑扣出来呢?

 

我大概看了下,这里好像还不好单独把加密的逻辑抠出来,因为有很长的参数调用,而且我抠了下,不好扣,索性整个拿出来吧,结果发现整个也不多,就几百行,复制到本地吧

 

代码调试

放在本地,取名crawler,用node执行下看看:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

发现报了个这个:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

什么referrer,那我们搞爬虫的,再熟悉不过了,但是这个是在js里面啊,这就是涉及到补环境了,那么referrer属于哪个js对象里的呢,这个没法展开说了,我就直接说,referrer是document对象里的,js的全局对象有,window,doucument,navigator,global,location

行,这里补齐如下,地址给个主站的地址就行了:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

继续执行,报错了:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

但其实,懂js的朋友应该知道,其实window.document可以简写成document,这个就你们自己去研究为什么了,如下写,执行立马不报错了:

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 当然也可以把window也补一下:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

就不会报错了:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

 

那现在我们要把那个生成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一个空字符串

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 那根据上面的断点逻辑,肯定不会是空的,直接省略下,变成如下:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 先把var o 删了:
python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

再通过||符号拆下:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 第一行,null肯定不会全等于(n=window.byted_acrawler)的,所以会走后面的逻辑,但是这一步,同时把window.byted_acrawler复制给了变量n

再看第二行,第二行是个三目运算,首先,0 ==== n肯定是不成立的,直接走后面的null=== (a = n.sgin),这个逻辑就跟第一行类似了,反正最后会把n.sign复制给变量a

再看第三行,这个跟第二行类似,同样的,会走到最后a.call(n,i)

 

不信可以看看这个下面:

先做简单的替换,本质的运算逻辑是没有区别的

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 意思就是,那么长一句,最后会直接执行:window.byted_acrawler.sign.call(n, i)

那么,这两个参数,n,i是啥,先看看n:

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

再看看i,i就是个url的路径,没有带参数的那种

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

ok,i变量好说,这个n变量就有点不好搞了,这样,直接在控制台看下需要啥参数:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 这个报错就很有价值了,它只需要一个url就行了,根本不需要那个什么n变量,那就好说了

 

如下测试,发现不行,需要一个带有url属性的object对象 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 那行,整一个:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

芜湖,出来了,行的,就是这么调用,放到本地掉就完了

 

结果一执行,完蛋,卧槽

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

报错的意思就是,这个对象没有sign属性,很奇怪啊,在控制台都可以用的,在node里不能用,那说明有检测环境的,把那个检测环境的部分改下试试,先把代码缩一下:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

先看那个三目运算,复制到控制台执行看看:

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

实际就还是window,那就改成window:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

卧槽,这里才看到是jsvmp啊,这他妈,大名鼎鼎啊,说实话我有点慌了,本篇博文到此结束?

 

 

 

不不不,还是要挣扎下的,先把它当作普通的函数看待,先看,上面主要的两段代码,第一段是定义,第二段是调用,最后的console打印是我自己加的

 

那行,那看看参数有没有问题:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

定义的时候用的b,e,f

看看下面穿的参数是啥,好家伙,不看不知道,一看传了这么多的东西,用sublime 打开看到:

上面一大段全是b变量,后面的中括号里的值,最后会变成e和f变量,而这里面又有三目运算符

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

把三目运算符整理下,先看第一个:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 那说明,这个三目运算符就是void 0了,把这相关的都替换成void 0,搜索看,只有一个,

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 替换之后再执行,貌似刚才那个sign属性解决了,但是又出现了新的错

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

这个一看还是补环境的问题了,把这个补了,href是location对象里的,补完又发现新的报错

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

到这一步的时候,因为报length的话,大概率是补环境除了问题,那么说明刚才的href没有补对,那我们直接再目标网站的控制台copy一下,

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 回车即可,一定要在目标网站的控制台里copy,copy完执行,至少当前的问题解决了,再搞新的问题

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

这个userAgent就再熟悉不过了,咋办呢?也直接copy吧,因为userAgent属于navigator,直接如下copy:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 再次强调,目标网站控制台里执行

copy完放到代码里执行看看:

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

发现,卧槽,终于tmd没报错,而且有结果了

 

但是,这个长度好像不大对劲,短这么多,好像差点啥,到底差什么东西呢

 

仔细推敲,网上也查了相关的,有说补齐cookie的,我补齐之后执行的结果还是很短,所以,应该还有什么东西没有注意到的

 

在目标网站的控制台里执行,就是可以拿到很长的字段,这就很骚了

 python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

那我觉得应该还是环境的问题,应该有个我们忽视了的地方

 

先打印window看看:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

location基本没有太大区别:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 不一样的主要是window.document和window.navigator,以及window.localStorage,但是恰恰这三个对象是没法直接copy的:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

因为你发现,粘贴出来的要嘛是undefined,要嘛是{}:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

这他妈就很秀了,难道这就是jsvm的威力吗?

 

我另开一个浏览器标签,把刚才抠出来的代码放到控制台执行,然后测试看看:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 首先,至少说明,补的基础环境没问题,就差一些特征值了

 

 

再看,目标控制台里的这个arguments,

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

新开的控制台的这个arguments的值:

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

所以这里就看出区别了,目标控制台里多了个这个:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

但是就不知道是不是这里不同导致的原因了。

 

跟着断点接着走,新开控制台:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

看这个c值

 python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

 

目标的控制台里的:

看这个c值:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

 差距也太大了,而且c就是window.document对象,也就是上面没法copy对象其一

 

而且,新开标签页,走到后面进入到了这里:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

目标控制台,进入到了这里:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

能走不一样的原因就是,这里的B[e]

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 目标控制台的B[e]是空的,所以,G穿的最后一个值是0,而新开控制台的B[e]有值,所以传的最后一个参数是1,也就导致上面走了不同的逻辑

 

 

那么这个B到底是啥:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

我去,这他妈的,最后经过我的调试,发现,主要是穿的这个参数的不同:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 导致取值不同,上面是目标控制台的,下面是新标签页控制台的:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

但是这一个值的变化,不是我们能控制的,写死也是没用的,唉,这就是jsvmp的强大吗?唉,想想后怕了。

 

 

但是中途放弃不是我的作风,我就不信了,我开始在漫无目的的找特征,回到最底部调用部分看这个window对象,突然的看到localStorage部分,我激动了,这个是目标网站里的:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

新标签的控制台,window部分,明显感觉有问题对吧,

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

但是,刚才我们分析的,就是那个this里的window对象不一样,那么我们尽量的去贴靠原网站的window里需要的值,我们给赋值下localstorage,用copy看看呢:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

哎,发现这个倒是可以复制哈

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

把这段封装成一个自执行函数:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

放到新标签的控制台里,然后放到控制台里执行,回车,再访问下验证是否成功了,发现可行的

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 好,现在再在控制台执行下sign:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

 

把localStorage部分放进去之后,再次执行:

 

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

卧槽,说实话,有点小激动,至少这个长度看着很像了,就不知道能不能用了,在代码里看看呢?

 

直接复制刚才的sign生成好的字段,执行测试,卧槽,牛逼啊,数据结构终于有了

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

还没完哈,现在要搞一个在本地能够直接生成的,而不是每次生成需要去浏览器的控制台执行再复制出来的

 

当把那段自治性函数放到node环境里执行的时候:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

很奇怪,提示的是没有setItem这个属性,这咋办呢?

尝试jsdom

理一下思路,目前就差一个localstorage的赋值了,但是上面的代码的setItem无法用,因为localstorage对象是window对象里的,那么我们直接用node伪造一个window吧,咋伪造呢?用jsdom,安装nodejs就不说了,网上一堆教程

 

在本地搞一个node项目,npm init命令初始化后,然后执行命令npm install jsdom安装jsdom,具体过程也省略了,网上教程同样一堆

现在创建一个js文件,把window对象引入,这里注意一下,因为我们要用localstorage,new JSDOM的时候必须要给个主域名,不然没法用localstorage

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

测试下现在setItem成功没有:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

 

ok了,现在把刚才抠出来的代码整合到一起,结果出现了这个,卧槽,心累啊

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

说明这段代码还验证了其他很多东西导致这个sign属性没有正常赋值,换路子吧

 

本地html文件生成

 

把抠出来的代码放到一个html文件里,同时要注意的,url里的时间戳,必须要跟sign生成时传进去的url里的时间戳保持一致,不然用不了

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

ok,用pycharm自带的轻量服务器执行查看:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

点击那个谷歌浏览器图标,自动打开并展示如下页面:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

拿到这两个值去请求测试,哭了,这他妈终于有数据了

 

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

那会过头想想,这个没法运用到实际啊,这个第一,还是用了浏览器自带的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前面,代码如下:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

执行看看,激动万分啊,这个长度,看着就跟目标网站出来的sign长度一致了

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

放到程序里执行测试:

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

 

执行,哇的一声就哭出来了,ok

python爬虫 - js逆向之扣出某平台的_signature加密字段

 

相关代码:

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,确实强,现在搞起来确实需要花很多时间研究了