# MISC
这次的 MISC 是真的佩服 Aura 佬的题目,给跪了,orz

# 派森
题目:
腐乳昂木 奥普瑞特儿 阴坡尔特 艾克斯奥尔
腐乳昂木 提克有第爱慕 阴坡尔特 ⭐
弗拉格 等于 布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉
印刻 等于 左中括号右中括号
佛儿 唉 因 梯软者左括号 零,楞左括号弗拉格右括号,四右括号冒号
印刻。鹅潘德(艾克斯奥尔(奥尔德(弗拉格【唉】),奥尔德(弗拉格【唉加二】)))
印刻。鹅潘德(艾克斯奥尔(奥尔德(弗拉格【唉加一】),奥尔德(弗拉格【唉加三】)))
印刻。鹅潘德(奥尔德(弗拉格【唉加二】))
印刻。鹅潘德(奥尔德(弗拉格【唉加三】))
普瑞因恩特左括号印刻右括号
井号[16, 29, 67, 84, 31, 75, 89, 48, 30, 111, 107, 48, 49, 52, 95, 67, 6, 2, 110, 51, 44, 69, 95, 118, 74, 45, 121, 95, 70, 84, 49, 49, 0, 0, 33, 33, 0, 10, 113, 125]
# 模型的秘密
里面的文件是 model.blend , 这里还特意下了一个 Blender 来打开,不过文件头有问题,直接拖到 Blender 无法打开,需要修改文件头,这里不知道 blend文件 的文件头可以随便保存一个 blend 文件查看即可。


可以看到文件头要修改为 42 4C 45 4E 44 45 52 , 后面即可用 Blender 正常打开

# Picture
给的文件是 1 个代码 + 2 个图片。两个图片就是代码运行的结果,这就是个逆推 Crypto 题,丢给大 G 老师写一个逆推脚本即可
from PIL import Image
def reconstruct_flag(flag1_path, flag2_path, output_path):
image1 = Image.open(flag1_path).convert('RGB')
image2 = Image.open(flag2_path).convert('RGB')
if image1.size != image2.size:
raise ValueError("flag1.png和flag2.png的尺寸不一致。")
width, height = image1.size
reconstructed = Image.new('RGB', (width, height))
for i in range(width):
for j in range(height):
r1, g1, b1 = image1.getpixel((i, j))
r2, g2, b2 = image2.getpixel((i, j))
r = (r1 + r2) % 256
g = (g1 + g2) % 256
b = (b1 + b2) % 256
reconstructed.putpixel((i, j), (r, g, b))
reconstructed.save(output_path)
print(f"成功还原图像并保存为 {output_path}")
# 使用示例
reconstruct_flag('flag1.png', 'flag2.png', 'restored_flag.png')
得到的图片是一个空白图片,第一反应就是 Stegsolve.jar 打开查看各通道

# 外星信号 Ultra
这一题真是被 Aura 折服了,套了好几层,尤其是第一个不同设备查看图片是真的抽象...
压缩包里面给了 3 个东西,给的图片有代码,丢给大 G 老师说是将 wav 文件生成 data,让大 G 写了逆推代码,不过少了关键 seed ,跑不出来,从图片本身下手了。

这个 Apple Devices may see this picture differently 是真的抽象,直接搜索发现几个文章可以看出来是个什么事。
这里看了几个文章了解了一下是怎么回事,后来参考 Ambiguous PNG Packer 作者 Issues 里面给的另一个项目 parallel-png-proposal,让博士(GPT o1-preview)写了一个还原脚本:
import zlib
from PIL import Image
def read_png_chunk(f):
length_bytes = f.read(4)
if len(length_bytes) < 4:
return None, None
length = int.from_bytes(length_bytes, 'big')
chunk_type = f.read(4)
chunk_data = f.read(length)
crc = f.read(4)
return chunk_type, chunk_data
def decompress_headerless(data):
d = zlib.decompressobj(wbits=-15)
result = d.decompress(data)
result += d.flush()
return result
def extract_pixel_data(decompressed_data, width, height):
pixels = []
pos = 0
stride = 1 + width * 3
for y in range(height):
if pos >= len(decompressed_data):
break
filter_type = decompressed_data[pos]
assert filter_type == 0, "Unsupported filter type"
pos +=1
row_data = decompressed_data[pos:pos + width * 3]
pixels.append(row_data)
pos += width *3
return b''.join(pixels)
def main():
with open('output.png', 'rb') as f:
png_magic = f.read(8)
assert png_magic == b'\x89PNG\r\n\x1a\n'
# Initialize variables
width = height = None
a = b''
b_data = b''
idat_chunk_count = 0
while True:
chunk_type, chunk_data = read_png_chunk(f)
if chunk_type is None:
break
if chunk_type == b'IHDR':
width = int.from_bytes(chunk_data[0:4], 'big')
height = int.from_bytes(chunk_data[4:8], 'big')
bit_depth = chunk_data[8]
color_type = chunk_data[9]
compression_method = chunk_data[10]
filter_method = chunk_data[11]
interlace_method = chunk_data[12]
elif chunk_type == b'IDAT':
idat_chunk_count +=1
if idat_chunk_count == 1:
a = chunk_data
elif idat_chunk_count >= 2:
b_data += chunk_data
elif chunk_type == b'IEND':
break
else:
pass
# Check that we have width and height
assert width is not None and height is not None
# For image 1 (world_output.png)
decompressed_a = decompress_headerless(a[2:]) # 跳过 zlib 头部的 2 个字节
decompressed_b = decompress_headerless(b_data[:-4]) # 去除末尾的 adler32 校验和
image1_data = decompressed_a + decompressed_b
image1_pixels = extract_pixel_data(image1_data, width, height)
image1 = Image.frombytes('RGB', (width, height), image1_pixels)
image1.save('world_output.png')
print("Saved world_output.png")
# For image 2 (apple_output.png)
data_ab = a + b_data
image2_data = zlib.decompress(data_ab)
image2_pixels = extract_pixel_data(image2_data, width, height)
image2 = Image.frombytes('RGB', (width, height), image2_pixels)
image2.save('apple_output.png')
print("Saved apple_output.png")
if __name__ == "__main__":
main()
这里放出两张图片的对比:


可以发现多了一个 random seed(496534891) ,这也是逆推回去的关键。
import random
import os
import sys
def decrypt_file(enc_file_path, output_file_path, seed=496534891):
"""
通过对加密文件执行XOR操作,还原原始文件。
:param enc_file_path: 加密后的输入文件路径(例如 'data')
:param output_file_path: 输出的还原文件路径(例如 'deepsea.wav')
:param seed: 用于生成随机数的种子(默认为496534891)
"""
# 检查输入文件是否存在
if not os.path.isfile(enc_file_path):
print(f"错误:加密文件 '{enc_file_path}' 不存在。")
sys.exit(1)
# 设置随机种子
random.seed(seed)
try:
# 读取加密文件的字节数据
with open(enc_file_path, 'rb') as f:
enc_data = f.read()
# 生成与加密时相同的随机数序列,并执行XOR操作
dec_data = bytearray()
for byte in enc_data:
rand_num = random.randint(10, 20)
dec_byte = byte ^ rand_num
dec_data.append(dec_byte)
# 将解密后的数据写入输出文件
with open(output_file_path, 'wb') as f:
f.write(dec_data)
print(f"成功:已还原文件 '{output_file_path}'。")
except Exception as e:
print(f"发生错误:{e}")
sys.exit(1)
def main():
"""
主函数,处理命令行参数并调用解密函数。
"""
import argparse
parser = argparse.ArgumentParser(description="还原被加密的deepsea.wav文件。")
parser.add_argument('-i', '--input', type=str, default='data', help="加密后的输入文件路径(默认: 'data')")
parser.add_argument('-o', '--output', type=str, default='deepsea_restored.wav', help="输出的还原文件路径(默认: 'deepsea_restored.wav')")
parser.add_argument('-s', '--seed', type=int, default=496534891, help="用于生成随机数的种子(默认: 496534891)")
args = parser.parse_args()
decrypt_file(args.input, args.output, args.seed)
if __name__ == "__main__":
main()
还原成功的 deepsea_restored.wav 是可以正常播放的一段音乐,到这里才解决第一步吧... 还原后的 wav 里还藏着东西,先拖到 DeepSound 里面分离一下,发现还有一段音频

分离出来的 sese.wav 以为是摩斯,哪知道解不出来,后来想到题目是外星信号,就回忆了之前 BaseCTF 的一题是无线电解密,搜了相关文章 ——SSTV 音频转图片,跟着步骤来解出了密码

得到的密码 9982443531668 ,解开压缩包得到一个 base(pow(2,11)) ,这里也是上次 0xGame 的题目, pow(2,11)=2048 ,也就是 base2048 解码(base2048 解码网站)。



