null
文章目录
  1. Checkin
  2. ppd
  3. ReadLongNovel
  4. jpeg and the tree
  5. Spy_Dog
  6. Bleach!
  7. Pixel
  8. Connecting…

MRCTF2022 Writeup

这次的 MRCTF 我受邀参加,并解决了 Misc 系列所有的题,且拿到了 6 个一血。题目质量好评!

Checkin

image-20220425184009822

好久没有看见过这么难的签到了(难是相对其他比赛的签到题而言)

以及怎么感觉自从我在 TQLCTF 出了 Wordle 之后就陆陆续续需要各种队出各种类型的 Wordle,也算是大开眼界了。等有机会整个 urandom Wordle。

浏览器的 Local Storage 可以很清楚地看到答案,所以一次就猜对也没啥难度。

找 flag 倒是找了好久,最后在 app/src/lib/stats.ts 找到了。

image-20220425183354382

ppd

image-20220425184503588

伪造 IP 很容易,直接加 X-Forwarded-For 就行。但尝试了之后发现永远不可能到 100。

和服务器的通信总是会包含一个 enc ,于是仔细研究,发现是经过了 AES 的 CBC 模式加密,且被加密对象是个字符串,其中的 name 是可控的,也就是说我们可以得到任意明文经过加密后的密文。那直接伪造一个 100 的成绩然后发送给服务器就行了。

(这应该是 Web+Crypto 题吧……)

ReadLongNovel

下载小说全文然后搜关键字答题即可。为了保险起见我还每次只新加五个答案。

(虽然有 AI 解法但是 AI 解法也会比手动搜索要麻烦且费时,如果不是为了 AK Misc 的话的确不想做这题……)

jpeg and the tree

个人觉得全场最佳题目,需要手写一个 jpeg 解码器。

这题和 Google CTF 2021 的某道题很相似:DAVID and the Tree,实际上解题思路也很像。

首先 jpeg 里面是有 Huffman Tree 的,用来压缩数据。Google CTF 的题是找不存在的 E 所对应的 01 编码,因为 E 根本没出现在原文,故 Huffman Tree 不可能出现 E 对应的 01 编码。换到 jpeg 就需要去研究 jpeg 是怎么编码的,然后找出 DHT1 这个块里面有哪个 01 编码从头到尾根本没用过,然后就会发现每张图都藏着一个没使用过的 01 编码。

全部打印出来保留二进制末尾 4 位串在一起,然后 bin2str 就能看到 flag 了。

def number(x):
if x[0] == '0':
x = x.replace('0', '.').replace('1', '0').replace('.', '1')
return int(x, 2)

flag = ''
__flag = ''

for id in range(78):
filename = f'pic/{id}.jpg'
with open(filename, 'rb') as f:
d = f.read()
DC, AC = {}, {}

ST = 0x6a
L = int.from_bytes(d[ST-2:ST], 'big')-2
B = d[ST:ST+L][1:][:16]
C = d[ST:ST+L][1:][16:]
v, n = 0, 0
for i in range(1, 16):
v <<= 1
for _ in range(B[i]):
DC[f'{v:b}'.zfill(i+1)] = C[n]
n += 1
v += 1

ST = 0x6a+L+4
L = int.from_bytes(d[ST-2:ST], 'big')-2
B = d[ST:ST+L][1:][:16]
C = d[ST:ST+L][1:][16:]
v, n = 0, 0
for i in range(1, 16):
v <<= 1
for _ in range(B[i]):
AC[f'{v:b}'.zfill(i+1)] = C[n]
n += 1
v += 1

# print(DC, AC)
ST = ST + L + 0xa
bits = ''.join([bin(x)[2:].zfill(8)
for x in d[ST:-2].replace(b'\xFF\x00', b'\xFF')])
st, ed = 0, 0
# print(bits[:100])
G_SET = set()
while True:
# print(ed, len(bits))
while DC.get(bits[st: ed]) is None and ed <= len(bits):
ed += 1
# print('debug', bits[st: ed])
if ed > len(bits):
break
DC_len = DC.get(bits[st: ed])
# print(DC_len, st, ed)
st, ed = ed, ed + DC_len
if DC_len:
G_0_0 = number(bits[st: ed])
st = ed
m = 0
while m < 63:
while AC.get(bits[st: ed]) is None:
ed += 1
# print(AC.get(bits[st: ed]), AC.get(bits[st: ed]) & 0b1111, AC.get(bits[st: ed]) >> 4)
G_SET.add(bits[st: ed])
if AC.get(bits[st: ed]) == 0:
st = ed
break
# 0
m += AC.get(bits[st: ed]) >> 4

# > 0
AC_len = AC.get(bits[st: ed]) & 0b1111
st = ed = ed + AC_len
m += 1
# print(bits[st: st+100])
diff = list(set(list(AC.keys())) - G_SET)
assert len(diff) == 1
assert diff[0][:-4] in ['0', '00', '000', '0000']
__flag += diff[0][-4:]
if len(__flag) == 8:
flag += chr(int(__flag, 2))
__flag = ''
print(flag)

print(flag)

Spy_Dog

不就是 99.9% 嘛,我用最简单的加噪音的方法给你打下来(然后就打下来了)

期待预期解,感觉可能是和 resize 有关的攻击。


import base64
import cv2
import random
import numpy as np
from keras.models import load_model
from copy import deepcopy


model = load_model('simplenn.model')

def checkSkin(img1, img2):
output = []
for i in range(0, len(img1)):
for j in range(0, len(img1[i])):
output.append(img2[i][j]-img1[i][j])
maxnum = 0
for i in output:
num = 0
for j in i:
if j >= 200:
j = 255 - j
num = j
if num >= maxnum:
maxnum = num
index = i
# print(index)
# print(maxnum)
if maxnum > 10:
return 0
else:
return 1

def checkMask(img):
predict = model.predict(img)
return predict[0][1]


origin = cv2.imread('dog.bmp')
origin = np.expand_dims(origin, axis=0)
origin_f = origin.astype(np.float32) / 255.

best_img = cv2.imread('best.bmp')
best_img = np.expand_dims(best_img, axis=0)
best_score = checkMask(best_img.astype(np.float32) / 255.)

def mutation(img):
for _ in range(1):
x = random.randint(0, 127)
y = random.randint(0, 127)
z = random.randint(0, 2)
d = random.randint(-10, 10)
img[0, x, y, z] = origin[0, x, y, z] + d
return img

while best_score <= 0.999:
img = mutation(deepcopy(best_img))
img_f = img.astype(np.float32) / 255.
score = checkMask(img_f)
if score > best_score:
# assert checkSkin(img[0], origin[0]) == 1
best_img = img
best_score = score
print(best_score, score)
cv2.imwrite('best.bmp', best_img[0])


# img = deepcopy(origin)

# while True:
# score = checkSkin(img, cv2.imread('dog.bmp'))
# img = cv2.resize(img, (128, 128))
# img_tensor = np.expand_dims(img, axis=0)
# img_tensor = img_tensor.astype(np.float32)
# img_tensor /= 255.
# score += checkMask(img_tensor)
# print(score)


# from pwn import *
# context(log_level='debug', os='linux')
# r = remote('82.156.190.31', 17271)


# r.sendafter(b'>', b'2')
# r.recvuntil(b'looks like\n')
# img_base64 = r.recvline()
# print(img_base64)

# r.interactive()

Bleach!

A picture in the music is cool?
16bit & 400x400

怎么有人的 IP 正好是 flag 这四个字符的 ASCII 码啊 hhhhh

提取所有和这个 IP 有关的 UDP 数据,从题目描述可以猜测是音视频之类的数据,于是找协议,找到了 RTP 协议。

使用 Wireshark 提取所有的 RTP 数据,拿到一个不知道是什么格式的数据。然后又试了好久才发现是 raw 的音频数据(想想也是,RTP 传的当然不可能是包装过的 MP3 或者 wav 文件)。

使用 Audacity 的导入原始数据功能导入原始数据,注意由题目可知这里的读取方式得设置成带符号 16 位浮点数,获得一个清晰的人声音频!将其导出成 wav 准备进行下一步,也就是「Picture in the music」。

直接说正解的方法:用 uint8 读取 wav 然后提取每一个帧的 LSB,提取总数为 400*400,然后用这么多的 01 数据打印成一张 400*400图,就能看到水印和 flag 了。实际上,直接对 raw 做 LSB 得到的图会更加清晰。

image-20220425191748362

所以最后这步真的是很脑洞……做完闪到腰了

Pixel

通过观察 blue 通道发现每张图都有一些 blue 不为 255 的像素点,且在 512 张图中这种像素点的数量恰好等于一张图的数量且位置都不一样,于是尝试将这些特殊像素点连同 RGB 三个通道的数据都提取出来,得到了一张红色的图:

image-20220425193130070

发现该图 red0 通道有异常,考虑 zig-zag 变换,也就是将像素点重新按照某个顺序排列。注意这里可能是 zig-zag 的逆操作,总之都试一遍准没错。然后得到这个:

image-20220425193311070

中间的空格在暗示说,这题就是 Arnold’s cat map 变换,且重复参数为 1,横纵参数分别为 20 和 22。然后就做一次逆操作就得到最后的 flag 了。

image-20220425193514590

Connecting…

.obj 有个错误的面,找到这个面对应的数据行之后,经过 hex2str 后发现是可以阅读的文字:

image-20220425185329172

.png 的 xml 信息里面有一句 <rdf:li xml:lang="x-default">Thank Fabien Petitcolas For his work.</rdf:li> 十分诡异,经过查找发现 Fabien Petitcolas 写了 MP3Stego,那么可以猜测 .obj 得到的可阅读文字是 MP3Stego 的密码。

sound.wav 解密得到 flag。

做完感觉就是套娃 + 工具题。按照做题的逻辑感觉应该是先看图片后看模型,但 Hint 先给的是模型的提示。以及还以为会涉及到和 3D 模型有关的一些有趣的知识点,但最后并没有,比较遗憾。

支持一下
资助 Nano 让 Nano 吃得更胖!
  • 微信扫一扫
  • 支付宝扫一扫