惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Spread Privacy
Spread Privacy
P
Palo Alto Networks Blog
P
Proofpoint News Feed
AI
AI
Help Net Security
Help Net Security
S
Securelist
T
Troy Hunt's Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cisco Blogs
Scott Helme
Scott Helme
Hacker News - Newest:
Hacker News - Newest: "LLM"
Vercel News
Vercel News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
B
Blog
GbyAI
GbyAI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
D
Darknet – Hacking Tools, Hacker News & Cyber Security
P
Proofpoint News Feed
S
Security Affairs
Cisco Talos Blog
Cisco Talos Blog
AWS News Blog
AWS News Blog
T
Tenable Blog
H
Help Net Security
NISL@THU
NISL@THU
F
Fortinet All Blogs
博客园_首页
G
GRAHAM CLULEY
L
LINUX DO - 最新话题
P
Privacy International News Feed
G
Google Developers Blog
博客园 - Franky
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Security Archives - TechRepublic
Security Archives - TechRepublic
The Register - Security
The Register - Security
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
T
Tor Project blog
P
Privacy & Cybersecurity Law Blog
量子位
C
Cyber Attacks, Cyber Crime and Cyber Security
Forbes - Security
Forbes - Security
S
Secure Thoughts
Simon Willison's Weblog
Simon Willison's Weblog
D
Docker
Recorded Future
Recorded Future
博客园 - 三生石上(FineUI控件)
L
Lohrmann on Cybersecurity
T
Tailwind CSS Blog

博客园 - *感悟人生*

PDFtoEXCEL批量处理高保真同步格式 批量处理苹果电脑的HEIF格式转成JPG|PNG 邮件群发系统 注册授权--续 独立授权模块 --可以为你的程序或者工具加上一把锁 音频转换合并切割工具 修改文件重命名(1、默认去掉预览和备份,2、默认当前文件路径) AI 平台 SQL语句解析 扣代码中,遇到this 应该怎么处理 AST解OB混淆,适用大部分的混淆 Akamai的形成与风控分析:聚焦Akamai 3.0 执行py3o的重启脚本(包含手动执行,以及自动执行的脚本) reduce与map+filter的复杂计算场景 py3o中汇总的计算:sum reduce map 三种形式来处理对比 py3o中数字金额转大写 加密解密基本概念 MJ提示词自动批处理GUI版 uv 在 Python 开发中的常用命令详解 表单和载荷的区别,以及python和js在处理json时的空格问题。 R函数处理异步迭代,在爬虫中的作用。
JS 逆向与前端安全加固实战指南
*感悟人生* · 2026-04-06 · via 博客园 - *感悟人生*

JS 逆向与前端安全加固实战指南

声明:本文所有内容仅适用于自己拥有或已获得授权的网站/项目的安全测试与加固。禁止用于未授权的第三方系统。


目录


一、整体分析流程

无论什么场景,逆向与安全测试的主线流程是固定的:

目标接口 → 抓包确认参数 → 定位JS来源 → 断点调试 → 还原逻辑 → 本地复现 → 修复加固

每个环节都有对应的工具和方法,后文逐一展开。


二、抓包与环境搭建

工具选型

工具 适用场景
Chrome DevTools 首选,Network/Sources/Console 联动调试
Fiddler Classic Windows 端,支持 HTTPS 解密、脚本改包
Charles Mac 首选,支持断点改包、Map Local
mitmproxy 脚本自动化处理,适合批量分析
Burp Suite 接口安全测试,重放攻击 / Intruder 爆破

HTTPS 抓包配置

# 启动 mitmproxy(系统代理模式)
mitmproxy -p 8080

# 安装证书:浏览器访问 http://mitm.it
# 选择对应平台证书下载并安装为受信任的根证书

Charles / Fiddler 类似,均需安装根证书后才能解密 HTTPS 流量。


三、JS 代码定位技巧

找到生成加密参数的 JS 代码,是逆向的第一步,也是最关键的一步。

3.1 XHR 断点(最常用)

DevTools → Sources → XHR/fetch Breakpoints
→ 点击 + 添加目标接口路径关键词(如 /api/sign)
→ 触发页面请求,自动断在 send() 前的调用栈顶
→ 在 Call Stack 面板逐层追溯,找到参数拼装位置

3.2 全局搜索关键参数名

DevTools → Sources → Ctrl+Shift+F(全局搜索)
搜索:sign=  /  _signature  /  x-bogus  /  token  /  encrypt

找到赋值语句后,在该行打断点,触发请求即可捕获。

3.3 DOM 事件断点

DevTools → Elements → 右键目标元素
→ Break on → subtree modifications / attribute modifications
适用于:点击按钮后触发加密逻辑的场景

3.4 Console Hook 注入

在 Console 中注入 Hook 代码,拦截所有网络请求,并打印调用栈:

// Hook XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
  console.log('[XHR Hook]', method, url);
  console.trace();
  return originalOpen.apply(this, arguments);
};

// Hook fetch
const originalFetch = window.fetch;
window.fetch = function(...args) {
  console.log('[Fetch Hook]', args[0]);
  console.trace();
  return originalFetch.apply(this, args);
};

3.5 Object.defineProperty Hook(追踪环境变量读取时机)

// 监控 navigator.webdriver 何时被读取(常见反爬检测点)
Object.defineProperty(navigator, 'webdriver', {
  get: function() {
    console.trace('[检测点] navigator.webdriver 被读取');
    return false;
  }
});

同理可 Hook navigator.pluginsscreen.width 等任意属性。


四、常见混淆类型识别与还原

4.1 混淆类型速查表

代码特征 混淆类型 还原方式
_0x1a2b、大量十六进制变量名 ob 混淆(JavaScript Obfuscator) AST 反混淆 / de4js
[][(![]+[])[+[]]...] JSFuck 直接粘入 Console 执行
eval(function(p,a,c,k,e,r){...}) AA Packer 替换 eval 为 console.log
大量 \u0041\u0042 Unicode 转义 unescape() / 在线工具
var _0x3f=['aGVsbG8=',...] 字符串数组混淆 找到数组 + 解密函数,手动还原

4.2 eval 混淆快速还原

// 方法一:Hook eval,输出原始代码后再执行
const _eval = eval;
eval = function(code) {
  console.log('[eval 内容]:', code);
  return _eval(code);
};

// 方法二:在 Sources 面板中 Ctrl+H 全文替换
// 将 eval( 替换为 console.log(
// 刷新页面,Console 即输出原始代码

4.3 AST 反混淆(推荐用于 ob 混淆)

ob 混淆的核心是字符串数组加密 + 控制流平坦化,AST 方式可以系统性还原。

npm install @babel/core @babel/traverse @babel/generator @babel/types
// ast-deobfuscate.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');

const code = fs.readFileSync('input.js', 'utf-8');
const ast = parser.parse(code);

traverse(ast, {
  // 还原字符串数组引用:将 _0xabc(0x1) 替换为实际字符串值
  CallExpression(path) {
    // 根据具体混淆逻辑,识别并内联字符串解密调用
    // 通常:找到字符串数组初始化位置 → 在 Node 中执行解密函数 → 替换所有调用
  },
  // 常量折叠:简化无意义运算
  BinaryExpression(path) {
    if (t.isNumericLiteral(path.node.left) && t.isNumericLiteral(path.node.right)) {
      const result = eval(
        `${path.node.left.value} ${path.node.operator} ${path.node.right.value}`
      );
      path.replaceWith(t.numericLiteral(result));
    }
  }
});

fs.writeFileSync('output.js', generator(ast).code);
console.log('还原完成 → output.js');

推荐配合 astexplorer.net 可视化分析节点结构。


五、加密算法识别与还原

5.1 常见算法特征速查

代码特征 算法 备注
MD5hexMD5、32 位 hex 输出 MD5 不可逆
SHA1/256/512HMAC SHA 系列 注意 HMAC 需要 key
CryptoJS.AESivkeymode AES 注意 CBC/ECB/CTR 模式差异
RSAPublicKeysetPublicKey RSA 非对称,注意填充方式
atobbtoaBase64 Base64 编码非加密,可直接解码
encodeURIComponent URL 编码 直接 decode

5.2 Python 本地复现加密逻辑

import hashlib, hmac, base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

# MD5
def md5(text: str) -> str:
    return hashlib.md5(text.encode()).hexdigest()

# HMAC-SHA256
def hmac_sha256(key: str, message: str) -> str:
    return hmac.new(key.encode(), message.encode(), hashlib.sha256).hexdigest()

# AES-CBC 加密
def aes_cbc_encrypt(key: str, iv: str, plaintext: str) -> str:
    cipher = AES.new(key.encode(), AES.MODE_CBC, iv.encode())
    ct = cipher.encrypt(pad(plaintext.encode(), AES.block_size))
    return base64.b64encode(ct).decode()

# AES-CBC 解密
def aes_cbc_decrypt(key: str, iv: str, ciphertext: str) -> str:
    ct = base64.b64decode(ciphertext)
    cipher = AES.new(key.encode(), AES.MODE_CBC, iv.encode())
    return unpad(cipher.decrypt(ct), AES.block_size).decode()

六、JSVMP 虚拟机保护分析

JSVMP(JS Virtual Machine Protection)是目前最难逆向的混淆方式。代码被编译成自定义字节码,由一个内置的 JS 解释器执行,无法直接阅读业务逻辑。常见于瑞数、数美、部分商业 SDK。

6.1 识别 JSVMP 特征

✓ 存在超大(数千行)的 switch-case 解释器主循环
✓ 大量位运算:>> << & | ^ >>>
✓ 一个核心调度函数反复跳转(pc 指针 / opcode 分发)
✓ 字节码以 ArrayBuffer / Uint8Array 形式存储
✓ 函数名极度混淆,逻辑完全无法直视

典型骨架特征(识别用):

while (true) {
  var opcode = bytecode[pc++];
  switch (opcode) {
    case 0x01: stack.push(constant_pool[bytecode[pc++]]); break;
    case 0x02: var b = stack.pop(), a = stack.pop(); stack.push(a + b); break;
    case 0x03: pc = stack.pop(); break; // 跳转指令
    // ... 几十到几百个 case
  }
}

6.2 四种实战分析策略

策略一:插桩追踪(最推荐)

不改源码,用条件断点输出每条指令的执行情况:

// DevTools → Sources → switch 行右键 → Add conditional breakpoint
// 条件填入(永远为 false,但会执行 console.log):
(console.log('op:', opcode, 'stack:', JSON.stringify(stack.slice(-3))), false)

// 或者直接修改本地副本,在 while(true) 第一行插入:
console.log('opcode:', opcode, 'stack:', JSON.stringify(stack.slice(-3)));

策略二:Hook 最终出口

JSVMP 无论内部多复杂,最终都要调用原生 API 输出结果。直接 Hook 出口:

// Hook 请求头,捕获最终加密值
const _setHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
  if (/sign|token|auth/i.test(name)) {
    console.log(`[Header 出口] ${name}: ${value}`);
    console.trace();
  }
  return _setHeader.apply(this, arguments);
};

策略三:算法层面猜测(已知明文攻击)

不硬啃字节码,从输出结果反推:

1. 抓到最终生成的加密值(如 32 位 hex)
2. 猜测算法类型(MD5 / SHA256 / 自定义)
3. 枚举输入参数组合,对比输出是否匹配
4. 验证成功后用 Python 直接复现,无需理解 JSVMP 内部

策略四:Proxy 内存追踪

用 Proxy 劫持对象,追踪 JSVMP 读写了哪些关键数据:

function createTracer(name, target) {
  return new Proxy(target, {
    get(obj, prop) {
      console.log(`[GET] ${name}.${String(prop)}`);
      const val = obj[prop];
      return typeof val === 'object' && val !== null
        ? createTracer(`${name}.${String(prop)}`, val)
        : val;
    },
    set(obj, prop, val) {
      console.log(`[SET] ${name}.${String(prop)} =`, val);
      obj[prop] = val;
      return true;
    }
  });
}

// 在解释器初始化前注入:
window = createTracer('window', window);

6.3 调试心法

① 缩小范围:二分法注释代码,定位生成加密值的最小代码段
② 时间切入:Network 面板观察请求时机,在对应事件处打断点
③ 逆向出口:不分析过程,只 Hook 所有可能携带加密值的 API 出口
④ 整段复用:将 JSVMP 代码整体复制到 Node.js,补好环境后直接调用

七、Webpack 打包代码深度分析

7.1 识别 Webpack 版本

// Webpack 4 特征:IIFE 结构,模块以对象形式传入
(function(modules) {
  var installedModules = {};
  function __webpack_require__(moduleId) { /* ... */ }
  __webpack_require__.m = modules;
  return __webpack_require__(__webpack_require__.s = 0);
})({ 0: function(module, exports, __webpack_require__) { /* ... */ } });

// Webpack 5 特征:搜索这些关键词
// __webpack_modules__  __webpack_require__  __webpack_exports__
var __webpack_modules__ = {
  "./src/utils/sign.js": (module, exports, require) => { /* ... */ }
};

7.2 浏览器内暴露 require(调试神器)

拿到 __webpack_require__ 后,可以直接加载任意内部模块:

// Webpack 4:在 __webpack_require__ 定义处打断点,断住后执行:
window.req = __webpack_require__;

// 枚举所有模块路径,找到目标
Object.keys(window.req.m).forEach(id => console.log(id));

// 直接加载并调用目标模块
const signModule = window.req('./src/utils/sign.js');
console.log(signModule.getSign({ page: 1 }));
// Webpack 5:在 bundle 最后一行打断点,断住后执行:
window._req = __webpack_require__;

// 枚举模块
for (let id in __webpack_modules__) console.log(id);

7.3 chunk 懒加载分析

现代 SPA 大量使用动态 import(),关键模块可能在操作触发时才加载:

// Hook 懒加载函数,追踪 chunk 加载顺序
const _e = __webpack_require__.e;
__webpack_require__.e = function(chunkId) {
  console.log('[懒加载 chunk]', chunkId);
  return _e.apply(this, arguments);
};

// 实战技巧:触发目标功能(如登录、提交),观察 Network 中新加载的 chunk.js
// 将对应 chunk 保存到本地,结合主 bundle 分析

7.4 生产环境误开 source map 的利用

检查方法:在 bundle.js 末尾查找:
  //# sourceMappingURL=bundle.js.map

如果存在,DevTools 会自动加载,Sources 面板直接显示原始源码!
也可手动挂载:Sources → 右键 js 文件 → Add source map → 填入 .map 文件 URL

7.5 批量提取 Webpack 模块

// extract-modules.js:将 bundle 中所有模块提取为独立文件
const fs = require('fs');
const code = fs.readFileSync('bundle.js', 'utf-8');

const moduleRegex = /"?([\w/.-]+)"?\s*:\s*function\s*\([^)]*\)\s*\{([\s\S]*?)(?=,?\s*"[\w/.-]+":\s*function|\}\s*\))/g;
let match, count = 0;
fs.mkdirSync('modules', { recursive: true });

while ((match = moduleRegex.exec(code)) !== null) {
  const [, id, content] = match;
  const filename = id.replace(/\//g, '_').replace(/[^a-zA-Z0-9_.]/g, '') + '.js';
  fs.writeFileSync(`modules/${filename}`, content);
  count++;
}
console.log(`提取了 ${count} 个模块 → ./modules/`);

八、Node.js 补环境(完整版)

在 Node.js 中执行浏览器 JS 时,缺少 window/document/navigator 等浏览器对象,需要手动"补环境"。

核心思路:先跑起来,再按报错逐个补。

8.1 第一步:用 Proxy 自动发现所有缺失属性

比手写快 10 倍,先用 Proxy 万能代理运行目标代码,从日志中找出所有被访问的属性:

// auto-env.js
function createAutoProxy(name = 'window', depth = 0) {
  if (depth > 5) return undefined;
  const cache = {};
  return new Proxy(function(){}, {
    get(target, prop) {
      if (prop === Symbol.toPrimitive) return () => 0;
      if (prop === 'toString') return () => `[AutoProxy:${name}]`;
      if (prop === Symbol.toStringTag) return 'Object';
      if (!(prop in cache)) {
        console.log(`[访问] ${name}.${String(prop)}`);
        cache[prop] = createAutoProxy(`${name}.${String(prop)}`, depth + 1);
      }
      return cache[prop];
    },
    set(target, prop, value) {
      console.log(`[设置] ${name}.${String(prop)} =`, typeof value);
      cache[prop] = value;
      return true;
    },
    apply(target, thisArg, args) {
      console.log(`[调用] ${name}(`, args.map(a => typeof a).join(', '), ')');
      return createAutoProxy(`${name}()`, depth + 1);
    },
    construct(target, args) {
      console.log(`[new] ${name}(${args.length} args)`);
      return createAutoProxy(`new ${name}`, depth + 1);
    }
  });
}

// 挂载万能代理
global.window = createAutoProxy('window');
['document','navigator','location','screen','history','localStorage','sessionStorage']
  .forEach(k => global[k] = createAutoProxy(k));

// 加载目标 JS,观察日志,记录所有被访问的属性
require('./target.js');

8.2 第二步:精细化补环境模板

根据上一步的日志,精准补充真实值:

// precise-env.js
'use strict';

// ── 基础全局 ──────────────────────────────────────────────
global.window = global;
global.self = global;
global.globalThis = global;

// ── Navigator ─────────────────────────────────────────────
global.navigator = {
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
  appVersion: '5.0 (Windows NT 10.0; Win64; x64)',
  platform: 'Win32',
  language: 'zh-CN',
  languages: ['zh-CN', 'zh', 'en'],
  cookieEnabled: true,
  onLine: true,
  hardwareConcurrency: 8,
  deviceMemory: 8,
  maxTouchPoints: 0,
  webdriver: false,           // 关键:必须为 false
  plugins: { length: 3 },
  mimeTypes: { length: 4 },
  vendor: 'Google Inc.',
  product: 'Gecko',
};

// ── Location ──────────────────────────────────────────────
global.location = {
  href: 'https://www.yoursite.com/page',
  origin: 'https://www.yoursite.com',
  protocol: 'https:',
  host: 'www.yoursite.com',
  hostname: 'www.yoursite.com',
  port: '',
  pathname: '/page',
  search: '',
  hash: '',
  reload: () => {},
  replace: (url) => console.log('[location.replace]', url),
};

// ── Screen & 窗口尺寸 ─────────────────────────────────────
global.screen = { width: 1920, height: 1080, availWidth: 1920, availHeight: 1040, colorDepth: 24, pixelDepth: 24 };
global.innerWidth = 1920; global.innerHeight = 937;
global.outerWidth = 1920; global.outerHeight = 1080;
global.devicePixelRatio = 1;
global.scrollX = 0; global.scrollY = 0;

// ── Document ──────────────────────────────────────────────
global.document = {
  title: 'Your Site',
  referrer: '',
  cookie: '',
  domain: 'www.yoursite.com',
  URL: 'https://www.yoursite.com/page',
  readyState: 'complete',
  characterSet: 'UTF-8',
  hidden: false,
  visibilityState: 'visible',
  createElement: (tag) => {
    const el = {
      tagName: tag.toUpperCase(), style: {}, className: '',
      setAttribute: () => {}, getAttribute: () => null,
      appendChild: () => {}, removeChild: () => {},
    };
    if (tag === 'canvas') {
      el.width = 300; el.height = 150;
      el.getContext = (type) => {
        if (type === '2d') return {
          fillStyle: '', strokeStyle: '', font: '', textBaseline: '',
          fillText: () => {}, strokeText: () => {},
          fillRect: () => {}, clearRect: () => {},
          beginPath: () => {}, closePath: () => {}, arc: () => {},
          fill: () => {}, stroke: () => {},
          measureText: (text) => ({ width: text.length * 8 }),
          getImageData: (x, y, w, h) => ({ data: new Uint8ClampedArray(w * h * 4).fill(128) }),
          putImageData: () => {},
          createLinearGradient: () => ({ addColorStop: () => {} }),
          save: () => {}, restore: () => {},
          translate: () => {}, rotate: () => {}, scale: () => {},
        };
        return null; // webgl 等返回 null
      };
      el.toDataURL = () =>
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
    }
    return el;
  },
  getElementById: () => null,
  querySelector: () => null,
  querySelectorAll: () => [],
  getElementsByTagName: () => [],
  addEventListener: () => {},
  removeEventListener: () => {},
  createEvent: () => ({ initEvent: () => {} }),
  dispatchEvent: () => true,
};

// ── 定时器(同步执行,避免异步阻塞)─────────────────────────
global.setTimeout  = (fn, _d, ...args) => { try { fn(...args); } catch(e){} return 1; };
global.clearTimeout  = () => {};
global.setInterval = (fn, _d, ...args) => { try { fn(...args); } catch(e){} return 1; };
global.clearInterval = () => {};
global.requestAnimationFrame = (fn) => { fn(Date.now()); return 1; };

// ── Performance API ───────────────────────────────────────
global.performance = {
  now: () => Date.now() - 1700000000000,
  timing: { navigationStart: Date.now() - 5000 },
  getEntriesByType: () => [],
  mark: () => {}, measure: () => {},
};

// ── Crypto API ────────────────────────────────────────────
const nodeCrypto = require('crypto');
global.crypto = {
  getRandomValues: (arr) => {
    const bytes = nodeCrypto.randomBytes(arr.byteLength);
    arr.set(new arr.constructor(bytes.buffer));
    return arr;
  },
  subtle: nodeCrypto.webcrypto?.subtle,
};

// ── 其他常用 ──────────────────────────────────────────────
global.atob = (str) => Buffer.from(str, 'base64').toString('binary');
global.btoa = (str) => Buffer.from(str, 'binary').toString('base64');
global.XMLHttpRequest = function() {
  return { open: () => {}, send: () => {}, setRequestHeader: () => {}, addEventListener: () => {}, readyState: 4, status: 200 };
};

console.log('[env] 环境初始化完成');

8.3 常见报错速查表

报错信息 原因 解决方案
window is not defined 缺少 window global.window = global
Cannot read property 'xxx' of undefined 子对象未定义 根据属性名补对应对象
atob is not defined 缺少 Base64 函数 见上方 atob/btoa 实现
crypto.getRandomValues is not a function 缺少 Crypto API 见上方 crypto 补充
document.createElement is not a function document 不完整 补完整 document 对象
Cannot read property 'length' of undefined plugins/mimeTypes 未定义 navigator.plugins = { length: 3 }
performance.now is not a function 缺少 performance 补 performance 对象
TextEncoder is not defined Node < 11 const {TextEncoder} = require('util'); global.TextEncoder = TextEncoder

8.4 vm 沙箱隔离执行

用 Node.js 内置 vm 模块运行目标 JS,避免污染当前进程全局变量:

const vm  = require('vm');
const fs  = require('fs');

const sandbox = {
  console,
  setTimeout: (fn) => fn(),
  // ... 将上面的补环境对象全部放进来
};

const code   = fs.readFileSync('target.js', 'utf-8');
const script = new vm.Script(code, { filename: 'target.js' });
const ctx    = vm.createContext(sandbox);
script.runInContext(ctx);

// 调用目标函数
const result = vm.runInContext('getSign({ page: 1 })', ctx);
console.log('签名结果:', result);

8.5 Python 集成调用 JS(生产环境)

# 方案一:PyExecJS(轻量,适合简单场景)
import execjs

with open('target_with_env.js') as f:
    ctx = execjs.compile(f.read())

result = ctx.call('getSign', {'page': 1, 'size': 20})
print('签名:', result)

# 方案二:py-mini-racer(V8 引擎,性能更好)
from py_mini_racer import MiniRacer

ctx = MiniRacer()
ctx.eval(open('target_with_env.js').read())
print('签名:', ctx.call('getSign', {'page': 1}))

# 方案三:subprocess 调用 Node.js(最稳定,推荐生产使用)
import subprocess, json

def call_js(func_name, *args):
    script = f"""
    require('./env.js');
    require('./target.js');
    const result = {func_name}(...{json.dumps(list(args))});
    process.stdout.write(JSON.stringify(result));
    """
    r = subprocess.run(['node', '-e', script], capture_output=True, text=True, timeout=10)
    return json.loads(r.stdout)

print(call_js('getSign', {'page': 1}))

九、常见风控检测点自测

自测自己的网站,看看以下弱检测点是否存在,在 DevTools Console 中逐一执行:

// 1. webdriver 检测(正常浏览器应为 undefined 或 false)
console.log('webdriver:', navigator.webdriver);

// 2. plugins 检测(headless 下通常为 0)
console.log('plugins length:', navigator.plugins.length);

// 3. languages 检测(headless 可能为空数组)
console.log('languages:', navigator.languages);

// 4. Chrome 对象检测(headless 下为 undefined)
console.log('window.chrome:', window.chrome);

// 5. 窗口尺寸一致性(headless 下 outerWidth 常为 0)
console.log('outer:', window.outerWidth, window.outerHeight);
console.log('inner:', window.innerWidth, window.innerHeight);

// 6. Canvas 指纹(不同机器/浏览器值不同)
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '14px Arial';
ctx.fillText('fingerprint_test', 10, 30);
console.log('canvas:', canvas.toDataURL().substring(0, 80));

// 7. AudioContext 支持(headless 通常返回 null)
try {
  const ac = new AudioContext();
  console.log('AudioContext state:', ac.state);
} catch(e) {
  console.log('AudioContext: not supported');
}

十、参考商业风控设计,加固自有系统

防御者核心思路:知道攻击者用哪些方法,就把对应漏洞堵死。

本章参考瑞数、加速乐、五秒盾、Akamai 等商业风控系统的防御体系,
整理为自有系统可直接落地的加固方案。


10.1 多维设备指纹体系

商业风控的核心壁垒。单一维度极易伪造,组合维度让攻击成本指数级上升

Canvas / WebGL 指纹

原理:同一段绘图代码在不同 GPU / 驱动 / 操作系统下,渲染结果存在微小差异。

攻击者弱点:Node.js / Headless 环境无法真实渲染,toDataURL() 返回空值或固定值,与真实浏览器截然不同。

// 前端指纹采集(多操作混合,增加伪造难度)
function getCanvasFingerprint() {
  const canvas = document.createElement('canvas');
  canvas.width = 240; canvas.height = 60;
  const ctx = canvas.getContext('2d');

  ctx.textBaseline = 'alphabetic';
  ctx.fillStyle = '#f60';
  ctx.fillRect(125, 1, 62, 20);
  ctx.fillStyle = '#069';
  ctx.font = '11pt "Times New Roman"';
  ctx.fillText('Cwm fjordbank glyphs vext quiz 😀', 2, 15);
  ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
  ctx.font = '18pt Arial';
  ctx.fillText('Cwm fjordbank', 4, 45);

  // WebGL 指纹(GPU 型号信息,伪造成本极高)
  const gl = document.createElement('canvas').getContext('webgl');
  const glInfo = gl ? {
    renderer: gl.getParameter(gl.RENDERER),
    vendor:   gl.getParameter(gl.VENDOR),
    version:  gl.getParameter(gl.VERSION),
  } : null;

  return { canvas: canvas.toDataURL(), webgl: glInfo };
}

加固建议

  • 服务端存储每个用户的历史指纹,指纹突变(尤其从真实值变为全零/固定值)立即触发风险标记
  • 结合 WebGL renderer 字符串,headless 环境通常返回 SwiftShader 或空字符串

AudioContext 指纹

原理:音频处理的浮点运算因硬件而异,产生微小精度差异。

攻击者弱点:Headless 下 AudioContext 输出全零,极易识别。

function getAudioFingerprint() {
  return new Promise((resolve) => {
    try {
      const ctx = new (window.AudioContext || window.webkitAudioContext)();
      const analyser = ctx.createAnalyser();
      const oscillator = ctx.createOscillator();
      const compressor = ctx.createDynamicsCompressor();

      oscillator.connect(compressor);
      compressor.connect(analyser);
      analyser.connect(ctx.destination);
      oscillator.start(0);

      setTimeout(() => {
        const arr = new Float32Array(analyser.frequencyBinCount);
        analyser.getFloatFrequencyData(arr);
        oscillator.stop();
        ctx.close();
        // 取前 30 个浮点值拼接即为指纹
        resolve(arr.slice(0, 30).join(','));
      }, 100);
    } catch(e) {
      resolve('unsupported'); // headless 常见
    }
  });
}

字体指纹

原理:每台机器安装的字体集合各不相同,可通过 measureText 差异检测。

攻击者弱点:虚拟机 / Headless 环境只有系统默认字体,字体集极小,特征明显。

function getInstalledFonts() {
  const testFonts = [
    'Arial', 'Times New Roman', 'Courier New', 'Georgia',
    'Microsoft YaHei', 'SimSun', 'PingFang SC',   // 中文字体
    'Helvetica Neue', 'Futura', 'Gill Sans',       // Mac 字体
    'Segoe UI', 'Calibri', 'Consolas',             // Windows 字体
  ];
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const result = {};

  testFonts.forEach(font => {
    ctx.font = '16px monospace';
    const base = ctx.measureText('mmmmmmmmmmlli').width;
    ctx.font = `16px "${font}", monospace`;
    const test = ctx.measureText('mmmmmmmmmmlli').width;
    result[font] = test !== base; // true = 字体存在
  });
  return result;
}

10.2 行为分析(最核心的护城河)

机器行为与人类行为存在本质差异,这是商业风控最难被复制的部分。

攻击者弱点

  • 爬虫不移动鼠标,或轨迹为直线 / 固定模式
  • 速度分布不符合人类运动规律(菲茨定律:目标越小越远,移动越慢)
  • 点击坐标过于精确(总是落在元素中心点)
  • 键盘输入节奏过于均匀(人类打字有自然抖动)
// 前端行为数据采集
class BehaviorCollector {
  constructor() {
    this.mouseTrail = [];
    this.clickEvents = [];
    this.keyEvents = [];
    this.startTime = Date.now();
    this._init();
  }

  _init() {
    // 鼠标轨迹(50ms 降采样)
    let lastMouseTime = 0;
    document.addEventListener('mousemove', (e) => {
      const now = Date.now();
      if (now - lastMouseTime < 50) return;
      lastMouseTime = now;
      this.mouseTrail.push({ x: e.clientX, y: e.clientY, t: now - this.startTime });
      if (this.mouseTrail.length > 200) this.mouseTrail.shift();
    });

    // 点击偏移(真实用户不总点中心)
    document.addEventListener('click', (e) => {
      const rect = e.target.getBoundingClientRect();
      this.clickEvents.push({
        offsetX: e.clientX - rect.left,
        offsetY: e.clientY - rect.top,
        elementWidth: rect.width,
        elementHeight: rect.height,
        t: Date.now() - this.startTime,
      });
    });

    // 键盘节奏(按键间隔)
    let lastKeyTime = 0;
    document.addEventListener('keydown', () => {
      const now = Date.now();
      this.keyEvents.push({ interval: now - lastKeyTime, t: now - this.startTime });
      lastKeyTime = now;
    });
  }

  getFeatures() {
    const trail = this.mouseTrail;
    if (trail.length < 2) return { suspicious: true, reason: 'no_mouse_movement' };

    const speeds = [];
    for (let i = 1; i < trail.length; i++) {
      const dx = trail[i].x - trail[i-1].x;
      const dy = trail[i].y - trail[i-1].y;
      const dt = trail[i].t - trail[i-1].t || 1;
      speeds.push(Math.sqrt(dx*dx + dy*dy) / dt);
    }

    const mean = arr => arr.reduce((a,b) => a+b, 0) / arr.length;
    const variance = arr => {
      const m = mean(arr);
      return arr.reduce((a,b) => a + (b-m)**2, 0) / arr.length;
    };

    return {
      mousePoints:          trail.length,
      avgSpeed:             mean(speeds),
      speedVariance:        variance(speeds),          // 太低 → 机器
      clickCount:           this.clickEvents.length,
      keyIntervalVariance:  variance(this.keyEvents.map(e => e.interval)), // 太低 → 机器
    };
  }
}
# 服务端行为风险评分
def calculate_risk_score(data: dict) -> dict:
    score, reasons = 0, []

    # 无鼠标移动
    if data.get('mousePoints', 0) == 0:
        score += 40; reasons.append('no_mouse_movement')

    # 速度过于均匀(机器特征)
    if data.get('speedVariance', 0) < 0.01:
        score += 30; reasons.append('unnatural_mouse_speed')

    # 点击太精准(总落在元素中心)
    clicks = data.get('clickEvents', [])
    if clicks:
        precise = sum(1 for c in clicks
                      if abs(c['offsetX'] - c['elementWidth']/2) < 2
                      and abs(c['offsetY'] - c['elementHeight']/2) < 2)
        if precise / len(clicks) > 0.8:
            score += 25; reasons.append('too_precise_clicks')

    # 键盘节奏太均匀
    if data.get('keyIntervalVariance', 0) < 5:
        score += 20; reasons.append('robotic_keyboard_rhythm')

    level = 'high' if score >= 60 else 'medium' if score >= 30 else 'low'
    return { 'risk_score': min(score, 100), 'risk_level': level, 'reasons': reasons }

10.3 自动化环境检测套件

这是瑞数、Akamai 等系统前端 JS 的核心检测逻辑,自有系统可直接复用:

const EnvDetector = {

  // 检测 Headless 浏览器(得分越高越可疑,满分 6)
  headlessScore() {
    return [
      !window.chrome,                                                     // 无 chrome 对象
      navigator.plugins.length === 0,                                     // 无插件
      !navigator.languages || navigator.languages.length === 0,           // 无语言列表
      navigator.webdriver === true,                                        // webdriver 标记
      screen.width === 0 || screen.height === 0,                          // 屏幕尺寸异常
      window.outerWidth === 0 && window.outerHeight === 0,                 // outer 尺寸为 0
    ].filter(Boolean).length;
  },

  // 检测自动化框架遗留变量
  isAutomationTool() {
    return !!(
      window._phantom || window.callPhantom ||             // PhantomJS
      window.__nightmare ||                                 // Nightmare.js
      window._selenium || window.domAutomation ||           // Selenium
      document.$cdc_asdjflasutopfhvcZLmcfl_ ||             // ChromeDriver 遗留变量
      window.__webdriver_evaluate || window.__selenium_evaluate
    );
  },

  // 检测虚拟机(通过 CPU 密集运算时间误差)
  isVM() {
    const start = performance.now();
    let c = 0; for (let i = 0; i < 1000000; i++) c++;
    const elapsed = performance.now() - start;
    return elapsed < 1 || elapsed > 100; // 正常机器约 5-15ms
  },

  // UA 与环境一致性交叉验证
  consistencyCheck() {
    return {
      uaChromeMismatch:  navigator.userAgent.includes('Chrome') && !window.chrome,
      platformMismatch:  navigator.userAgent.includes('Windows') && !navigator.platform.includes('Win'),
      langMismatch:      navigator.language !== navigator.languages?.[0],
    };
  },

  report() {
    return {
      headlessScore:   this.headlessScore(),
      isAutomation:    this.isAutomationTool(),
      isVM:            this.isVM(),
      consistency:     this.consistencyCheck(),
    };
  }
};

10.4 网络层防御(TLS / IP / 请求特征)

TLS 指纹(JA3)

每种客户端(Chrome / Python requests / curl / Node.js)的 TLS 握手参数不同,包括 TLS 版本、加密套件顺序、扩展列表、椭圆曲线等,服务端可以直接识别。

加固方案

  • 在 Nginx 层采集 JA3 指纹(使用 nginx-module-ja3 模块)
  • 建立正常用户 JA3 白名单,异常 JA3 触发二次验证
  • 或直接使用 Cloudflare Bot Management / WAF

IP 信誉评估

import ipaddress

# 已知数据中心 IP 段(需持续维护更新)
DATACENTER_RANGES = [
    '13.0.0.0/8',    # AWS
    '34.0.0.0/8',    # GCP
    '40.0.0.0/8',    # Azure
    '47.0.0.0/8',    # 阿里云
    '119.29.0.0/16', # 腾讯云
]

def assess_ip_risk(ip: str, headers: dict) -> dict:
    addr = ipaddress.ip_address(ip)
    risk = {}

    # 数据中心 IP
    risk['is_datacenter'] = any(
        addr in ipaddress.ip_network(r) for r in DATACENTER_RANGES
    )

    # UA 与 Accept-Language 不一致
    ua   = headers.get('User-Agent', '')
    lang = headers.get('Accept-Language', '')
    risk['lang_ua_mismatch'] = ('Windows' in ua and 'zh' not in lang and 'en' not in lang)

    # 缺少 br 压缩(正常 Chrome 必带 gzip, deflate, br)
    risk['missing_br'] = 'br' not in headers.get('Accept-Encoding', '')

    return risk

10.5 Challenge Token 机制(参考瑞数 / 加速乐设计)

核心流程:
  ① 用户首次访问 → 服务端下发带随机 challengeKey 的 JS 挑战代码
  ② 浏览器执行 JS → 采集设备指纹 → 用 SubtleCrypto 计算 Token
  ③ 带 Token 再次请求 → 服务端验证合法性
  ④ Token 绑定:IP + UA + 设备指纹 + 时间窗口(5 分钟有效)

攻击者面临的困境:
  ✗ 必须真实执行 JS(不能跳过)
  ✗ 必须有完整浏览器环境(补环境成本极高)
  ✗ Token 有时效,无法复用
  ✗ 每次 challengeKey 不同,无法固化破解脚本
// 前端:生成 Challenge Token
async function generateChallengeToken(challengeKey) {
  const fp = {
    canvas: getCanvasFingerprint().canvas.substring(100, 150),
    ua:     navigator.userAgent,
    lang:   navigator.language,
    tz:     Intl.DateTimeFormat().resolvedOptions().timeZone,
    screen: `${screen.width}x${screen.height}x${screen.colorDepth}`,
    plugins: navigator.plugins.length,
    ts:     Date.now(),
  };

  const payload = JSON.stringify(fp) + challengeKey;
  const data    = new TextEncoder().encode(payload);
  const hash    = await crypto.subtle.digest('SHA-256', data);
  const token   = Array.from(new Uint8Array(hash))
                       .map(b => b.toString(16).padStart(2,'0')).join('');

  return { token, fingerprint: fp };
}
# 服务端:验证 Token
import hashlib, json, time

def verify_token(token: str, fingerprint: dict, challenge_key: str) -> tuple:
    payload  = json.dumps(fingerprint, separators=(',',':')) + challenge_key
    expected = hashlib.sha256(payload.encode()).hexdigest()

    if token != expected:
        return False, 'token_mismatch'

    ts = fingerprint.get('ts', 0) / 1000
    if abs(time.time() - ts) > 300:   # 5 分钟有效期
        return False, 'token_expired'

    return True, 'ok'

10.6 蜜罐与主动对抗

隐藏蜜罐元素

<!-- 页面中插入对普通用户不可见的链接,爬虫可能会访问 -->
<a href="/honeypot/trap" style="display:none;position:absolute;left:-9999px;opacity:0">
  hidden
</a>
# 服务端:蜜罐路径访问直接标记为高风险
@app.route('/honeypot/<path:path>')
def honeypot(path):
    ip = request.remote_addr
    mark_as_suspicious(ip, reason='honeypot_triggered', path=path)
    return '', 404   # 正常返回 404,不暴露蜜罐存在

检测关键函数是否被 Hook

攻击者在分析自有系统时,常常会 Hook XMLHttpRequestfetch 等原生函数。可以在前端埋入探针,检测到 Hook 立即上报:

function detectHook() {
  const suspicious = [];
  const nativeFns = [
    [XMLHttpRequest.prototype.open,  'XHR.open'],
    [XMLHttpRequest.prototype.send,  'XHR.send'],
    [fetch,                          'fetch'],
    [JSON.stringify,                 'JSON.stringify'],
    [JSON.parse,                     'JSON.parse'],
    [Object.defineProperty,          'Object.defineProperty'],
  ];

  nativeFns.forEach(([fn, name]) => {
    const str = Function.prototype.toString.call(fn);
    if (!str.includes('[native code]')) {
      suspicious.push(name);
    }
  });

  // 检测 Function.prototype.toString 自身是否被篡改
  const toStr = Object.getOwnPropertyDescriptor(Function.prototype, 'toString');
  if (toStr && toStr.value !== Function.prototype.toString) {
    suspicious.push('Function.prototype.toString');
  }

  if (suspicious.length > 0) {
    // 上报到服务端,标记该用户/IP 为可疑
    navigator.sendBeacon('/api/risk-report', JSON.stringify({
      type: 'hook_detected',
      targets: suspicious,
      ts: Date.now(),
    }));
  }

  return suspicious;
}

动态混淆关键参数名

策略:每次响应中,关键参数名由服务端动态生成(非固定字符串)
效果:爬虫硬编码参数名后,下次更新即失效,维护成本极高
实现:服务端渲染时注入当次参数名映射,前端 JS 从中读取

10.7 风控分级响应策略

参考商业系统的验证码降级体系:

风险等级 触发条件 响应策略
🟢 低风险 指纹正常 + 行为自然 + IP 干净 无感通过,正常服务
🟡 中风险 1-2 项异常(如数据中心 IP) 滑块验证码 / 图形验证
🔴 高风险 多项异常 + 自动化特征明显 短信验证 / 人脸识别
⛔ 极高风险 蜜罐触发 / Hook 检测 / Token 伪造 直接封禁 IP + 设备指纹

10.8 加固优先级总览

优先级 措施 攻击者对抗成本 实现难度
⭐⭐⭐⭐⭐ 服务端鉴权 + 业务逻辑校验 极高(无法绕过)
⭐⭐⭐⭐⭐ Challenge Token(JS 挑战)
⭐⭐⭐⭐ 行为分析(鼠标 / 键盘节奏)
⭐⭐⭐⭐ 多维设备指纹组合
⭐⭐⭐ 自动化环境检测 中(可被针对性绕过)
⭐⭐⭐ TLS / JA3 指纹(CDN 层) 低(配置即可)
⭐⭐⭐ IP 信誉 + 频率限制
⭐⭐ 蜜罐 + Hook 探针
⭐⭐ JS 代码混淆 低(仅延缓,不能防止)

核心结论:没有任何单一措施能防住所有攻击。商业系统的真正壁垒在于多维度组合 + 持续动态更新。自有系统应优先落地服务端鉴权和行为分析,这两项性价比最高,且攻击者无法通过客户端手段绕过。


十一、工具推荐汇总

用途 工具 / 地址
代码反混淆(在线) https://de4js.kshift.me/
AST 可视化分析 https://astexplorer.net/
编码 / 加密瑞士军刀 https://cyberchef.org/
加密算法识别 https://www.dcode.fr/
JS 代码格式化 https://beautifier.io/
本地执行 JS Node.js + vm 模块
Python 调用 JS PyExecJS / py-mini-racer / subprocess
Webpack 分析 source-map-explorer / webpack-bundle-analyzer
JSVMP 分析 条件断点插桩 + Proxy 追踪
自动补环境 Proxy 万能代理(见第八章)
抓包工具 Charles / Fiddler / mitmproxy / Burp Suite
设备指纹库(参考) FingerprintJS(开源版)

本文整理自实战安全测试经验,所有代码示例均以防御加固为目的。如发现自有系统存在以上风险点,建议优先修复服务端鉴权逻辑,再逐步完善前端防护体系。