文章

AES 加密逆向实战——从定位到还原

AES 加密逆向实战——从定位到还原

前言

在 JS 逆向中,AES 加密是最常见的对称加密算法之一。无论你是爬虫工程师还是安全研究员,掌握 AES 的逆向方法都是基本功。

本文将从实际角度出发,讲解如何在混淆的 JS 代码中定位 AES 加密、提取关键参数,并完整还原加密过程。


1. AES 基础回顾

AES(Advanced Encryption Standard)是一种对称加密算法,加解密使用同一个密钥。几个关键参数:

参数 说明 常见值
密钥长度 128 / 192 / 256 bit AES-128 最常见
加密模式 块密码工作模式 CBC(最常见)、ECB、CTR、GCM
填充方式 数据不足时的补齐方式 PKCS7(最常见)、ZeroPadding
IV(初始向量) CBC 等模式需要 16 字节随机值

在逆向中,最重要的就是找到:密钥(Key)、IV(若使用 CBC 模式)和加密模式(Mode)


2. 如何定位 AES 加密

2.1 搜索关键词法

在浏览器开发者工具中搜索以下关键词:

// 关键词搜索
AES
CryptoJS
encrypt
decrypt
wordArray
parse
createCipheriv
createDecipheriv

实战技巧:

  • 全局搜索 CryptoJS → 几乎可以确定使用了 AES
  • 搜索 encrypt → 配合上下文判断
  • 搜索 \"iv\"\"mode\" → 定位加密配置对象

2.2 特征定位法

AES 加密在 JS 中有一些固定特征模式:

CryptoJS 模式(最常见):

// 特征:CryptoJS.AES.encrypt / decrypt
const encrypted = CryptoJS.AES.encrypt(plaintext, key, {
  iv: CryptoJS.enc.Utf8.parse('1234567890123456'),
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
});

Web Crypto API 模式(现代浏览器):

// 特征:window.crypto.subtle.encrypt
const encrypted = await crypto.subtle.encrypt(
  { name: "AES-CBC", iv: iv },
  key,
  data
);

Node.js crypto 模式:

// 特征:createCipheriv
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);

2.3 Hook 断点法

当你找到可疑的加密函数后,在控制台 Hook 住它:

// Hook CryptoJS AES.encrypt
const originalEncrypt = CryptoJS.AES.encrypt;
CryptoJS.AES.encrypt = function() {
  console.log('AES.encrypt 被调用:', arguments);
  debugger;  // 触发断点
  return originalEncrypt.apply(this, arguments);
};

或者 Hook 更底层的 WordArray

// Hook WordArray 的创建
const originalParse = CryptoJS.enc.Utf8.parse;
CryptoJS.enc.Utf8.parse = function() {
  console.log('Utf8.parse 被调用:', arguments);
  debugger;
  return originalParse.apply(this, arguments);
};

3. 实战案例:定位加密参数

场景

一个登录接口有 encryptData 参数,我们需要逆向出它的生成逻辑。

Step 1: 搜索 encrypt 关键词

在 Sources 面板中搜索 encrypt,找到可疑函数:

function getEncryptData(t) {
  var e = CryptoJS.enc.Utf8.parse("a1b2c3d4e5f6g7h8"),
      n = CryptoJS.enc.Utf8.parse("1234567890123456"),
      i = CryptoJS.AES.encrypt(t, e, {
        iv: n,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
      });
  return i.toString();
}

Step 2: 提取参数

一眼就能看出:

  • 密钥(Key)a1b2c3d4e5f6g7h8(16 字节,AES-128)
  • IV1234567890123456(16 字节)
  • 模式:CBC
  • 填充:PKCS7

Step 3: 还原验证

在控制台直接调用验证:

// 验证加密结果
var key = CryptoJS.enc.Utf8.parse("a1b2c3d4e5f6g7h8");
var iv = CryptoJS.enc.Utf8.parse("1234567890123456");
var encrypted = CryptoJS.AES.encrypt("password123", key, {
  iv: iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
});
console.log(encrypted.toString());  // 与抓包结果对比

4. 处理更复杂的情况

4.1 密钥被混淆

有时密钥不是明文,而是经过运算生成的:

// 混淆后的密钥生成
var _0x3f2e = function() {
  var key = '';
  var chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
  for (var i = 0; i < 16; i++) {
    key += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return key;
};

对策:在控制台执行这段函数,拿到实际生成的密钥。

4.2 自定义加密配置

有些网站会自定义 modepadding

// 自定义配置
var cfg = {
  mode: CryptoJS.mode.ECB,      // ECB 模式不需要 IV
  padding: CryptoJS.pad.ZeroPadding  // ZeroPadding
};

注意:ECB 模式不需要 IV,如果漏掉了这个会导致解密失败。

4.3 Base64 和 Hex 编码

密钥或密文可能经过额外编码:

// Base64 编码的密钥
var key = CryptoJS.enc.Base64.parse("YWJjZGVmZzEyMzQ1Njc4OTA=");

// Hex 编码的密钥
var key = CryptoJS.enc.Hex.parse("6162636465666731323334353637383930");

识别方法:

  • Base64.parse → 密钥是 Base64 编码
  • Hex.parse → 密钥是 Hex 编码
  • Utf8.parse → 密钥是明文字符串

5. 完整还原模板

当你定位到 AES 加密后,可以用以下模板来还原整个加密过程:

Python 还原

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

def aes_encrypt(plaintext, key, iv, mode=AES.MODE_CBC):
    """
    还原 JS AES 加密
    :param plaintext: 明文字符串
    :param key: 密钥(bytes,16/24/32 字节)
    :param iv: 初始向量(bytes,16 字节,CBC 模式需要)
    :param mode: 加密模式
    :return: base64 编码的密文
    """
    key = key.encode('utf-8')
    iv = iv.encode('utf-8')
    cipher = AES.new(key, mode, iv)
    padded_data = pad(plaintext.encode('utf-8'), AES.block_size)
    encrypted = cipher.encrypt(padded_data)
    return base64.b64encode(encrypted).decode()

def aes_decrypt(ciphertext_b64, key, iv, mode=AES.MODE_CBC):
    """
    还原 JS AES 解密
    """
    key = key.encode('utf-8')
    iv = iv.encode('utf-8')
    cipher = AES.new(key, mode, iv)
    encrypted = base64.b64decode(ciphertext_b64)
    decrypted = cipher.decrypt(encrypted)
    return unpad(decrypted, AES.block_size).decode()

# 使用示例
key = "a1b2c3d4e5f6g7h8"
iv = "1234567890123456"
result = aes_encrypt("password123", key, iv)
print(f"加密结果: {result}")

JavaScript(Node.js)还原

const CryptoJS = require("crypto-js");

function aesEncrypt(plaintext, key, iv) {
  const keyWord = CryptoJS.enc.Utf8.parse(key);
  const ivWord = CryptoJS.enc.Utf8.parse(iv);
  const encrypted = CryptoJS.AES.encrypt(plaintext, keyWord, {
    iv: ivWord,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });
  return encrypted.toString();
}

function aesDecrypt(ciphertext, key, iv) {
  const keyWord = CryptoJS.enc.Utf8.parse(key);
  const ivWord = CryptoJS.enc.Utf8.parse(iv);
  const decrypted = CryptoJS.AES.decrypt(ciphertext, keyWord, {
    iv: ivWord,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });
  return CryptoJS.enc.Utf8.stringify(decrypted);
}

// 使用
console.log(aesEncrypt("password123", "a1b2c3d4e5f6g7h8", "1234567890123456"));

6. 调试技巧汇总

技巧 用法
断点定位 在 Sources 面板中给 CryptoJS.AES.encrypt 调用行打断点
堆栈追踪 断点触发后看 Call Stack,找到调用来源
控制台测试 在断点处直接在 Console 中执行表达式验证变量值
搜索侧信道 ivkeymodepadding 等配置关键词
重放验证 用 Python/Node 还原加密后对比抓包结果

7. 结语

AES 逆向的核心就三步:

  1. 定位 → 找到加密函数(搜索关键词或 Hook)
  2. 提取 → 拿到 key、iv、mode、padding
  3. 还原 → 用 Python/Node 重放验证

掌握了这个流程,绝大多数 JS 加密都能轻松应对。

声明:本文仅供学习交流,请勿用于非法用途。逆向分析应仅限于自己拥有权限的系统或 CTF 比赛环境。

本文由作者按照 CC BY 4.0 进行授权