公司动态

Web前端JS逆向实战:定位与提取AES加密的Key和IV

📅 2026/8/3 12:00:18
Web前端JS逆向实战:定位与提取AES加密的Key和IV
1. 逆向分析的前置认知为什么是AES以及为什么是JS在Web前端安全领域JavaScript文件中的AES加密逻辑逆向分析是一个既经典又充满挑战的课题。说它经典是因为AES作为对称加密的黄金标准在数据传输、接口签名、本地存储保护等场景中被广泛应用说它充满挑战是因为现代前端工程化如Webpack打包、代码混淆、压缩和开发者有意为之的“反逆向”手段让寻找关键的key密钥和iv初始化向量变得像在迷宫里寻宝。我这次遇到的案例源于一个数据接口的调用分析。目标网站的核心数据通过一个加密的JSON字符串传输前端拿到后需要解密才能渲染。控制台里看到的是一串毫无意义的密文而网络请求的响应体里也没有任何解密提示。显然解密逻辑被写在了前端的某个JS文件里。我们的目标很明确找到执行AES解密的函数并从中提取出用于解密的key和iv。这里需要先明确一点AES加密有多种模式如ECB、CBC、CFB等其中CBC模式最为常用它就需要key和iv两个参数。key是核心秘密而iv用于增加随机性即使同一明文、同一key每次加密结果也不同提升了安全性。在JS中通常使用CryptoJS这个库或者现代浏览器原生的SubtleCryptoAPI来实现AES。逆向的第一步就是确认它用了哪种方式并定位到关键代码段。2. 逆向环境与工具链搭建从浏览器开发者工具出发工欲善其事必先利其器。对于JS逆向浏览器自带的开发者工具DevTools是我们的主战场配合一些插件和思路能极大提升效率。2.1 核心工具浏览器开发者工具以Chrome为例F12打开DevTools以下几个面板是关键Sources面板这是大本营。所有加载的JS文件都在这里可以设置断点、单步调试、查看调用栈。重点关注“Page”标签下的文件特别是那些看起来是主业务逻辑的、文件名不像是第三方库的JS文件。Network面板记录所有网络请求。筛选XHR/Fetch请求找到那个返回加密数据的接口。点击该请求在“Response”标签页可以看到加密后的数据。更重要的是“Initiator”列它显示了是哪个JS文件发起了这个请求这通常是逆向的绝佳入口。Console面板可以执行任意JS代码用于测试我们找到的key和iv是否正确或者调用我们定位到的解密函数。2.2 代码搜索与过滤技巧面对一个可能被Webpack打包成单一bundle.js可能有几MB甚至十几MB的站点直接阅读是天方夜谭。必须善用搜索CtrlShiftF。关键词搜索这是最直接的方法。我们可以尝试搜索以下关键词AESCryptoJS(如果使用了这个库)decrypt/encryptkey、iv、mode、padding(AES相关参数)加密接口返回的密文的前几个字符有时密钥或解密函数就在密文附近像CBC、PKCS7这样的模式或填充方式名称。搜索策略如果直接搜AES无果很可能代码被混淆了函数和变量名被替换成了a、b、c、_0x1a2b3c这种形式。这时可以搜索一些更“底层”或不易被混淆的字符串比如魔法数字0x2可能对应CBC模式或者字符串常量如AES即使变量名被混淆字符串内容通常保留。2.3 断点调试的艺术搜索到可疑代码后断点调试是理清逻辑的不二法门。事件监听器断点在Sources面板的右侧有个“Event Listener Breakpoints”区域。可以勾选“XHR/Fetch” - “readystatechange” 或 “load”。这样当页面发起任何Ajax请求并接收到数据时代码会自动暂停我们可以直接跳到处理响应数据的逻辑处。XHR/Fetch断点在Network面板找到目标请求右键选择“Break on” - “response body”。当这个请求的响应体到达时代码会暂停此时调用栈Call Stack会显示从接收到数据到当前暂停点的所有函数调用链逆向查找解密逻辑非常高效。函数断点如果我们已经通过搜索定位到了一个疑似解密函数比如一个名叫decryptData或混淆后的_0xabcd函数可以直接在这个函数定义的行号上点击设置断点。注意现代网站常使用fetchAPI而非传统的XMLHttpRequest。为fetch设置断点稍微麻烦可以在Console中执行以下代码重写fetch以便下断点const originalFetch window.fetch; window.fetch function(...args) { console.trace(Fetch called:, args); // 这里可以下断点 return originalFetch.apply(this, args); };3. 逆向实战定位并剖析AES解密逻辑假设我们通过上述方法在庞大的JS文件中搜索字符串decrypt找到了一个可疑的代码块。它可能长这样这是美化后的示例实际可能更混乱function _0x12ab34(data) { var key CryptoJS.enc.Utf8.parse(这是一个32位密钥字符串); var iv CryptoJS.enc.Utf8.parse(这是一个16位IV字符串); var decrypted CryptoJS.AES.decrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8); }3.1 识别加密库与模式一眼就能看到这里使用了CryptoJS库。CryptoJS.AES.decrypt是解密函数。参数非常清晰data传入的密文通常是Base64格式的字符串。key密钥注意它被CryptoJS.enc.Utf8.parse处理了。这意味着密钥本身是一个UTF-8字符串但需要被转换成CryptoJS内部使用的WordArray格式。这里的字符串这是一个32位密钥字符串就是我们要找的key。对于AES-256它需要32字节即32个英文字符或16个中文字符因为UTF-8中一个中文字符占3字节计算需小心AES-128需要16字节。第三个参数是配置对象iv初始化向量同样被处理。字符串这是一个16位IV字符串就是iv。CBC模式要求iv为16字节。modeCryptoJS.mode.CBC明确了是CBC模式。paddingCryptoJS.pad.Pkcs7填充方式为PKCS7。3.2 处理混淆与动态生成的情况然而现实往往更骨感。更常见的情况是key和iv不是硬编码的字符串而是通过一系列复杂的计算动态生成的。代码可能被混淆成这样function d(t) { var e o(0x12, D%#l); var n o(0x13, G8h); var r m(e, n); // m是一个复杂的计算函数 var a CryptoJS.AES.decrypt(t, r.key, {iv: r.iv, mode: CryptoJS.mode.CBC}); return a.toString(CryptoJS.enc.Utf8); }这时我们的工作就变成了“计算过程的逆向”定位生成函数找到o函数和m函数的定义。在Sources面板中可以点击函数名跳转或者在整个文件中搜索function o和function m。理解生成逻辑o函数可能是一个“字符串解密”函数用于对抗简单的字符串搜索。它接收一个十六进制字符串如0x12和一个密钥返回真正的字符串。我们需要在Console中手动调用这个函数或者跟读它的逻辑得到原始的key和iv字符串。动态调试取值更高效的办法是直接下断点。在var r m(e, n);这一行下断点当断点触发时将鼠标悬停在变量r上或者在Console中直接输入r并回车查看其key和iv属性的值。这是最直接获取动态生成密钥的方法。3.3 使用浏览器原生Crypto API的情况有些现代应用会使用浏览器原生的window.crypto.subtleAPI。它的代码风格差异很大async function decryptData(encryptedData) { const keyData new TextEncoder().encode(你的32位密钥字符串); const ivData new TextEncoder().encode(你的16位IV字符串); const key await crypto.subtle.importKey( raw, keyData, { name: AES-CBC }, false, [decrypt] ); const decrypted await crypto.subtle.decrypt( { name: AES-CBC, iv: ivData }, key, encryptedData // 这里encryptedData应该是ArrayBuffer或Uint8Array ); return new TextDecoder().decode(decrypted); }逆向思路是类似的找到importKey时传入的keyData和decrypt时传入的iv。它们可能来自其他函数计算、网络请求甚至是页面中隐藏的HTML元素属性。4. 密钥提取与验证从代码到可用的数据找到疑似key和iv的变量或字符串后绝不能想当然地认为它们就是正确的。必须进行验证。4.1 提取密钥与IV硬编码字符串直接复制出来。注意检查其长度和字符集。一个32字节的AES-256密钥如果显示为64个十六进制字符0-9, a-f可能需要用CryptoJS.enc.Hex.parse来处理而不是Utf8.parse。动态计算值在断点处通过Console的交互功能提取。例如在Console中输入copy(r.key); // 将r.key的值复制到系统剪贴板 console.log(Key:, r.key.toString()); // 打印查看 console.log(IV:, r.iv.toString());函数返回值如果key/iv是一个函数的返回值可以在Console中尝试直接调用该函数注意可能需要提供正确的参数或者更稳妥地在函数返回处下断点查看返回值。4.2 本地验证解密逻辑这是最关键的一步。我们需要在可控的环境下用提取到的key和iv尝试解密一段已知的密文。方法一在浏览器Console中验证如果原站点的解密函数例如_0x12ab34在全局作用域可访问我们可以直接在Console中调用它传入从Network面板复制的接口返回的密文看是否能输出可读的明文。var ciphertext 从Network面板复制的Base64密文; var result _0x12ab34(ciphertext); console.log(result);如果输出是乱码或报错说明key/iv或模式/填充方式不对。方法二使用Node.js或Python本地脚本验证这是更独立和可靠的方法。将提取的参数用于一个标准的AES解密库。Python示例使用pycryptodome库from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 # 替换成你提取的值 key_str 这是一个32位密钥字符串 iv_str 这是一个16位IV字符串 ciphertext_b64 从网络抓取的Base64密文 # 转换UTF-8字符串 - 字节 key key_str.encode(utf-8) iv iv_str.encode(utf-8) ciphertext base64.b64decode(ciphertext_b64) # 创建解密器并解密 cipher AES.new(key, AES.MODE_CBC, iv) decrypted_padded cipher.decrypt(ciphertext) # 移除PKCS7填充 plaintext unpad(decrypted_padded, AES.block_size).decode(utf-8) print(plaintext)Node.js示例使用crypto模块const crypto require(crypto); const key Buffer.from(这是一个32位密钥字符串, utf-8); const iv Buffer.from(这是一个16位IV字符串, utf-8); const encryptedText Buffer.from(从网络抓取的Base64密文, base64); const decipher crypto.createDecipheriv(aes-256-cbc, key, iv); let decrypted decipher.update(encryptedText); decrypted Buffer.concat([decrypted, decipher.final()]); console.log(decrypted.toString(utf-8));如果本地解密成功得到了结构化的JSON或可读文本那么恭喜你key和iv逆向成功。5. 进阶挑战与应对策略在实际逆向中你几乎一定会遇到比上述示例更复杂的情况。5.1 对抗代码混淆与压缩代码美化Sources面板左下角有{}Pretty-print按钮可以将压缩成一行的代码格式化便于阅读。跟读执行流不要试图一次性理解整个混淆后的文件。利用断点和“Step Over”、“Step Into”功能紧紧跟随数据的流向。关注函数调用的输入和输出。重命名变量在Sources面板中可以右键点击一个混淆的变量名如_0x1a2b3c选择“Rename variable”给它起一个有意义的名字如decryptKey这能极大提升后续分析的效率。5.2 对抗动态密钥与环境依赖有时key/iv并非固定值而是每次请求都动态变化可能由以下方式生成基于时间戳或随机数密钥是Date.now()的某种哈希。你需要分析其生成算法并在自己的请求中复现。来自服务器首次访问页面时服务器通过另一个接口或隐藏在HTML中下发一个“种子”或临时密钥。你需要先抓取这个种子。依赖浏览器环境密钥生成用到了navigator.userAgent、屏幕分辨率等浏览器指纹。你的模拟请求需要携带相同的环境信息。应对策略是完整复现密钥生成链路。从最初的源头可能是某个全局变量、某个接口响应、某个固定字符串开始通过断点记录下每一个转换步骤直到得到最终的key和iv。然后将这个生成过程用Python或Node.js重写。5.3 非标准实现与自定义算法偶尔开发者会“魔改”AES比如自定义S盒、修改轮函数或者将AES与其他编码如自定义的Base64、加密如XOR结合。这时标准的AES库将无法解密。识别方法是观察解密函数内部除了标准的AES调用外是否还有其他的位运算、查表操作或循环。如果发现代码中有大量看似随机的常量数组可能是魔改的S盒或者加密/解密过程与标准AES描述不符那很可能就是自定义算法。应对这种“白盒加密”极其困难通常需要深厚的密码学和逆向工程功底动态调试跟踪每一步的中间状态逐步理解其算法逻辑然后进行还原。这已经超出了大多数Web逆向的范畴。6. 逆向后的思考安全、伦理与工具化成功提取出key和iv并不意味着万事大吉。6.1 关于前端加密的安全性这次逆向过程本身就深刻地揭示了前端加密不可信这一核心原则。无论JS代码如何混淆、压缩、动态生成只要解密逻辑必须在用户的浏览器中执行那么密钥最终必然以某种形式暴露在内存和代码中。前端加密的主要作用是增加攻击者的成本和难度即“防君子不防小人”以及对传输数据进行简单的编码混淆防止明文在网络上被随意窥探。绝不能依赖前端加密来保护真正的敏感秘密如数据库密码、支付密钥这些必须由后端服务器来保障。6.2 逆向分析的伦理边界进行JS逆向分析务必遵守法律法规和网站的服务条款。你的目的应该是安全研究在授权范围内评估自身或客户网站的安全性。数据集成为合法的、无官方API的业务需求如个人数据备份、学术研究开发工具且不损害对方服务、不进行商业牟利、不侵犯隐私。学习与教育就像本文一样在技术层面进行探讨提升技能。绝对禁止将其用于恶意爬虫、数据盗窃、服务攻击或任何侵犯他人合法权益的行为。6.3 将过程工具化如果你需要频繁地对同一类加密进行逆向可以考虑将过程工具化编写浏览器插件自动在页面加载时注入脚本Hook关键的加密/解密函数如重写CryptoJS.AES.decrypt自动捕获并输出每次调用时的key、iv和密文/明文。使用自动化调试工具如Puppeteer或Playwright编写脚本自动打开页面、执行操作、在关键点下断点并提取数据。构建本地解密服务将逆向出来的密钥生成逻辑封装成一个独立的服务如一个Flask或Express API供其他程序调用避免每次都需要启动浏览器调试。逆向分析是一个需要耐心、细心和逻辑思维的过程。每一次成功的逆向不仅解决了一个具体的技术问题更是一次对代码执行逻辑、加密原理和系统设计的深度理解。保持好奇保持敬畏在技术的边界内探索这才是安全研究的正确姿态。