导语: 只要有足够的时间和耐心, 人们总能调试和逆向出 JavaScript 代码片段中的逻辑相反, 我们想要提供的只是一些点子, 以增加理解代码内部机制的难度
在去年夏天的时候, 我曾经与 @cgvwzq 一道, 就 JavaScript 中的反调试技巧进行过长期的探讨刚开始, 我们试图从网上查找该方面的资源或文章, 遗憾的是, 这方面的文档非常匮乏, 并且即使找到的那些文档, 其内容也很不完整而本文的目的, 就是收集与 JavaScript 反调试有关的技巧(其中一些已被恶意软件或商业产品所采用, 而其他想法则是我们独创的)
需要说明的是: 我们谈论的不是银弹, 而是 JavaScript 只要有足够的时间和耐心, 人们总能调试和逆向出 JavaScript 代码片段中的逻辑相反, 我们想要提供的只是一些点子, 以增加理解代码内部机制的难度事实上, 我们在这里展示的反调试技术与混淆技术无关(而对于混淆技术来说, 网上有大量的信息和工具可用), 它们更侧重于如何让调试过程更加困难
本文将涵盖以下主题:
. 检测非预期执行环境(我们只想在浏览器中执行)
. 检测调试工具(例如 DevTools)
. 代码完整性控制
. 执行流完整性控制
. 反仿真
我们的主要想法是将这里介绍的技术与混淆技术和加密技术整合起来其中, 代码被分割成一系列加密的代码块, 每块的解密过程取决于先前解密的其他块预期的程序执行流程, 应该以已知的顺序在加密块之间跳转如果我们检测到任何奇怪的事情, 程序流程就会修改原执行流程, 转向一些伪装块 所以, 当发现有人正在调试我们的代码时, 就会直接让他们跳转到一个蜜罐代码段中, 让他们无法接触真正想要了解的部分
当然, 我们所了解的相关技巧也还不完善, 如果读者有更多的技巧, 请通过 @ TheXC3LL 与我联系, 以便将它们添加到本文中
0x01 函数重定义
这是用来避免代码被别人调试的最简单和最著名的技术在 JavaScript 中, 用于检索信息的函数通常都可以进行重定义, 如 console.log(), 它们用于在控制台中显示函数变量等方面的信息如果我们重新定义这些函数, 并且改变其行为, 那么就可以隐藏某些信息, 或者伪造某些信息
为了帮助大家理解, 请在 DevTools 中运行下列代码:
- console.log("Hello World");
- var fake = function() {};
- window['console']['log'] = fake;
- console.log("You can't see me!");
将看到下列输出:
VM48:1 Hello World
我们发现, 第二条消息并没有被显示, 因为我们已经将其重新定义为一个空函数, 从而禁用了该函数 但我们的做法可以更巧妙一点, 直接改变其行为, 让它显示虚假信息, 具体代码如下所示:
- console.log("Normal function");
- // First we save a reference to the original console.log function
- var original = window['console']['log'];
- // Next we create our fake function
- // Basicly we check the argument and if match we call original function with other param.
- // If there is no match pass the argument to the original function
- var fake = function(argument) {
- if (argument === "Ka0labs") {
- original("Spoofed!");
- } else {
- original(argument);
- }
- }
- // We redefine now console.log as our fake function
- window['console']['log'] = fake;
- // Then we call console.log with any argument
- console.log("This is unaltered");
- // Now we should see other text in console different to "Ka0labs"
- console.log("Ka0labs");
- // Aaaand everything still OK
- console.log("Bye bye!");
如果一切正常的话, 将会看到:
- Normal function
- VM117:11 This is unaltered
- VM117:9 Spoofed!
- VM117:11 Bye bye!
如果你之前玩过 hooking, 那么对此肯定不会陌生
我们可以做的更巧妙一些, 重新定义更有趣的其他功能, 以便以出其不意的方式控制被执行的代码 例如, 我们可以根据前面的代码构建一个代码片段来重新定义 eval 函数我们可以将 JavaScript 代码传递给 eval 函数, 这样的话, 这些 JavaScript 代码就能得到执行了 但是, 如果我们重新定义该函数, 我们甚至可以运行一些不同的代码也就是说, 我们实现了所见非所得:)
- // Just a normal
- eval eval("console.log('1337')");
- // Now we repat the process...
- var original = eval;
- var fake = function(argument) {
- // If the code to be evaluated contains 1337...
- if (argument.indexOf("1337") !== -1) {
- // ... we just execute a different code
- original("for (i = 0; i < 10; i++) { console.log(i);}");
- }
- else {
- original(argument);
- }
- }
- eval = fake;
- eval("console.log('We should see this...')");
- // Now we should see the execution of a for loop instead of what is expected
- eval("console.log('Too 1337 for you!')");
是的, 我们执行的是不同的代码(for 循环, 而不是 console.log, 字符串为 Too 1337 for you!)
- 1337
- VM146:1 We should see this...
- VM147:1 0
- VM147:1 1
- VM147:1 2
- VM147:1 3
- VM147:1 4
- VM147:1 5
- VM147:1 6
- VM147:1 7
- VM147:1 8
- VM147:1 9
通过这种方式修改程序的执行流程是一种很酷的方法, 但是, 就像前面提到的那样, 这也是最基本的方法很容易被检出和击败这是因为, 在 JavaScript 中, 每个函数都有一个返回自身代码的 toString 方法 (对于 Firefox 来说为 toSource) 所以, 为了检测这种反调试方法, 只需要检查目标函数的代码是否被改变就可以了当然, 我们也可以重新定义 toString/toSource 方法, 但是, 在这种情况下, 我们会面临另一个令人头疼的函数: function.toString.toString()
在后文中, 我们将使用另一种基于代理对象的方法, 对 hooking 和函数重定义展开更全面和深入的探讨
0x02 断点
对于各种 JavaScript 调试工具 (例如 DevTools) 来说, 都能够在任意位置让脚本停止执行, 以帮助了解正在发生的事情实际上, 这可以通过断点来实现 在调试时, 使用断点可以帮助我们了解发生了什么, 正在发生什么以及接下来会发生什么, 因此, 断点是最为基本的调试功能
如果读者在 x86 系统上面玩过调试器的话, 很可能会了解 0xCC 指令 在 JavaScript 中, 我们有一个称为 debugger 的模拟指令在脚本中放入一个 debugger 指令; 当调试程序遇到这个指令的时候, 就会停止执行该脚本例如:
- console.log("See me!");
- debugger;
- console.log("See me!");
如果在启动 DevTools 的情况下执行上面的代码的话, 系统会提示您恢复执行在按 Continue 按钮之前, 该脚本将处于停止状态在商业产品中用过的一个 (非常愚蠢的) 技巧就是: 在一个无限循环中放入 debugger; 语句有些浏览器能够防止这种无限循环, 但是, 某些浏览器则不会这样做无论如何, 这只是为了给调试代码的人制造麻烦该循环会不断跳出窗口, 要求他们恢复执行, 所以, 在解决这个麻烦之前, 根本无法正常调试代码
setTimeout(function() {while (true) {eval("debugger")
与断点有关的其他技巧将在下一节中介绍
0x03 时间差异
从经典的反逆向技术中借鉴的另一个反调试技巧是使用基于时间的检测技术当使用 DevTools(或类似的软件)执行脚本时, 执行时间会明显变长 我们可以利用这种现象, 将时间检测代码是否处于调试状态为此, 可以使用多种不同的方法来达到这个目的
例如, 我们可以测量代码内两点或多点之间的执行时间如果我们知道在正常条件下这些点之间的平均时间, 就可以使用这个值作为基准如果执行的时间比预期更长, 则意味着代码正在调试器下面运行
基于这个概念的其他思路, 是使用含有循环或其他耗时代码的函数:
- setInterval(function(){
- var startTime = performance.now(), check, diff;
- for (check = 0; check < 1000; check++){
- console.log(check);
- console.clear();
- }
- diff = performance.now() - startTime;
- if (diff > 200){
- alert("Debugger detected!");
- }
- }, 500);
首先, 在未启动 DevTools 的情况下运行一下代码, 然后启动 DevTools 工具 正如您所看到的, 我们可以检测到调试器的存在, 因为这里的时间差比预期的要大 这种将时间作为参考指标的方法, 可以与上一节中所介绍的方法结合使用这样, 我们可以分别测试放置断点前后所消耗的时间如果执行了断点, 则在恢复执行之前, 肯定会耗费一定的时间, 以此检测调试器的存在
- var startTime = performance.now();
- debugger;
- var stopTime = performance.now();
- if ((stopTime - startTime) > 1000) {
- alert("Debugger detected!")
- }
这些时间检测点, 可以在代码中的随机分布, 因此分析人员很难找到它们
0x04 DevTools 检测(Chrome)
我是在这篇文章 (https://www.reddit.com/r/firefox/comments/5gtedd/ublock_origin_developer_raymond_hill_on/dav4iiu/) 中第一次看到该 DevTools 检测技术的, 根据该文的说法:
使用的技术是在 div 元素的 id 属性上实现一个 getter 当该 div 元素被发送到 console.log(div)等控制台时, 为方便起见, 浏览器会自动尝试获取元素的 ID 因此, 如果在调用 console.log 后执行 getter 的话, 这就意味着控制台已打开
下面给出一个简单的概念验证代码:
- let div = document.createElement('div');
- let loop = setInterval(() => {
- console.log(div);
- console.clear();
- });
- Object.defineProperty(div, "id", {get: () => {
- clearInterval(loop);
- alert("Dev Tools detected!");
- }});
0x05 执行流程完整性的隐式控制
对 JavaScript 代码进行去混淆处理时, 通常先对变量和函数进行重命名, 以提高源代码的可读性为此, 只需将代码分成更小的代码块, 然后重新命名即可在 JavaScript 中, 我们可以检测函数的名称是否修改过, 或从未改变更准确的说, 我们可以检查堆栈跟踪是否包含原始名称和原始顺序
使用 arguments.callee.caller, 我们可以创建一个堆栈跟踪, 用于保存之前执行过的函数我们可以使用这些信息来生成一个哈希值, 然后将其用于生成解密 JavaScript 其他部分的密钥的种子通过这种方式, 我们可以对执行流程完整性进行隐式控制, 因为如果函数被重命名或被执行的函数的顺序略有不同, 则生成的哈希值就会变得截然不同如果该哈希值不同的话, 则生成的密钥也会不同如果密钥不同, 我们就无法解密代码为了便于理解, 下面举例说明:
- function getCallStack() {
- var stack = "#", total = 0, fn = arguments.callee;
- while ( (fn = fn.caller) ) {
- stack = stack + "" +fn.name;
- total++
- }
- return stack
- }
- function test1() {
- console.log(getCallStack());
- }
- function test2() {
- test1();
- }
- function test3() {
- test2();
- }
- function test4() {
- test3();
- }
- test4();
执行这段代码时, 将显示字符串#test1test2test3test4 如果修改了某个函数的名称, 则返回的字符串也会随之发生变化所以, 我们可以用这个字符串计算一个安全的哈希值, 稍后用它作为种子来导出用于解密其他代码块的密钥需要注意的是, 如果无法解密下一个代码块 (分析人员一旦修改了函数名, 就会导致密钥无效) 的话, 可以捕获异常并将执行流程重定向到一个虚假路径
请记住, 这个技巧需要结合强大的混淆技术才能发挥作用
小结
在本文中, 我们为读者介绍了与 JavaScript 反调试有关的一些技巧, 其中包括函数重定义断点时间差异 DevTools 检测和执行流程完整性的隐式控制等, 更多的技巧, 我们将在下篇中介绍
本文转载自嘶吼
阿里聚安全
阿里聚安全 (http://jaq.alibaba.com) 由阿里巴巴安全部出品, 面向企业和开发者提供互联网业务安全解决方案, 全面覆盖移动安全数据风控内容安全实人认证等维度, 并在业界率先提出以业务为中心的安全, 赋能生态, 与行业共享阿里巴巴集团多年沉淀的专业安全能力
来源: https://jaq.alibaba.com/community/art/show?articleid=1512