前端SDK逆向实战:Python实现AES-CBC解密私有协议数据

前端SDK逆向实战:Python实现AES-CBC解密私有协议数据
1. 项目缘起一个看似简单的逆向需求最近在做一个教育类应用的自动化测试项目客户那边用的是一个叫“气球云”的在线教育平台。测试过程中我需要模拟一些核心业务流程比如获取课程列表、提交作业状态等。理想情况下如果平台提供了完善的开放API我直接用requests库写几个脚本就搞定了。但现实是很多这类平台对核心数据的保护比较严格尤其是涉及到用户学习记录、题目内容这些敏感信息时它们往往不会提供明文接口而是通过自己封装的SDK通常是JavaScript或移动端SDK来与服务器通信通信过程中的数据很可能被加密了。我遇到的正是这种情况。在浏览器的开发者工具里我看到前端与后端交互的请求其payload请求体是一串毫无规律、看似乱码的字符响应数据也是一样。这明显不是常见的JSON或Form Data。追踪前端代码发现它加载了一个名为qiqiuyun-sdk.js的文件所有的请求构造和加解密逻辑都封装在里面。我的目标很明确理解这个SDK的加密机制并用Python实现对应的解密逻辑从而能够直接解析出服务器返回的原始数据方便后续的自动化处理和分析。这本质上是一个针对特定私有协议的前端SDK逆向工程任务。它不涉及破解任何安全算法那是违法的而是通过分析公开的、运行在用户浏览器中的JavaScript代码来理解其数据封装格式和加解密流程并实现一个功能对等的客户端。这对于自动化测试、数据迁移、第三方工具开发等场景是常见且合理的需求。2. 逆向分析第一步定位核心加解密函数面对一个混淆过的、压缩过的生产环境JS文件直接阅读是痛苦的。第一步是让代码“说话”。我将qiqiuyun-sdk.js文件保存到本地并使用Chrome DevTools的“Pretty print”美化打印功能或者用js-beautify这样的工具进行格式化使其结构变得清晰。格式化后我开始搜索关键词。对于加解密常见的线索有encrypt/decryptCrypto(Web Crypto API)AES,DES,RSAencode/decodecipher,iv,key一些自定义的函数名如_0x123abc在搜索过程中我发现了几个关键函数它们的命名经过混淆但通过其调用上下文比如在发送XMLHttpRequest或fetch请求前被调用处理数据在收到响应后被调用处理数据可以判断出它们就是目标。核心发现一加密模式通过跟踪函数调用栈和观察参数我确定了SDK主要使用AES对称加密算法。这是前后端数据加密非常常见的选择因为其加解密速度快适合对大量业务数据进行加密。通常AES会搭配一种分组模式如CBC、GCM和一个初始化向量IV来使用。核心发现二关键参数来源这是逆向中最关键的一步密钥Key和初始化向量IV从哪里来硬编码在代码中这是安全性最差的方式但一些早期或内部系统可能这么做。我全局搜索了十六进制字符串或Base64字符串没有发现固定的、明显的密钥。从首次请求的服务器响应中动态获取这是更常见的做法。我观察到在SDK初始化或第一个API请求后服务器会在响应头如X-Encrypt-Key或响应体的某个字段如一个握手协议的数据包中返回一个token或key。这个key很可能就是用于本次会话或一段时间内加解密的AES密钥或者是生成密钥的种子。由固定密钥动态因子派生结合了上述两点。可能存在一个写在代码里的固定盐值Salt或密钥与服务器下发的某个随机数Nonce或会话ID结合通过如PBKDF2这样的密钥派生函数生成最终的加密密钥。在我的案例中经过反复调试和日志输出我确认采用的是第二种方式会话密钥动态下发。SDK在初始化后会先与服务器进行一次“握手”服务器返回一个结构体其中包含了一个用于本次会话的aes_key可能是Base64编码的和一个aes_iv。注意这里的“握手”不一定是一个独立的HTTP请求它可能隐藏在登录接口的响应里或者第一个业务请求的响应头中。需要仔细追踪网络请求和SDK的状态变化。3. 算法与模式确认拆解JavaScript实现找到了关键函数和参数来源接下来就需要精确理解它们使用的算法、模式和填充方式。我通过console.log在关键函数中打印出输入、输出以及中间变量如key,iv,ciphertext并与Python端进行对照。具体分析过程如下我定位到一个名为_decryptData名称已脱敏的函数它接收两个参数加密的字符串encryptedData和一个options对象内含key和iv。// 示例JavaScript代码片段经过简化和脱敏 function _decryptData(encryptedData, options) { const key CryptoJS.enc.Base64.parse(options.key); const iv CryptoJS.enc.Base64.parse(options.iv); // 1. 将加密数据从Base64解码为WordArray const encryptedBytes CryptoJS.enc.Base64.parse(encryptedData); // 2. 使用AES-CBC模式解密 const decrypted CryptoJS.AES.decrypt( { ciphertext: encryptedBytes }, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); // 3. 将解密后的WordArray转为UTF-8字符串 return decrypted.toString(CryptoJS.enc.Utf8); }从这段代码可以清晰地读出以下信息算法 AES模式 CBC填充 PKCS7密钥/IV编码 传入的key和iv是Base64编码的字符串在解密前需要先用CryptoJS.enc.Base64.parse解析。密文编码encryptedData本身也是一个Base64编码的字符串。输出编码 解密后的原始字节流被当作UTF-8字符串解码。这几乎是一个标准的AES-CBC解密流程。难点往往在于一些“坑”比如字符编码 确保在Python端密钥、IV、密文在字节bytes和字符串str之间的转换与JavaScript端一致。JavaScript的CryptoJS库内部使用一种叫WordArray的格式处理二进制数据而Python使用bytes。Base64是两者间可靠的桥梁。IV的处理 在CBC模式下IV必须与加密时使用的IV完全相同。通常IV会随密文一起传输可能拼接在密文前或者像本例一样由服务器单独提供。4. Python实现使用cryptography库构建解密器有了明确的算法参数用Python实现就相对直接了。我选择使用Python生态中强大且现代的cryptography库。它比古老的pycryptodome有更清晰的API和更好的维护。首先安装必要的库pip install cryptography然后根据分析结果编写解密函数import base64 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from cryptography.hazmat.backends import default_backend def decrypt_qiqiuyun_data(encrypted_data_b64: str, key_b64: str, iv_b64: str) - str: 解密气球云SDK的AES-CBC加密数据。 Args: encrypted_data_b64: Base64编码的加密字符串。 key_b64: Base64编码的AES密钥通常是128位或256位。 iv_b64: Base64编码的初始化向量IV。 Returns: 解密后的原始字符串UTF-8格式。 # 1. 将Base64字符串解码为字节bytes key base64.b64decode(key_b64) iv base64.b64decode(iv_b64) ciphertext base64.b64decode(encrypted_data_b64) # 2. 创建AES-CBC解密器 # 注意CBC模式需要相同的IV cipher Cipher(algorithms.AES(key), modes.CBC(iv), backenddefault_backend()) decryptor cipher.decryptor() # 3. 执行解密得到填充后的明文字节 padded_plaintext decryptor.update(ciphertext) decryptor.finalize() # 4. 移除PKCS7填充 unpadder padding.PKCS7(algorithms.AES.block_size).unpadder() plaintext_bytes unpadder.update(padded_plaintext) unpadder.finalize() # 5. 将字节解码为UTF-8字符串并返回 return plaintext_bytes.decode(utf-8) # 示例用法 if __name__ __main__: # 这些key和iv需要从实际的网络请求中捕获 sample_key_b64 你的Base64编码密钥 sample_iv_b64 你的Base64编码IV sample_ciphertext_b64 加密的Base64数据 try: result decrypt_qiqiuyun_data(sample_ciphertext_b64, sample_key_b64, sample_iv_b64) print(解密成功:) print(result) except Exception as e: print(f解密失败: {e})代码关键点解析解码顺序 所有输入密文、密钥、IV都先经过base64.b64decode转换成原始的字节流。这是与JavaScript端CryptoJS.enc.Base64.parse对等的操作。算法与模式algorithms.AES(key)指定算法modes.CBC(iv)指定CBC模式及IV。backend参数通常使用默认即可。解密操作decryptor.update()和decryptor.finalize()是标准流程。update可以处理数据流对于一次性数据最后调用finalize完成解密。PKCS7解填充 这是非常关键且容易出错的一步。加密时明文长度必须是AES块大小16字节的整数倍不足的部分会用PKCS7规则填充。解密后必须用对应的unpadder将这个填充移除才能得到原始明文。cryptography库的padding模块让这个操作变得很简单。异常处理 解密过程可能因密钥错误、IV错误、密文损坏、填充错误等原因失败。务必用try...except包裹并打印详细的错误信息这对于调试至关重要。5. 实战调试与关键“踩坑”记录理论很美好但一次跑通的概率很低。以下是我在调试过程中遇到的主要问题和解决方案坑一密钥或IV的长度错误AES密钥的长度必须是16AES-128、24AES-192或32AES-256字节。IV必须是16字节。如果从Base64解码后的字节长度不符合解密器初始化就会直接报错。排查 打印len(key)和len(iv)。可能原因 Base64字符串可能包含换行符或空格或者服务器返回的本身就不是正确的Base64格式。确保在解码前做了必要的字符串清理如strip()。坑二密文长度不是16字节的整数倍在CBC模式下密文的长度也必须是16字节的整数倍。如果不是在decryptor.update(ciphertext)阶段就可能报错。排查 打印len(ciphertext)。可能原因 Base64解码不正确或者密文在传输过程中被截断或修改。确保捕获的是完整的、未经篡改的密文。坑三填充错误Invalid padding这是最常见的错误之一提示信息通常是Invalid padding bytes或类似。这意味着解密出来的数据的最后一块其填充字节不符合PKCS7规则。根本原因密钥、IV或密文三者中任何一个有误都会导致整个解密过程错位最终体现在填充错误上。所以填充错误是一个结果而不是原因。系统化排查步骤交叉验证 用同一个密钥、IV和密文在JavaScript环境中比如在浏览器控制台临时调用SDK的解密函数运行一次看是否能成功解密。这是最直接的验证方法。检查源头 再次确认你从网络请求中捕获的key_b64、iv_b64和encrypted_data_b64是否100%正确。特别注意HTTP响应是否是压缩的如gzip你的爬虫或抓包工具是否自动解压了有时原始数据需要解压后才能得到正确的Base64串。核对算法细节 确认JavaScript端使用的确实是CBC模式和PKCS7填充。我遇到过一些SDK使用ECB模式不推荐或NoPadding。如果模式不对解密出来的数据就是乱码。坑四解密成功但输出乱码如果解密过程没有报错但输出的字符串是乱码可能的原因有字符编码错误 解密后的字节流可能不是UTF-8。尝试其他编码如gbk,latin-1。或者解密出的根本就不是文本而是二进制数据如压缩后的数据、序列化后的对象。这时需要进一步分析其结构。数据还有一层封装 有些SDK在AES加密外层可能还有自定义的封装比如在密文前加了几个字节的版本号或长度信息。你需要观察JavaScript代码在调用CryptoJS.AES.decrypt之前是否对encryptedData做了额外的处理如字符串切片、Hex解码等。调试技巧搭建最小测试环境 在浏览器中手动复制一次成功的网络请求的key,iv,ciphertext到你的Python脚本中。确保环境变量一致。打印中间变量 在JavaScript端和Python端分别打印key,iv,ciphertext的Base64字符串和字节长度进行逐字节对比可以转成Hex对比。使用在线工具辅助 在完全确认算法参数AES-128-CBC, PKCS7后可以先用一个在线的AES加解密工具用你的key和iv去解密ciphertext验证这组参数本身是否正确。这能帮你快速定位问题是出在参数上还是代码实现上。6. 集成到自动化流程动态获取密钥解密函数写好后不能总靠手动粘贴密钥和IV。我们需要将其集成到自动化脚本中核心就是模拟SDK的行为动态获取会话密钥。这个过程的逻辑如下模拟初始化/登录 使用requests或selenium模拟用户登录或触发SDK初始化请求。拦截密钥响应 从登录响应或紧随其后的某个特定请求的响应中提取出aes_key和aes_iv。它们可能在JSON响应体的某个字段里也可能在响应头中。存储会话密钥 将这些密钥保存在一个会话级别的变量或对象中。发起业务请求并解密 在后续的业务请求中用获取到的密钥解密响应体。import requests import json # 假设你的解密函数在一个叫 qiqiuyun_decryptor 的模块里 from qiqiuyun_decryptor import decrypt_qiqiuyun_data class QiqiuyunClient: def __init__(self, username, password): self.session requests.Session() self.username username self.password password self.aes_key None self.aes_iv None self.login() def login(self): 模拟登录并获取会话密钥 login_url https://api.qiqiuyun.com/v1/login # 示例URL login_payload { username: self.username, password: self.password, client: web } resp self.session.post(login_url, jsonlogin_payload) resp.raise_for_status() # 假设登录响应是一个JSON其中包含了加密的data字段和密钥信息 login_result resp.json() # 情况1密钥直接放在响应体里 if encrypt_info in login_result: self.aes_key login_result[encrypt_info][key] self.aes_iv login_result[encrypt_info][iv] # 登录响应数据本身可能也是加密的需要解密 encrypted_data login_result[data] if encrypted_data: decrypted_data_str decrypt_qiqiuyun_data(encrypted_data, self.aes_key, self.aes_iv) user_info json.loads(decrypted_data_str) print(f登录成功用户: {user_info.get(nickname)}) # 情况2密钥在响应头里 elif X-Encrypt-Key in resp.headers: self.aes_key resp.headers[X-Encrypt-Key] self.aes_iv resp.headers[X-Encrypt-IV] # 假设也有这个头 print(从响应头获取密钥成功) else: # 可能需要触发另一个握手请求 self._handshake() def _handshake(self): 如果没有在登录时获取密钥可能需要单独的握手请求 handshake_url https://api.qiqiuyun.com/v1/handshake resp self.session.get(handshake_url) handshake_data resp.json() self.aes_key handshake_data[key] self.aes_iv handshake_data[iv] print(握手成功获取会话密钥) def get_course_list(self): 获取课程列表并自动解密响应 if not self.aes_key or not self.aes_iv: raise ValueError(会话密钥未初始化请先登录) course_url https://api.qiqiuyun.com/v1/courses resp self.session.get(course_url) resp.raise_for_status() # 假设业务接口的响应体直接就是加密的Base64字符串 encrypted_response_b64 resp.text try: decrypted_text decrypt_qiqiuyun_data(encrypted_response_b64, self.aes_key, self.aes_iv) course_list json.loads(decrypted_text) return course_list except json.JSONDecodeError as e: print(f解密成功但解析JSON失败: {e}) print(f解密后的文本: {decrypted_text[:200]}...) # 打印前200字符调试 return None except Exception as e: print(f解密过程出错: {e}) return None # 使用客户端 client QiqiuyunClient(your_username, your_password) courses client.get_course_list() if courses: for course in courses[:5]: print(course[name])这个QiqiuyunClient类封装了登录、密钥管理和自动解密的逻辑使得后续的业务调用就像调用普通API一样简单解密过程对使用者透明。7. 总结与扩展思考通过这个项目我们完成了一次完整的针对私有协议前端SDK的逆向与Python客户端实现。核心步骤可以总结为观察网络行为 - 定位前端加密代码 - 分析算法与参数 - Python对等实现 - 集成到自动化流程。这个过程有几个更深层次的要点值得分享合法与道德边界 我们分析的是运行在自己浏览器里的、公开的JavaScript代码目的是为了实现与官方客户端对等的功能用于合法的自动化测试或数据集成。绝对不要尝试破解或绕过身份认证不要对服务器进行暴力请求不要窃取非授权数据。一切操作应在你拥有合法权限的账户和接口范围内进行。逆向的通用性 这个方法不仅适用于“气球云”对于其他使用类似技术前端加密、私有协议的Web应用或小程序SDK同样有效。关键在于耐心地调试和分析JavaScript代码。熟练使用浏览器的“Sources”面板设置断点、查看调用栈、监控变量变化是这项工作的基本功。应对变化 服务端的加密策略可能会升级比如从AES-CBC切换到AES-GCM或者更换密钥交换机制。因此你的解密脚本需要有良好的日志记录和错误处理机制。一旦发现大量解密失败就需要重新进行第一步的逆向分析工作。性能与维护 对于频繁的请求加解密会成为性能开销的一部分。Python的cryptography库是C扩展性能已经很好。但在极端高并发下可能需要考虑异步IO或连接池。此外将密钥管理、解密逻辑封装成独立的服务或中间件有利于代码的维护和复用。最后这种技能是一把双刃剑。它极大地提升了我们在面对复杂系统时的集成和自动化能力但也要求我们对网络安全、密码学基础有更深的敬畏和理解。每一次成功的“解密”背后都是对系统设计者思路的一次理解这本身就是一个极佳的学习过程。

最新新闻

日新闻

周新闻

月新闻