做“某古加”这个响应解密任务我一直觉得最大的坎不在算法本身而在第一步怎么迈。它核心就是要回答一个问题接口返回的数据明显被加密了浏览器里却能正常显示那解密的动作藏在哪一段JS里用的什么算法密钥又从哪来把这三个问题串起来整条链路就通了。之前遇到的大多数响应解密其实都逃不开几种常见姿势AES、RSA、Base64加自定义混淆、或者是自研的异或运算。你只要能把“请求响应→加密函数→解密函数→明文”这条链路走通后面复现逻辑就是照葫芦画瓢。这篇就按我自己实操的路径来写从思路到定位再到还原和踩坑尽量把每一步为什么这么做也讲清楚。提示本文所有调试和分析均以合规的授权测试、安全研究和CTF学习为前提请勿用于未经授权的数据获取。1. 动手之前先把“解密链路”想明白1.1 网站到底在防什么我刚接手这个项目时第一反应不是去翻混淆后的JS而是先想明白一件事后端为什么非要把响应体加密再返回。最常见的三个原因防止直接被抓取。明文JSON扔在线上爬虫写个脚本就能拿但密文能挡住一大批低水平采集者至少逼你都要懂点逆向。防止数据被篡改。响应内容如果混入了类似{code:0,data:{...}}的完整结构攻击者改个code或者改个字段前端可能就会展示错误数据。加密后再加签名校验能保证数据从服务端到浏览器整个过程没被动过。保护敏感字段。比如用户手机号、订单金额这种业务字段即使被中间人抓到不是明文也相对安全。但不管它防什么前端一定要能解开。这就是所有逆向工作的突破口。页面里只要还有明文渲染就必然存在一段解密逻辑我们要做的只是把这段逻辑从成千上万行混淆代码里揪出来。1.2 本次项目的核心目标要拆开我给自己定的目标从来不是“把整个站点逆向出来”而是聚焦三个点定位解密函数在哪个JS文件、哪个作用域。确定加密算法和模式AES-CBC还是ECBRSA还是自研异或。拿到密钥或者密钥的生成规则并在本地脚本里复现解密。项目标题里写“某古加响应数解密逻辑”“响应数”我理解就是响应数据本质是接口返回那串密文。拆成这三个小目标之后操作路径就清晰多了每完成一个目标就离还原更近一步而不是像个没头苍蝇一样在混淆代码里乱点。1.3 先区分“加密”还是“纯编码”很多人一看到响应是一串U2FsdGVkX1开头就直接去搜AES其实不对。它可能是Base64编码也可能是URL编码甚至可能是zlib压缩后再转Base64。编码和密码学意义上的加密完全是两码事。判断方法很简单把响应体复制到控制台或本地脚本里先用atob解一下Base64再用decodeURIComponent解一下URL编码看看能不能直接看到可读内容。如果解出来是一段乱码很可能是压缩或加密。再试一下gzip解压能解出来就说明服务端没加密只是做了传输压缩。这些都试完还是乱码才基本确定是密码学加密。这个判断动作虽然基础但能帮我少走很多弯路。我见过有人拿zlib压缩当AES硬破解白白浪费一晚上。2. 解密逻辑定位从一个响应样本开始2.1 抓包并观察响应特征无论目标多复杂第一步永远是打开开发者工具找到那个返回加密数据的接口。以某古加的/api/v1/list为例响应体是这样{ code: 0, data: U2FsdGVkX1tJ6kGx8qmZG5cHv1yYw0QZx3Bk43cV2eA }先说这个字符串的特征长度是固定的几组结尾带字符都在A-Za-z0-9/范围内这符合Base64编码特征。但是直接把这段字符串做Base64解码得到的却是乱码字节。再看到开头U2FsdGVkX1这个特征值很熟悉是CryptoJS处理密文时常见的魔数说明大概率用了AES类的加密。这里有一个小经验看响应的长度和字符集。AES-CBC加密后的密文长度基本是16字节的倍数转成Base64后往往比较规整RSA加密后长度取决于密钥长度通常是128或256字节自研异或或位运算后长度可能和原明文的长度接近。多积累这些指纹特征后面定位算法会有很强的指向性。2.2 用关键词搜索快速框定范围拿到加密样本后接下来就是找到那一段解密代码。最粗暴但有效的操作是切到Sources面板按CtrlShiftF全局搜索一批关键词decryptCryptoJSAESparseJSON.parsekeyiv在我的项目里最先搜到的是decrypt命中了一堆压缩后的JS文件。关键点不在这里而在于搜索结果中有个文件名特别像业务代码比如app.js或者chunk-xxx.js而不是库文件。顺着这个文件进去看能看到一个函数虽然函数名被混淆成a.b或者_0x3f2a但内部调用结构基本是明文的很容易识别。2.3 在请求管线里下断点搜索关键词有时因为混淆太严重而失效这时候靠断点来抓。在Network面板里找到目标接口右键选择Break on Response或者直接在Sources面板里添加XHR/fetch断点条件是URL包含/list。原理是这样的浏览器任何一次接口请求返回时都要触发响应回调如果你在发送请求阶段设置了XHR断点就能立即停在那个执行上下文里。这时候不要急着往下走先看右侧的Call Stack一层层翻。一般翻到靠近顶部的某一帧就能看到读取responseText并传给解密函数的代码const t JSON.parse(e.responseText); const decrypted decryptResponse(t.data);虽然函数名可能是a、b这种但我们已经知道入口在哪了。下一步把所有断点去干净重新点击翻页触发接口然后在decryptResponse函数调用处手动打断点。每次页面要展示数据时函数都会在这里停住我们就可以看到传入参数和返回值。2.4 判断加密算法的几个特征看到函数内部有CryptoJS.AES.decrypt这种写法时算法基本就能定性了。但很多项目会故意把库内部方法打包成难以识别的形式这时候可以靠几个特征去判断特征可能算法Base64字符串长度是16的倍数IV固定长度16字节AES-CBC密文长度与密钥长度相关如256字节RSA密文长度和明文长度几乎一致解密后出现异或痕迹自研异或字符串固定前缀如Salted__或U2FsdGVkX1CryptoJS常用封装我在某古加这里看到的是CryptoJS.AES.decrypt还带了明确的mode和padding参数。这意味着算法是AES-CBC配合PKCS7填充。确定算法不等于结束还需要拿到密钥和IV。3. 从密文到明文的完整还原实录3.1 给解密函数挂上“日志钩子”直接在看混淆代码里找密钥很费劲更稳的操作是给解密函数加日志。某古加的decryptResponse函数的调用点在业务代码里但函数体在一个独立作用域还好它没有被彻头彻尾地做闭包隐藏。我当时的做法是在Console里先找到这个函数所在的全局对象。如果它挂在window上直接覆盖window.decryptResponse function(...args) { console.log(入参:, args[0]); const result originalDecrypt(...args); console.log(出参:, result); return result; };但现实很骨感大多数函数都被包在模块作用域里根本拿不到window引用。这时候换一种思路不找函数找数据。直接在Console里注入一个全局的JSON.parse钩子因为解密完成后最终还是要JSON.parse一下才能变成对象const rawParse JSON.parse; JSON.parse function(...args) { const result rawParse.apply(this, args); const str String(args[0]); if (str.length 50 str.startsWith({)) { console.trace(JSON.parse被调用, str.slice(0, 200)); } return result; };重新刷新页面控制台立刻会打印JSON.parse的调用堆栈而调用堆栈的某一帧就是解密函数所在位置。点击那一条堆栈就能直接跳到真正的源码处。这个方法对大多数混淆程度不深的前端都很有效算是响应解密里一个通用技巧。3.2 拿到AES的密钥和IV跳到源码后发现某古加的decryptResponse内部逻辑并不复杂核心调用是function decryptResponse(b) { var key CryptoJS.enc.Utf8.parse(abcdef0123456789); var iv CryptoJS.enc.Utf8.parse(00112233445566778899aabbccddeeff); return CryptoJS.AES.decrypt(b, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString(CryptoJS.enc.Utf8); }密钥和IV就明晃晃写在代码里。看到这里别高兴太早因为有些平台动态密钥的生成逻辑是分布在多个函数里的。我这次碰到的算相对友好。但更常见的场景是key并不是写死的而是由某个变量拼接、混淆、甚至从服务端接口二次获取。这种情况下需要在断点处手动把key和iv的值复制出来再去倒推来源。3.3 用本地脚本复现解密拿到密钥、IV和算法参数就可以完全脱离浏览器做复现了。我习惯用Node.js先跑一遍逻辑和代码都直观。用CryptoJS写是这样的const CryptoJS require(crypto-js); const encrypted U2FsdGVkX1tJ6kGx8qmZG5cHv1yYw0QZx3Bk43cV2eA; const key CryptoJS.enc.Utf8.parse(abcdef0123456789); const iv CryptoJS.enc.Utf8.parse(00112233445566778899aabbccddeeff); const decrypted CryptoJS.AES.decrypt(encrypted, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); console.log(decrypted.toString(CryptoJS.enc.Utf8));如果你更习惯Python用pycryptodome也能做到from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 encrypted U2FsdGVkX1tJ6kGx8qmZG5cHv1yYw0QZx3Bk43cV2eA key babcdef0123456789 iv b00112233445566778899aabbccddeeff cipher AES.new(key, AES.MODE_CBC, iviv) plain unpad(cipher.decrypt(base64.b64decode(encrypted)), 16) print(plain.decode())跑完看到控制台输出一串合法JSON时说明算法、模式、密钥、IV全齐了。这一步的爽感在于以前必须在浏览器里点来点去才能看到的数据现在一条命令就能还原后续做批量数据抽取就方便多了。3.4 反向验证防错觉解密成功后很多人就收工了这是不对的。我会再往前多走一步随便换另一条接口的加密响应用同样脚本解如果也能得到明文说明解密逻辑通用如果解出来是乱码那说明这个接口用了不同密钥或者算法并非完全一样。某古加这次就是把部分接口的IV固定但key换了差点被一条数据骗过去。验证的目的不只是确认“能解”更重要的是确认“参数是否正确”。很多项目虽然表面调用同一个decrypt函数但密钥是根据用户会话动态生成的。如果没验证第二天接口返回变了脚本直接作废。把验证流程固化成习惯以后接手别的项目就知道那串乱码到底是不是参数错误。4. 常见坑点与排查实录4.1 一进调试就无限debugger有些站点会在主逻辑外挂一个死循环式的debugger打开控制台就卡死。某古加这边倒是没有这个坑但我做其他项目时遇到得很多。处理办法大致有三种找到触发debugger的代码位置用浏览器Overrides把那段代码替换成空。在Sources里右键点击断点位置选择“Never pause here”。用条件断点把原本触发debugger的条件改成false。这种反调试本质是给自己制造操作障碍破解思路就是绕过去不跟它硬刚。4.2 变量名全被混淆成_0x开头很多响应解密的卡点不是算法不够熟而是代码根本没法读。JavaScript混淆工具生成的_0xabc123风格变量名会把可读性降到零。我的习惯是先做局部替换在源码里看到关键函数后把_0x3f2a这些变量名手动重命名成str、key、iv在脑中对应的含义。某些反混淆工具能一次性还原但完全自动化的效果有限特别是控制流平坦化严重时还是靠手动打断点看返回值最直观。4.3 控制台注入的hook不生效有时候你按前面的方式在Console里覆盖了JSON.parse但页面刷新后hook丢失或者hook生效了却不打印。第一个问题可以通过Snippets解决在Sources里的Snippets保存注入代码每次刷新后重新执行。第二个问题更关键通常是页面里有一个独立的域内JSON.parse引用或者数据不是经过JSON.parse而是用了eval、Function构造器。这时候改hook目标把console.trace的位置换个数据入口比如劫持Response.text()const rawText Response.prototype.text; Response.prototype.text async function(...args) { const body await rawText.apply(this, args); console.log(响应原文, body); return body; };这种能抓到更底层的数据对某些自定义parse场景反而更有效。4.4 密钥是动态生成的不是写死的碰到的最现实问题就是key从接口里动态获取或者由一串随机数拼接。排查思路是回到断点处看key这个变量的来源。如果是来自某个全局变量就去搜这个全局变量什么时候被赋值如果来自请求那就先把请求拉出来看找到生成逻辑。某古加这个项目中其实IV也是从一个固定函数计算出来的只是结果恰好固定成00112233445566778899aabbccddeeff。当时我用Memory搜索功能搜过内存里的密钥字符串发现它不单纯是硬编码而是先经过一段bytesToHex再参与计算。遇到动态密钥时最稳妥的结论不是“拿到密钥”而是“复现密钥生成函数”不然下次刷新又失效。5. 几点务实建议响应解密做多了会发现真正提升效率的是流程化的思路和稳定的工具习惯。建议平时至少把DevTools的Search、Local Overrides、XHR断点、Snippets这几个功能练熟。Search帮助定位Overrides帮助批量修改JS逻辑XHR断点帮追请求链路Snippets帮注入调试脚本。遇到一个陌生站点按“观察响应特征→搜关键词→断点定位→抠解密函数→本地验证”的顺序走基本不会偏。另外密钥和算法还原之后不要只跑一次就完事。建议把解密逻辑封装成一个可复用的脚本函数输入密文输出明文再配合请求脚本以后每天拉数据都稳定。像某古加这次我用Python重写了AES解密并接到数据抓取流程里每天定时跑都没有再出过问题。最后分享一个个人习惯每做完一个项目我都会把“响应特征→算法类型→定位方法→密钥来源”写成一份最小记录。下次遇到相似特征的项目翻一下笔记就能快速复用。这个习惯帮我省了不少时间也推荐给你。