尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JS逆向实战:破解私募排行加密接口的完整流程

JS逆向实战:破解私募排行加密接口的完整流程 我先把结论放在前面别看“私募pp网-私募排行js逆向”这标题像个选股工具测评其实它是个纯正的前端加密对抗实战项目。核心不是私募数据本身而是“排行榜页面的数据是加密接口吐出来的怎么把这个加密过程还原出来”。完完整整走完一遍你至少能掌握XHR断点、调用栈回溯、混淆JS还原、Node补环境这几板斧往后遇到更复杂的加密接口思路会清晰很多。这篇博文就按我当时做这个项目的顺序来写从最开始的抓包定位到断点找入口再到还原加密函数最后用Python把整条链路串起来。中间那些踩过的坑、误判过的方向我也会一并写出来免得你重复交学费。1. 逆向目标与前期观察1.1 先说清楚要抓什么这个项目的目标很明确某家做私募产品展示的垂直网站里面有个“私募排行”栏目按收益、回撤等指标对产品排序。页面本身能看到数据但直接请求接口返回的是一团乱码加密字段一个叠一个没法直接用于分析。我习惯把这类逆向项目分成三层第一层是“看得见的”浏览器里渲染出来的排行榜表格字段名、排序规则、前端展示逻辑第二层是“藏在请求里的”Network面板里那个XHR请求请求头、query参数、request payload第三层是“真正干活的”JS代码里生成签名、加密参数的逻辑一般隐藏在压缩混淆的chunk文件里。这次的核心矛盾出在接口的两个请求参数上一个类似sign的签名值以及整个响应体被做了编码处理。不解决这两个点哪怕你把Cookie、User-Agent全部带齐服务器照样返回非200或者一团乱码。1.2 打开开发者工具快速定位请求推荐直接用Chrome DevTools不推荐一开始就上抓包工具。原因很简单我们面对的是HTTPS站点Fiddler或Charles即使配好证书有时候也会被前端指纹检测干扰而DevTools是浏览器自带的天然在“可信环境”里干扰最少。实际操作步骤进入排行榜页面按F12打开DevTools切到Network面板勾选Preserve log防止页面跳转刷掉请求记录手动刷新页面等列表数据加载完毕在Network面板过滤XHR找返回内容带产品净值或收益率的请求。这里有个小技巧不要急着看请求头先看请求入口。在Network面板的请求列表里把鼠标移到某个可疑请求上会显示Initiator链。我们直接点进去能看到是页面里的哪个JS文件发起的请求、对应的调用栈是什么。这一下就能省下很多全局搜索的时间。我这次遇到的请求URL大概是/api/rank/list?page1size20sortprofitasc0但后面跟着一长串sign...ts...nonce...之类的参数。一眼就能判断这是典型的“动态签名”模式。2. 定位加密参数从网络请求到JS调用栈2.1 遇到底层加密参数怎么办请求头里的参数不是我们要找的核心。真正要关心的是query参数里那几个看起来随机的东西。比如sign、ts、nonce它们往往是一个组合。判断优先级的方式很简单时间戳类参数比如ts1691234567890一眼能认出来随机串类参数比如nonce7f3a912b...大概率是UUID或随机字节的十六进制表示签名类参数比如signda3c8f...最头疼通常是前面多个参数拼起来之后做摘要算法。这次三个都占全了。我当时的策略是先不管签名算法是什么先去全局搜索“sign”这个字符串。在Sources面板里按CtrlShiftF全局搜索搜出来的文件可能很多怎么缩小范围排除所有.min.js里的source map注释行优先看主入口文件webpack的runtime、app.bundle.js之类搜的时候带上关键字上下文比如sign:、sign 、setRequestHeader(sign。全局搜索的好处是能直接找到赋值语句的源头。坏处是压缩混淆后的代码里你会搜出来上百个匹配项挨个看会疯。2.2 关键词搜索定位加密函数我当时没有死磕全局搜索我用了更直接的XHR断点方式。操作流程切到Sources面板右侧Breakpoints区域打开XHR/fetch Breakpoints点击加号输入URL中的关键字片段比如rank/list刷新页面脚本会在发起XHR请求前自动断住这时候右侧Call Stack就是金子一层层点进去找自己写的业务代码。断住以后我通常先看Scope区域找包含所有参数的那个对象。比如config、requestData、body这个对象里通常能看见明文参数和那个sign值。然后沿着Call Stack往下走找最近的那个非第三方库函数。我记得当时断住的位置是在一个叫ajax的封装函数内部参数已经拼好了就差被加密。往上一层看到调用方是类似fetchRankList的方法里面有一行代码非常关键大致是这样var params buildParams({ page: page, size: size, sort: sort, asc: asc, ts: Date.now(), nonce: generateNonce() }); params.sign signParams(params);看到这里基本可以确定加密入口就是signParams。接下来只需要搞清楚两件事generateNonce生成的是什么signParams内部用了什么算法摘要的输入格式是什么。3. JavaScript逆向实战还原加密逻辑3.1 分析混淆代码时别慌先做格式化定位到signParams函数以后我先把那段JS源码完整复制出来放到本地一个临时文件里用Prettier格式化了一遍。这一步强烈建议做压缩后的代码经常是一整行人眼看不出嵌套关系格式化之后才看得清函数边界和变量作用域。格式化完这个函数的结构基本清楚了function signParams(params) { var keys Object.keys(params).sort(); var str ; for (var i 0; i keys.length; i) { var key keys[i]; var value params[key]; if (value ! null value ! undefined) { str key value ; } } str str.slice(0, -1); str secret_keyX9sK2wR7; return md5(str); }这里有几个典型特征先对参数名做字典序排序这是签名算法的常见套路目的是保证服务端拿到参数后按同样规则拼串能对齐把参数拼成keyvaluekeyvalue的格式结尾再拼一个固定密钥最终做MD5摘要。理论上到这一步签名算法就明确了。但我不能光看这个函数就完事因为页面上跑的JS是压缩混淆过的真实场景可能还嵌套了rsa、aes、base64等操作。这次比较走运只用了MD5。但我还是要提醒一句看到MD5别激动很多网站现在会用MD5做第一层包装后面再套一层AES或者用自定义的字符替换规则。你需要继续确认md5这个函数是不是真的就是标准MD5。最简单的验证方法是找个在线MD5工具用同样输入算一遍对比结果。如果能对上说明没有摘歪。3.2 补环境绕过检测光知道算法还不够因为你不能保证signParams内部没有依赖window、document、navigator这些浏览器环境对象。比如有些加密函数内部会读取navigator.userAgent、document.cookie作为盐值。这时候直接扔进Node.js里跑肯定会报错。我的处理办法是“补环境”。先跑一遍看报错node sign_logic.js如果报window is not defined就在文件头部补一个最小化的window对象如果报navigator is not defined同样补一个。这次运气不错signParams是纯函数只依赖参数和固定的secret_key所以不用补环境就能直接跑。但为了演示通用流程我建议你建立一套“最小补环境模板”哪怕这次用不上下次可能就用上了。模板大致长这样global.window global.window || {}; global.navigator global.navigator || { userAgent: Mozilla/5.0 ... Chrome/120.0.0.0 }; global.document global.document || { cookie: , getElementById: function() { return null; }, createElement: function() { return { getContext: function(){ return null; } }; } };补环境的核心原则是能不补就不补补了就不要慌。只补报错涉及到的属性千万别想着把整个浏览器环境模拟出来那是浪费时间。3.3 用Python调用还原后的加密函数签名算法确认没问题之后我把签名逻辑用Python重写了一遍。有人说“直接用Node子进程调用不就行了”但这会引入额外依赖而且在高并发请求下进程开销不小。对于这种简单MD5签名用Python重写是最高效的方案。Python实现代码import time import uuid import hashlib def generate_nonce(): return uuid.uuid4().hex def generate_sign(params): sorted_keys sorted(params.keys()) raw_string for key in sorted_keys: if params[key] is not None and params[key] ! : raw_string f{key}{params[key]} raw_string raw_string[:-1] raw_string secret_keyX9sK2wR7 return hashlib.md5(raw_string.encode(utf-8)).hexdigest() def build_params(page1, size20, sortprofit, asc0): params { page: page, size: size, sort: sort, asc: asc, ts: int(time.time() * 1000), nonce: generate_nonce() } params[sign] generate_sign(params) return params if __name__ __main__: print(build_params())这里我强调几个容易踩坑的细节ts参数我用了毫秒级时间戳服务端判断过期时间通常也是按毫秒算的如果用了秒级很容易返回“签名过期”nonce直接用UUID的十六进制字符串去掉连字符格式正好是32位和网站自带的策略一致排序参数asc别写成布尔值。JS里true会被转成字符串true但Python里True会被转成True首字母大写签名对不上。当年我第一次跑的时候就栽在asc这个参数上。JS那边传的是数字0和1我在Python里写成False和True结果签名一直验证失败排查了好久才发现是大小写问题。4. 常见问题与排查技巧4.1 浏览器环境与Node环境差异这个坑在多数字签名场景里都会出现。浏览器里的JS自带window、document等对象而Node.js里没有。如果你把一段依赖navigator.userAgent的加密函数直接搬到Node里它就会报错。我遇到的情况是加密函数本身是纯函数不依赖任何浏览器对象但外层有个模块初始化代码一加载就试图访问window.localStorage。遇到这种最简单的办法是把那行初始化代码注释掉或者提前声明一个伪localStorage对象。global.localStorage { getItem: function() { return null; }, setItem: function() {} };不要觉得这是作弊逆向工程里“够用就行”是常态。只要能跑出和浏览器一致的结果补丁越少越好。4.2 动态加载与Hook技巧有些网站会把加密逻辑拆成动态加载的组件主入口里只有一个import()然后异步拉取真正的加密模块。这种情况用XHR断点可能捕捉不到直接入口因为请求发起者在异步回调里。我的处理方式是Hook这里推荐两个方向Hookwindow.fetch和XMLHttpRequest.prototype.open把所有请求公共信息打印出来快速定位发起时间点HookString.prototype的关键方法比如replace、slice因为很多加密函数会调用这些方法处理参数字符串通过堆栈就能抓到调用链。但Hook也有风险如果网站做了原型链检测你贸然改原型可能会导致页面直接崩溃。我一般只在调试阶段Hook确认逻辑后立刻移除。还有一点要注意现在的网站非常喜欢用webpackJsonp机制。你在全局搜索某个关键字找不到时可以尝试搜索webpackChunk相关变量。加密逻辑往往被单独打进一个chunk文件里只有触发某个行为时才加载。解决办法是手动触发对应行为比如切页、排序、点击查询然后再去Network里找chunk文件。4.3 请求频率与合规提醒这部分不属于纯技术但我必须写。我见过不少新手拿到签名算法之后直接写个循环每秒请求几十次结果IP被风控封禁。正确的姿势是控制请求频率加个time.sleep(random.uniform(1, 2))之类的限速尽量复用Session和Cookie不要频繁新建连接对目标域名做robots检查确认哪些路径允许爬取下载下来的数据只用于个人学习研究不要用于商业用途更不要二次公开传播。另外提一句逆向JS本身是中性技术但不同站点的服务条款差异很大。我在做这个项目前会查看该站是否有反爬声明。如果明确禁止了数据爬取我会缩小抓取范围只做少量验证测试不做全量数据采集。4.4 调试过程中的三个救命技巧最后分享几个小技巧都是本项目中实际帮上大忙的技巧一巧用Console来验证加密结果在DevTools里直接执行signParams({page:1})会得到当前页面下的签名值。然后你在Python里跑同一条逻辑对比两个结果。不一致的话优先检查排序规则和类型转换。技巧二把response的编码逻辑也还原出来这次响应体不是单纯的JSON外面套了一层自定义编码。服务端把数据做了某种变换前端拿到后再还原。针对这类情况别急着从零看编码算法。先去全局搜JSON.parse看它之前有没有经过其他方法加工。我在这个项目里就是用“搜JSON.parse”的办法找到了解码函数。技巧三善用Source面板的Overrides有些JS文件经过格式化和修改后可以通过Overrides保存到本地。刷新页面时浏览器会用本地文件替换线上文件。这个功能很适合在验证签名逻辑时用你可以在JS里手动console.log打印中间变量然后观察真实请求。具体操作不复杂Sources面板左侧能找到Overrides标签选一个本地文件夹点击允许授权然后在文件上右键选择Save for overrides之后重新加载页面即可。调试完记得删除本地临时文件否则会影响后续正常浏览。写在最后的一点心得这个项目从定位到跑通第一个接口我大概花了两个晚上。第一晚全耗在断点和全局搜索上始终找不到加密入口后来切换思路用了XHR断点才从调用栈里一下子揪出signParams。第二晚就是格式化代码、还原逻辑、用Python重写顺手把响应解码也一并处理了。如果让我重新做一遍我会先花五分钟看请求调用栈而不是一上来就全局搜索。调用栈给你的是“动态视角”全局搜索是“静态视角”对于动态生成的参数前者高效得多。另外像“私募排行”这种偏垂直领域的数据站前端加密通常不会上特别重的方案但只要思路通了任何加密都只是时间问题。过程中真正值钱的不是那个签名算法本身而是你练出来的调试直觉。最后提醒一句做逆向要守住底线别把爬下来的数据当成自己开发的宝贝去售卖也别写什么“绕过反爬”的教程去教唆别人批量爬取人家核心数据。学技术是为了遇到问题能解决不是为了制造问题。
返回列表