JS `Source Map` `Deobfuscation`:从压缩代码还原原始代码

各位靓仔靓女,欢迎来到今天的“代码还原术:Source Map Deobfuscation解密”讲座!我是你们今天的导游,将带大家一起探索如何将那些让人头大的压缩代码,变回我们熟悉的原始代码。准备好了吗?Let’s roll!

第一站:代码压缩与混淆——为什么要搞事情?

在正式开始解密之前,我们先来聊聊为什么要对代码进行压缩和混淆。简单来说,主要有以下几个目的:

  • 减少文件大小: 压缩可以减少 JavaScript 文件的大小,从而加快页面加载速度,提升用户体验。想象一下,如果你的网站加载速度慢如蜗牛,用户早就跑去竞争对手那里了。
  • 保护代码: 混淆可以使代码更难被理解,增加破解和逆向工程的难度,保护你的知识产权。虽然不能完全阻止,但至少可以提高门槛,让那些心怀不轨的人望而却步。
  • 优化性能: 一些压缩工具还可以优化代码结构,删除不必要的空格和注释,进一步提升性能。

常见的压缩和混淆工具包括:

  • UglifyJS: 一个流行的 JavaScript 压缩工具,可以删除空格、注释,缩短变量名等。
  • Terser: UglifyJS 的一个分支,修复了 UglifyJS 的一些问题,并增加了 ES6+ 的支持。
  • Webpack/Rollup/Parcel: 这些打包工具通常集成了代码压缩和混淆的功能。
  • JavaScript Obfuscator: 一个专业的 JavaScript 混淆工具,提供多种混淆选项,可以有效地保护代码。

第二站:Source Map——迷雾中的灯塔

既然代码被压缩和混淆了,那我们如何调试呢?难道要对着一堆乱码抓耳挠腮?别担心,Source Map 就是来拯救我们的。

Source Map 是一个文本文件,它描述了压缩代码和原始代码之间的映射关系。简单来说,它告诉浏览器或调试工具,压缩代码的哪一部分对应于原始代码的哪一行、哪一列。有了 Source Map,我们就可以像调试原始代码一样调试压缩代码了。

Source Map 文件通常以 .map 结尾,例如 app.min.js.map。它包含以下关键信息:

  • version: Source Map 的版本号。
  • file: 生成的压缩文件名。
  • sourceRoot: 原始代码的根目录。
  • sources: 原始代码的文件列表。
  • names: 原始代码中使用的变量名和函数名列表。
  • mappings: 最重要的部分,描述了压缩代码和原始代码之间的映射关系。

mappings 字段使用 VLQ (Variable-length quantity) 编码,将位置信息压缩成字符串。虽然看起来像一堆乱码,但它包含了所有必要的映射信息。

一个简单的 Source Map 示例:

假设我们有以下原始代码 original.js:

function add(a, b) {
  return a + b;
}

console.log(add(1, 2));

经过压缩后,代码变为 minified.js:

function add(a,b){return a+b}console.log(add(1,2));

对应的 minified.js.map 文件可能如下所示(为了方便阅读,这里进行了格式化):

{
  "version": 3,
  "file": "minified.js",
  "sourceRoot": "",
  "sources": ["original.js"],
  "names": ["add", "a", "b", "console", "log"],
  "mappings": "AAAA,SAASA,GAAGC,CAACC,EAAIC,CAAE,OAAOA,GAAGC,CAAE;AAACC,QAAOC,IAAIH,GAAGC,CAAC,EAAD,CAAI"
}

如何使用 Source Map?

大多数现代浏览器都支持 Source Map。当你打开开发者工具时,如果发现加载了 .map 文件,浏览器会自动使用 Source Map 将压缩代码还原成原始代码,方便你进行调试。

通常,你需要确保以下几点:

  • 你的服务器正确地配置了 MIME 类型,将 .map 文件作为 application/json 提供。
  • 压缩后的 JavaScript 文件包含了指向 Source Map 文件的注释,例如:
//# sourceMappingURL=app.min.js.map

或者,你也可以在 HTTP 响应头中指定 Source Map 文件:

X-SourceMap: app.min.js.map

第三站:Deobfuscation——拨开迷雾见真相

有了 Source Map,我们就可以调试压缩代码了。但是,如果代码经过了混淆,即使有了 Source Map,看到的仍然是一些难以理解的变量名和代码结构。这时候,我们就需要进行 Deobfuscation(反混淆)。

Deobfuscation 的目标是将混淆后的代码还原成更易于理解的形式。这通常涉及到以下几个步骤:

  1. 美化代码: 使用工具将压缩后的代码格式化,使其更易于阅读。
  2. 还原变量名: 如果 Source Map 包含了原始变量名,我们可以使用这些信息将混淆后的变量名替换成原始变量名。
  3. 分析代码结构: 尝试理解混淆后的代码逻辑,并将其转换成更清晰的形式。这可能涉及到重命名函数、提取公共代码、简化表达式等。
  4. 使用 Deobfuscation 工具: 一些专门的 Deobfuscation 工具可以自动执行上述步骤,大大简化反混淆的过程。

Deobfuscation 工具推荐:

  • JavaScript Deobfuscator (Online): 一个在线的 JavaScript 反混淆工具,可以处理多种混淆方式。
  • AST Explorer: 一个在线的抽象语法树 (AST) 分析工具,可以帮助你理解代码结构,并进行代码转换。
  • jsnice: 一个基于机器学习的 JavaScript 反混淆工具,可以自动还原变量名和函数名。

一个 Deobfuscation 示例:

假设我们有以下混淆后的代码:

var _0x4a52 = ["x6cx6fx67", "x68x65x6cx6cx6fx20x77x6fx72x6cx64"];
(function(_0x1e9651, _0x4604c3) {
  var _0x37719d = function(_0x33007c) {
    while (--_0x33007c) {
      _0x1e9651["x70x75x73x68"](_0x1e9651["x73x68x69x66x74"]());
    }
  };
  _0x37719d(++_0x4604c3);
})(_0x4a52, 0xb9);
var _0x3300 = function(_0x1e9651, _0x4604c3) {
  _0x1e9651 = _0x1e9651 - 0x0;
  var _0x37719d = _0x4a52[_0x1e9651];
  return _0x37719d;
};
console[_0x3300("0x0")](_0x3300("0x1"));

这段代码使用了字符串编码和数组混淆,让人难以理解。我们可以使用以下步骤进行反混淆:

  1. 美化代码: 使用代码美化工具,使代码更易于阅读。
  2. 还原字符串: 将字符串编码还原成原始字符串。例如,x6cx6fx67 还原成 log
  3. 还原数组: 分析 _0x4a52 数组的用途,并将其还原成原始变量名。例如,_0x4a52[0] 还原成 log
  4. 重命名变量: 将混淆后的变量名重命名成更易于理解的名称。例如,_0x3300 还原成 getValue

经过反混淆后,代码变为:

var array = ["log", "hello world"];
(function(arr, count) {
  var shiftArray = function(n) {
    while (--n) {
      arr["push"](arr["shift"]());
    }
  };
  shiftArray(++count);
})(array, 0xb9);
var getValue = function(index, count) {
  index = index - 0x0;
  var value = array[index];
  return value;
};
console[getValue("0x0")](getValue("0x1"));

虽然仍然有些冗余,但已经比之前的代码易于理解多了。我们可以进一步简化代码,得到最终结果:

console.log("hello world");

第四站:实战演练——Deobfuscate 一个复杂的例子

让我们来看一个更复杂的例子,并使用工具进行反混淆。

假设我们有以下混淆后的代码:

(function(_0x4d4e1b, _0x498276) {
  var _0x3e73a3 = function(_0x3c2f06) {
    while (--_0x3c2f06) {
      _0x4d4e1b['push'](_0x4d4e1b['shift']());
    }
  };
  _0x3e73a3(++_0x498276);
}(_0x39a8, 0x1a9));
var _0x2321 = function(_0x36a929, _0x18c606) {
  _0x36a929 = _0x36a929 - 0x0;
  var _0x57e67d = _0x39a8[_0x36a929];
  return _0x57e67d;
};
(function() {
  var _0x334695 = function() {
    var _0x42d457 = !![];
    return function(_0x53575a, _0x1c5600) {
      var _0x102147 = _0x42d457 ? function() {
        if (_0x1c5600) {
          var _0x3c689e = _0x1c5600['apply'](_0x53575a, arguments);
          _0x1c5600 = null;
          return _0x3c689e;
        }
      } : function() {};
      _0x42d457 = ![];
      return _0x102147;
    };
  }();
  var _0x3e9163 = _0x334695(this, function() {
    var _0x45460e = function() {};
    var _0x3a71e6 = function() {
      var _0x15f94f;
      try {
        _0x15f94f = Function(_0x2321('0x0') + ');')();
      } catch (_0x51485c) {
        _0x15f94f = window;
      }
      return _0x15f94f;
    };
    var _0x542a9f = _0x3a71e6();
    if (!_0x542a9f[_0x2321('0x1')]) {
      ! function() {}.constructor(_0x2321('0x2'))()[_0x2321('0x3')](_0x2321('0x4'));
    } else {
      _0x542a9f[_0x2321('0x1')] = _0x45460e;
    }
  });
  _0x3e9163();
}());

这段代码使用了多种混淆技术,包括:

  • 数组混淆:使用数组 _0x39a8 存储字符串,并通过 _0x2321 函数访问。
  • 控制流混淆:使用 while 循环和 shift 函数来改变数组的顺序。
  • 自执行函数:使用自执行函数来隐藏代码逻辑。
  • 防调试技术:使用 Function 构造函数和 constructor 属性来检测调试器。

为了反混淆这段代码,我们可以使用以下步骤:

  1. 使用 JavaScript Deobfuscator (Online): 将代码粘贴到 JavaScript Deobfuscator (Online) 中,点击 "Deobfuscate" 按钮。该工具会自动执行一些基本的反混淆操作,例如还原字符串和格式化代码。
  2. 分析代码结构: 使用 AST Explorer 分析代码结构,理解代码逻辑。我们可以看到,这段代码主要包含一个立即执行函数和一个闭包。
  3. 还原数组: 分析 _0x39a8 数组的用途,并将其还原成原始变量名。我们可以通过调试代码或阅读代码来确定数组中每个元素的含义。
  4. 移除防调试代码: 移除用于检测调试器的代码,例如 ! function() {}.constructor(_0x2321('0x2'))()[_0x2321('0x3')](_0x2321('0x4'))
  5. 重命名变量: 将混淆后的变量名重命名成更易于理解的名称。
  6. 简化代码: 移除冗余的代码,例如不必要的闭包和立即执行函数。

经过反混淆后,代码变为:

(function() {
  var preventDebugging = function() {
    var isPrevented = !![];
    return function(context, func) {
      var wrapper = isPrevented ? function() {
        if (func) {
          var result = func.apply(context, arguments);
          func = null;
          return result;
        }
      } : function() {};
      isPrevented = ![];
      return wrapper;
    };
  }();
  var preventDebuggingWrapper = preventDebugging(this, function() {
    var noop = function() {};
    var getGlobal = function() {
      var global;
      try {
        global = Function('return this')();
      } catch (e) {
        global = window;
      }
      return global;
    };
    var global = getGlobal();
    if (!global['console']) {
      ! function() {}.constructor('debugger')()['call']('debugger');
    } else {
      global['console'] = noop;
    }
  });
  preventDebuggingWrapper();
}());

这段代码仍然有些复杂,但已经比之前的代码易于理解多了。我们可以看到,这段代码主要用于防止调试器,通过检测 console 对象是否存在,如果不存在则执行 debugger 语句,如果存在则将 console 对象替换成一个空函数。

第五站:总结与展望

今天,我们一起探索了 Source Map 和 Deobfuscation 的奥秘。我们学习了如何使用 Source Map 调试压缩代码,以及如何使用 Deobfuscation 工具将混淆后的代码还原成更易于理解的形式。

虽然 Deobfuscation 可以帮助我们理解和分析混淆后的代码,但它并不能完全还原原始代码。一些高级的混淆技术,例如控制流扁平化和代码虚拟化,可以使代码难以理解和反混淆。

未来,随着混淆技术的不断发展,Deobfuscation 也将面临更多的挑战。我们需要不断学习和探索新的 Deobfuscation 技术,才能更好地保护和分析代码。

希望今天的讲座对大家有所帮助!记住,代码还原术是一门需要不断练习和探索的艺术。多尝试,多实践,你也能成为一名代码还原大师!

感谢各位的参与,下次再见!

发表回复

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