浏览器插件后续-某勾网

390 阅读6分钟

接着上一篇pc版boss插件

背景

在做前几个平台(boss、前程、智联)的时候都还挺顺,基本都是首页 ssr + 后续接口请求。主要是找到 ssr 注入的数据,并拦截后续请求即可。但是到了某勾突然发现数据都是加密的。 看下图

image.png

这可老大难了,也没干过加密相关的事情,啥也不会啊。然后去找了些 js 逆向的文章在看(这里找了个逆向的视频

简单来说就是通过控制台 debugger 找到 js 文件里的加解密方法,然后暴露出这些方法挂载到全局,这样爬虫请求数据时就可以调用这些方法进行加解密从而获取数据。

尝试

但是看完之后并没有头绪从哪下手去做,就想着先看看拿到的数据是什么。这里依然是通过重写 Ajax 实现的,具体看这篇文章

很神奇,我明明没有做任何解密操作,却获取到的明文数据。

image.png

好了到此结束,插件的后续实现已经没有难度了。

为什么会这样?

虽然需求解决了,但是为什么会这样?按理来说代理的 Ajax 响应值应该拿到的也是密文。这不合理!

猜测

  1. 可能 Ajax 有什么处理加密的方式,使得传入和响应是明文,在控制台获取是密文。
  2. 既然通过 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 看看哪些文件中用了。

image.png

看到结果就麻爪了,13 个文件这要看到什么时候去。想想还有其他什么办法吧。

下面的介绍是从发送数据开始寻找

根据已有的拦截数据来看,positionAjax.json 是实际的请求列表接口,那么就再搜下这个

image.png

发现只有两个文件中有,并且代码还很相似,那么就都打个断点跑下看看。

(通过红框进入控制台 Sources 面板) image.png

(通过搜索找到对应行代码,点击左侧打上断点) image.png

点击切换页码,进入到断点中 image.png

这里就需要比较多的耐心一步步调试,找到最终进入的 XHR 中

(进入 Axios 中开始发送请求,(e.adapter || s.adapter)(e)) image.png

这里是 Axios 中 adapter 代码(有兴趣可以自己去看),看到有实例化XMLHttpRequest 了

new XMLHttpRequest 也是一种实例化方式,但是不建议使用了,等同 new XMLHttpRequest()

image.png

发现进入到 proxyAjax 中了,但这时候还只是实例化,没有走发送逻辑,后面重点关注进入 send 方法 image.png

Axios 代码最后会调用 send 方法,这时就会先进入 proxyAjax 的 send 方法中,然后调用 send.apply(realXHR, arg) 这里就会进入到某勾的重写方法中

下图中可以看到在 main.js 文件中对 XMLHttpRequest 进行了重写

image.png

随后进入到某勾的 send 方法里,这里注意 setTimeout(t)异步调用,提前在 t 函数里打上断点

image.png

继续调试,会进入 o 函数中,其实就是 onRequest 方法,在这里会设置X-S-HEADERX-K-HEADERX-SS-REQ-HEADER请求头,继续往下走,到 case 50,会发现在这里对 body 进行加密,并重新赋值 image.png

到这整个 send 方法就走完了,可以看到 Axios 先是实例化 XMLHttpRequest,然后代码结尾执行 xhr.send,随后代码进入到 proxyAjax 中的 send 方法,再经过 send.apply 方法进入某勾的重写方法中,最后调用了原生 xhr 的 send 方法。

image.png

接下来看接收到数据是怎么进行处理的

经过前面的 debug 已经知道了,某勾自己内部也会进行一次重写,并且用的也是 XHR,那么直接找 onreadystatechange 方法进行断点。

切换页码,进入断点,发现请求还没有发送出去,readyState = 1,说明这里并不是接受响应的地方 image.png

单步调试,继续执行进入 a 函数。这里 t.readyState = 1,所以判断为 false 走后面的 v(t, p) 方法,在这个方法里去触发了一次 onreadystatechange 事件。但这个不是我们想要的,所以直接在 readyState 等于 4 返回的函数里打上断点,等待断点触发(这里请求很多,可以取消掉前面打的一些断点) image.png

获取到了响应,并且 responseURL 也是 positionAjax.json,但此时数据是加密的,继续走看后续是如何解密的。进入到 n(t, r)

image.png

进入到 n(t, r) 中后发现,实际对应的就是 onResponse 函数,在这个函数中有个 T.ow 函数,以 r.data 为参数,那么可以大胆猜测这个函数是不是对参数进行解密了。那么进入这个函数看看 image.png

进入函数中发现,确实是解密函数,通过 Tt.AES.decrypt 对 body 进行解密,此时已经获取到明文了,但是还没有触发 proxyAjax 的事件,还得继续往下看

image.png

继续走,通过调用 e.next 进入到下一个函数中,发现又有一个新的函数(v)调用,且参数为 readystatechange,进入这个 v 函数看看

image.png

在这个函数中,通过新建一个 Event 来派发 readystatechange

image.png

继续调试,最终来到了 proxyAjax 中,参数是已经修改过得 XHR 对象,并且 response 是已经是明文了

image.png

到这里就全部结束了,整理下流程

image.png