动态加载/执行代码 (eval, new Function, script 标签注入) 在混淆中的作用?如何追踪其加载的真实代码?

各位观众老爷,晚上好!我是你们的老朋友,今晚咱们来聊聊一个略带神秘,却又无处不在的话题:动态代码执行在代码混淆中的作用,以及如何像福尔摩斯一样追踪到它们背后隐藏的真实代码。

开场白:动态代码,混淆的得力助手

话说,代码混淆这玩意儿,就像给代码穿上了一层又一层的迷彩服,让人难以一眼看穿它的真实意图。而动态代码执行,比如 evalnew Function<script> 标签注入,就像是混淆工具箱里的秘密武器,能让迷彩服更加复杂,更加难以破解。

为什么这么说呢?因为动态代码执行可以在运行时生成、修改甚至执行代码,这打破了静态分析的局限性。静态分析就像是拿着一张地图找宝藏,而动态代码执行就像是在寻宝过程中突然有人把地图给改了,甚至告诉你宝藏根本不在地图上!

第一幕:动态代码执行的三剑客

咱们先来认识一下动态代码执行的三位主角:

  1. eval():老牌劲旅,简单粗暴

    eval() 函数可以将一个字符串作为 JavaScript 代码执行。

    let code = "console.log('Hello from eval!');";
    eval(code); // 输出: Hello from eval!

    优点: 简单易用,直接执行字符串。

    缺点: 安全风险高,容易受到恶意代码注入攻击。性能也相对较差,因为它需要在运行时编译代码。
    混淆方式:可以将复杂的代码逻辑拆分成字符串片段,然后通过 eval() 拼接并执行。

  2. new Function():构造函数,灵活多变

    new Function() 可以创建一个新的函数,它的参数是字符串形式的函数参数和函数体。

    let myFunction = new Function('a', 'b', 'return a + b;');
    let result = myFunction(2, 3);
    console.log(result); // 输出: 5

    优点: 可以动态创建函数,比 eval() 稍微安全一些,因为它可以控制函数的参数。

    缺点: 仍然存在安全风险,并且性能也不如直接定义的函数。
    混淆方式:可以动态生成复杂的函数逻辑,增加代码的逆向难度。

  3. <script> 标签注入:瞒天过海,暗度陈仓

    通过动态创建 <script> 标签,并将代码添加到标签的 src 属性或 textContent 属性中,可以加载和执行外部或内联的 JavaScript 代码。

    let script = document.createElement('script');
    script.textContent = "console.log('Hello from script tag!');";
    document.body.appendChild(script); // 输出: Hello from script tag!

    或者加载外部脚本:

    let script = document.createElement('script');
    script.src = "https://example.com/evil.js"; //加载一个恶意脚本
    document.body.appendChild(script);

    优点: 可以加载外部资源,方便代码模块化。

    缺点: 容易受到跨站脚本攻击 (XSS),安全性较低。
    混淆方式:可以将关键代码放在外部文件中,然后通过动态 <script> 标签加载,增加代码的隐藏性。

第二幕:混淆与动态代码的共舞

动态代码执行之所以能成为混淆的得力助手,是因为它可以实现以下几种混淆技巧:

  • 字符串拼接与解密: 将代码分割成多个字符串片段,然后使用动态代码执行将它们拼接起来。或者将代码进行加密,然后在运行时解密并执行。

    let part1 = "console.";
    let part2 = "log('";
    let part3 = "Hello, ";
    let part4 = "world!";
    let part5 = "');";
    
    eval(part1 + part2 + part3 + part4 + part5); // 输出: Hello, world!
    
    // 加密的代码
    let encryptedCode = "Uryyb, jbeyq!"; // ROT13 加密
    let decryptionKey = 13;
    
    function decrypt(text, key) {
      let result = "";
      for (let i = 0; i < text.length; i++) {
        let charCode = text.charCodeAt(i);
        if (charCode >= 65 && charCode <= 90) { // 大写字母
          result += String.fromCharCode(((charCode - 65 - key + 26) % 26) + 65);
        } else if (charCode >= 97 && charCode <= 122) { // 小写字母
          result += String.fromCharCode(((charCode - 97 - key + 26) % 26) + 97);
        } else {
          result += text[i];
        }
      }
      return result;
    }
    
    let decryptedCode = decrypt(encryptedCode, decryptionKey);
    eval("console.log('" + decryptedCode + "')"); // 输出: Hello, world!
  • 代码变形与重构: 将代码进行变形,比如改变变量名、函数名、代码结构等,然后使用动态代码执行将变形后的代码还原成可执行的代码。

    let obfuscatedCode = "var _0xabc=['log','console'];(function(_0x234,_0x567){var _0xdef=function(_0x890){while(--_0x890){_0x234['push'](_0x234['shift']());}};_0xdef(++_0x567);}(_0xabc,0x123));var _0x456=function(_0x9ab,_0xdef){_0x9ab=_0x9ab-0x0;var _0x123=_0xabc[_0x9ab];return _0x123;};_0x567[_0x456('0x0')](_0x456('0x1'));";
    eval(obfuscatedCode); // 输出: console
  • 控制流平坦化: 将代码的控制流打乱,然后使用动态代码执行根据特定的条件来执行不同的代码块。

    这种混淆方式会让代码的逻辑变得非常复杂,难以理解。通常会结合状态机和 switch 语句来实现。

  • 动态加载模块: 将代码分割成多个模块,然后使用动态 <script> 标签或 import() 函数动态加载这些模块。

第三幕:追踪动态代码的真相

面对这些狡猾的混淆技巧,我们该如何追踪到动态代码背后隐藏的真实代码呢?这里有一些常用的方法:

  1. 静态分析 + 猜测:

    • 寻找关键字符串: 比如 evalnew Functiondocument.createElement('script') 等关键字。
    • 分析代码结构: 尝试理解代码的整体逻辑,找出动态代码执行的位置。
    • 猜测代码意图: 根据代码的上下文,猜测动态代码执行的目的是什么。
    • 代码美化: 使用工具将混淆后的代码进行格式化,使其更易于阅读。

    局限性: 当混淆程度较高时,静态分析可能会变得非常困难,甚至无法进行。

  2. 动态调试:

    • 使用浏览器开发者工具: 在浏览器开发者工具中设置断点,跟踪代码的执行过程,查看变量的值和函数调用堆栈。
    • Hook 函数: 通过重写 evalnew Function 等函数,记录它们的调用信息和参数,从而了解动态代码执行的内容。
    // Hook eval 函数
    let originalEval = eval;
    eval = function(code) {
      console.log("eval called with code:", code);
      debugger; // 暂停执行,方便调试
      return originalEval(code);
    };
    
    // Hook new Function 函数
    let originalFunction = Function;
    Function = function() {
      let args = Array.from(arguments);
      console.log("new Function called with arguments:", args);
      debugger; // 暂停执行,方便调试
      return new originalFunction(...args);
    };
    
    // Hook document.createElement 函数
    let originalCreateElement = document.createElement;
    document.createElement = function(tagName) {
      let element = originalCreateElement.apply(this, arguments);
      if (tagName === 'script') {
        // 监听 script 标签的属性设置
        Object.defineProperty(element, 'src', {
          set: function(src) {
            console.log('Script tag src set to:', src);
            debugger; // 暂停执行,方便调试
            this.setAttribute('src', src);
          },
          get: function() {
            return this.getAttribute('src');
          }
        });
        Object.defineProperty(element, 'textContent', {
          set: function(textContent) {
            console.log('Script tag textContent set to:', textContent);
            debugger; // 暂停执行,方便调试
            this.textContent = textContent;
          },
          get: function() {
            return this.textContent;
          }
        });
      }
      return element;
    };

    优点: 可以实时观察代码的执行过程,获取动态生成的信息。

    缺点: 需要一定的调试技巧,并且可能会受到反调试技术的干扰。

  3. AST (Abstract Syntax Tree) 分析:

    • 将代码解析成 AST: 使用工具将 JavaScript 代码解析成抽象语法树。
    • 分析 AST 结构: 遍历 AST,查找 evalnew Function 等节点,提取它们的参数和上下文信息。
    • 重构 AST: 对 AST 进行修改,比如将动态生成的代码插入到 AST 中,然后将 AST 转换回 JavaScript 代码。
    const esprima = require('esprima');
    const estraverse = require('estraverse');
    const escodegen = require('escodegen');
    
    let code = "let x = 1; eval('console.log(x + 1)');";
    
    // 解析代码成 AST
    let ast = esprima.parseScript(code);
    
    // 遍历 AST
    estraverse.traverse(ast, {
      enter: function(node) {
        if (node.type === 'CallExpression' && node.callee.name === 'eval') {
          console.log('Found eval call:', node.arguments[0].value);
          // 这里可以对 eval 的参数进行分析和处理
        }
      }
    });
    
    // 将 AST 转换回代码
    let generatedCode = escodegen.generate(ast);
    console.log('Generated code:', generatedCode);

    优点: 可以对代码进行深层次的分析和修改,不受动态执行的限制。

    缺点: 需要一定的 AST 知识,并且实现起来比较复杂。

第四幕:实战演练

咱们来个实战演练,假设我们遇到一段混淆后的代码:

let encryptedCode = "ZnVuY3Rpb24gYWFhKGEpe3JldHVybiBhKzEwfQ==";
let decodedCode = atob(encryptedCode);
let func = new Function("return " + decodedCode)();
console.log(func(5));
  1. 静态分析: 我们发现代码中使用了 atob 函数进行解码,然后使用 new Function 创建了一个函数。

  2. 动态调试: 我们可以使用浏览器开发者工具,在 new Function 的地方设置断点,查看 decodedCode 的值。

    或者使用 Hook 函数的方式:

    let originalFunction = Function;
    Function = function() {
      let args = Array.from(arguments);
      console.log("new Function called with arguments:", args);
      debugger;
      return new originalFunction(...args);
    };
    
    let encryptedCode = "ZnVuY3Rpb24gYWFhKGEpe3JldHVybiBhKzEwfQ==";
    let decodedCode = atob(encryptedCode);
    let func = new Function("return " + decodedCode)();
    console.log(func(5));

    通过调试,我们可以看到 decodedCode 的值是 "function aaa(a){return a+10}"

  3. 理解代码: 我们可以知道这段代码定义了一个函数 aaa,它的作用是将传入的参数加上 10。然后调用这个函数,传入参数 5,最终输出 15。

表格总结:追踪动态代码的利器

方法 优点 缺点 适用场景
静态分析 简单易用,不需要运行代码。 当混淆程度较高时,分析难度大。 适用于简单的混淆代码。
动态调试 可以实时观察代码的执行过程,获取动态生成的信息。 需要一定的调试技巧,可能会受到反调试技术的干扰。 适用于复杂的混淆代码,需要跟踪代码的执行过程。
AST 分析 可以对代码进行深层次的分析和修改,不受动态执行的限制。 需要一定的 AST 知识,实现起来比较复杂。 适用于需要对代码进行自动化分析和修改的场景,比如代码重构、漏洞扫描等。
Hook 函数 可以拦截关键函数的调用,获取函数的参数和返回值,方便调试。 可能会影响代码的正常执行,需要谨慎使用。 适用于需要监控特定函数的调用情况,比如检测恶意代码。

结语:道高一尺,魔高一丈

代码混淆和反混淆就像一场永无止境的猫鼠游戏。混淆技术不断发展,反混淆技术也在不断进步。掌握动态代码执行的原理和追踪方法,能够帮助我们更好地理解混淆后的代码,保护自己的代码安全,也能够更好地分析恶意代码,维护网络安全。

记住,没有绝对安全的代码,只有不断学习和进步的安全意识!

感谢各位的观看,咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注