JavaScript逆向爬取实战

前面我们学习了各种 JavaScript 逆向技巧,本节中我们综合应用之前学习到的知识点进行一次完整的 JavaScript 逆向分析和爬取实战。

案例介绍

本节的案例网站不仅在 API 参数有加密,而且前端 JavaScript 也带有压缩和混淆,其前端压缩打包工具使用 webpack,混工具使用 javascript-obfuscator。分析该网站需要熟练掌握浏览器的开发者工具和一定的调试技巧,另外还需要用到一些 Hook 技术等辅助分析手段。

案例的地址为 https://spa6.scrape.center/ ,页面首页如图 11-133 所示。初看之下,和之前的网站并没有什么不同之处,但仔细观察可以发现其 Ajax 请求接口和每部电影的 URL 都包含了加密参数。

图11-133 页面首页

比如,我们点击任意一部电影,观察下 URL 的变化,如图11-134所示。

图11-134 点击任意一部电影后 URL 的变化

可以看到详情页的 URL 包含了一个长字符串,看上去像是 Base64 编码的内容。

接下来,看看 Ajax 的请求。我们从列表页的第 1 页到第 10 页依次点一下,观察 Ajax 请求是怎样的,如图 11-135 所示。

可以看到,Ajax 接口的 URL 里多了一个 token,而且在不同的页码,token 是不一样的,它们同样看似是 Base64 编码的字符串。

图11-135 Ajax 请求列表

另外,更困难的是,这个接口还有时效性。如果我们把 Ajax 接口的 URL 直接复制下来,短期内是可以访问的,但是过段时间之后就无法访问了,会直接返回401状态码。

我们再看一下列表页的返回结果,比如打开第一个请求,看看第一部电影数据的返回结果,如图 11-136所示。

图11-136 第一部电影数据的返回结果

这里看似是把第一部电影的返回结果全展开了,但是刚才我们观察到第一步电影的 URL 是 https://spa6.scrape.center/detaiV/ZWYzNCNOZXVxMGJOdWEjKCO1N3cxcTVvNSOtakA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx ,看起来是 Base64 编码,我们对它进行解码,结果为 ef34#teuqobtua#(-57w1q5o5-j@98xygimlyfxs*-!i-0-mb1,看起来似乎还是毫无规律,这个解码后的结果又是怎么来的呢?返回结果里也并不包含这个字符串,这又是怎么构造的呢?

还有,这仅仅是某个详情页页面的 URL,其真实数据是通过 Ajax 加载的,那么 Ajax 请求又是怎样的呢?我们再观察下,如图 11-137 所示。

图11-137 Ajax请求列表

这里我们发现其 Ajax 接口除了包含刚才所说的URL中携带的字符串,又多了一个 token,同样也是类似 Base64 编码的内容。总结下来,这个网站就有如下特点:

  • 列表页的 Ajax 接口参数带有加密的 token

  • 详情页的 URL 带有加密 id

  • 详情页的 Ajax 接口参数带有加密 id 和加密 token

如果我们要想通过接口的形式进行爬取,必须把这些加密 idtoken 构造出来才行,而且必须一步步来。首先我们要构造出列表页 Ajax 接口的 token 参数,然后获取每部电影的数据信息,接着根据数据信息构造出加密 id 和加密 token

到现在为止,我们知道了这个网站接口的加密情况,下一步就是去找这个加密实现逻辑。

由于是网负,所以其加密逻辑一定藏在前端代码里,但上一节我们也说了,前端为了广保护其接口加密逻辑不被轻易分析出来,会采取压缩、混淆等方式来加大分析的难度。下面我们就来看看这个网站的源代码和 JavaScript 文件是怎样的。

首先看网站的源代码,我们在网站上点击鼠标右键,此时会弹出快捷菜单,然后点击 “查看源代码” 选项,可以看到结果如图 11-138 所示。

图11-138 查看源代码

内容如下:

这是一个典型的 SPA(单页 Web 应用)页面,其 JavaScript 文件名带有编码字符、chunkvendors 等关键字,这就是经过 webpack 打包压缩后的源代码,目前主流的前端开发框架 Vue.js、React.js 等的输出结果都是类似这样的。

接下来,我们再看一下其 JavaScript 代码是什么样子的。在开发者工具中打开 Sources 选项卡下的 Page 选项卡,然后打开 is 文件夹,在这里我们能看到 JavaScript 的源代码,如图 11-139 所示。

图11-139 JavaScript 的源代码

我们随便复制一些出来,看看是什么样子的,结果如下:

嗯,就是这种感觉,可以看到一些变量是十六进制字符串,而且代码全被压缩了。

没错,我们就是要从这里找出 tokenid 的构造逻辑。

要完全分析出整个网站的加密逻辑还是有一定难度的,不过不用担心,本节中我们会一步步地讲解逆向的思路、方法和技巧。如果你能跟着这个过程走完,相信还是会对整个 JavaScript 逆向分析过程更加熟练。

寻找列表页 Ajax 入口

我们就开始第一步,寻找入口吧!这里简单介绍两种寻找入口的方式:

  • 全局搜索标志字符串

  • 设置 Ajax 断点

全局搜索标志字符串

一些关键的字符串通常会被作为寻找 JavaScript 混淆入口的依据,我们可以通过全局搜索的方式来查找,然后根据搜索到的结果大体观察是否为我们想找的入口。

重新打开列表页的 Ajax 接口,看一下请求的 Ajax 接口,如图 11-140 所示。

图11-140 请求的 Ajax 接口

这里 Ajax 接口的 URL 为 https://spa6.scrape.center/api/movie/?limit=10&offset=0&token=M2ZjNzBi YTg4Mjk5OGFmMjVkOGU3NmNjODFIN2NjMjZjNDgxMTAxNSwxNjIzNDk1MDAy ,可以看到带有 limitoffsettoken 三个参数,关键就是找 token,我们就全局搜索是否存在 token 吧!点击开发者工具右上角的 “三个小竖点” 选项卡,然后点击 Search,如图 11-141 所示。

图11-141 搜索功能

这样我们就能进入全局搜索模式,搜索 token,可以看到的确搜索到了几个结果,如图 11-142 所示。

图11-142 搜索 token 的结果

观察一下,下面的两个结果可能是我们想要的,点击第一个进入看看,此时定位到一个JavaScript 文件,如图 11-143 所示。

图11-143 点击第个结果,定位到一个 JavaScript 文件

这时可以看到整个代码都是经过压缩的,只有一行,不好看,点击左下角的按钮,格式化 JavaScript 代码,格式化后的结果如图11-144所示。

图11-144 格式化后的代码

可以看到,这里弹出来一个新的选项卡,其名称是 “JavaScript文件名+:formatted”,代表格式化后的代码结果。这里我们再次定位到 token 观察一下。

可以看到,这里有 limitoffsettoken。然后观察其他的逻辑,基本上能够确定这就是构造 Ajax 请求的地方,如果不是的话,可以继续搜索其他文件观察下。

现在,我们就成功找到了混淆的入口,这是一个寻找入口的首选方法。

设置 Ajax 断点

由于这里的字符串 token 并没有被混淆,所以上面的方法是奏效的。之前我们也讲过,由于这种字符串非常容易成为寻找入口的依据,所以这样的字符串也会被混淆成类似 Unicode、Base64、RC4 等的编码形式,这样我们就没法轻松搜索到了。

另外,前面我们也介绍过 XHR 断点,利用该方法我们可以方便地找到发起 Ajax 请求的一些入口位置。

我们可以在 Sources 选项卡右侧 XHR/fetch Breakpoints 处添加一个断点。首先点击 + 号,此时就会让我们输入匹配的 URL 内容。由于 Ajax 接口的形式是 /api/movie/?limit=10…​ 这样的格式,所以截取一段填进去就好了,这单填的就是 /api/movie,如图 11-145 所示。

图11-145 添加断点

添加完毕后,重新刷新页面,进入了断点模式,如图11-146所示。

图11-146 断点模式

接下来,我们重新点击格式化按钮 0,格式化代码,看看断点在哪里,如图 11-147 所示。这里有一个字符 send,我们可以初步猜测它相当于发送 Ajax 请求的一瞬间。

图11-147 格式化代码

前面我们说过怎样回溯查找相关逻辑的方法。点击右侧的 CallStack,这里记录了JavaScript 方法逐层调用的过程,如图 11-148 所示。

图11-148 CallStack

当前指向的是一个名为 anonymous(也就是名)的调用,在它的下方显示了调用 anonymous 的方法,名字叫作 0x516365,然后在下一层就显示了调用 0x2099d8 方法的方法,以此类推。我们可以继续找下去,注意观察类似 token 这样的信息,就能找到对应的位置了。最后,我找到了 onFetchData,这个方法实现了 token 的构造逻辑,这样就成功找到 token 的参数构造位置了,如图 11-149 所示。

图11-149 token 的参数构造位置

到现在为止,我们已经通过两个方法找到入口了。其实还有其他寻找入口的方式,比如 Hook 关键函数,稍后我们会讲到。

寻找列表页加密逻辑

我们已经找到 token 的位置了,可以观察这个 token 对应的变量,它叫作 0x2b4ffd,所以关键就是要看看这个变量是哪里来的。

怎么找呢?添加断点就好了。

看一下这个变量是在哪里生成的,然后我们在对应的行添加断点。我们先取消刚才打的XHR断点,如图 11-150 所示。

图11-150 取消 XHR 断点

这时我们就设置了一个新断点。由于只有一个断点,刷新网页后,我们会发现网页停在新的断点上,如图 11-151 所示。

图11-151 网页停在新断点上

这时我们就可以观察正在运行的一些变量了,比如把鼠标放在各个变量上,可以看到变量的值和类型:把鼠标放在变量 _0x51c425 上,会有一个浮窗显示,如图 11-152 所示。

图 11-152 将鼠标放在变量上, 会有浮窗显示

另外, 还可以在右侧的 Watch 面板中添加想要查看的变量, 如这行代码的内容为:

,_0x2b4ffd = Object(\_0x51c425['a'])(this['$store']['state']['url']['url']['index']);

我们比较感兴趣的可能就是 _0x51c425, 还有 this 里的 $store 属性。展开 Watch 面板, 然后点击 + 号, 把想看的变量添加到 Watch 面板里面, 如图 11-153 所示。

可以发现, _0x51c425 是一个对象, 它具有属性 a, 其值是一个方法。this['$store']['state']['url']['url']['index'] 的值其实就是 /api/movie, 即 Ajax 请求 URL 的 Path。_0x2b4ffd 就是调用前者的方法传入 /api/movie 得到的。

图11-153 把想看的变量添加到 Watch 面板

下一步就是去寻找这个方法。我们可以把 Watch 面板的 _0x51c425 展开,这里会显示的 FunctionLocation 就是这个函数的代码位置,如图 11-154 所示。

图11-154 函数的代码位置

点击进入,发现它仍然是未格式化的代码,于是再次点击价按钮格式化代码。

这时我们就进入一个新的名字为 _0xc9e475 的方法里,在这个方法中,应该就有 token 的生成逻辑了。添加断点,然后点击面板右上角蓝色箭头状的 Resume script execution 按钮,如图 11-155 所示。

图11-155 Resume script execution 按钮

这时会发现我们单步执行到如图 11-155 所示的这个位置了。接下来,我们不断进行单步调试,观察一下这里面的执行逻辑和每一步调试的结果都有什么变化,如图 11-156 所示。

图 11-156 单步调试, 观察结果的变化

在每步的执行过程中, 我们可以发现一些运行值会被打到的代码的右侧并高亮表示, 同时在 Watch 面板下还能看到每步的具体结果。

最后, 我们总结出这个 token 的构造逻辑, 如下。

  • 传入的 /api/movie 会构造一个初始化列表, 将变量命名为 _0x5b4f53

  • 获取当前的时间戳, 命名为 _0x4814ff, 调用 push 方法将其添加到 _0x5b4f53 变量代表的列表中。

  • _0x5b4f53 变量用逗号拼接, 然后进行 SHA1 编码, 命名为 _0x32d914

  • _0x32d914 (SHA1 编码的结果) 和 _0x4814ff (时间戳) 用逗号拼接, 命名为 _0x829249

  • _0x829249 进行 Base64 编码, 命名为 _0x3ea520, 得到最后的 token

经过反复观察, 以上逻辑可以比较轻松地总结出来, 其中有些变量可以实时查看, 同时也可以 自己输入到控制台上进行反复验证。

现在加密逻辑我们就分析出来了, 基本思路就是:

  • /api/movie 放到一个列表里;

  • 在列表中加入当前时间戳;

  • 将列表内容用逗号拼接;

  • 将拼接的结果进行 SHA1 编码;

  • 将编码的结果和时间戳再次拼接;

  • 将拼接后的结果进行 Base64 编码。

验证一下, 如果逻辑没问题, 就可以用 Python 来实现啦。

使用 Python 实现列表页的爬取

要用 Python 实现这个逻辑, 我们需要借助两个库: 一个是 hashlib, 它提供了 sha1 方法; 另外一个是 base64 库, 它提供了 b64encode 方法对结果进行 Base64 编码。实现代码如下:

import hashlib
import time
import base64
from typing import list, Any
import requests

INDEX_URL = 'https://spa6.scrape.center/api/movie?limit={limit}&offset={offset}&token={token}'
LIMIT = 10
OFFSET = 0

def get_token(args: list[Any]):
    timestamp = str(int(time.time()))
    args.append(timestamp)
    sign = hashlib.sha1(','.join(args).encode('utf-8')).hexdigest()
    return base64.b64encode(','.join([sign, timestamp]).encode('utf-8')).decode('utf-8')

args = ['/api/movie']
token = get_token(args=args)
index_url = INDEX_URL.format(limit=LIMIT, offset=OFFSET, token=token)
response = requests.get(index_url)
print('response', response.json())

我们根据上面的逻辑把加密流程实现出来了, 这里我们先模拟爬取了第一页的内容。最后运行一 下, 就可以得到最终的输出结果了。

寻找详情页加密id入口

观察上一步的输出结果,把结果格式化,这里看看部分结果:

{
    "count": 100,
    "results": [
        {
            "id": 1,
            "name": "霸王别姬",
            "alias": "Farewell My Concubine",
            "cover": "https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c",
            "categories": [
                "剧情",
                "爱情"
            ],
            "published_at": "1993-07-26",
            "minute": 171,
            "score": 9.5,
            "regions": [
                "中国大陆",
                "中国香港"
            ]
        },
      ...
    ]
}

这里我们看到有个 id 是 1, 另外还有一些其他字段, 如电影名称、封面、类别等, 这里面一定有 某个信息是用来唯一区分某个电影的。

但是, 当我们点击第一部电影的信息时, 可以看到它跳转到了 URL 为 https://dynamic6.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxeTVvNS0takA5OHh5Z2ltbHlmeHMQLSFPbTAtbWIx 的页面, 可以看到这里的 URL 里面有一个加密 id 为 ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxeTVvNS0takA 5OHh5Z2ltbHlmeHMQLSFPbTAtbWIx, 它和电影的这些信息有什么关系呢?

如果你仔细观察, 其实可以比较容易地找出规律来, 但是这总是观察出来的, 如果遇到一些观 察不出规律的, 那就麻烦了。因此, 还需要靠技巧去找到它真正地加密位置。这时候该怎么办呢?

分析一下, 这个加密 id 到底是怎么生成的。

点击详情页的时候, 我们就可以看到它访问的 URL 里面就带上了 ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxeTVvNS0takA5OHh5Z2ltbHlmeHMQLSFPbTAtbWIx 这个加密 id 了。而且不同详情页的加密 id 是不同的, 这说明这个加密 id 的构造依赖于列表页 Ajax 的返回结果。因此, 可以确定这个加密 id 的生成发生在 Ajax 请求完成后或者点击详情页的一瞬间。

为了进一步确定这发生在何时,我们查看页面源码,可以看到在没有点击之前,详情页链接的 href 里面就已经带有加密 id 了,如图 11-157 所示。

图11-157 详情页链接的 href

由此可以确定,这个加密 id 是在 Ajax 请求完成之后生成的,而且背定也是由 JavaScript 生成的。

怎么去找 Ajax 完成之后的事件呢?是否应该去找 Ajax 完成之后的事件呢?

可以试试。在 Sources 面板的右侧,有一个 EventListenerBreakpoints,这里有一个 XHR 的监听包括发起时、成功后、发生错误时的一些监听,这里我们勾选上 readystatechange 事件,代表 Ajax 得到响应时的事件,其他断点可以都删除,然后刷新页面看一下,如图 11-158 所示。

图11-158 刷新后效果

可以看到, 此时就停在 Ajax 得到响应的位置了。我们怎么知道这个 id 是如何加密的呢? 可以 选择通过断点一步步调试下去, 但这个过程非常烦琐, 因为这里可能会逐渐用到页面 UI 渲染的一些底层实现, 甚至可能找着找着都不知道找到哪里去了。

怎么办呢? 这里我们又可以用上文介绍的用于快速定位的方法, 那就是 Hook, 这里就不再展 开讲解了, 具体可参见 11.3 节。

那么, 这里怎么用 Hook 的方式来找到加密 id 的加密入口呢?

想一下, 这个加密 id 是一个 Base64 编码的字符串, 那么生成过程中就必然调用了 JavaScript 的 Base64 编码的方法, 这个方法名叫 btoa。当然, Base64 也有其他的实现方式, 比如利用 crypto-js 库实现。但可能底层调用的就不是 btoa 方法了。

现在, 我们其实并不确定是不是通过调用 btoa 方法实现的 Base64 编码, 那就先试试吧。

要实现 Hook, 关键在于将原来的方法改写, 这里我们其实就是 Hook btoa 这个方法了, btoa 这个方法属于 window 对象, 这里直接改写 window 对象 btoa 方法即可。改写的逻辑如下:

(function () {
    'use strict'
    function hook(object, attr) {
        var func = object[attr]
        object[attr] = function () {
            console.log('hooked', object, attr, arguments)
            var ret = func.apply(object, arguments)
            debugger
            console.log('result', ret)
            return ret
        }
    }
    hook(window, 'btoa')
})()

这里我们定义了一个 hook 方法, 给其传入 objectattr 参数, 意思就是 Hook 对象 objectattr 参数。例如, 如果我们想 Hook alert 方法, 那就把 object 设置为 window, 把 attr 设置为字符串 alert。 这里我们想要 Hook Base64 的编码方法, 所以只需要 Hook window 对象 btoa 方法就好了。

hook 方法的第一句 var func = object[attr],相当于把它赋值为一个变量, 我们调用 func 方法 就可以实现和原来相同的功能。然后我们改写这个方法的定义, 将其改成一个新的方法。在新的方法 中, 通过 func.apply 方法又重新调用了原来的方法。这样我们可以保证前后方法的执行效果不影响 的前提下, 在 func 方法执行的前后加入自己的代码, 如使用 console.log 将信息输出到控制台, 通过 debugger 进入断点等。在这个过程中, 我们先临时保存 func 方法, 然后定义一个新方法来接管程序 控制权, 在其中自定义我们想要的实现, 同时在新方法里重新调用 func 方法, 保证前后的结果不受影响。因此, 我们达到了在不影响原有方法效果的前提下, 可以实现在方法的实现前后自定义的功能, 就是 Hook 的完整实现过程。

最后, 我们调用 hook 方法, 传入 window 对象和 btoa 字符串即可。

怎么去注入这个代码呢? 这里我们介绍 3 种注入方法。

  • 控制台注入

  • 重写 JavaScript

  • Tampermonkey 注入

控制台注入

对于我们这个场景,控制台注入其实就够了,我们先来介绍这个方法。这其实很简单,就是直接在控制台输入这行代码并运行即可,如图 11-159 所示。

图11-159 在控制台输入代码并运行

执行完这段代码之后,相当于我们已经把 windowbtoa 方法改写了,取消前面打的所有断点,然后在控制台调用 btoa 方法试试,如:

btoa('germey')

回车之后,就可以看到它进入我们自定义的 debugger 的位置并停下了,如图 11-160 所示。

图11-160 进入我们自定义的 debugger 的位置并停下

我们把断点向下执行,然后点击 Resume script execution 按钮,就可以看到控制台也输出了一些对应的结果,如被 Hook 的对象、Hook 的属性、调用的参数、调用后的结果等,如图 11-161 所示。

图 11-161 控制台输出的结果

我们通过 Hook 的方式改写了 btoa 方法, 使其每次在调用的时候都能停到一个断点, 同时还能输 出对应的结果。

接下来, 怎么用 Hook 找到对应的加密 id 的入口呢?

由于此时我们是在控制台直接输入的 Hook 代码, 所以页面刷新就无效了。但我们这个网站是 SPA 页面, 点击详情页的时候页面是不会刷新的, 因此这段代码依然生效。如果不是 SPA 页面, 即每次访问都需要刷新页面的网站, 那么这种注入方式就不生效了。

我们想要 Hook 列表页 Ajax 加载完成后逻辑, 对应的就是加密 id 的 Base64 编码过程, 怎样在 不刷新页面的情况下, 再次复现这个操作呢? 也很简单, 点击一下页就好。

这时候点击第 2 页的按钮, 可以看到它确实再次停到了 Hook 方法的 debugger 处。由于列表页的 Ajax 和加密 id 都带有 Base64 编码的操作, 所以都能 Hook 到。接着, 观察对应的 Arguments 或当前 网站的行为, 或者观察栈信息, 我们就能大体知道现在走到哪个位置了, 从而进一步通过栈的调用信 息找到调用 Base64 编码的位置。

根据调用栈的信息, 可以观察这些变量是在哪一层发生变化的。比如对于最后一层的, 我们可以 很明显看到它执行了 Base64 编码, 编码前的结果是:

ef34#teuqobtua#{-57w1q5o5--j@98xygimlyfxs*-1i-0-mb1

编码后的结果是:

ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxeTVvNS0takA5OHh5Z2ltbHlmeHMQLSFPbTAtbWIx

如图 11-162 所示, 这很明显。

图11-162 编码后的结果

核心问题就来了,编码前的结果 ef34#teuqobtua#(-57w1q5o5—​j@98xygimlyfxs*-!i-0-mb1 又是怎么来的呢?我们展开栈的调用信息,一层层看这个字符串的变化情况。如果不变,就看下一层;如果变了,就停下来仔细看。最后,我们可以在第 5 层找到它的变化过程,如图 11-163 所示。

图11-163 看看字符串的变化情况

_0x5dfcc0 是一个写死的字符串 ef34#teuqobtua#(-57w1q5o5—​j@98xygimlyfxs*-!i-0-mb,然后和传入的 _0x2fo999 拼接起来就形成了最后的字符串。

_0x2fo999 又 是怎么来的呢? 再往下追一层, 可以看到就是 Ajax 返回结果的单个电影信息的 id

因此, 这个加密逻辑就清楚了。其实非常简单, 就是 ef34#teuqobtua#{-57w1q5o5—​j@98xygimlyfxs*-li-0-mb1 加上电影 id, 然后进行 Base64 编码即可。

到此, 我们就成功用 Hook 的方式找到加密 id 的生成逻辑了。

但是想想有什么不太科学的地方吗? 刚才其实也说了, 我们的 Hook 代码是在控制台手动输入的, 一旦刷新页面就不生效了, 这的确是个问题。而且它必须在页面加载完了才能注入, 所以它并不能在 一开始就生效。

下面我们再介绍几种 Hook 注入方式。

重写 JavaScript

借助 Chrome 浏览器的 Overrides 功能,我们可以实现某些 JavaScript 文件的重写和保存。Overrides 会在本地生成一个 JavaScript 文件副本,以后每次刷新,都会使用副本的内容。

这里我们需要切换到 Sources 面板中的 Overrides 选项卡,然后选择一个文件夹,比如这里我自定义了一个 ChromeOverrides 文件夹,如图 11-164 所示。

图11-164 Sources 面板中的 Overrides 选项卡

然后随便选一个 JavaScript 脚本,在后面贴上这段注入脚本,如图 11-165 所示。

图11-165 在脚本后面贴上注入脚本

保存文件,此时可能提示页面崩溃,但是不用担心,重新刷新页面就好了。可以发现,现在浏览器加载的 JavaScript 文件就是我们修改过后的了,文件名左侧会有一个圆点标识符,如图 11-166 所示。

图11-166 修改后的 JavaScript 文件

同时我们还注意到,目前直接进入断点模式,并且成功 Hook 到 btoa 方法。

其实 Overrides 的功能非常有用,有了它,我们可以持久化保存任意修改的 JavaScript 代码,想在哪单改都可以了,甚至可以直接修改 JavaScript 的原始执行逻辑。

Tampermonkey 注入

如果不想用 Overrides 的方式改写 JavaScript 来注入,我们也可以使用前面介绍的 Tampermonkey 插件来注入,详细的使用方法可以参考 11.3 节。

开始之前请清除所有的断点,并且把刚才的 Overrides 功能关闭,以防对本方法产生干扰,如图 11-167 所示。

图11-167 关闭 Overrides 功能

接下来,我们创建一个新的脚本试试。点击左侧的 “+” 号,此时会显示如图 11-168 所示的页面。

图11-168 新建用户脚本

我们可以将脚本改写为如下内容:

// ==UserScript==
// @name         HookBase64
// @namespace    https://scrape.center/
// @version      0.1
// @description  Hook Base64 encode function
// @author       Germey
// @match        https://spa6.scrape.center/
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict'
    function hook(object, attr) {
        var func = object[attr]
        console.log('func', func)
        object[attr] = function () {
            console.log('hooked', object, attr)
            var ret = func.apply(object, arguments)
            debugger
            return ret
        }
    }
    hook(window, 'btoa')
})()

这时候启动脚本, 重新刷新页面, 可以发现可以成功 Hook btoa 方法, 如图 11-169 所示。接着, 我们再顺着调用逻辑即可。

图11-169 成功 Hook btoa 方法

这样我们就成功通过 Hook 的方式找到加密id的实现了。

寻找详情页 Ajax 的 token

现在我们已经找到详情页的加密 id 了,但是还差一步,其 Ajax 请求也有一个 token,如图 11-170 所示。

图11-170 详情页的 Ajax 请求

因为也是 Ajax 请求,我们可以通过上文提到的同样的方法对该 token 的生成逻辑进行分析,最终可以发现其实这个 token 和详情页 token 的构造逻辑是一样的。

使用 Python 实现详情页爬取

现在, 我们已经成功把详情页的加密 id 和 Ajax 请求的 token 找出来了, 下一步就是使用 Python 完成爬取。这里我只实现第一页的爬取, 示例代码如下:

import hashlib
import time
import base64
from typing import List, Any
import requests

INDEX_URL = 'https://spa6.scrape.center/api/movie?limit={limit}&offset={offset}&token={token}'
DETAIL_URL = 'https://spa6.scrape.center/api/movie/{id}?token={token}'
LIMIT = 10
OFFSET = 0
SECRET = 'ef34#teuqobtua#{-57w1q5o5--j@98xygimlyfxs*-li-0-mb'

def get_token(args: List[Any]):
    timestamp = str(int(time.time()))
    args.append(timestamp)
    sign = hashlib.sha1(','.join(args).encode('utf-8')).hexdigest()
    return base64.b64encode(','.join([sign, timestamp]).encode('utf-8')).decode('utf-8')

args = ['/api/movie']
token = get_token(args=args)
index_url = INDEX_URL.format(limit=LIMIT, offset=OFFSET, token=token)
response = requests.get(index_url)
print('response', response.json())

result = response.json()
for item in result['results']:
    id = item['id']
    encrypt_id = base64.b64encode((SECRET + str(id)).encode('utf-8')).decode('utf-8')
    args = [f'/api/movie/{encrypt_id}']
    token = get_token(args=args)
    detail_url = DETAIL_URL.format(id=encrypt_id, token=token)
    response = requests.get(detail_url)
    print('response', response.json())

这里模拟了详情页的加密 idtoken 的构造过程,然后请求了详情页的 Ajax 接口,这样我们就可以爬取到详情页的内容了。

总结

本节内容很多,一步步介绍了整个网站的 JavaScript 逆向过程,其中的技巧有:全局搜索查找入口、代码格式化、设置 Ajax 断点、变量监听、断点设置和跳过、栈查看、Hook 原理、Hook 注入、Overrides 功能、Tampermonkey 插件、Python 模拟实现。掌握了这些技巧,我们就能更加得心应手地实现 JavaScript 逆向分析了。