2026黄河流域CTF(Misc全)
Misc
signin?


给了这样子一个txt文件
随便阅读一下可以发现语义压根没啥内容,题目也说明了“毕竟签到题通常不会真的要求选手精读一大段废话”
让我们看形式本身,猜测就是看标点符号
猜测需要标点进行映射,我们可以尝试一下,按照一般的排列逻辑也是,。;:
, = 0
。 = 1
; = 2
: = 3
只需要每四个四进制转为一个ASCII即可
试了一下发现能解出flag头,继续写得脚本
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 位整数:
0xb47e923c
0x5aeb49a7
0xa3cd7af0
三个数,每个数都是8位十六进制数,很明显是某个算法的内部状态
需要我们联想到ZipCrypto
这是一个很老的传统ZIP加密手段,属于流加密,只要正确的密钥流就能解密了
要注意这边的ZipCrypto只是拿来作为一种加密算法,不是像普通zip加密这样子
普通 ZIP 加密:
ZIP 容器头不加密,只加密里面的文件数据
这题:
把整个 ZIP 文件本身当成一段普通数据,再用 ZipCrypto 算法加密
ZipCrypto是这样子运行的
当加密的时候,不会直接拿密码去加密
而是先用密码生成三个内部状态key0,key1,key2
然后对他们初始化,初始化好之后把密码的每一个字节喂进去,不断更新这三个key
那这题已经直接把最后的三个key都给我们了,我们只需要进行解密就好了
怎么解密呢,分为三步
第一步:生成密钥流字节k
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
p = c ^ k
第三步,更新密钥状态
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各司其职
key0:用明文字节更新 CRC32。
key1:加上 key0的低 8 位,然后乘以常数 134775813(0x8088405)并加 1。
key2:用 key1的高 8 位更新另一个 CRC32
就是以上三步,我们来回做这三步对每一个字节进行解密,最后就能得到一个文件
完整解密脚本如下:
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 或者直接遍历三个通道。
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字节码

python3 pyinstxtractor.py Encrypt.exe
用pyinstxtractor解包一下
解出来很多东西啊
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的,不太好反编译,不过在线工具还是能反编译出一点

from encrypt import encrypt_image
能反编译出这句话就够了,我们接着去找encrypt模块即可
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,可以看到逻辑
Image.open
convert("RGB")
np.array
reshape(-1)
swapped[index], swapped[target] = swapped[target], swapped[index]
即主逻辑是对图片 RGB 三个通道分别做多轮像素交换,本质是 swap 交换
所以可以通过反向遍历来恢复原图
根据3.png 的文件名提示,猜测 rounds=3
加密时大致流程为:
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)
逆过程的时候我们只需要将轮数、像素下标反向执行即可
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)
写得脚本
#!/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:
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 保留
#!/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 检查,发现蓝色通道最低位中隐藏了一段数据。提取脚本如下:
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 像素为高度,将大整数作为纵坐标进行绘制
渲染脚本如下:
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的文件头是这样子写的
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也可以
dd if=flag.png of=hidden.zip bs=1 skip=68588

得到9个txt文件

发现这些文件内容都是 base64 片段。按文件名顺序拼接并解码:
cat hidden/*.txt | base64 -d > clue.png
得到这样子一张图

合并成一句就是:
To the stars we come from
因此继续分析AD ASTRA.wav
一开始可以尝试常见音频隐写方向,比如频谱图、LSB等
但是都没什么收获
进一步检查 WAV 数据区,发现其隐藏了 DeepSound 数据
DeepSound 会把数据藏在 WAV 音频采样的低位中,可以通过解码低 4 bit 检查是否存在 DeepSound 头
检测脚本如下:
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)
运行结果:
data_start = 46
header_hex = 445343320401f54db3d444654ff613ca115add3377e62a17205e
magic = b'DSC2'
mode = 4
encrypted = 1
hash = f54db3d444654ff613ca115add3377e62a17205e
这里的 DSC2 是 DeepSound 的标志、
说明我们的 AD ASTRA.wav 中确实存在 DeepSound 隐写数据,并且是加密状态
使用前面提示图得到的密码:
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
ffprobe -hide_banner -show_format -show_streams "总部发来的讯息.wav"

听一下,这个刺耳朵的声音很像SSTV

频谱图看着也很像,SSTV解一下看看

用虚拟声卡比较好

可以得到这样子一张图片
写了密码packed up and ready
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: 幻影/无影及其杂交坦克编解码工具 · GitHub

对应了这样子一个幻影坦克的类lsb隐写术
大致原理上文也说了,就是一种基于棋盘格幻影坦克图的隐写术
我们看到的png需要把像素分成两类
黑格 / 低亮格:RGB 接近 0
白格 / 高亮格:RGB 接近 255
然后利用RGB离0和255的偏移量来存数据
我们这个图每个通道存2bit
低亮格:
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棋盘化
所以我们肉眼看着像有网纹一样
其实随便看几个像素就很明显
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
B A
A是一层图,B是一层图
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
给他们拆开就好了
那因为这边二维码藏的比较淡,我们也可以调一下灰度,增强对比度(不调也没啥事
#!/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的高级用法可以私信我,用不来()
更多推荐



所有评论(0)