2023 China Industrial Internet Security Competition Finals Retrospective

免责声明:本文为个人技术学习与工程实践笔记,所涉操作仅应在获得授权的环境中进行。因不当使用造成的后果由使用者自行承担。

首次遇到场景题全场 0 解,抽到的都是 0 解题,运气确实不太好。

部分环境很糟糕,另外做了一个相册站,欢迎参观:https://photo.iloli.moe,这也是首次参加线下赛事

gigached

下载题目,010 打开发现是一个 mp4 文件,加个后缀发现是 gigached(Can you Feel My Heart~)

img

结合题目猜测是要让我们获取视频的 LSB,ffmpeg 简单分离一下视频帧

1
ffmpeg -i gigached.mp4 -o frames_%4b.png

img

然后写个脚本挨张提取 PNG LSB 通道数据

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
# encoding=utf-8
from PIL import Image
def getlsb(f1, f2):
tmp = Image.open(f1)
w, h = tmp.size
cnt = 0
data = ""
for height in range(0, h):
for weight in range(0, w):
pixel = tmp.getpixel((weight, height))
# 处理三通道(RGB)
if cnt % 3 == 0:
cnt += 1
data += str((int(pixel[0]) % 2))
if cnt % 3 == 1:
cnt += 1
data += str((int(pixel[1]) % 2))
if cnt % 3 == 2:
cnt += 1
data += str((int(pixel[2]) % 2))
with open(f2, "wb") as file:
for i in range(0, len(data), 8):
t = int(data[i: i + 8], 2)
file.write(chr(t))
t = ""
file.close()
for i in range(1, 423):
filename = str("frames_" + str(i).zfill(5) + ".png")
new = filename
output = str("output_" + str(i).zfill(5) + ".txt")
out = output
getlsb(new, out)

结果得到

img

然后挨个查看 txt 文本数据,看了一圈后发现只有前几个文件有 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
./output_00002.txt: binary file matches
./output_00003.txt: binary file matches
./output_00004.txt: binary file matches
./output_00005.txt: binary file matches
./output_00006.txt: binary file matches
./output_00007.txt: binary file matches
./output_00008.txt: binary file matches
./output_00009.txt: binary file matches
./output_00010.txt: binary file matches
./output_00011.txt: binary file matches
./output_00012.txt: binary file matches
./output_00013.txt: binary file matches
./output_00014.txt: binary file matches
./output_00015.txt: binary file matches
./output_00016.txt: binary file matches

整理得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
8:dp6UYHfO
8:NZhyAEYx
8:Js8o3024
8:l+je3k5k
8:aJmsL+Hx
8:w43oQ3oz
8:XjTT3/LQ
8:qF4fuu/w
8:MNqZVqiA
8:Q3vzixxY
8:QYbnuQ==
8: the key
8: isgYpt3
8:GvM7X1A1
3:CoT

根据上文,结合压缩包注释给出的代码逆向推导,即可解得 flag(本题实际上是一套密码管理软件的加密过程,只是相关脚本需要联网检索才能找到)

1
2
3
4
5
6
7
8
9
10
11
# 题目提供的加密脚本
def encrypt(key, source):
source = source.encode()
key = key.encode()
key = SHA256.new(key).digest()
IV = Random.new().read(AES.block_size)
encryptor = AES.new(key, AES.MODE_CBC, IV)
padding = AES.block_size - len(source) % AES.block_size
source += bytes([padding]) * padding
data = IV + encryptor.encrypt(source)
return data

解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def decrypt(key, source):
source = base64.b64decode(source)
key = key.encode() # 转换为字节类型
key = SHA256.new(key).digest()
IV = source[:AES.block_size]
decryptor = AES.new(key, AES.MODE_CBC, IV)
data = decryptor.decrypt(source[AES.block_size:])
padding = data[-1]
if data[-padding:] != bytes([padding]) * padding:
raise ValueError("Invalid padding...")
return data[:-padding].decode()
key = "gYpt3GvM7X1A1CoT"
flag = "dp6UYHfONZhyAEYxJs8o3024l+je3k5kaJmsL+Hxw43oQ3ozXjTT3/LQqF4fuu/wMNqZVqiAQ3vzixxYQYbnuQ=="
print(decrypt(key, flag))

flag{d53dbf36-444a-471d-a663-91a9e6f400e0}

img断网环境下尝试了一下午,难度确实不小,也说明自身知识储备仍有欠缺

DNS_Query

给了流量包,tshark 提取流量,写脚本提取 dns 记录

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
11111110100111100100101111111
10000010011001100001101000001
10111010001011110011101011101
10111010000011000110101011101
10111010010100100011001011101
10000010001011000100101000001
11111110101010101010101111111
00000000101101101001000000000
11011010011100010101101000001
11011101110010011011100111100
01100111001101110110110011000
10101001011110111100110011011
11110011110110110011011100011
10001101010001010000101111001
01101011111111110001100100101
11001101111100111010011110101
10001110100001111001110001001
11100000111110111111010110000
11011011100101110111011110001
11011101101011100000000000101
11110011000010100101111111101
00000000101001011011100010100
11111110001010010101101011100
10000010010000100100100010000
10111010111001110011111110010
10111010111010110111100101001
10111010010000110001000010011
10000010101011111001011111101
11111110110110010000100010000

然后写脚本还原

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PIL import Image
from zlib import *
MAX = 29
pic = Image.new("RGB",(MAX,MAX))
str ="1111111010011110010010111111110000010011001100001101000001101110100010111100111010111011011101000001100011010101110110111010010100100011001011101100000100010110001001010000011111111010101010101010111111100000000101101101001000000000110110100111000101011010000011101110111001001101110011110001100111001101110110110011000101010010111101111001100110111111001111011011001101110001110001101010001010000101111001011010111111111100011001001011100110111110011101001111010110001110100001111001110001001111000001111101111110101100001101101110010111011101111000111011101101011100000000000101111100110000101001011111111010000000010100101101110001010011111110001010010101101011100100000100100001001001000100001011101011100111001111111001010111010111010110111100101001101110100100001100010000100111000001010101111100101111110111111110110110010000100010000"
i=0
for y in range(0,MAX):
for x in range(0,MAX):
if(str[i] == '1'):
pic.putpixel([x,y],(0,0,0))
else:pic.putpixel([x,y],(255,255,255))
i = i+1
pic.show()
pic.save("flag.png")

img

其余题目待后续补充。

防御启示

  1. 媒体文件是隐写的常见载体:本题把信息藏在视频帧的像素最低有效位中。业务侧对上传的图片、视频应做重编码与元数据清洗,并对 LSB 平面做统计分析,避免隐写内容随业务数据对外发布。
  2. 口令派生不应只做一次哈希:给出的加密实现直接把 SHA256(口令) 当作 AES 密钥,没有盐值与迭代次数,低熵口令可被离线快速爆破。应改用 PBKDF2、bcrypt 或 Argon2 派生密钥,并为每个用户使用独立盐值。
  3. 解密失败必须统一响应:给出的实现会对 padding 校验失败抛出区分性异常,若出现在服务端,这类可区分的错误响应与耗时差异会形成类似 padding oracle 的侧信道。错误提示、状态码与处理耗时都应统一。
  4. CBC 模式需要额外的完整性保护:AES-CBC 只提供机密性,不防篡改。新系统应优先采用带认证的 AES-GCM,或对密文附加 HMAC 校验,并保证 IV 随机且不复用。
  5. DNS 是常见的隐蔽外传通道:对边界 DNS 应做审计与限制——关注异常长度的子域、短时间内同一父域下大量随机标签、非常规记录类型,并禁止终端直连外部递归解析。

Support via Solana

Solana

Solana

Solana Pay

Solana Pay

WeChat

WeChat