ISCC2020 Misc - 耳听为实

送分部分

题目地址: https://hikawa.ml/CTF/iscc-misc5/ear.rar

解压得到 ABC.mp3

Maksim Mrvica - New Silk Road

联想到 MP3Stego, 使用密码解压出隐藏数据

1
2
3
flag is here!
https://pan.baidu.com/s/1L3cq1CRVhvv6mq8qogq-sA
dHc0aQ==

dHc0aQ== base64解码得 tw4i,看出是百度网盘的密码

下载后又是一个mp3

file一下,你就知道,这是个zip

binwalk解压得 ctf-produce.pyflag-RD.wav

ctf-produce.py 分析

flag-RD.wav听起来像是杂讯,我们看看py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import wave
import numpy as np
import os

f = wave.open(os.path.abspath('./flag.wav'), 'rb')
# 读取音频信号
# 二进制只读模式,打开音频文件
# open返回一个的是一个Wave_read类的实例,通过调用它的方法读取WAV文件的格式和数据:

params = f.getparams()
# 返回音频参数,元组:声道数,量化位数(byte单位),采样频率,采样点数,压缩类型,压缩类型的描述

nchannels, sampwidth, framerate, nframes=params[:4]
# 赋值声道数,量化位数,采样频率,采样点数
# wave模块只支持非压缩的数据,因此可以忽略最后两个信息

str = f.readframes(nframes)
# readframes:读取声音数据,传递一个参数指定需要读取的长度(以取样点为单位)
# readframes 返回的是二进制数据(一大堆bytes),在Python中用字符串表示二进制数据

wave_data = np.fromstring(str, dtype=np.short)
# 字符串转换为short类型
# 根据声道数和量化单位,将读取的二进制数据转换为一个可以计算的数组:
# 通过fromstring函数将字符串转换为数组,通过其参数dtype指定转换后的数据格式
# 由于我们的声音格式是以两个字节表示一个取样值,因此采用short数据类型转换。
# 现在我们得到的wave_data是一个一维的short类型的数组,
# 但是因为我们的声音文件是双声道的,因此它由左右两个声道的取样交替构成:
# LRLRLRLR....LR(L表示左声道的取样值,R表示右声道取样值)

time = np.arange(0, nframes) * (1.0 / framerate) # 通过采样点数和取样频率计算出每个取样的时间,即第n帧所在时间为(n-1)/采样率

# 语音信号分帧处理
wlen = 100 # 帧长
inc = 50 # 帧移
signal_length = len(wave_data)
# 信号总长度,为声道数*采样率*时间


if signal_length <= wlen:
nf = 1
else:
nf = int(np.ceil((1.0*signal_length-wlen+inc)/inc))
# 若信号长度小于一个帧的长度,则帧数 nf 定义为1,否则,计算帧的总长度
pad_length = int((nf-1)*inc+wlen)
# 所有帧加起来总的铺平后的长度
zeros = np.zeros((pad_length-signal_length), dtype=int)
# 不够的长度使用0填补
pad_signal = np.concatenate((wave_data,zeros))
# 填补后的信号记为pad_signal
# 以上应该是把信号长度补成50的倍数


indices = np.tile(np.arange(0,wlen),(nf,1))+np.tile(np.arange(0,nf*inc,inc), (wlen,1)).T # 相当于对所有帧的时间点进行抽取,得到nf*wlen长度的矩阵,转置了
indices = np.array(indices, dtype=np.int32) # 将indices转化为矩阵
# 生成一个nf*wlen的矩阵
# 其中第n行的范围为[(n-1)*50, (n-1)*50+100-1],整数
# 这个可以看成是信号的坐标
# 第二句应该不重要


indices = np.random.permutation(indices)
# 这里是难点,他把信号坐标对应的行数打乱了

frames = pad_signal[indices] # 得到帧信号
# 把坐标对应的信号写道frames里

frames = frames.flatten()
# 降回一维

w = wave.open(os.path.abspath('./flag-RD.wav'), "wb") # 打开WAV文档
# 配置声道数、量化位数和取样频率
w.setnchannels(nchannels)
w.setsampwidth(sampwidth)
w.setframerate(framerate*2) # 采样频率至少是信号频率最高频率的两倍以上才能重新恢复为原来的模拟信号
w.writeframes(frames.tostring()) # 将wav_data转换为二进制数据写入文件
w.close()
f.close()

总结一下

1
2
3
4
5
6
7
8
9
10
我们现在有一堆长度为50整倍数的数据,假设他是长度为150
[1,2,8,4,...77,12,54]无规律,是什么不重要
然后每行100,偏移50生成坐标矩阵
[0,1,2...,98,99]
[50,51,52...148,149]
其中第n行的范围为[(n-1)*50, (n-1)*50+100-1],整数
然后打乱行
[50,51,52...148,149]
[0,1,2...,98,99]
然后把对应坐标的数据填进去降维就是加密后的数据

所以我们只需要把数据读成 numpy.ndarray 在像拼拼图一样把数据按原来的顺序拼好就行啦

解密脚本

其实可以优化一下,这个是暴力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import wave
import numpy as np
import os

f = wave.open(os.path.abspath('./flag-RD.wav'), 'rb')
params = f.getparams()
nchannels, sampwidth, framerate, nframes=params[:4]
str = f.readframes(nframes)
res_data = np.frombuffer(str, dtype=np.short)
res = []

# 由于之前导出wav导致每两个数据间插入了一个0或-1,我们只需要res_data偶数位的数据
for i in range(len(res_data)):
if i % 2 == 0:
res.append(res_data[i])

wave_data = np.array(res)
time = np.arange(0, nframes) * (1.0 / framerate)
wlen = 100
inc = 50
signal_length = len(wave_data)
if signal_length <= wlen:
nf = 1
else:
nf = int(np.ceil((1.0*signal_length-wlen+inc)/inc))
pad_length = int((nf-1)*inc+wlen)
zeros = np.zeros((pad_length-signal_length), dtype=int)
pad_signal = np.concatenate((wave_data,zeros))
frames = pad_signal.reshape(len(pad_signal) // 100, 100) # 将数据砍成每行100个的若干行

def force_cmp(a,b):
if list (a)[:50] == list(b)[-50:]:
return -1 # b在a前
elif list(b)[:50] == list(a)[-50:]:
return 1 # a在b前
else:
return 0 # 无关

seed = list(frames[0]) # 将第一个作为种子开始双向拼图
while True:
print(frames.shape[0])
for i in range(frames.shape[0]):
if force_cmp(np.array(seed), frames[i]) == -1: # 找到已经拼好数据的前一块
tmp = list(frames[i])[:50]
seed = tmp+seed # 把这块拼到已经拼好数据的前面
frames = np.delete(frames,i,0) # 把这块从未拼列表中删除
break
elif force_cmp(np.array(seed), frames[i]) == 1: # 找到已经拼好数据的后一块
tmp = list(frames[i])[-50:]
seed = seed+tmp # 把这块拼到已经拼好数据的后面
frames = np.delete(frames,i,0) # 把这块从未拼列表中删除
break
if frames.shape[0] == 1:
break

w = wave.open(os.path.abspath('./flag-res.wav'), "wb")
w.setnchannels(nchannels)
w.setsampwidth(sampwidth)
w.setframerate(int(framerate/2))
# 由于之前两倍采样,不还原的话会导致时间是原来的一半,音调变高
res_str = np.array(seed)
w.writeframes(res_str.tostring())
# 将排序好的数据变为numpy.array并写进wav文件
w.close()
f.close()

然后用木耳听,细听

可以听到 The flag is 'password'