ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JS逆向实战:从抓包到签名算法还原的完整攻防指南

JS逆向实战:从抓包到签名算法还原的完整攻防指南 1. 项目概述与分析1.1 这个逆向任务的本质是什么这段时间接了个活儿需要抓取某个私募排行网站的数据做投研分析线下没法直接拿到结构化数据页面又看不到完整的接口返回一抓包发现请求头和请求参数里全是加密字段。就这么入了JS逆向的坑。先说清楚这个任务的本质所谓JS逆向不是“破解网站让你为所欲为”而是把前端JavaScript里做参数加密和签名校验的那套逻辑用我们能控制的方式还原出来从而让服务端认为我们的请求是合法的、来自真实浏览器的最终拿到我们需要的JSON数据。私募排行网站的业务逻辑并不复杂核心难点基本集中在以下几层。接口请求头或请求体里存在需要动态计算的参数比如sign、token、ts、nonce之类。这些参数是通过一段或几段JavaScript代码在浏览器环境里计算出来的代码常常经过压缩、混淆甚至套了控制流平坦化。请求前可能还要完成某个环境指纹的采集比如canvas指纹、webdriver检测、cookie生成逻辑等。不完整的环境模拟会导致参数算对了但服务端依然识别你是脚本。本次要处理的pp网私募排行页就是典型的“反爬参数前置校验”模式。排行页的列表接口本身不复杂真正难缠的是启动时那段自动执行的加密逻辑它会动态生成一个请求签名签名依赖当前时间戳、固定的密钥片段、以及一个由浏览器环境参与计算的因子。如果你直接忽略它接口返回的永远是“签名校验失败”或者直接403。1.2 适合谁看看完了能获得什么这个项目案例适合下面几类人刚接触JS逆向想找个完整案例练手的已经在做爬虫相关工作但主要靠Selenium或Playwright硬扛想让效率和稳定性上一个台阶的还有对前端加密、混淆对抗感兴趣的开发者。读完这篇博文你能掌握的不仅仅是“把某个网站的签名参数逆向出来”这一个点而是一整套通用方法论包括如何通过抓包确定要逆的参数缩小排查范围。如何在Chrome DevTools里精准地下断点、看调用栈、分析作用域。如何把混淆代码还原成可读逻辑找出签名算法的本质。Hook与补环境的基本套路让加密函数脱离浏览器也能运行。常见反调试手段和绕过思路。这些方法论换一个目标网站依然适用这也是我认为比具体代码更值钱的地方。2. 核心思路与准备工作2.1 抓包定位先把目标接口和加密参数圈出来做JS逆向最忌讳的就是拿到网站就从头开始读源码那是大海捞针。正确做法永远是先抓包用数据流反向定位。打开Chrome DevTools的Network面板勾选Preserve log然后刷新私募排行页面一排请求刷刷刷地进来。我们要找的是那个返回排行数据的XHR请求通常它的名字里会带有list、rank、score这类字样响应体是JSON格式。把它的Headers仔仔细细看一遍重点关注几个位置。Query String ParametersURL问号后面的键值对这里最容易出现签名参数。Request Headers自定义Header里经常藏着token、sign、nonce。Request PayloadPOST请求的请求体同样可能有加密字段。我这次遇到的情况是排行接口的URL里带了sign、ts、nonce三个参数其中ts是毫秒级时间戳nonce是一串32位的十六进制字符串sign是一长串40位的十六进制字符串。一眼看去ts是时间戳无疑nonce和sign都需要动态计算。先不急着看代码多刷新几次页面对比这几趟请求里参数的变化规律。你会发现nonce每次都变sign也每次都变但它们和ts之间可能存在某种关联——比如sign可能就是对某些固定参数加上ts和nonce做了一次摘要运算。这个“多对比几次”的步骤特别重要它能帮你快速判断哪些参数是随机生成的、哪些是时间相关的、哪些是基于其他参数派生的后续逆向的时候可以少走很多弯路。2.2 工具选型Chrome DevTools为主抓包工具为辅工具方面我的习惯是以Chrome DevTools为主力Fiddler或Charles作为辅助。Chrome DevTools里最常用的是Sources面板。通过“Search”功能可以全局搜索关键词比如你看到接口参数里有sign直接在Sources里搜sign:、sign、sign 几乎是秒定位到加密逻辑所在文件。这个操作在绝大多数场景下都适用因为不管代码怎么混淆参数名最终还是要和接口字段对应上作为纯字符串存在代码里。Fiddler或Charles主要用来做接口的断点修改和请求重放。某些场景下需要把请求拦下来手动改一改参数看看服务端反应这种情况下抓包工具比DevTools灵活得多。另外如果目标网站有Service Worker或者比较激进的请求隔离DevTools的网络面板不一定能完整看到请求细节挂个代理抓包会稳定很多。还需要提一下的是油猴脚本配合Hook。有时候直接在DevTools Console里往window上挂一些Hook函数能极大加速逆向过程。比如想快速定位sign在哪个函数里被赋值可以提前Hook掉JSON.stringify或String.prototype的某些方法不过这属于进阶技巧后面实操部分我再展开。2.3 确定加密算法类型的基本方法拿到一串看起来乱七八糟的密文第一步要判断它是MD5、SHA1、SHA256、AES还是RSA。判断依据主要靠长度和字符集。32位十六进制大概率是MD5也可能是SHA256截断。40位十六进制SHA1或者MD5盐再哈希。64位十六进制SHA256。128位十六进制SHA512或者MD5多次迭代。Base64字符串且长度有规律、结尾经常有AES、DES、RSA等需要进一步看算法模式。密文长度随机、每次请求都变化RSA可能性较大。这次遇到的sign是40位十六进制所以第一反应就是SHA1。之后在代码里搜sha1、SHA1、CryptoJS这些关键词果然找到了相关逻辑。这个判断方法虽然朴素但在实战里非常高效能帮你省掉大量盲目分析的时间。3. 实操过程与核心环节实现3.1 定位sign参数的猴子补丁式搜索打开DevTools Sources面板用CtrlShiftF全局搜索输入sign。搜索结果会铺天盖地地返回一堆文件里出现的“sign”字样。别慌我们先看接口URL里参数名出现的顺序和上下文。因为URL里参数是signxxxxx所以往代码里搜sign大概率找到的是对象键名。把搜索结果的几个关键文件打开肉眼扫一眼代码。如果文件是压缩过的一大坨代码挤在一行里先点击左下角的{}按钮做一次格式化。格式化之后再搜sign出现的上下文会清晰很多。此时可能会看到类似这样的代码var t { ts: Date.now(), nonce: generateNonce(32), sign: calcSign(params, key) };看到calcSign、generateNonce这种可读性极高的命名说明网站的前端工程师没有做深度混淆这对我们来说就是开卷考试。但也有可能遇到a.b(c.d)这种压缩命名这时就需要靠断点来辅助定位。3.2 断点调试与调用栈回溯用DevTools在搜索到的赋值语句处打一个断点然后刷新页面。如果断点命中说明这一行代码就是生成请求参数的地方。此时观察右侧Scope面板所有局部变量、闭包变量、全局变量一览无余sign的当前值、生成它时用到的params和key都能直接看到。接下来要做的是“跳进函数内部”。比如calcSign(params, key)这个函数鼠标点击DevTools里的Step into按钮进入函数体一行一行地看它是怎么把params和key变成最终的sign的。这个过程就是“调用栈回溯”。我这次遇到的情况就比较典型calcSign内部其实就三行核心代码先把params对象里的键值对排序拼接成一个字符串。把拼接后的字符串和key拼在一起。然后调用CryptoJS.SHA1做摘要再转成十六进制字符串。但这三行代码外面套了一层try-catchcatch块里有一段干扰代码专门生成一个假签名来误导调试者。遇到这种故意埋坑的情况果断跳出来看真正的逻辑流就行。3.3 定位密钥与算法还原密钥是怎么藏的这个网站的做法比较典型密钥片段分散在几个不同的常量里通过一个字符串拼接函数组合起来。比如var key1 abc; var key2 def; var realKey key1 key2 ghi;这种方式看起来简陋但实际对抗效果不差因为如果你只搜某个固定字符串不一定能直接搜到完整密钥。需要先通过调用栈回溯到密钥组装的位置才能一次性还原。再看签名算法代码里调用的是CryptoJS.SHA1但CryptoJS本身支持多种算法为什么这里选SHA1而不是MD5呢我分析可能的原因有两个。一是SHA1输出40位比MD5的32位长从签名强度上看稍微好那么一丢丢二是这个项目的后端可能是Java或老版本PHP写的这两个技术栈对SHA1的兼容性非常高老代码里用SHA1做签名的传统保持到了现在。还原出来的签名算法伪代码如下function calcSign(params, ts, nonce) { var keys Object.keys(params).sort(); var str ; keys.forEach(function(key) { str key params[key] ; }); str ts ts nonce nonce; return sha1(str secretKey); }核心就是“参数排序拼接 时间戳 随机数 固定密钥做SHA1摘要”。这种签名设计在行业内非常普遍它防的是参数被篡改而不是防爬虫。但对我们来说只要还原出这个算法请求就能轻松打过去了。3.4 动态时间和随机数的生成策略再来看看ts和nonce。ts直接用Date.now()就能得到这个没什么好说的。nonce的生成方式值得留意它是通过一个叫generateNonce(32)的函数生成的这个函数内部其实是用了一个伪随机数生成器每次取随机字节再转为十六进制。常见的实现是这样的function generateNonce(len) { var chars 0123456789abcdef; var result ; for (var i 0; i len; i) { result chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }如果我们按照这个逻辑去模拟用Math.random()生成32位随机十六进制字符串就够了。但要注意的是某些网站的nonce校验会要求全局唯一或者一段时间内唯一用随机数一般没问题但如果遇到“同一nonce不能出现两次”的严格校验最好仿照原站使用带时间戳种子的随机算法。这里因为是常见场景用普通随机数就行。3.5 Python端算法移植与完整请求模拟算法还原后接下来就是把它移植到Python里。我习惯用requests库配合哈希库和随机数模块实现完整的签名和请求逻辑。import requests import time import hashlib import random def generate_nonce(length32): chars 0123456789abcdef return .join(random.choice(chars) for _ in range(length)) def calc_sign(params, ts, nonce): sorted_keys sorted(params.keys()) raw for key in sorted_keys: raw f{key}{params[key]} raw fts{ts}nonce{nonce} secret_key your_secret_key_placeholder raw secret_key return hashlib.sha1(raw.encode(utf-8)).hexdigest() def fetch_rank_data(): ts int(time.time() * 1000) nonce generate_nonce() params {page: 1, size: 20} sign calc_sign(params, ts, nonce) url https://example.com/api/rank headers { User-Agent: Mozilla/5.0 ..., Referer: https://example.com/rank } params_ {page: 1, size: 20, ts: ts, nonce: nonce, sign: sign} resp requests.get(url, paramsparams_, headersheaders, timeout10) return resp.json()这段代码把前面的算法还原全部落到了实处。第5-8行生成了一个32位随机nonce第10-16行严格按照排序拼接拼接时间和随机数拼接密钥的规则做SHA1摘要最终拼出完整的请求参数。跑一遍请求能正常拿到JSON数据的话逆向核心工作就算完成了。4. 常见问题与排查技巧实录4.1 无限debugger反调试怎么绕过几乎每个做JS逆向的都会遇到无限debugger。这个网站的排行页里也埋了这种反调试代码表现形式是在代码里写了一个setInterval或者循环调用debugger语句只要打开DevTools就会被反复断住烦得不行。我试过几种方案最有效的是把debugger关键字在代码层面上屏蔽掉。Chrome DevTools的Sources面板里右键点击代码行选择“Never pause here”可以禁用当前行断点。但无限debugger一般不是写在某一行的而是写在循环或setInterval回调里每一轮都会触发所以需要一点点地把所有相关行都禁用。另一个更彻底的办法是在DevTools里右键点击debugger语句所在行选择“Add script to ignore list”这样脚本运行时凡是命中该脚本的暂停都会被忽略。如果反调试代码是动态注入的每次刷新位置都变那可以考虑直接改代码把包含debugger的代码片段从文件里替换掉。用Local Overrides功能可以覆盖网络请求中的JavaScript文件把文件本地修改后再加载。具体做法是在Sources面板里打开目标JavaScript文件右键选择“Overrides”里的“Save for overrides”然后本地编辑文件把debugger删除保存后刷新页面就会加载修改后的版本。4.2 加密参数在闭包内部直接搜不到怎么办有些网站不把参数逻辑写在全局作用域而是包在IIFE或模块闭包里直接全局搜索sign搜不到。这时候就要换一个思路从网络请求触发点反查。先给XMLHttpRequest.prototype.open和send方法挂Hook在Console里执行(function() { var originalOpen XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open function(method, url) { if (url.indexOf(/api/rank) ! -1) { console.trace(XHR opened:, method, url); } return originalOpen.apply(this, arguments); }; })();这样当页面发起请求时Console会打印完整的调用堆栈顺着堆栈一层层点进去就能找到发送请求的函数再往上追溯加密参数的生成位置。这个方法对绝大多数前端框架都有效因为Ajax请求最终都要走XHR或fetch挂Hook是通用解法。4.3 参数拼接方式不对导致签名校验失败就算还原的算法是对的也经常会遇到签名校验失败的提示。这种情况十有八九是拼接顺序或细节不一致导致的。这里分享几个高频的踩坑点。参数排序规则不一定按字母升序也可能是按照参数名ASCII码降序、或者按照参数在URL中出现的顺序。需要仔细看原始代码。某些值为空的参数可能被过滤掉不参与签名。数组或嵌套对象序列化时JSON.stringify与手工拼接结果差异巨大。布尔值true和字符串true在拼接时可能不同。时间戳可能使用秒级而非毫秒级可能与nonce或其他字段组合后再参与签名。遇到签名校验失败建议在还原的代码里加几行log把参与签名的原始字符串打印出来。然后在浏览器Console里同样打印一份两者逐字符对比找到差异后修正。4.4 本地执行计算签名时报错“window is not defined”这一条主要是想强调“浏览器环境”在加密逻辑中的重要性。很多加密函数虽然核心算法简单但会在计算过程中引用window、navigator、document等浏览器全局对象。比如用window.screen.width参与指纹计算或者用navigator.userAgent参与字符串拼接。在Python里复现时直接用hashlib计算会报错或算出完全不同的结果因为环境变量缺失。解决方案有两种。一是把这些环境变量作为配置参数传给Python的签名函数从浏览器Network面板里复制对应的值来填充。二是如果环境变量会动态变化就需要用Js2Py、PyExecJS或Node.js子进程来在Python里直接执行还原后的JavaScript代码。我的个人建议是能扣核心算法就扣核心算法最好别直接用整个JS文件跑补环境补到怀疑人生是常有的事。5. 隐藏难点反爬指纹与动态Cookie5.1 指纹验证是怎么影响请求的说完了签名再来讲个更容易被忽略的难点。很多网站的排行接口不仅有签名参数还要求请求头带上一个由前端动态生成的Cookie值这个Cookie的生成和浏览器指纹强相关。Python端如果用裸的requests请求即使签名算对了服务端也有可能在Cookie校验环节就把你拦下来。本次案例中pp网的排行接口就存在这样的校验。它在请求前执行了一段脚本读取canvas指纹、WebGL渲染器信息、屏幕分辨率、语言环境甚至鼠标移动轨迹的综合特征生成一个唯一标识写进Cookie。服务端拿到Cookie后会校验这个标识是否存在且合理。如果你直接用requests访问缺少这个Cookie服务端就会判定为可疑请求。对于这种情况从纯requests层面去模拟是极其痛苦的因为canvas渲染结果在不同设备上不一样鼠标轨迹更是无法伪造。明智的做法是缩短攻击面把可控的指纹参数固定下来比如锁定User-Agent、屏幕分辨率、语言等然后用Node.js或Selenium的CDP协议Chrome DevTools Protocol获取真实生成的Cookie再交给requests使用。5.2 用CDP获取真实Cookie的落地方法用Selenium或Puppeteer加载页面启动时会自动执行页面里的所有JS包括生成指纹Cookie的那部分。等Cookie生成完之后从浏览器上下文中把Cookie取出来再交给requests或Scrapy去请求接口。具体可以用Selenium来操作from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headless) options.add_argument(--disable-blink-featuresAutomationControlled) driver webdriver.Chrome(optionsoptions) driver.get(https://example.com/rank) cookies driver.get_cookies() cookie_str ; .join(f{c[name]}{c[value]} for c in cookies) print(cookie_str) driver.quit()拿到这串Cookie后后续的requests请求直接把它塞进Headers里。这种方式的好处是指纹是真实浏览器环境生成的过了服务端的环境校验而数据获取仍然走高效的requests不用在Selenium里做解析和翻页速度优势依然保留。5.3 为什么有时候用浏览器直接访问正常但requests就异常很多人在这一步会陷入一个误区浏览器里明明能看到数据requests里却拿到空结果或验证码。核心原因是前端签名只是第一层防护后端往往还会校验请求头里的User-Agent、Accept、Referer、Origin等字段是否真实一致以及Cookie是否有效。如果这些字段和浏览器环境不一致即便签名算法完全正确请求依然可能被判为异常。我的做法是在DevTools Network面板里找到一个成功的请求把它的Headers整体复制出来和requests代码里的Headers逐项比对。常见差异包括少带了Accept-Language、Accept-Encoding、Upgrade-Insecure-Requests等。把这些缺失的字段补上成功率会明显提升。6. 进阶思考对抗升级与应对思路6.1 加密强度升级的常见方向做完这个案例之后我不禁想聊聊这个领域的趋势。现在的大型网站已经很少用单一静态密钥摘要算法来做签名了很多都在向“动态密钥非对称加密行为验证”的方向演进。所谓动态密钥就是每次会话或者每次请求都重新生成一把密钥密钥本身也通过非对称加密传输。这种情况下即使你逆向出了某一次的签名算法也无法长期复用因为密钥已经变了。对抗这种机制通常需要在浏览器环境里动态执行JS获取签名或者用RPC调用浏览器内部函数。所谓行为验证就是引入滑块、点选、无感验证等判断操作者到底是人还是程序。这一层对传统静态爬虫的打击是毁灭性的因为行为数据很难模拟。绝大多数情况下行业内的做法是引入浏览器自动化或真机设备池来绕过。6.2 逆向代码的可持续维护策略还有一点想单独拎出来说就是“逆向代码的可持续维护”。很多初学者逆向成功一个网站后就把代码扔进生产环境里一直跑。但网站前端工程师不可能坐视不理他们会定期更新加密逻辑可能是一次大版本升级直接改掉算法也可能是做一次混淆升级导致参数位置变化。等到你某天发现数据抓不到了再回头排查往往已经过去了很久损失已经造成。我自己的习惯是代码里加入签名算法版本检测和告警。每次请求观察sign值长度、接口返回的错误码有没有变化一旦出现“签名校验失败”或者接口结构变更日志系统第一时间告警。同时定期用真实的浏览器环境跑一遍流程确保逆向结果与新版本页面没有偏差。这套机制能帮你在网站更新后最短时间内感知到问题从而快速跟进新的逆向工作。7. 踩坑心得与写在最后的实用建议最后分享几个这次项目里踩过的坑以及沉淀下来的通用方法论。第一个坑是“过度关注混淆代码本身”。初学的时候容易陷进混淆代码的海洋里出不来一个函数一个函数地啃效率极低。后来发现与其硬啃混淆代码不如多抓几遍包、多下几个断点、多对比几次参数变化。数据流是会说话的只要顺着数据流走再深的混淆也藏不住核心逻辑。第二个坑是“低估了环境校验的复杂度”。签名算法只占整个反爬体系的一部分很多时候真正卡住你的是指纹校验、Cookie合法性、请求头不一致这类细节。拿到接口数据前至少要在浏览器里成功请求过一次然后把请求头和Cookie整体搬到代码里先保证能通再去优化动态获取Cookie的策略。第三个坑是“忘记合规与授权”。这里必须多说一句。JS逆向属于技术对抗可以做、可以学、可以用于授权范围内的安全测试和数据分析但千万不要把它用在非法获取他人数据、恶意攻击等场景上。你逆向的网站如果是自己的、或已获得合法授权的那没问题如果是别人的一定要仔细阅读服务协议和相关法律法规。尊重网站的服务条款合理控制请求频率不把逆向能力用于破坏和牟利这是一个从业者最基础的底线。再分享一个小技巧全局搜索加密参数时如果同一个关键词命中太多结果试着搜索它出现的“赋值语境”比如搜索sign、sign:、sign\:这类带符号的字符串能瞬间过滤掉大量无关噪声。这个案例本身不大但技术栈相当完整从抓包到断点调试从算法还原到环境模拟再到反调试绕过和Cookie获取几乎覆盖了JS逆向的所有核心环节。方法论沉淀下来之后换任何一个类似的网站你都可以沿着“抓包定位参数、断点回溯逻辑、还原算法、模拟环境、验证请求”这条路走一遍大概率能跑通。
返回列表