JavaScript逆向技巧总结

前面我们已经学习了不少 JavaScript 逆向相关的知识,包括浏览器调试、Hook、AST、无限 debugger 的绕过以及 JavaScript 的模拟调用等,这些知识点都比较松散,有时候大家学完了可能觉得没有形成一个知识体系,或者说没有一个常规 “套路” 来应对一些 JavaScript 逆向的处理流程。

本节中,我们就对前面的知识点做一个串联和总结,总结出 JavaScript 逆向过程中常用的一个流程,这个流程适用于大多数 JavaScript 逆向过程。大家熟练运用之后,可以在不同情况下运用不同的技巧来进行 JavaScript 逆向操作。

总的来说,JavaScript 逆向可以分为三大部分:寻找入口、调试分析和模拟执行。下面我们来分别介绍。

  • 寻找入口:这是非常关键的一步,逆向在大部分情况下就是找一些加密参数到底是怎么来的比如一个请求中 tokensign 等参数到底是在哪里构造的,这个关键逻辑可能写在某个关键的方法里面或者隐藏在某个关键变量里面。一个网站加载了很多 JavaScript 文件,那么怎么从这么多 JavaScript 代码里面找到关键的位置,那就是一个关键问题。这就是寻找入口。

  • 调试分析:找到入口之后,比如说我们可以定位到某个参数可能是在某个方法里面执行的了 ,那么里面的逻辑究竟是怎样的,里面调用了多少加密算法,经过了多少变量赋值和转换等,这些我们需要先把整体思路搞清楚,以便于我们后面进行模拟调用或者逻辑改写。在这个过程中,我们主要借助于浏览器的调试工具进行断点调试分析或者借助于一些反混淆工具进行代码的反混淆等。

  • 模拟执行:经过调试分析之后,我们差不多已经搞清楚整个逻辑了,但我们的最终目的还是写爬虫,怎么爬到数据才是根本,因此这里就需要对整个加密过程进行逻辑复写或者模拟执行,以把整个加密流程模拟出来,比如输入是一些已知变量,调用之后我们就可以拿到一些 token 内容,再用这个 token 来进行数据爬取即可。

本节中,我们就来对以上内容进行梳理。

寻找入口

首先,我们来看下怎么寻找入口,其中包括查看请求、搜索参数、分析发起调用、断点、Hook 等操作,下面我们来分别介绍一下。

查看请求

一般来说,我们都是先分析想要的数据到底是从哪里来的。比如说对于示例网站 https://spa6.scrape.center/ ,我们可以看到首页有一条条数据,如 “霸王别姬”、“这个杀手不太冷” 等,这些数据肯定是某个请求返回的,那它究竟是从哪个请求里面来的呢?我们可以先尝试搜索下。

打开浏览器开发者工具,打开 Network 面板,然后点击搜索按钮,比如这里我们就搜索 “霸王别姬” 这四个字,如图 11-119 所示。

图 11-119 搜索 “霸王别姬”

此时可以看到对应的搜索结果,点击搜索到的结果,我们就可以定位到对应的响应结果的位置, 如图 11-120 所示。

图 11-120 定位到对应的响应结果的位置

找到对应的响应之后,我们也就可以顺便找到是哪个请求发起的了,如图 11-121 所示。

图 11-121 对应请求

比如,这单我们就顺利找到想要的数据所对应的请求位置了,可以看到这是一个 GET 请求,同时还有一个 token 参数,我们可以在后面继续分析。

一般来说,我们可以通过这种方法来尝试寻找最初的突破口。如果这个请求带有加密参数,就顺着继续找下这个参数究竟是在哪里生成的。如果这个请求对应的参数甚至都没有什么加密参数,那么这个请求都可以直接模拟爬取了。

搜索参数

在上一步中,我们找到了最初的突破口,也就是关键请求是怎么发起的,带有什么加密参数。比如,在上面的例子中,我们发现这里有一个关键的加密参数 token,那这又是怎么构造出来的呢?

一种简单有效的方法就是直接进行全局搜索。一般来说,参数名大多数情况下就是一个普通的字符串,比如这里就叫作 token,那么这个字符串肯定隐藏在某个 JavaScript 文件里面,我们可以尝试进行搜索,也可以加冒号、空格、引号等来配合搜索。因为一般来说这个参数通常会配合一些符号一起出现,比如说我们可以搜索 tokentoken:token :"token": 等。

在哪里搜索呢?我们可以直接利用浏览器调试面板的搜索功能,如图11-122所示。

图 11-122 浏览器调试面板的搜索功能

这是一个资源搜索的入口,比如可以搜索下载下来的 JavaScript 文件的内容,这里我们输入 token 来进行搜索,结果如图11-123所示。

图 11-123 搜索结果

这样我们就可以找到一些关键的位置点了,一共五个结果,结果不多,我们可以进一步点击并定位到对应的 JavaScript 文件中,然后进一步进行分析。

分析发起调用

上述的搜索是其中一种查找入口的方式,这是从源码级别上直接查找。当然,我们也可以通过其他的思路来查找入口,比如可以查看发起调用的流程,怎么查看呢?

可以直接从 Network 请求里面的 Initiator 查看当前请求构造的相关逻辑,如图 11-124 所示。

图 11-124 从 Initiator 查看当前请求构造的相关逻辑

把光标对应到 Initiator 这一列,就会出现发起这个请求都经过了哪些调用,也就是调用发起方的一步步热行流程,如图 11-125 所示。

右侧显示了一步步调用对应的源码的位置,我们可以顺次点进去找到对应的位置,比如这里第 8 层调用里面有一个 onFetchData 方法。点击右侧的代码位置,就可以找到一些相关逻辑,如图 11-126 所示。

图11-125 调用发起方的一步步执行流程

图11-126 相关逻辑

这里可以看到一些 token 相关的逻辑调用过程了。

断点

另外,我们还可以通过一些断点来进行入口的查找,比如 XHR 断点、DOM 断点、事件断点等。我们可以在开发者工具 Sources 面板里面添加设置,比如这里我们就添加了 XHR 断点和全局 Load 事件断点,如图 11-127 所示。

图 11-127 添加 XHR 断点和全局 Load 事件断点

这样网页就可以在整个网页加载之后和发起 Ajax 请求的时候停下来,进入断点调试模式。也就是说,通过浏览器强大的断点调试功能,我们也可以找到对应的入口。

Hook

Hook 也是一个非常常用的查找入口的功能。有时候,一些代码搜索或者断点并不能很有效地找到对应的入口位置,这时候就可以使用 Hook 了。

比如说,我们可以对一些常用的加密和编码算法、常用的转换操作都进行一些 Hook,比如说 Base64 编码、Cookie 的赋值、JSON 的序列化等。

比较方便的 Hook 方式就是通过 TemporMonkey 这个插件实现,使用它我们不仅可以方便地自定义脚本执行的时间点,也可以引人一些额外的脚本来辅助 Hook 代码的编写,具体的实现流程可以参考 11.3 节的内容,这里不再赞述。

其它

以上便是一些常见的分析入口的方法,当然还有很多其他方法,比如使用 Pyppeteer、PlayWright 里面内置的 API 实现一些数据拦截和过滤功能,也可以使用一些抓包软件对一些请求进行拦截和分析,还可以使用一些第三方工具或浏览器插件来辅助分析。

调试分析

找到对应的入口位置之后,接下来我们就需要进行调试分析了。在这个步骤中,我们通常需要进行一些格式化、断点调试、反混淆等操作来辅助整个流程的分析。

格式化

格式化这个流程是非常重要的,它可以大大增强代码的可读性,一般来说很多 JavaScript 代码都是经过打包和压缩的。多数情况下,我们可以使用 Sources 面板下 JavaScript 窗口左下角的格式化按钮对代码进行格式化,如图11-128所示。

图 11-128 格式化按钮

另外,有一些网站的 HTML 和 JavaScript 是混杂在一起的,比如 https://spa8.scrape.center/ ,如图11-129 所示。

图 11-129 HTML 和 JavaScript 混杂在一起

可以看到,这里 JavaScript 代码被压缩成一行,并且放在 script 节点里面,这时候我们就需要手动复制出来,然后用一些格式化工具进行格式化。可以搜索 JavaScript Beautifier 相关工具,比如 https://beautifier.io/ ,然后把 JavaScript 代码粘贴进去,此时即可看到格式化之后的 JavaScript 代码,如图 11-130 所示。

图 11-130 格式化之后的 JavaScript 代码

另外,我们还可以选择一些格式化选项,比如缩进、换行等。

断点调试

代码格式化之后,我们就可以进入正式的调试流程了,基本操调试的代码漆加断点,同时在对应的面板里面观察对应变量的值。

如图 11-131 所示,这里我们在第 169 行添加断点,然后逐行运行对应的代码,这时代码页面就会出现对应变量的值,同时我们也可以在 Watch 面板上监听关注的变量。

图11-131 在第 169 行添加断点

通过这样的方式。我们就可以对整个代码的执行流程有一个大致的了解。关于断点调试的使用,可以参考 11.2 节和后续实战章节。

反混淆

在某些情况下,我们还有可能遇到一些混淆方式,比如控制流扁平化、数组移位等。对于一些特殊的混淆,我们可以尝试使用 AST 技术来对代码进行还原。

比如说,案例 https://antispider10.scrape.center/ 就使用控制流扁平化的方式对代码进行混淆,如图 11-132 所示。

图 11-132 使用控制流扁平化的方式对代码进行混淆

可以看到,这里有一个 while 循环,循环内通过一些判断条件执行某些逻辑,有的逻辑放在了 if 区块,有的逻辑放在了 else 区块,还有的逻辑放在了 catch 区块,这就导致我们无法一下子了解这几个区块的真正执行顺序。

对于此类混淆,为了更好地还原其真实执行逻辑,我们可以尝试使用 AST 进行还原,具体的实现流程可以参考 11.9 节的内容。

模拟执行

经过一系列调试,现在我们已经可以厘清其中的逻辑了,接下来就是一些调用执行的过程了。在前面的章节中,我们已经讲过一些案例执行的流程了。

Python 改写或模拟执行

由于 Python 简单易用,同时也能够模拟调用执行 JavaScript。如果整体逻辑不复杂的话,我们可以尝试使用 Python 来把整个加密流程完整实现一遍。如果整体流程相对复杂,我们可以尝试使用 Python 来模拟调用 JavaScript 的执行。具体的内容可以参考11.5节。

JavaScript 模拟执行 + API

由于整个逻辑是 JavaScript 实现的,使用 Python 来执行 JavaScript 难免会有一些不太方便的地方。而 Node.js 天生就有对 JavaScript 的支持。为了更通用地实现 JavaScript 的模拟调用,我们可以用 express 来模拟调用 JavaScript,同时将其暴露成一个 API,从而实现跨语言的调用。具体内容可以参考 11.6 节。

浏览器模拟执行

由于整个逻辑是运行在浏览器里面的,我们当然也可以将浏览器当作整个执行环境。比如使用 Selenium、PlayWright 等来尝试执行一些 JavaScript 代码,得到一些返回结果。具体内容可以参考 11.7 节。

调用执行的方式有很多,不同情况下我们可以根据实现的难易程度来选择不同的方案。

总结

本节中,我们对本章所学的知识进行了一些串联和总结,通过三大步骤一寻找入口、调试分析模拟执行来梳理 JavaScript 逆向过程中常用的技巧。另外,我希望大家能多结合实战案例对这些技巧进行运用,熟能生巧。