JS逆向实战:前端参数加密与算法还原全攻略
做前端数据采集和技术研究的朋友对JS逆向这个词肯定不陌生。尤其是这两年你会发现越来越多的网站把核心参数做了加密处理登录态、搜索参数、翻页参数几乎每一个关键请求都带着一串看不透的密文。我最初接触JS逆向的时候就是被这种参数加解密逼出来的——明明浏览器里清清楚楚的数据换到脚本里请求就直接失败返回一句“参数错误”或者“签名无效”那种憋屈感相信很多人都有过。这篇内容不是教科书式的理论讲解而是我这些年在实际项目中反复踩坑、反复验证后整理的一整套JS逆向参数加解密处理方法。我尽量用大家都能听懂的大白话把参数加密的常见形态、定位加密入口的方法、算法识别与还原的思路、以及最终把逆向结果落地成可用工具的全过程讲透。无论你是刚入门的爬虫学习者还是已经在做接口分析的技术人员这篇文章都能给你一套可以直接上手的参考方案。特别是那些遇到“搜不到加密函数”“还原算法后签名不对”“代码被混淆到怀疑人生”的朋友我会把这些问题逐一拆开讲明白。1. 先搞清楚你到底在逆向什么很多新手拿到一个加密参数就开始埋头翻代码翻了两小时什么也没找到其实是对目标缺乏最基本的判断。JS逆向参数加解密本质上做的事情是一个网页在浏览器里发请求时JS代码在发送前对某些字段做了一次变换服务端收到后再做对应的逆变换来校验身份或者保证数据完整性。1.1 前端参数加密的常见形态我先列一个清单这是我在实际项目中遇到的最常见的几种加密形态后面的所有分析和还原思路都是围绕这些基础形态展开的类型代表算法/方式特点可逆性编码Base64、URL编码字符集固定A-Za-z0-9/肉眼可见特征直接可解码哈希摘要MD5、SHA1、SHA256定长输出32/40/64位十六进制不可逆不可逆只能碰撞或查库对称加密AES、DES、3DES有密钥密文长度与明文成正比通常有填充有密钥即可还原非对称加密RSA有公钥和私钥密文长度固定如128字节有私钥才可还原国密算法SM2、SM3、SM4国内金融、政企系统高频出现特征类似RSA/AESSM3不可逆SM2/SM4可逆自定义魔改混合编码、自研算法无固定特征需要在JS里跟读逻辑看具体实现如何快速判断类型最简单的方法是看密文本身。Base64编码通常以“”结尾哈希值一般就是32位或64位的十六进制字符0-9和a-fAES密文是一串不可读的二进制转字符串RSA密文则明显更长更规整。这种“先看症状再开药”的思路能帮你节省大量时间。1.2 为什么会有参数加密业务和非业务的原因从业务的视角看前端参数加密绝对不是无缘无故的设计。理解背后的原因对你推断加密算法的实现方式非常有帮助。最常见的目的是防篡改与防重放。比如一个下单接口前端传了商品ID和价格如果不对价格做签名攻击者完全可以抓包改掉价格再提交。服务端拿到签名后会校验参数是否被改过这就是为什么签名参数sign通常是把所有业务字段拼接后在特定规则下做哈希运算。第二个目的是增加自动化采集的成本。同样的接口浏览器直接访问没问题但脱离这个环境请求头里少了一个或几个动态变化的字段服务端一比对就识破了。这种加密未必多复杂但确实能把很多半吊子脚本挡在外面。第三个原因则偏合规和审计。一些金融、政企系统要求数据传输过程全程可追踪所以前端落在请求里的参数必须是密文服务端留存的日志里也不会出现明文敏感字段。理解这些目的之后再去看JS代码你会发现加密算法的选型是有迹可循的。防篡改往往用MD5/SHA摘要敏感数据传输用AES对称加密或RSA非对称加密而“防机器人”的往往是把好几个算法揉在一起再套一层Base64。有了这层预判再翻代码就会有的放矢。2. 定位加密入口一套稳定的方法论加密入口定位是整个JS逆向里最耗时也最考验耐心的环节。很多人卡在这里不是能力不行而是没有一套稳定的搜索和调试方法。下面这套流程是我反复验证后总结的按顺序来基本不会走偏。2.1 从数据流入手先抓包后定位拿到一个目标网站不要急着去看JS源码。第一步永远是打开浏览器的开发者工具切到Network面板勾选Fetch/XHR然后正常触发一个需要分析的操作比如登录、搜索、翻页。找到关键请求后仔细看Payload和请求头。加密参数通常出现在这几个位置请求体Request Payload里的某个字段比如pwd、sign、data、params查询参数Query String Parameters比如?tokenxxxsignyyy自定义请求头比如Authorization、X-Sign、X-Token这一步的目的不是立刻定位而是建立一个“参照系”——你先知道最终发给服务器的密文长什么样后面所有分析和验证都围绕这个密文展开。个人习惯我会在Network面板里右键这个请求选择Copy as cURL保存到一个临时笔记里。这样哪怕页面刷新了我也能随时知道原始请求长什么样不至于调着调着忘了初始状态。2.2 全局搜索关键词快速缩小范围接下来打开Sources面板按CtrlShiftF全局搜索。不要直接搜密文本身因为密文是运行时生成的源码里不可能存在。正确做法是搜索参数名比如刚才看到请求体里有个pwd字段那就搜“pwd”。如果参数名太通用搜索结果会非常多。这时候换思路搜一些加密函数常见的特征关键词encrypt、decrypt、sign、token、secret、md5、sha、aes、rsa、cipher、key、iv。搜到可疑的函数后点进对应的JS文件搜索这个函数的定义位置和调用位置。还有一种常见情况是参数名在源码里被拼接而成比如上面的案例中pwd实际上是“p”“wd”。这时候直接模糊搜索“wd”或者“pwd”的一部分可能更快。遇到压缩混淆过的代码优先搜索特征字符串比如类库名、算法名、常量值。2.3 用XHR断点拿调用栈如果全局搜索定位效率太低或者碰上了混淆严重的代码可以直接用XHR断点。具体操作是在Sources面板右侧的XHR/fetch Breakpoints里点加号添加一个URL片段比如/api/login然后重新触发请求。请求发出前调试器会自动停在发请求的那一行JS代码上这时候看右侧的Call Stack调用栈逐层往上回溯。每一层都会显示对应的函数名或变量加密逻辑就藏在这些函数里。这一步是我在实际操作中使用频率最高的一种方式因为它能直接“从终点倒推起点”。顺着调用栈往回走很快就能看到一个函数输出一串密文然后把它赋值给某个请求字段。那个函数就是你的主攻目标。3. 核心细节常见加解密算法的识别与还原定位到了函数之后下一步就是把算法看明白。很多开发者对算法本身并不陌生但在JS代码里看到各种函数调用时经常反应不过来尤其是当代码做过变量重命名和函数封装之后。这一节我把最常见的几种算法识别方法和还原思路展开讲。3.1 一看便知的哈希与Base64先说最简单的。如果密文是32位十六进制字符串基本就是MD540位就是SHA164位就是SHA256。JS里实现哈希时通常引用CryptoJS库代码大概长这样var md5 CryptoJS.MD5(str).toString(); var sha1 CryptoJS.SHA1(str).toString(); var sha256 CryptoJS.SHA256(str).toString();这类算法不可逆没法直接还原明文但如果你要用它拼接参数只需要在本地用相同的算法和原文跑一遍即可。关键难点在于搞清楚被哈希的原文是什么拼接格式比如时间戳、随机数、其它参数按什么顺序连接。这就要回到调用处去看那行函数入参。Base64也一样源码里看到btoa、Base64.encode或者CryptoJS.enc.Base64.stringify就是Base64编码。Base64看着像密文实际上是可逆的在浏览器控制台直接调用atob(密文)就能解码。遇到这类题属于送分题别花太多时间。3.2 AES的四大要素模式、填充、密钥、IVAES是实战中最常见的对称加密算法。之所以排第一是因为它加密效率高适合敏感数据传输而且JS生态里CryptoJS用得极其广泛。识别AES不难CryptoJS.AES.encrypt或者aes.js文件出现时基本可以确定。难点在于AES有四个核心要素缺一个都对不上结果加密模式CBC最常用ECB也有还有CFB、OFB、CTR等填充方式因为AES是分组加密明文长度必须对齐16字节倍数常见的是Pkcs7即Pkcs5密钥Key16/24/32字节对应AES-128/192/256偏移量IVCBC模式下需要ECB不需要一段典型的CBC加密代码长这样var key CryptoJS.enc.Utf8.parse(0123456789abcdef); var iv CryptoJS.enc.Utf8.parse(abcdef0123456789); var encrypted CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString();注意这里的key和iv都经过了Utf8.parse处理说明密钥是字符串直接转字节。有些实现用Hex.parse或Base64.parse这都会影响最终结果。还原思路把这段代码复制到浏览器Console里手动执行AES.encrypt输入一个测试明文看输出是否和抓包里的密文一致。一致就说明你找对了。然后用同样的逻辑在Node.js或者Python里实现密钥、IV、模式和填充方式保持一致就能在本地生成合法请求参数。3.3 RSA的经典玩法公钥内嵌与私钥还原RSA在JS里同样常见尤其是登录密码加密场景。识别特征是出现JSEncrypt这个库代码里通常有var encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); var encrypted encryptor.encrypt(password);publicKey是一段PEM格式的公钥字符串以“-----BEGIN PUBLIC KEY-----”开头。RSA是公钥加密、私钥解密所以如果你想反向解读密文拿到明文必须得有私钥。爬虫场景下一般不需要反向解密你只需要用同样的公钥对同样的明文做加密得到与浏览器一致的密文就能通过服务端校验。核心数字RSA加密时有一个关键点是公钥里的模数n和指数e。如果你在代码里看到一串很长的十六进制数字比如“00c1...”那很可能就是n。实操中直接把包含公钥的那段代码整体拿下来在Node.js里用jsencrypt模块加载公钥然后加密比手动还原数字更省事。3.4 富文本和自定义加密从结果反推过程最难缠的是自定义加密常见于有专门风控团队的平台。密文可能是一长串随机大小写和数字的组合看不出任何标准算法特征。这类项目没有捷径只能硬着头皮断点跟读。我的做法是三步走找到最终输出密文的那一行打断点逐步执行Step into观察每一步对数据的变换把每一步变换记录下来翻译成能读懂的伪代码举个例子我之前遇到过一个签名算法外部看是64位十六进制以为就是SHA256。跟进去才发现它内部对这个64位字符串又做了Base64编码再去掉前8位最后又补上了两个固定字符。这种“套娃”操作光靠猜是猜不出来的必须断点看逐个环节还原。遇到这种情况最忌讳的就是浮躁老老实实记录每一步最终一定能还原。4. 实操演示一个完整参数的还原过程理论说得再多不如亲手走一遍。这一节我用一个虚拟的登录场景完整演示从抓包到定位再到还原的整个流程。场景设定是登录接口/login请求体里有username和password其中password是一段32位十六进制密文。4.1 建立问题域打开浏览器登录页输入任意账号密码抓包看到POST /api/login HTTP/1.1 Content-Type: application/json {username:test_user,password:e10adc3949ba59abbe56e057f20f883e}password是32位十六进制初步判断是MD5或类似哈希。为了验证我手动把“123456”拿去做MD5哈希得到的结果正好是e10adc3949ba59abbe56e057f20f883e。那么第一个方案就成立了这个网站对密码做了MD5摘要。但要注意很多网站在MD5之前还会加盐salt比如md5(md5(password) timestamp)。所以不能急着下结论还得看完整逻辑。4.2 在源码中定位密码被加工的位置打开Sources面板全局搜索“password”。在某个函数里看到这样一行代码var encryptedPwd md5($(#password).val());这就说明密码确实是被MD5后提交的。但为了防止遗漏更多变换我决定在md5这一行打断点重新点击登录观察函数调用栈。好在栈里只有两层最外层是一个登录事件处理器没有额外加盐逻辑。此时可以确认后端收到的password就是明文密码的MD5值。4.3 在Console里验证为了确保万无一失我在Console里手工执行md5(123456) e10adc3949ba59abbe56e057f20f883e // true输出为true证明定位无误。到这里这个参数的逆向就完成了——只需要把待提交的密码用MD5哈希后拼进JSON就能通过服务器校验。4.4 把加密逻辑整理成独立代码最后一步是把算法落地成工程代码。由于这里用到的只是MD5Python的hashlib可以直接处理import hashlib password 123456 encrypted_pwd hashlib.md5(password.encode(utf-8)).hexdigest()如果场景里的加密逻辑复杂比如AES或RSA我更推荐把扣下来的JS函数原封不动放在Node.js环境中调用然后用Python的subprocess或Node.js的HTTP接口去对接。这样既保留了原始实现又避免了重写算法带来的差异问题。5. 工具与工程化把逆向结果变成生产力定位到算法、验证通过之后距离真正稳定运行还差一步——如何把逆向出来的加密逻辑嵌入到自己的工程里并且尽量降低后续维护成本。这一节讲工具选型、环境适配和稳定运行的技巧。5.1 常用工具链汇总我的日常逆向工作台分成五个部分每一部分都有明确用途工具定位核心用途Chrome DevTools浏览器调试打断点、搜代码、看调用栈、Console验证Fiddler4 / Charles抓包代理查看HTTPS请求/响应适合移动端或非浏览器场景whistle抓包改包本地规则注入、mock响应、JS替换Overrides本地代码替换把线上JS映射到本地文件方便修改调试Node.js本地运行环境运行扣下来的JS加密逻辑生成合法参数Chrome DevTools的Overrides功能特别值得一说。操作很简单在Sources面板右键任意JS文件选Override Content选择一个本地文件夹后线上JS就会被本地文件接管。之后你可以在本地自由修改代码、加console.log、甚至直接替换加密函数。这个功能在调试复杂加密时非常好用不用反复在线上代码里找位置。5.2 用Node.js做加密逻辑的本地化很多逆向文章会教你把扣下来的JS代码精简成几行然后翻译成Python。这个思路在算法简单时可行算法复杂时其实效率很低而且容易出错。我的经验是能跑原版就尽量跑原版。具体做法是从浏览器里把包含加密逻辑的那段JS函数完整复制出来保存到一个独立的js文件里然后在Node.js中调用。如果这个函数使用了window、document、navigator等浏览器对象需要做最小化的mock// mock最小浏览器环境 global.window global; global.navigator { userAgent: Mozilla/5.0 }; global.document { cookie: , getElementById: function() { return { value: }; } }; // 引入目标JS文件 const cryptoFuncs require(./target_crypto.js);然后再用导出函数的方式生成参数const encrypted cryptoFuncs.getEncryptedPwd(123456); console.log(encrypted);这样做的优势是一旦网站加密逻辑升级你只需要同步更新JS文件无需重写算法代码。当然缺点是多了一层Node.js进程的调用开销但只要控制好频率完全在可接受范围内。5.3 调试提速技巧Hook和验证再说一个能大幅提速的技巧——Hook。Hook的本质是提前“劫持”目标函数的执行在函数运行前后插入你自己的逻辑。比如你知道某网站的加密函数叫makeSign你可以在页面加载后执行下面的代码var originalMakeSign window.makeSign; window.makeSign function() { console.log(arguments); var result originalMakeSign.apply(this, arguments); console.log(result); return result; };这样每次调用makeSign时入参和出参都会打印到控制台你不需要打断点反复调试就能在短时间内拿到一组组输入输出样本。这对后续在本地还原算法、验证算法正确性非常有帮助。另一个实用的技巧是“控制变量法”手动修改一个参数观察密文变化。如果密文变化范围只出现在某一段说明这个参数只参与了部分运算如果密文整体面目全非说明参数参与了首轮变换。这类观察能帮你快速定位被哈希/加密的原始字段。6. 常见问题与排查技巧实录最后这部分是我这些年被问得最多的问题每一个都是真实踩坑记录。我把它整理成一个速查表你遇到类似情况时可以直接对照排查。6.1 加密入口找不到怎么办全局搜参数名搜不到、搜特征词也搜不到这种情况通常有三个可能参数名被动态拼接加密代码被隐藏到了Web Worker或者跨域JS文件里代码经过了变量名混淆。我的排查顺序是先搜参数名的一部分比如签名是cksign直接搜“cksign”和“sign”都试一遍确认是否有Service Worker或Worker线程在后台请求用XHR断点拿到调用栈直接回溯不依赖关键词搜索如果Stack里出现大量匿名函数说明有混淆优先看当前作用域里的函数引用6.2 算法还原后签名不匹配签名不匹配是所有环节里最容易让人崩溃的。一旦本地生成的密文和浏览器的对不上按优先级从高到低检查这几项检查项说明编码方式明文字符串是UTF-8、GBK还是Base64编码JS里通常默认UTF-8密钥和IV是否经过了Hex.parse或Utf8.parse的转换模式与填充使用的是CBC还是ECB填充是Pkcs7还是ZeroPadding数据顺序多个字段拼接时的顺序是否和线上完全一致动态参数是否包含时间戳、nonce随机数本地环境时间是否准确我在实际项目中最常栽的跟头就是动态参数——函数内部偷偷调用new Date().getTime()生成时间戳并参与加密。本地测试时这个时间戳和请求发出的时间如果相差超过允许窗口就会签名失败。解决办法是让本地代码与线上代码保持“同一个环境变量”比如统一使用当前时间戳避免用固定值。6.3 代码被混淆了怎么办现在很多站点的JS都做了压缩混淆函数名、变量名全是a、b、c阅读难度极高。面对这种情况不要试图完全看懂每一行而是抓住一条主线输入是什么输出是什么中间经历了哪几步变换。先用浏览器自带的漂亮打印Pretty print把压缩代码格式化再用局部变量动态观察法在怀疑的代码位置打断点然后逐条执行观察每个变量的变化。只要确定了输入输出即使中间逻辑看不懂你也能通过“黑盒复刻”的方式把整段函数抽出来直接调用。6.4 有风控和加密壳怎么办高阶网站会在加密逻辑之上再加一层环境校验比如检测当前是否在浏览器环境、是否使用了自动化工具。这类校验一旦触发返回的密文本身就可能是一个诱饵或者请求直接失败。我的态度是这类场景千万不要硬碰硬。单纯破解风控本身是一个无底洞而且容易触碰合规红线。正确做法是优先通过官方API、合法授权或者业务合作渠道获取数据。如果只是技术研究建议在授权的靶场环境或自己搭建的测试站上验证方案而不是拿真实业务系统练手。逆向的核心是理解原理不是突破防线。折腾JS逆向这几年我最大的一个体会是加密防不住真正想研究的人但它能把一大批投机取巧的人挡在门外。所以我自己做逆向时越来越强调“先分析再动手”的流程——从抓包到定位从识别算法到还原逻辑每一步都尽量理清楚再进行下一步。这个习惯能帮你省下大量返工时间。另外想给初学者一个实在的建议不要一上来就挑战高难度混淆站点先从简单的MD5加盐、AES加密这样的项目练手把控制台调试、全局搜索、XHR断点这些基本功练熟。等这些操作形成肌肉记忆了再面对复杂参数时就不会慌。说到底JS逆向的核心不是“破解”而是“读懂另一段代码的思路”——理解了这一点参数加解密的大门才算真正向你敞开。