Misc
1000-7
题目描述

有些旋律会一直留在你的脑海里。
有些旋律则会在离开后留下别的东西。
解题思路
检查文件
先检查附件,其后缀为 .mid,所以存储的是MIDI指令/事件,它通常会记录
什么时候按下哪个音符
什么时候松开
按键力度多大
使用什么乐器
音量是多少
音高怎么变化
节拍和速度是多少
同时又根据题目“有些旋律会在离开后留下别的东西”,所以数据被隐藏在MIDI文件中
安装读取MIDI的Python库
sudo apt update
sudo apt install python3-mido -y
观察MIDI中有什么
新建文件
check.py
打开后写入
from mido import MidiFile
from collections import Counter
# 打开 MIDI 文件
midi = MidiFile("chal.mid")
print("MIDI 类型:", midi.type)
print("轨道数量:", len(midi.tracks))
for number, track in enumerate(midi.tracks):
print("\n轨道编号:", number)
print("轨道名称:", track.name)
# 统计每一种 MIDI 消息出现了多少次
types = []
for message in track:
types.append(message.type)
print("消息类型统计:")
print(Counter(types))
保存后运行
python3 check.py
从中我们可以看出
MIDI类型:1 —— 文件中可以包含多条轨道,各轨道分别保存不同内容,但共用同一时间轴。
轨道数量:2 —— 文件共有两条轨道,轨道0和轨道1
轨道0:
名称:Unravel - Tokyo Ghoul
包含事件:
track_name:1 —— 曲目名称
set_tempo:1 —— 音乐速度
end_of_track:1 —— 轨道结束标记
因此,轨道0没有实际内容,只保存曲名和速度等全局信息
轨道1:实际演奏内容
其中:
note_on:2015 —— 按下
note_off:2015 —— 松开
但是
pitchwheel:752 —— 752个弯音轮(临时升高或降低音高)事件,这里的pitchwheel事件数量过多十分可疑
其他消息:
control_change:6 —— 控制音量、踏板等参数
program_change:1 —— 切换乐器音色
time_signature:1 —— 拍号,例如4/4拍
的数量都比较少,看起来比较正常
所以当前最重要的是提取这752个pitchwheel,看看它们有哪些数值以及是否存在规律
查看pitchwheel具体数值
新建python文件check2.py
from mido import MidiFile
from collections import Counter
midi = MidiFile("chal.mid")
pitch_values = []
# 遍历所有轨道和所有 MIDI 消息
for track in midi.tracks:
for message in track:
# 只提取 pitchwheel 弯音事件
if message.type == "pitchwheel":
pitch_values.append(message.pitch)
print("pitchwheel 总数量:", len(pitch_values))
print("不同数值及出现次数:")
print(Counter(pitch_values))
print("前 30 个 pitchwheel 数值:")
print(pitch_values[:30])
运行后得到
从结果我们可以看出
pitchwheel总数量:752
数值有:2304 -2304且二者的出现次数都为376
同时也可以猜测有两种组合:
(2304, -2304)
(-2304, 2304)
统计两种组合
新建文件check3.py
from mido import MidiFile
from collections import Counter
midi = MidiFile("chal.mid")
pitch_values = []
# 提取所有 pitchwheel 数值
for track in midi.tracks:
for message in track:
if message.type == "pitchwheel":
pitch_values.append(message.pitch)
pairs = []
# 每两个数值组成一组
for i in range(0, len(pitch_values), 2):
first = pitch_values[i]
second = pitch_values[i + 1]
pairs.append((first, second))
print("总共有多少组:", len(pairs))
print("不同组合及出现次数:")
print(Counter(pairs))
print("前 20 组:")
print(pairs[:20])
从输出结果可以看出:
总共有376组
只有两种组合:(2304,-2304)出现了193次;(-2304,2304)出现了183次
说明作者是故意让两个相反的弯音值组成一组,刚好可以表示0和1 另外,
376/8 = 47
也就是说,这些数据可以组成47个字节。ASCII文本通常一个字节对应一个字符,所以很可能藏有47个字符的文本
将两种组合转换成文本0和1
from mido import MidiFile
midi = MidiFile("chal.mid")
pitch_values = []
# 提取所有 pitchwheel 的数值
for track in midi.tracks:
for message in track:
if message.type == "pitchwheel":
pitch_values.append(message.pitch)
bits = ""
# 每两个数值作为一组
for i in range(0, len(pitch_values), 2):
pair = (pitch_values[i], pitch_values[i + 1])
# 先上升再下降,记为 1
if pair == (2304, -2304):
bits += "1"
# 先下降再上升,记为 0
elif pair == (-2304, 2304):
bits += "0"
# 出现其他组合就报出来
else:
print("发现异常组合:", pair)
print("二进制位数:", len(bits))
print("完整二进制:")
print(bits)
print("\n每 8 位分成一组:")
for i in range(0, len(bits), 8):
print(bits[i:i + 8], end=" ")
print()
每8位试着转换成一个字符
11000000 → 192,干扰字符
11011110 → 222,干扰字符
00101010 → *
01100111 → g
01110010 → r
01101111 → o
01100100 → d
01101110 → n
01101111 → o
01111011 → {
因此后面很明显出现
grodno{
与flag格式相符,说明我们的解码方向正确
每八位转换成字符
from mido import MidiFile
midi = MidiFile("chal.mid")
pitch_values = []
# 提取所有 pitchwheel 数值
for track in midi.tracks:
for message in track:
if message.type == "pitchwheel":
pitch_values.append(message.pitch)
bits = ""
# 每两个 pitchwheel 事件转换成一个二进制位
for i in range(0, len(pitch_values), 2):
pair = (pitch_values[i], pitch_values[i + 1])
if pair == (2304, -2304):
bits += "1"
elif pair == (-2304, 2304):
bits += "0"
# 保存解码后的文本
text = ""
# 每 8 位二进制转换成一个字符
for i in range(0, len(bits), 8):
one_byte = bits[i:i + 8]
# 把二进制转换成十进制
number = int(one_byte, 2)
# 把十进制转换成字符
character = chr(number)
text += character
print("解码结果:")
print(repr(text))
所以最终flag为
grodno{U1tr@_m3g@_5up3r_Gul_M1d_SF_1000-7}
Ghost Layers
题目描述

What stays visible is not always what matters most.
一直可见的东西,并不总是最重要的。
解题思路
检查文件
查看文件后缀为.svg文件
.svg是一种矢量图片文件,全称是:
Scalable Vector Graphics(可缩放矢量图形)
SVG通常用代码描述图形,例如线条、圆形、颜色和文字
特点:
放大不会模糊;
文件通常比较小;
可用浏览器直接打开;
可以修改颜色、大小和形状;
本质上是一种XML文本文件,可以用VS Code、记事本打开查看代码
可以通过CSS或JavaScript添加动画和交互
SVG内部可以包含脚本代码
寻找隐藏数据
SVG中通常会在
mask 蒙版
clipPath 裁剪
opacity 透明度
display 是否显示
visibility 是否可见
defs 只定义、不直接显示
中隐藏数据
于是先用VS Code打开文件,再用CTRL+F搜索“mask”,找到名字为“mk9”的蒙版

<mask id="mk9">
<rect x="0" y="0"
width="577"
height="635"
fill="black"/>
<polyline points="..."
stroke="white"
.../>
</mask>
这里可以理解为
黑色区域:隐藏
白色区域:显示
灰色区域:部分显示
这里先使用黑色矩形遮住整个画布,然后指通过一些白色线条显示少量内容,因此隐藏图形很难被观察到
找到使用蒙版的图层
从中我们可以看到信息:
<g id="ghost-wash"
opacity="0.72" —— 降低透明度
mask="url(#mk9)" —— 使用蒙版mk9
filter="url(#glowSoft)"> —— 使用发光、模糊效果
同时还有
clip-path="url(#cp4)"
说明真正决定图形轮廓的可能是cp4
分析cp4的裁剪路径
搜索
id="cp4"
引用id为s17的图形,并把它作为裁剪区域
于是继续搜索
id="s17"
找到
<g id="s17">
<g transform="translate(62.500,454.000)
scale(0.009400,-0.009400)"
fill="#f8efc9"
stroke="#f8efc9"
stroke-width="36"
stroke-linejoin="round">
<path d="..."/>
<path d="..."/>
...
</g>
</g>
s17 内部包含大量 <path> 标签。
这些路径不是随机数据,而是由文字转换得到的矢量轮廓。也就是说,隐藏的 Flag 已经被转换为一组路径,所以直接搜索:
grodno
无法找到明文
隐藏原理
s17
│
│ 保存 Flag 文字的矢量路径
↓
cp4
│
│ 将 s17 作为裁剪形状
↓
ghost-wash
│
│ 只在 Flag 文字内部绘制渐变和线条
↓
mk9
│
│ 再通过蒙版遮挡绝大部分区域
↓
glowSoft + opacity
│
│ 添加发光并降低透明度
↓
Flag 变得若隐若现
于是试图直接显示隐藏图层
EXP
先将原文件复制一份 用VS Code打开文件,跳转到结尾,找到
</svg>
在其前面加上
<rect x="0"
y="390"
width="577"
height="110"
fill="black"/>
<use href="#s17"/>
保存文件后用浏览器打开即可看见flag
