逆向JS混淆加密:从AES算法识别到Python模拟实战

📅 2026/8/26 10:58:23
逆向JS混淆加密:从AES算法识别到Python模拟实战
1. 项目概述逆向某混淆网站加密流程最近在分析一个数据接口时遇到了一个典型的JS混淆加密场景。网站对关键请求参数进行了复杂的加密处理常规的抓包、改包手段完全失效。核心的加密逻辑被隐藏在层层混淆的JavaScript代码中函数名、变量名都被替换成了无意义的字符代码结构也被打乱一眼看去如同天书。这种保护措施在涉及版权、用户数据或核心业务逻辑的网站中非常常见比如一些流媒体平台的播放凭证生成、电商平台的商品价格查询接口或是金融类应用的数据提交。我们的目标就是穿透这层迷雾逆向出完整的加密、解密以及密钥生成流程最终用Python或Node.js等后端语言实现一套功能完全一致的模拟程序从而能够自主构造合法的请求。这个过程不仅仅是“破解”更是一次深入理解前端安全机制和现代Web加密实践的绝佳机会。通过逆向我们能清晰地看到开发者是如何将AES等标准算法与自定义的混淆逻辑结合如何在前端安全与性能之间做出权衡以及有哪些常见的防御手段可以被绕过。无论你是从事爬虫开发、安全研究还是单纯对Web前端技术感兴趣掌握这套分析方法都极具价值。接下来我将以一个模拟的、但高度还原真实混淆场景的案例带你走完从静态分析、动态调试到算法还原、代码模拟的全过程。2. 逆向环境准备与初步侦察工欲善其事必先利其器。面对混淆代码一套顺手的工具链能极大提升效率。2.1 核心工具选型与配置我的分析环境主要基于浏览器开发者工具和代码格式化工具。浏览器首选最新版的Google Chrome或基于Chromium的Microsoft Edge。它们内置的开发者工具DevTools功能最强大、最稳定。务必关闭所有广告拦截或脚本管理插件防止其干扰目标网站的JS加载。开发者工具DevTools这是我们作战的主战场。重点关注以下几个面板Sources源代码用于查看、格式化、调试JavaScript文件。可以在这里设置断点单步执行。Network网络记录所有网络请求筛选XHR/Fetch请求找到携带加密参数的那个关键接口。查看其请求头Headers和请求体Payload。Console控制台执行JavaScript代码片段测试函数查看日志输出。在逆向中常用于动态调用可疑函数并查看结果。代码美化工具混淆的JS代码通常是一整行完全没有可读性。Chrome DevTools自带的“Pretty Print”功能Sources面板下点击{}图标是第一步。对于更复杂的混淆如变量名十六进制化、控制流平坦化可能需要借助本地工具如WebStorm、VS Code的JavaScript格式化插件或者在线工具进行初步整理。注意有些网站会检测开发者工具是否打开并跳转到空白页或抛出错误。遇到这种情况可以尝试在无痕模式下打开或者使用一些插件禁用检测。但这属于更高级的对抗本次案例暂不涉及。2.2 锁定加密入口与关键请求分析的第一步是找到加密发生的地方。打开目标网站进行触发加密接口的操作比如点击搜索、登录等。同时打开Network面板并勾选“Preserve log”保留日志。筛选请求在Network面板中使用筛选器选择“XHR”或“Fetch”。观察请求列表寻找那个响应时间点与你操作时间点吻合的、状态码为200或特殊的POST请求。识别加密参数点击该请求查看“Headers”和“Payload”。加密的参数通常具备以下特征参数名可能为data、encryptData、sign、cipher等。参数值是一长串毫无规律的、由字母和数字组成的字符串很像Base64编码的结果可能包含/或-_且长度通常是4的倍数或者是十六进制字符串。在“Preview”或“Response”标签中服务器返回的数据也可能是加密的同样表现为乱码或Base64字符串。全局搜索在Sources面板中使用全局搜索CtrlShiftF功能搜索你发现的加密参数名如encryptData。这能快速定位到设置该参数的JavaScript代码段这里很可能就是加密函数的调用点。在我的案例中我找到了一个向/api/submit发送的POST请求其请求体包含一个名为cipherText的字段值是一串Base64字符串。通过全局搜索cipherText我成功在某个巨大的、被混淆的app.min.js文件中找到了类似o[cipherText] f(x[data])的代码。这里的f函数极有可能就是加密函数。3. 深入混淆代码静态分析与动态调试找到疑似加密函数调用的代码只是开始真正的挑战在于理解被混淆的f函数内部逻辑。3.1 代码格式化与结构梳理首先在Sources面板中找到包含目标代码的JS文件点击{}进行美化。美化后的代码虽然有了换行和缩进但变量名可能仍然是a,b,c,_0x12ab,_0xdef1这种无意义的名称函数逻辑也可能被“控制流平坦化”打乱。控制流平坦化是一种常见的混淆技术它把原本顺序或分支执行的代码改造成一个巨大的switch-case或while循环通过一个“分发器”变量来决定下一个执行哪一块代码。这极大地增加了人工阅读的难度。静态分析策略寻找常量搜索明显的字符串常量如AES、CBC、PKCS5、数字常量如0x10、256或数组常量。这些往往是算法类型、密钥长度、初始向量IV长度或S盒等关键信息。识别标准算法特征AES加密通常涉及key、iv、mode、padding。RSA涉及modulus、publicExponent。搜索encrypt、decrypt、createCipher、createDecipher、CryptoJS、SubtleCrypto等关键词。函数调用追踪从f(x[data])这个调用点出发右键点击f选择“Go to definition”跳转到函数定义处。然后分析它的参数、返回值以及内部调用的其他函数。3.2 动态调试让代码自己“说话”当静态分析陷入僵局时动态调试是最强大的武器。其核心思想是在关键位置设置断点让程序在执行到此处时暂停然后我们可以查看当时所有变量的真实值、调用栈甚至可以修改运行逻辑。操作流程设置断点在Sources面板中找到疑似加密函数f的定义行在行号左侧点击设置一个断点蓝色标记。触发断点回到网页再次执行触发加密请求的操作如点击按钮。此时浏览器会自动暂停在断点处。观察与操作Scope作用域查看当前作用域下的所有变量及其值。这是获取密钥、IV、明文数据的黄金时刻。Call Stack调用栈查看函数是如何被一层层调用过来的有助于理解整体逻辑。Step Over/Into单步执行使用工具栏的按钮F10Step Over,F11Step Into逐行执行代码。Step Over会执行完当前行函数不进入其内部Step Into会跳进当前行调用的函数内部。这是理清复杂逻辑的关键。Console在暂停时你可以在Console中直接输入变量名来查看其值甚至可以调用当前作用域下的函数进行测试。例如输入f.toString()可以查看函数f的完整源代码即使它被混淆这比在编辑窗口看更直接。实战技巧我经常在加密函数入口、以及任何涉及key、iv、mode赋值的代码行设置断点。当程序暂停时我重点查看那些其值看起来像随机字符串或ArrayBuffer的变量。通过多次触发可能每次密钥不同观察这些值的变化规律。有时密钥或IV并非硬编码而是由另一个函数g()实时生成这时就需要对g()函数也进行同样的逆向分析。4. 核心算法还原AES的识别与模拟经过动态调试我确认目标网站使用的是AES-128-CBC加密算法并采用PKCS7填充模式在JS和Java中常叫PKCS5但填充方式相同。密钥Key和初始向量IV都是16字节128位。4.1 算法参数确认密钥Key调试时发现一个长度为16的字节数组或32位的十六进制字符串被传入了一个名为_0xabc[encrypt]的函数。这就是AES密钥。初始向量IV同样是一个16字节的数组在CBC模式下必不可少。它有时是固定的有时是随机生成并随着密文一起传输通常密文前16字节就是IV。模式与填充代码中找到了字符串CBC和Pkcs7作为参数。这明确指出了加密模式是CBC填充是PKCS7。4.2 密钥生成逻辑剖析更复杂的是密钥的生成方式。它并非固定不变而是由前端根据某些因子动态计算得出。通过跟踪调用栈我发现密钥生成函数generateKey()的逻辑如下获取当前用户的某个Token从Cookie或LocalStorage。获取一个时间戳并向下取整到最近的10秒Math.floor(Date.now() / 10000) * 10000。这是为了降低密钥的变化频率实现一个时间窗口内的密钥一致性。将Token和格式化后的时间戳字符串进行拼接如token | timestamp。对这个拼接字符串计算MD5哈希值。取MD5哈希值的前16个字符即前16字节128位作为本次AES加密的密钥。这种设计兼顾了“动态性”随时间变化和“可复现性”服务器端可以用同样的规则计算出密钥。服务器只需要知道Token和同样的时间窗口计算规则就能独立生成相同的密钥进行解密。4.3 加密流程还原完整的加密流程在代码中被封装成了一个函数function encryptData(plainText, userToken) { // 1. 生成密钥 const timestamp Math.floor(Date.now() / 10000) * 10000; const keyMaterial userToken | timestamp; const keyHex MD5(keyMaterial).substring(0, 32); // MD5输出32位十六进制 const key CryptoJS.enc.Hex.parse(keyHex); // 2. 生成IV (16字节随机数) const iv CryptoJS.lib.WordArray.random(16); // 3. AES-128-CBC 加密 const encrypted CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // 4. 组合结果: IV 密文然后转为Base64 const ivBase64 CryptoJS.enc.Base64.stringify(iv); const cipherTextBase64 CryptoJS.enc.Base64.stringify(encrypted.ciphertext); // 有时IV会放在密文前面一起做Base64 const finalResult ivBase64 cipherTextBase64; // 注意这里需要根据实际观察调整可能是 ivBase64 : cipherTextBase64 或直接拼接 return finalResult; }实操心得在动态调试时最好在encrypt函数执行后立即在Console中检查encrypted对象的各个属性如ciphertext、iv、key等并和最终发送的cipherText值进行比对确认其组合和编码方式是Hex还是Base64IV是单独传输还是拼接。我遇到的这个案例就是将IV和密文直接拼接后再做了一次Base64编码。5. 使用Python模拟完整加密流程逆向分析的最终目的是实现自主模拟。这里我们用Python的cryptography库来复现上述流程。5.1 环境搭建与库安装首先确保安装了必要的库pip install cryptography pycryptodomecryptography是主流且维护积极的加密库pycryptodome是另一个功能全面的库二者择一即可本文使用cryptography。5.2 Python模拟代码实现import base64 import hashlib import time from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from cryptography.hazmat.backends import default_backend import os class WebsiteCryptoSimulator: def __init__(self, user_token: str): 初始化模拟器需要用户Token。 self.user_token user_token def _generate_key_and_iv(self) - (bytes, bytes): 模拟前端密钥生成逻辑。 返回: (key_bytes, iv_bytes) # 1. 计算时间戳向下取整到10秒 current_ts int(time.time() * 1000) # 毫秒时间戳 windowed_ts (current_ts // 10000) * 10000 # 2. 拼接密钥材料 key_material f{self.user_token}|{windowed_ts} # 3. 计算MD5并取前16字节作为密钥 m hashlib.md5() m.update(key_material.encode(utf-8)) key_hex m.hexdigest() # 32位十六进制字符串 key_bytes bytes.fromhex(key_hex[:32]) # 取前32个十六进制字符即16字节 # 4. 生成16字节随机IV (模拟CryptoJS的random) iv_bytes os.urandom(16) return key_bytes, iv_bytes def encrypt(self, plaintext: str) - str: 模拟前端AES-128-CBC加密流程。 参数: plaintext: 待加密的明文字符串。 返回: Base64编码的字符串格式为: Base64(IV) Base64(Ciphertext) # 生成密钥和IV key, iv self._generate_key_and_iv() # 准备加密器 cipher Cipher(algorithms.AES(key), modes.CBC(iv), backenddefault_backend()) encryptor cipher.encryptor() # PKCS7填充 padder padding.PKCS7(algorithms.AES.block_size).padder() padded_data padder.update(plaintext.encode(utf-8)) padder.finalize() # 加密 ciphertext encryptor.update(padded_data) encryptor.finalize() # 组合并Base64编码 (IV Ciphertext) combined iv ciphertext result_base64 base64.b64encode(combined).decode(utf-8) return result_base64 def decrypt(self, ciphertext_b64: str) - str: 解密函数用于验证或处理服务器返回的密文。 注意此函数假设密文是 Base64(IV Ciphertext) 的格式。 combined base64.b64decode(ciphertext_b64) # 分离IV和密文 iv combined[:16] actual_ciphertext combined[16:] # 生成密钥解密需要同样的密钥生成逻辑 key, _ self._generate_key_and_iv() # IV从密文中获取这里不需要 # 准备解密器 cipher Cipher(algorithms.AES(key), modes.CBC(iv), backenddefault_backend()) decryptor cipher.decryptor() # 解密 padded_plaintext decryptor.update(actual_ciphertext) decryptor.finalize() # 去除PKCS7填充 unpadder padding.PKCS7(algorithms.AES.block_size).unpadder() plaintext unpadder.update(padded_plaintext) unpadder.finalize() return plaintext.decode(utf-8) # 使用示例 if __name__ __main__: # 假设从Cookie或Storage中获取的Token token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 simulator WebsiteCryptoSimulator(token) plain_data {page: 1, keyword: test} # 加密 encrypted_result simulator.encrypt(plain_data) print(f加密结果: {encrypted_result}) # 解密自我验证 decrypted_result simulator.decrypt(encrypted_result) print(f解密结果: {decrypted_result}) print(f加解密是否一致: {plain_data decrypted_result})5.3 代码关键点解析时间戳对齐_generate_key_and_iv函数中的(current_ts // 10000) * 10000是关键它确保了在10秒的时间窗口内生成的密钥是一致的。必须与前端逻辑严格对齐否则服务器解密会失败。密钥生成使用hashlib.md5模拟前端的MD5计算。注意编码一致性前端拼接字符串时是UTF-8还是其他编码需要确认。加密流程使用cryptography库的CipherAPI选择AES算法、CBC模式。PKCS7填充通过padding.PKCS7对象手动处理。IV处理IV是随机生成的并前置于密文之前然后整体进行Base64编码。这是CBC模式下一种常见的传输方式方便解密方分离。编码解码注意bytes、hex、base64之间的转换。前端CryptoJS默认使用WordArray对象在转换为Base64时可能与Python的base64.b64encode有细微差别如是否包含换行符通常需要设置decode(utf-8)获取纯字符串。6. 常见问题排查与实战技巧在逆向和模拟过程中你几乎一定会遇到各种问题。下面是我总结的一些常见坑点和解决思路。6.1 密文比对不一致这是最常遇到的问题。你的Python代码生成的密文和浏览器里生成的密文即使输入相同结果也不同。排查清单密钥不一致时间戳检查时间戳的生成规则是否完全一致单位是毫秒还是秒取整窗口是多少。可以分别在Console和Python中打印出用于生成密钥的时间戳和中间字符串进行比对。Token/源数据确认用于生成密钥的源数据如Token、设备ID等是否完全一致。注意是否有URL编码、大小写等问题。哈希算法与截取确认MD5计算前字符串的编码以及截取长度是取16字节还是32位十六进制字符的前16位。IV不一致确认IV是固定的、从服务器获取的、还是随机生成的。如果是随机的那每次密文必然不同但这不影响解密。你需要确认的是IV的拼接和编码方式。是放在密文前、后还是作为单独的参数传输是Hex还是Base64加密参数不一致算法真的是AES吗是128位、192位还是256位模式是CBC、ECB还是GCMGCM模式还会产生认证标签。填充是PKCS7、ZeroPadding还是NoPadding填充错误会导致解密失败或最后字节错误。字符编码明文在加密前转换成字节时用的什么编码UTF-8GBK输出编码不一致最终输出的密文是Base64编码、Hex编码还是Base64URL编码将/替换为-_且去掉调试技巧在浏览器加密函数的入口处和出口处设置断点。在入口处记录下明文、密钥Key、IV的原始值可能是WordArray将其转为Hex或Base64打印出来。在Python脚本中在加密前也打印出这些值。进行逐项比对总能找到差异所在。6.2 解密时遇到Padding错误在Python端解密时可能会报错Invalid padding bytes或类似错误。可能原因密钥或IV错误这是根本原因。如果密钥或IV不对解密出来的字节串就是乱的最后的填充字节自然也不符合PKCS7规范。密文被篡改或编码错误Base64解码失败或者传输过程中密文被截断、多出空格等。填充模式不匹配前端用的是ZeroPadding但你用PKCS7去解。解决方案先确保加密能自闭环即用你的Python加密再用你的Python解密成功。然后再用浏览器的密文用你的Python解密。如果失败回到上一步进行逐项比对。6.3 应对代码混淆升级网站可能会更新混淆策略增加反调试、代码动态加载等机制。进阶策略Hook关键函数在Console中重写标准函数如CryptoJS.AES.encrypt在其内部加入日志记录所有参数和结果。var _encrypt CryptoJS.AES.encrypt; CryptoJS.AES.encrypt function(plaintext, key, cfg){ console.log([Hook] Plaintext:, plaintext); console.log([Hook] Key:, key); console.log([Hook] Config:, cfg); var result _encrypt(plaintext, key, cfg); console.log([Hook] Result:, result); return result; }“油猴脚本”自动化编写Tampermonkey脚本在页面加载时自动注入你的Hook代码无需每次手动操作。本地替换将混淆的JS文件下载到本地使用反混淆工具如de4js、jsnice进行初步反混淆然后阅读和修改逻辑再通过Fiddler、Charles等代理工具将线上请求的JS文件映射到本地修改后的版本进行深度调试。6.4 效率与工程化考虑当需要大规模、稳定地调用该接口时模拟代码需要进一步优化。密钥缓存由于密钥在时间窗口内不变可以将其缓存起来避免每次请求都重复计算MD5和时间戳取整。错误重试与时间同步如果服务器时间与本地时间有微小偏差可能导致密钥无效。实现一个简单的错误重试机制如果解密失败或服务器返回特定错误码将本地时间戳向前或向后调整一个时间窗口如10秒再重试。封装为SDK或服务将加密解密逻辑封装成独立的类或模块甚至提供一个简单的HTTP服务供其他业务系统调用。日志与监控记录加密失败、解密失败的日志便于排查线上问题。逆向工程是一个需要耐心和细致观察的过程。没有一套固定的方法能解决所有问题核心在于理解加密的基本原理熟练使用开发者工具进行动态分析并具备严谨的比对和调试能力。当你成功模拟出一个复杂的加密流程时那种成就感是无与伦比的同时你对Web安全、前后端交互的理解也会深刻得多。记住技术是用来学习和提升的请务必在合法合规的范围内使用这些知识。