Source Map Deobfuscation:如何自动化地从压缩/混淆代码中还原原始代码,并处理多级 Source Map?

各位观众老爷们,晚上好!我是你们的老朋友,Bug猎手小智。今天给大家带来一场“Source Map Deobfuscation:从压缩/混淆代码中抽丝剥茧”的脱口秀…哦不,是技术讲座!

相信大家都有过这样的经历:打开控制台,想看看某个JS库的源码,结果发现全是些a、b、c、d之类的变量名,还有一堆你根本看不懂的符号,简直像外星语一样。这都是代码压缩和混淆搞的鬼!

但是别怕,有了Source Map,我们就能像福尔摩斯一样,还原代码的真相!今天我们就来聊聊如何自动化地利用Source Map,从这些乱码中提取出原始代码,甚至还能处理多级Source Map的嵌套!

一、 Source Map:代码的“藏宝图”

首先,我们要搞清楚Source Map到底是什么东西。简单来说,它就是一个JSON文件,里面记录了压缩/混淆后的代码和原始代码之间的映射关系。就像一张藏宝图,指引你找到宝藏(原始代码)。

Source Map主要包含以下信息:

  • version: Source Map的版本号。
  • file: 压缩/混淆后的文件名。
  • sourceRoot: 原始代码的根目录。
  • sources: 原始代码的文件列表。
  • names: 原始代码中使用的变量名和函数名列表。
  • mappings: 最核心的部分,记录了压缩/混淆后的代码位置和原始代码位置的映射关系。

mappings 才是重中之重! 它使用 VLQ (Variable Length Quantity) 编码来高效地存储这些位置信息。VLQ 是一种变长编码方式,可以根据数值的大小使用不同数量的字节来表示,从而减少文件大小。

举个栗子,假设我们有如下的原始代码:

function greet(name) {
  return "Hello, " + name + "!";
}

经过压缩后,可能变成这样:

function a(b){return"Hello, "+b+"!"}

对应的Source Map中的mappings部分可能长这样(简化版):

"mappings": "AAAA,SAASA,GAAIC,CAAC,QAAC,UAAUA,CAAI,IAAG"

这个mappings字符串看起来像乱码,但它包含了原始代码中每个字符的位置信息,以及对应的压缩后代码的位置信息。

二、 Source Map Deobfuscation 的基本原理

Source Map Deobfuscation 的过程就是解析 Source Map 文件,然后根据 mappings 中的映射关系,将压缩/混淆后的代码还原成原始代码。

这个过程可以概括为以下几个步骤:

  1. 找到 Source Map 文件: 通常 Source Map 文件的 URL 会以注释的形式嵌入在压缩后的代码中,例如 //# sourceMappingURL=app.js.map。我们需要解析这个注释,获取 Source Map 文件的 URL。
  2. 下载 Source Map 文件: 根据 URL 下载 Source Map 文件。
  3. 解析 Source Map 文件: 将 JSON 格式的 Source Map 文件解析成 JavaScript 对象。
  4. 根据 mappings 还原代码: 遍历压缩后的代码,根据 mappings 中的映射关系,找到对应的原始代码位置,并将其替换成原始代码。

三、 自动化 Source Map Deobfuscation:代码实战

现在,我们来用代码实现一个简单的 Source Map Deobfuscation 工具。这里我们使用 Node.js 和 source-map 库。

首先,安装 source-map 库:

npm install source-map

然后,创建一个名为 deobfuscate.js 的文件,并添加以下代码:

const fs = require('fs');
const sourceMap = require('source-map');

// 1. 从压缩后的代码中找到 Source Map 的 URL
function findSourceMapURL(minifiedCode) {
  const sourceMappingURLRegex = ///# sourceMappingURL=(.*)/;
  const match = minifiedCode.match(sourceMappingURLRegex);
  if (match) {
    return match[1];
  }
  return null;
}

// 2. 下载 Source Map 文件(这里假设 Source Map 文件和压缩后的代码在同一目录下)
function loadSourceMap(sourceMapURL) {
  try {
    const sourceMapContent = fs.readFileSync(sourceMapURL, 'utf-8');
    return JSON.parse(sourceMapContent);
  } catch (error) {
    console.error('Error loading Source Map:', error);
    return null;
  }
}

// 3. 使用 source-map 库进行 Deobfuscation
async function deobfuscateCode(minifiedCode, sourceMapJSON) {
  const consumer = await new sourceMap.SourceMapConsumer(sourceMapJSON);
  let deobfuscatedCode = '';
  const lines = minifiedCode.split('n');

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    for (let j = 0; j < line.length; j++) {
      const originalPosition = consumer.originalPositionFor({
        line: i + 1, // 行号从 1 开始
        column: j // 列号从 0 开始
      });

      if (originalPosition && originalPosition.source) {
        deobfuscatedCode += originalPosition.name || consumer.sourceContentFor(originalPosition.source).charAt(originalPosition.line - 1).charAt(originalPosition.column); // 获取原始字符
        j += (originalPosition.name && originalPosition.name.length) || 0; // 跳过已还原的变量名长度
      } else {
        deobfuscatedCode += line.charAt(j);
      }
    }
    deobfuscatedCode += 'n';
  }

  consumer.destroy(); // 释放资源
  return deobfuscatedCode;
}

// 主函数
async function main(minifiedCodePath) {
  try {
    const minifiedCode = fs.readFileSync(minifiedCodePath, 'utf-8');
    const sourceMapURL = findSourceMapURL(minifiedCode);

    if (!sourceMapURL) {
      console.error('Source Map URL not found in minified code.');
      return;
    }

    const sourceMapJSON = loadSourceMap(sourceMapURL);

    if (!sourceMapJSON) {
      return;
    }

    const deobfuscatedCode = await deobfuscateCode(minifiedCode, sourceMapJSON);
    console.log('Deobfuscated Code:n', deobfuscatedCode);

  } catch (error) {
    console.error('Error:', error);
  }
}

// 获取命令行参数
const minifiedCodePath = process.argv[2];

if (!minifiedCodePath) {
  console.error('Usage: node deobfuscate.js <path_to_minified_code>');
  process.exit(1);
}

main(minifiedCodePath);

代码解释:

  • findSourceMapURL(minifiedCode): 从压缩后的代码中提取 Source Map 文件的 URL。
  • loadSourceMap(sourceMapURL): 读取 Source Map 文件内容并解析为 JSON 对象。
  • deobfuscateCode(minifiedCode, sourceMapJSON): 核心函数,使用 source-map 库的 SourceMapConsumer 解析 Source Map,然后逐行逐字符地遍历压缩后的代码,根据映射关系找到对应的原始代码位置,并将其替换成原始代码。
  • main(minifiedCodePath): 主函数,负责读取压缩后的代码,找到 Source Map URL,加载 Source Map 文件,然后调用 deobfuscateCode 函数进行 Deobfuscation,最后输出还原后的代码。

使用方法:

  1. 准备一个压缩后的 JavaScript 文件 (app.min.js) 和对应的 Source Map 文件 (app.min.js.map),并且确保 Source Map 文件和压缩后的代码在同一目录下。
  2. 在命令行中运行以下命令:

    node deobfuscate.js app.min.js

    程序会将还原后的代码输出到控制台。

四、 多级 Source Map 的处理

有时候,我们遇到的代码可能经过了多次压缩和混淆,这意味着会存在多级 Source Map 嵌套的情况。例如:

  • app.js -> app.min.js (第一级压缩)
  • app.min.js -> app.min.gz.js (第二级压缩)

这时,app.min.gz.js 对应的 Source Map 文件 (app.min.gz.js.map) 指向的是 app.min.js,而 app.min.js 对应的 Source Map 文件 (app.min.js.map) 指向的才是原始的 app.js

要处理多级 Source Map,我们需要递归地解析 Source Map 文件,直到找到最原始的代码。

修改上面的 deobfuscateCode 函数,使其支持多级 Source Map:

async function deobfuscateCode(minifiedCode, sourceMapJSON) {
  const consumer = await new sourceMap.SourceMapConsumer(sourceMapJSON);
  let deobfuscatedCode = '';
  const lines = minifiedCode.split('n');

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    for (let j = 0; j < line.length; j++) {
      let originalPosition = consumer.originalPositionFor({
        line: i + 1, // 行号从 1 开始
        column: j // 列号从 0 开始
      });

      // 处理多级 Source Map
      if (originalPosition && originalPosition.source && originalPosition.source.endsWith('.map')) {
          const nestedSourceMapURL = originalPosition.source;
          const nestedSourceMapJSON = loadSourceMap(nestedSourceMapURL);

          if (nestedSourceMapJSON) {
            const nestedConsumer = await new sourceMap.SourceMapConsumer(nestedSourceMapJSON);
            originalPosition = nestedConsumer.originalPositionFor({
              line: originalPosition.line,
              column: originalPosition.column
            });
            nestedConsumer.destroy();
          }

      }

      if (originalPosition && originalPosition.source) {
        deobfuscatedCode += originalPosition.name || consumer.sourceContentFor(originalPosition.source).charAt(originalPosition.line - 1).charAt(originalPosition.column); // 获取原始字符
        j += (originalPosition.name && originalPosition.name.length) || 0; // 跳过已还原的变量名长度
      } else {
        deobfuscatedCode += line.charAt(j);
      }
    }
    deobfuscatedCode += 'n';
  }

  consumer.destroy(); // 释放资源
  return deobfuscatedCode;
}

修改说明:

  • originalPositionFor 之后,判断 originalPosition.source 是否以 .map 结尾,如果是,则说明这是一个嵌套的 Source Map 文件。
  • 如果是嵌套的 Source Map 文件,则加载并解析该 Source Map 文件,然后再次调用 originalPositionFor 获取更原始的位置信息。

注意: 上述代码只是一个简单的示例,实际情况可能更复杂,需要根据具体情况进行调整。例如,可能需要处理 Source Map 文件不在同一目录下的情况,或者需要处理 Source Map 文件使用相对路径的情况。

五、 总结与展望

今天,我们一起探索了 Source Map Deobfuscation 的原理和实现方法,并用代码实现了一个简单的自动化工具。我们还讨论了如何处理多级 Source Map 嵌套的情况。

虽然 Source Map Deobfuscation 可以帮助我们还原代码的真相,但是它也存在一些局限性:

  • 依赖于 Source Map 文件: 如果没有 Source Map 文件,或者 Source Map 文件不完整,就无法还原代码。
  • 无法还原所有信息: Source Map 只能还原代码的位置信息,无法还原代码的逻辑结构和注释。
  • 可能存在安全风险: 如果 Source Map 文件泄露,可能会暴露代码的敏感信息。

未来,我们可以进一步研究以下方向:

  • 更智能的 Deobfuscation 算法: 利用机器学习等技术,开发更智能的 Deobfuscation 算法,可以自动识别和处理各种混淆技术。
  • 更完善的 Source Map 生成工具: 开发更完善的 Source Map 生成工具,可以生成更精确、更完整的 Source Map 文件。
  • Source Map 安全性研究: 研究如何保护 Source Map 文件,防止敏感信息泄露。

好了,今天的讲座就到这里。希望大家有所收获,以后再遇到压缩/混淆的代码,不要再害怕了!记住,有了 Source Map,你就是代码界的福尔摩斯! 感谢各位的观看,我们下期再见!

发表回复

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