JS逆向入门实战:模拟登录接口的加密参数定位与签名还原 搞前端和爬虫的朋友应该都有过这种体验接口地址、请求头、表单数据全都照着复制了一遍结果服务端甩回来一句“参数校验失败”或者“签名不正确”。这时候你即使骂骂咧咧也没办法只能打开开发者工具对着某个加密函数开始发呆——这其实就是JS逆向最常见的入口。JS逆向说白了就是把网页上运行的JavaScript逻辑搞清楚弄明白接口参数是怎么生成的然后在你自己的代码里去复现这个过程。它不是什么黑魔法也不是只有大牛才能玩的东西本质上就是前端调试技能的延伸。今天我不打算讲那些硬核的字节码、AST还原而是用一个非常典型的模拟登录接口作为案例把从抓包、定位、调试到还原的完整过程一步步走一遍。这个思路适用于绝大多数入门级JS逆向场景学会之后你会发现自己再看那些“加密参数”时心里会有底得多。先提醒一句逆向分析必须只用于自己拥有权限或获得授权的项目比如自己开发的测试站点、公司产品、公开接口的学习研究。千万别拿它去打别人系统的歪主意这里面风险和法律责任都不小。1. 动手之前先把工具和思路备齐逆向不是拿着F12一顿乱点就行的工欲善其事必先利其器。我第一次带新人时很多人连调试面板的按钮都没认全就开始找函数结果折腾半小时连加密入口都没摸到。所以先把基础工具和完整思路理清楚。1.1 三件套工具别贪多新手不需要装一堆高大上的工具有Chrome浏览器、一个能跑JavaScript的环境、一个抓包工具完全够了。Chrome DevTools这是绝对的主战场。平时用的Network看网络请求、Sources看源代码和断点调试、Console跑点临时脚本这三个面板覆盖了九成以上的入门需求。新版Chrome还自带了Overrides和本地替换功能调试混淆代码的时候特别有用。Fiddler或Charles抓包工具的作用是补足浏览器Network面板看不到的内容或者针对非浏览器端比如小程序、APP的调试。基础入门阶段其实用不上但如果你想深入HTTP层的重放和修改请求可以提前安装一个熟悉熟悉。Node.js这是还原阶段的主力。分析出加密函数以后总不能在人家的浏览器里跑你需要在本地用Node.js复现一遍拿到跟网页端一样的结果。Node.js内置的crypto模块尤其重要MD5、SHA、AES、RSA这些常见算法它都直接支持省去了自己找第三方库的麻烦。除了这些还可以准备一个顺手编辑代码的IDEVS Code就够了。不要一上来就研究什么AST、扣代码工具那样容易掉进工具主义的漩涡里反而被绕晕。1.2 记住这条逆向主线我一般会把逆向过程概括成一条主线找入口、看依赖、理逻辑、复现验证。找入口的意思是定位那个可疑参数是在哪里生成的。很多时候你看到一堆压缩函数根本不知道怎么下手这时候就要靠Network面板里请求触发的调用链或者全局搜索某个参数名。看依赖的意思是搞清楚加密函数调用了哪些变量、哪些依赖。比如一个RSA加密公钥从哪来的可能是写死在JS里也可能来自一个接口。一个MD5签名可能拼接了时间戳还可能拼接了一个固定的secret。理逻辑是最花时间的一步。你要逐步跟进每个变量的赋值过程看清楚哪些是写死的哪些是动态计算出来的。这一步不能靠猜得用断点去逐行验证。复现验证则是最后一步你可以用Node.js把算法写出来跑一遍跟浏览器里抓到的结果做对比。如果结果一致那就说明你把逻辑吃透了。如果不一致回头检查是某个参数拼接顺序错了还是某个编码处理漏了。这条主线几乎适用于所有入门级场景。后面我展开讲每一步的坑和技巧。2. 四个办法快速找到加密函数定位加密函数是整个逆向过程中最让人头疼的环节。很多新手都会问我同一个问题“这么多JS文件我到底该看哪个”答案通常不是靠肉眼扫而是靠浏览器给的线索。下面四个方法是我平时最常用的按从拿到请求到深入逻辑的顺序排列。2.1 从网络请求里锁定可疑参数第一步永远是打开Network面板勾选Fetch/XHR然后触发目标操作比如登录、提交表单、翻页。找到对应的接口请求后重点看Payload或Form Data不要只看简单的参数名要看参数值长什么样。举个例子如果某个参数是一串32位的十六进制字符串那基本可以断定它是MD5或类似哈希算法的产物如果参数是超长的一串字母数字很可能经过了Base64编码如果参数里有类似-----BEGIN PUBLIC KEY-----的片段那就是RSA公钥或公钥的变体。通过参数特征能快速缩小加密算法的范围。还有一个细节在Network面板里点击该请求选择“Initiator”可以看到发起这个请求的调用栈。这里会直接列出相关的JS函数和文件位置点进去就能跳到Sources面板里的对应代码。这对于定位加密函数来说是最直接的导航。2.2 全局搜索参数名直捣黄龙如果Initiator不够精确或者跳过去的代码太晦涩那就用全局搜索功能。在Sources面板里按CtrlShiftF输入你想找的参数名比如sign、password然后回车浏览器会在所有加载的JS文件中搜索这个字符串。这里要踩好一个点很多生产环境的JS都是压缩过的整个文件可能只有几行所有变量名都被替换成了短名字这时候直接搜sign大概率搜不到。你需要先格式化。点击左下角的{}按钮把压缩代码美化一下搜索命中率会高很多。我建议把搜索范围限定在当前页面加载的几个关键JS上别全站搜否则结果太多根本看不过来。如果搜不到很可能参数名是动态拼接的比如params[sign这时可以尝试搜索sign或者sign甚至搜索某个算法特征比如md5、sha256。2.3 事件断点沿着点击动作找逻辑如果搜索之后找到的位置是个空壳或者逻辑太复杂那就直接动态调试。用事件断点的方式可以精准捕获用户操作触发的JS执行路径。具体做法在Sources面板右侧的“Event Listener Breakpoints”里展开“Control”勾选click事件。然后回到页面点击登录按钮。此时浏览器会在触发点击事件的那个监听函数处自动暂停。这时候你在调用栈Call Stack里能清晰看到是从哪里进入的顺着调用栈往上找就能找到参数生成的位置。这种方法对于登录、提交按钮这类交互尤其好用。因为无论是React、Vue还是原生JS点击事件总会经过某个函数事件断点能帮你绕开框架的复杂封装直接看到真实逻辑。2.4 Hook一把梭参数出现瞬间截住如果某些加密函数在页面加载时就已经执行了或者请求是由定时器发起的光靠点击事件断点不一定抓得住。这时候可以用Hook的方法提前改写关键函数让它在执行到某一处时自动暂停。最简单的Hook方式是重写某个常见原生函数比如JSON.stringify因为很多加密逻辑都会经过序列化这一步。在Console里先执行这样一段代码(function() { const originalStringify JSON.stringify; JSON.stringify function(obj) { console.log(JSON.stringify 被调用了参数, obj); debugger; return originalStringify.call(this, obj); }; })();这里用debugger;会在函数调用时触发断点浏览器随即暂停。你可以在调用栈里回溯看看是哪个函数调用了JSON.stringify它准备把什么数据序列化出去。这个方法非常暴力但对新手识别加密函数位置很有帮助。当然Hook不只限于JSON.stringify像JSON.parse、atob、btoa这些都可能成为加密链路的必经之路。等你熟练以后还可以通过Override Sources里的文件把Hook代码永久注入到页面里不过那是进阶玩法入门阶段用Console临时执行就够了。3. 完整案例模拟登录接口的签名还原理论说了不少我们直接走一个完整的入门案例。我特意准备了一个模拟项目“某跨平台系统”的登录接口它的前端结构和加密方式都很典型不涉及任何真实站点只是用来演示方法。目标是还原出登录请求里的password和sign参数生成逻辑。3.1 案例设定这个模拟系统的登录接口是POST https://example.test/api/login提交的JSON格式如下{ username: test_user, password: a1b2c3d4e5f67890..., fingerprint: fp_8f9d2c1b, timestamp: 1704240000000, sign: d41d8cd98f00b204e9800998ecf8427e }username是明文用户名fingerprint是一个设备指纹看起来是一串带前缀的哈希。password明显不是明文是一个很长的Base64字符串。sign是32位十六进制八成是MD5。现在我们要搞清楚它们分别怎么来的。3.2 抓包先看清请求长什么样打开开发者工具的Network面板勾选Fetch/XHR在页面上输入用户名和密码点击登录。查看login请求在Payload里能看到上面那几个参数。先核对哪些参数是固定的哪些看起来是动态的。可以看到username是我输入的明文timestamp是当前时间戳fingerprint在页面加载时就生成了password和sign是最可疑的。点击请求的Initiator标签页浏览器会显示调用栈发现这个请求是从某个utils.js文件里的buildLoginParams函数发出的。直接点击跳转到Sources面板。3.3 定位签名函数并下断点跳转过去后发现utils.js是压缩过的先用左下角的{}格式化。格式化之后搜索“sign”一下就看到了一段可疑代码function buildLoginParams(username, password) { var timestamp Date.now(); var passwordEncrypted encryptPassword(password); var fingerprint getFingerprint(); var params { username: username, password: passwordEncrypted, fingerprint: fingerprint, timestamp: timestamp }; params.sign md5(sortParams(params) a1b2c3); return params; }这里已经非常清楚了。sign是由md5(sortParams(params) a1b2c3)生成也就是说把所有参数排序拼接之后加一个固定字符串a1b2c3再取MD5。password来自encryptPassword(password)这个函数继续跳进去看。3.4 读取函数逻辑并还原加密流程跳到encryptPassword函数内部看到这样的代码function encryptPassword(value) { var publicKey -----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...; var encrypted new JSEncrypt(); encrypted.setPublicKey(publicKey); return encrypted.encrypt(value); }这是非常标准的jsencrypt库做RSA加密用的是服务端下发的公钥加密方式是PKCS#1 v1.5填充。只要在Node.js里引入对应的RSA模块填入相同的公钥就能得到同样的结果。接着看sortParamsfunction sortParams(obj) { return Object.keys(obj) .filter(function(key) { return obj[key] ! undefined; }) .map(function(key) { return key obj[key]; }) .sort() .join(); }这个逻辑就是对参数对象取key过滤掉值不是undefined的拼成keyvalue形式然后按照字典序排序最后用连接成一个字符串。正好对应常见的参数签名规则。再往下看getFingerprint它是基于Canvas和UserAgent生成的一个值但这里还好它并不是每次随机变化而是页面加载后固定。我们可以先手动把它模拟出来直接写死在本地代码里这在实际项目中很常见。3.5 用Node.js验证还原结果梳理清楚逻辑之后用Node.js实现一遍。先安装必要的依赖npm install node-rsa然后写一个还原脚本按上面分析的流程生成完整请求参数const crypto require(crypto); const NodeRSA require(node-rsa); const publicKey -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC... ; function encryptPassword(value) { const rsa new NodeRSA(publicKey, pkcs8-public); return rsa.encrypt(value, base64); } function md5(str) { return crypto.createHash(md5).update(str, utf8).digest(hex); } const username test_user; const password 123456; const timestamp Date.now(); const fingerprint fp_8f9d2c1b; const passwordEncrypted encryptPassword(password); const params { username: username, password: passwordEncrypted, fingerprint: fingerprint, timestamp: timestamp }; function sortParams(obj) { return Object.keys(obj) .filter(key obj[key] ! undefined) .map(key ${key}${obj[key]}) .sort() .join(); } const sign md5(sortParams(params) a1b2c3); console.log(JSON.stringify({ username, password: passwordEncrypted, fingerprint, timestamp, sign }, null, 2));跑一遍之后把生成的sign和浏览器里抓到的sign对比如果一致说明逻辑完全复现。如果偶尔不一样注意检查timestamp是不是同一个因为签名依赖时间戳每次请求的值都不同。所以这里的验证思路是在浏览器里断点停住时记录下当时的参数内容和结果然后在本地脚本里用完全相同的时间戳去算才能比对。这一步做完整个入门案例就闭环了。以后遇到类似的接口你只需要把加密函数和签名规则拆出来用Node.js实现一份就能在自己代码里生成完全合法的请求参数。4. 新人最容易踩的四个坑附排查思路光会跑通一次流程还不够实操中总有几个反复出现的坑。我把新人问得最多的四类问题集中整理一下每个都给了解决思路遇到的时候直接照着排查就行。4.1 无限调试一步一弹窗怎么办有些工程为了防调试会在代码里埋下debugger语句你一打开Sources面板就会被卡住关掉又继续弹非常烦人。最简单的处理方式是点击DevTools上的“Deactivate breakpoints”按钮快捷键CtrlF8它会忽略所有断点包括代码里的debugger。这个按钮是一个禁止符号图标使用后会在断点处暂停的只有你自己手动下的断点了。如果这样还不行说明代码里用了定时器反复设置debugger或者有断点死循环。这时可以用Overrides功能在Sources面板里找到那个JS文件右键选“Save for overrides”通过本地文件替换掉线上代码把讨厌的debugger删掉再保存。这是比较常用的绕过手段不过仅适用于你自己研究代码的时候不要把它用在未授权的地方。4.2 压缩混淆的代码格式化成什么样生产环境的JS几乎都是压缩过的基本的格式化可以通过{}按钮解决。但如果碰到混淆程度比较高的代码比如变量名全是_0x1234这样的十六进制字符串格式化之后依然很难读。入门阶段我不建议你去研究AST反混淆那是进阶范畴。更好的方式是先找到可读性较高的“关键片段”比如某一段字符串常量、某个加密函数名利用搜索直接在压缩代码里定位它。然后只看这一小段逻辑把变量名在本地手动替换成有意义的名字这样反而比整体反混淆更快。举个例子当你搜索md5时可能跳出一行c(0x2a1b, _0x9e2f)不要被变量名吓到你可以在调用位置打断点运行时通过Watch面板看这个函数返回的实际值通过输出反推逻辑。记住一句话变量名是乱的但数据流是清晰的用断点观察变量值比硬读代码更高效。4.3 动态参数时间戳、随机数与指纹很多签名直接把Date.now()或随机数作为参数拼接进去导致你每次生成的请求都不一样。新人常见错误是照抄浏览器里的固定值结果一跑就失败。遇到timestamp最佳实践是看它生成之后是否参与签名。如果参与你就要确保自己代码里的时间戳与你计算签名的时刻一致最好在同一毫秒内完成或者从服务器时间获取。如果服务端允许一定时间窗口比如5分钟那你用本地时间基本没问题。如果遇到随机数比如nonce你需要找到它的来源。可能是Math.random()生成的也可能是从某个固定算法生成的。对于Math.random()生成的只要它参与签名你同样需要在本地生成一个随机数再参与签名计算而不能拿旧的。对于固定算法生成的指纹比如Canvas指纹可以尝试在浏览器里先抓一次然后在本地固定使用。很多平台会把指纹作为弱校验固定值也能通过但要注意不同浏览器环境可能验证不一致。4.4 加密函数藏在多文件/混淆模块里有时候找到的加密逻辑不在同一个文件里而是分散在多个模块中甚至通过webpack打包成了一大坨代码。你很难直接复制函数因为它依赖了一堆其他函数比如module.exports、require之类。基础解法是先找到打包后的模块入口。如果你看到代码里有类似webpackJsonp、__webpack_require__这样的结构说明是webpack打包的。可以用Sources面板的搜索功能定位你要的那个模块ID然后把相关的模块代码复制下来给它们构造一个最小的运行环境。我讲一个简单实用的办法与其花费大量时间去抠模块不如直接在该函数调用位置下断点通过Console执行copy(函数名.toString())把函数源码复制出来然后再单独补全依赖。对于入门案例通常只需要补一个秘钥变量或一个工具函数就够了。等以后遇到更复杂的webpack模块再考虑用专门的模块加载器来跑整包代码。写在后面的实在话入门JS逆向我见过最快的人从零开始到独立还原一个签名逻辑大概花了一个星期最慢的则卡在“找入口”这一步转了半个多月。区别不在于天赋而在于他们有没有把调试当成一项系统性的技能去练。我个人带新人的习惯是先不要碰混淆过分的网站找一个最简单的模拟接口把抓包、断点、调用栈、还原验证这条链路完整跑通。跑通一次后再换一个参数更多、逻辑更绕的接口重复同样的流程。几次下来你会慢慢形成一种“肌肉记忆”看到请求参数就能猜出大致的加密形态看到函数结构就知道该从哪里断点。最后再分享一个小技巧当你复现的签名偶尔对不上时别急着怀疑算法先检查编码问题。同样的字符串在浏览器和Node.js里UTF-8编码细节可能不一致尤其是中文参数和特殊符号。很多看似“玄学”的签名失败其实都是少了一个encodeURIComponent或者编码顺序不对导致的。希望对还在入门阶段的朋友有点帮助。下一步可以试试看自己对RSA、AES这类非对称加密更熟还是对MD5、SHA这类摘要算法更熟从自己熟悉的方向入手逆向会顺很多。