浏览器环境下JavaScript的模拟执行

在前面两节中,我们了解了利用 PyExecJS 和 Nodejs 对 JavaScript 进行模拟执行的方法,但在某些复杂的情况下可能还是有一定的局限性。

分析

比如说:我们在测览器中找到了一个类似的加密算法,其生成逻辑如下:

const token = encrypt(a, b)

我们最终需要获取的就是 token 这个变量究竟是什么。这个 token 模拟出来了,就可以直接拿着去构造请求进行数据爬取了。但这个 token 是由一个 encrypt 方法返回的,参数是 a 和 b。对于参数 a 和 b,我们可能比较容易找到它们是怎么生成的,但是这个 encrypt 方法非常复杂,其内部又关联了许多变量和对象,甚至方法内部的逻辑也进行了混淆等操作,向内追踪非常困难。

这时候如果我们要用 Python 和 Nodejs 来模拟整个调用过程,关键其实就两步:

  • 把所有的依赖库都下载到本地;

  • 使用 PyExecJS 或 Nodejs 来加载依赖库并模拟调用 encrypt 方法

但在某些情况下可能存在一定的问题,我们分两个方面来进行探讨。

环境差异

前面提到过,Node.js 中没有全局 window 对象,取而代之的是 global 对象。如果 JavaScript 文件中有任何引用 window 对象的方法,就没法在 Nodejs 环境中运行。我们需要做的就是把 window 对象改写成 global 对象,或者把一些浏览器中的对象用其他方法代替。

依赖库查找

在上面的例子中,encrypt 所依赖的全部逻辑和依赖库其实都已经加载到浏览器。如果我们要在其他环境中模拟执行,要从中完全剥离出 encrypt 所依赖的 JavaScript 库,肯定还需要费一些功夫。一旦缺少了必备的依赖库,就会导致 encrypt 方法无法成功运行。

对于一些复杂的情况,为什么我们不直接用测览器作为执行环境来辅助逆向呢?

本节中,我们就来介绍一个借助浏览器模拟辅助逆向的方法,可以实现任意位置的代码注入和修改,同时可以实现全局和任意时刻调用,非常方便。

准备工作

本节中,我们使用 playwright 来实现浏览器辅助逆向。首先,安装 playwright,相关命令如下:

pip3 install playwright
playwright install

运行如上两条命令之后,会安装 playwright 库,并安装 Chromjum、Firefox、WebKit 三个内核的浏览器供 playwright 直接使用。具体的安装方法可以参考; https://setup.scrape.center/playwright

案例介绍

本节中,我们要分析的目标站点是 https://spa2.scrape.center/ 。可以看到,其 Ajax 请求参数带有一个 token,并且每次都会变化,如图 11-75 所示。

实战

首先,我们来实现 Object(i["a"]) 的全局挂载,只需要将其赋值给 window 对象的一个属性即可,属性名称任意,只要不和现有的属性冲突即可。

比如我们需要在代码:

var a = (this.page-1) * this.limit,e = Object(i["a"])(this.$store.state.url.index,a);

下方添加如下用于挂载全局 window 对象的代码:

window.encrypt = Object(i["a"]);

比如,这里我们将 Object(i["a"]) 挂载给 window 对象的 encrypt 属性。这样只要该行代码执行完毕,我们调用 window.encrypt 方法就相当于调用了 Object["a"] 方法。

接着,我们将修改后的整个 JavaScript 代码文件保存到本地,并将其命名为 chunk.js,如图 11-79 所示。

图 11-79 chunk.js 文件

接下来,我们利用 playwright 启动一个测览器,并使用 Request Interception 将 JavaScript 文件替换,实现如下:

from playwright.sync_api import sync_playwright

BASE_URL = "https://spa2.scrape.center"
context = sync_playwright().start()
browser = context.chromium.launch()
page = browser.new_page()
page.route(
    "/js/chunk-10192a00.243cb8b7.js",
    lambda route: route.fulfill(path="./chunk.js")
)
page.goto(BASE_URL)

这里首先使用 playwright 创建一个 Chromium 无头测览器,然后利用 new_page 方法创建一个新的页面,并定义了一个关键的路由:

page.route(
    "/js/chunk-10192a00.243cb8b7.js",
    lambda route: route.fulfill(path="./chunk.js")
)

这里路由的第一个参数是原本加载的文件路径,比如原本加载的 JavaScript 路径为 /j5/chunk-10192a00.243cb8b7.js,如图 11-80 所示。

图 11-80 原JavaScript文件加载路径

第二个参数利用 route 的 fulfill 方法指定本地的文件,也就是我们修改后的文件 chunk.js。

这样 playwright 加载 /js/chunk-10192a00.243cb8b7.js 文件的时候,其内容就会被替换为我们本地保存的 chunk.js 文件。当执行之后,Object(i["a"]) 也就被挂载给 window 对象的 encrypt 属性了,所以调用 window.encrypt 方法就相当于调用了 object(i["a"]) 方法了。

怎么模拟调用呢?很简单,只需要在 playwright 环境中额外执行 JavaScript 代码即可,比如可以定义如下的方法:

def get_token(offset):
    result = page.evaluate('''() => (
        return window.encrypt("%s","%s")
    }''' %('/api/movie', offset))
    return result

这里我们声明了 get_token 方法,经过上文的分析,模拟执行方法需要传入两个参数,第一个参数是固定值 /api/movie,另一个参数是变值,所以将其当作参数传入。

在模拟执行的过程中,我们直接使用 page 对象的 evaluate 方法,传入 JavaScript 字符串即可,这个 JavaScript 字符串是一个方法,返回的就是 window.encrypt 方法的执行结果。最后将结果赋给 result 变量,并返回。

到此为止,核心代码就说完了。最后,我们只需要完善一下逻辑,将上面的代码串联调用即可。最终整理的代码如下:

from playwright.sync_api import sync_playwright
import time
import requests

BASE_URL = 'https://spa2.scrape.center'
INDEX_URL = BASE_URL + '/api/movie?limit={limit}&offset={offset}&token={token}'
MAX_PAGE = 10
LIMIT = 10

context = sync_playwright().start()
browser = context.chromium.launch(headless=False)
page = browser.new_page()
page.route(
    "/js/chunk-10192a00.243cb8b7.js",
    lambda route: route.fulfill(path="./chunk.js")
)
page.goto(BASE_URL)


def get_token(offset):
    result = page.evaluate('''() => {
        return window.encrypt("%s", "%s")
    }''' % ('/api/movie', offset))
    return result


for i in range(MAX_PAGE):
    offset = i * LIMIT
    token = get_token(offset)
    index_url = INDEX_URL.format(limit=LIMIT, offset=offset, token=token)
    response = requests.get(index_url)
    print('response', response.json())

time.sleep(10)

这里我们遍历了 10 页,然后构造了 offset 变量,传给 get_token 方法获取 token 即可,最终运行结果如下:

可以看到,每一页的数据就被成功爬取到了,简单方便。

总结

本节中,我们介绍了在浏览器环境中模拟执行 JavaScript 来辅助 JavaScript 逆向的方法,这会在一定程度上减轻逆向的压力。熟练掌握此技能,我们可以少走很多弯路。