Misc signin?
给了这样子一个txt文件
随便阅读一下可以发现语义压根没啥内容,题目也说明了“毕竟签到题通常不会真的要求选手精读一大段废话”
让我们看形式本身,猜测就是看标点符号
猜测需要标点进行映射,我们可以尝试一下,按照一般的排列逻辑也是,。;:
只需要每四个四进制转为一个ASCII即可
试了一下发现能解出flag头,继续写得脚本
1 2 3 4 5 6 7 8 9 import sys text = open(sys.argv[1] if len(sys.argv) > 1 else "ai_reply.txt", encoding="utf-8").read() mp = {",": "0", "。": "1", ";": "2", ":": "3"} s = "".join(mp[c] for c in text if c in mp) out = "".join(chr(int(s[i:i+4], 4)) for i in range(0, len(s) - 3, 4)) print(out)
sdpcsec{welcome_2026_4nd_competiton!}
Cake
打开来俩文件
cake_knife.txt就只有这样子三行十六进制数
另一个cake_base.bin放到Winhex看看,发现啥都看不出
猜测需要那个knife去处理这个蛋糕的base
所以这题的关键点落在了 cake_knife.txt 中的三个 32 位整数:
1 2 3 0xb47e923c 0x5aeb49a7 0xa3cd7af0
三个数,每个数都是8位十六进制数,很明显是某个算法的内部状态
需要我们联想到ZipCrypto
这是一个很老的传统ZIP加密手段,属于流加密,只要正确的密钥流就能解密了
要注意这边的ZipCrypto只是拿来作为一种加密算法,不是像普通zip加密这样子
1 2 3 4 5 普通 ZIP 加密: ZIP 容器头不加密,只加密里面的文件数据 这题: 把整个 ZIP 文件本身当成一段普通数据,再用 ZipCrypto 算法加密
ZipCrypto是这样子运行的
当加密的时候,不会直接拿密码去加密
而是先用密码生成三个内部状态key0,key1,key2
然后对他们初始化,初始化好之后把密码的每一个字节喂进去,不断更新这三个key
那这题已经直接把最后的三个key都给我们了,我们只需要进行解密就好了
怎么解密呢,分为三步
第一步:生成密钥流字节k
1 2 3 def decrypt_byte(key2): temp = (key2 | 2) & 0xffffffff # 确保最低位为 0,次低位为 1 return ((temp * (temp ^ 1)) >> 8) & 0xff
key2 | 2,保证temp是偶数,然后计算temp * (temp ^ 1),右移 8 位,取低 8 位作为密钥流字节 k
第二步:解密得到明文字节p
第三步,更新密钥状态
1 2 3 4 5 6 def update_keys(key0, key1, key2, plain): key0 = crc32_update(key0, plain) # 更新 CRC32 key1 = (key1 + (key0 & 0xff)) & 0xffffffff key1 = (key1 * 134775813 + 1) & 0xffffffff # 线性同余 key2 = crc32_update(key2, (key1 >> 24) & 0xff) return key0, key1, key2
这边三个key各司其职
1 2 3 key0:用明文字节更新 CRC32。 key1:加上 key0的低 8 位,然后乘以常数 134775813(0x8088405)并加 1。 key2:用 key1的高 8 位更新另一个 CRC32
就是以上三步,我们来回做这三步对每一个字节进行解密,最后就能得到一个文件
完整解密脚本如下:
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 import zlib import zipfile import os import cv2 KEY0 = 0xb47e923c KEY1 = 0x5aeb49a7 KEY2 = 0xa3cd7af0 def crc32_update(old_crc, c): return (zlib.crc32(bytes([c]), old_crc ^ 0xffffffff) ^ 0xffffffff) & 0xffffffff def decrypt_byte(key2): temp = (key2 | 2) & 0xffffffff return ((temp * (temp ^ 1)) >> 8) & 0xff def update_keys(key0, key1, key2, plain): key0 = crc32_update(key0, plain) key1 = (key1 + (key0 & 0xff)) & 0xffffffff key1 = (key1 * 134775813 + 1) & 0xffffffff key2 = crc32_update(key2, (key1 >> 24) & 0xff) return key0, key1, key2 def decrypt_zipcrypto(data, key0, key1, key2): out = bytearray() for c in data: k = decrypt_byte(key2) p = c ^ k out.append(p) key0, key1, key2 = update_keys(key0, key1, key2, p) return bytes(out) with open("cake_base.bin", "rb") as f: enc = f.read() dec = decrypt_zipcrypto(enc, KEY0, KEY1, KEY2) print(dec[:4]) with open("cake_decrypted.zip", "wb") as f: f.write(dec) os.makedirs("out", exist_ok=True) with zipfile.ZipFile("cake_decrypted.zip", "r") as z: z.extractall("out") print(os.listdir("out"))
运行后就能解密这个cake的base,发现文件头是一个zip,解压即可
解压得到这样子三个文件:
接着就是对这三个文件进行探秘了
这边我不知道为什么上边俩文件到最后都没用到,不知道怎么用(),txt里边可能是哈希吧,但是我爆破不出来
而flag的提取也只和这个avi文件有关
发现有一些像素逐帧发生了改变,但是不确定读哪一个坐标,所以进行了遍历
我们直接遍历所有坐标,然后检查 RGB 三个通道中是否存在可打印 ASCII 字符
需要注意,OpenCV 读取图片时默认通道顺序是 BGR,因此脚本中需要转成 RGB 或者直接遍历三个通道。
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 import cv2 cap = cv2.VideoCapture("scream.avi") frames = [] while True: ret, frame = cap.read() if not ret: break frames.append(frame) cap.release() h, w = frames[0].shape[:2] for y in range(h): for x in range(w): s = "" for frame in frames: b, g, r = frame[y, x] # OpenCV 默认是 BGR,这里按 RGB 顺序检查 for v in [r, g, b]: v = int(v) if 32 <= v <= 126: s += chr(v) if "flag{" in s: print(x, y, s)
运行后发现了在(123,45)的地方有,flag是flag{W0w_d3lici0us_c4ke!!}
encrypt
上来给了俩文件,一个exe一个png,怀疑是exe对png做了什么处理,我们先分析exe
先塞到DIE看看
不难发现是PyInstaller打包的Python程序,我们尝试提取Python字节码
1 python3 pyinstxtractor.py Encrypt.exe
用pyinstxtractor解包一下
解出来很多东西啊
1 2 3 4 5 pyiboot / pyimod / pyi_rth → PyInstaller 运行时 PIL / numpy / dll / pyd → 第三方库和运行环境 struct.pyc → Python 标准库 srpm_cli.pyc → 很像题目自己的入口文件 PYZ.pyz → 真正 Python 模块压缩包
主要还是看srpm_cli.pyc和PYZ.pyz
srpm_cli.pyc由于python版本有点高,是python3.13的,不太好反编译,不过在线工具还是能反编译出一点
1 from encrypt import encrypt_image
能反编译出这句话就够了,我们接着去找encrypt模块即可
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 import sys import re import dis import types sys.path.insert(0, ".") from pyimod01_archive import ZlibArchiveReader z = ZlibArchiveReader("PYZ.pyz", 0) for name in z.toc: if re.search(r"encrypt|srpm|image", name, re.I): print("[MODULE]", name) for modname in ["encrypt", "srpm_utils"]: print("=" * 80) print("MODULE:", modname) code = z.extract(modname) for obj in code.co_consts: if isinstance(obj, types.CodeType): print("\n--- FUNCTION:", obj.co_name, "---") print("varnames:", obj.co_varnames) print("names:", obj.co_names) dis.dis(obj)
分析PYZ.pyz,可以看到逻辑
1 2 3 4 5 Image.open convert("RGB") np.array reshape(-1) swapped[index], swapped[target] = swapped[target], swapped[index]
即主逻辑是对图片 RGB 三个通道分别做多轮像素交换,本质是 swap 交换
所以可以通过反向遍历来恢复原图
根据3.png 的文件名提示,猜测 rounds=3
加密时大致流程为:
1 2 3 4 5 for r in range(rounds): for c in range(3): for i in range(n): target = (i * i + (2 * r + 3) * i + 7 * (c + 1)) % n swap(i, target)
逆过程的时候我们只需要将轮数、像素下标反向执行即可
1 2 3 4 5 for r in range(rounds - 1, -1, -1): for c in range(3): for i in range(n - 1, -1, -1): target = (i * i + (2 * r + 3) * i + 7 * (c + 1)) % n swap(i, target)
写得脚本
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 #!/usr/bin/env python3 from PIL import Image import numpy as np import sys def undo_one_round(flat: np.ndarray, round_index: int, channel_index: int) -> None: n = flat.size a = 2 * round_index + 3 b = 7 * (channel_index + 1) # inverse of: for i in range(n): swap(i, target(i)) for i in range(n - 1, -1, -1): j = (i * i + a * i + b) % n flat[i], flat[j] = flat[j], flat[i] def decrypt(input_png: str, output_png: str, rounds: int) -> None: arr = np.array(Image.open(input_png).convert('RGB')) h, w, _ = arr.shape out = np.empty_like(arr) for c in range(3): flat = arr[:, :, c].reshape(-1).copy() for r in range(rounds - 1, -1, -1): undo_one_round(flat, r, c) out[:, :, c] = flat.reshape(h, w) Image.fromarray(out, 'RGB').save(output_png) if __name__ == '__main__': if len(sys.argv) != 4: print(f'Usage: {sys.argv[0]} <encrypted.png> <output.png> <rounds>') sys.exit(1) decrypt(sys.argv[1], sys.argv[2], int(sys.argv[3]))
成功解得下图
明显flag这个图片有关,但是尝试了经典隐写都没有成果
这边想到由于题目先通过像素置换隐藏图片,说明还原后的图可能还藏有频域信息
因此我们可以继续尝试傅里叶变换分析
对还原后的图片做灰度处理,然后进行 FFT:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from PIL import Image import numpy as np import matplotlib.pyplot as plt img = Image.open("dec3.png").convert("L") arr = np.array(img) f = np.fft.fft2(arr) fshift = np.fft.fftshift(f) spectrum = np.log(np.abs(fshift) + 1) plt.figure(figsize=(10, 10)) plt.imshow(spectrum, cmap="gray") plt.axis("off") plt.savefig("fft_result.png", bbox_inches="tight", pad_inches=0)
运行后我们得到了文件fft_result.png,可以在频谱图边缘发现隐藏文字:
所以flag是flag{Wei_Hai_journey}
鲨士比亚王国的金融危机
上来就是这样子两张图
这边的flag.png 是 889×889,观察一下可以发现明显是像素螺旋打乱,也符合题目的漩涡
而SCB.png 是 2587×307
发现很巧啊,SCB.png中白色像素数量刚好889 × 889 ,和我们flag.png一样
所以猜测我们可以先对 flag.png 做反螺旋恢复,然后再把这些像素替换掉SCB.png的白色像素,黑色的 SCB 保留
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 #!/usr/bin/env python3 from pathlib import Path from zipfile import ZipFile from PIL import Image import numpy as np flag = np.array(Image.open('flag.png').convert('RGB')) scb = np.array(Image.open('SCB.png').convert('RGB')) n = flag.shape[0] assert flag.shape == (n, n, 3) assert scb.shape[0] * scb.shape[1] >= n * n def spiral_indices(n: int): top, left = 0, 0 bottom, right = n - 1, n - 1 out = [] while top <= bottom and left <= right: for x in range(left, right + 1): out.append((top, x)) for y in range(top + 1, bottom + 1): out.append((y, right)) if top < bottom: for x in range(right - 1, left - 1, -1): out.append((bottom, x)) if left < right: for y in range(bottom - 1, top, -1): out.append((y, left)) top += 1 bottom -= 1 left += 1 right -= 1 return out path = [(y, n - 1 - x) for y, x in spiral_indices(n)][::-1] coin = np.empty_like(flag) coin.reshape(-1, 3)[:] = flag[[y for y, x in path], [x for y, x in path]] Image.fromarray(coin).save('SCB_coin.png') white = np.all(scb == 255, axis=2) assert int(white.sum()) == n * n res = np.full_like(scb, 255) res[~white] = [0, 0, 0] res[white] = coin.reshape(-1, 3) Image.fromarray(res).save('revealed.png') r, g, b = res[:, :, 0].astype(int), res[:, :, 1].astype(int), res[:, :, 2].astype(int) green = (g - r > 20) & (g - b > 20) & (g < 250) mask = np.full_like(res, 255) mask[green] = [0, 0, 0] Image.fromarray(mask).save('revealed_green_mask.png') print(f'wrote: {"SCB_coin.png"}') print(f'wrote: {"revealed.png"}') print(f'wrote: {"revealed_green_mask.png"}')
成功还原出硬币
填到刚刚的文件里
得到flag——flag{You_saved_SCB_by_the_coin}
Upper Tower
上来只有两张照片,但是题目还有提示,题目暗示了Tupper,而文本的寂静暗示了SilentEye
上来先分析一下1.png,考虑图片隐写
对 PNG 的 RGB 通道做 LSB 检查,发现蓝色通道最低位中隐藏了一段数据。提取脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from PIL import Image import numpy as np img = np.array(Image.open("1.png").convert("RGBA")) # 取蓝色通道 LSB bits = (img[:, :, 2] & 1).ravel() out = bytearray() for i in range(0, len(bits) // 8 * 8, 8): v = 0 for b in bits[i:i + 8]: v = (v << 1) | int(b) out.append(v) data = bytes(out) # 前 4 字节为后续十进制字符串长度 ln_bits = int.from_bytes(data[:4], "big") digits = data[4:4 + ln_bits // 8].decode() print(digits[:100]) print(len(digits))
可以得到一串非常大的十进制整数。
接着就是Tupper了,利用Tupper公式还原密码
Tupper塔珀自指公式通常以 17 像素为高度,将大整数作为纵坐标进行绘制
渲染脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from PIL import Image import numpy as np k = int(digits) h = 17 w = (k.bit_length() + 16) // 17 arr = np.zeros((h, w), dtype=np.uint8) for x in range(w): for y in range(h): bit = ((k + y) // 17 >> (17 * x + y)) & 1 arr[h - 1 - y, x] = 255 if bit else 0 Image.fromarray(arr, "L").resize( (w * 10, h * 10), Image.Resampling.NEAREST ).save("tupper.png")
我们成功得到了Tupper公式绘图的成果
生成的图片中可以看到密码是4thHHLY
所以现在我们手里有一串密码,还有一个2.jpg,可以想一些加密手段
结合题目描述中的“寂静”,不难想到应该对应的是SilentEye
得到flag
AD ASTRA
上来就一个wav和一个png文件
先看看这个flag.png,先看看文件类型file flag.png
发现 flag.png 并不是真正的 PNG,而是一个Webp文件
我们知道Webp的文件头是这样子写的
1 2 3 52 49 46 46 -> "RIFF" xx xx xx xx -> RIFF size,小端序 57 45 42 50 -> "WEBP"
所以size是 0x10be4(注意小端序啊)
所以正常的Webp结束位置应该是 8 + 0x10be4 = 0x10be8
但是发现这边结束后还有追加数据
明显是一个zip啊
提取一下追加的,用什么其实都可以,正好今天群里在聊这个,foremost也行,binwalk也行,直接dd if也可以
1 dd if=flag.png of=hidden.zip bs=1 skip=68588
得到9个txt文件
发现这些文件内容都是 base64 片段。按文件名顺序拼接并解码:
1 cat hidden/*.txt | base64 -d > clue.png
得到这样子一张图
合并成一句就是:
1 To the stars we come from
因此继续分析AD ASTRA.wav
一开始可以尝试常见音频隐写方向,比如频谱图、LSB等
但是都没什么收获
进一步检查 WAV 数据区,发现其隐藏了 DeepSound 数据
DeepSound 会把数据藏在 WAV 音频采样的低位中,可以通过解码低 4 bit 检查是否存在 DeepSound 头
检测脚本如下:
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 import struct path = "AD ASTRA.wav" data = open(path, "rb").read() pos = 12 data_start = None while pos + 8 <= len(data): cid = data[pos:pos+4] size = struct.unpack("<I", data[pos+4:pos+8])[0] if cid == b"data": data_start = pos + 8 break pos += 8 + size + (size & 1) if data_start is None: raise SystemExit("no data chunk") def decode_normal(buf): out = bytearray() for i in range(0, len(buf) // 4 * 4, 4): out.append(((buf[i] & 0x0f) << 4) | (buf[i+2] & 0x0f)) return bytes(out) ds = decode_normal(data[data_start:data_start + 104]) print("data_start =", data_start) print("header_hex =", ds[:26].hex()) print("magic =", ds[:4]) print("mode =", ds[4]) print("encrypted =", ds[5]) print("hash =", ds[6:26].hex()) full = decode_normal(data[data_start:]) open("deepsound_stream.bin", "wb").write(full)
运行结果:
1 2 3 4 5 6 data_start = 46 header_hex = 445343320401f54db3d444654ff613ca115add3377e62a17205e magic = b'DSC2' mode = 4 encrypted = 1 hash = f54db3d444654ff613ca115add3377e62a17205e
这里的 DSC2 是 DeepSound 的标志、
说明我们的 AD ASTRA.wav 中确实存在 DeepSound 隐写数据,并且是加密状态
使用前面提示图得到的密码:
1 To the stars we come from
用 DeepSound 打开 AD ASTRA.wav,输入该密码,可以成功提取出下一层文件:
接下来这个加密的docx毫无头绪,最后我直接爆破了,不知道是不是非预期
直接hashcat爆破哈希值得到docx的密码是skadi2530(是斯卡蒂啊
(浊蒂真好看,虽然不是深海队的吃不到buff)
图片挪开就能得到flag了
sdpcsec{AD_ASTRA_INFINITUM}
Do you know RA2?
比赛没做出来,赛后Alex教的,谢谢Alex
打开来三个文件,我们一个个看
先看这个总部发来的讯息.wav
1 ffprobe -hide_banner -show_format -show_streams "总部发来的讯息.wav"
听一下,这个刺耳朵的声音很像SSTV
频谱图看着也很像,SSTV解一下看看
用虚拟声卡比较好
可以得到这样子一张图片
写了密码packed up and ready
1 2 3 4 5 6 7 from collections import Counter from math import log2 data = open("盟军战车工厂", "rb").read() cnt = Counter(data) entropy = -sum((n/len(data)) * log2(n/len(data)) for n in cnt.values()) print(entropy)
中间那个文件,算熵,发现接近8,高熵且13.0mb,像是vc容器
用刚刚的密码成功加载
没什么文件,就一个png
看着有点怪,猜测有隐写,但是想不到是什么隐写,这个时候就想到读一下最后一个txt文件
发现这边提到了一个新型坦克-幻影坦克
GitHub - TankFactory/Mirage_Cloak: 幻影/无影及其杂交坦克编解码工具
对应了这样子一个幻影坦克的类lsb隐写术
大致原理上文也说了,就是一种基于棋盘格幻影坦克图的隐写术
我们看到的png需要把像素分成两类
1 2 黑格 / 低亮格:RGB 接近 0 白格 / 高亮格:RGB 接近 255
然后利用RGB离0和255的偏移量来存数据
我们这个图每个通道存2bit
1 2 3 4 5 6 7 8 9 10 11 低亮格: 0 附近 -> 00 2t 附近 -> 01 4t 附近 -> 10 6t 附近 -> 11 高亮格: 255 附近 -> 00 255-2t 附近 -> 01 255-4t 附近 -> 10 255-6t 附近 -> 11
t是threshold,我们图的t是8
利用这个项目就可以进行解密得到这样子一个31B的文件
这就是容器的隐藏卷密码了
这次挂载得到了flag.png
又是一张浊蒂的照片,我怀疑和上一题同一个出题人,浊蒂真好看
不难发现照片右下角藏着一个二维码,但是怎么扫都扫不出,发现是又做了一层2x2棋盘化
所以我们肉眼看着像有网纹一样
其实随便看几个像素就很明显
1 2 3 4 5 6 0: (194, 211, 216, 255) 1: (22, 25, 25, 255) 2: (196, 213, 218, 255) 3: (24, 25, 25, 255) 4: (195, 215, 217, 255) 5: (24, 25, 25, 255)
可以看见是亮暗交错的
原理就是把图片切成2x2小格
A是一层图,B是一层图
1 2 3 4 hidden = ( arr[0:2*H:2, 1:2*W:2].astype(np.uint16) + arr[1:2*H:2, 0:2*W:2].astype(np.uint16) ) // 2
给他们拆开就好了
那因为这边二维码藏的比较淡,我们也可以调一下灰度,增强对比度(不调也没啥事
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 #!/usr/bin/env python3 # -*- coding: utf-8 -*- from PIL import Image, ImageOps import numpy as np import sys def main(): if len(sys.argv) < 2: print("Usage: python3 extract_flag_from_png.py flag.png") raise SystemExit(1) in_path = sys.argv[1] img = Image.open(in_path).convert("RGB") arr = np.array(img) h, w, _ = arr.shape H, W = h // 2, w // 2 # 提取棋盘格中的隐藏层 hidden = ( arr[0:2*H:2, 1:2*W:2].astype(np.uint16) + arr[1:2*H:2, 0:2*W:2].astype(np.uint16) ) // 2 hidden = hidden.astype(np.uint8) hidden_img = Image.fromarray(hidden) # 灰度化 + 自动对比度 gray = ImageOps.autocontrast(ImageOps.grayscale(hidden_img)) gray.save("hidden_layer.png") # 裁剪右下角二维码 qr = gray.crop((800, 470, 920, 570)) qr = ImageOps.autocontrast(qr) qr = qr.resize((960, 800), Image.Resampling.NEAREST) # 二值化 + 反色 qr_bw = qr.point(lambda p: 255 if p > 140 else 0) qr_out = ImageOps.invert(qr_bw) qr_out.save("extracted_qr.png") print("[+] saved hidden layer: hidden_layer.png") print("[+] saved QR image: extracted_qr.png") try: import cv2 data, points, _ = cv2.QRCodeDetector().detectAndDecode( np.array(qr_out.convert("RGB")) ) print("[+] QR content:", data) except Exception: print("[!] OpenCV unavailable, scan extracted_qr.png manually") if __name__ == "__main__": main()
得到flag:sdpcsec{Focusing_light_energy}
问卷
做好即可,感谢出题人的付出
SDPCSEC{SE#_Y@U_NE7T_Y2AR!!!}
LLM问卷
没开玩笑,我真全用的网页,花了0元,如果谁愿意教我ai的高级用法可以私信我,老了用不来()