JS逆向实战:死磕某蓝厂社区AES+RSA混合加密,从混淆代码到Python还原全流程
前言
在做数据采集和安全研究的过程中,我们经常会遇到各种前端加密防护。最近在对某“蓝厂”(懂的都懂,国内头部手机厂商)社区进行协议分析时,发现其登录及核心业务接口的请求参数不再是简单的MD5签名,而是采用了 AES加密数据 + RSA加密AES密钥 的混合加密方案。
这种方案在金融级应用和大型厂商中越来越常见。很多初学者在遇到这种情况时,往往因为找不到密钥生成逻辑或者无法处理非对称加密而卡壳。本文将从零开始,记录一次完整的逆向分析过程,包括加密点定位、混淆代码还原、算法识别以及最终的Python模拟实现。
⚠️ 免责声明
本文所涉及的技术仅用于安全研究与学习交流,请勿用于任何非法用途。文中出现的网址、接口均已做脱敏处理。尊重知识产权,遵守相关法律法规。
一、 抓包分析与加密特征识别
1.1 接口观察
打开浏览器开发者工具(F12),切换到Network面板,触发登录操作。我们可以观察到关键的login请求,其Form Data中包含两个核心加密字段:
encData: 一串很长的Base64编码字符串,目测是加密后的业务数据。encKey: 另一串Base64字符串,长度固定,疑似RSA加密后的密文。encVer: 版本号标识,如 “1_1_2”。
1.2 响应数据
不仅请求加密,响应体也是一串密文。这说明服务端和客户端约定了一套完整的加解密协议。如果我们不能还原这套协议,就无法正常解析返回的业务数据。
二、 加密入口定位
面对压缩混淆过的JS代码,全局搜索是最直接的手段。
2.1 关键字搜索
我们在Sources面板中全局搜索 encData。由于代码经过Webpack打包和混淆,变量名可能变成了单字母。但字符串常量通常不会被完全改变。
搜索后发现约4处匹配。通过观察上下文,我们发现一处赋值逻辑:
e.encData = t.ciphertext, e.encKey = n, e.encVer = "1_1_2"
这与我们抓包看到的结构完全一致!在此处打下断点,重新触发登录,成功断住。
2.2 调用栈回溯
断住后,查看右侧Call Stack(调用栈)。我们需要找到加密函数的“源头”。逐层向上点击,观察Scope中的变量值,直到找到一个函数,它的入参包含了我们的明文密码和手机号,而出参正是加密后的对象。
这个函数就是我们要逆向的核心加密入口。
三、 算法还原与代码剥离
3.1 识别加密库
在核心加密函数内部,我们可以看到大量特征代码。通过搜索 CryptoJS 或 AES 等关键字,确认该站点使用的是标准的 crypto-js 库。
但关键在于:AES的密钥是如何生成的?RSA的公钥写在哪里?
3.2 提取RSA公钥
在加密函数附近,我们发现了一个硬编码的长字符串,以 MIGfMA0GCSqGSIb3DQEBAQUAA4GN... 开头。这是标准的PKCS#1格式RSA公钥的Base64编码。将其保存下来,后续Python复现时需要用到。
3.3 梳理加密逻辑
通过单步调试,我们还原出如下加密流程:
- 生成一个随机的16字节字符串作为AES密钥(
aesKey)。 - 使用AES-CBC模式,以
aesKey为密钥,对JSON序列化后的表单数据进行加密,得到encData。 - 将
aesKey转为Hex字符串,使用预置的RSA公钥进行加密,得到encKey。 - 组装最终请求体。
💡 踩坑提示
在扣取JS代码时,不要试图把整个webpack模块都搬走。只提取加密相关的纯函数,并手动补齐navigator、window等必要的环境变量即可。过多的环境依赖会导致Node.js运行报错。
四、 Python协议还原
逆向的终点不是看懂JS,而是用Python稳定地模拟请求。以下是核心还原代码(已脱敏):
import json
import base64
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad
class LanChangCrypto:
def __init__(self):
# 从JS中提取的RSA公钥
self.rsa_public_key = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...(脱敏)...IDAQAB
-----END PUBLIC KEY-----"""
def encrypt(self, plain_data: dict) -> dict:
# 1. 生成随机AES密钥
aes_key = get_random_bytes(16)
# 2. AES加密业务数据
plain_text = json.dumps(plain_data, separators=(',', ':'))
cipher_aes = AES.new(aes_key, AES.MODE_CBC, iv=aes_key) # 注意:该站IV与Key相同
encrypted_data = cipher_aes.encrypt(pad(plain_text.encode(), AES.block_size))
enc_data = base64.b64encode(encrypted_data).decode()
# 3. RSA加密AES密钥
rsa_key = RSA.import_key(self.rsa_public_key)
cipher_rsa = PKCS1_v1_5.new(rsa_key)
enc_key = base64.b64encode(
cipher_rsa.encrypt(aes_key.hex().encode())
).decode()
return {
"encData": enc_data,
"encKey": enc_key,
"encVer": "1_1_2"
}
# 测试
crypto = LanChangCrypto()
payload = crypto.encrypt({"phone": "13800138000", "password": "test123"})
print(payload)
4.1 关键细节对齐
在还原过程中,最容易出错的是Padding方式和IV向量。
- 通过JS调试发现,该站AES使用的是
Pkcs7填充(Python的pycryptodome默认也是此填充)。 - IV并没有单独生成,而是直接复用了AES Key本身。这一点如果不通过动态调试,很难从静态混淆代码中直接看出来。
五、 响应数据解密
请求搞定后,响应解密就简单了。因为我们自己生成了AES密钥,所以只需要用同一个密钥对响应体进行AES-CBC解密即可。
需要注意的是,服务端返回的密文可能包含额外的校验位或头部信息,解密前可能需要先截取特定长度的字节。建议先在浏览器中断点观察解密函数的输入输出,确认密文的实际格式。
六、 总结与思考
本次某蓝厂社区的逆向实战,核心难点不在于算法本身(都是标准算法),而在于:
- 混合加密的定位:如何在海量混淆代码中快速找到AES和RSA的结合点。
- 细节参数的确认:IV是否等于Key?Padding模式是什么?公钥格式是否需要转换?这些都必须通过动态调试验证,不能靠猜。
- 环境最小化:扣代码时保持克制,只取必要逻辑,避免陷入补环境的泥潭。
给新手的建议:
不要过度依赖AST还原或自动化补环境框架。对于这种标准加密库的场景,手动调试+理解原理才是最快的路径。当你能够看着混淆代码就在脑海中勾勒出数据流向时,才算真正入了逆向的门。
🔗 参考资料
- CryptoJS官方文档
- PyCryptodome使用指南
- 《JavaScript逆向开发实战》
如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连支持!有任何问题欢迎在评论区交流讨论。
更多推荐
所有评论(0)