惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
Forbes - Security
Forbes - Security
The Last Watchdog
The Last Watchdog
TaoSecurity Blog
TaoSecurity Blog
Schneier on Security
Schneier on Security
SecWiki News
SecWiki News
V
Vulnerabilities – Threatpost
Project Zero
Project Zero
O
OpenAI News
W
WeLiveSecurity
Security Archives - TechRepublic
Security Archives - TechRepublic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
H
Hacker News: Front Page
Cisco Talos Blog
Cisco Talos Blog
Spread Privacy
Spread Privacy
Help Net Security
Help Net Security
P
Privacy & Cybersecurity Law Blog
K
Kaspersky official blog
S
Security @ Cisco Blogs
Latest news
Latest news
AWS News Blog
AWS News Blog
U
Unit 42
Martin Fowler
Martin Fowler
阮一峰的网络日志
阮一峰的网络日志
S
Secure Thoughts
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Know Your Adversary
Know Your Adversary
Scott Helme
Scott Helme
博客园 - 司徒正美
B
Blog RSS Feed
C
Check Point Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
D
Docker
Google Online Security Blog
Google Online Security Blog
Jina AI
Jina AI
aimingoo的专栏
aimingoo的专栏
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Last Week in AI
Last Week in AI
月光博客
月光博客
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
SegmentFault 最新的问题
NISL@THU
NISL@THU
T
The Blog of Author Tim Ferriss
C
Cisco Blogs
Attack and Defense Labs
Attack and Defense Labs
小众软件
小众软件

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog
Shinym@s 初探 02 - hash | Yesterday17's Blog
Yesterday17 · 2020-04-12 · via Yesterday17's Blog

嘛,本来只有上下两篇的,上篇讲 JS 相关,下篇试着看看能不能摸出点 WASM 的东西,结果……

这谁顶得住,正好趁这个机会了解一下 SC 音乐资源的编号方式,不也挺好的吗(

ToC

  • 结果
    • demo
    • main
  • 资源加载
    • 总索引
    • 分索引
  • 资源的分类
    • 图片
    • 视频
  • 卡面 ID 的编号方式
  • 结语

结果

总之今天的目的是(早就)达到了,结果在这里:

demo

main

资源加载

我们知道,SC 有大量的资源需要加载,这在客户端下载资源的时长以及浏览器的各种 Now Loading 就能看出来。

土豆别笑,两个 Loading 条你笑个锤子

因此,我们推测,一定有一些资源索引文件,记录了当前所有的资源文件信息。而根据源码的阅读,这个推测被证实是正确的。

总索引

由于资源实在太多,因此 SC 将索引分类两层,一层是总索引,索引的是各分索引,而各分索引对应的则是到真正数据的索引。总索引位于 /assets/asset-map.json,加密时的两个输入则为 asset-map.jsonasset-map,得到的输出是 aac011a61415a220560587aaf0177b2d98e8c7897d7697db4dd51b509162040d。根据索引的命名规则,在路径前面要加上 asset-map-,于是最终得到的路径就是 https://shinycolors.enza.fun/assets/asset-map-aac011a61415a220560587aaf0177b2d98e8c7897d7697db4dd51b509162040d,和观察的结果是一致的。

RESULT MATCH
RESULT MATCH

这时候我们不妨来看看这个文件的内容:

挺标准的分 chunk,还记录了 versiontotalSize。这里 totalSize 估算了一下,快 6 个 G 了。这就是 SC 吗,爱了爱了(不是

分索引

分索引记录的就是实际的素材了,像这种:

某一个文件
某一个文件

这个 Object 对应的 value 项就是资源的版本了,因此我们可以通过后者来判断资源是否需要更新。

最后,对于获取到的数据,SC 会把它们统一都塞进 hashMap 里。生成一个巨大的 Object,如下图所示(浏览器卡爆了,所以只截了一点):

资源的分类

在我们可以获取明文返回内容之后,我们首先想要知道的就是资源的具体分类方式。通过上面的图片我们也不难发现,SC 的资源本质是以目录的形式组织的,比如 ae/common/com_eff01_front_catastrophe/data.json 这种,对应的实际路径就是 https://shinycolors.enza.fun/assets/----经过 encryptPath 的字符串---(当然了,如果对应的后缀名较为特殊,比如 .m4a.mp4 之类的会保留后缀名)。

那这里我们可以观察到,资源的路径经历了一次 hash。在上一篇中,我简单地认为只要直接 hash 就可以了,但事实是并不能。来看代码:

N = {

createImagePath: function (e, t, n) {

var r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "";

return i.default.join(m, e, t, x(e, t, n, r));

},

createMoviePath: function (e, t, n) {

var r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "",

o = !(arguments.length > 4 && void 0 !== arguments[4]) || arguments[4],

a = i.default.join(_, e, t, R(e, n, r));

return o ? this.getEncryptedMoviePath(a) : a;

},

getEncryptedMoviePath: function (e) {

var t = i.default.basename(e, C),

n = i.default.join(l.default.env.ASSET_ROOT, e),

r = p.default.getQueryString(n);

return (

l.default.env.ENABLE_CRYPTO && (e = f.default.encryptPath(n, t) + C),

r && (e += r),

e

);

},

createSpinePath: function (e, t, n) {

return i.default.join(y, e, t, O(e, n), T);

},

createVoicePath: function (e, t, n) {

var r = t || n ? e + "/" + M(e, t, n) : "" + e + M(e, t, n);

return i.default.join(v, r);

},

createConcertMusicPath: function (e, t) {

return i.default.join(E, e, "" + O("unit", t) + w);

},

createTipsImagePath: function (e) {

return i.default.join(P, "" + e + S);

},

createAdminImagePath: function (e, t, n) {

return i.default.join(this.createAdminFolderPath(e, t), "" + n + A);

},

createAdminFolderPath: function (e, t) {

var n = i.default.join(g, e, O(e, t));

return n;

},

};

(这里的行号对应的是 Chrome 格式化后的行号,即 debugger:///VM493 pure-app-57696458079a3b7abba5.js.map:formatted。)

我们不妨分别来看。

图片

图片对应的函数是 createImagePath,可以看到,关键的函数是 x(e, t, n, r)。我们来看:

N = {

createImagePath: function (e, t, n) {

var r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "";

return i.default.join(m, e, t, x(e, t, n, r));

},

};

x = function (e, t, n) {

var r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "",

o = O(e, n),

i = (b[e] && b[e][t]) || A;

return "" + k(r) + o + i;

};

这里 r 对应的是 hasho 对应的是原名,i 对应的是后缀名,而 k(r) 函数输出的结果实际是 r !== '' ? r + '_' : r。所以我们可以发现,对于图片的路径,在 encryptPath 之前,需要将其加上对应的 hash 值。那 hash 怎么来呢?

经过进一步的挖掘,我们发现,hash 是随着 api 实时传给我们的,但这就出现了不一致了。如果 hash 只有在你有那张卡的时候才能拿到,那么客户端又是怎么缓存资源的呢?于是我们发现了 hashResourcesgetHashPrefixAssets,并得到了如下的结果:

经过整理的 hashResources 结果
经过整理的 hashResources 结果

有了这份 hash 表,我们就可以简单地计算出对应的真实路径了。

视频

视频的处理其实同理,虽然函数表示形式不同,但实际上和图片是一样的。这里就不再赘述了。

卡面 ID 的编号方式

我们(或许)知道,偶像是有其对应编号的,现在是从 001023,因此对应的卡面也有编号。我们知道,卡面有 RSRSSR 三种,其对应的 rarity 在代码中分别是 234。于是某一张卡面的 ID 就会遵循如下的格式:

1 0 3 001 001 0

^ ^ ^ ^ ^ ^

[CardType] [Category] [Rarity] [IdolID] [CardNum] [Unknown]

这里的 CardType 代表的是 P 卡还是 S 卡,1 为 P 卡,2 为 S 卡;Category 代表的是分类,目前 0 代表普通卡,9 代表 IDOLROADRarity 就是上面所说的 2~4(但其实是有 1 的,见下文),Idol ID 也是同理,Card Num1 开始,跟随 Rarity 大分类而增加。最后的 0 就意味不明了,至少现在大家的最后都是 0

(根据 Hash 表,S 卡里有四张以 201 开头的卡,游戏里也找到了。这四张卡相当“至少得有”的 Support 卡(如果你抽到的都是 P,虽然概率很低,但是这样的话你就没 S 卡了(或者说抽到的 S 卡数量不足 4 张:

我新来的.jpg
我新来的.jpg

结语

今天是在上次的基础上进一步的探索,在有了路径加密之后,SC 在此之上又增加了一层保护。然而世上总没有不透风的墙,客户端的出现也使得 hash 值这层保护的意义渐渐模糊了起来。

或者逻辑有可能恰恰是反过来的:SC 在原本没有路径加密的时候使用的就是 Hash,而后来才转移到以 WASM 为基础的路径加密上来,同时保留了过去的保护手段。或许是怕删除之后又出什么岔子,又或者只是单纯的想多一层保护手段,这或许只有 enza 的程序才知道了吧。

嘛,总而言之,现在我们已经可以下载所有的数据文件了。之后如果有空或许会搞一个查卡器什么的,不过这都是后话了。

总之,就到这里,去写操作系统的作业了,溜了溜了(