2026黄河流域 公安院校网络安全技能挑战赛 misc writeup

signin-ZLSk4ever

稍微有点脑洞了感觉,但比较好解

把文字部分都删掉只留下标点符号
Pasted image 20260607221119.png
将这四种标点映射为二进制的 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的密码Pasted image 20260607222004.png
docx有密码,采用john提取hash后换hashcat用rockyou爆破,得到密码skadi2520
Pasted image 20260607222153.png
挪开图片就可以看到隐藏文字的flag了

Upper Tower-ZLSk4ever

对1.png,观察到b0通道有LSB隐写痕迹,提取得到:

1025658991124355712810934889700140919747617421003079315985624061054296140009382465857058434674074275120892336766575969588338976341017367264157852190739650926220934210231417469330039947068382806764673001051221448447380001720482620813085941827875644858794111714812288537472795099949264885342147007039538061069523051452567281974840302631700453742804355540730431759203779622365023345254780691947483808187557231093625244295437363124650609010561963221808334829801615614695629099613864198462854382911806884707317102998488625789222163237260888078730026407441064787968

超大十进制数而且可被17整除,判断是塔珀自指公式
解出key 4thHHLY
Pasted image 20260607221730.png

由题目中寂静中的真相可想到Silenteye
用key解码得到flag
Pasted image 20260607221850.png

Do you know RA2?-ZLSk4ever

第一层SSTV
解码出来图片上有密码packed up and ready
Pasted image 20260607220749.png
第二层在vc外层卷,幻影坦克隐写
使用幻影坦克隐写工具或从 RGBA 字节流中按阈值提取 2 bit pair,并校验奇偶校验位
得到隐藏卷密码Nobody's here but that's trees!
第三层vc隐藏卷里的图片是光棱坦克
Pasted image 20260607220847.png
拉高图片曝光,给左下角截出来反色二维码就能扫了,扫码得到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了

posted @ 2026-06-07 22:37  summer_2lc  阅读(196)  评论(0)    收藏  举报