DesCTF

shr1mp 发布于 4 天前 7 次阅读


wireshark

Traffic revealed normal Modbus traffic plus "extra" communications—all protocol-compliant, evading firewall alerts. Attacker knows the protocol.

译:流量显示正常的Modbus流量加上“额外”的通信——所有协议都符合,可以避开防火墙警报。攻击者知道协议。

首先拿到一个流量包,一共有四个流(1-4),发现是工控流量,全是modbus

192.168.100.102返回的Read Holding Registers响应中包含了一个假的flag

检查其他Modbus响应,发现192.168.100.101的响应中包含特殊字符串:

S7COMM01

查询资料得到:这是DES密钥(8字节长度,符合DES密钥特征)

#!/usr/bin/env python3
import struct
import socket
from typing import List, Dict, Optional
from Crypto.Cipher import DES
PCAP_FILE = "challenge.pcapng"
def parse_pcap(path: str) -> List[Dict]:
    """Parse PCAP file and extract Modbus/TCP packets."""
    with open(path, "rb") as f:
        data = f.read()
    # Detect endianness from magic number
    endian = {
        b"\xd4\xc3\xb2\xa1": "<",
        b"\xa1\xb2\xc3\xd4": ">"
    }.get(data[:4])
    if endian is None:
        raise ValueError("Unsupported pcap format")
    offset = 24  # Skip global header
    packets = []
    pkt_index = 0
    while offset + 16 <= len(data):
        # Parse packet header
        ts_sec, ts_usec, incl_len, orig_len = struct.unpack(
            f"{endian}IIII", data[offset:offset + 16]
        )
        offset += 16
        
        # Extract packet data
        raw = data[offset:offset + incl_len]
        offset += incl_len
        pkt_index += 1
        # Skip non-IPv4 packets
        if len(raw) < 14 or struct.unpack("!H", raw[12:14])[0] != 0x0800:
            continue
        # Parse IP header
        ip = raw[14:]
        if len(ip) < 20 or ip[9] != 6:  # Skip non-TCP packets
            continue
        src_ip = socket.inet_ntoa(ip[12:16])
        dst_ip = socket.inet_ntoa(ip[16:20])
        # Parse TCP header
        tcp = ip[(ip[0] & 0x0F) * 4:]
        if len(tcp) < 20:
            continue
        src_port, dst_port = struct.unpack("!HH", tcp[:4])
        if 502 not in (src_port, dst_port):  # Skip non-Modbus packets
            continue
        payload = tcp[(tcp[12] >> 4) * 4:]
        if len(payload) < 8:  # Skip incomplete Modbus headers
            continue
        # Parse Modbus/TCP MBAP
        tid, pid, length = struct.unpack("!HHH", payload[:6])
        unit_id = payload[6]
        func_code = payload[7]
        pdu = payload[8:]
        packets.append({
            "index": pkt_index,
            "ts": ts_sec + ts_usec / 1_000_000,
            "src": src_ip,
            "dst": dst_ip,
            "sport": src_port,
            "dport": dst_port,
            "tid": tid,
            "unit": unit_id,
            "func": func_code,
            "pdu": pdu,
            "raw": payload,
        })
    return packets
def printable(bs: bytes) -> str:
    """Convert bytes to printable ASCII with dots for non-printable chars."""
    return "".join(chr(b) if 32 <= b < 127 else "." for b in bs)
def des_ecb_decrypt(ciphertext: bytes, key: bytes) -> bytes:
    """Decrypt DES-ECB using pycryptodome."""
    cipher = DES.new(key, DES.MODE_ECB)
    return cipher.decrypt(ciphertext)
def find_fake_flag(packets: List[Dict]) -> Optional[str]:
    """Find fake flag in Modbus Read Holding Registers responses."""
    for pkt in packets:
        if (pkt["func"] == 3 and pkt["src"] == "192.168.100.102" 
                and len(pkt["pdu"]) >= 1):
            text = printable(pkt["pdu"][1:])
            if "flag{" in text:
                return text[text.index("flag{"):]
    return None
def find_des_key(packets: List[Dict]) -> Optional[Dict]:
    """Find DES key in Modbus Read Holding Registers responses."""
    for pkt in packets:
        if (pkt["func"] == 3 and pkt["src"] == "192.168.100.101" 
                and len(pkt["pdu"]) >= 1 and b"S7COMM01" in pkt["pdu"][1:]):
            return {
                "key": b"S7COMM01",
                "packet_index": pkt["index"]
            }
    return None
def extract_cipher_blocks(packets: List[Dict], after_index: int) -> List[bytes]:
    """Extract cipher blocks from Diagnostics/Return Query Data packets."""
    blocks = []
    for pkt in packets:
        if pkt["index"] <= after_index:
            continue
            
        if not (pkt["src"] == "192.168.100.10" 
                and pkt["dst"] == "192.168.100.101" 
                and pkt["func"] == 8 
                and len(pkt["pdu"]) >= 10):
            continue
            
        subfunc = struct.unpack("!H", pkt["pdu"][:2])[0]
        data = pkt["pdu"][2:]
        
        # Filter out obvious test messages and collect valid blocks
        if subfunc == 0x0000 and len(data) == 8 and data not in (b"LINK", b"PING", b"TEST", b"ECHO"):
            blocks.append(data)
            if len(blocks) == 6:
                break
                
    return blocks
def main():
    packets = parse_pcap(PCAP_FILE)
    # Step 1: Find fake flag and DES key
    fake_flag = find_fake_flag(packets)
    if fake_flag:
        print("[+] Fake flag found:", fake_flag)
    key_info = find_des_key(packets)
    if not key_info:
        raise RuntimeError("DES key not found")
        
    des_key = key_info["key"]
    key_packet_index = key_info["packet_index"]
    print(f"[+] DES key found in packet #{key_packet_index}: {des_key!r}")
    # Step 2: Extract cipher blocks after key packet
    blocks = extract_cipher_blocks(packets, key_packet_index)
    if len(blocks) != 6:
        raise RuntimeError(f"Expected 6 blocks, got {len(blocks)}")
    ciphertext = b"".join(blocks)
    print("[+] Ciphertext:", ciphertext.hex())
    # Step 3: Decrypt and extract flag
    plaintext = des_ecb_decrypt(ciphertext, des_key)
    print("[+] Decrypted raw:", plaintext)
    # Remove PKCS#7 padding
    pad = plaintext[-1]
    if 1 <= pad <= 8 and plaintext.endswith(bytes([pad]) * pad):
        plaintext = plaintext[:-pad]
    print("[+] Real flag:", plaintext.decode())
if __name__ == "__main__":
    main()

输出:

[+] Fake flag found: flag{this_is_fake_try_harder!}
[+] DES key found in packet #30612: b'S7COMM01'
[+] Ciphertext: ded7825ede4fd19c9f37371c37c6fa2d54e6fe2801f0df1d763175a586db1c629efa82d0f8eacb417b4419392b4a6aa8
[+] Decrypted raw: b'flag{a3f8e2d1-7c19-4a6b-b5e8-9d2f0c4a7e31}\x06\x06\x06\x06\x06\x06'
[+] Real flag: flag{a3f8e2d1-7c19-4a6b-b5e8-9d2f0c4a7e31}

Real sign in

The picture for pixls ,Please find the secret and send it to the WeChat official account background to get the flag!!

译:图片为pixls,请找到秘密并将其发送到微信公众号后台以获取旗帜

拿到的压缩包经过加密,同时发现压缩包加密算法为:ZipCrypto,压缩算法需为:Store

并且里面有PNG文件,想到进行明文爆破

C:\Users\q1388\Desktop\工具\bkcrack-1.8.1-win64>bkcrack.exe -C 1.zip -c challenge.png -p png_header
bkcrack 1.8.1 - 2025-10-25
[15:26:41] Z reduction using 9 bytes of known plaintext
100.0 % (9 / 9)
[15:26:41] Attack on 712831 Z values at index 6
Keys: 5eb34ede c49019bf 815834b9
42.5 % (302894 / 712831)
Found a solution. Stopping.
You may resume the attack with the option: --continue-attack 302894
[15:28:07] Keys
5eb34ede c49019bf 815834b9


C:\Users\q1388\Desktop\工具\bkcrack-1.8.1-win64>bkcrack -C 1.zip -c challenge.png -k 5eb34ede c49019bf 815834b9 -U out.zip 123
bkcrack 1.8.1 - 2025-10-25
[15:33:00] Writing unlocked archive out.zip with password "123"
100.0 % (1 / 1)
Wrote unlocked archive.

打开png:

接下来是:

-------------------------------------------------------------------------------------------------------------------

Zigzag变换(Zigzag Transformation)

知识点:
Zigzag变换通过之字形路径遍历矩阵元素,将二维数据展平为一维序列。这种遍历方式确保了相邻的二维数据在一维序列中也保持相邻。
原始矩阵:          Zigzag序列:
0  1  5  6         0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
2  4  7  8         
3  8  9  10
实际遍历路径:
→ ↘ ↓ ↘
↓ ↗ → ↘
→ ↘ ↓ ↘

**主要应用***
1. JPEG图像压缩(最著名)
    对DCT(离散余弦变换)系数矩阵进行Zigzag扫描
    将低频系数(重要信息)排在前面,高频系数(通常为零)排在后面
    便于后续的行程编码和霍夫曼编码
2. 视频编码(H.264/HEVC等)
    用于变换系数的熵编码优化
3. 数据压缩与加密
    改变数据排列顺序,增强压缩效率或安全性

算法实现要点:

def zigzag(matrix):
    rows, cols = len(matrix), len(matrix[0])
    result = []
    for s in range(rows + cols - 1):
        if s % 2 == 0:  # 偶数对角线:向上遍历
            for i in range(max(0, s - cols + 1), min(s + 1, rows)):
                j = s - i
                result.append(matrix[i][j])
        else:  # 奇数对角线:向下遍历
            for i in range(min(s, rows - 1), max(-1, s - cols), -1):
                j = s - i
                result.append(matrix[i][j])
    return result

优点:

特性说明
保持局部性相邻像素/系数在序列中仍相邻
能量集中将重要数据前置,利于压缩
简单高效计算复杂度低,实现简单

--------------------------------------------------------------------------------------------------------------------

from PIL import Image

def zigzag_decrypt_pixels(input_image, output_image, rows):
    """
    PNG 图片像素 ZigZag 解密
    
    Args:
        input_image: 输入的加密 PNG 图片路径
        output_image: 输出的解密 PNG 图片路径
        rows: ZigZag 行数
    """
    # 打开图片
    img = Image.open(input_image)
    width, height = img.size
    pixels = list(img.getdata())
    
    total_pixels = len(pixels)
    
    if rows <= 1 or rows >= total_pixels:
        img.save(output_image)
        print("行数无效,直接保存原图")
        return
    
    # 计算每行应该有多少像素
    fence_lengths = [0] * rows
    row = 0
    direction = 1
    
    for i in range(total_pixels):
        fence_lengths[row] += 1
        
        if row == 0:
            direction = 1
        elif row == rows - 1:
            direction = -1
        
        row += direction
    
    # 分割像素到各行
    fence = []
    index = 0
    for length in fence_lengths:
        fence.append(list(pixels[index:index + length]))
        index += length
    
    # 按 ZigZag 路径读取并重组
    result_pixels = []
    row = 0
    direction = 1
    row_indices = [0] * rows
    
    for i in range(total_pixels):
        result_pixels.append(fence[row][row_indices[row]])
        row_indices[row] += 1
        
        if row == 0:
            direction = 1
        elif row == rows - 1:
            direction = -1
        
        row += direction
    
    # 创建新图片
    decrypted_img = Image.new(img.mode, (width, height))
    decrypted_img.putdata(result_pixels)
    decrypted_img.save(output_image)
    
    print(f"解密完成!输出:{output_image}")


# 使用示例
if __name__ == "__main__":
    # 修改为你的文件路径和行数
    input_path = "encrypted.png"      # 输入的加密图片
    output_path = "decrypted.png"     # 输出的解密图片
    num_rows = 3                      # ZigZag 行数 (根据题目调整)
    
    zigzag_decrypt_pixels(input_path, output_path, num_rows)

执行后得到新的图片,是个二维码:

扫描后得到:

I_love_DesCTF

关注公众号,发送信息后得到flag;

DesCTF{Have fun and enjoy the challenges ahead!!!}
这个人很菜,但是在学
最后更新于 2026-04-13