)
案例地址QQ音乐热歌榜这个案例我一开始是冲着 vmp 去的结果逆到一半发现页面请求里的 sign 和 ag-1 加密服务端根本没拿它们做判断至少这条链路没有。所以这篇的重点不是“怎么硬啃 vmp”而是“怎么先验证它到底作不作数”。声明文章只做学习研究分享请控制请求频率别给人家添麻烦。一、先把请求抓下来打开热歌榜按 F12切到Network网络面板勾上Fetch/XHR刷新页面。先说个容易误会的事榜单歌曲本身是服务端直出的你右键“查看网页源代码”在里面能直接搜到“我好像在哪见过你”。所以页面首屏的榜单不一定是某条接口给的。但页面加载过程中确实会发一条很可疑的请求POST https://shu6.y.qq.com/cgi-bin/musics.fcg?_1793329363141encodingag-1signzzc3f18dad0tjdqaqp4a9bauamorgx9jx8q45de24c5d Content-Type: text/plain Origin: https://y.qq.com Referer: https://y.qq.com/三个参数先解释一下_时间戳防缓存用的encodingag-1告诉服务端“我这个 body 是加密过的”sign40 多位、zzc开头一看就是签名。它的请求体不是 JSON而是一大串 base64 样的密文长度 1484 个字符。看到这里小白的本能反应肯定是sign 怎么算的密文怎么解我也一样。二、sign 是在哪算出来的在 Sources 里全局搜_getSecuritySign能定位到一个vendor.chunk.xxxx.js。这种打包文件别一行行读直接看调用点ar.data, snew XMLHttpRequest, ... return n.next10, ie.getSecuritySign(r.data); case 10: return un.sent, n.next13, re.encrypt(r.data); case 13: r.datan.sent, r.urlr.url.replace(cgi-bin/musicu.fcg,cgi-bin/musics.fcg), r.urlz({encoding:ag-1,sign:u},r.url)翻译成人话一共四步先把 payload 转成字符串r.dataie.getSecuritySign(r.data)算签名结果存到ure.encrypt(r.data)把 body 加密把 URL 从musicu.fcg换成musics.fcg再拼上encodingag-1signu。我在musics.fcg这个 XHR 请求上下了断点再刷新榜单页触发请求。断住时 Scope 里能看到两个关键值变量真实值r.data明文已截断{comm:{cv:4747474,ct:24,...},req_1:{module:music.musicsearch.HotkeyService,...},req_2:{module:music.globalComment.CommentRead,method:GetNewCommentList,param:{BizType:4,BizId:26,...}},...}usignzzc3f18dad0tjdqaqp4a9bauamorgx9jx8q45de24c5d这里有个很重要的细节这条加密请求的明文里装的是热搜词、评论、白名单、充值状态这些模块并不是榜单歌曲。也就是说我一开始盯上的“加密请求”跟我想爬的榜单其实不是一回事。三、sign 的真身TME WebSec 的 vmp顺着ie往下找会看到这样一段const ne (...).__TmeWebSec_D; const re (...).__TmeWebSec_seccgi, ie (...).__TmeWebSec_sign; delete (...).__TmeWebSec_sign;__TmeWebSec_sign不是普通函数而是一段vmpJS 虚拟机保护一个大字符串加一个while/switch分发器真正的算法被编译成字节码。旧脚本里这段 vmp 大概长这样)()(Xh7YHJgHOBoIAEwUFMgBFMoBogEUzAEU0gEU3AFGFMoBnAEUABQyGBSgARQ2IqIBog... case 82: h[n[p]] String.fromCharCode(n[p]), h[n[p]] h[n[p]][h[n[p]]], ...这种不用硬啃。为了确认它是不是纯函数我又在ie.getSecuritySign(r.data)这一行下了个逐行断点断住后在调用帧里直接调用ie.getSecuritySign(abc)把浏览器结果和本地旧版对比。案例目录里原来的52.js就是把这段 vmp 整段扣下来、补环境跑的老脚本。它能跑出zzc开头的结果但和线上对不上。同一个字符串abc浏览器 zzc369981d08l6noj1uu5ylcp8kabaw6rnxos49c78ba06 本地旧版 zzc7a68a42pouohx0jk5xfki7t91l4tsmcay4a9c12c73 MATCH: False同一份输入结果不一样说明旧版 vmp 和线上版本已经不是同一份代码了。补环境最怕的就是这个你补得再像人家代码一更新结果立刻对不上。四、停一下先验证 sign 到底校不校验这是整篇文章最重要的一步。我没有继续啃 vmp而是问自己服务端真的会检查这个 sign 吗拿刚才抓到的真实请求原样重放三次原 sign把 sign 最后一个字符改掉直接把sign参数删掉。结果原始 sign → 200194896 字节 改一个字符 → 200194896 字节 删掉 sign → 200194896 字节三次长度一模一样。为了排除“只是长度碰巧一样”我又用同一个原始请求连发两次做对照组原始 vs 原始对照组差 55 字节 原始 vs 改 sign 差 51 字节 原始 vs 删 sign 差 56 字节两次一模一样的请求密文也会差 55 个字节应该是ts、traceid这类字段每次都变的缘故。改 sign 差 51删 sign 差 56和对照组几乎一模一样——说明服务端根本没拿 sign 做判断。这里要诚实说明响应是 ag-1 密文我手上旧版的 seccgi 已经解不开所以这一步是用“长度 差异字节数 对照组”来判断的。真正的实锤在下一节。五、明文入口同一套 CGI 的 musicu.fcgQQ 音乐的 CGI 有两个常用入口musics.fcg加密入口页面里的评论、热搜等模块走这里musicu.fcg明文入口页面自己的安全校验模块也在用它。我把断点里看到的同款评论模块BizType4、BizId26原样搬到musicu.fcg不带encoding、不带sign、不带 cookie{ comm: {ct: 24, cv: 0}, req_1: { module: music.globalComment.CommentRead, method: GetNewCommentList, param: { BizType: 4, BizId: 26, LastCommentSeqNo: , PageSize: 25, PageNum: 0, FromCommentId: , WithHot: 1, PicEnable: 1, LastTotal: 0, LastTotalVer: 0 } } }返回直接就是明文 JSON里面是真实评论夜行这个歌单循环了一整晚 夜行这个榜单的歌单曲循环很久了 追求光和影子现在的歌手好菜啊……都是文盲吗可是我看歌手节目有唱的不错的啊同样的思路换成榜单模块musicToplist.ToplistInfoServer.GetDetail也能直接拿明文{code:0,ts:...,req_1:{code:0,data:{data:{topId:26,...,title:热歌榜,period:2026-10-30,totalNum:300,...}}}}到这里就能下结论了这个案例的 sign 和 ag-1 加密都不是必需品。sign 是 TME WebSec 的 vmp 产物服务端在明文入口不校验加密只是musics.fcg这条路的包装换musicu.fcg就能绕开。六、相互证明接口前四 页面前四口说无凭拿页面和接口对一下。页面长这样热歌榜 2026-10-30红框是前四名接口返回1 我好像在哪见过你 | 薛之谦 2 季候风 | 王菲 3 天龙八部之宿敌 | 许嵩 4 我不难过 | 孙燕姿排名、歌名、歌手全部对得上。再加上前面的评论模块也能出真实评论可以确认拿到的不是假数据也不是缓存页就是当前这一刻线上正在用的那份。七、重写脚本案例目录里原来的52.py是execjs deasync 补环境跑 vmp 的版本已经对不上跑不通。我新写了52_明文直连.py核心就三步拼 payload、POST 明文、读 JSON。关键代码API https://u6.y.qq.com/cgi-bin/musicu.fcg payload { comm: {ct: 24, cv: 0}, req_1: { module: musicToplist.ToplistInfoServer, method: GetDetail, param: {topId: 26, offset: 0, num: 20}, }, } resp requests.post( API, params{_: int(time.time() * 1000)}, headersHEADERS, datajson.dumps(payload, ensure_asciiFalse, separators(,, :)).encode(utf-8), timeout20, ) detail resp.json()[req_1][data][data]HEADERS里就是Content-Type、Origin、Referer、User-Agent这几项脚本里有完整定义不带 cookie 也能出数据。实跑输出榜单热歌榜 期数2026-10-30 总数300 ------------------------------------------------------------ 1. 我好像在哪见过你 - 薛之谦 2. 季候风 - 王菲 3. 天龙八部之宿敌 - 许嵩 4. 我不难过 - 孙燕姿 5. 甲乙丙丁 (你我怎么两清) - 李佳薇 ... 20. 过渡期 - 小宇-宋念宇脚本里还留了个--comments参数用来复现第五节的评论验证python 52_明文直连.py --comments八、踩坑小结看到sign先别急着逆 vmp。改一个字符、删掉先看服务端认不认。看响应别只看状态码。密文响应可以看长度、差异字节数再做一组“同一请求连发两次”的对照。同一套 CGI 往往有加密和明文两个入口页面自己就在用明文入口。补环境不是万能的。代码版本一变旧脚本的结果就对不上能绕过就别硬啃。控制请求频率别把学习搞成攻击。如有问题请及时提出加油加油。