题目

题目内容:聽住呢首歌跟住跳足84轉就攞到flag
附件:nutty_rick.mid

解题思路

题目是 Misc,附件是一个 MIDI 文件,题面又提到“听住呢首歌”“84 转”,第一反应就是不要真的去听歌,而是先检查 MIDI 里有没有藏信息。

1. 先看 MIDI 里有什么可疑内容

直接用 strings 扫一下文件里的可打印字符串:
strings -n 4 nutty_rick.mid
能看到很多正常的轨道名,例如:

  • E.PIANO 2
  • SYN BASS 2
  • CLEAN GTR
  • MELODY
  • DRUMS
  • WHISTLE

但最后还会出现一个很可疑的隐藏轨名:

Phantom’s Breath

正常乐器轨里混进一个这种名字,基本可以判断这就是藏数据的地方。


2. 解析隐藏轨 Phantom's Breath

把 MIDI 事件解析出来之后会发现,这条轨里面有大量重复的事件,形式大概是:

note_on channel=14 note=0 velocity=114
note_off channel=14 note=0 velocity=0
note_on channel=14 note=0 velocity=105
note_off channel=14 note=0 velocity=0

这里有两个明显特征:

  1. note 基本固定不变
  2. 变化的是 velocity,而且数值都落在可打印 ASCII 范围附近

所以很自然地想到:把 velocity 当成 ASCII 解码

例如前几个值:

  • 114 -> r
  • 105 -> i
  • 99 -> c
  • 107 -> k

拼起来就是 rick...

继续解完整条隐藏轨,得到:

rickroll_lyrics[84],then,dcb58-<rickroll_lyrics[84]>-aa401.github.io

到这里题意就很清楚了:

  • “84 转”并不是真的让你转圈
  • 而是在提示你去取 rickroll 歌词里的第 84 个词
  • 然后把它代进仓库名模板里

3. 不用硬数歌词,直接定位 GitHub 仓库

其实这里没必要去纠结歌词到底按哪种方式分词。
因为字符串已经给了非常明显的模板:

dcb58-<某个词>-aa401.github.io

直接拿固定前后缀去搜,就能定位到 GitHub Pages 仓库:

dcb58-aching-aa401/dcb58-aching-aa401.github.io

仓库页面显示它是一个公开仓库,当前只有一个 index.html 文件。

于是就能反推出:

rickroll_lyrics[84] = aching


4. 打开仓库源码拿 flag

继续打开这个仓库的 raw index.html,第一行就是 flag。

最终得到:

PUCTF26{Ri3k_R011_Phant0m_Velo31ty_9f3a7c2e1b8d4a0f6c5e2d9b1a7f4c8e}


复现脚本

下面这个脚本不依赖第三方库,直接解析 MIDI 并提取隐藏轨里的 velocity 文本:

from pathlib import Path

def read_vlq(data, off):
value = 0
while True:
b = data[off]
off += 1
value = (value << 7) | (b & 0x7F)
if b < 0x80:
return value, off

def parse_midi(path):
data = Path(path).read_bytes()
off = 0

assert data[off:off+4] == b"MThd"
off += 4
hdr_len = int.from_bytes(data[off:off+4], "big")
off += 4
hdr = data[off:off+hdr_len]
off += hdr_len
ntracks = int.from_bytes(hdr[2:4], "big")

tracks = []
for _ in range(ntracks):
assert data[off:off+4] == b"MTrk"
off += 4
tlen = int.from_bytes(data[off:off+4], "big")
off += 4
tdata = data[off:off+tlen]
off += tlen

events = []
i = 0
running = None

while i < len(tdata):
_, i = read_vlq(tdata, i)
status = tdata[i]

if status < 0x80:
status = running
else:
i += 1
if status < 0xF0:
running = status
else:
running = None

if status == 0xFF:  # meta event
meta_type = tdata[i]
i += 1
length, i = read_vlq(tdata, i)
payload = tdata[i:i+length]
i += length
events.append(("meta", meta_type, payload))

elif status in (0xF0, 0xF7):  # sysex
length, i = read_vlq(tdata, i)
i += length

else:
ev_type = status & 0xF0
ch = status & 0x0F

if ev_type in (0xC0, 0xD0):
d1 = tdata[i]
i += 1
events.append(("midi", ev_type, ch, d1))
else:
d1 = tdata[i]
d2 = tdata[i + 1]
i += 2
events.append(("midi", ev_type, ch, d1, d2))

tracks.append(events)

return tracks

tracks = parse_midi(“nutty_rick.mid”)

for tr in tracks:
names = [e[2].decode(errors=”ignore”) for e in tr if e[0] == “meta” and e[1] == 0x03]
if names and names[0] == “Phantom’s Breath”:
msg = “”.join(
chr(e[4]) for e in tr
if e[0] == “midi” and e[1] == 0x90 and e[4] > 0
)
print(msg)
break

运行输出:

rickroll_lyrics[84],then,dcb58-<rickroll_lyrics[84]>-aa401.github.io


总结

这题的核心是一个很典型的 MIDI 隐写

  • 先从 MIDI 轨道名里发现隐藏轨 Phantom's Breath
  • 再把隐藏轨里的 Note On velocity 当作 ASCII 解码
  • 得到 GitHub Pages 仓库名模板
  • 最后去仓库源码里拿到 flag