JS `Reverse Engineering` `Minified/Bundled Code`:识别模块边界与函数入口

各位好,今天咱们来聊聊JS逆向工程里一个挺让人头疼,但也挺有意思的话题:怎么在那些被minify和bundle过的代码里,找到模块的边界和函数的入口。 别看那些代码现在长得像一坨坨乱码,只要找对了方法,咱们也能把它们拆解开来,看看里面到底藏了些什么秘密。

一、 为什么要找模块边界和函数入口?

首先,得明白咱们为啥要费这个劲。简单来说,就是为了理解代码的结构和逻辑。想象一下,你拿到了一本没有章节、没有段落、甚至没有标点符号的书,是不是完全不知道该从哪儿下手?Minified/Bundled 的JS代码,差不多就是这个感觉。

  • 理解代码逻辑: 找到了模块边界和函数入口,就能把代码拆分成一个个小块,然后逐个分析每个模块的功能和函数的作用。这就像把一个复杂的机器拆成一个个零件,然后研究每个零件的工作原理。
  • 定位关键代码: 很多时候,我们逆向一个JS文件,并不是要搞清楚每一行代码,而是要找到其中关键的部分,比如加密算法、数据处理逻辑等等。 找到了函数入口,就更容易定位到这些关键代码。
  • 方便修改和调试: 如果你需要修改或者调试这段代码,找到模块边界和函数入口就更加重要了。这能让你知道哪些代码可以安全地修改,哪些代码需要特别小心。

二、Minification和Bundling:代码变形记

在深入分析之前,先简单了解一下Minification和Bundling 这两个“罪魁祸首”:

  • Minification (压缩): 主要目的是减少文件大小。它会移除代码中的空格、注释,还会把变量名、函数名改成短小的无意义的名字,比如把getUserData改成a,把XMLHttpRequest改成b
  • Bundling (打包): 主要目的是把多个JS文件合并成一个。这样做可以减少HTTP请求,提高页面加载速度。但是,这也意味着原本清晰的模块边界被打破了,所有的代码都揉到了一起。

举个例子:

// 原始代码 (module1.js)
function greet(name) {
  console.log("Hello, " + name + "!");
}

// 原始代码 (module2.js)
function goodbye(name) {
  console.log("Goodbye, " + name + "!");
}

// Bundled and Minified 代码
function a(a){console.log("Hello, "+a+"!")}function b(a){console.log("Goodbye, "+a+"!")}

看到了吧?原始代码还算清晰,但是经过Bundling 和Minification 之后,就变得面目全非了。

三、识别模块边界的常用技巧

在Bundled的代码里,模块边界变得模糊不清,但是,一些模式还是会留下蛛丝马迹。

  1. 查找IIFE (Immediately Invoked Function Expression)

    IIFE 是一种常见的模块封装方式。它长这样:

    (function() {
      // 模块代码
    })();

    或者这样:

    !function() {
      // 模块代码
    }();

    在Bundled 的代码里,IIFE 可能会被压缩,但是它的基本结构还是能看出来的。可以尝试搜索 (function(){ 或者 !function(){,看看能不能找到一些模块的起始位置。

    代码示例:

    !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).MyModule={})}(this,function(e){"use strict";function t(e,t){return Object.prototype.hasOwnProperty.call(e,t)} ... });

    这个例子就是一个典型的UMD (Universal Module Definition) 格式的模块,它使用了IIFE 来封装模块代码,并且兼容多种模块加载方式。

    注意点: IIFE 并不一定代表一个完整的模块,有些代码可能会使用多个IIFE 来组织代码。

  2. 查找exportsmodule.exportsdefine等关键词:

    这些关键词是CommonJS、AMD 等模块化规范中用来导出模块的。 即使代码经过压缩,这些关键词一般也不会被修改。

    • CommonJS (Node.js): 使用module.exports或者 exports来导出模块。
    • AMD (RequireJS): 使用define函数来定义模块。
    • UMD (Universal Module Definition): 兼容多种模块化规范。

    代码示例:

    // CommonJS
    module.exports = {
      myFunction: function() {
        // ...
      }
    };
    
    // AMD
    define(function() {
      return {
        myFunction: function() {
          // ...
        }
      };
    });

    在Bundled 代码中,可能会看到类似这样的代码:

    a.exports={b:function(){...}}; // CommonJS
    define(function(){return{a:function(){...}}}); // AMD

    这些代码片段通常标志着一个模块的结束和另一个模块的开始。

  3. 利用Source Map:

    如果Bundled 代码提供了Source Map 文件,那就太幸运了。Source Map 可以将压缩后的代码映射回原始代码, 让你看到原始的模块结构和变量名。

    使用方法:

    • 在浏览器的开发者工具中,打开Source Map 支持。
    • 刷新页面,开发者工具会自动加载Source Map 文件,并将压缩后的代码还原成原始代码。

    有了Source Map, 识别模块边界就变得非常简单了。

四、识别函数入口的常用技巧

找到了模块边界,接下来就要找函数入口了。 函数入口是指那些被其他模块或者代码调用的函数。 找到函数入口,就能更容易理解代码的执行流程。

  1. 查找全局变量和函数:

    在JavaScript 中,全局变量和函数可以直接被其他代码访问。 因此,它们通常是程序的入口点。

    识别方法:

    • 在代码中搜索window. 或者 global. (在Node.js 环境下)。
    • 查找没有使用varletconst 声明的变量和函数。

    代码示例:

    // 全局变量
    myGlobalVariable = 123;
    
    // 全局函数
    function myGlobalFunction() {
      // ...
    }

    在Bundled 代码中,全局变量和函数可能会被重命名,但是它们的作用域仍然是全局的。

  2. 查找事件监听器:

    事件监听器是响应用户操作或者系统事件的函数。它们通常是程序的入口点。

    识别方法:

    • 搜索addEventListeneronclickonload 等关键词。
    • 查找与DOM 元素相关的代码。

    代码示例:

    // 添加事件监听器
    document.getElementById("myButton").addEventListener("click", function() {
      // ...
    });
    
    // onclick 属性
    <button onclick="myFunction()">Click me</button>

    在Bundled 代码中,事件监听器可能会被压缩,但是它们的绑定关系仍然存在。

  3. 查找定时器:

    定时器可以周期性地执行代码。它们也可以被认为是程序的入口点。

    识别方法:

    • 搜索setTimeoutsetInterval 等关键词。

    代码示例:

    // 设置定时器
    setTimeout(function() {
      // ...
    }, 1000);

    在Bundled 代码中,定时器可能会被压缩,但是它们的功能仍然不变。

  4. 分析调用栈:

    在浏览器的开发者工具中,可以使用调用栈来跟踪函数的调用关系。 从一个已知的函数入口点开始,可以沿着调用栈向上或者向下追踪,找到其他的函数入口点。

    使用方法:

    • 在开发者工具中,设置断点。
    • 当代码执行到断点时,查看调用栈。

五、实战案例:分析一个简单的Bundled 代码

假设我们有这样一段Bundled 代码:

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t){console.log("Hello from module 0");var n=require("./module1");n.sayHello("World")},function(e,t,n){e.exports.sayHello=function(e){console.log("Hello, "+e+" from module 1")}}]);

这段代码看起来很吓人,但是我们可以一步一步地分析它。

  1. 识别模块边界:

    • 首先,我们可以看到一个IIFE: !function(e){ ... }([ ... ]); 这说明整个代码被封装在一个大的IIFE 里面。
    • 然后,我们可以看到一个数组 [function(e,t){ ... },function(e,t,n){ ... }]。 这很可能是一个模块列表,每个元素对应一个模块。
  2. 识别函数入口:

    • 在第一个模块中,我们可以看到 console.log("Hello from module 0");n.sayHello("World")。 这说明第一个模块的入口点是这两行代码。
    • 在第二个模块中,我们可以看到 e.exports.sayHello=function(e){ ... }。 这说明第二个模块导出了一个名为sayHello的函数。
  3. 理解代码逻辑:

    • 整个代码使用了一个简单的模块加载器。
    • 第一个模块打印一条消息,然后调用第二个模块导出的sayHello函数。
    • 第二个模块定义了一个sayHello函数,用于打印一条带有问候语的消息。

六、工具推荐

  • 浏览器的开发者工具: Chrome DevTools、Firefox Developer Tools 等。
  • 代码格式化工具: Prettier、js-beautify 等。
  • 反混淆工具: JavaScript Deobfuscator 等。
  • 在线分析工具: AST Explorer 等。

七、总结

在Minified/Bundled 的JS 代码中,识别模块边界和函数入口是一个挑战,但也是一个很有价值的过程。 掌握一些常用的技巧和工具,可以帮助我们更好地理解代码的结构和逻辑, 从而更好地进行逆向工程、调试和修改。

希望今天的分享能对大家有所帮助。 记住,逆向工程是一门需要耐心和技巧的艺术, 只有不断地学习和实践,才能成为真正的JS逆向高手。 咱们下次再见!

发表回复

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