爬虫逆向中常见的 AES 加解密逻辑复现

fengxiaoyao
15
2026-02-03

前言

在接口逆向过程中,AES 是非常常见的一类对称加密方案。
很多站点在请求发送之前,会先对参数进行 AES 加密;有些站点在接口返回之后,还需要先对密文结果进行 AES 解密,才能拿到真实业务数据。

对于这类场景,真正影响复现结果的,并不是“会不会调用加密库”,而是下面这些参数是否能够和目标站点保持一致:

  • key

  • iv

  • mode

  • padding

  • 密文输出格式

只要这些参数能够完全对齐,网页中的 AES 加解密逻辑通常都可以在本地直接复现。

这篇文章整理两份最基础的示例代码,一份使用 JavaScript,一份使用 Python。
代码不做额外封装,只保留最直接的加解密过程,方便在逆向时逐步验证每一个环节。


一、AES 逆向时重点看的是什么

在实际逆向中,AES 本身并不是难点。
真正决定复现是否成功的,通常是下面几个点是否和目标站点保持一致:

  1. 密钥 key

  2. 偏移量 iv

  3. 加密模式 mode

  4. 填充方式 padding

  5. 密文编码格式

例如前端很常见的一类写法如下:

CryptoJS.AES.encrypt(data, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
})

如果已经定位到这一类代码,那么基本可以确认:

  • 使用的是 AES

  • 分组模式是 CBC

  • 填充方式是 PKCS7

  • 输出结果通常还会再做一次 Base64 编码

只要再把 keyiv 提取出来,本地复现通常就不会太困难。


二、JavaScript 版 AES 加解密逻辑

前端场景中,最常见的是 crypto-js
如果网页里使用的是 AES-CBC + Pkcs7Padding,那么本地复现时通常也会沿用同样的写法。

JS 示例源码

// 引入 crypto-js 库
var CryptoJS = require("crypto-js")

/**
 * 在 JavaScript 中,AES 加密常见有两种使用方式:
 * 1. 简单调用方式:通常只作了解
 * 2. 指定 key、iv、mode、padding 的完整方式:逆向场景中更常见
 */

// ==================== AES 加密 ====================

// 定义 AES 加密所需的 key 和 iv
var key = "1111111188888888";
var iv = "1234567887654321";

// 将 key、iv 和明文转换为 CryptoJS 可处理的字节对象
var keyBytes = CryptoJS.enc.Utf8.parse(key);
var ivBytes = CryptoJS.enc.Utf8.parse(iv);
var plainTextBytes = CryptoJS.enc.Utf8.parse("我爱周杰伦!");

// 执行 AES 加密,当前配置为 CBC 模式 + Pkcs7 填充
var encryptResult = CryptoJS.AES.encrypt(plainTextBytes, keyBytes, {
    iv: ivBytes,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
});

// 输出加密结果对象
console.log(encryptResult);

// 输出 Base64 格式的密文字符串
console.log(encryptResult.toString()); // tkSgq8Kgtdo+4UhQJWUKyY1MgZERrTfNRGcRLR+Vbkk=


// ==================== AES 解密 ====================

// 待解密的 Base64 密文
var cipherText = "tkSgq8Kgtdo+4UhQJWUKyY1MgZERrTfNRGcRLR+Vbkk=";

// 将 key 和 iv 转换为字节对象
var keyBytes = CryptoJS.enc.Utf8.parse(key);
var ivBytes = CryptoJS.enc.Utf8.parse(iv);

// 执行 AES 解密,参数配置需要与加密阶段保持一致
var decryptResult = CryptoJS.AES.decrypt(cipherText, keyBytes, {
    iv: ivBytes,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
});

// 方式一:直接转为 UTF-8 明文
console.log(decryptResult.toString(CryptoJS.enc.Utf8)); // 我爱周杰伦!

// 方式二:显式将 WordArray 转为 UTF-8 字符串
console.log(CryptoJS.enc.Utf8.stringify(decryptResult)); // 我爱周杰伦!

这段 JS 逻辑需要注意的点

1. keyiv 是固定值

这种场景通常最容易复现,因为 AES 所需参数直接写在前端逻辑中,只需要原样提取即可。

2. CryptoJS.enc.Utf8.parse() 用于将字符串转成字节对象

crypto-js 中,key、iv 和明文通常都要先转换成内部可处理的格式,然后再参与 AES 运算。

3. 加密和解密阶段的配置必须一致

这里使用的是:

  • CryptoJS.mode.CBC

  • CryptoJS.pad.Pkcs7

如果本地复现时这两个参数和目标站点不一致,最终结果就不会匹配。

4. encryptResult.toString() 一般得到的是 Base64 密文

很多前端实现里,AES 运算后的结果不会直接以原始字节形式传输,而是进一步转成 Base64 字符串。


三、Python 版 AES 加解密逻辑

在 Python 中,AES 复现通常使用 pycryptodome

Python 示例源码

# 安装依赖:
# pip install pycryptodome

# 导入 AES 模块
from Crypto.Cipher import AES

# 导入填充与去填充工具
from Crypto.Util.Padding import pad, unpad

# 导入 Base64 编码模块
import base64


# 定义需要加密的明文内容
plain_text = "我爱你,就像老鼠爱大米!"

# ================== AES 加密 ==================

# 创建 AES 密钥
# AES 密钥长度必须为 16、24 或 32 字节,分别对应 AES-128、AES-192、AES-256
secret_key = b'1111111188888888'

# 定义 CBC 模式下使用的 iv
iv = b'1234567891234567'

# 创建 AES 加密器
# 当前使用 CBC 模式
aes = AES.new(key=secret_key, mode=AES.MODE_CBC, iv=iv)

# 明文需要先转为字节,再按 16 字节分组进行 PKCS7 填充
padded_plain_text = pad(plain_text.encode('utf-8'), 16)
print(padded_plain_text)

# 执行 AES 加密,得到密文字节
cipher_bytes = aes.encrypt(padded_plain_text)

# 密文字节通常不会直接传输,一般会再做一次 Base64 编码
cipher_text_base64 = base64.b64encode(cipher_bytes).decode()

print('加密前的明文结果:', plain_text)
print('AES 加密后的结果:', cipher_text_base64)


# ================== AES 解密 ==================

# 1. 获取待解密的密文
cipher_text = cipher_text_base64

# 2. 对 Base64 密文进行解码,还原为密文字节
decoded_cipher_bytes = base64.b64decode(cipher_text)
print('Base64 解码后:', decoded_cipher_bytes)

# 3. 创建 AES 解密器
secret_key = b'1111111188888888'
iv = b'1234567891234567'
aes = AES.new(key=secret_key, iv=iv, mode=AES.MODE_CBC)

# 4. 执行 AES 解密,得到带填充的原始字节
decrypted_padded_bytes = aes.decrypt(decoded_cipher_bytes)
print('AES 解密后:', decrypted_padded_bytes)

# 5. 去除填充内容
decrypted_bytes = unpad(decrypted_padded_bytes, 16)
print("去除填充后:", decrypted_bytes)

# 6. 按 UTF-8 解码,还原为最终明文
decrypted_text = decrypted_bytes.decode("utf-8")

print('未解密之前的结果:', cipher_text_base64)
print('AES 解密后的明文结果:', decrypted_text)

这段 Python 逻辑需要注意的点

1. key 长度必须合法

AES 的密钥长度必须是:

  • 16 字节

  • 24 字节

  • 32 字节

这里使用的是 16 字节,对应 AES-128 。

2. CBC 模式必须提供 iv

如果使用的是 AES.MODE_CBC,除了 key 之外,还必须传入 iv。
缺少 iv 或 iv 不一致,都会导致结果错误。

3. 明文通常需要先填充

AES 是分组加密,分组长度通常为 16 字节。
如果明文长度不是 16 的整数倍,就需要先补位。这里使用的是 pad(..., 16)

4. Base64 只是密文展示形式

很多站点会把 AES 加密后的字节结果再做一次 Base64 编码,方便传输。
所以在解密之前,通常要先执行 Base64 解码 。


四、这类 AES 逻辑的标准处理流程

如果从流程上概括,这类 AES-CBC + PKCS7Padding + Base64 的处理过程通常可以整理成下面两条链路。

加密流程

明文字符串
-> UTF-8 编码
-> PKCS7 填充
-> AES-CBC 加密
-> Base64 编码
-> 最终密文

解密流程

Base64 密文
-> Base64 解码
-> AES-CBC 解密
-> 去除 PKCS7 填充
-> UTF-8 解码
-> 最终明文

只要把这条链路理顺,很多网页里的 AES 逻辑本质上都只是这个流程的不同写法而已。


五、逆向复现时最容易出问题的几个地方

在实际调试中,AES 复现最常见的问题通常集中在下面几个位置。

1. keyiv 写错

包括长度不合法、字符内容不一致、编码方式不一致。

2. mode 判断错误

最常见的是把 CBCECB 混淆。
模式不一致,结果一定不会匹配。

3. padding 不一致

有些站点使用 Pkcs7,有些站点可能使用其他填充方式。
这一点也必须和目标逻辑保持一致。

4. 密文格式判断错误

有些场景输出的是 Base64,有些输出的是 Hex。
如果格式判断错误,解密阶段通常会直接失败。

5. 字符串与字节混用

尤其在 Python 场景中,这一点非常容易出问题。
中文内容如果编码处理不一致,密文结果通常就会偏掉。


六、总结

在爬虫逆向里,AES 本身并不复杂,真正的关键在于参数定位。
只要能够确认前端使用的 keyivmodepadding,再结合密文的输出格式进行还原,本地复现这类加解密流程通常并不困难。

这类逻辑的价值并不在于“自己实现 AES 算法”,而在于能够快速识别目标站点采用的加密方案,并在本地稳定重放。
一旦本地复现成功,后续无论是请求参数加密还是响应结果解密,处理起来都会直接很多。