一、模块系统:CommonJS、ESM、UMD
模块系统的目标:
将代码拆分为独立的逻辑单元(模块),实现封装、复用、依赖管理。
在 Web 前端/Node 中,因为 JavaScript 起初没有模块机制,因此出现了多个模块系统:
-
CommonJS:用于 Node.js
-
ESM:浏览器标准模块系统
-
UMD:兼容 CommonJS + AMD + 浏览器全局,常用于库
1. CommonJS(Node.js 标准)
由 CommonJS 组织提出,是 Node.js 默认模块格式。每个 .js
文件都是一个模块。
核心语法
// math.js
const add = (a, b) => a + b;
module.exports = { add };
// main.js
const math = require('./math');
console.log(math.add(2, 3));
模块机制
特性 | 描述 |
---|---|
加载方式 | 同步加载,适合服务器环境 |
导出 | module.exports 或 exports.xxx |
引入 | require() ,在运行时加载模块 |
缓存机制 | 加载一次后,缓存模块对象 |
文件单位 | 每个 .js 文件就是一个模块 |
缓存机制举例:
require('./a'); // 执行 a.js 里的代码
require('./a'); // 不再执行,只返回上次结果
有助于判断模块初始化逻辑的位置,比如入口代码是否只执行一次。
逆向识别特征
表现形式 | 说明 |
---|---|
require("xxxx") | 模块导入 |
module.exports = {} | 模块导出 |
exports.func = ... | 导出函数/变量 |
__dirname 、__filename | 模块当前路径 |
2. ESM(ECMAScript Module)
ESM 是 ES6 中引入的 JavaScript 原生模块标准,现代浏览器与 Node.js(v14+)都支持。
核心语法
// math.js
export const add = (a, b) => a + b;
export default function sub(a, b) { return a - b; }
// main.js
import { add } from './math.js';
import sub from './math.js';
模块机制
特性 | 描述 |
---|---|
加载方式 | 静态分析 + 异步加载 |
导出 | export / export default |
引入 | import (必须写在顶层) |
支持 Tree-shaking | 未使用的代码会在打包时被移除 |
跨模块引用 | 静态结构清晰,易于优化 |
静态 vs 动态加载
import x from './x.js'; // 静态导入,编译时就知道依赖
const m = await import('./m.js'); // 动态导入,返回 Promise
在混淆还原中,可以通过静态导入结构推断模块依赖。
逆向识别特征
表现形式 | 含义 |
---|---|
import ... from ... | 标准导入语法 |
export const ... | 命名导出 |
export default | 默认导出 |
.mjs 文件 | 明确表示该文件是 ESM 模块 |
3. UMD(Universal Module Definition)
UMD 是一种兼容所有主流模块系统的格式,适用于发布 JS 库(如 jQuery、lodash、crypto-js)。
核心结构
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory); // AMD
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(); // CommonJS
} else {
root.myLib = factory(); // 浏览器全局变量
}
}(this, function () {
return {
sayHi: () => console.log('Hi!')
};
}));
模块机制
特性 | 描述 |
---|---|
自动适配 | 判断当前运行环境自动使用适合的加载方式 |
导出对象 | 返回一个全局对象,挂载在 window 或 global |
多平台支持 | 同时支持浏览器、Node.js、AMD、RequireJS 等 |
多见于库文件 | 如 crypto-js、axios 发布的 umd 文件 |
逆向识别特征
表现形式 | 含义 |
---|---|
typeof module === 'object' && module.exports | CommonJS 检测 |
typeof define === 'function' && define.amd | AMD 检测 |
root.XXX = factory(); | 浏览器全局对象挂载 |
如果在逆向某个库函数(比如混淆的加密函数),看到这种结构,那几乎可以断定是一个通用的 UMD 打包库。
总结
对比项 | CommonJS | ESM | UMD |
---|---|---|---|
加载方式 | 同步 | 异步(静态) | 自适应 |
使用平台 | Node.js | 浏览器 + Node.js | 浏览器、Node、RequireJS |
导出方式 | module.exports | export / export default | root.xx = factory() 等 |
Tree-shaking | 不支持 | 支持 | 不支持 |
缓存机制 | 有 | 有 | 有 |
是否支持动态导入 | 是(require) | 是(import() ) | 否 |
二、import/export
传统 JS(使用 <script>
标签)的问题:
-
全局变量污染
-
模块依赖混乱
-
无法静态分析依赖关系
-
不支持按需加载优化(tree-shaking)
ES6 模块系统引入 export/import
,解决了以上问题:
-
每个 JS 文件就是一个模块(默认独立作用域)
-
export
明确模块对外暴露的内容 -
import
明确模块依赖,且是静态结构,可优化、分析
1. export
导出语法详解
1)命名导出(Named Export)
每个模块可以导出多个内容:
// utils.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
export const PI = 3.14;
导入时用花括号:
import { add, sub } from './utils.js';
可以重命名:
import { add as addFunc } from './utils.js';
2)默认导出(Default Export)
每个模块只能有一个默认导出,常用于导出单个功能或类:
// logger.js
export default function log(msg) {
console.log('Log:', msg);
}
导入时无需花括号,名字可随意:
import log from './logger.js';
默认导出可以是函数、类、对象、值:
export default {
name: 'module',
version: 1
};
3)混合使用(命名 + 默认)
export const name = 'abc';
export default function main() {}
导入方式:
import main, { name } from './mod.js';
2. import
导入语法详解
导入命名导出
import { a, b } from './mod.js';
导入默认导出
import anyName from './mod.js';
导入所有(命名空间方式)
import * as utils from './utils.js';
utils.add(1, 2);
动态导入(异步 import)
const mod = await import('./mod.js');
mod.fn();
动态导入常用于懒加载、条件加载、前端按需打包(如 Webpack code splitting)。
3. 模块的执行与缓存机制
-
每个模块 只执行一次
-
多次
import
实际复用缓存(单例引用) -
模块中定义的变量、状态会被共享
例子:
// counter.js
let count = 0;
export function increment() {
count++;
console.log(count);
}
多次导入使用 increment()
,会打印递增数字,说明是同一个模块实例。
总结
// mod.js
export const a = 1;
export default function b() {}
// main.js
import b, { a } from './mod.js';
一个模块 = 一个作用域单元
export
定义接口 → import
引入依赖
多次 import / 执行一次 / 缓存共享
三、require
在 Node.js 中,每个 .js
文件都被视为一个模块,模块内部通过 module.exports
导出内容,其他模块通过 require()
导入使用。
1. 基本语法与例子
// math.js
const add = (a, b) => a + b;
module.exports = { add };
// app.js
const math = require('./math.js');
console.log(math.add(1, 2)); // 输出:3
注意:require
是同步、运行时加载的。
2. 模块导出:module.exports
vs exports
// 正确用法
module.exports = {
name: 'hello'
};
// 另一种写法(推荐只用一种)
exports.name = 'hello'; // 等价于 module.exports.name = ...
// 错误用法
exports = { name: 'lost' }; // 此时 exports 与 module.exports 脱钩
exports
只是 module.exports
的引用。如果重新赋值,会失效。
3. 模块加载机制
require('模块名')
加载路径规则:
-
核心模块(如
fs
、path
) -
自定义模块(相对/绝对路径)
-
第三方模块(从
node_modules
向上查找)
4. 模块缓存机制
每个模块在第一次被 require()
时会被执行并缓存,后续 require
返回的是缓存对象,不会重复执行。
示例:
// a.js
console.log('a 加载了');
module.exports = { count: 0 };
// b.js
const a = require('./a');
a.count++;
console.log('b:', a.count);
// c.js
const a = require('./a');
console.log('c:', a.count);
输出顺序为:
a 加载了
b: 1
c: 1
说明 a.js
只执行一次,模块状态共享。
5. 清除缓存(热更新、绕过防护)
const path = require.resolve('./a.js');
delete require.cache[path];
或者:
delete require.cache[require.resolve('./a.js')];
再次 require
将重新加载并执行模块。
require.resolve('./a.js')
和 require('./a.js')
的区别:
方法 | 作用 |
---|---|
require('./a.js') | 加载并执行模块,返回导出的内容 |
require.resolve('./a.js') | 只返回路径字符串,不加载模块本身 |
6. require 的高级用法
1)动态路径(运行时字符串)
const lang = 'zh';
const messages = require(`./lang/${lang}.js`);
这种写法常见于国际化、本地化、配置分离。
2)条件加载
if (process.env.NODE_ENV === 'dev') {
require('./mock-server.js');
}
这种方式是 CommonJS 的优势,ESM 模块不允许这样写。
3)只执行模块副作用(无导出)
// monitor.js
console.log('开启监控');
// main.js
require('./monitor.js'); // 只为执行副作用
总结
特性 | 描述 |
---|---|
模块系统 | CommonJS |
加载方式 | 同步、运行时 |
是否支持动态路径 | 是 |
是否缓存 | 是,require.cache |
导出方式 | module.exports 或 exports |
是否能清除缓存 | 可以手动清除 |
是否支持顶层 await | 不支持 |
四、模块缓存机制
模块缓存(Module Cache) 是指:
一个模块在第一次通过 require()
或 import
被加载时,系统会将其“执行结果”缓存起来,后续再次加载该模块时,不会再次执行代码,而是直接返回缓存结果。
这保证了模块是“单例”的,并且执行效率更高。
1. Node.js 中 CommonJS 的缓存机制
模块加载流程:
-
首次
require()
:-
解析路径
-
读取文件内容
-
包裹成函数并执行
-
缓存导出的
module.exports
-
-
再次
require()
:-
直接返回缓存的
module.exports
对象
-
示例代码:
// counter.js
let count = 0;
module.exports = {
add: () => ++count,
};
// a.js
const counter = require('./counter');
console.log('A:', counter.add()); // 输出 A: 1
// b.js
const counter = require('./counter');
console.log('B:', counter.add()); // 输出 B: 2
即使 a.js
和 b.js
分别加载,只会执行一次 counter.js
,并共享其导出对象。
2. 缓存存储位置:require.cache
Node.js 的模块缓存实际是一个对象:
console.log(require.cache);
结构类似:
{
'/path/to/counter.js': {
id: '/path/to/counter.js',
filename: '/path/to/counter.js',
loaded: true,
exports: {...},
children: [...],
...
}
}
可以:
-
查看缓存:
Object.keys(require.cache)
-
清除缓存:
delete require.cache[require.resolve('./counter')]
3. 清除缓存机制
方法 1:删除 require.cache
项
delete require.cache[require.resolve('./counter')];
下次再 require('./counter')
,会重新加载并执行模块。
方法 2:热更新模块(开发工具使用)
开发服务器(如 nodemon
)就是检测文件变化后清除缓存并重新加载模块。
4. 模块多次加载行为图解
首次 require('./a') ➜ 加载并缓存
第二次 require('./a') ➜ 直接返回缓存
删除缓存后 require('./a') ➜ 重新加载
这就是 模块单例性(singleton) 和缓存机制的来源。
5. 缓存导致的副作用
1)模块状态共享(计数器、连接池、缓存等)
// database.js
let conn = null;
module.exports = {
connect: () => {
if (!conn) conn = createConnection();
return conn;
}
};
多个模块调用 require('./database')
,会复用同一个连接。
2)某些模块只执行一次副作用
// logger.js
console.log('Logger initialized');
不管被多少文件 require()
,只打印一次。
6. 与 ESModule(ESM)的缓存机制对比
ESM 也缓存模块,但:
特性 | CommonJS | ESM |
---|---|---|
缓存机制 | 是 | 是 |
可清除缓存 | 手动清除 | 不可(静态结构) |
影响变量共享 | 是 | 是 |
是否单例 | 是 | 是 |
ESModule 的缓存机制更严格、不可清除、更适合编译优化(如 Tree-shaking)
7. 逆向与安全分析场景中常见用法
1)Webpack 模块缓存结构
打包后的 Webpack 也有缓存机制(变种):
// 内部使用 __webpack_module_cache__ 缓存模块
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
...
}
在分析 Webpack 产物时,要特别关注这个缓存对象,便于定位真实模块和还原逻辑。
2)绕过缓存注入 Payload
可以通过以下手段实现注入:
// 修改 module.exports 中的方法
require('./target').login = function() {
console.log('Hooked!');
};
或直接替换整个模块:
require.cache[require.resolve('./target')].exports = fakeModule;
总结
点 | 内容 |
---|---|
缓存位置 | require.cache |
缓存对象 | module.exports 返回值 |
缓存作用 | 避免重复加载、提高性能 |
清除方式 | delete require.cache[...] |
注意事项 | 会导致模块共享状态(副作用) |
五、使用 Babel 转译源码
Babel 是一个 JavaScript 编译器,核心作用是:
功能 | 描述 |
---|---|
转译 | 把 ES6+ / JSX / TypeScript 转为 ES5 |
分析 | 生成、遍历、修改 AST(抽象语法树) |
插件系统 | 支持自定义插件操作 AST |
逆向用途 | 可用于提取、重构混淆代码 |
Babel 转译的三个阶段
Babel 的整体流程是:
源码(string)
→ Parse(转成 AST)
→ Transform(操作 AST)
→ Generate(再生成 JS)
环境搭建:核心依赖
需要安装以下 npm 包:
npm install @babel/core @babel/parser @babel/traverse @babel/generator @babel/types
这些库的作用如下:
包名 | 用途 |
---|---|
@babel/core | Babel 核心引擎 |
@babel/parser | 将源码转成 AST |
@babel/traverse | 遍历/修改 AST |
@babel/types | 判断/构建 AST 节点 |
@babel/generator | AST 转回代码字符串 |
1. 实战流程:转译并操作 JS 源码
1)读取源码
const fs = require("fs");
const code = fs.readFileSync("input.js", "utf-8");
2)解析为 AST
const parser = require("@babel/parser");
const ast = parser.parse(code, {
sourceType: "module", // 可选:script | module
});
3)遍历并修改 AST
const traverse = require("@babel/traverse").default;
traverse(ast, {
Identifier(path) {
if (path.node.name === "_0xabc123") {
path.node.name = "decryptedData";
}
},
});
4)AST 生成新代码
const generate = require("@babel/generator").default;
const output = generate(ast, {}, code);
fs.writeFileSync("output.js", output.code);
2. 逆向实战:混淆变量改名
输入代码:
const _0x12a3f = 1 + 2;
console.log(_0x12a3f);
操作 AST:
traverse(ast, {
Identifier(path) {
if (path.node.name === "_0x12a3f") {
path.node.name = "sum";
}
},
});
输出结果:
const sum = 1 + 2;
console.log(sum);
3. 结合 Babel Types 进行节点构造
也可以手动构造一个 AST 节点:
const t = require("@babel/types");
const newVar = t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier("injectedVar"),
t.stringLiteral("hacked!")
),
]);
path.insertBefore(newVar);
这段代码会向目标节点前插入:
const injectedVar = "hacked!";
总结
阶段 | 工具 | 作用 |
---|---|---|
解析(parse) | @babel/parser | 源码 ➝ AST |
遍历(traverse) | @babel/traverse | 读/改 AST 节点 |
判断构造 | @babel/types | 判断类型 / 构造新节点 |
生成(generate) | @babel/generator | AST ➝ JS 源码 |
输出文件 | fs.writeFileSync | 写出修改结果 |
六、生成 AST 分析工具
AST(抽象语法树)分析工具 = 使用 Babel 将 JS 源码转换成结构化的树状结构,供我们进行以下操作:
功能 | 示例 |
---|---|
结构还原 | 混淆变量名一键改回 a, b, c |
函数提取 | 找出所有包含 CryptoJS 的函数 |
参数还原 | 替换复杂表达式为简单值 |
注入调试 | 自动加 console.log() |
依赖安装
npm install @babel/core @babel/parser @babel/traverse @babel/types @babel/generator
1. AST 分析工具目录结构
ast-tool/
├── input.js # 待分析的混淆源码
├── output.js # 处理后的输出代码
├── index.js # 分析主程序
└── rename-map.json # 存储改名映射(可选)
2. 核心流程 = 四步法
1 读取源码 → 2 解析 AST → 3 遍历修改 → 4 输出新代码
// index.js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
const t = require("@babel/types");
// 1. 读取原始源码
const code = fs.readFileSync("input.js", "utf-8");
// 2. 生成 AST
const ast = parser.parse(code, {
sourceType: "unambiguous", // 自动识别是 script 还是 module
});
// 3. 遍历并操作 AST
traverse(ast, {
Identifier(path) {
if (path.node.name === "_0x12ab") {
path.node.name = "decodedStr";
}
},
CallExpression(path) {
if (
t.isIdentifier(path.node.callee) &&
path.node.callee.name === "eval"
) {
path.replaceWith(t.stringLiteral("eval removed"));
}
},
});
// 4. 生成新的源码并写入
const output = generator(ast, {}, code);
fs.writeFileSync("output.js", output.code);
3. 常见操作合集
1)遍历函数定义(提取加密函数)
traverse(ast, {
FunctionDeclaration(path) {
const name = path.node.id.name;
if (path.toString().includes("btoa") || path.toString().includes("CryptoJS")) {
console.log("找到加密函数:", name);
}
}
});
2)遍历字符串字面量(提取混淆密文)
traverse(ast, {
StringLiteral(path) {
console.log("字符串:", path.node.value);
}
});
3)自动注入 console.log
traverse(ast, {
FunctionDeclaration(path) {
const logStmt = t.expressionStatement(
t.callExpression(t.identifier("console.log"), [
t.stringLiteral("进入函数:" + path.node.id.name),
])
);
path.get("body").unshiftContainer("body", logStmt);
}
});
4)记录并保存重命名映射
const renameMap = {};
let counter = 0;
traverse(ast, {
Identifier(path) {
if (/^_0x/.test(path.node.name)) {
const newName = "var" + counter++;
renameMap[path.node.name] = newName;
path.node.name = newName;
}
}
});
fs.writeFileSync("rename-map.json", JSON.stringify(renameMap, null, 2));
总结
用途 | 技术实现 |
---|---|
解混淆 | 遍历 Identifier 重命名 |
提取加密函数 | 查找 FunctionDeclaration 含 btoa 、CryptoJS |
提取字符串 | 遍历 StringLiteral |
替换表达式 | 替换某个节点为固定返回值 |
注入日志 | path.insertBefore() 或 path.get("body").unshiftContainer(...) |
七、Webpack 打包结构识别
Webpack 是当前网页常见的构建工具之一,使用它打包后的 JavaScript 文件具有以下特点:
特性 | 描述 |
---|---|
模块化封装 | 所有模块合并为一个大函数 |
模块索引化 | 模块用数字或混淆变量标识,如 0xabc123 |
闭包封装 | 整个包被包装为自执行函数 |
隐藏真实函数名 | 所有函数变量名被重命名为无意义的标识符 |
通过 __webpack_require__ 加载模块 | 模拟 CommonJS 的 require() |
1. 常见 Webpack 打包结构(3 种核心模式)
1)IIFE 自执行结构(所有模块被包装)
(function(modules) {
function __webpack_require__(moduleId) {
// 模块缓存与执行
}
return __webpack_require__(0);
})({
0: function(module, exports, __webpack_require__) {
// 主模块代码
},
1: function(module, exports) {
// 其它模块
}
});
识别点:
-
自执行匿名函数
-
内部存在
__webpack_require__
-
参数是一个对象或数组,模块编号为 0、1、2...
-
模块实现是
function(module, exports, ...) {}
2)数组结构(简化混淆模式)
(function(modules) {
// modules 是数组
})([
function(module, exports) { console.log("main"); },
function(module, exports) { console.log("sub"); }
]);
识别点:
-
参数是一个数组,每个模块是数组中的一个函数
-
常用于小项目或极简打包
3)eval 模式(加速构建,调试模式常见)
eval("module.exports = 'hello';\n//# sourceURL=webpack://app/./src/index.js?");
识别点:
-
eval(...)
中是模块代码字符串 -
sourceURL=webpack://...
是调试信息 -
通常用于
devtool: 'eval'
模式,方便调试
2. Webpack 核心结构拆解图(常见标准打包)
(function(modules) { // modules 是一个数组,保存所有模块函数
// 模块缓存
var installedModules = {}; // 用来缓存已经加载的模块,防止重复执行
// 模拟 require 函数
function __webpack_require__(moduleId) { // 自定义的模块加载函数,参数是模块编号
if (installedModules[moduleId]) { // 如果该模块已经缓存过了
return installedModules[moduleId].exports; // 直接返回该模块的导出结果
}
var module = installedModules[moduleId] = { // 创建一个新的模块对象
exports: {} // 初始导出对象为空
};
// 执行模块函数
// 把 module, exports, 和 __webpack_require__ 传入模块函数
// 相当于 CommonJS 中的 (module, exports, require)
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports; // 返回该模块的导出对象
}
// 加载入口模块
return __webpack_require__(0); // 启动程序,从模块 ID 为 0 的模块开始执行
})([ // 模块数组,每个模块是一个函数,对应一个模块文件
// 模块 0(主入口模块)
function(module, exports, __webpack_require__) {
const helper = __webpack_require__(1); // 加载模块 1 的导出内容
console.log(helper()); // 执行模块 1 返回的函数并打印返回值
},
// 模块 1(被导入模块)
function(module, exports) {
module.exports = function() { // 导出一个函数
return "helper result"; // 这个函数返回一个字符串
};
}
]);
3. 如何逆向分析 Webpack 打包代码?
步骤一:找到 Webpack 包装函数入口
-
搜索关键词:
__webpack_require__
-
或搜索自执行函数
(function(modules)
或(function(){...})({...})
步骤二:提取模块数组/对象
-
将 modules 拆成一个个子模块
-
如果是数组,可用索引访问
-
如果是对象,可用数字/字符串 key
const modules = {
0: function (module, exports, __webpack_require__) { ... },
1: function (module, exports) { ... }
};
步骤三:寻找主模块
通常是 __webpack_require__(0)
或类似写法
return __webpack_require__(0);
找到模块 0
的实现,即为主入口。
步骤四:定位关键加密逻辑模块
可按如下思路定位关键逻辑模块:
技术 | 操作 |
---|---|
关键词匹配 | 在所有模块中搜索 "CryptoJS"、"md5"、"AES"、"btoa" 等关键字 |
hook __webpack_require__ | 打印加载顺序,定位哪些模块被频繁加载 |
使用 Babel AST | 分析各模块函数结构,找加密点或 WebSocket 通信逻辑 |
4. 结合 Babel 工具自动拆包 + 重构
示例:识别所有模块并重命名变量
traverse(ast, { // 遍历整个 AST 抽象语法树
CallExpression(path) { // 当遍历到调用表达式 (函数调用) 时触发
const callee = path.node.callee; // 获取调用的函数名部分(callee = 被调用的函数)
if (t.isIdentifier(callee) && callee.name === "__webpack_require__") {
// 如果 callee 是一个标识符,并且名字是 "__webpack_require__"
// 说明这是 webpack 打包后调用模块的语句
console.log("发现模块调用:", path.toString());
// 打印出当前调用语句的源码形式,比如:__webpack_require__(134)
}
}
});
总结
特征 | 说明 |
---|---|
自执行函数结构 | (function(modules){...})([...]) |
__webpack_require__ 函数存在 | 标志着模块加载机制 |
模块数组 / 对象 | 所有业务逻辑封装为函数 |
模块编号 | 模块索引常是数字或 _0xabc123 形式混淆字符串 |
主模块加载入口 | 一般是 __webpack_require__(0) ,可定位逻辑起点 |
八、sourceMap 的作用与反调试方法识别
概念:
sourceMap
是一种映射文件,用来将压缩/混淆后的代码映射回原始源码。
作用:
-
帮助开发者调试混淆压缩后的代码
-
允许浏览器控制台显示源代码位置
-
支持断点调试和还原变量名/函数名
1. sourceMap 结构详解
通常是一个 .map
文件,如 bundle.js.map
,是一个 JSON 文件,包含以下字段:
{
"version": 3,
"file": "bundle.js",
"sources": ["webpack:///src/index.js"],
"names": ["add", "a", "b"],
"mappings": "AAAA,IAAIA..."
}
字段 | 含义 |
---|---|
version | sourceMap 版本 |
file | 对应的打包输出文件 |
sources | 原始源文件路径 |
names | 源码中的变量名列表 |
mappings | 压缩代码到源码的具体位置映射 |
2. sourceMap 的作用总结
作用 | 举例 |
---|---|
恢复可读源码 | a=function(x){return x*x} → function square(x) { return x * x } |
浏览器调试支持 | Chrome 可直接点进原始文件如 src/index.js |
JS 逆向分析辅助 | 可定位真实加密逻辑、函数名、调用链 |
调试时插桩 | 可在原始位置插入 console.log 、hook |
3. sourceMap 常见获取方式
方式 1:页面中引用了 .map
<script src="app.js"></script>
<!-- HTML 源码中包含 -->
<!-- 浏览器会自动下载 app.js.map -->
方式 2:JS 文件末尾提示
//# sourceMappingURL=app.js.map
可以在 Chrome 的 Source 面板中看到带 /src/xxx.js
的文件。
方式 3:手动尝试拼接 URL
https://example.com/static/js/app.js → app.js.map
尝试访问 同目录 + .map
通常能获取
4. 常见反调试技术(及识别方式)
技术手段 | 描述 | 识别方法 |
---|---|---|
debugger | 中断调试器执行 | 搜索关键词:debugger |
toString 欺骗 | 隐藏函数体内容 | console.log(fn.toString()) 看是否与预期不符 |
控制台检测 | 检测 devtools 是否打开 | 查看是否有 console.log.toString() 、window.outerWidth 相关判断 |
死循环卡调试 | 利用大量计算阻塞调试 | 搜索关键字如 while(true) 、for(;;) |
动态构造函数名 | 函数名用 eval 、Function 动态生成 | 搜索 Function("return this")() 结构 |
堆栈检查 | 检查 call stack 中是否有 debugger 或调试器函数 | 搜索 Error().stack |
Object.defineProperty 劫持 | 禁止控制台输出 | 检查是否重写了 console 相关属性 |
5. 识别反调试代码的技巧
1)关键字扫描(用于 AST、静态分析)
grep -i 'debugger' app.js
grep -i 'devtools' app.js
或用 Babel AST:
traverse(ast, {
DebuggerStatement(path) {
path.remove(); // 删除所有 debugger
}
});
2)动态调试行为检测(浏览器)
-
打开 Chrome DevTools
-
看是否自动跳转或自动刷新
-
是否不断触发
debugger
、页面假死、console 乱码
3)插桩日志 Hook 法
替换关键函数为 console.log
包装函数
window.alert = function(msg) {
console.log("[alert 调用] 参数:", msg);
};
6. 如何绕过反调试逻辑
技术 | 描述 |
---|---|
删除 debugger | 静态或 AST 方式清除 |
hook Function | 替换为你自己的构造器 |
替换死循环 | 修改为空代码块 |
patch console 检测 | Object.defineProperty(console, 'log', { get() {...} }) hook 掉 |
使用 Puppeteer Stealth | Puppeteer 自动规避部分 DevTools 检测逻辑 |
7. 总结:如何结合 sourceMap 与反调试分析代码
-
判断是否存在 .map 文件
-
查看是否加载、尝试拼接获取
-
-
使用 Chrome Source 查看源文件结构
-
找到有意义的源文件,跳转到
关键函数位置
-
-
清除反调试代码
-
使用 Babel AST 或手动清理
debugger
、死循环
-
-
结合 AST 做深入分析
-
提取加密函数、通信逻辑、参数生成等模块
-