各位观众老爷们,晚上好!我是你们的老朋友,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
中的映射关系,将压缩/混淆后的代码还原成原始代码。
这个过程可以概括为以下几个步骤:
- 找到 Source Map 文件: 通常 Source Map 文件的 URL 会以注释的形式嵌入在压缩后的代码中,例如
//# sourceMappingURL=app.js.map
。我们需要解析这个注释,获取 Source Map 文件的 URL。 - 下载 Source Map 文件: 根据 URL 下载 Source Map 文件。
- 解析 Source Map 文件: 将 JSON 格式的 Source Map 文件解析成 JavaScript 对象。
- 根据
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,最后输出还原后的代码。
使用方法:
- 准备一个压缩后的 JavaScript 文件 (
app.min.js
) 和对应的 Source Map 文件 (app.min.js.map
),并且确保 Source Map 文件和压缩后的代码在同一目录下。 -
在命令行中运行以下命令:
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,你就是代码界的福尔摩斯! 感谢各位的观看,我们下期再见!