ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/8/3 12:03:55 拓冰建站 浏览量
Web前端JS逆向实战:定位与提取AES加密的Key和IV

1. 逆向分析的前置认知:为什么是AES,以及为什么是JS

在Web前端安全领域,JavaScript文件中的AES加密逻辑逆向分析,是一个既经典又充满挑战的课题。说它经典,是因为AES作为对称加密的黄金标准,在数据传输、接口签名、本地存储保护等场景中被广泛应用;说它充满挑战,是因为现代前端工程化(如Webpack打包、代码混淆、压缩)和开发者有意为之的“反逆向”手段,让寻找关键的key(密钥)和iv(初始化向量)变得像在迷宫里寻宝。

我这次遇到的案例,源于一个数据接口的调用分析。目标网站的核心数据通过一个加密的JSON字符串传输,前端拿到后需要解密才能渲染。控制台里看到的是一串毫无意义的密文,而网络请求的响应体里也没有任何解密提示。显然,解密逻辑被写在了前端的某个JS文件里。我们的目标很明确:找到执行AES解密的函数,并从中提取出用于解密的keyiv

这里需要先明确一点:AES加密有多种模式,如ECB、CBC、CFB等,其中CBC模式最为常用,它就需要keyiv两个参数。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代码,用于测试我们找到的keyiv是否正确,或者调用我们定位到的解密函数。

2.2 代码搜索与过滤技巧

面对一个可能被Webpack打包成单一bundle.js(可能有几MB甚至十几MB)的站点,直接阅读是天方夜谭。必须善用搜索(Ctrl+Shift+F)。

  • 关键词搜索:这是最直接的方法。我们可以尝试搜索以下关键词:
    • AES
    • CryptoJS(如果使用了这个库)
    • decrypt/encrypt
    • keyivmodepadding(AES相关参数)
    • 加密接口返回的密文的前几个字符(有时密钥或解密函数就在密文附近)
    • CBCPKCS7这样的模式或填充方式名称。
  • 搜索策略:如果直接搜AES无果,很可能代码被混淆了,函数和变量名被替换成了abc_0x1a2b3c这种形式。这时可以搜索一些更“底层”或不易被混淆的字符串,比如魔法数字0x2(可能对应CBC模式),或者字符串常量如"AES"(即使变量名被混淆,字符串内容通常保留)。

2.3 断点调试的艺术

搜索到可疑代码后,断点调试是理清逻辑的不二法门。

  1. 事件监听器断点:在Sources面板的右侧,有个“Event Listener Breakpoints”区域。可以勾选“XHR/Fetch” -> “readystatechange” 或 “load”。这样,当页面发起任何Ajax请求并接收到数据时,代码会自动暂停,我们可以直接跳到处理响应数据的逻辑处。
  2. XHR/Fetch断点:在Network面板找到目标请求,右键选择“Break on” -> “response body”。当这个请求的响应体到达时,代码会暂停,此时调用栈(Call Stack)会显示从接收到数据到当前暂停点的所有函数调用链,逆向查找解密逻辑非常高效。
  3. 函数断点:如果我们已经通过搜索定位到了一个疑似解密函数(比如一个名叫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 处理混淆与动态生成的情况

然而,现实往往更骨感。更常见的情况是,keyiv不是硬编码的字符串,而是通过一系列复杂的计算动态生成的。代码可能被混淆成这样:

function d(t) { var e = o('0x12', 'D%#l'); var n = o('0x13', 'G8@h'); 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); }

这时,我们的工作就变成了“计算过程的逆向”:

  1. 定位生成函数:找到o函数和m函数的定义。在Sources面板中,可以点击函数名跳转,或者在整个文件中搜索function ofunction m
  2. 理解生成逻辑o函数可能是一个“字符串解密”函数,用于对抗简单的字符串搜索。它接收一个十六进制字符串(如'0x12')和一个密钥,返回真正的字符串。我们需要在Console中手动调用这个函数,或者跟读它的逻辑,得到原始的keyiv字符串。
  3. 动态调试取值:更高效的办法是直接下断点。在var r = m(e, n);这一行下断点,当断点触发时,将鼠标悬停在变量r上,或者在Console中直接输入r并回车,查看其keyiv属性的值。这是最直接获取动态生成密钥的方法。

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. 密钥提取与验证:从代码到可用的数据

找到疑似keyiv的变量或字符串后,绝不能想当然地认为它们就是正确的。必须进行验证。

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 本地验证解密逻辑

这是最关键的一步。我们需要在可控的环境下,用提取到的keyiv,尝试解密一段已知的密文。

  • 方法一:在浏览器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或可读文本,那么恭喜你,keyiv逆向成功。

5. 进阶挑战与应对策略

在实际逆向中,你几乎一定会遇到比上述示例更复杂的情况。

5.1 对抗代码混淆与压缩

  • 代码美化:Sources面板左下角有{}(Pretty-print)按钮,可以将压缩成一行的代码格式化,便于阅读。
  • 跟读执行流:不要试图一次性理解整个混淆后的文件。利用断点和“Step Over”、“Step Into”功能,紧紧跟随数据的流向。关注函数调用的输入和输出。
  • 重命名变量:在Sources面板中,可以右键点击一个混淆的变量名(如_0x1a2b3c),选择“Rename variable”,给它起一个有意义的名字(如decryptKey),这能极大提升后续分析的效率。

5.2 对抗动态密钥与环境依赖

有时,key/iv并非固定值,而是每次请求都动态变化,可能由以下方式生成:

  • 基于时间戳或随机数:密钥是Date.now()的某种哈希。你需要分析其生成算法,并在自己的请求中复现。
  • 来自服务器:首次访问页面时,服务器通过另一个接口或隐藏在HTML中下发一个“种子”或临时密钥。你需要先抓取这个种子。
  • 依赖浏览器环境:密钥生成用到了navigator.userAgent、屏幕分辨率等浏览器指纹。你的模拟请求需要携带相同的环境信息。

应对策略是:完整复现密钥生成链路。从最初的源头(可能是某个全局变量、某个接口响应、某个固定字符串)开始,通过断点记录下每一个转换步骤,直到得到最终的keyiv。然后将这个生成过程用Python或Node.js重写。

5.3 非标准实现与自定义算法

偶尔,开发者会“魔改”AES,比如自定义S盒、修改轮函数,或者将AES与其他编码(如自定义的Base64)、加密(如XOR)结合。这时,标准的AES库将无法解密。

识别方法是:观察解密函数内部,除了标准的AES调用外,是否还有其他的位运算、查表操作或循环。如果发现代码中有大量看似随机的常量数组(可能是魔改的S盒),或者加密/解密过程与标准AES描述不符,那很可能就是自定义算法。

应对这种“白盒加密”极其困难,通常需要深厚的密码学和逆向工程功底,动态调试跟踪每一步的中间状态,逐步理解其算法逻辑,然后进行还原。这已经超出了大多数Web逆向的范畴。

6. 逆向后的思考:安全、伦理与工具化

成功提取出keyiv,并不意味着万事大吉。

6.1 关于前端加密的安全性

这次逆向过程本身,就深刻地揭示了前端加密不可信这一核心原则。无论JS代码如何混淆、压缩、动态生成,只要解密逻辑必须在用户的浏览器中执行,那么密钥最终必然以某种形式暴露在内存和代码中。前端加密的主要作用是增加攻击者的成本和难度(即“防君子不防小人”),以及对传输数据进行简单的编码混淆,防止明文在网络上被随意窥探。绝不能依赖前端加密来保护真正的敏感秘密(如数据库密码、支付密钥),这些必须由后端服务器来保障。

6.2 逆向分析的伦理边界

进行JS逆向分析,务必遵守法律法规和网站的服务条款。你的目的应该是:

  • 安全研究:在授权范围内,评估自身或客户网站的安全性。
  • 数据集成:为合法的、无官方API的业务需求(如个人数据备份、学术研究)开发工具,且不损害对方服务、不进行商业牟利、不侵犯隐私。
  • 学习与教育:就像本文一样,在技术层面进行探讨,提升技能。

绝对禁止将其用于恶意爬虫、数据盗窃、服务攻击或任何侵犯他人合法权益的行为。

6.3 将过程工具化

如果你需要频繁地对同一类加密进行逆向,可以考虑将过程工具化:

  1. 编写浏览器插件:自动在页面加载时注入脚本,Hook关键的加密/解密函数(如重写CryptoJS.AES.decrypt),自动捕获并输出每次调用时的keyiv和密文/明文。
  2. 使用自动化调试工具:如Puppeteer或Playwright,编写脚本自动打开页面、执行操作、在关键点下断点并提取数据。
  3. 构建本地解密服务:将逆向出来的密钥生成逻辑封装成一个独立的服务(如一个Flask或Express API),供其他程序调用,避免每次都需要启动浏览器调试。

逆向分析是一个需要耐心、细心和逻辑思维的过程。每一次成功的逆向,不仅解决了一个具体的技术问题,更是一次对代码执行逻辑、加密原理和系统设计的深度理解。保持好奇,保持敬畏,在技术的边界内探索,这才是安全研究的正确姿态。