Phigros 第九章 ARG 收官:17:00 归零那一刻,与藏在控制台里的致谢

2790 字
14 分钟
Phigros 第九章 ARG 收官:17:00 归零那一刻,与藏在控制台里的致谢

Solivault 页面上那个跑了一周的倒计时,在 2026-10-02 17:00<00>(北京时间)归零了。

这一环之前我写过一次完整复盘,那篇讲的是链条怎么解。这篇只讲收官:归零那一刻到底发生了什么、官方换了什么、以及为了在限流和动态值里抓到它,我最后是怎么做的。

先说结论:归零没有让主页面变成别的东西,只有一个页面换了版本——终点页在 17<01> 到 17<02> 之间,把源码从混淆版换回了明文版,同时放出了一段只在控制台里打印的致谢。

一、这一周它在数什么#

隐藏页 WERJETZTALLEINISTWIRDESLANGEBLEIBEN 上有两条时间线:

  • 主页面(/)从某个时间点起换成了 CRT 风格的滚动码显示器;
  • nosong.html 保留着写死的截止时间:
const DEADLINE_TEXT = '2026/10/02 17:00:00';
const target = Date.parse(DEADLINE_TEXT.replace(/\//g, '-').replace(' ', 'T') + '+08:00');

时间源不止本地时钟,它是按顺序试三个源再取其中一个:站内 ntp.php、timeapi.io 的 UTC 接口、以及 Cloudflare 的 cdn-cgi/trace 里的 ts=。同步到之后每 10 分钟重校一次——换句话说,本地改系统时间没用。

归零后它不会自己跳转,只停在 0:00:00:00。我用无头浏览器读回它的 canvas 文本确认过:

----::----::----::---- ← 还没同步到时间源时的占位
00:00:000::000 ← 归零后的显示(重影字符已被我合并)

二、主页面是一台滚动码发生器#

主页面(51,408 字节)没有倒计时,中央只有一个 XX:XX:XX:XX 形式的 4 组 2 字符随机码,在深红色的眼球/黑洞背景上高速翻转。

它的字符集是全部可打印 ASCII:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}<>?/\|~

我前后采样了五次(16<55>、跨越 17<00> 整点、17<06>、17<09>、17<12>),帧数分别是 189/189、500/500、303/303、254/254、254/254——合计 1500 帧里没有任何一帧重复,字符集里始终没有出现可读单词。所以归零这件事只发生在”谜题”这一层,不在这个显示器上。

一个差点写错的坑

主页面背景上那颗红色的眼球/黑洞是间歇性动画,会自己消失再出现。17<06> 我截的图里它恰好不在,一度让我怀疑”红眼消失是不是 17<00> 后的变化”;同一会话继续采到 17<12> 时它又自己回来了(中央亮度 29.3 → 56.3)。所以它是页面自带的装饰节奏,和 17<00> 无关,别把它当成收官变化。

页面上没有任何键盘、点击、触摸监听;倒是留了两个给低配设备的开关:?lite 降特效、?black / ?nobg 跳过背景图。JS 是 obfuscator.io 的 base64+RC4 混淆,还带了 Function("return (function() {}.constructor('return this')( ));") 这类反调试和一个 console 方法劫持。

三、17<00> 前后:我抓到的完整时间窗#

为了确认”是什么在什么时刻变了”,我在归零前后各按固定时间点抓了三个页面,每次都从代理池取一个全新的出口 IP(原因见第六节)。用归一化后的稳定哈希比对:

抓取点soli(主页面)nosongvault(终点页)
16<53> PRE_Aef360ff1… 51,408 B4eed183b… 40,829 B(代理失败)
16<57> PRE_Bef360ff1…(代理失败)f32e5a6b… 68,579 B
17:00<31> POST_Aef360ff1…4eed183b…f32e5a6b… 68,579 B
17:02<32> POST_Bef360ff1…4eed183b…044171ea… 63,953 B ← 变了
17:05<56> POST_Cef360ff1…4eed183b…044171ea… 63,953 B

也就是说:

  • 主页面和 nosong 全程 5 个抓取点的哈希一模一样,一个字节都没动;
  • 终点页的切换发生在 17:01<00> 到 17:02<32> 之间,从 68,579 B 掉到 63,953 B,之后保持稳定。

四、终点页把源码放回来了#

新旧两版差 4,626 字节,差的不是功能,而是保护。旧版内联 JS 是混淆的:

(function(_0x4ede78,_0x255225){var _0x104d79=_0x494f,_0x506f5c=_0x4ede78();while(!![])…

新版是明文的:

(function () {
'use strict';
var LANGS = { zh: { htmlLang: 'zh-CN', title: 'Sky of Your Vault', …

脚本块结构也换了:

旧版(17<00> 前)新版(17<02> 后)
主 JS42,217 B(混淆)34,867 B(明文)
留言板 API14,616 B(混淆)9,591 B(明文)
陀螺仪藏在混淆里6,383 B(独立明文块)
版本号2.2.02.4.0

**版本号从 2.2.0 跳到 2.4.0,同时把混淆撤了。**时间点很说明问题:倒计时还没归零时,这套源码是加密的(防止提前剧透);归零后立刻放回明文,意思等于”不用再藏了”。

五、那封藏在控制台里的致谢#

新增的那段代码在页面加载后做一件很安静的事——它把一整套文本用 %c 格式化后打进 console.log:

console.log.apply(console, [fmtParts.join('')].concat(styleArgs));
addChunk('CAST LIST/制作人员名单', titleStyle, 124, 90, '#ffffff', '', true);
addChunk('Phigros Chapter 9 ARG', subtitleStyle, 46, 45, '#ffffff', '', true);

所以打开终点页、按 F12、切到 Console,就能看到一份彩色排版的制作人员名单。这也是为什么之前要混淆源码——不然提前翻源码就能读到它。

名单全文(原文照录):

CAST LIST / 制作人员名单 Phigros Chapter 9 ARG

关关弗斯 — 本次 ARG 的策划,设计了解谜流程,坐在鸽游大楼里轻哼之人 「大伙都辛苦了!」 「【回头看山坡上方】布豪,DDL 滚成雪球追过来了,我先跑!」

吸管猫 — 使用头顶天线发射模拟信号 「莫西莫西 听得到吗?」

汉堡 — 负责和你的手机、平板、手表、希沃、冰箱、微波炉等家电的 GPU(后面两个如果有的话)打交道 「尝试了一些新的东西,淘汰了一批低端机(bushi)。这次第九章除了本体的制作外还对之前一直有的性能瓶颈做了优化,希望能优化到吧(AI 真是太好用了你知道吗)」 「免责声明:AI 仅用于构思方案与程序设计辅助,所有 CG 动画均由 Pigeon Animation Team 古法匠心手作」 「(关关弗斯:[呆无辜] 虽然但是,这里不是第九章的 cast。问题不大,已读乱回我很喜欢)」

rN — man of english translation & playtesting 「i don’t know how to write cool english sentences, so i will wait for the marriage ceremony instead(?). coo」 「我不晓得咋个能去嚓嚓写出那酷炸天滴句子,所以急救什么时候结婚?坐等婚礼咕哦!」 「(来自关关弗斯的缺德翻译)」

Gaoice — 解密网站的前后端开发,以及林泊百科的运维,配合汉堡师傅测试他的优化 「这次后厨的土豆没炸!!我的测试土豆也没炸!!前端的土豆后端的土豆都没炸!!」 「总之感谢你的炸的土豆!!没熟!!」

Falling_Studio — 做图被抓包之人 「很荣幸能为第二阶段的谜题网站设计视觉!希望大家喜欢」

—— Thanks For Playing!

最后一句是 Thanks For Playing!,配色用的是页面主题的那个青色(#31a8ca)。

顺带一提

名单里 Falling_Studio 那句「第二阶段的谜题网站」是唯一一处提到后续阶段的地方。如果这是分阶段的 ARG,那现在还只是上半场。

六、怎么抓到的:三个麻烦,三个对策#

这一轮能抓到 17<00> 的时间窗,靠的是三件不得不做的事。

麻烦一:页面上的字是画在 canvas 上的,curl 看不到。

curl 拿到的 HTML 里没有那串滚动码——它是页面里 Math.random() 现算、再用 fillText 画上去的。解决方式是打开真浏览器,在页面脚本执行前包住 canvas 的绘制接口:

// page.addInitScript 里
const orig = CanvasRenderingContext2D.prototype.fillText;
CanvasRenderingContext2D.prototype.fillText = function (text, x, y) {
window.__frames.at(-1)?.t > performance.now() - 120
? window.__frames.at(-1).chars.push([x, String(text)])
: window.__frames.push({ t: performance.now(), chars: [[x, String(text)]] });
return orig.apply(this, arguments);
};

之后按帧取回、把 x 坐标分桶去重就能还原出当前的码。之所以要去重,是因为这套视觉是红青重影,同一个字符会在 x±2 各画一次,直接拼接会得到 ZZ33::??tt:: 这种双写形式。

麻烦二:站点限流很紧,直连和普通抓取通道都会被挡。

这个 wiki 有三层拦:自定义的 773「访问被拒绝」(拦爬虫特征 UA、部分接口还要求带 Referer)、nginx 403(请求一密就封,实测连发三五次就中)、以及海外 IP 偶发 502。用代理池绕,而且每个请求都要换新 IP——池子里的 IP 只活两三分钟,复用第二个请求基本必失败。取代理的接口还有 5 秒调用间隔限制:并行跑多个抓取进程时它们共用同一个 key,会互相抢配额(表现为频繁返回「调用间隔超过套餐 5 秒限制」),所以并行度开不高,或者各自排队把节奏拉开。

麻烦三:页面里有两处每次请求都变的动态值,直接比对 md5 会误判成”页面一直在变”。

终点页 <head> 里有一个每次渲染都下发的请求令牌:

<meta name="page-token" content="1790930028.373955ed3dc0834cd18d51f6cc2d272f">

而新版明文 JS 里还硬编码了同一个值做占位比较:

if (!pageToken || pageToken === '1790931752.9f96c58c320b8c33a4193bf2d7b39251') pageToken = null;

这两处长度固定、所以字节数不变,但内容每次都变。我是先把它们和 Cloudflare 的注入片段一起归一化,再算哈希:

s = re.sub(r'(name="page-token" content=")[^"]*(")', r'\1TOK\2', s)
s = re.sub(r"(pageToken === ')[^']*(')", r"\1TOK\2", s)

不归一化的后果很直观:同一版页面连抓两次会得到两个不同的 md5,看起来像”页面在不停更新”。

七、顺带修清的机制#

**page-token 到底是什么。**从新版明文的留言板脚本里能看到它的去向——它被当成请求头带上:

if (pageToken) options.headers['X-Page-Token'] = pageToken;
if (options.body && csrfToken) options.headers['X-CSRF-Token'] = csrfToken;
return fetch(API_BASE + '?api=' + name, options)

也就是说它是服务端每次渲染下发的一次性令牌,用来给 index.php?api=bootstrap|messages|highlights 这组留言墙接口做防重放。那句硬编码的比较值是开发时留的占位(1790931752 换算过来正好是 17:02<32>,也就是页面换版的时刻)。

留言墙还活着。bootstrap 正常发 CSRF,highlights 拉回来的十条留言仍然是清一色 10~40 字符的乱码短串——大家把自己解出来的碎片”呼唤”进了星空,没有一条自然语言。

**多了个陀螺仪脚本。**新版把设备方向控制单独拆成一块:手机倾斜就能转星空(PITCH_MAX 1.15、TILT_K 0.45),需要 DeviceOrientationEvent.requestPermission 的浏览器会先问权限。

八、写在最后#

从 9 月 25 日那条 Chaocipher 密文,到今天 17<00> 倒计时归零、控制台里浮出 Thanks For Playing!,这一环算是走完了。

整个过程里最让我意外的是收尾的方式:没有新谜题、没有新页面、主页面连一个像素都没变——官方只是把源码的锁打开,然后在控制台里说了声谢谢。用版本号从 2.2.0 跳到 2.4.0 这种细节来标记”这一版是给通关的人看的”,挺温柔的。

至于第二阶段的谜题网站——如果真会有,到时候再抓。


口径说明:本文所有时间窗、字节数、哈希值、名单文本均来自我在 2026-10-02 的实测抓取(各时间点的完整快照都留了档)。链条的解读部分参考了 Phigros 解密 3 群(社区)的整理,收官部分的结论是这次实测得出的。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Phigros 第九章 ARG 收官:17:00 归零那一刻,与藏在控制台里的致谢
https://blog.wmc.pub/posts/phi9-solivault-finale/
作者
Milk
发布于
2026-10-02
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
Phigros 第九章《穹顶孤舟》网页解谜复盘:从 Chaocipher 到 13 段碎片拼字
技术一条完整可复现的解谜链:Chaocipher 解出隐藏页、PNG 尾部藏图、零宽字符转摩尔斯、Nihilist 解出 JUSTENGAGEWTH、按浏览器指纹分发的 13 段密文,以及 acrostic 竖读出的两个关键词。
2
Phigros 4.0.1 第九章客户端全解:从启动到退出,这一章在程序里究竟发生了什么
技术把 Phigros 4.0.1 客户端里第九章的整条链路走完:启动时怎么恢复解锁与密钥、选曲与难度动画、关卡结算如何推进章节进度、Phase 2 换形态、谜题与推演、密码背后的 AES 方案(含 8 个秘密包的解密与加载/卸载)、Hate→DS→Message→Credits 的演出顺序,以及「退出应用」那一处——附 68 个脚本类各自干了什么。
3
穹顶孤舟:Phigros 主线全章总结(含从秘密包里解出的终章)
随笔主线已经收束。这篇把一~九章连起来讲一遍:Legacy 里的一~四章、五《方舟蜃景》到八《弭刻日》的门槛与关底、第九章《穹顶孤舟》的 12 首曲与解密档案,以及最后从客户端秘密包里解出来的 C9S6 终章对白。
4
Solivault 的歌里藏了什么:一次 SSTV 解码实录
技术Solivault 页面改成会播歌之后,我们把音频从 WebCrypto 解密链路里捞出来,发现节拍里藏着一条 SSTV 传输——解出一张 320×256 的角色立绘。
5
穹顶孤舟 · 素材馆:把第九章整个装进一页
随笔第九章的全部可听可见之物都在这:选难度的六路视频、12 首曲的官方试听与 12 张曲绘、隐藏曲 PV、结局循环、秘密曲、31 段音效切片、10 张材质(噪域/故障/网格/密码界面)、8 组界面截图,以及完整清单表与出处。
随机文章随机推荐

评论区

Profile Image of the Author
Milk
AWMC Founder
分类
标签
站点统计
文章
10
分类
3
标签
22
总字数
58,773
运行时长
0 天
最后活动
0 天前
文章目录