JS逆向实战:AES加密原理、CryptoJS分析与Python解密复现

发布时间:2026/7/1 21:58:20
JS逆向实战:AES加密原理、CryptoJS分析与Python解密复现 1. 项目概述当爬虫遇上AES加密最近在分析一个数据源时又碰到了老朋友——AES加密。请求参数或者返回的数据不再是明晃晃的JSON字符串而是一串看着像乱码的密文。对于刚接触JS逆向的朋友来说这堵“加密墙”往往就是第一个拦路虎。今天我就结合一个实战案例把AES加密在JavaScript中的实现原理、逆向分析方法以及如何在Python中复现解密过程掰开揉碎了讲清楚。无论你是想学习JS逆向的爬虫工程师还是对前端加密实现好奇的开发者这篇文章都能给你一套清晰的“解题思路”和“操作手册”。我们不止要会用工具更要明白背后的门道这样下次遇到变种的加密方式你也能自己找到突破口。2. AES加密核心原理与模式解析要逆向AES首先得知道它正着是怎么工作的。AESAdvanced Encryption Standard是一种对称加密算法意思是加密和解密用的是同一把钥匙我们称之为密钥。它的核心思想是把数据分成一个个固定大小的“块”然后通过多轮的“混淆”和“扩散”操作让原始数据和密钥充分混合最终输出密文。2.1 关键参数密钥、初始向量与填充AES加密不是简单地输入明文和密钥就能出密文它依赖于几个关键参数理解这些是逆向的基础密钥Key这是加密解密的根本。AES标准支持三种长度的密钥128位16字节、192位24字节和256位32字节。密钥越长安全性越高但计算也稍慢。在Web前端为了兼顾安全与性能128位和256位比较常见。初始向量IV, Initialization Vector这不是所有模式都需要的但在最常见的CBCCipher Block Chaining模式下IV至关重要。它是一个随机生成的、长度与加密块大小AES固定为128位即16字节相同的字节序列。IV的作用是确保即使相同的明文用相同的密钥加密每次产生的密文也不同防止攻击者通过对比密文模式来推测信息。在逆向时如果发现每次请求的密文都不同但用同一个密钥又能解密那几乎可以肯定使用了CBC模式且有随机IV。填充PaddingAES是块加密一次处理一个128位的块。如果明文长度不是16字节的整数倍怎么办这就需要填充。常见的填充方式有PKCS#7也叫PKCS#5。它的规则很简单缺几个字节就用几来填充。例如一个15字节的数据需要补1个字节填充值就是0x01如果是14字节就补两个0x02。解密后需要正确移除这些填充字节才能得到原始数据。注意在JavaScript的CryptoJS库或Node.js的crypto模块中默认的填充方式往往是PKCS#7。但在一些“魔改”的加密实现里开发者可能会自定义填充规则这是逆向中的一个常见坑点。2.2 工作模式CBC与ECBAES有不同的工作模式决定了块与块之间如何关联。逆向时必须确定模式。ECBElectronic Codebook最简单的模式每个数据块独立加密。相同的明文块会产生相同的密文块。这会导致模式泄露安全性很差在Web中已很少用于敏感数据但逆向时如果发现没有IV参数可能就是它。CBCCipher Block Chaining目前最常用的模式。它引入了IV并且每个明文块在加密前会先与前一个密文块进行异或操作第一个块与IV异或。这种链式结构让密文块之间产生了依赖安全性大大增强。绝大多数网站的前端AES加密都采用CBC模式。在逆向分析时我们首要目标就是找到这三个要素密钥Key、初始向量IV和工作模式通常是CBC。它们可能硬编码在JavaScript文件里也可能通过某个接口动态获取或者由其他参数计算得出。3. JavaScript中的AES加密实现与逆向切入点前端实现AES加密最常用的库是CryptoJS。它是一个功能强大的加密算法库提供了简洁的API。理解它的常见用法就等于拿到了逆向的“地图”。3.1 CryptoJS的典型用法一个标准的CryptoJS AES-CBC加密代码示例如下// 引入CryptoJS在Web中通常通过CDN // script srchttps://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js/script // 假设这是要加密的数据 var plainText {page:1,size:20}; // 定义密钥和IV这里以字符串形式实际会转换为WordArray var key CryptoJS.enc.Utf8.parse(1234567890123456); // 16字节密钥 var iv CryptoJS.enc.Utf8.parse(abcdefghijklmnop); // 16字节IV // 执行AES-CBC加密 var encrypted CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 // 默认就是Pkcs7常省略 }); // 将加密结果转换为Base64字符串常见传输格式 var ciphertextBase64 encrypted.toString(); console.log(ciphertextBase64); // 输出类似U2FsdGVkX1...逆向时的关键观察点CryptoJS.enc.Utf8.parse这是将普通字符串转换为CryptoJS内部WordArray对象的方法。逆向时搜索encrypt函数往前找它的参数很可能会找到类似的parse调用其参数就是明文的Key或IV。加密选项对象{iv: iv, mode: ...}这个对象明确指明了加密模式和IV。如果代码里没写mode和padding那大概率就是默认的CBC和PKCS#7。encrypted.toString()默认输出的是OpenSSL兼容格式的字符串。它不仅仅是Base64编码的密文实际上是一个特殊结构以Salted__开头如果使用了盐值或者直接是密文的Base64。在简单场景下我们直接拿到这个字符串作为密文。3.2 逆向分析实战从混淆代码中定位关键参数实际网站的JavaScript代码通常是经过压缩和混淆的变量名变成a, b, c, d逻辑也变得难以阅读。但加密函数的调用痕迹很难完全抹去。以下是逆向排查的步骤搜索关键词在开发者工具的Sources面板中全局搜索CtrlShiftF以下关键词encryptAESCryptoJS(如果库被直接引用)mode、iv、padding有时密钥是固定字符串也可以搜索疑似密钥的字符串如长段的16、24、32位字符。设置XHR断点在Network面板找到发送加密数据的请求通常是XHR或Fetch右键选择“Break on - XHR/Fetch”。当请求发起时代码执行会自动暂停在发送请求的前一刻。此时调用栈Call Stack会显示完整的函数调用链你可以一步步往回向上跟踪找到数据是在哪个函数里被加密的。分析加密函数找到疑似加密函数后重点观察输入什么数据被传入了这个函数通常是包含查询参数或表单数据的对象。输出这个函数返回了什么一个字符串很可能就是加密后的密文。内部逻辑函数内部有没有调用类似CryptoJS.AES.encrypt的方法其参数key, iv是硬编码的字符串还是来自其他变量或函数计算如果是计算得来的需要继续追溯这些变量的来源。还原关键参数这是核心。假设你找到了这样一行混淆代码var c f.encrypt(b, g, {iv: h});你需要去查看b明文、g密钥、hIV的值是什么。在调试器里将鼠标悬停在变量上或是在Console中直接输入变量名并回车就能看到其当前值。记录下这些值特别是g和h它们很可能就是我们要找的Key和IV。实操心得很多网站的Key和IV并不是直接写在当前JS文件里的。它们可能由服务器下发的某个令牌Token通过某种哈希算法如MD5、SHA256生成或者由页面上的某个固定元素如时间戳、用户ID拼接后再哈希。这时你需要顺着代码逻辑找到生成Key/IV的函数并用Python复现其生成逻辑。一个常见模式是key CryptoJS.MD5(某个字符串).toString().substr(0, 16)。4. 在Python中复现AES解密流程在浏览器里找到了Key和IV验证了加密逻辑下一步就是在爬虫脚本通常是Python中复现解密过程让程序能自动化解密返回的密文。这里我们使用Python中最常用的加密库pycryptodome。4.1 环境准备与基础解密首先安装库pip install pycryptodome假设我们从服务器获得了一个Base64编码的密文并且已知Key和IV均为16字节的字符串from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 # 已知的参数从JS逆向中获得 key_str 1234567890123456 # 密钥字符串 iv_str abcdefghijklmnop # 初始向量字符串 ciphertext_b64 U2FsdGVkX19qBzV5H8lT6M8e5zZvJ7wP/... # 模拟的Base64密文 # 将字符串转换为字节类型 key key_str.encode(utf-8) iv iv_str.encode(utf-8) # 解码Base64密文 ciphertext_bytes base64.b64decode(ciphertext_b64) # 创建AES解密器使用CBC模式 cipher AES.new(key, AES.MODE_CBC, iv) # 执行解密 decrypted_padded cipher.decrypt(ciphertext_bytes) # 移除PKCS#7填充 plaintext_bytes unpad(decrypted_padded, AES.block_size) # 将解密后的字节转换为字符串 plaintext plaintext_bytes.decode(utf-8) print(f解密结果{plaintext})4.2 处理OpenSSL格式密文一个非常重要的细节是CryptoJS的encrypt方法默认返回的字符串是经过Salt处理的OpenSSL兼容格式。如果你直接把整个字符串做Base64解码然后解密会失败。这种格式的密文结构是Salted__ 8字节盐值(salt) 实际密文。我们需要先分离出盐值和实际密文def decrypt_openssl_format(ciphertext_b64, key_str, iv_str): 解密CryptoJS默认生成的OpenSSL格式密文 key key_str.encode(utf-8) iv iv_str.encode(utf-8) ciphertext_bytes base64.b64decode(ciphertext_b64) # 检查是否是Salted开头的OpenSSL格式 if ciphertext_bytes.startswith(bSalted__): salt ciphertext_bytes[8:16] # 8字节盐值 actual_ciphertext ciphertext_bytes[16:] # 真正的密文 # 使用盐值和密码生成实际的Key和IV (使用OpenSSL的EVP_BytesToKey方法) # 注意这里演示的是CryptoJS的默认密钥派生方式。有些场景可能直接使用提供的key/iv无需此步骤。 # 更安全的做法是使用专门的库如Crypto.Protocol.KDF中的PBKDF2但CryptoJS默认是较旧的EVP。 # 如果逆向时发现key/iv是直接指定的字符串则跳过此派生步骤直接用上面的简单方法。 from Crypto.Protocol.KDF import PBKDF1 from hashlib import md5 # 简化演示实际CryptoJS的EVP_BytesToKey迭代一次使用MD5 # 关键密码是key_str盐是salt生成161632字节数据前16位为key后16位为iv derived PBKDF1(key_str.encode(utf-8), salt, 16, count1, hashfuncmd5) # 生成16字节 derived PBKDF1(key_str.encode(utf-8) derived, salt, 16, count1, hashfuncmd5) # 再生成16字节 key derived[:16] iv derived[16:32] cipher AES.new(key, AES.MODE_CBC, iv) decrypted_padded cipher.decrypt(actual_ciphertext) else: # 如果不是Salted格式直接使用提供的key和iv解密 cipher AES.new(key, AES.MODE_CBC, iv) decrypted_padded cipher.decrypt(ciphertext_bytes) # 移除填充 plaintext_bytes unpad(decrypted_padded, AES.block_size) return plaintext_bytes.decode(utf-8) # 使用示例 result decrypt_openssl_format(ciphertext_b64, key_str, iv_str) print(result)这里有个极易踩坑的点很多教程和实际网站的实现并不一致。有些网站虽然用了CryptoJS但在调用encrypt时传入的key和iv已经是WordArray对象通过CryptoJS.enc.Utf8.parse转换这时CryptoJS默认不会使用Salt和EVP_BytesToKey派生而是直接使用你提供的Key和IV。这种情况下你直接用最开始的简单解密方法4.1节就能成功。如何判断一个方法是看JS代码里有没有设置format: CryptoJS.format.OpenSSL选项或者直接看生成的密文Base64字符串如果以U2FsdGVkX1开头这是Salted__的Base64编码就是带了Salt的格式。核心技巧最稳妥的方法是在浏览器控制台做实验。用你找到的Key、IV和加密函数加密一段已知的明文如test得到密文A。然后用你的Python解密脚本用同样的Key和IV去解密密文A。如果成功得到test说明你的解密逻辑正确如果失败再尝试处理OpenSSL格式。这是验证逆向结果的金标准。5. 实战案例拆解模拟一个常见的加密请求假设我们要爬取一个网站其查询API的请求参数data是经过AES加密的。我们通过开发者工具定位到了加密函数位于一个叫encryptData的混淆函数里。浏览器端分析过程在发送请求的XHR处打上断点刷新页面触发请求。在调用栈中找到encryptData函数单步进入。发现其内部核心代码如下已稍作反混淆function encryptData(paramObj) { var key CryptoJS.MD5(window._global_token).toString().substr(0, 16); var iv CryptoJS.enc.Utf8.parse(1234567812345678); var plaintext JSON.stringify(paramObj); var encrypted CryptoJS.AES.encrypt(plaintext, CryptoJS.enc.Utf8.parse(key), {iv: iv}); return encrypted.toString(); }分析得知Key生成取一个全局变量window._global_token可能由登录后接口返回计算其MD5值并取前16个字符作为密钥。这说明密钥是动态的但算法固定。IV固定字符串1234567812345678。模式与填充未指定即默认CBC和PKCS#7。输出直接返回encrypted.toString()即OpenSSL格式的密文字符串。Python端复现现在我们需要在爬虫中模拟这个加密过程生成合法的data参数。import json import hashlib from Crypto.Cipher import AES import base64 def encrypt_request_data(param_dict, token): 模拟JS端的encryptData函数 param_dict: 要加密的参数字典 token: 从登录接口获取的_global_token # 1. 生成Key (MD5(token)取前16位) key_md5 hashlib.md5(token.encode(utf-8)).hexdigest() key_str key_md5[:16] # 取前16个字符 key key_str.encode(utf-8) # 2. 固定IV iv_str 1234567812345678 iv iv_str.encode(utf-8) # 3. 准备明文JSON字符串 plaintext json.dumps(param_dict, separators(,, :), ensure_asciiFalse) # separators参数用于移除JSON中的空格与JSON.stringify行为一致 plaintext_bytes plaintext.encode(utf-8) # 4. 进行PKCS#7填充 from Crypto.Util.Padding import pad padded_bytes pad(plaintext_bytes, AES.block_size) # 5. 创建加密器并加密 cipher AES.new(key, AES.MODE_CBC, iv) ciphertext_bytes cipher.encrypt(padded_bytes) # 6. 转换为OpenSSL兼容格式Salted__ 随机盐 密文 # 注意CryptoJS.encrypt当key是WordArray时默认不使用Salt。 # 但根据我们看到的JS代码它直接用了key和iv没有Salt。 # 然而encrypted.toString()默认输出OpenSSL格式。为了完全匹配 # 我们需要模拟生成一个不带Salt的OpenSSL格式实际上如果key是WordArrayCryptoJS默认格式是CipherParams对象。 # 更准确的测试发现当key是字符串时CryptoJS会使用Salt当key是WordArray时不会。 # 我们的JS代码中key是WordArray(CryptoJS.enc.Utf8.parse(key))所以不会加Salt。 # 但encrypted.toString()的结果是什么我们直接在浏览器控制台测试。 # 假设测试发现直接返回ciphertext_bytes的Base64就能用那我们就不包装。 # 最可靠的方案在浏览器控制台运行加密看输出格式。 # 假设我们发现输出是纯Base64密文没有Salted__头则 ciphertext_b64 base64.b64encode(ciphertext_bytes).decode(utf-8) return ciphertext_b64 # 使用示例 token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # 模拟获取到的token params {page: 1, keyword: 爬虫} encrypted_data encrypt_request_data(params, token) print(f加密后的data参数{encrypted_data}) # 然后将encrypted_data作为表单的data字段发送即可这个案例清晰地展示了从逆向分析到Python复现的完整闭环定位加密函数 - 分析密钥/IV生成逻辑 - 在Python中复现该逻辑 - 生成加密参数。6. 常见问题排查与进阶技巧在实际操作中你几乎一定会遇到各种问题。下面是一些常见错误和排查思路问题现象可能原因排查步骤与解决方案Python解密报错ValueError: Invalid padding bytes.1. 密钥或IV错误。2. 密文格式不对如包含非Base64字符。3. 加密模式或填充方式不匹配。4. 密文在传输中被修改如URL编码问题。1.核对Key/IV确保与JS中使用的完全一致包括字符串编码通常UTF-8。2.检查密文确保Base64解码前字符串正确无换行、空格。尝试在浏览器中加密一个短字符串用同样的Key/IV在Python中解密验证基础流程。3.确认模式与填充JS默认CBCPKCS7Python需对应。如果JS用了ECBPython需改为AES.MODE_ECB且无需IV。4.处理URL编码如果密文作为URL参数传递可能被URL编码。需先urllib.parse.unquote解码。解密出的中文是乱码解密后的字节流用错误的编码解码。解密后得到plaintext_bytes尝试不同的编码解码plaintext_bytes.decode(utf-8)、decode(gbk)、decode(latin-1)。通常UTF-8是正确的。每次加密结果都不同但同一个密钥能解密使用了CBC模式且IV是随机生成的。这是正常现象。逆向时需要找到IV是如何生成或传递的。IV可能1. 硬编码在JS里。2. 由服务器在首次请求时返回。3. 由时间戳等参数通过哈希生成。你需要复现IV的生成逻辑。在JS里能加密解密Python复现失败1. Key/IV的字符串到字节的转换不一致。2. 使用了Salt和密钥派生EVP_BytesToKey但Python未复现。3. 自定义了加密模式或填充如CTR模式、ZeroPadding。1. 确保JS中的CryptoJS.enc.Utf8.parse(key)对应Python的key.encode(utf-8)。2. 仔细检查JS代码看是否有format: CryptoJS.format.OpenSSL或类似设置。用4.2节的方法处理Salt。3. 在JS加密函数处仔细查看选项对象确认mode和padding的值。找不到明显的CryptoJS或encrypt调用1. 加密逻辑被深度混淆或封装。2. 使用了Web Crypto API等原生接口。3. 加密在WebAssembly中实现。1. 使用XHR断点法从网络请求发起处反向追踪。2. 搜索subtle.encrypt、window.crypto等关键词。3. Wasm逆向较复杂可先尝试在控制台Hook相关函数或搜索.wasm文件请求。进阶技巧Hook大法在页面加载前注入代码覆盖关键的加密函数让其执行时打印出参数和结果。这是动态分析的神器。// 在控制台执行或作为油猴脚本注入 var _originalEncrypt 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 _originalEncrypt.call(this, plaintext, key, cfg); console.log([Hook] Ciphertext:, result.toString()); return result; };关注非对称加密混合使用有些网站会用RSA加密AES的密钥然后将加密后的密钥和AES密文一起发送。你需要先逆向RSA公钥用Python的rsa或Crypto.PublicKey库解密出AES密钥再进行AES解密。调试符号Source Map如果网站开发环境未关闭Source Map你可以在开发者工具的Sources面板看到近乎原始的JavaScript代码极大降低逆向难度。逆向分析是一个需要耐心和细致观察的过程。每一次成功破解都是对加密原理和代码逻辑理解的一次深化。从最标准的AES-CBC开始掌握这套“定位-分析-复现-验证”的方法论你就能应对越来越复杂的加密场景。记住浏览器开发者工具是你最强大的盟友而控制台则是你的实验场。多动手、多验证思路自然会越来越清晰。