Chapter Two:无限debugger的原理与绕过与断点调试

1.无限debugger的原理与绕过

1.1 案例介绍

debugger 是JavaScript中定义的一个专门用于断点调试的关键字,只要遇到它,JavaScript的执行便会在此处中断,进入调试模式。有了 debugger 这个关键字,我们就可以非常方便地对JavaScript 代码进行调试,比如使用 JavaScript Hook 时,我们可以加入debugger关键字,使其在关键的位置停下来,以便查找逆向突破口。但有时候 debugger 会被网站开发者利用,使其成为阻挠我们正常调试的拦路虎。本节中,我们介绍一个案例来绕过无限 debugger。

案例介绍:我们先看一个案例,网址是 https://www.funddb.cn/mogul/index,打开这个网站,一般操作和之前的网站没有什么不同。但是,一旦我们打开开发者工具,就发现它立即进入了断点模式,如下图所示:

在这里插入图片描述

我们既没有设置任何断点,也没有执行任何额外的脚本,它就直接进入了断点模式。这时候我们可以点击 Resume script execution(恢复脚本执行)按钮,尝试跳过这个断点继续执行,如下图所示:

在这里插入图片描述
然而不管我们点击多少次按钮,它仍然一次次地进入断点模式,无限循环下去,我们称这样的情况为无限debugger。怎么办呢?似乎无法正常添加断点调试了,有什么解决办法吗? 办法当然是有的,本节中我们就来总结一下无限debugger的应对方案。在后面部分实战的案例中我们也会遇到无限debugger。

1.2 实现原理

首先要做的是找到无限 debugger 的源头。在 Sources 面板中可以看到,debugger 关键字出现在一个 JavaScript 文件里,这时点击左下角的格式化按钮,如下图所示:
在这里插入图片描述
格式化后的代码如下图所示,可以发现这里通过 setInterval 循环,每秒执行1次 debugger 语句。
在这里插入图片描述
当然,还有很多类似的实现,比如无限 for 循环、无限 while 循环、无限递归调用等,它们都可以实现这样的效果,原理大同小异。

1.3 禁用断点

因为 debugger 其实就是对应的一个断点,它相当于用代码显示地声明了一个断点,要解除它,我们只需要禁用这个断点就好了。

1、禁用所有断点。 全局禁用开关位于 Sources 面板的右上角,叫做 Deactivate breakpoints,如下图所示:
在这里插入图片描述
点击它,该按钮会被激活,变成蓝色,如下图所示:
在这里插入图片描述
这个时候我们再重新点击一下 Resume script execution(恢复脚本执行)按钮,跳过当前断点,页面就不会再进入到无限 debugger 的状态了。但是这种全局禁用其实并不是一个好的方案,因为禁用之后我们也无法在其他位置增加断点进行调试了,所有的断点都失效了

2、禁用局部断点。

取消刚才的 Deactivate breakpoints 模式,页面会重新进入无限 debugger 模式,尝试使用另一种方法来跳过这个无限 debugger。在 debugger 语句所在的行的行号上单击鼠标右键,此时会出现一个快捷菜单,如下图所示:

在这里插入图片描述
这里有一个 Never pause here 选项,意思是从不在此处暂停。选择这个选项,于是页面变成如下图所示的样子:
在这里插入图片描述
当前断点显示为橙色,并且断点前面多了一个 ? 符号,同时 Breakpoints 也出现了刚才添加的断点位置,这时再次点击 Resume script execution(恢复脚本执行)按钮,就可以发现我们不会再进入无限 debugger 模式了。当然,我们也可以选择另外一个选项 Add conditional breakpoint,如下图所示:
在这里插入图片描述
这个模式更加高级,我们可以设置进入断点的条件,比如在调试过程中,期望某个变量的值大于某个具体的值的时候才停下来。但在本案例中,由于这里是无限循环,我们没有什么具体的变量可以作为判定依据,因此可以直接写一个简单的表达式来控制。选择 Add conditional breakpoint 选项,直接填入 false 即可,如下图所示:
在这里插入图片描述
此时其效果就和选择 Never pause here 选项一样,重新点击 Resume script execution(恢复脚本执行)按钮,也不会进入无限 debugger 循环了。

3、替换文件。

利用 Overrides 面板我们可以将远程的 JavaScript 文件替换成本地的 JavaScript 文件,这里我们依然可以使用这个方法来对文件进行替换,替换成什么呢?很简单,我们只需要在新的文件里把 debugger 这个关键字删除。我们将当前的 JavaScript 文件复制到文本编辑器中,删除或者直接注释掉 debugger 这个关键字,修改如下:
在这里插入图片描述
打开 Sources 面板下的 Overrides 面板,将修改后的完整 JavaScript 文件复制进去,修改的内容如上图所示。替换完成之后,重新刷新网页,这时候发现不会进入无限 debugger 模式了。如果该操作不熟悉,可以参照下面的 1.4 改写 JavaScript 文件。

1.4 补充:改写 JavaScript 文件

我们知道,一个网页里面的 JavaScript 是从对应服务器上下载下来并在浏览器执行的。有时候,我们可能想要在调试的过程中对 JavaScript 做一些更改,比如说有以下需求:

  1. 发现 JavaScript 文件中包含很多阻挠调试的代码或者无效代码、干扰代码,想要将其删除(如上面的无限debugger)。
  2. 调试到某处,想要加一行 console.log 输出一些内容,以便观察某个变量或方法在页面加载过程中的调用情况。在某些情况下,这种方法比打断点调试更方便。
  3. 调试过程遇到某个局部变量或方法,想要把它赋值给 window 对象以便全局可以访问或调用。
  4. 在调试的时候,得到的某个变量中可能包含一些关键的结果,想要加一些逻辑将这些结果转发到对应的目标服务器。

这时候我们可以试着在 Sources 面板中对 JavaScript 进行更改,但这种更改并不能长久生效,一旦刷新页面,更改就全都没有了。比如我们在 JavaScript 文件中写入一行 JavaScript 代码,然后保存,如下图所示:
在这里插入图片描述
注意:点击了左下角的格式化按钮后,不能向格式化的文件中添加内容。

这时候我们可以发现 JavaScript 文件名左侧上出现了一个警告标志,提示我们做的更改是不会保存的。这时候重新刷新一下页面,再看一下更改的这个文件,如下图所示:
在这里插入图片描述
有什么方法可以修改呢?其实有一些浏览器插件可以实现,比如:ReRes。在插件中,我们可以添加自定义的 JavaScript 文件,并配置 URL 映射规则,这样浏览器在加载某个在线 JavaScript 文件的时候就可以将内容替换成自定义的 JavaScript 文件了。另外,还有一些代理服务器也可以实现,比如 Charles、Fiddler,借助它们可以加载 JavaScript 文件时修改对应 URL 的响应内容,以实现对 JavaScript 文件的修改。其实浏览器的开发者工具已经原生支持这个功能了,即浏览器的 Overrides 功能,它在 Sources 面板左侧,如下图所示:
在这里插入图片描述
我们可以在 Overrides 面板上选定一个本地的文件夹,用于保存需要更改的 JavaScript 文件,下面来实际操作一下。切到 Overrides 面板,点击 + 按钮,如下图所示:
在这里插入图片描述
这时候浏览器会提示我们选择一个本地文件夹,用于存储要替换的 JavaScript 文件。这里我选定了一个新建的文件夹:FunddbOverrides,注意这时候可能会遇到下图所示的提示,如果没有问题,直接点击 允许 即可。
在这里插入图片描述
这时,在 Overrides 面板下就多了 FunddbOverrides 文件夹,用于存储所有我们想要更改的 JavaScript 文件,如下图所示:
在这里插入图片描述
我们可以看到,现在所在的 JavaScript 选项卡是 app.d0a16ab3b7972174cc88.js:formatted,代码已经被格式化了。因为格式化后的代码是无法直接在浏览器中修改的,所以为了方便,我把格式化后的文件复制到了 Notepad++ 中,然后把 window.eval 这行代码注释了,如下图所示:
在这里插入图片描述
接着把修改后的内容替换到原来的 JavaScript 文件中。这里要注意,要切换到 app.d0a16ab3b7972174cc88.js 文件才能修改,直接替换 JavaScript 文件的所有内容即可,如下图所示:
在这里插入图片描述
替换完毕之后 ctrl + s 保存,这时候再切换回 Overrides 面板,就可以发现成功生成了新的 JavaScript 文件,它用于替换原有的 JavaScript 文件,如下图所示:
在这里插入图片描述
替换完成之后,重新刷新网页,正如我们所料,这时候不会再进入无限 debugger 模式了,证明改写 JavaScript 成功!而且刷新页面也不会丢失了,除了注释掉干扰代码外,在一些场景下,我们还可以增加一些 JavaScript 逻辑,比如直接将某个变量的结果通过 API 发送到远程服务器,并通过服务器将数据保存下来,也就完成了直接拦截 Ajax请求并保存数据的过程了,修改 JavaScript 文件有很多用途,此方案可以为我们进行 JavaScript 逆向带来极大便利。

2.断点调试

接下来,我们介绍一个非常重要的功能--------断点调试。在调试代码的时候,我们可以在需要的位置上打断点,当对应事件触发时,浏览器就会自动停在断点的位置等待调试,此时我们可以选择单步调试,在面板中观察调用栈、变量值,以更好地追踪对应位置的执行逻辑。断点怎么打呢?我们还是以上面 JavaScript 选项卡是 app.d0a16ab3b7972174cc88.js:formatted 的例子来说,首先鼠标左键单击下图所示的代码行号:
在这里插入图片描述
行号处出现了一个蓝色的箭头,这就证明断点已经添加好了,同时在右侧的 Breakpoints 选项卡会出现我们添加的断点的列表。这个网页比较简单,我们换此网站进行学习:https://spa2.scrape.center/
在这里插入图片描述
此小节我们主要学习的是如何调试断点,就不具体去分析网页了,假设我们知道这个断点是用来处理翻页按钮的点击事件的,所以可以在网页里面点击按钮试一下,比如点击第4页的按钮,这个时候就会发现断点被触发了,如下图所示:
在这里插入图片描述
这时候我们可以看到页面中显示了一个叫做 Paused in debugger 的提示,这说明浏览器执行到刚才我们设置断点的位置就不再继续执行了,等待我们发号施令执行调试。此时代码停在了第 4446 行,回调参数 e 就是对应的点击事件 PointerEvent。在右侧的 Scope 面板处,可以观察到各个变量的值,比如在 Local 域下有当前方法的局部变量,我们可以在这里看到 PointerEvent 的各个属性,如下图所示:
在这里插入图片描述
另外,这里有一个 o 方法,它在 Jr 方法下面,所以切换到 Closure(Jr) 域,可以查看它的定义及其接收的参数,如下图所示:
在这里插入图片描述
我们可以看到,FunctionLocation 又指向了方法 o,点击之后便又可以调到指定的位置,用同样的方法进行断点调试即可。在 Scope 面板还有多个域,这里就不展开介绍了。总之,通过 Scope 面板,我们可以看到当前执行环境下变量的值和方法的定义,知道当前代码究竟执行了怎样的逻辑。接下来,切换到 Watch 面板,在这里可以自行添加想要查看的变量和方法。点击右上角的 + 按钮,我们可以任意添加想要监听的对象,如下图所示:
在这里插入图片描述
比如我们这里比较关注 o.apply 方法,于是点击添加 o.apply,这里就会把对应的方法定义呈现出来,展开之后再点击 FunctionLocation 定位其源码位置。我们还可以切换到 Console 面板,输入任意的 JavaScript 代码,此时便会执行、输出对应的结果,如下图所示:
在这里插入图片描述
如果我们想看看变量 arguments 的第一个元素是什么,那么可以直接敲入 arguments[0],此时便会输出对应的结果是 PointerEvent。只要在当前上下文能访问到的变量都可以直接引用并输出。此时我们还可以选择单步调试,这里有三个重要按钮,如下图所示:
在这里插入图片描述
用得比较多的是第一个,相当于逐行调试,比如,点击 Step over next function call 按钮,就运行到了 4447 行,高亮的位置就变成了这一行,如下图所示:
在这里插入图片描述
在调试的过程中,我们可以会跳到一个新的位置,比如点击几下 Step over next function call 按钮,可能会到一个叫作 ct 的方法中,这时候我们也不知道发生了什么,如下图所示:
在这里插入图片描述
究竟是怎么跳过来的呢?我们观察一下右侧的 Call Stack 面板,就可以看到全部的调用过程了。比如它的上一步是 ot 方法,再上一步是 pt 方法,点击对应的位置也可以跳到对应的代码位置,如下图所示:
在这里插入图片描述
有时候调用栈是非常有用的,利用它我们可以回溯到某个逻辑的执行流程,从而快速找到突破口。在调试过程中,如果想快速跳到下一个断点或者让 JavaScript 代码运行下去,可以点击 Resume script execution 按钮,如下图所示:
在这里插入图片描述
这时浏览器会直接执行到下一个断点的位置,从而避免陷入无穷无尽的调试中。当然,如果没有其他断点了,浏览器就会恢复正常状态。

Ajax 断点

有时候我们可以通过 DOM 节点的监听器(Listener),通过监听器我们可以手动设置断点并进行调试。但其实针对这个网站:https://spa2.scrape.center/,通过翻页的点击事件监听器是不太容易找到突破口的。接下来我们介绍一个方法--------Ajax断点,它可以在发生 Ajax 请求的时候触发断点。对于这个例子,我们的目标其实就是找到 Ajax 请求的那一部分逻辑,找出加密参数是怎么构造的。可以想到,通过 Ajax 断点,使页面在获取数据的时候停下来,我们就可以顺着找到构造 Ajax 请求的逻辑了。把之前的断点全部取消,切换到 Sources 面板下,然后展开 XHR/fetch Breakpoints,这里就可以设置Ajax断点,如下图所示:
在这里插入图片描述
要设置断点,就要先观察 Ajax 请求,和之前一样,点击翻页按钮2,在 Network 面板里面观察Ajax请求是怎么样的,请求的URL如下图所示:
在这里插入图片描述
可以看到,URL 里面包含 /api/movie 这样的内容,所以我们可以再刚才的 XHR/fetch Breakpoints 面板中添加拦截规则。点击 + 按钮,可以看到一行 Break when URL contains: 的提示,意思是当 Ajax 请求的 URL 包含填写的内容时,会进入断点停止,这里可以填写 /api/movie,如下图所示:
在这里插入图片描述
这时候我们再点击翻页按钮3,触发第3页的 Ajax 请求,会发现点击之后页面走到断点停下来了,如下图所示:
在这里插入图片描述
格式化代码看一下,发现它停到了 Ajax 最后发送的那个时候,即底层的 XMLHttpRequest 的 send 方法,可是似乎还是找不到 Ajax 请求是怎么构造的,前面我们说过 Call Stack,通过它可以顺着找到前序调用逻辑,所以顺着它一层一层找,也可以找到构造 Ajax 请求的逻辑,最后会找到一个叫作 onFecthData 的方法,如下图所示:
在这里插入图片描述
接下来,切换到 onFetchData 方法并将代码格式化,可以看到下图所示的调用方法:
在这里插入图片描述
可以发现,这里可能使用 axios 库发起了一个 Ajax 请求,还有 limit、offset、token 这3个参数,基本就能确定了,顺利找到了突破口!这里我们就不再此展开分析了,后文会有完整的分析实战,因此在某些情况下,我们可以比较容易地通过 Ajax 断点找到分析的突破口,这是一个常见的寻找 JavaScript 逆向突破口的方法,要取消断点也很简单,只需要在 XHR/fetch Breakpoints 面板取消勾选即可,如下图所示:
在这里插入图片描述


版权声明:本文为xw1680原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。