接着上一篇pc版boss插件
背景
在做前几个平台(boss、前程、智联)的时候都还挺顺,基本都是首页 ssr + 后续接口请求。主要是找到 ssr 注入的数据,并拦截后续请求即可。但是到了某勾突然发现数据都是加密的。 看下图
这可老大难了,也没干过加密相关的事情,啥也不会啊。然后去找了些 js 逆向的文章在看(这里找了个逆向的视频)
简单来说就是通过控制台 debugger 找到 js 文件里的加解密方法,然后暴露出这些方法挂载到全局,这样爬虫请求数据时就可以调用这些方法进行加解密从而获取数据。
尝试
但是看完之后并没有头绪从哪下手去做,就想着先看看拿到的数据是什么。这里依然是通过重写 Ajax 实现的,具体看这篇文章 。
很神奇,我明明没有做任何解密操作,却获取到的明文数据。
好了到此结束,插件的后续实现已经没有难度了。
为什么会这样?
虽然需求解决了,但是为什么会这样?按理来说代理的 Ajax 响应值应该拿到的也是密文。这不合理!
猜测
- 可能 Ajax 有什么处理加密的方式,使得传入和响应是明文,在控制台获取是密文。
- 既然通过 proxyAjax 时依然还是明文,那说明在我重写之前请求方法就已经被处理过了。
验证
猜测1
虽然当时觉得猜测1可能性不大,但也许有着没了解过的 api 方法,所以还是去看了。
首先是 MDN 上查了文档,发现确实没有加密相关的,然后又 google 了下并没有相关的用法。然后又找了 Http 报文加密相关,还有几个某勾的请求头字段,也没发现特别的用法,所以排除猜测1。
猜测2
方向1. Axios
最开始并没有往重写 XMLHttpRequest 方向去想,我以自己的习惯去思考,如果我来做这个加解密功能会怎么去做?我的做法就是通过 Axios 的拦截器去做,请求拦截器中加密请求体,响应拦截器中解密。但由于对 Axios 源码并不熟悉所以还得去看下源码中怎么做的。
// Axios 源码简单实现
fucntion dispatch(config) {
// Axios 发送请求方法
return new Promise(resolve => {
let request = new XMLHttpRequest();
request.open(config.method.toUpperCase(), config.url, true);
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
resolve(request.response)
};
})
}
function request(config) {
// 外部调用 Axios 请求方法
let promise;
const chain = [
...requestInterceptorChain,
dispatchRequest.bind(this), undefined,
...responseInterceptorChain,
];
len = chain.length;
promise = Promise.resolve(config)
while (i < len) {
// 每一个拦截器都对应 resolve 和 reject 两个方法
promise = promise.then(chain[i++], chain[i++]);
}
return promise;
}
看完源码后发现我之前的想法有问题,如果是通过 Axios 拦截器来做加密,那么在 Axios 调 XMLHttpRequest 发送请求的时候已经完成参数的加密,那么 proxyAjax 里拿到的也一定是加密的。在接受响应的时候,也是 XHR 先接收到请求,再经过响应拦截器,所以也拿不到解密后的内容。所以不可能是在 Axios 里做的加解密。
方向2: 重写 XMLHttpRequest
这里的重写不是指的 proxyAjax,而是指某勾的代码里面重写 Ajax(后续称 lagouAjax,自己起的名字实际代码中并不是)
这里又有新问题了,怎么去找到这段重写的代码?毕竟所有的 js 文件都是经过混淆可读性很差。但是混淆不是万能的,对于 js 原生的 api 名称,字符串,类的方法都是无法被混淆的,所以直接搜索 XMLHttpRequest 看看哪些文件中用了。
看到结果就麻爪了,13 个文件这要看到什么时候去。想想还有其他什么办法吧。
下面的介绍是从发送数据开始寻找
根据已有的拦截数据来看,positionAjax.json 是实际的请求列表接口,那么就再搜下这个
发现只有两个文件中有,并且代码还很相似,那么就都打个断点跑下看看。
(通过红框进入控制台 Sources 面板)
(通过搜索找到对应行代码,点击左侧打上断点)
点击切换页码,进入到断点中
这里就需要比较多的耐心一步步调试,找到最终进入的 XHR 中
(进入 Axios 中开始发送请求,(e.adapter || s.adapter)(e))
这里是 Axios 中 adapter 代码(有兴趣可以自己去看),看到有实例化XMLHttpRequest 了
new XMLHttpRequest 也是一种实例化方式,但是不建议使用了,等同 new XMLHttpRequest()
发现进入到 proxyAjax 中了,但这时候还只是实例化,没有走发送逻辑,后面重点关注进入 send 方法
Axios 代码最后会调用 send 方法,这时就会先进入 proxyAjax 的 send 方法中,然后调用 send.apply(realXHR, arg) 这里就会进入到某勾的重写方法中
下图中可以看到在 main.js 文件中对 XMLHttpRequest 进行了重写
随后进入到某勾的 send 方法里,这里注意 setTimeout(t)异步调用,提前在 t 函数里打上断点
继续调试,会进入 o 函数中,其实就是 onRequest 方法,在这里会设置X-S-HEADER、X-K-HEADER、X-SS-REQ-HEADER请求头,继续往下走,到 case 50,会发现在这里对 body 进行加密,并重新赋值
到这整个 send 方法就走完了,可以看到 Axios 先是实例化 XMLHttpRequest,然后代码结尾执行 xhr.send,随后代码进入到 proxyAjax 中的 send 方法,再经过 send.apply 方法进入某勾的重写方法中,最后调用了原生 xhr 的 send 方法。
接下来看接收到数据是怎么进行处理的
经过前面的 debug 已经知道了,某勾自己内部也会进行一次重写,并且用的也是 XHR,那么直接找 onreadystatechange 方法进行断点。
切换页码,进入断点,发现请求还没有发送出去,readyState = 1,说明这里并不是接受响应的地方
单步调试,继续执行进入 a 函数。这里 t.readyState = 1,所以判断为 false 走后面的 v(t, p) 方法,在这个方法里去触发了一次 onreadystatechange 事件。但这个不是我们想要的,所以直接在 readyState 等于 4 返回的函数里打上断点,等待断点触发(这里请求很多,可以取消掉前面打的一些断点)
获取到了响应,并且 responseURL 也是 positionAjax.json,但此时数据是加密的,继续走看后续是如何解密的。进入到 n(t, r) 中
进入到 n(t, r) 中后发现,实际对应的就是 onResponse 函数,在这个函数中有个 T.ow 函数,以 r.data 为参数,那么可以大胆猜测这个函数是不是对参数进行解密了。那么进入这个函数看看
进入函数中发现,确实是解密函数,通过 Tt.AES.decrypt 对 body 进行解密,此时已经获取到明文了,但是还没有触发 proxyAjax 的事件,还得继续往下看
继续走,通过调用 e.next 进入到下一个函数中,发现又有一个新的函数(v)调用,且参数为 readystatechange,进入这个 v 函数看看
在这个函数中,通过新建一个 Event 来派发 readystatechange
继续调试,最终来到了 proxyAjax 中,参数是已经修改过得 XHR 对象,并且 response 是已经是明文了
到这里就全部结束了,整理下流程