- JavaScript 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- 拦截 tx*.com /h5/{user/info,system/info,movie/block,movie/search},AES-128-ECB/PKCS7 解包后改写会员态与广告位
- /h5/movie/detail 转发上游桥接服务解锁(含签名/加码/保护包协议实现)
- 支持 crypt_key 本地模式,完全脱离上游服务
- dump_crypt / dump_detail 只读诊断开关
- 与原脚本逐字段等价对拍通过
|
||
| tools | ||
| .gitignore | ||
| README.md | ||
| tx-vip.js | ||
| tx-vip.sgmodule | ||
糖心VLOG 解锁方案(提取自 YQC「私密专区」模块)
来源:https://yqc-premium.pages.dev/Surge/nsfw.sgmodule(需带 User-Agent: Surge/... 请求头,否则站点返回 403)。
模块本身只是壳,真正的逻辑在 https://yqc-premium.pages.dev/Scripts/NSFW/tx.vip.js(240 KB,string-array + RC4/Base64 混淆,另含 vue/crypto-js/URL polyfill 打包)。
本目录是该脚本中糖心VLOG部分的方案提取与可读重写。
tx-vip.sgmodule 仅含糖心VLOG的 Surge 模块(3 条脚本规则 + MITM + 桥接直连规则)
tx-vip.js 提取后的可读实现(等价于原脚本的糖心VLOG分支)
tools/dump-crypt.js 离线解开 crypt 保护包,取出 tx 接口 AES 密钥
安装
Surge → 模块 → 从 URL 安装:
https://git.szzn.dev/congshuhao/tx-vip/raw/branch/main/tx-vip.sgmodule
装完直接打开模块的参数界面填写,不用改模块正文、改完立即生效:
| 参数 | 默认 | 说明 |
|---|---|---|
crypt_key |
空 | tx 接口 AES 密钥(16 位)→ 本地模式,不访问上游服务 |
vip_key |
空 | 上游桥接服务会员密钥 → 桥接模式(上游仅允许中国大陆 IP) |
policy_group |
DIRECT |
桥接请求使用的策略组 |
dump_crypt |
空 | 填 1:把 crypt_key 连同来源推送出来(优先读原脚本缓存,可离线取密钥) |
dump_detail |
空 | 填 1:把 /h5/movie/detail 明文推送出来(只读,不改写) |
crypt_key 与 vip_key 填一个即可;两个都填时 crypt_key 优先。模块里 script-path 写的是绝对 URL(script-update-interval=3600,最多 1 小时拉一次更新),因此脚本文件必须与模块一起放在仓库里。
1. 拦截面
| 接口 | 类型 | 作用 |
|---|---|---|
/h5/user/info |
http-response | 伪造尤物卡(会员态) |
/h5/system/info |
http-response | 去除全局广告配置 |
/h5/movie/block |
http-response | 过滤信息流广告条目 |
/h5/movie/search |
http-response | 过滤搜索结果中的推广条目 |
/h5/movie/detail |
http-request | 详情页解锁(转发到上游服务端) |
- MITM:
tx*.com、*.tx*.com - 规则:
^https?://(\w+\.)?tx[a-z]{1}\d+\.com/h5/(user/info|movie/(block|search)|system/info)、^https?://(\w+\.)?tx[a-z]{1}\d+\.com/h5/movie/detail - 四个响应接口全部
requires-body=true,响应体是密文。
2. 接口体编解码
ciphertext = Base64( AES-128-ECB / PKCS7 ( UTF-8(JSON) ) )
key = crypt.key(16 位 ASCII,由服务端按 app 版本下发)
- 解密:
CryptoJS.AES.decrypt(body, Utf8.parse(crypt.key), {mode: ECB, padding: Pkcs7}) - 回写:同一密钥加密后
$done({body}) - 解密失败 → 强制刷新一次 crypt 配置再试,仍失败则
$done({})(原样放行)
/h5/movie/detail 是请求脚本(echo response):把原始请求原样转给上游,上游返回的内容直接作为响应体回写(已由上游加密/改写好),脚本本身不碰详情体。
3. crypt 配置获取协议(私有桥接服务)
糖心的 AES 密钥不在客户端,由作者的服务端下发,请求经 vipKey 签名。
POST https://api.yqc-premium.com/nsfw
headers:
x-ver: 3
content-type: application/json; charset=utf-8
x-auth-uid: shuffle(vipKey) # 确定性洗牌,LCG(seed=42, 9301, 49297, 233280)
x-tools: Base64(UTF-8(encodeURIComponent(环境名))) # Surge/Loon/Stash/Quantumult X/Egern/Shadowrocket
x-timestamp: UTC+8 毫秒时间戳
x-id: txvlog # app 代号
x-sign: HmacMD5(message, vipKey) # message = "environment:<env>\ntimestamp:<ts>\ncode:<code>\nv9w$=gxaqa"
x-policy: encodeURIComponent(策略组)
body: {"y": <逐字符加码(JSON.stringify(payload), code)>} # charCodeAt 相加,code=tvxlog 循环作为密钥
payload(action=crypt): {"action":"crypt"}
响应若带 x-enc: 1,体格式为 $<key>$<Base64(AES-CBC/PKCS7)>,其中 key 同时充当 IV。
保护包解包(crypt 下发体):
{"t.me/yqc_000": {"v":"p1", "s":<seed>, "d":<密文>, "h":<校验>}}
derived = MD5(vipKey + ":" + appId + ":" + s + ":yqc-crypt") # 32 位十六进制
key = derived[8:24] signSeed = derived[0:16]
maskShift = parseInt(derived[24:26],16) % 65
chunkSize = 4 + parseInt(derived[26:28],16) % 3
校验 : h == MD5(v + ":" + s + ":" + d + ":" + signSeed)
解 d : ① 按 chunkSize 逐块反转;② 用错位 65 字符表映射回标准 Base64;③ AES-CBC(key=IV=key) 解密
解出的 JSON 使用单字符短字段名,需还原:e→expiresAt r→refreshIntervalMs t→fetchedAt g→fetchedDate k→key v→iv c→crypt d→data f→profile。
结果缓存在 $persistentStore,默认 24h(expiresAt/refreshIntervalMs/跨天任一不满足即重取)。
3.1 取出 crypt.key
密钥只在桥接服务的响应里,且桥接服务只接受中国大陆出口 IP(实测:HK/JP/US/NL/搬瓦工/直连一律 403 {"code":403,"message":"仅允许中国大陆 IP 访问"})。三条路径:
-
手机上有缓存(最省事):原模块跑成功过一次,密文包就在 Surge 持久化里。 给本模块参数「导出密钥」填
1,跑一次糖心,通知/日志即给出crypt_key(优先读原脚本缓存YQC_PREMIUM.txvlog,无需任何桥接请求):通知: crypt_key = xxxxxxxxxxxxxxxx 来源: 原脚本缓存若把持久化存储导出成 JSON,也可离线解:
node tools/dump-crypt.js <会员密钥> <存储 JSON 或桥接响应> -
从桥接服务现取:必须在中国大陆网络下(手机蜂窝/国内宽带)发请求;本机所有可用出口都被拒。
-
从 App 里取:密钥物理上必须在客户端(App 要能解密自己
/h5/*的响应体)。模块里的tx*a?.com域名已全部轮换/被抢注(实测 260 个候选今天无一是原 App),txapp.tv已死,发布地址页已被 ParkLogic 停放,需要重新拿到当前 APK 或 H5 包再逆向(搜AES/ECB/Pkcs7/16 位常量;若运行时下发则 hook App 的解密函数)。
crypt_key 是按 App 版本轮换的快照,本地模式不会自动刷新:密钥失效后本脚本会逐项失败回退为原样放行(这也是原方案要挂桥接服务的原因)。
4. 本地改写语义
/h5/system/info → data 合并:
{ ad_auto_jump:'n', ad_show_time:0, ad:null, ads:[], bottom_ad:null, bottom_ads:[],
layer_ad:null, layer_ads:[], layer_app:[], post_banner:[], dating_frame:[], dark_tips:'' }
/h5/movie/block → data = data.filter(item => !item.ad) // 丢弃带 ad 字段的条目
/h5/movie/search → data = data.filter(item => item.ad_link === '') // ad_link 非空即推广位
/h5/user/info → data = {...原 data, nickname, headico, sign, ad:[],
is_vip:'y', is_dark_vip:'y', is_up:'y', is_mer:'y',
phone:'18888888888', exp_level:'6',
group_name:'糖心至尊卡', group_end_time:'2099-01-01', fans_club_link}
movie/search 是保留 ad_link === '' 的条目(ad_link 非空代表推广结果),原脚本即如此,不要写反。
5. 详情解锁
/h5/movie/detail 走 script-analyze-echo-response:把被拦截的原始请求
{ "url": "...", "method": "...", "headers": {...}, "body": "..." }
按第 3 节的签名协议 POST 到 https://api.yqc-premium.com/nsfw(x-id: txvlog),服务端用同样的 tx 密钥/算法取号并回写已解锁的详情体,脚本以
$done({ response: { status, headers, body } })
短路上游。失败时 $done({}),请求按原样继续。解锁逻辑在服务端,脚本侧没有本地实现。
注意作者的实现选择:他们自己就持有 tx 密钥(四个净化接口的 key 由他们下发),却没有本地改写详情,说明播放权限(能否拿到可播地址)是在服务端判定的,客户端改包改不动。这是推断,证据是他们的实现方式。
5.1 详情探针:判断本地能不能补
本地模式(crypt_key)放行详情请求后,响应体会被下面的规则解密出来看一眼,不改写:
详情探针=1 → 通知/日志给出 /h5/movie/detail 的明文 JSON
用法:找一部看不了的视频与一部能看的视频,分别打开一次,对比两份明文。差异字段就是权限开关:
- 若差异出现在
is_buy/is_vip/play_auth这类可改字段 → 本地解锁有戏,可以照净化接口的方式补; - 若
m3u8/dash是空串、播放地址由服务端另行签发 → 本地无论怎么改都拿不到地址,只能走桥接服务。
6. 与 tx-vip.js 的差异
- 保留:全部接口、编解码算法、桥接协议、签名/加码/保护包算法、缓存与强制刷新策略、
$done回写形态(与原脚本逐字节比对通过)。 - 去除:作者通知中的 Telegram/图片推广链接、拉黑/授权逻辑、其它 app 分支、打包的 vue/crypto-js/URL polyfill(改用宿主
require('crypto-js'))。 user/info的nickname/headico/sign/fans_club_link改为中性值(原为作者推广),功能字段一致。- 新增
crypt_key参数:直接给出 tx 密钥即可完全脱离桥接服务(vip_key与crypt_key二选一)。此模式下详情接口无法解锁,会原样放行。 - 新增
dump_crypt/dump_detail诊断开关:只读导出,不改写任何响应。
7. 部署
仓库(Forgejo):git@git.szzn.dev:congshuhao/tx-vip.git。
模块用绝对 URL 拉脚本,改脚本后无需重装模块(script-update-interval=3600)。
nginx/Gitea raw 目录结构:
tx-vip.sgmodule
tx-vip.js
tools/dump-crypt.js
模块里的 [Rule] DOMAIN-SUFFIX,yqc-premium.com,DIRECT 保证桥接请求走直连(上游只要大陆 IP);只用本地模式可以删掉这条。
8. 验证
原脚本与本实现在同一套 Surge mock($request/$response/$done/$httpClient/$persistentStore/$notification,x-enc 信封、保护包、签名全部按上文生成)下逐项比对:
/h5/user/info、/h5/system/info、/h5/movie/block、/h5/movie/search:解密结果逐字段一致,会员字段与广告清理一致;/h5/movie/detail:转发载荷与最终$done({response})完全一致;桥接请求经签名/加码校验(x-sign、x-tools、x-auth-uid、x-id、x-policy、体加码全部匹配);- 缓存命中不重复请求、密钥失配后强制重取、
crypt_key本地模式零桥接请求、未配置密钥时$done({})原样放行、模块三条规则匹配/拒绝用例; dump_crypt:用真实原脚本跑出的持久化缓存离线解出 key,桌面工具与脚本内两条路径都能还原;dump_detail:只读导出详情明文,响应体逐字节原样放行,且不影响其它接口。