前言
在接口逆向过程中,AES 是非常常见的一类对称加密方案。
很多站点在请求发送之前,会先对参数进行 AES 加密;有些站点在接口返回之后,还需要先对密文结果进行 AES 解密,才能拿到真实业务数据。
对于这类场景,真正影响复现结果的,并不是“会不会调用加密库”,而是下面这些参数是否能够和目标站点保持一致:
keyivmodepadding密文输出格式
只要这些参数能够完全对齐,网页中的 AES 加解密逻辑通常都可以在本地直接复现。
这篇文章整理两份最基础的示例代码,一份使用 JavaScript,一份使用 Python。
代码不做额外封装,只保留最直接的加解密过程,方便在逆向时逐步验证每一个环节。
一、AES 逆向时重点看的是什么
在实际逆向中,AES 本身并不是难点。
真正决定复现是否成功的,通常是下面几个点是否和目标站点保持一致:
密钥
key偏移量
iv加密模式
mode填充方式
padding密文编码格式
例如前端很常见的一类写法如下:
CryptoJS.AES.encrypt(data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
如果已经定位到这一类代码,那么基本可以确认:
使用的是 AES
分组模式是 CBC
填充方式是 PKCS7
输出结果通常还会再做一次 Base64 编码
只要再把 key 和 iv 提取出来,本地复现通常就不会太困难。
二、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. key 和 iv 是固定值
这种场景通常最容易复现,因为 AES 所需参数直接写在前端逻辑中,只需要原样提取即可。
2. CryptoJS.enc.Utf8.parse() 用于将字符串转成字节对象
在 crypto-js 中,key、iv 和明文通常都要先转换成内部可处理的格式,然后再参与 AES 运算。
3. 加密和解密阶段的配置必须一致
这里使用的是:
CryptoJS.mode.CBCCryptoJS.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. key 或 iv 写错
包括长度不合法、字符内容不一致、编码方式不一致。
2. mode 判断错误
最常见的是把 CBC 和 ECB 混淆。
模式不一致,结果一定不会匹配。
3. padding 不一致
有些站点使用 Pkcs7,有些站点可能使用其他填充方式。
这一点也必须和目标逻辑保持一致。
4. 密文格式判断错误
有些场景输出的是 Base64,有些输出的是 Hex。
如果格式判断错误,解密阶段通常会直接失败。
5. 字符串与字节混用
尤其在 Python 场景中,这一点非常容易出问题。
中文内容如果编码处理不一致,密文结果通常就会偏掉。
六、总结
在爬虫逆向里,AES 本身并不复杂,真正的关键在于参数定位。
只要能够确认前端使用的 key、iv、mode 和 padding,再结合密文的输出格式进行还原,本地复现这类加解密流程通常并不困难。
这类逻辑的价值并不在于“自己实现 AES 算法”,而在于能够快速识别目标站点采用的加密方案,并在本地稳定重放。
一旦本地复现成功,后续无论是请求参数加密还是响应结果解密,处理起来都会直接很多。