2026黄河流域 公安院校网络安全技能挑战赛 misc writeup
signin-ZLSk4ever
稍微有点脑洞了感觉,但比较好解
把文字部分都删掉只留下标点符号

将这四种标点映射为二进制的 00 01 10 11
每四个标点组成一个字节,转 ASCII 后得到 flag
from pathlib import Path
text = Path("ai_reply.txt").read_text(encoding="utf-8")
mapping = {",": "00", "。": "01", ";": "10", ":": "11"}
bits = "".join(mapping[ch] for ch in text if ch in mapping)
flag = "".join(chr(int(bits[i:i+8], 2)) for i in range(0, len(bits), 8))
print(flag)
AD ASTRA-ZLSk4ever
foremost提取flag.png得到一个压缩包 解压里面有9段base64,
拼一块解码出图片 根据星空深处的声音,可知用deepsound,
且图片上写的To the stars we come from是deepsound的密码
docx有密码,采用john提取hash后换hashcat用rockyou爆破,得到密码skadi2520

挪开图片就可以看到隐藏文字的flag了
Upper Tower-ZLSk4ever
对1.png,观察到b0通道有LSB隐写痕迹,提取得到:
1025658991124355712810934889700140919747617421003079315985624061054296140009382465857058434674074275120892336766575969588338976341017367264157852190739650926220934210231417469330039947068382806764673001051221448447380001720482620813085941827875644858794111714812288537472795099949264885342147007039538061069523051452567281974840302631700453742804355540730431759203779622365023345254780691947483808187557231093625244295437363124650609010561963221808334829801615614695629099613864198462854382911806884707317102998488625789222163237260888078730026407441064787968
超大十进制数而且可被17整除,判断是塔珀自指公式
解出key 4thHHLY

由题目中寂静中的真相可想到Silenteye
用key解码得到flag

Do you know RA2?-ZLSk4ever
第一层SSTV
解码出来图片上有密码packed up and ready

第二层在vc外层卷,幻影坦克隐写
使用幻影坦克隐写工具或从 RGBA 字节流中按阈值提取 2 bit pair,并校验奇偶校验位
得到隐藏卷密码Nobody's here but that's trees!
第三层vc隐藏卷里的图片是光棱坦克

拉高图片曝光,给左下角截出来反色二维码就能扫了,扫码得到flag
幻影坦克隐写工具:mtcloak.uyanide.com
Cake-Camellia
最开始是有zip文件的加密数据部分和三段密钥,看到bin文件最后有信息,是个网址,前面有前缀提示要删掉
删掉多余部分后pkzip还原zip文件内容
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ==================== 已知三段密钥 ====================
key0 = 0xb47e923c
key1 = 0x5aeb49a7
key2 = 0xa3cd7af0
# ==================== CRC32 表 ====================
crc_table = [0]*256
for i in range(256):
c = i
for _ in range(8):
if c & 1:
c = (0xEDB88320 ^ (c >> 1)) & 0xFFFFFFFF
else:
c = (c >> 1) & 0xFFFFFFFF
crc_table[i] = c
# ==================== CRC32 更新函数 ====================
def crc32(old_crc, c):
return (crc_table[(old_crc ^ c) & 0xFF] ^ (old_crc >> 8)) & 0xFFFFFFFF
# ==================== 更新密钥 ====================
def update_keys(p):
global key0, key1, key2
key0 = crc32(key0, p)
key1 = (key1 + (key0 & 0xFF)) & 0xFFFFFFFF
key1 = (key1 * 134775813 + 1) & 0xFFFFFFFF
key2 = crc32(key2, (key1 >> 24) & 0xFF)
# ==================== 解密字节 ====================
def decrypt_byte():
temp = (key2 | 3) & 0xFFFFFFFF
return ((temp * (temp ^ 1)) >> 8) & 0xFF
# ==================== 解密函数 ====================
def decrypt(ciphertext):
plain = bytearray()
for b in ciphertext:
k = decrypt_byte()
p = b ^ k
update_keys(p)
plain.append(p)
return plain
# ==================== 主程序 ====================
def main():
infile = "final_encrypted.bin"
outfile = "decrypted.zip"
with open(infile, "rb") as f:
encrypted = f.read()
print(f"[+] 已读取 {len(encrypted)} 字节")
# 解密
plain = decrypt(encrypted)
with open(outfile, "wb") as f:
f.write(plain)
print(f"[+] 解密完成 -> {outfile} (请自行分析 ZIP 内容)")
if __name__=="__main__":
main()
得到fruit.bin,instruction.txt,scream.avi,fruit.bin其实是个vc容器(只有加密数据的部分,没有header),instruction.txt内容长度为 64 字节,也就是 128 位十六进制字符,符合 VeraCrypt AES-XTS 数据区 master key 的长度。
前 32 字节:AES-XTS data key
后 32 字节:AES-XTS tweak key
#!/usr/bin/env python3
from pathlib import Path
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
SECTOR_SIZE = 512
START_SECTOR = 256
def load_master_key(txt_path):
text = Path(txt_path).read_text().strip()
if ":" in text:
key_hex = text.split(":", 1)[1].strip()
else:
key_hex = text.strip()
key = bytes.fromhex(key_hex
if len(key) != 64:
raise ValueError(f"invalid master key length: {len(key)} bytes")
return key
def decrypt_sector(ciphertext, key, sector_no):
tweak = sector_no.to_bytes(16, "little")
cipher = Cipher(
algorithms.AES(key),
modes.XTS(tweak)
)
dec = cipher.decryptor()
return dec.update(ciphertext) + dec.finalize()
def main():
key = load_master_key("passwd.txt")
data = Path("fruit.bin").read_bytes()
enc = data[0x20000:0x2e0000]
out = bytearray()
for i in range(0, len(enc), SECTOR_SIZE):
sector = enc[i:i + SECTOR_SIZE]
sector_no = START_SECTOR + i // SECTOR_SIZE
out += decrypt_sector(sector, key, sector_no)
Path("fruit_decrypted.img").write_bytes(out)
print("[+] decrypted -> fruit_decrypted.img")
if __name__ == "__main__":
main()
此时 fruit_decrypted.img 已经是解密后的裸磁盘镜像,不再是 VeraCrypt 容器,可直接得到txt文件,发现是一串数字,之前的网站就是数字转换的,输入后得到了一个坐标(123,45)
继续看视频这个位置的像素,发现一直有一个通道有数据,转成字符得到flag
#!/usr/bin/env python3
import argparse
import csv
import sys
from pathlib import Path
import cv2
def parse_args():
parser = argparse.ArgumentParser(
description="Extract one pixel's color values from every frame of a video."
)
parser.add_argument("video", help="Path to the input video file")
parser.add_argument("x", type=int, help="Pixel X coordinate")
parser.add_argument("y", type=int, help="Pixel Y coordinate")
parser.add_argument(
"--csv",
dest="csv_path",
help="Optional CSV output path. Columns: frame_index,b,g,r,rgb_r,rgb_g,rgb_b",
)
parser.add_argument(
"--limit",
type=int,
default=None,
help="Optional max number of frames to process",
)
parser.add_argument(
"--mode",
choices=["both", "bgr", "rgb"],
default="both",
help="Which channel order to print",
)
parser.add_argument(
"--quiet",
action="store_true",
help="Suppress per-frame stdout output",
)
return parser.parse_args()
def format_line(frame_index, x, y, b, g, r, mode):
if mode == "bgr":
return f"frame={frame_index} xy=({x},{y}) bgr=({b},{g},{r})"
if mode == "rgb":
return f"frame={frame_index} xy=({x},{y}) rgb=({r},{g},{b})"
return (
f"frame={frame_index} xy=({x},{y}) "
f"bgr=({b},{g},{r}) rgb=({r},{g},{b})"
)
def main():
args = parse_args()
video_path = Path(args.video)
if not video_path.is_file():
print(f"Video not found: {video_path}", file=sys.stderr)
return 1
cap = cv2.VideoCapture(str(video_path))
if not cap.isOpened():
print(f"Failed to open video: {video_path}", file=sys.stderr)
return 1
csv_file = None
writer = None
if args.csv_path:
csv_file = open(args.csv_path, "w", newline="", encoding="utf-8")
writer = csv.writer(csv_file)
writer.writerow(["frame_index", "b", "g", "r", "rgb_r", "rgb_g", "rgb_b"])
frame_index = 0
try:
while True:
ret, frame = cap.read()
if not ret:
break
height, width = frame.shape[:2]
if not (0 <= args.x < width and 0 <= args.y < height):
print(
f"Coordinate out of bounds for frame size {width}x{height}: ({args.x}, {args.y})",
file=sys.stderr,
)
return 1
b, g, r = frame[args.y, args.x]
b, g, r = int(b), int(g), int(r)
if not args.quiet:
print(format_line(frame_index, args.x, args.y, b, g, r, args.mode))
if writer is not None:
writer.writerow([frame_index, b, g, r, r, g, b])
frame_index += 1
if args.limit is not None and frame_index >= args.limit:
break
finally:
cap.release()
if csv_file is not None:
csv_file.close()
print(f"Processed {frame_index} frame(s).")
if args.csv_path:
print(f"CSV written to: {args.csv_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
没想到ai直接就定位到视频的那个像素点了,被非预期了........
鲨士比亚王国的金融危机-Camellia
先是一个很明显的螺旋加密,从中心点顺时针提取像素然后重新排列得到原图
螺旋解密脚本
from PIL import Image
import argparse
W = 889
H = 889
def spiral_positions(width, height):
if width != height or width % 2 == 0:
raise ValueError("图片必须是奇数正方形,比如 889x889")
positions = []
x, y = width // 2, height // 2
dx = [1, 0, -1, 0]
dy = [0, 1, 0, -1]
direction = 0
steps_to_take = 1
steps_count = 0
turn_count = 0
positions.append((x, y))
while len(positions) < width * height:
x += dx[direction]
y += dy[direction]
if 0 <= x < width and 0 <= y < height:
positions.append((x, y))
steps_count += 1
if steps_count == steps_to_take:
steps_count = 0
direction = (direction + 1) % 4
turn_count += 1
if turn_count % 2 == 0:
steps_to_take += 1
return positions
def decrypt(input_path, output_path):
img = Image.open(input_path).convert("RGB")
if img.size != (W, H):
raise ValueError(f"加密图尺寸必须是 {W}x{H},当前是 {img.size}")
enc_pixels = img.load()
out = Image.new("RGB", (W, H), "white")
out_pixels = out.load()
positions = spiral_positions(W, H)
index = 0
for y in range(H):
for x in range(W):
sx, sy = positions[index]
out_pixels[x, y] = enc_pixels[sx, sy]
index += 1
out.save(output_path)
print("[+] 螺旋解密完成")
print("[+] 输入文件:", input_path)
print("[+] 输出文件:", output_path)
print("[+] 图片尺寸:", out.size)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input", required=True, help="输入加密图片,必须是889x889")
parser.add_argument("-o", "--output", required=True, help="输出还原图片")
args = parser.parse_args()
decrypt(args.input, args.output)
if __name__ == "__main__":
main()
然后发现SCB.PNG的空白像素值和解密的图片的像素值是一样的,再根据原图的绿色像素痕迹可以猜出是把原图填入scb(shark_crown_bill)里面,得到flag
填充脚本
from PIL import Image
import argparse
def get_blank_positions(mask_path, threshold=230):
mask = Image.open(mask_path).convert("RGB")
w, h = mask.size
positions = []
for y in range(h):
for x in range(w):
r, g, b = mask.getpixel((x, y))
# 接近白色的地方认为是空白区域
if r >= threshold and g >= threshold and b >= threshold:
positions.append((x, y))
return positions, mask
def encrypt(secret_path, mask_path, output_path):
secret = Image.open(secret_path).convert("RGB")
sw, sh = secret.size
blank_positions, out = get_blank_positions(mask_path)
secret_pixels = []
# 从原图片左上角开始,按行提取所有像素
for y in range(sh):
for x in range(sw):
secret_pixels.append(secret.getpixel((x, y)))
if len(secret_pixels) > len(blank_positions):
raise ValueError(
f"空白区域不够大,需要 {len(secret_pixels)} 个像素,但只有 {len(blank_positions)} 个空白像素"
)
# 把原图像素依次填充进 SCB 以外的空白区域
for i in range(len(secret_pixels)):
out.putpixel(blank_positions[i], secret_pixels[i])
out.save(output_path)
print("[+] 加密完成")
print("[+] 原图尺寸:", sw, sh)
print("[+] 使用像素:", len(secret_pixels))
print("[+] 输出文件:", output_path)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-s", "--secret", required=True, help="要隐藏的原图片")
parser.add_argument("-m", "--mask", required=True, help="SCB 对照图")
parser.add_argument("-o", "--output", required=True, help="输出图片")
args = parser.parse_args()
encrypt(args.secret, args.mask, args.output)
if __name__ == "__main__":
main()
encrypt-Camellia
这是个python打包的exe,用pyinstxtractor.py拆
得到一堆pyc,看encrypt.pyc和srpm_utils.pyc,得到图片加密代码是把图片补零成方阵,分离 R/G/B 三个通道
,每轮按上面的公式做一次逐元素交换
核心交换为:
target = (index * index + (2 * round_index + 3) * index + 7 * (channel_index + 1)) % length
解密代码为
import argparse
import numpy as np
from PIL import Image
def swap_target(index: int, length: int, round_index: int, channel_index: int) -> int:
return (
index * index
+ (2 * round_index + 3) * index
+ 7 * (channel_index + 1)
) % length
def undo_quadratic_swaps(values: np.ndarray, round_index: int, channel_index: int) -> np.ndarray:
restored = values.copy()
for index in reversed(range(restored.size)):
target = swap_target(index, restored.size, round_index, channel_index)
restored[index], restored[target] = restored[target], restored[index]
return restored
def decrypt_channel(channel_array: np.ndarray, rounds: int, channel_index: int) -> np.ndarray:
current = channel_array.reshape(-1).astype(np.uint8).copy()
for round_index in reversed(range(rounds)):
current = undo_quadratic_swaps(current, round_index, channel_index)
return current.reshape(channel_array.shape)
def decrypt_image(
input_path: str,
output_path: str,
rounds: int,
width: int | None = None,
height: int | None = None,
) -> None:
encrypted = Image.open(input_path).convert('RGB')
encrypted_array = np.array(encrypted)
channels = []
for channel_index, name in enumerate(['R', 'G', 'B']):
print(f'Decrypting {name} channel...')
channels.append(decrypt_channel(encrypted_array[:, :, channel_index], rounds, channel_index))
decrypted = np.stack(channels, axis=2).astype(np.uint8)
if width is not None and height is not None:
if width <= 0 or height <= 0:
raise ValueError('width 和 height 必须为正整数')
if height > decrypted.shape[0] or width > decrypted.shape[1]:
raise ValueError('给定的 width/height 超过了解密后图像尺寸')
decrypted = decrypted[:height, :width, :]
elif (width is None) ^ (height is None):
raise ValueError('width 和 height 要么都提供,要么都不提供')
Image.fromarray(decrypted, 'RGB').save(output_path)
print(f'Decrypted image written to: {output_path}')
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description='Decrypt an RGB image encrypted by SRPM 2.0.')
parser.add_argument('--input', required=True, help='加密后的图片路径')
parser.add_argument('--output', required=True, help='输出解密图片路径')
parser.add_argument('--rounds', type=int, required=True, help='加密时使用的轮数')
parser.add_argument('--width', type=int, default=None, help='原图宽度(可选)')
parser.add_argument('--height', type=int, default=None, help='原图高度(可选)')
return parser.parse_args()
def main() -> None:
args = parse_args()
decrypt_image(
input_path=args.input,
output_path=args.output,
rounds=args.rounds,
width=args.width,
height=args.height,
)
if __name__ == '__main__':
main()
图片的大小没变,爆破一下轮次就是3,得到风景图,然后再盲水印解一下就出来flag了

第四届黄河流域公安院校网络安全技能挑战赛 misc writeup
浙公网安备 33010602011771号