☰
JS逆向打靶分析:从抓包到还原前端加密参数
2026/10/21 8:06:23 网站建设 项目流程

做JS逆向的人,圈内常说自己“不是在debug,就是在找入口”。标题这七个字——“JS逆向打靶分析”,说白了就是在一套刻意布置的目标环境里,把前端JavaScript的加密逻辑、接口鉴权、参数生成规则一层层剥开,搞清楚“这个页面是怎么把数据藏起来的”。很多刚入门的朋友问过我:这玩意儿到底练的是什么?是不是跟黑客挂钩?其实把它想复杂了。往前端安全、爬虫对抗、接口防刷这些方向走,你需要具备的第一项硬功夫,就是读懂一段经过了混淆的JavaScript代码,并还原它真正的执行意图。

这篇文章我想拿“打靶”这件事作为主线,讲讲JS逆向分析的核心思路、工具链路、实操手法和踩坑经验。内容适合前端开发、安全测试、数据采集方向的朋友参考,也适合那些想搞明白“登录框里的密码到底是怎么被加密的”的好奇派。我会避免只丢结论,尽量把每一个关键操作的“为什么这么做”解释清楚。

1. 整体设计与思路拆解:什么是“打靶”,为什么要以靶场为训练场

1.1 “打靶”的本质是刻意练习

“靶场”这个词来自安全领域,指一套人为搭建的、存在特定漏洞或加密机制的练习环境。JS逆向打靶,就是把一个带有参数加密、传输签名、前端混淆的网页当作靶子,通过分析它前端JavaScript的逻辑,还原参数生成算法,最终达到“预测请求参数、理解接口校验规则、评估系统安全边界”的目的。

你会发现,真正核心的并不是某个网站的某个加密参数本身,而是一套分析方法论——拿到一段陌生的JS代码,怎么快速找出加密发生在哪个函数里、用了什么算法、key来自哪里、怎么在外部环境复现同样的计算结果。这套方法论练熟了,换任何目标都只是换一层皮。

1.2 为什么前端代码需要“被逆向”的视角

很多人有个误解:前端代码是“自己人写的”,逻辑都在浏览器里,服务器只管校验。但恰恰因为运行环境是用户的浏览器,所有JS代码对用户都是可见的、可调试的、可修改的。所谓的前端安全,并不是让攻击者完全看不破,而是提高他分析、复现、批量利用的时间成本。

从防守方视角看,理解JS逆向等于理解“敌人会怎么拆我的代码”,这样才能在接口层面做好真正有效的防护,而不是只靠前端加密“意思一下”。从研究视角看,分析加密参数生成链路,能帮你发现系统里的逻辑漏洞、鉴权缺失、重用密钥等问题。

1.3 这需要怎么样的分析链路

我习惯把一套完整的JS逆向分析拆成五段链路:

  • 抓包定位:先搞清楚目标请求了哪个接口、带了哪些参数、哪些参数看起来“可疑”。
  • 入口搜索:在JS堆里全局搜索可疑参数名或特征字符串,定位相关代码位置。
  • 逻辑还原:顺着函数调用链读懂加密过程,识别算法与密钥来源。
  • 环境复现:脱离浏览器,在Node.js里用同样的算法跑出相同结果。
  • 验证闭环:把复现结果带回到实际请求里,校验参数是否通过服务端校验。

后面的实操部分,我会用一个模拟登录场景走一遍这五步。你练靶场的时候也按这个流程走,效率会高很多。

1.4 合规边界要先讲清楚

再说一遍,JS逆向打靶的正确姿势,是拿自己搭的环境、授权的测试系统、公开的CTF靶场去练。未授权分析他人线上系统,在法律和道德上都有严重风险。这篇文章的所有演示都是基于“你有一台属于自己的目标环境”这个前提。技术是工具,用在哪、怎么用,边界得自己守住。

2. 工具选型与调试准备:五个必备的“Code解剖”工具

好工具能省一半时间。JS逆向和普通前端开发调试不一样,它更依赖“动态观察”和“代码定位”。下面这五个工具,是我每次打靶都会开着的,按优先级排个序。

2.1 浏览器开发者工具:一切调试的起点

严格来说不是“选不选”的问题,而是必须熟练。Chrome DevTools或Edge DevTools都行,重点用三个面板:

  • Sources(源代码):打断点、看调用栈、临时改代码,都在这里。
  • Network(网络):看请求列表、请求头、请求体、响应体。顺带提一句,勾选“Preserve log”(保留日志)能防止跳转页面时把关键请求弄丢。
  • Console(控制台):直接在页面上下文里执行表达式,查看变量值,测试小段代码。

打靶时我极少用“美化(Pretty-print)”之外的一键还原工具,更多是靠断点配合调用栈去理解执行流程。因为代码逻辑和变量名被压缩混淆后,静态看很难受,但运行时断点可以让你直接看真实的值。

2.2 抓包代理:看到HTTP层之外的细节

浏览器开发者工具能覆盖大部分场景,但遇到以下情况就必须上代理工具:客户端不走浏览器(比如Electron封装的程序或手机App)、请求被ServiceWorker拦截、需要改包重发。抓包代理工具里比较主流的是Fiddler(Windows强)和Burp Suite(渗透测试综合性强),也有人用Charles。我自己最常用的是Fiddler,因为它对前端工程师友好,能直接改断点、改响应、伪造请求。

只要遇到“浏览器里能出包,但系统抓不到”这种诡异问题,第一时间想到抓包工具能补位的可能性,别在DevTools一棵树上吊死。

2.3 Node.js:把浏览器里的JS搬到本地跑

JS逆向的最终验证,绝大多数都要脱离浏览器环境。Node.js就是那个“脱离浏览器环境”的关键执行器。下载安装Node以后,你可以在本地新建一个目录,把从页面上拷贝出来的加密算法代码整理成模块,加上必要的模拟环境补丁,直接node xxx.js跑出来看结果。

有人会问:页面用的浏览器API(比如window、document)Node里面没有怎么办?这正是逆向工程的精髓之一——你不需要完整模拟浏览器,只需创建一个“最小满足环境”。比如代码里读了一个navigator.userAgent,你在Node里把它global.navigator = { userAgent: 'xxx' }补上就行。后面实操环节我会演示怎么处理这种依赖。

2.4 代码文本编辑器与脚本工具

当你需要批量搜索JS代码里的关键词、批量替换混淆变量名、写Hook脚本时,光靠浏览器的小搜索框不够。我习惯把目标JS文件通过Sources面板里的“保存”功能下载到本地,用文本编辑器打开做大范围搜索和修改。这样做的好处是:可以结合编辑器里的正则搜索、多文件搜索、代码折叠,快速梳理混淆代码的结构。

常用的文本编辑器有VS Code和Sublime Text。对JS逆向而言,VS Code的“全局搜索文件夹”功能非常有用——尤其当你有一堆JS文件需要找出某个关键词都出现在哪些位置时。

2.5 辅助工具:Hook脚本与AST工具

Hook是JS逆向里“动态插桩”的俗称。通俗讲,就是在程序执行到某个方法时,临时替换或包裹这个方法,打印出入参和返回值,这样就能知道它内部接收了什么、吐出了什么。简单场景下直接在控制台覆盖一个函数即可;复杂场景会用油猴脚本(Tampermonkey)注入自定义代码,在页面加载前抢先Hook。

AST(抽象语法树)工具属于进阶范畴。当目标代码做了高强度混淆,比如整个文件变成“一大串数组 + 位移解密”的结构,肉眼已经难以还原。这时候可以把代码解析成AST,写脚本把解密规则批量算出来,输出一份相对干净的代码。市面常说的“反混淆插件”本质上都是AST操作,只是做成了工具。

3. 实操演练:一个AES加密登录参数的完整还原过程

这一节是全文最硬核的部分。我搭了一个模拟登录页面,逻辑很简单:用户输入用户名和密码,点击登录后,前端把传输参数加密后再发请求。整个过程的还原链路非常适合作为靶场练习的第一课。

假设环境地址:本地搭建的某个演练靶场,登录请求如下:

POST /api/login HTTP/1.1 Content-Type: application/json {"username":"admin","password":"5f4dcc3b5aa765d61d8327deb882cf99","timestamp":"1710000000","sign":"9f2b..."}

肉眼可见:password不是原文,sign是一串看不出规律的hash,timestamp看起来是时间戳。目标:搞清楚password是怎么加密的,sign是怎么算出来的,能不能在Node.js里算出同样的结果。

3.1 第一步:全局搜索特征字符串

打开DevTools,切到Network面板,找到这个登录请求。重点是看Payload里参数名,挑一个不常见的——这里挑sign。它的值比较像MD5或SHA系列的输出。回到Sources面板,按快捷键Ctrl+Shift+F(全局搜索文件内容),输入sign,回车。

搜出来后,逐一点击搜索结果看上下文。别一上来就找“sign:”这样的JSON键,JS源码里更可能是"sign"作为对象key,或者变量名_sign。搜索时也试试password、encrypt、aes、md5这些关键词,扩大定位面。

经验:优先搜“赋值语句右侧出现了可疑值”的区域,比如:

var sign = hex_md5(password + timestamp);

这种一眼就能看懂大半的代码是最好搞的;遇到btoa、AES.encrypt、JSEncrypt这类跨层调用,说明加密在后面藏着。

3.2 第二步:打断点观察变量值变化

定位到可疑代码后,在那一行左侧点击鼠标打断点。然后回到页面,重新点击登录按钮,程序会停在断点处。这时候:

  • 鼠标悬停到变量上,看当前的值;
  • 右侧Call Stack面板看当前调用链;
  • Scope面板看当前作用域里有哪几个变量。

假设我停在的地方是:

var sign = hex_md5(password + timestamp);

执行前,右侧Scope里显示的password是5f4dcc3b5aa765d61d8327deb882cf99,timestamp是1710000000。这马上暴露了一个关键点:传给sign函数的password,本身已经是加密过的值,不是明文“123456”。换句话说,还有一层加密在更早的逻辑里。

顺着Call Stack往上翻几层,或者搜索password在哪些地方被赋值,会发现一个encryptByAes函数,参数是用户输入的裸密码和一把 key。这是非常典型的“先AES加密,再拼接时间戳做MD5签名”的双层结构。

3.3 第三步:分析AES加密参数与密钥来源

定位到AES加密函数后,不能只满足于“知道是AES”,还要弄清模式、填充、密钥、IV。这些参数决定了你能不能本地复现。常见写法如下:

function encryptByAes(plaintext, key) { var keyHex = CryptoJS.enc.Utf8.parse(key); var ivHex = CryptoJS.enc.Utf8.parse('0000000000000000'); var encrypted = CryptoJS.AES.encrypt(plaintext, keyHex, { iv: ivHex, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.ciphertext.toString().toUpperCase(); }

这里点很小但很坑:CryptoJS.AES.encrypt直接传字符串key,默认会认为key是密码,内部走的是OpenSSL KDF派生,不是拿字符串当AES-128密钥。只有把key转成WordArray来传,才是标准的原始密钥模式。我见过不少新手在这一步卡住——本地跑出来的结果跟抓包对不上,就是因为忽略了这种“语义差异”。

密钥还存在另一个问题:它是硬编码还是动态生成的?很多靶场为了降低难度,会把key直接以字符串形式写在JS里。级别更高的会把key藏在某段加密配置里,或者先从某个接口拉取。调试时如果发现密钥不是明文,可以先断到CryptoJS.AES.encrypt的函数入口处,看函数的实际传参——那要么是key本身,要么能顺藤摸瓜找到产生它的代码。

我把AES和MD5型签名的区别整理一下:

特征AES加密MD5类哈希
输出长度随明文长度变化固定32位(16进制)
是否可逆有密钥时可解密不可逆,只可碰撞/比对
常见伴随参数模式(CBC/ECB)、填充、IV拼接盐值、时间戳
定位关键词AES、encrypt、parse、WordArraymd5、sha1、hash、digest

在断点处确认encryptByAes的参数后,我用控制台试一下:

encryptByAes('123456', 'a1b2c3d4e5f6g7h8')

发现结果恰好等于抓包里的password值。到这里,password的加密链路已经搞清楚。

3.4 第四步:还原sign的签名规则

继续看sign的生成,常见套路是把“加密后的password + timestamp + 固定盐值”拼一起,做个hash。断点看下执行那一刻的拼接字符串就能确定。

断点执行到hash函数前,在Console里输入那段拼接表达式,可能得到:

5f4dcc3b5aa765d61d8327deb882cf991710000000jsk_2627

尾部多出jsk_2627,这是签名盐。盐的作用是防止直接拿参数拼接规律伪造签名。

算出hash后,跟抓包里的sign比对一致,验证完成。到这一步,整个链路就清晰了:

明文密码 -> AES(CBC, PKCS7, key=a1b2c3d4e5f6g7h8, iv=16个0) -> password参数 password + timestamp + 盐值 -> MD5() -> sign参数

3.5 第五步:在Node.js里复现算法

拿到完整规则后,不复制页面里的CryptoJS库,而是用Node.js的crypto模块重写一遍,代表你真正理解了它在做什么:

const crypto = require('crypto'); function aesEncrypt(plaintext, key, iv) { const cipher = crypto.createCipheriv('aes-128-cbc', Buffer.from(key), Buffer.from(iv)); let encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted.toUpperCase(); } function md5(data) { return crypto.createHash('md5').update(data, 'utf8').digest('hex'); } const key = 'a1b2c3d4e5f6g7h8'; const iv = '0000000000000000'; const password = aesEncrypt('123456', key, iv); const timestamp = '1710000000'; const sign = md5(password + timestamp + 'jsk_2627');

运行后输出:

5f4dcc3b5aa765d61d8327deb882cf99 9f2b...

跟抓包里的参数一对,完全一致。这就是一次标准的JS逆向打靶闭环。

注意:Node.js的aes-128-cbc要求key长度必须是16字节,IV长度必须是16字节。页面CryptoJS里如果传的key是32字节,对应算法就要改成aes-256-cbc。这个小细节能省很多排查时间。

4. 进阶打法:RSA、混淆对抗与反调试绕过

打完第一层靶,很多人的问题变成了:如果代码被混淆过、变量名全是乱码、甚至开了反调试,还怎么玩?这套进阶体系同样可以通过靶场循序渐进地练。

4.1 非对称加密场景怎么定位

现在的登录系统很少只上AES,反而更流行RSA。前端拿公钥加密,后端拿私钥解密,密钥不暴露在前端。定位RSA加密的思路和AES相似,搜索关键词通常是:

  • JSEncrypt(一个很常用的RSA库)
  • setPublicKey
  • encrypt.encrypt/Encrypt方法名
  • -----BEGIN PUBLIC KEY-----这样的公钥片段特征

找到后直接断在encrypt方法,看它传入的明文和公钥字符串。因为RSA公钥本身可以公开,所以网页里硬编码公钥不算漏洞。逆向价值在于:了解登录密码的加密方式后,才能判断系统是否完善——比如后端有没有及时校验时间戳、有没有防重放。

复现RSA加密在Node里也简单:

const crypto = require('crypto'); const publicKey = `-----BEGIN PUBLIC KEY----- MIIBIjANBg... ...AQAB -----END PUBLIC KEY-----`; const encrypted = crypto.publicEncrypt(publicKey, Buffer.from('123456')).toString('base64');

注意RSA加密结果会受padding影响,页面上JSEncrypt默认使用PKCS1 padding,Node里publicEncrypt默认也和它兼容,但如果你先试了没问题就别乱改。

4.2 代码混淆的识别与还原思路

我见过的靶场代码混淆,大致分四个层次:

  • 变量名压缩:像a、b、c这样的短名,逻辑本身没变,只是可读性差。解决方式靠断点看值,不需要还原命名。
  • 字符串编码:把明文字符串变成Base64、Unicode码点或自定义编码,运行前再解码。搜索关键词时字符串是乱的,所以全局搜索要换搜“特征值”或“函数行为”。
  • 数组偏移与位移算法:代码里维护一个大数组,数组里的元素顺序被打乱,每次通过一个移动函数取。这种就是“数据流混淆”,纯静态看不清,建议在运行时Hook取值的函数,把真实数据打印出来。
  • 控制流平坦化:把if-else逻辑改成一连串switch+shape变量,执行顺序被打散。还原思路是记录每个shape值的变化路径,重新组织流程。

AST工具在处理第4层时优势明显。比如Token、Babel插件都能解析JS为AST,再通过插件改写节点、合并分支结构,输出一份逻辑相对正常的代码。初学AST不要直接上重型框架,先拿短小的混淆Demo练手,理解“遍历-改造-生成”三个环节。

4.3 反调试机制怎么处理

靶场到了进阶难度会加上反调试。最常见的是无限debugger:

setInterval(function() { debugger; }, 100);

页面一打开就不断断住,非常恶心。处理办法有两类:

  • 绕过式:在Sources面板里把这个位置“Never pause here”,或者直接把这段脚本当做垃圾代码删除掉再执行。
  • Hook式:用油猴脚本在页面加载前把setInterval替换成空白函数,让它压根不起作用。

还有一种反调试是检测开发者工具是否打开。检测方式包括判断窗口尺寸差异、console.log的输出时间差、debugger重复执行的频率等。这类检测通常在setInterval里轮询,绕过思路大同小异:找到检测函数,在控制台手动覆盖为function(){ return false; }。

需要注意一个原则:绕过反调试的目的不是“破解系统”,而是“解除它对你调试训练的干扰”。靶场设计者加了反调试,本身就是希望你能在这种条件下继续完成分析目标,所以这个练习很有价值。

4.4 平台层HTTP封装带来的干扰

真实系统里JS往往不会裸写XMLHttpRequest,而是封装在axios、jQuery、或私有请求库里面。这会带来一个麻烦:你在请求库里打断点,看到的都是通用代码,而不是某个业务字段的赋值逻辑。

我常用的定位策略是:在Network面板里找到那个请求,右键选择“Copy as fetch”,拿到完整请求后到Console里用fetch重放。如果重放成功,说明加密参数已经被服务器接受,此时可以在Console环境的fetch调用栈附近找有没有遗漏的逻辑;如果重放失败,那就代表还有别的参数或Header需要补全,继续回Sources研究。

另一个更直接的思路:与其顺着代码执行流找加密,不如从“存储”角度判断。很多系统把请求参数中央管理,尤其是RSA公钥、加密后的最终结果,常会被暂存在某个变量里。通过控制台Object.keys(window)扫全局变量,也确实能发现一些靶场故意埋的“彩蛋”。

5. 常见问题与排查技巧实录

打靶打得多了,会遇到一些重复出现的问题。我整理成一张速查表,每个问题都能对应到实际场景。

现象原因排查/解决
本地Node算出结果与页面不一致加密参数细节差异(模式/填充/IV/key编码)回页面断点核对每个入参,特别是key是否转WordArray
找不到关键词搜索结果代码被字符串编码,明文字段名不直接暴露改搜特征URL、算法库名、请求Header里的公共字符串
一开DevTools就不断断住无限debugger反调试Hook setInterval/覆盖debugger,或者删除该段代码
Node运行AES报“wrong key length”key长度与算法不匹配检查key字节数,aes-128对应16字节,aes-256对应32字节
在全局搜索里搜索不到请求参数参数由后端下发的配置动态拼装断点Network模块里的请求构建函数,观察封装过程
页面执行JS依赖DOMNode环境缺window/document逐个补全局变量,或用jsdom做轻量模拟
混淆代码里找不到主逻辑函数堆在数组里动态调用先别静态读,Hook数组取值函数,动态打印调用顺序
签名总是差一位大小写或编码不一致统一比较hex/uuid/base64格式,加上toUpperCase()测试

5.1 Hook脚本的一个实战模板

很多情况下,与其一步步断点深度调试,不如写一段Hook脚本,在加密函数外层打印参数。油猴脚本里,只需要这样:

(function() { const originalEncrypt = window.CryptoJS.AES.encrypt; window.CryptoJS.AES.encrypt = function() { console.log('[Hook] args:', arguments); const result = originalEncrypt.apply(this, arguments); console.log('[Hook] result:', result.toString()); return result; }; })();

这段代码的原理是:先把原函数保存到一个变量里,再用新函数覆盖同名属性,新函数内部打印参数、调用原函数、打印结果、最后还是把原函数的结果返回去。这样页面业务逻辑不知道被偷换了,但你已经拿到了加密过程的输入输出。

这个模板可以泛化,比如HookJSON.stringify,看请求对象被序列化前的完整结构;Hook某自定义函数,确认大数组的取数顺序。

5.2 “无限debugger”的多种绕过形式

不要以为一个Hook模板能解决所有反调试。有的靶场把debugger写在多个异步分支里,有的在setInterval里加上时间差检测,有的干脆在函数内部用迭代算法生成递延debugger。这些情况我都遇到过,实际经验是:

  • 先用全局搜索定位 “debugger” 字符串或类似关键词。
  • 如果搜不到,就在Sources面板里全局搜索“MSL”这类疑似字节码特征。
  • 然后根据情况选择“Never pause here”或Hook底层函数,不必追求一次性搞定。

有些硬核靶场会检测“源代码面板是否被打开”,利用的方法是计算页面窗口内外层的高度差。绕过思路是把打开工具的行为变成“不受检测”。这属于进阶内容,初学阶段能处理前三种情况已经很不错了。

5.3 环境补全时别乱填充

在Node里复现加密函数时,密钥、盐值、平台标识、随机数,这几类值补全错误会导致结果对不上。我最常犯的错误是:把一段代码从页面拷贝下来后,直接在Node里require,没考虑它内部用到了某个局部变量,这个变量来自页面全局。解决方法不是硬补一个假值,而是回到页面断点,在Console里Object.keys(globalThis)找到真实变量名和值,再在Node里原样设置。

注意:不要用简化思维去猜测“应该等于什么”,一定要以断点观察到的实际值为准。加密算法讲究的就是字节级别的对齐,差一个空格都可能算不出原结果。

6. 最后再聊两个实操心得

第一,不管靶场题目多难,始终记住“请求参数是从数据到密文的变换”。遇到复杂逻辑从入口退一步,用Move小步走、Console验证每个中间值的方式推进,效率远高于盯着混淆代码硬想。

第二,打靶的目的不是“背下某个网站的算法”,而是建立一套“观察-假设-验证”的循环。我见过太多人卡在第一步搜索不到关键词,就开始怀疑人生。实际上只要换个思路,从函数调用栈、请求封装层、全局变量、存储层四个入口排查,大部分加密参数都能在15分钟内定位到源头。

对我来说,JS逆向最有魅力的地方,不是解密成功那一刻的爽感,而是分析过程中被迫去理解浏览器、理解加密库、理解HTTP、理解前端工程化——这些知识点单拎出来都很“基础”,但当你带着逆向问题回头去补这些基础时,知识会异常牢固。靶场练习的价值也正在于此:它像一个极其严格的教练,逼你在实战中把一个又一个模糊的概念变成确定的技术动作。

有机会的话,建议你从手边最简单的练习环境开始,把一个带加密参数的静态页面完整走一遍抓包、断点、复现的三步流程。完成一次之后,再去碰带混淆、带反调试的高阶靶场,成就感会完全不同。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询