前言 如果你是没有任何 js 逆向经验的爬虫萌新,且没看过上篇的《初探js逆向》,建议先移步去看,因为本篇所使用的案例相对上篇,难度会更大一些。

在上一篇中,我们学习了一个入门案例的逆向流程。然而,加密的方式有很多,不可能以一概全。在进入本篇正题前,我们先回忆一下上篇的逆向思路:

不妨思考一下,参数虽做了加密,但网页毕竟要正常显示内容,所以在网页渲染的过程中,一定有个地方对这个参数做了解密,然后将数据写入html。

也就是说,我们需要在网页渲染的过程里,一步步观察,看看到底是哪个位置对这个参数做了解密。

… 其实,这个思路有个前提:加密参数必须是请求返回的结果参数。如果网站在请求发起时就对请求参数做了加密,这个思路就不管用了。

另外,在上篇中,因为案例比较简单,在找到解密函数之后工作就完成了 90%,所以抠代码的部分我们一笔带过。而本篇的案例,即便找到了加密位置,可能也只完成了一半工作。

所以本篇将以七麦数据这个网站为例,介绍‘当请求参数被加密时的逆向思路’以及‘抠代码’的正确姿势。

网站分析

访问 https://www.qimai.cn/rank/marketRank/market/3/category/-2/date/2019-05-18 这个地址,可以看到:

jquery 统一加密网络请求参数 js请求参数加密_python

红框中的App榜单列表即为我们的目标数据。来看看它发起了哪些请求:

jquery 统一加密网络请求参数 js请求参数加密_python_02

在 marketRank 这个请求的响应内容里能够找到我们的目标数据,而且是清晰的 json 结构。但不要高兴得太早,我们再看一下它的请求参数:

jquery 统一加密网络请求参数 js请求参数加密_js_03

market(3)、category(-2)、date 分别表示应用商店(应用宝)、类别(全部游戏)和日期,想构造它们都很简单,为了后文描述方便,我们暂且称之为‘简单三参’。

需要重点关注的是这个 analysis,它是一个被加密的必选参数,请求时必须携带,否则无法正常返回数据。这时候你可能会想:“那我直接把它拷贝下来,模拟请求时再带上不就行了?”,然而,只要你稍微分析下就会发现,这个参数并不是固定的,它会随着简单三参的变化而改变。且这类榜单数据通常具有时效性,如果你想进行批量、持久地爬取有效数据,这种‘提前收集 analysis ’的方式是不现实的。

说了这么多,好像有些偏离主题了,我们的目的不是爬取网站,而是学习 js 逆向。

下面进入正题。 逆向思路 我们先用上篇中提到的方式,在xhr请求里打上断点,刷新一下网页。

jquery 统一加密网络请求参数 js请求参数加密_ajax_04

代码执行到了 h.send(f),等等,好像哪里不对。send 不就是把请求发送出去吗?那是不是意味着请求参数在这步之前就已经生成完毕?观察一下上面几行代码,果然如此:

jquery 统一加密网络请求参数 js请求参数加密_js_05

在 t 对象的 url 属性中可以看到,analysis 已经生成好了。那么我们再往下执行也没意义了,因为这个请求已经被发送到了服务端,客户端没必要再对它的参数进行解密。况且我们的目的是研究如何生成 analysis,而不是如何解密。

那怎样才能找到生成 analysis 的位置呢?我们可以先把它的值记录下来,

fGR5SX10dQ0oY3lVeGIkBH1HDQ1wExcWVlYPG1sAQltVRGJRXlMkFAxXBANUAQUHBQQFcBtV

然后把断点打在一个比较早的地方(analysis 生成之前),一步步往下执行,这个值首次出现的位置,就是它生成的位置。

想把断点打在 analysis 生成之前,可以在 Network 选项卡下,该请求的 Initiator 列里看到它的调用栈,调用顺序由上而下:

jquery 统一加密网络请求参数 js请求参数加密_爬虫_06

在前几条里随便选一个,点进去打上断点,这里我看get比较顺眼,就选了这条,并在默认位置打上断点。打上断点后别急着刷新网页,这里有个坑需要先避一下。因为除了marketRank之外,marketList这个请求也有一个 analysis 参数,它请求的是应用商店列表(百度、应用宝、360…)。为了避免干扰,我们不要刷新网页,而是切换类别:

jquery 统一加密网络请求参数 js请求参数加密_python_07

通过这种方式来触发调试界面,就不会再去请求应用商店列表了。切换类别后即可触发弹出调试界面:

jquery 统一加密网络请求参数 js请求参数加密_ajax_08

可以发现,简单三参都已经出现了,但还没发现 analysis,它应该还没生成,让我们继续往下执行。注意,执行过程中时刻关注是否出现类似(切换了类别,analysis肯定会变化,所以是类似)我们之前记录下来的那个值。此处省略一万步调试……,只要你耐心足够,就能找到下图中的代码:

jquery 统一加密网络请求参数 js请求参数加密_jquery 统一加密网络请求参数_09

可以看到,r 的值与我们之前记录的值很类似,为了进一步确定,可以在调试执行完成后看下请求中的 analysis 值是否与这个 r 的值一致。

答案是一致的。也就是说,接下来只要把 r 的生成代码全部抠下来,我们就能生成 analysis 了。感觉也没那么难对吧?其实这个网站,抠代码才是重头戏。

抠代码 开头中提到,本案例即便找到了加密位置,也只完成了一半工作。因为加密函数中做了大量的代码混淆和迷惑眼球的函数调用,想把它完整抠下来也不是件容易的事。如果你没有强大的心脏和足够的耐心,请止步于此;但如果你就喜欢折腾,请接着往下看。

鉴于本文的目的主要在于介绍‘请求参数被加密时的逆向思路’,所以不会对‘抠代码’的部分做详细讲解,也不会提供完整代码,但会稍微做一些提示,希望能对你有所帮助。以下提示内容只有真正去尝试抠代码的人才能看懂了。

  1. b 写死。
  2. 抠出 e 的生成函数,生成 e 。 时间戳与difftime(写死好像不影响)
  3. 抠出 m 的生成函数,通过简单三参+URL 后缀+e 生成 m 。
  • 简单三参 sort 与 join
  • 忽略迷惑代码 ano[Ao]…
  • 进入 e 内部打断点
  • 只关注被调用并执行的代码
  • new一个Unit8Array
  • 找到写入Unit8Array的代码
  • 转base64
  • 封装f[La],便于步骤4调用
  1. 抠出 r 的生成函数,通过 m 和 b 生成 r。
  • 搞定步骤3之后心态千万不要崩,因为胜利就在眼前
  • 几个写死的变量
  • String[“fromCharCode”]

贴张逆向成功的截图:

jquery 统一加密网络请求参数 js请求参数加密_js_10

总结

当请求的参数被加密时,将断点打在加密参数生成之前,在它的值首次出现的位置找到它的加密函数。

抠代码时,如果代码做了大量混淆。需要辨别哪些代码是迷惑你的,哪些代码是真正起作用的,哪些代码是不需要抠的,哪些代码是可以自己用其他方式替代的。



分割线-下面补充的是我实践过程的一些记录


jquery 统一加密网络请求参数 js请求参数加密_爬虫_11

var ajaxObj = {
    param: {
        "market": 6,
        "category": 6,
        "date": "2021-08-23"
    },
    url: '/rank/marketRank',
    baseUrl: 'https://api.qimai.cn',
}


var analysis = [];
Object.keys(ajaxObj.param).forEach((function(key){
    if (key == 'analysis') {
        return false;
    }
    ajaxObj.param.hasOwnProperty(key) && analysis.push(ajaxObj.param[key]);
}));
// 取出基础参数
analysis = analysis.sort().join(''); // 排序转义string
analysis += "@#" + ajaxObj.url.replace(ajaxObj.baseUrl, ''); // 移除根域名
analysis += "@#" + (new Date - (1482 || 0) - 1515125653845);
analysis += "@#" + 1;

// n.cv
var encodeBase = (e)=>{
    return function(e) {
        try {
            return btoa(e)
        } catch (t) {
            return Buffer.from(e).toString("base64")
        }
    }(encodeURIComponent(e).replace(/%([0-9A-F]{2})/g, (function(e, t) {
        return i("0x" + t)
    }
    )))
}
// n.oZ
var encodeAnalysis = (e, t) => {
    function i(e) {
        var t, a = (t = "",
        ["66", "72", "6f", "6d", "43", "68", "61", "72", "43", "6f", "64", "65"].forEach((function(e) {
            t += unescape("%u00" + e)
        }
        )),
        t);
        return String[a](e)
    }
    t || (t = s());
    for (var a = (e = e.split("")).length, n = t.length, o = "charCodeAt", r = 0; r < a; r++)
        e[r] = i(e[r][o](0) ^ t[(r + 10) % n][o](0));
    return e.join("")
}
// encodeBase("2021-08-2366"); // => "MjAyMS0wOC0yMzY2"