Web 前端安全机制分析:以 Webpack 打包混淆为例

免责声明: 本文所有分析均基于公开可访问的前端 JS 代码及研究过程中所接触网站的正常访问流量,仅用于安全研究、学习与了解 Web 前端防护机制。请勿将本文技术用于任何未授权的系统,违者后果自负。

研究对象: 国家医疗保障局公共服务平台(fuwu.nhsa.gov.cn),该网站使用 Webpack 打包,接口存在全链路加密机制:请求体用 SM4 加密、请求体用 SM2 签名、请求头用 SHA256 哈希、响应体用 SM4 解密。


一、Webpack 是什么

Webpack 是目前最主流的前端模块打包工具,广泛应用于 Vue、React 等现代 Web 应用的构建流程中。它将项目中所有模块(JS、CSS、图片等)打包为一个或多个 bundle 文件,最终部署到服务端供浏览器加载。

对于安全研究而言,Webpack 打包产物具有如下特点:

  • 模块封装 :每个原始模块被包裹在函数中,通过 module id(数字或字符串)进行标识和索引
  • 统一加载器 :打包文件头部存在一个标准的模块加载函数(通常命名为 __webpack_require__ 或混淆后的短名),负责按需加载和缓存所有模块
  • 代码混淆 :变量名、函数名均被替换为短随机字符串(如 _7d92eo),增加可读性难度

二、Webpack Bundle 结构分析

2.1 整体结构

一个典型的 Webpack bundle 结构如下:

javascript 复制代码
!function(e) {        // e 是所有模块的集合对象(moduleFactory)
    var n = {}        // n 是模块缓存

    function o(t) {   // o 就是 __webpack_require__,模块加载器核心
        if (n[t])
            return n[t].exports;  // 命中缓存直接返回
        var i = n[t] = { i: t, l: false, exports: {} };
        e[t].call(i.exports, i, i.exports, o);  // 执行模块工厂函数
        i.l = true;
        return i.exports;
    }

    // 【逆向改动】手动添加:将加载器暴露到全局,原始 bundle 中没有这行
    window.load_func = o

    // ... 其他辅助函数(异步加载、公共路径等)
    
    o('app')  // 入口模块启动
}({
    "7d92": function(module, exports, require) { /* 模块7d92的代码 */ },
    "a1b2": function(module, exports, require) { /* 模块a1b2的代码 */ },
    // ...
})

2.2 核心组件说明

组件 变量名(混淆后) 作用
模块集合 e 以 module id 为键,模块工厂函数为值的对象
模块缓存 n 已执行过的模块的 exports 缓存,避免重复执行
模块加载器 o / __webpack_require__ 按 id 查找并执行模块,返回其 exports
已加载标志 i.l 标记模块是否已完成加载

2.3 模块加载器工作原理

o(t) 函数(即 __webpack_require__)是整个 Webpack bundle 的核心,其执行逻辑:

复制代码
调用 o("7d92")
  ↓
查缓存 n["7d92"]:是否已执行过
  ↓(未命中)
创建空模块对象:{ i: "7d92", l: false, exports: {} }
存入缓存 n["7d92"]
  ↓
执行模块工厂函数:e["7d92"].call(exports, module, exports, o)
    (工厂函数内可通过第三参数 o 继续 require 其他模块)
  ↓
标记 l = true,返回 module.exports

三、快速定位目标模块

3.1 手动暴露加载器

原始 bundle 中加载器函数(__webpack_require__)通常不会主动暴露到外部。逆向时需要手动在 bundle 内加载器定义处添加一行 ,将其挂载到 window,供外部调用:

javascript 复制代码
// 在 bundle 中找到加载器定义处(即 window.load_func = o 的上方),手动添加:
window.load_func = o   // 手动添加:将加载器暴露到全局,供 Node.js 中调用

添加这行后,就可以在 02_main.js 中通过 window.load_func(moduleId) 按需加载任意模块。

如何定位加载器函数的位置?

在 bundle 源码中搜索以下特征模式,即可定位加载器函数:

javascript 复制代码
// 典型特征:函数参数为 t,内部创建模块缓存对象 n[t]
function o(t) {
    if (n[t]) return n[t].exports;        // 命中缓存直接返回
    var i = n[t] = { i: t, l: false, exports: {} };
    e[t].call(i.exports, i, i.exports, o); // 执行模块工厂函数
    i.l = true;
    return i.exports;
}
// 在这行之后添加:
window.load_func = o

3.2 注释入口调用(避免补环境的关键技巧)

Webpack bundle 在最后会有一行入口调用,启动整个应用:

javascript 复制代码
// bundle 末尾的入口(未注释的原始状态)
o(o.s = 0)   // 加载模块 0,触发整个应用的启动链

这行代码会递归加载所有依赖模块,包括路由、状态管理、环境检测等应用层逻辑。在 Node.js 中执行时,这些模块会访问 windowdocumentnavigator 等浏览器环境,如果不补就会抛错。

解决办法:注释入口行

javascript 复制代码
// 将 bundle 末尾的入口调用注释掉:
// o(o.s = 0)   ← 加 // 注释

注释后效果:

未注释 注释后
require('./01_source') 行为 注册模块 + 立即执行入口,触发环境检测 仅注册所有模块到加载器,不执行任何业务代码
是否需要补环境
模块是否可用 是,只要等到 load_func 注册完成

适用条件 :当目标模块(如加密模块 7d92)并不依赖入口模块的初始化状态时,该方案完全有效。若目标模块内部依赖入口模块设置的某些全局变量,则可能返回错误结果。

3.2 定位加密/解密模块

方法一:抓包分析请求头

观察接口请求,找到动态变化的加密参数(如 x-tif-signaturex-tif-noncex-tif-timestamp 等),在 DevTools 中搜索这些字段名,定位到赋值代码,往上跟栈找到模块 id。

方法二:Hook 关键函数

在 DevTools Console 中 Hook 常见加密函数,如:

javascript 复制代码
// Hook CryptoJS / SM4 / AES 等
const _encrypt = CryptoJS.AES.encrypt;
CryptoJS.AES.encrypt = function(...args) {
    console.trace('AES encrypt called', args);
    return _encrypt.apply(this, args);
};

方法三:搜索加密算法关键字

在 Sources 面板搜索以下关键字,快速定位加密相关模块:

复制代码
SM4  SM2  AES  RSA  MD5  SHA  hmac  CryptoJS  encrypt  sign

3.3 验证模块可用性

在浏览器断点中,通过堆栈内的加载器引用执行目标模块:

javascript 复制代码
// 在 bundle 内部断点停下后,控制台中 o 就是当前作用域内的加载器引用
let mod = o("7d92")       // 按 id 加载目标模块
console.log(mod)           // 查看模块导出了哪些函数
console.log(mod.a)         // 查看加密函数
console.log(mod.b)         // 查看解密函数

注意window.load_func 是本地 bundle 中手动添加的,线上原始 bundle 不存在。在浏览器中调试时,应在断点堆栈内直接使用局部的加载器变量(如 o)进行验证。


四、本站加密机制分析

4.1 加密方案识别

通过抓包分析,该站点(fuwu.nhsa.gov.cn)的接口存在请求体加密 + 请求头动态签名 + 响应体解密的全链路加密机制:

  • 请求体加密 :业务数据用 SM4 加密,密钥由 appCodeappSecret 通过自定义函数动态派生(非标准 SM4,密钥派生逐辑需逆向确认)
  • 请求体签名signData 字段用 SM2 对排序后的请求字段做非对称签名(hash: true
  • 请求头动态签名x-tif-signature = SHA256 (timestamp + nonce + timestamp),x-tif-timestampx-tif-nonce 由加密模块动态生成
  • 响应体解密:服务端返回的密文用相同派生密钥做 SM4 解密
  • 涉及算法 :SM4(加解密)、SM2(签名)、SHA256(请求头哈希),均由模块 7d92 统一实现

4.2 关键请求头

请求头字段 说明
x-tif-signature 请求签名,由加密模块动态生成
x-tif-timestamp 时间戳
x-tif-nonce 随机字符串,防重放
x-tif-paasid 应用标识

4.3 模块 7d92 接口

javascript 复制代码
// 加载目标模块
_7d92 = window.load_func("7d92")

// 加密:传入请求数据对象,返回加密后的 {data, headers} 结构
_7d92.a(requestData)
// 返回示例:{ data: "加密密文...", headers: { "x-tif-signature": "...", ... } }

// 解密:传入算法名 + 响应密文,返回明文 JSON 字符串
_7d92.b("SM4", cipherText)

五、Node.js 本地还原方案

5.1 整体思路

直接将 Webpack bundle(01_source.js)在 Node.js 中执行,利用手动暴露的 window.load_func 调用目标模块,实现与浏览器端完全一致的加密/解密。

复制代码
Python (nhas.py)
  └─ subprocess 调用 node 02_main.js encrypt_data <json>
       └─ 02_main.js
            ├─ window = global          # 将 Node.js global 作为 window
            ├─ require('./01_source')   # 执行 Webpack bundle,注册所有模块
            ├─ _7d92 = window.load_func("7d92")  # 加载目标模块
            └─ encrypt_data(data) / decrypt_data(data)  # 调用加解密

5.2 02_main.js 入口代码

javascript 复制代码
window = global                    // 关键:让 bundle 中的 window.xxx 赋值生效
require("./01_source")             // 执行 bundle,触发 window.load_func = o

_7d92 = window.load_func("7d92")  // 按模块 id 加载加密模块

function encrypt_data(data) {
    return JSON.stringify(_7d92.a(data))
}

function decrypt_data(data) {
    return JSON.stringify(_7d92.b("SM4", data))
}

// 命令行参数分发
const args = process.argv.slice(2);
const functionName = args[0];
const data = args[1];

if (functionName === 'encrypt_data') {
    console.log(encrypt_data(JSON.parse(data)));
}
if (functionName === 'decrypt_data') {
    console.log(decrypt_data(JSON.parse(data)));
}

5.3 Python 调用代码

python 复制代码
import json
import os
import subprocess
import requests

_DIR = os.path.dirname(os.path.abspath(__file__))

def call_js_function(js_file_path, function_name, arg):
    result = subprocess.run(
        ['node', js_file_path, function_name, arg],
        capture_output=True, text=True, check=True, encoding='utf-8'
    )
    return result.stdout.strip()

# 1. 加密请求数据,同时获取动态请求头
js_data = call_js_function(os.path.join(_DIR, "02_main.js"), "encrypt_data", json.dumps(data))
js_data = json.loads(js_data)
encrypted_body = js_data["data"]          # 加密后的请求体
dynamic_headers = js_data["headers"]      # 动态签名等请求头

# 更新请求头中的动态字段
for key in dynamic_headers:
    if key in headers:
        headers[key] = str(dynamic_headers[key])

# 2. 发送请求
response = requests.post(url, headers=headers, cookies=cookies, data=encrypted_body)

# 3. 解密响应
plaintext = call_js_function(os.path.join(_DIR, "02_main.js"), "decrypt_data", response.text)
print(json.loads(plaintext))

六、Webpack 逆向通用方法论

6.1 快速入手流程

复制代码
1. 抓包:找到加密参数 → 确定哪些字段需要逆向
      ↓
2. 搜索:DevTools Sources 搜索字段名或加密关键字(SM4/SM2/SHA/encrypt/sign等)
      ↓
3. 跟栈:往上追调用链 → 找到 require(moduleId) 调用和目标模块 id
      ↓
4. 确认:在断点处通过堆栈内的加载器引用执行模块,
      观察导出内容并确认目标函数的参数和返回值格式
      ↓
5. 保存:在 DevTools Sources 面板右键目标 bundle → Save as,
      将完整 JS 文件保存到本地(即 01_source.js)
      ↓
6. 改动 bundle:
      ① 在加载器函数定义后手动添加 window.load_func = o(暴露加载器)
      ② 将 bundle 末尾的入口行注释:// o(o.s = 0)(避免执行应用层与补环境)
      ↓
7. 封装:写 02_main.js,window = global 后 require bundle,用 window.load_func(moduleId)
      加载目标模块,封装加解密函数供 Python subprocess 调用

6.2 常见坑点

问题 原因 解决方案
window is not defined Node.js 中无 window 对象 在 02_main.js 顶部加 window = global
document is not defined bundle 内部访问了 DOM 补充 document = { cookie: '', ... }
load_func is not exposed 网站未将加载器挂载到 window 手动修改 bundle,在加载器定义处加 global.load_func = o
模块 id 不稳定 每次构建 id 可能变化 用加密函数特征(算法名、关键字符串)搜索,而非硬编码 id
请求体格式不对 加密后返回的 data 格式需特殊处理 观察浏览器实际发送的 Content-Type 和 body 格式

6.3 模块 id 查找技巧

当网站加载器没有暴露到 window 时,可在 bundle 中搜索以下模式手动提取:

javascript 复制代码
// 搜索 SM4 / AES / 签名相关关键字,找到模块定义
// 典型结构:
"7d92": function(module, exports, require) {
    // ... 包含 SM4 或加密逻辑
    module.exports = { a: encryptFn, b: decryptFn }
}

找到后,在加载器函数定义结束处(window.load_func = o 附近)添加一行导出:

javascript 复制代码
window.load_func = o
// 手动添加:
global.load_func = o  // 兼容 Node.js 环境

七、依赖安装

bash 复制代码
# 仅需 Node.js 运行环境(无需额外 npm 包)
node --version   # 建议 v16+

# Python 依赖
pip install requests

本文技术仅供安全研究与学习,切勿用于任何未授权系统,违者后果自负。

相关推荐
ywf12152 小时前
Spring aop 五种通知类型
java·前端·spring
Lee川2 小时前
Go语言:并发编程的艺术与实践
前端
暗不需求2 小时前
React新手小白:如何入门 React 响应式交互与 JSX 艺术
前端·react.js
隐退山林2 小时前
JavaEE进阶:Spring Web MVC入门(1)
前端·spring·java-ee
前端缘梦2 小时前
深入理解React Fiber架构:渲染流程与双缓冲机制全解析
前端·react.js·面试
尘埃落定wf2 小时前
2026 年 LangChain (记忆)Memory 怎么用?三个核心类 + 完整代码示例
开发语言·前端·python
Maic2 小时前
用AI写了一个命理应用
前端
毛骗导演2 小时前
Claude Code REPL.tsx 架构深度解析
前端·架构
Mike_jia2 小时前
AllinSSL:SSL证书自动化管理的终极利器,让HTTPS部署再无烦恼
前端