关于 js:7. 模块化、构建与工具链

#王者杯·14天创作挑战营·第1期#

一、模块系统: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.exportsexports.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!')
  };
}));

模块机制

特性描述
自动适配判断当前运行环境自动使用适合的加载方式
导出对象返回一个全局对象,挂载在 windowglobal
多平台支持同时支持浏览器、Node.js、AMD、RequireJS 等
多见于库文件如 crypto-js、axios 发布的 umd 文件

逆向识别特征

表现形式含义
typeof module === 'object' && module.exportsCommonJS 检测
typeof define === 'function' && define.amdAMD 检测
root.XXX = factory();浏览器全局对象挂载

如果在逆向某个库函数(比如混淆的加密函数),看到这种结构,那几乎可以断定是一个通用的 UMD 打包库

总结

对比项CommonJSESMUMD
加载方式同步异步(静态)自适应
使用平台Node.js浏览器 + Node.js浏览器、Node、RequireJS
导出方式module.exportsexport / export defaultroot.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('模块名') 加载路径规则:

  1. 核心模块(如 fspath

  2. 自定义模块(相对/绝对路径)

  3. 第三方模块(从 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.exportsexports
是否能清除缓存 可以手动清除
是否支持顶层 await 不支持

四、模块缓存机制

模块缓存(Module Cache) 是指:

一个模块在第一次通过 require()import 被加载时,系统会将其“执行结果”缓存起来,后续再次加载该模块时,不会再次执行代码,而是直接返回缓存结果。

这保证了模块是“单例”的,并且执行效率更高。

1. Node.js 中 CommonJS 的缓存机制

模块加载流程:

  1. 首次 require()

    • 解析路径

    • 读取文件内容

    • 包裹成函数并执行

    • 缓存导出的 module.exports

  2. 再次 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.jsb.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 也缓存模块,但:

特性CommonJSESM
缓存机制 是 是
可清除缓存 手动清除 不可(静态结构)
影响变量共享 是 是
是否单例 是 是

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/coreBabel 核心引擎
@babel/parser将源码转成 AST
@babel/traverse遍历/修改 AST
@babel/types判断/构建 AST 节点
@babel/generatorAST 转回代码字符串

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/generatorAST ➝ 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 重命名
提取加密函数查找 FunctionDeclarationbtoaCryptoJS
提取字符串遍历 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 包装函数入口

  1. 搜索关键词:__webpack_require__

  2. 或搜索自执行函数 (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..."
}
字段含义
versionsourceMap 版本
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(;;)
动态构造函数名函数名用 evalFunction 动态生成搜索 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 StealthPuppeteer 自动规避部分 DevTools 检测逻辑

7. 总结:如何结合 sourceMap 与反调试分析代码

  • 判断是否存在 .map 文件

    • 查看是否加载、尝试拼接获取

  • 使用 Chrome Source 查看源文件结构

    • 找到有意义的源文件,跳转到 关键函数位置

  • 清除反调试代码

    • 使用 Babel AST 或手动清理 debugger、死循环

  • 结合 AST 做深入分析

    • 提取加密函数、通信逻辑、参数生成等模块

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值