不少做前端开发、接口联调或者数据监控的朋友应该都遇到过这种情况明明在浏览器里能看到页面上的数字但打开开发者工具一看接口返回的却是乱码一样的东西。第一次遇到某药品展示平台的价格Price字段返回一串加密字符串时我也是这种反应。这篇文章就用一个模拟项目完整复盘我当时对某药品网站价格字段解密的全过程包括定位加密入口、还原加密逻辑、环境补齐、常见坑点这几个核心环节。先说清楚我为什么要做这件事。正经业务场景里我们并不需要去“爬”别人的数据。真实需求是我负责的一个医药信息展示平台需要对接第三方数据做展示校验而对方接口返回的字段是加密的文档只给了一小段说明。为了完成合法的联调和数据核对我必须搞清楚前端是怎么把密文解开、展示到页面上的。这种场景在前后端分离、敏感字段加密的系统中非常常见解密的目的不是绕过安全机制而是理解数据链路保证联调能正常进行。1. 定位加密入口从接口响应到JS代码1.1 先看清请求长什么样打开开发者工具的网络面板刷新页面找到那个返回药品价格的XHR请求。我习惯先看响应体因为能直接确认返回的是不是密文。那次看到的是类似于这样的一串数据{code:0,data:[{drugName:阿莫西林胶囊,price:Ax8fQ3kP2sLmNcVb...}]}price字段明显不是明文而其他字段比如drugName、spec都是明文的。这就说明了一个重要信息加密不是整包加密而是字段级别的加密。这是很多业务系统的常见做法——接口本身走HTTPS敏感字段再加一层业务加密防止数据在中间链路被窃取后直接暴露。这一步的关键动作是区分“传输加密”和“业务加密”。如果整个响应体都是二进制或者Base64乱码那大概率是整体加密或压缩如果只有个别字段异常那就是单独处理过的。只有把类型判断对了后续分析才不会走弯路。看请求头也是必做动作。Content-Type、Accept、自定义Header等情况能反映出对方前后端的约定方式。如果请求头里带了X-Requested-With: XMLHttpRequest说明这是一个标准的Ajax请求加密逻辑大概率在前端JS里完成。如果还看到了自定义的sign、timestamp之类的Header那就要多个心眼——价格字段的解密可能需要先处理签名逻辑。1.2 搜索关键字找到加密函数在Sources面板里定位加密逻辑我一般用全局搜索。在JS文件堆里找关键字优先搜索这些price字段名decrypt解密encrypt加密AES、DES、RSABase64、CryptoJS在Chrome开发者工具里可以通过CtrlShiftF在所有的JS文件中进行全局搜索。搜索price关键字结果往往会非常多那就先加上引号搜索price或者直接搜索price:这样能更快定位到赋值语句。比如在某次分析中搜索price之后在static/js/chunk-xxxx.js里找到了一段疑似处理价格显示的代码let showPrice decryptPrice(list[i].price);这里出现了decryptPrice函数这就是关键的搜索锚点。顺着这个函数名我直接在Sources面板里搜索decryptPrice找到了它的定义位置。这种方式比一条条看堆栈效率高很多。1.3 从调用栈回溯逻辑找到decryptPrice之后按CtrlShiftF搜索函数名点击搜索结果左侧的代码行号可以跳转到对应的JS文件位置。在行号上打一个断点刷新页面当程序执行到这一行时会自动停下来。这时候在右侧的Call Stack调用栈面板里能看到完整的调用链。调用栈的好处是能看清“数据从哪来、经过什么处理、最后到哪去”。从栈顶到栈底可以梳理出类似这样的链路decryptPrice (被调用处) - formatDrugInfo (格式化药品信息) - renderList (渲染列表) - handleResponse (处理接口响应)如果继续往栈下面看可能还能找到接口回调函数里是否已经做了数据预处理。分析这个过程的实际价值在于加密逻辑可能不是直接在渲染层解开的而是在数据流早期的某个拦截器里就已经解密了。搞清楚调用链才能避免在错误的层级里浪费大量时间。2. 核心加密逻辑拆解2.1 还原加密流程在断点处单步执行F11一步步进入decryptPrice函数内部看到类似这样的代码结构function decryptPrice(encryptedData) { var key CryptoJS.enc.Utf8.parse(a1b2c3d4e5f6g7h8); var iv CryptoJS.enc.Utf8.parse(1234567890abcdef); var decrypted CryptoJS.AES.decrypt(encryptedData, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8); }看到CryptoJS.AES.decrypt事情就简单了。这是很典型的AES-CBC解密。key和iv就直接写在JS代码里变量名虽然不起眼但实际上没有任何混淆。这里要解释一下AES-CBC的基本原理方便新接触的朋友理解AES高级加密标准是分组加密算法每组固定16字节。CBC模式密码分组链接模式在加密时每个明文分组会先与前一个密文分组进行异或再执行加密解密就是逆过程。Pkcs7是填充方式当明文长度不是16字节的整数倍时会在末尾填充额外字节填充分子节值等于填充长度。这种组合在业务加密中非常普遍因为JS端的加密库成熟稳定后端也能用对应的库轻松解开。2.2 参数生成规则光看到key和iv还不够还要弄清楚它们是怎么来的。有些项目是硬编码的就像上面这段但也有项目是动态生成的比如通过某个接口获取、或者由固定字符串和时间戳拼接后经过摘要算法生成。在这个模拟项目中我顺着变量定义往上查注意到key和iv是通过一个工具函数生成的var key getKey(); var iv getIv(); function getKey() { return CryptoJS.enc.Utf8.parse(a1b2c3d4e5f6g7h8); } function getIv() { return CryptoJS.enc.Utf8.parse(1234567890abcdef); }如果这两个函数内部是固定的字符串拼接那本质上还是硬编码。但常见的动态方案有两种密钥由某个公开请求如获取配置的接口返回密钥由固定字符加登录用户的token拼接后截取如果是后者那么在分析时必须定位到token的获取方式因为每次登录的token不同密钥也随之变化写死是行不通的。在那个模拟项目里好在key和iv都是固定值省了很多事。我通常会在本地先写个测试脚本验证一下解密逻辑是否正确const CryptoJS require(crypto-js); let encryptedData Ax8fQ3kP2sLmNcVb...; let key CryptoJS.enc.Utf8.parse(a1b2c3d4e5f6g7h8); let iv CryptoJS.enc.Utf8.parse(1234567890abcdef); let decrypted CryptoJS.AES.decrypt(encryptedData, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); console.log(decrypted.toString(CryptoJS.enc.Utf8));跑一下如果能输出类似49.50这样的明文说明逻辑就对了。2.3 解密后的数据与页面映射解密本身只是一步接下来还要确认解出来的数据在页面上是怎么用的。回到渲染代码价格解密后会拼入商品卡片模板比如html span classprice¥ decryptPrice(item.price) /span;这就确认了price接的是价格数值字符串。我习惯再检查一下有没有后续处理比如乘以折扣、加上单位、保留小数位等。如果存在这种后处理接口返回的密文和最终展示值之间可能隔了两层逻辑只看页面显示会得出错误结论。3. 实操过程与工具链3.1 断点调试的实用技巧断点调试是整个分析过程里效率最高、也最容易卡住人的环节。分享几个我反复使用的技巧第一条件断点。如果列表很长每次循环都会断下来逐个排查太费时间。在断点处右键选择编辑输入条件比如item.price Ax8fQ3kP2sLmNcVb...那么只有价格匹配到这个特定值时才会暂停。第二小黑板监视Watch。在调试时把decryptPrice、key、iv加到Watch面板里程序暂停时可以直接看到当前值。这比一个一个鼠标悬停看变量值高效得多。第三Call Stack里快速跳转。有时候函数被多个地方调用栈里能看到非常长的调用链。双击调用栈里不同的层可以直接跳转到对应代码位置方便梳理完整链路。第四Overrides本地替换。Chrome的Overrides功能可以把远程的JS文件映射到本地文件。对于分析加密逻辑这个方法特别适合“改一行刷新看效果”的场景。你在本地文件里加上一个console.log(decrypted.toString(CryptoJS.enc.Utf8))保存后刷新页面控制台就打印出所有解密后的价格。不用反复打断点非常方便。3.2 补环境跑通解密函数有时候因为JS函数体依赖了页面的全局变量直接复制到Node.js环境运行会报错。比如函数内部用了window、document、localStorage等浏览器对象。报错信息常见的有ReferenceError: window is not definedTypeError: Cannot read property getItem of undefined解决办法通常是补环境。最简单的方案是在Node.js里定义一个假的window、document对象global.window global; global.document { getElementById: function() { return null; } }; global.localStorage { getItem: function() { return null; }, setItem: function() {} };当然如果直接用CryptoJS重写一遍解密逻辑其实不用补这些环境因为AES解密本身不依赖DOM。我推荐这样做先用重写的方式验证算法正确性如果遇到复杂的混淆或者环境校验再考虑补环境运行原始函数。关于运行环境还有一个“组合拳”思路把整个加密相关的JS代码放进一个单独的沙箱脚本用vm模块跑const vm require(vm); const fs require(fs); const code fs.readFileSync(./target.js, utf-8); const context { window: {}, document: {}, localStorage: {} }; context.window context; vm.createContext(context); vm.runInContext(code, context);这样做的好处是尽量还原浏览器环境。如果目标函数还依赖了CryptoJS库需要把库文件也合并进来再执行。3.3 本地验证并批量测试对于单个密文解密验证没什么问题但要确认这个解密逻辑对请求返回的全部数据都有效不能只测一条。我会把接口抓下来存成本地的JSON文件批量跑解密脚本检查是不是所有price字段都能顺利解开是否有异常情况比如有的字段不是加密的、有的密文格式不同。这里贴一个批量验证的脚本框架const fs require(fs); const CryptoJS require(crypto-js); const rawData fs.readFileSync(./response.json, utf-8); const data JSON.parse(rawData).data; const key CryptoJS.enc.Utf8.parse(a1b2c3d4e5f6g7h8); const iv CryptoJS.enc.Utf8.parse(1234567890abcdef); data.forEach((item, index) { if (!item.price) { console.log(第 index 条价格为空); return; } try { let decrypted CryptoJS.AES.decrypt(item.price, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); let plainText decrypted.toString(CryptoJS.enc.Utf8); console.log(item.drugName plainText); } catch (e) { console.log(第 index 条解密失败:, e.message); } });实际跑下来价格字段基本都能正常解出数字。少数几条因为密文里多了一些不可视字符或者Base64换行符导致解密失败就说明做数据时还应该对密文做一次合法性清洗。4. 常见问题与排查技巧实录4.1 动态混淆与变量名压缩很多网站在生产环境的JS是经过混淆压缩的变量名是一堆a、b、c或者_0xabc123这样的十六进制风格。刚开始看着很头疼但有几个应对技巧不直接读变量名而是通过断点观察输出来判断逻辑。利用格式化工具Sources面板左下角的{}按钮把混淆JS格式化成可读形式。专注于字符串常量因为混淆器一般不会改字符串内容。看到AES、Base64、Pkcs7这些字符串就可以反推用的算法。借助搜索功能定位特定字符串然后看周围的逻辑。还要留意控制流平坦化。这种混淆把原本顺序执行的代码拆散到大量分支里用循环和状态变量控制执行顺序。遇到这种建议放弃人肉梳理直接在关键调用处打断点实际跑数据。运行时的数据比静态代码更可靠。4.2 环境检测机制现在不少前端JS会做环境安全检测检测是否是正常的浏览器环境。常见的检测点包括window.chrome对象及其内部属性navigator.webdriver是否为truedocument.hidden状态是否存在phantomjs、selenium等自动化工具的特征浏览器语言、时区、字体等指纹信息我们正常联调时别人页面里的JS不应该做这么重的校验。但如果遇到环境检测不要往对抗方向想而应该回到业务本身如果这个接口文档明确说要做联调大概率有测试环境或者对应的开放接口前端加密只是为了防外部的普通访问真正的合作方一定有官方通道。这时最务实的做法是确认自己是否有合法的密钥与接口文档而不是去逆向别人的前端。从技术学习角度看做环境检测观察是很有意思的但不建议在真实系统上做违规操作。4.3 字符编码与Base64的坑解密后得到乱码最常见的原因就是编码不一致。解密程序在很多场景下默认按UTF-8处理但对方后端如果用的是GBK或者其他编码中间转换就乱了。我遇到过的具体情况是这样的解密后toString(CryptoJS.enc.Utf8)输出了一堆方块字符但用CryptoJS.enc.Hex输出十六进制字符串时可以看到数据其实是完整的说明问题出在字符解码不算法本身。解决方式是把解密结果先转成字节数组再手动按正确的编码解码。如果对方没有说明编码优先尝试UTF-8其次GBK。另一个坑是Base64密文里包含换行符或者空格。接口返回的密文有时因为传输格式化被切成了多行直接把多行密文传进解密函数会报错。应对办法是在解密前做一次清理let normalized encryptedData.replace(/\s/g, );第四种情况是密文前缀。有些系统会在密文前面加一段标识比如AES$、enc:用来区分加密方式这种必须先去掉前缀再做Base64解码。4.4 关于返回数据中其他加密字段的联动如果页面上不仅价格是加密的库存、销量、优惠券金额也加密了但加密方式不一样——不要分别去逆每一个字段。先尝试用同一套解密逻辑验证其他字段如果失败再单独分析。不同字段用不同密钥的情况也存在这时候在接口回调入口打断点一次性观察所有字段的密文和对应的解密函数调用情况效率是最高的。我见过一种比较危险的设计单个页面发起多个接口请求价格数据来自A接口展示解密参数来自B接口。如果只盯着A接口分析永远也找不全逻辑。所以拿到一个加密字段时我的习惯是先把所有接口的响应都检查一遍看看有没有哪个接口直接返回了密钥或者配置信息。5. 写在最后的几点体会做完整个分析我对“JS逆向”这件事的看法是这样的它是一种理解现代前端架构的手段核心价值在于梳理数据链路、排查联调问题。实际工作中正规的业务对接一定会有对应的文档或密钥不应该也不需要依赖破解对方前端保护。从我个人的经验出发最值得记住的是三个习惯第一动手前先分清目标。你要做的是联调、是性能监控、是数据核对还是纯技术学习目的不同操作边界完全不同。第二善用调试工具而不是盲目搜索代码。断点、调用栈、条件断点、本地替换这套组合能解决80%以上的加密字段定位问题。剩下的无非是算法识别和环境补齐。第三解密脚本尽量本地化。一拿到密文立刻在本地写一个最小可跑的验证脚本用真实数据反复验证再去追深层的逻辑。这样每一步都有结果反馈不容易跑偏。那次分析最终在半天内完成关键就在定位准确、方法对路。如果你也正好在处理类似的加密字段问题希望这篇复盘能帮你少踩几个坑。不同项目的加密强度差别很大但分析思路是相通的核心就是找入口、抓函数、看参数、跑数据。把这条路走通大部分问题都能迎刃而解。