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

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - *感悟人生*

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 = globalCannot 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(开源版)
用途 工具 / 地址

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