JS逆向实例:怎么分析并还原AES加密的请求参数

作者:极速慢跑

时间:2026-10-30

浏览:619

详细讲解在前端JS逆向中如何抓取、定位并还原AES加密请求参数。包含开发者工具调试技巧、断点Hook方法、AES四要素解析及Python和Node.js完整还原示例。

文章目录

在前端接口交互与数据爬取中,许多网站会将提交给服务器的表单数据、查询参数或整个载荷包装为一串乱码样式的密文字符串(如 params: "U2FsdGVkX1+..." 或 32/64 位十六进制散列)。这类加密在商业系统中以 AES(对称加密算法)最为普遍。还原此类参数的核心路径可以概括为三步:先通过浏览器开发者工具断点捕获加密发生的入口函数,再顺着调用栈解析出 AES 运算所必需的四项配置(密钥 Key、偏移向量 IV、工作模式 Mode、填充模式 Padding),最后在本地使用 Python 或 Node.js 完整复现相同的加密逻辑并对齐输出格式。本文将以标准的前端逆向排查流程为主线,完整演示这一过程的具体操作方法与排错细节。

JS逆向实例:怎么分析并还原AES加密的请求参数
图 1:AES 加密请求参数逆向与还原的完整工程流线

一、分析前准备与网络请求特征识别

在开展任何代码调试前,必须先在网络面板中掌握待分析请求的密文表现形式。按 F12 打开浏览器开发者工具(DevTools),切换至 Network(网络) 面板,触发页面操作,找到提交数据的目标请求。

1. 观察请求载荷结构

点击该请求并查看 Payload 选项卡。常见的 AES 加密载荷通常有以下两类形态:

  • 单字段封装:请求体为 JSON 格式,如 {"data": "8fbc90a1...", "ts": 1714012000},其中 data 为密文,其余字段为辅助的时间戳或随机数。
  • 全报文加密:整个请求体直接是一串裸字符串,格式通常是 Base64(结尾常带有 = 或 ==)或者 Hex 字符串(纯 0-9、a-f 组成的偶数长度字符)。

2. 判断密文是否随时间或请求动态变化

保持表单或查询参数完全相同,连续发起两次相同的请求,比对两次生成的密文字符串:

  • 两次密文完全相同:说明该接口大概率使用了 ECB 模式,或者使用了固定的 IV(偏移向量),且明文中未掺入动态时间戳。这种情况下逆向难度较低,提取出固定的 Key/IV 即可还原。
  • 两次密文不同:说明前端在明文中拼入了当前时间戳/随机数,或者在每次加密时随机生成了 IV。若 IV 是随机生成的,前端通常会将 IV 与密文拼接在一起传输(例如前 16 字节或后 16 字节为 IV),或者在请求头中单独携带。

二、在前端代码中快速定位加密入口

定位加密函数是逆向分析中耗时最多的环节。前端代码大多经过 Webpack、Vite 等构建工具压缩混淆,变量名常被替换为单个无意义字母(如 a、b、e)。直接肉眼阅读不可行,需要结合以下三种定位策略。

JS逆向实例:怎么分析并还原AES加密的请求参数
图 2:三种前端加密断点拦截定位策略对比与适用场景

1. 实践路径:配置 XHR/Fetch Breakpoints(最稳妥)

这是最推荐的做法,不依赖变量名是否混淆:

  1. 在 DevTools 中打开 Sources(源代码) 面板。
  2. 在右侧侧边栏中展开 XHR/fetch Breakpoints 折叠项。
  3. 点击 + 图标,输入目标请求 URL 的特征路径片段,例如 user/query。
  4. 回到页面重新发起请求。此时浏览器会精确暂停在底层 send() 发出网络请求的那一行代码上。

2. 审查 Call Stack(调用栈)逆向回溯

断点停在底层网络库后,请求体已经变成了密文。此时需要观察右侧的 Call Stack 列表:

调用栈自顶向下展示了当前函数的调用层级。最顶部是 xhr.js 或 fetch.js。从顶部开始,逐个向下点击前几个栈帧:

  • 栈帧 1~3:通常是 axios.js 的适配器和派发函数,此时载荷已是密文;
  • 栈帧 4~5:通常是请求拦截器 interceptors.request.use(...) 或业务封装的 http.post。在这个层级,往往能看到明显的业务代码;
  • 一旦看到某一层函数的输入变量还是明文对象(如 { username: "test", page: 1 }),而返回给下一层时变成了密文字符串,这就找到了执行加密的核心业务调用点。

3. 利用 Hook 拦截 JSON.stringify(针对深度混淆)

许多前端应用在加密前,会先用 JSON.stringify 将数据对象转为 JSON 字符串,然后再送入 AES 加密函数。我们可以通过在控制台输入以下 Hook 代码,在数据被序列化的瞬间自动触发断点:

(function() {
  const originalStringify = JSON.stringify;
  JSON.stringify = function(data) {
    // 根据数据中包含的关键字段触发断点
    if (data && (data.username || data.keyword || data.page)) {
      console.log("捕获到目标明文数据:", data);
      debugger; // 自动停在此处,此时查看 Call Stack 即可直接找到后续加密函数
    }
    return originalStringify.apply(this, arguments);
  };
})();

三、断点调试与 AES 加密四要素解析

顺着调用栈进入具体加密函数后,我们通常会看到前端调用了某个加密库。在 90% 以上的 Web 前端项目中,开发者都会直接引入社区成熟的 crypto-js 库。标准 CryptoJS 的加密调用模式如下:

// 典型的 CryptoJS 加密调用代码结构
const cipherText = CryptoJS.AES.encrypt(plainText, key, {
  iv: iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
}).toString();

为了在本地完整复现这个过程,必须准确记录并提取出以下四项要素:

JS逆向实例:怎么分析并还原AES加密的请求参数
图 3:AES 加密四要素与 CryptoJS 内部数据流转换
在 Console 控制台直接打印变量属性:
断点停在 CryptoJS.AES.encrypt 那一行时,在 Console 中执行:
key.toString(CryptoJS.enc.Utf8) 或 key.toString(CryptoJS.enc.Hex)
可以直接查看当前密钥的真实文本或十六进制字节,避免因为 WordArray 内部结构混淆判断。

四、本地代码还原与一致性对齐

在拿到前端确切的四要素之后,就可以脱离浏览器环境,使用 Python 或 Node.js 在本地编写独立的加解密模块。本节提供两个语言的完整还原示例。

1. 假设前端真实逻辑如下

// 前端混淆还原后的实际代码
const secretKey = CryptoJS.enc.Utf8.parse("abcdef0123456789"); // 16字节Key (AES-128)
const secretIv  = CryptoJS.enc.Utf8.parse("9876543210fedcba"); // 16字节IV

function encryptPayload(plainObj) {
  const plainText = JSON.stringify(plainObj);
  const encrypted = CryptoJS.AES.encrypt(plainText, secretKey, {
    iv: secretIv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });
  return encrypted.toString(); // 默认为 Base64 字符串
}

2. Python 完整还原实现(基于 pycryptodome)

在 Python 环境中执行 pip install pycryptodome 安装密码学标准库。注意 Python 中对于 PKCS7 的补位处理和字符编码转换:

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

class AesEncryptor:
    def __init__(self, key_str: str, iv_str: str):
        # 确保 Key 和 IV 编码为标准的 utf-8 字节
        self.key = key_str.encode('utf-8')
        self.iv = iv_str.encode('utf-8')
        self.block_size = AES.block_size # 固定为 16 字节

    def encrypt(self, plain_dict: dict) -> str:
        """输入明文字典,输出与前端完全一致的 Base64 密文"""
        # 注意: json.dumps 必须保持紧凑格式 (无多余空格),避免与前端 JSON.stringify 字符不一致
        raw_text = json.dumps(plain_dict, separators=(',', ':'), ensure_ascii=False)
        raw_bytes = raw_text.encode('utf-8')

        # 1. 执行 PKCS7 补位填充
        padded_bytes = pad(raw_bytes, self.block_size, style='pkcs7')

        # 2. 初始化 AES-CBC 密码器
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)

        # 3. 执行加密运算
        cipher_bytes = cipher.encrypt(padded_bytes)

        # 4. 转为 Base64 文本
        return base64.b64encode(cipher_bytes).decode('utf-8')

    def decrypt(self, cipher_b64: str) -> dict:
        """输入 Base64 密文,解密还原明文字典"""
        cipher_bytes = base64.b64decode(cipher_b64)
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        padded_bytes = cipher.decrypt(cipher_bytes)
        
        # 去除 PKCS7 填充
        raw_bytes = unpad(padded_bytes, self.block_size, style='pkcs7')
        return json.loads(raw_bytes.decode('utf-8'))

# --- 本地自测验证 ---
if __name__ == '__main__':
    KEY = "abcdef0123456789"
    IV  = "9876543210fedcba"
    
    encryptor = AesEncryptor(KEY, IV)
    sample_data = {"username": "test_user", "page": 1, "query": "逆向实战"}
    
    encrypted_str = encryptor.encrypt(sample_data)
    print("Python 加密输出密文:", encrypted_str)
    
    decrypted_obj = encryptor.decrypt(encrypted_str)
    print("Python 解密还原结果:", decrypted_obj)

3. Node.js 完整还原实现(原生 crypto 模块)

如果你使用 Node.js 爬虫或代理服务,可以直接使用 Node.js 原生的 crypto 模块,性能优越且无需安装额外三方依赖:

const crypto = require('crypto');

const KEY_STRING = 'abcdef0123456789';
const IV_STRING  = '9876543210fedcba';

function encryptNode(plainObj) {
  const plainText = JSON.stringify(plainObj);
  const key = Buffer.from(KEY_STRING, 'utf8');
  const iv  = Buffer.from(IV_STRING, 'utf8');

  // Node 原生 createCipheriv 默认开启 PKCS7 (AutoPadding)
  const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
  
  let encrypted = cipher.update(plainText, 'utf8', 'base64');
  encrypted += cipher.final('base64');
  return encrypted;
}

function decryptNode(cipherBase64) {
  const key = Buffer.from(KEY_STRING, 'utf8');
  const iv  = Buffer.from(IV_STRING, 'utf8');

  const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
  let decrypted = decipher.update(cipherBase64, 'base64', 'utf8');
  decrypted += decipher.final('utf8');
  return JSON.parse(decrypted);
}

// 验证
const testData = { username: "test_user", page: 1, query: "逆向实战" };
const enc = encryptNode(testData);
console.log('Node 加密结果:', enc);
console.log('Node 解密还原:', decryptNode(enc));

五、常见问题排查与对比验证清单

在本地还原代码编写完毕后,最常遇到的问题是:本地代码加密出来的密文,与浏览器页面控制台生成的密文对不上,导致发给服务器后返回 400 或签名错误。遇到该问题时,请参照下表排查四项核心差异:

排查维度 前端常见行为 本地还原易错点与对齐方法
JSON 序列化格式 JSON.stringify(obj) 生成无冗余空格的紧凑字符串。 Python 的 json.dumps 默认会在冒号后加空格 {"a": 1},必须显式声明 separators=(',', ':')。
Key / IV 编码转换 前端经常使用 CryptoJS.enc.Hex.parse(key)。 若 Key 字符串本就是十六进制表达(如 32 字符的 MD5 串),本地必须用 bytes.fromhex(key) 而非 .encode('utf-8')。
工作模式与填充 默认 CBC 模式与 PKCS7 填充;部分接口为 ECB。 ECB 模式下不需要 IV 参数;若前端显式指定了 ZeroPadding,Python 代码必须换用 style='zero'。
密文输出形式 通常是 Base64 编码,少数接口输出为纯 Hex 字符串。 确认服务端接收的是 Base64 还是 Hex。若为 Hex,Python 中使用 .hex() 输出,Node 中使用 'hex' 选项。
JS逆向实例:怎么分析并还原AES加密的请求参数
图 4:AES 逆向结果不一致四步排查与对齐流程图

六、总结与实战建议

分析并还原前端 AES 加密参数并不是一个黑盒猜测的过程,而是一套高度确定性的工程排查流程:

  1. 先抓包断点,不盲目看代码:善用 XHR/fetch Breakpoints 停在底层,通过调用栈向上找临界点,能规避 90% 以上由 Webpack 打包带来的混淆干扰。
  2. 精准提取四要素:将断点停在加密函数入口,在控制台打印并记录 Key、IV、Mode 与 Padding 的具体值。必须注意区分字符串字面量与 Hex 解析后的字节。
  3. 严格对齐序列化与编码:在本地编写 Python 或 Node.js 脚本时,重点关注 json.dumps 的紧凑分隔符 separators=(',', ':')、字符集 UTF-8 以及最终的 Base64 编码方式。
  4. 基准对比验证:用固定输入测试本地脚本和浏览器控制台,二者输出完全相同时,再将该加解密模块封装进爬虫或自动化调试工具中,即可长期稳定地还原与构造请求参数。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章