JS逆向实战:AES加密接口破解与参数还原全流程 1. 案例背景一个必须解决的接口加密做web数据采集的人大概都遇到过这种场景接口文档看着挺正常参数名也猜得八九不离十可真把请求发出去对方直接甩一个参数错误或者返回一段看不懂的密文。我这个笔记要写的就是处理这类问题时会碰到的一个典型分支JS逆向里的AES加解密。起因是做某站点的数据整合时发现登录接口和列表接口的请求体里都多了一个encryptedData字段。明文数据无论怎么模拟都不被接受一查才发现是前端把所有业务参数AES加密后提交响应体里的数据也走了同样的加密通道。整个排查过程不算多复杂但从中抽出来的环节——特征识别、函数定位、参数还原、本地验证——几乎覆盖了JS逆向的完整骨架。这篇案例我就按这个骨架把过程逐段拆开讲把你也能照着走一遍的东西全部写清楚。AES在web前端加密里出现频率相当高登录密码、手机号加密、请求体签名、响应数据解密全都能撞上它。相比RSA那种非对称加密要处理密钥对的生成、传输和管理AES作为对称加密密钥单一、处理速度快、代码实现简单前端项目里大量使用。所以这个案例的参考价值不在于“难”而在于“常见”——把AES加解密逆向这套流程走顺了后面再碰上GCM模式、自定义填充、动态IV之类变体都能少走不少弯路。这篇笔记适合刚接触JS逆向、想找个真实案例练手的人也适合已经能处理简单混淆、但一直没系统梳理过AES参数体系的同学。我不打算只贴关键代码还会把当时判断依据、踩坑过程、验证思路都交代清楚因为这些东西才是遇到问题时真正救命的。1.1 前端为什么要拿AES做数据保护先理解一下业务动机后面看代码时才能抓到重点。前端加密的诉求通常逃不开几个方向第一不让请求参数被轻易改包重放第二满足合规要求比如密码不能明文传输到服务端第三核心业务数据不想让调用方直接看懂于是返回内容也是加密的。AES在里面扮演的角色就是“对称加密”。同一个密钥前端用来加密服务端拿到密文后解密。它的优势是运算速度快、加解密效率高适合对大批量数据做处理尤其适合前端在浏览器里跑的场景。劣势也很明显密钥放在前端JS里本质上只要愿意花时间分析就一定能找到。这也是JS逆向能成立的根本前提——前端代码对用户是完全透明的。这个案例里有个很关键的细节AES同时承担了“请求参数加密”和“响应数据解密”两件事。我一开始只盯着请求加密逻辑把加密函数扣出来以后高高兴兴去请求接口结果响应体回来一堆Base64密文当时就懵了。后来往调用链下面追才发现还有一层解密逻辑挂在请求完成回调里。所以你在做类似分析时务必要把“请求加密”和“响应解密”当成两条线分别排查缺一条都不完整。1.2 逆向闭环的四个环节先摆个框架我习惯把一次完整的JS逆向拆成四步每一步有清晰的目标和出口。抓包定位打开开发者工具找到加密参数出现的具体位置确认它是请求体参数、请求头参数还是响应数据密文。这一步的核心是搞清楚“加密到底发生在哪”以及“需要还原的是哪些数据”。脚本定位通过全局搜索关键字、断点配合调用栈找到真正执行加密的核心函数。有时候你搜索到的只是某个库的引用真正调用的地方还得靠断点一层层往回追。逻辑还原分析AES的模式、填充方式、密钥、IV等参数把关键JS函数抠出来在本地环境重新执行。这一步要特别注意编码细节密钥怎么parse、IV怎么生成、密文要不要二次编码每一样都直接影响结果。一致性验证用已知明文和页面输出的密文做对照确保本地还原的加密逻辑和线上行为完全一致。验证通过后再拿本地生成的密文真实请求一次接口服务端正常响应才算闭环。这四个环节在我看来一个都不能省。尤其是第四步很多人扣完代码以为本地跑通就结束了没有和线上输出做一致性对比等真正发请求时才暴露问题浪费时间不说排查起来还容易怀疑人生。下面我就按这个流程把这个AES案例从头到尾还原一遍。2. 识别加密算法抓包、看特征、定位函数动手之前我习惯先花10分钟观察而不是一上来就点进Sources里翻源码。观察什么观察请求长什么样、响应长什么样、密文是什么格式。别小看这一步很多信息一眼就能判断出来能帮你节省大量时间。2.1 密文特征里藏着算法方向AES加密后的数据一般输出为几种格式一种是Base64字符串字符集会比较规矩经常以等号结尾一种是十六进制字符串肉眼看起来就是一串0-9a-f的组合还有一种是做了自定义编码处理的密文比如外层套了一层URL编码或自定义字符映射看起来就不太规律了。这个案例里encryptedData字段的值就是一段长长的Base64字符串而且每次请求长度还不完全一样。这一点很关键——如果密文长度固定且很短那基本不考虑AES更像是某种哈希算法或固定长度编码。长度不固定说明加密过程大概率引入了随机因子。CBC模式下IV是随机生成的密文长度才会跟明文长度以及随机IV产生联动变化。后面核代码时确认了确实是CBC模式且IV随机生成。还有一个小细节值得留意响应体里的密文也是Base64但字段名和请求体不一样。当时没注意导致我以为响应体数据是纯文本结果解密之后才发现里面是JSON字符串和其他业务数据的拼接。密文特征不仅要看格式还要看位置和字段名这些信息组合起来基本能锁定方向。2.2 关键字搜索实战从源码堆里捞加密函数确定大致方向后就要从压缩混淆的前端代码里找到具体加密函数。我用的标配操作是全局搜索快捷键CtrlShiftF在Sources面板里搜整个页面的资源。搜索关键字怎么选是有讲究的。我一般优先搜下面几类加密框架特征名AES、encrypt、decrypt、CryptoJS业务参数名encryptedData、params、sign、secret、token内置加密对象crypto、subtle、getRandomValues、createCipher特殊请求标志algorithm、mode、padding、iv这个案例里命中率最高的是CryptoJS。这个库在前端AES加密场景里几乎成了标配老项目用它新项目也有不少还在用。搜到CryptoJS后能看到某处引入了它并且在某个函数里调用了CryptoJS.AES.encrypt。但这里有个坑如果前端代码做了模块化打包搜索结果可能只是一堆压缩后的代码片段光靠肉眼很难判断哪个是真正的业务调用。我当时是先看看代码里CryptoJS被引用了几处然后优先点开包含AES.encrypt字样的片段。如果搜索出来很多无关的库代码就要配合断点来定位这就是下一步要说的内容。2.3 断点把加密调用链钉在案板上全局搜索能找到函数位置但确认“加密参数到底是在哪一步生成的”还得靠断点。我的标配操作是在Network面板里找到目标接口右键选择Break on URL这样请求发出之前页面就会停下来。断下来以后查看调用栈从顶层往下一层一层看。压缩混淆后的调用栈看起来会很乱函数名全被替换成了a、b、c之类但不用慌找一下哪一层包含加密参数赋值动作。我的经验是优先看包含encryptedData赋值语句的那一层而不是一上来就断进CryptoJS.AES.encrypt内部。进入库函数内部只能看到算法细节看不到业务逻辑对定位帮助不大反而容易被绕晕。断点还有个附加好处就是可以观察局部变量的值。我在调试时直接把需要加密的明文结构、加密后的密文、密钥变量、IV变量都打印出来这些值后面做一致性验证时会用到。特别是IV如果它是随机生成的断点里可以看到它生成了多少字节以及是否被拼接进密文。这些信息在扣代码阶段价值极大。3. AES参数拆解模式、填充、密钥、IV一个都不能少抓到加密函数只是开始真正的重头戏是搞懂这串AES到底用了哪些参数。我见过不少人在这一步栽跟头算法看对了、密钥找对了只是填充方式判断错了一个或者IV处理错了一步结果就完全对不上。AES参数体系说白了就几个维度但每个维度都可能藏变数。3.1 AES基础参数快速扫盲AES本身是分组对称加密标准固定分组长度16字节。密钥长度支持128位、192位、256位对应16字节、24字节、32字节。前端项目里最常见的是AES-128和AES-256具体用哪一种看CryptoJS传入的key实际字节数就能判断。影响加解密结果的关键参数有四样模式CBC、ECB、GCM、CTR等。ECB不需要IV但安全性弱很多正规项目不太用CBC最常见需要IVGCM在web端也越来越常见自带认证机制。填充PKCS7、ZeroPadding、NoPadding。当明文长度不是16的倍数时需要填充到分组对齐。PKCS7是CryptoJS默认也是最常见的。密钥同样是abc123这串字符用UTF-8 parse和用Base64 decode解析得到的是完全不同长度的字节解密结果天差地别。IVCBC模式必需初始向量决定第一组密文结果。有些项目IV写死有些项目每次随机生成还有些项目把随机IV拼在密文前面一起传出。我做案例拆解的时候习惯先做一个表格把这几项列出来然后拿着从断点里观察到的实际值一项一项填。填完以后加密逻辑基本就清楚了。3.2 从扣出的代码里逐一确认参数这个案例加密函数核心逻辑其实很标准网上很多项目也长这样。我当时的简化还原版本是下面这段const CryptoJS require(crypto-js); function encryptData(plainText) { const key CryptoJS.enc.Utf8.parse(0123456789abcdef); const iv CryptoJS.enc.Utf8.parse(abcdef0123456789); const encrypted CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return CryptoJS.enc.Base64.stringify(encrypted.ciphertext); }这里有两个细节值得展开。第一CryptoJS.enc.Utf8.parse这个处理很关键。如果你直接把字符串key传进去而不做parseCryptoJS会自动把它当成passphrase走另一套密钥派生流程那结果就跟目标完全不一致了。我当时第一次跑本地脚本密文对不上排查半天就是这个原因。第二encrypted.ciphertext取的是原始二进制密文再经过Base64.stringify转成Base64字符串。还有一种常见写法是直接encrypted.toString()默认输出和ciphertext一致但如果项目自己定义了toString的行为结果可能不同。这一步要跟页面上密文的实际格式对齐。3.3 容易被忽略的二次编码与预处理把AES本身搞定了不代表整个逻辑就通了。很多项目会在AES外面再套一层编码或预处理这层东西看不见就很容易卡住。我在这个案例里就遇到一个干扰项加密前业务参数对象先被序列化成JSON但JSON序列化时字段顺序不是按我自然书写顺序排的而是按前端代码里对象属性赋值的顺序。我如果要本地构造同样的明文就必须按照同样的字段顺序序列化否则密文永远对不上。另外加密完的Base64字符串在发请求时被做了URL编码因为其中包含、/、这些URL特殊字符。后端解析时又会自动解码回来。这个逻辑如果不在本地还原时模拟就会出现“本地生成的密文和服务端收到的密文不同”的错觉。我的经验是逆向AES不要只盯算法本身还要把数据从明文到最终请求参数的整条链上下游都走一遍。凡是中间经过的编码转换、序列化处理、字段过滤都要在本地脚本里复现。这也是为什么我一直强调至少要用三组数据做交叉验证而不是拿一组数据试通就完事。4. 实操还原本地扣代码、跑验证、发请求前面把识别和参数拆解讲清楚了现在进入真正动手的阶段。我会按实际操作顺序把整个过程记录下来包括我用的环境、写的脚本、验证的方式以及最后是怎么确认还原成功的。4.1 环境准备与依赖说明本地还原JS加密逻辑我一般直接跑Node.js不用浏览器环境。这样后续想批量调用、集成进采集程序时更方便。AES加解密只需要一个crypto-js库安装命令很简单npm init -y npm install crypto-js这个案例不需要其他依赖但有些前端项目会依赖window、document之类的浏览器对象那种情况就得考虑用jsdom来做环境补齐。还有一种项目是使用Web Crypto API也就是浏览器内置的crypto.subtle如果是那种写法Node.js里需要额外的polyfill或用crypto模块模拟。判断方式很简单看到CryptoJS.AES就是crypto-js看到crypto.subtle.encrypt就是Web Crypto两者不能混为一谈。安装完依赖后先跑一个最简单的自测确认crypto-js在当前Node版本下能正常加载。我遇到过npm装完以后require报Cannot find module的情况检查一下是不是项目根目录和安装路径不一致或者是不是用了pnpm的隔离node_modules结构。这种环境问题虽然小但很踩心态。4.2 完整还原脚本与参数对照根据断点观察和代码定位的结果我把加密、解密两个函数都写了出来。解密函数之所以也要写是因为响应体需要解密才能拿到真实业务数据。下面是简化后的可用版本const CryptoJS require(crypto-js); // 从页面JS中还原出的固定常量 const SECRET_KEY 0123456789abcdef; const SECRET_IV abcdef0123456789; function encrypt(plainText) { return CryptoJS.AES.encrypt( CryptoJS.enc.Utf8.parse(plainText), CryptoJS.enc.Utf8.parse(SECRET_KEY), { iv: CryptoJS.enc.Utf8.parse(SECRET_IV), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ).ciphertext.toString(CryptoJS.enc.Base64); } function decrypt(cipherText) { const decrypted CryptoJS.AES.decrypt( cipherText, CryptoJS.enc.Utf8.parse(SECRET_KEY), { iv: CryptoJS.enc.Utf8.parse(SECRET_IV), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); return decrypted.toString(CryptoJS.enc.Utf8); } module.exports { encrypt, decrypt };需要说明的是案例里的IV虽然是写死的但我自己遇到过随机IV的项目那种情况下IV就不能从常量里取。随机IV的还原方式通常是看代码怎么生成、怎么传递有的是把IV拼接在密文前面然后整体Base64有的是单独放在请求头的另一个字段里还有的是通过另一套密钥交换协议动态获取。你在还原时一定先确认IV从哪来再决定本地怎么写。4.3 与页面输出做一致性验证一步都不少脚本写完后最关键的就是验证。我习惯分两步走。第一步是已知明文验证从页面Console里手动调用页面自己的加密函数用一串固定的明文比如{page:1,size:20}拿到页面的输出密文。然后在本地Node里用同样的明文跑encrypt函数对比两边密文是否一致。当时第一次对比本地密文和页面密文完全不同差点以为代码扣错了。后来逐行排查发现是页面加密前对明文做了JSON.stringify且字段顺序被JS对象遍历顺序影响本地没有复现这个步骤所以明文本身就是错的。调整之后密文就完全对上了。第二步是真实请求验证用本地脚本生成的密文组装一个完整的请求直接发到接口。这一步能一次性验证很多东西——密钥、IV、模式、填充、密文编码、明文序列化顺序任何一个环节不对接口都会报参数错误或返回异常。我当时请求发出去以后服务端正常返回了密文响应体再用decrypt函数解回来拿到了完整JSON数据到这里整个还原流程才算真正闭环。4.4 解密函数联调拿真实响应体验证请求通了以后还要专门验证一下响应解密。很多人只验了请求加密响应解密没有覆盖到结果采集程序跑起来才发现拿到的全是密文没法直接用。我的做法是先用本地加密函数组一个请求发出去把响应体保存下来然后再调用本地的decrypt函数去解看解出来的内容是不是合法的JSON。如果解出来乱码先检查密文是不是有额外的URL解码步骤或者是不是在AES之前还有一层解压。这个案例里没有压缩层但我见过响应体先Zlib解压再AES解密的组合那种情况就得在解密前先跑一下zlib.inflateSync。另外我还发现一个常见现象响应密文的Base64字符串里偶尔会有换行符直接丢给CryptoJS.AES.decrypt会报错需要先去掉换行。网上很多文档不会写这些细节但实操里概率不低。5. 问题排查与心得纪要这一段我整理一下实际调试中遇到过的问题和处理方法再聊几个花了比较多时间的坑。你可以直接拿这个当速查表用踩坑时对照着排查效率高不少。5.1 典型报错和对应解法报错或现象常见原因排查方法Cannot read property encrypt of undefinedCryptoJS引入方式不对或使用了不兼容的构建版本检查require路径重新安装crypto-js确认版本CryptoJS.enc.Utf8.parse is not a function全局变量冲突或混用了不同版本在干净Node进程里测试排除全局污染本地加密结果与页面不一致明文序列化顺序不同、密钥未按正确编码parse、IV不一致用三组已知输入输出交叉对比逐环节定位解密结果乱码密钥/IV错误、密文有额外编码或压缩层先去掉多余编码核对AES参数必要时用已知明文分组调试请求发出后服务端返回参数错误加密结果正确但请求头/字段结构有差异对比页面请求的完整头信息和参数结构响应体返回密文但解不开响应数据还有压缩或二次加密或密文带换行检查Content-Encoding去除换行符后再解密这六类问题基本覆盖了AES逆向里八成以上的坑。我之前在群里见过有人卡在“加密结果与页面不一致”上三天最后发现只是密钥少了个Utf8.parse很可惜。所以这个核对步骤我建议当成强制流程每一步都确认一下再往下走。5.2 三个特别耗费时间的坑第一个坑是密钥处理方式。CryptoJS里AES.encrypt的第二个参数如果传字符串库会自动将其视为密码短语走PBKDF2密钥派生但如果传的是用Utf8.parse处理过的WordArray就会直接作为密钥字节使用。这两种路径得到的实际密钥可能是完全不同的结果自然对不上。我一开始直接传了字符串密文怎么都对不上查了好久才发现是这个问题。第二个坑是随机IV的拼接方式。这个案例里IV是写死的所以没踩到但我在另一个项目里遇到过前端每次请求生成一个随机IV然后把IV的16字节直接拼在密文头部再整体Base64输出。只看调用代码根本看不出来因为加密结果是先Concat再encode。如果你发现密文每次长度变化、且本地用固定IV总是解不开就要怀疑这种拼接模式。处理办法是先按偏移量分割出IV和密文再分别使用。第三个坑是响应数据还套了一层协议。我把请求加密还原成功能拿到响应了但解出来的内容是类似{code:0,data:...}的结构data字段里又是一段不明字符串最后才发现这里面还嵌套了一层Base64编码。做逆向时要记住加密链路可能不止一层响应先Base64再AES、或者先AES再Base64的情况都很常见不要找到一层加密就认为大功告成。5.3 下次再遇到这类加密我会怎么快速上手做完这个案例我给自己沉淀了一套更顺手的流程写在这里供参考。拿到疑似AES加密的接口先抓包看密文格式和长度排除哈希和简单编码的可能然后花五分钟全局搜索CryptoJS、AES这些关键字再辅以XHR断点查看调用栈确认加密入口拿到加密函数后先把模式、填充、密钥、IV四个参数填进表格包含编码细节写完本地脚本后强制做三组一致性验证分别是固定明文对比、随机明文对比、真实请求联调。最后再检查一下响应解密链路确保数据能完整解回明文。这套流程我现在一直在用不管遇到CBC、ECB还是GCM流程骨架都不用变变的只是具体参数。JS逆向这个领域套路远多于创意只要你把标准流程跑顺了剩下的就是时间问题。我个人实操下来的体会是JS逆向里最值钱的能力不是拿到一段能跑的代码而是能快速定位“为什么对不上”。这个能力只能靠多练、多踩坑、多记录来积累。建议你每次做完一个加密的逆向都把密文特征、关键参数、踩坑点整理成笔记下次再遇到类似项目可以直接检索复用。做过的案例多了你会发现很多加密套路都是相似的真正需要从零开始研究的反而很少。