强网拟态线下Misc复现

复现参考:

https://goodlunatic.github.io/posts/353513a/

https://0ran9e.fun/2025/11/30/qwnt/wp/

泄漏的时间与电码

1
2
3
题目描述:
你苦苦寻觅的东西或许就藏在他最显眼的地方
hint1:ModR/M

附件有以下三个

其中chal是elf可执行文件,chal.py内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import time
import random
import sys

class SecureTypewriter:
def __init__(self):
self.lfsr = 0x92
self.time_unit = 0.005
self.jitter = 0.001
self.base_overhead = 10
self.branch_penalty = 30
def step_lfsr(self):
bit = ((self.lfsr >> 0) ^ (self.lfsr >> 2) ^ (self.lfsr >> 3) ^ (self.lfsr >> 4)) & 1
self.lfsr = (self.lfsr >> 1) | (bit << 7)
return self.lfsr

def scramble(self, val):
return ((val * 0x1F) + 0x55) & 0xFF

def process_char(self, char):
c = ord(char)

k = self.step_lfsr()

val = c ^ k

base_ops = self.scramble(val)

current_ops = self.base_overhead + base_ops

if base_ops % 2 != 0:
current_ops += self.branch_penalty

real_duration = current_ops * self.time_unit

noise = random.uniform(-self.jitter, self.jitter)
total_time = max(0, real_duration + noise)

return total_time

def process_text(self, text):
timings = []
for char in text:
elapsed = self.process_char(char)
timings.append(elapsed)
return timings

if __name__ == "__main__":
try:
with open("flag.txt", "r") as f:
content = f.read().strip()
except FileNotFoundError:
print("Error: flag.txt not found.")
sys.exit(1)

machine = SecureTypewriter()
print(f"Processing {len(content)} characters with SecureTypewriter v2.0...")

logs = machine.process_text(content)

with open("timing.log", "w") as f:
for t in logs:
f.write(f"{t:.6f}\n")

print("Processing complete. Timing data saved to timing.log")

timing.log的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
1.110270
0.924169
1.139244
0.670085
0.915054
1.154452
0.224613
0.329060
0.774615
0.279617
0.954166
0.430143
0.414914
1.224826
1.310686
1.265828
0.110950
1.225669
1.404647
0.575287
1.455927
0.975492
0.305642
0.835893
1.245893
0.569651
1.060266
0.149129
0.844243
1.294104
0.079101
0.914897
1.025389
0.270495
0.225577
0.654189
1.385665
0.755860
0.450597
0.950750
0.839268
1.015624
0.895000
0.794687
1.064966
1.200042
0.559413
0.980588
0.525959
0.514992
0.629261
0.489585
1.089786
0.880690
1.374392
0.789075
0.814771
1.455273
1.050996
0.234891
1.074220
0.099300
1.319762
0.935773
0.454985
0.425895
0.704892
1.095786
1.165433
1.295589
0.749113
0.885320
1.244904
0.659642
0.635889
0.435427
0.520476
0.870549
0.890145
1.125522
1.064915
0.399210
0.865873

是侧信道攻击的相关内容,模拟了一个“安全打字机”,在处理每个字符时,根据字符的值和内部状态产生了不同的计算量,从而导致处理时间不同,同时chal文件解包并反编译后发现代码逻辑与chal.py文件相同,先恢复timing.log的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import sys
import re

class Solver:
def __init__(self):
# 题目中硬编码的初始种子
self.lfsr = 0x92
self.base_overhead = 10
self.branch_penalty = 30

def step_lfsr(self):
# 完全复现题目中的 LFSR 逻辑
bit = ((self.lfsr >> 0) ^ (self.lfsr >> 2) ^ (self.lfsr >> 3) ^ (self.lfsr >> 4)) & 1
self.lfsr = (self.lfsr >> 1) | (bit << 7)
return self.lfsr

def scramble(self, val):
# 复现混淆逻辑
return ((val * 0x1F) + 0x55) & 0xFF

def solve():
log_file = "timing.log"

try:
with open(log_file, "r") as f:
content = f.read()
except FileNotFoundError:
print(f"Error: {log_file} not found.")
return

# 使用正则提取所有的浮点数时间
# 这样可以忽略像 "" 这样的无关前缀
timings_str = re.findall(r"(\d+\.\d+)", content)
timings = [float(t) for t in timings_str]

print(f"[*] Loaded {len(timings)} timing samples from {log_file}")

solver = Solver()
recovered_text = ""

# 定义待爆破的字符集:常用可打印字符 + 换行符
# 如果 flag 包含特殊字符,可以扩大到 range(256)
charset = list(range(32, 127)) + [10, 13]

print("[*] Starting brute-force recovery...")

for i, t in enumerate(timings):
# 1. 逆向计算操作数 (去除时间系数和噪音)
# ops = round(time / 0.005)
target_ops = int(round(t / 0.005))

# 2. 获取当前轮次的密钥流 (必须与处理字符同步进行)
k = solver.step_lfsr()

found_char = None

# 3. 爆破字符
for c_code in charset:
# 模拟处理过程
val = c_code ^ k
base_ops = solver.scramble(val)

# 计算理论开销
current_ops = solver.base_overhead + base_ops
if base_ops % 2 != 0:
current_ops += solver.branch_penalty

# 匹配检查
if current_ops == target_ops:
found_char = chr(c_code)
break

if found_char:
recovered_text += found_char
else:
recovered_text += "?"
print(f"[!] Warning: No matching char found for sample {i} (ops={target_ops})")

print("\n" + "="*40)
print("Recovered Content:")
print("="*40)
print(recovered_text)
print("="*40)

if __name__ == "__main__":
solve()

得到恢复的表

1
2
3
4
5
6
h i j k l m n
8 9 0 / - _ =
a b c d e f g
v w x y z { }
o p q r s t u
1 2 3 4 5 6 7

结合hint1:ModR/M,可以在谷歌找到下面这个项目

https://github.com/woodruffw/steg86

1
steg86 extract chal > flag.txt

1
326a31306c206b6b6868203a332024206a686820346820326b32682024336a203468336b206a323068206a6a366c206b6b6c6c206c6c6b205e6a206b6b24686820306a6a202f7a203a3620356b24206a6a206a

也就是

1
2j10l kkhh :3 $ jhh 4h 2k2h $3j 4h3k j20h jj6l kkll llk ^j kk$hh 0jj /z :6 5k$ jj j

这是一段vim编辑操作,对应上面的字符表

指令 坐标 字符
2j10l (2, 10) f
kkhh (0, 8) l
:3 (2, 0) a
$ (2, 12) g
jhh (3, 10) {
4h (3, 6) y
2k2h (1, 4) 0
$3j (4, 12) u
4h3k (1, 8) -
j20h (2, 0) a
jj6l (4, 6) r
kkll (2, 8) e
llk (1, 10) _
^j (2, 0) a
kk$hh (0, 10) m
0jj (2, 0) a
/z (3, 8) z
:6 (5, 0) 1
5k$ (0, 12) n
jj (2, 12) g
j (3, 12) }

flag{y0u-are_amaz1ng}

返璞归真

附件下载的压缩包存在伪加密,随波逐流工具直接自动修复了,解压时发现注释有hashisk3y

将图片拖入随波逐流中处理,发现文件尾还藏有一个bmp文件,分离出来得到:

是paperback,之前在L3HCTF2025做到过,附上工具下载链接:http://www.ollydbg.de/Paperbak/

得到wow.txt:

1
jNX+xu2QKBm23AUlwClt+3xDkQcJGjM=

结合之前的hashisk3y,我们把之间的那张image.jpg删去末尾BMP的数据后MD5一下

001a62ee54d1c28a8b769ab5499011cb

是rc4解密,得到

flag{examp13_f0r_r3a11}

猫咪电台

附件得到1.wav和cat0.png

首先处理cat0.png图片,在随波逐流发现存在LSB隐写,其中的顺序为BGR,是一个png文件,到stegsolve里面导出文件

得到

flag part0: ==gNWRWTFRjY4IGV4sGczg0QzAFUyQTQ

得到part0:Ci4l10~

接下来我们处理1.wav文件,可以发现该文件还藏有一个zip文件:

得到一个压缩包,里面有2.wav但是解压需要密码

发现有两条很清晰的频率,猜测是RTTY

用minimodem解码,自动检测:

1
minimodem -a --rx rtty -i -f 1.wav

得到flag1:R77YM30W1SFUN

1
2
3
4
5
6
7
8
CQ CQ CQ DE CATHUB
THIS IS CAT RADIO HUB MEOW MEOW MEOW
WHISKERS THE CAT IS BROADCASTING
PURRRRRRR PURRRRRRR PURRRRRRR
FLAG PART1: R77YM30W1SFUN
PASS IS 450KTFQY1D4KX8JB
MEOW MEOW MEOW
SKSK

以及压缩包解压密码 450KTFQY1D4KX8JB

普通的音频采样率通常是 44100 Hz (44.1 kHz) 或 48000 Hz,这里的采样率是 2.4 MHz,这是无线电频谱的原始录制数据。这个采样率(2.4 MSps)是常见入门级 SDR 设备(如 RTL-SDR)的典型采样率,跑ai跑出来一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import soundfile as sf
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import decimate

def solve_radio_challenge(filename):
print(f"[*] Loading {filename}...")
data, sample_rate = sf.read(filename)

# 1. 转换为 IQ 信号
if data.ndim > 1:
iq_signal = data[:, 0] + 1j * data[:, 1]
else:
print("[!] Error: Data is not stereo.")
return

# 2. 自动寻找载波频率 (Auto-Tuning)
print("[*] Detecting carrier frequency...")
# 计算功率谱密度 (PSD) 来找到能量最强的点(那条黄线)
fft_size = 2048 * 8
psd = np.abs(np.fft.fft(iq_signal[:fft_size]))**2
freqs = np.fft.fftfreq(fft_size, d=1/sample_rate)

# 找到峰值对应的频率
carrier_index = np.argmax(psd)
carrier_freq = freqs[carrier_index]

print(f"[+] Found strong signal at: {carrier_freq/1000:.2f} kHz")

# 3. 移频 (Frequency Shifting)
# 将信号乘以 e^(-j*2*pi*f*t) 把载波搬移到 0Hz
print("[*] Shifting signal to center (0 Hz)...")
t = np.arange(len(iq_signal)) / sample_rate
# 注意这里用负的 carrier_freq 来抵消偏移
shifted_signal = iq_signal * np.exp(-1j * 2 * np.pi * carrier_freq * t)

# 4. 降采样 (Downsampling)
# 原始 2.4MHz 包含太多噪音,我们需要“聚焦”到信号上
# 目标音频采样率 48kHz
target_rate = 48000
decimation_factor = int(sample_rate / target_rate)

print(f"[*] Decimating (Zooming in) by factor of {decimation_factor}...")
# 使用 scipy 的 decimate 自带低通滤波,比简单的切片更干净
try:
audio_iq = decimate(shifted_signal, decimation_factor, ftype='fir')
except:
# 如果数据量太大内存不够,回退到简单切片
audio_iq = shifted_signal[::decimation_factor]

# 5. 多种模式解调 (导出所有可能)
print("[*] Demodulating...")

# 模式 A: NFM (窄带调频) - CTF最常见
# 相位差 -> 频率
phase = np.unwrap(np.angle(audio_iq))
audio_fm = np.diff(phase)
# 去除直流分量 (Center the audio)
audio_fm = audio_fm - np.mean(audio_fm)
# 归一化
audio_fm = audio_fm / np.max(np.abs(audio_fm))
sf.write("flag_FM.wav", audio_fm, target_rate)
print("[+] Saved: flag_FM.wav (Likely the answer)")

# 模式 B: AM (调幅) - 虽然看起来不像,但也生成一份备用
audio_am = np.abs(audio_iq)
audio_am = audio_am - np.mean(audio_am)
audio_am = audio_am / np.max(np.abs(audio_am))
sf.write("flag_AM.wav", audio_am, target_rate)
print("[+] Saved: flag_AM.wav")

# 模式 C: SSB (单边带) - 直接听移频后的实部
audio_ssb = np.real(audio_iq)
audio_ssb = audio_ssb / np.max(np.abs(audio_ssb))
sf.write("flag_SSB.wav", audio_ssb, target_rate)
print("[+] Saved: flag_SSB.wav")

if __name__ == "__main__":
solve_radio_challenge("2.wav")

运行结果如下

1
2
3
4
5
6
7
8
9
[*] Loading 2.wav...
[*] Detecting carrier frequency...
[+] Found strong signal at: -300.15 kHz
[*] Shifting signal to center (0 Hz)...
[*] Decimating (Zooming in) by factor of 50...
[*] Demodulating...
[+] Saved: flag_FM.wav (Likely the answer)
[+] Saved: flag_AM.wav
[+] Saved: flag_SSB.wav

在flag_FM.wav里面可以听到类似于sstv音频特点的声音,使用MMSSTV得到

2nd part _C4T9L1V3S

最后的flag:flag{Ci4l10~R77YM30W1SFUN_C4T9L1V3S}

标准的绝密压缩

是一道流量分析题目,我们追踪tcp流,可以发现有一些png数据(89504e47),手动提取并分别排序命名为12345…..(neta提取的图片数量不完全,而且没有顺序)

得到以下照片,而且它们的分辨率都为100 x 100

用pngcheck检查一下,结果表明这个极有可能不是一张真正的图片,而是一段被伪装成PNG IDAT数据块的文本信息,将文本信息进行了zlib压缩,直接塞进了IDAT块

脚本提取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import zlib
import struct
import os

def extract_text_from_fake_png(file_path):
try:
with open(file_path, 'rb') as f:
data = f.read()
# 寻找 IDAT 块
idat_offset = data.find(b'IDAT')
if idat_offset == -1:
return None

# 读取长度
length_bytes = data[idat_offset-4 : idat_offset]
length = struct.unpack('>I', length_bytes)[0]

# 提取并解压
compressed_data = data[idat_offset+4 : idat_offset+4+length]
try:
# 尝试标准解压
return zlib.decompress(compressed_data)
except:
# 尝试无 header 解压
return zlib.decompress(compressed_data, -15)
except Exception as e:
print(f"Error processing {file_path}: {e}")
return None

# 主程序
full_conversation = b""
index = 0

print("开始批量提取...")

while True:
# 假设文件名为 0.png, 1.png, 2.png ...
filename = f"{index}.png"

if not os.path.exists(filename):
break # 找不到文件则停止

content = extract_text_from_fake_png(filename)

if content:
print(f"[{filename}] 提取成功")
full_conversation += content
else:
print(f"[{filename}] 提取失败或非预期格式")

index += 1

print("\n" + "="*30)
print("🎉 完整对话内容如下:")
print("="*30)
# 尝试解码为 utf-8 打印,如果失败则打印原始 bytes
try:
print(full_conversation.decode('utf-8'))
except:
print(full_conversation)

得到

1
Connection established. Hey, you online? It’s been a while since we last talked.Yeah, I’m here. Busy as always. Feels like the days are getting shorter.Tell me about it. I barely have time to sleep lately. Between maintenance logs and incident reports, I’m drowning.Sounds rough. I’ve been buried in audits myself. Every time I finish one, another pops up.Classic. Sometimes I wonder if the machines are easier to deal with than the people.No kidding. At least machines don’t ask pointless questions.True. Anyway, before I forget—how’s that side project you were working on? The one you wouldn’t shut up about months ago.Still alive… barely. Progress is slow, but steady. You know me—I don’t give up easily.Good. I hope it pays off one day.Thanks. Alright… I’m guessing you didn’t ping me just to chat?Well, half of it was. It’s been a while. But yes—I do have something for you today. Before sending the core cipher, I’ll transmit an encrypted archive first. It contains a sample text and the decryption rules.Okay. What’s special about this sample text?And… inside the sample text, I used my favorite Herobrine legend—you know the one I always bring up.Of course I know. The hidden original text from that weird old site, right?What can I say—old habits die hard. Anyway, the important part: the sample packet and the core cipher are encrypted with the same password.Got it. So if I can decrypt the sample, the real one should be straightforward.Exactly. Send the sample when ready.I’m ready. Go ahead.UEsDBBQAAQAIABtFeFu1Ii0dcwAAAHwAAAAJAAAAcnVsZXMudHh07XuRBFDbojGKhAz59VaKEpwD6/rKaZnqUxf+NMH0rybWrAMPewZ/yGyLrMKQjNIcEbPAxjmP5oTh8fP77Vi1wnFwzN37BmrQ9SCkC27FC/xeqbgw/HWcDpgzsEoiNpqT9ZThrbAScyg5syfJmNactjelNVBLAwQUAAEACACGOXhbpdvG1ysBAAAVAgAACgAAAHNhbXBsZS50eHTA1fy4cMLZwZkTI1mEk88yOXy9rmbTbCNBQOo9hqKQPK6vjZVo9aCtTVflmkKYGV99+51qXbinmG7WGik5UvLJk9MKRosThBCDMHrmjibOCzjzNELwEgEyX8DjqJkSc8pIFwj+oRM3bb4i0GtRxbwqgsxCtgwiKdCVoXVdetN7RKLIQ7DD+Huv/ZptNdd0yRNHis9LEA3loB+IHZ+dK7IknqPh4lYF8JwAjx5/wwp0YAM6Bcec7uAvk6B5t1pEztm1rLl8TjniVz5/bBUTo1LjUXnar/pnm1NvE9EAuxz/s6b+O8/ew7/A4ItdNJGzDudh6YULfiV3pCTXFIbR4GCe4LwkohWZIlAjysA+zLRrgkTDoB10vWdNGdfoBAlLRoUdZ95mS7X5/bXV41BLAQI/ABQAAQAIABtFeFu1Ii0dcwAAAHwAAAAJACQAAAAAAAAAIAAAAAAAAABydWxlcy50eHQKACAAAAAAAAEAGABIv3f82lzcAQAAAAAAAAAAAAAAAAAAAABQSwECPwAUAAEACACGOXhbpdvG1ysBAAAVAgAACgAkAAAAAAAAACAAAACaAAAAc2FtcGxlLnR4dAoAIAAAAAAAAQAYAFP0sZjOXNwBAAAAAAAAAAAAAAAAAAAAAFBLBQYAAAAAAgACALcAAADtAQAAAAA=got it. Decrypting… yeah, it works.Good. That means the channel is stable.Alright. Whenever you’re ready, send the real thing.The core cipher will be transmitted through our secret channel. You remember how to decrypt it, right?Of course. I’ve got the procedure ready. Start when you’re ready.Done. Core cipher fully received. Integrity verified—no corruption.Same to you. And hey… nice talking again.Agreed. Take care.Good. Keep things quiet for the next few days.Yeah. Let’s not wait so long next time.You too.

其中的一串字符是base64编码的加密zip压缩包,需要sample.txt作为明文进行明文攻击

结合对话中提到的Herobrine legend和weird old site可以在网上找到sample.txt:

1
It has been reported that some victims of torture, during the act, would retreat into a fantasy world from which they could not WAKE UP. In this catatonic state, the victim lived in a world just like their normal one, except they weren't being tortured. The only way that they realized they needed to WAKE UP was a note they found in their fantasy world. It would tell them about their condition, and tell them to WAKE UP. Even then, it would often take months until they were ready to discard their fantasy world and PLEASE WAKE UP.

010查看加密压缩包的压缩方式,其加密的标志位为0x3F,猜测是用7z压缩的

进行明文攻击得到内部密钥:

1
b47e923c 5aeb49a7 a3cd7af0

得到rules.txt:

1
2
1.you need to calc the md5 of port to decrypt the core data.
2.The cipher I put in the zip, in segments, has been deflated.

大致的思路利用对应的端口的MD5作为key解密内容

1
tshark -r capture.pcapng -T fields -e tcp.srcport -e tcp.dstport -e tcp.payload >1.txt

刚提取出来的数据是分块的,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
30012	8900	7c9062acee29aaa61768
8900 30012
30012 8900 9da62a4b8bed889e01e6
8900 30012
30012 8900 c50f8566ba622cef6583
8900 30012
30012 8900 2f65d15cab15cd5c17c9
8900 30012
30012 8900 937312dc74526dab11fa
8900 30012
30012 8900 c16788105bc707a25d50
8900 30012
30012 8900 f5fe8add47d10edc2567
8900 30012
30012 8900 e7f6570c7248dc81ad1a
8900 30012
30012 8900 bc19e44f6cd47a265fdf
8900 30012
30012 8900 baef0e08859931abe905
8900 30012
30012 8900 23b675657b57f930a53d
8900 30012
30012 8900 42f4e1f6062efd733db8
8900 30012
30012 8900 2d4fe08f08a1c98bd0c8
8900 30012
30012 8900 1c4cc46ee3cefcd7c136
8900 30012
30012 8900 58748a913c28656b1fd7
8900 30012
30012 8900 d1cb2029d9e76f67d043
8900 30012
30012 8900 c504e75a379c0700fc30
8900 30012
30012 8900 ef8722dbb1ac

也就是:

1
7c9062acee29aaa617689da62a4b8bed889e01e6c50f8566ba622cef65832f65d15cab15cd5c17c9937312dc74526dab11fac16788105bc707a25d50f5fe8add47d10edc2567e7f6570c7248dc81ad1abc19e44f6cd47a265fdfbaef0e08859931abe90523b675657b57f930a53d42f4e1f6062efd733db82d4fe08f08a1c98bd0c81c4cc46ee3cefcd7c13658748a913c28656b1fd7d1cb2029d9e76f67d043c504e75a379c0700fc30ef8722dbb1ac

对应端口号是30012,md5为7c365ebfc34003c40033cc47f6116dd1作为key

AES-ECB模式解密:

得到压缩包

大小为4字节,符合crc爆破的条件,写脚本提取所有的数据,进行爆破

以下脚本来自 Lunatic 师傅:https://goodlunatic.github.io/posts/353513a/#%E9%A2%98%E7%9B%AE%E5%90%8D%E7%A7%B0-%E6%A0%87%E5%87%86%E7%9A%84%E7%BB%9D%E5%AF%86%E5%8E%8B%E7%BC%A9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import subprocess
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import hashlib
import zipfile

def md5(data):
return hashlib.md5(data).hexdigest()

def aes_ecb_decrypt(ciphertext, key):
if len(ciphertext) % 16 != 0:
ciphertext = ciphertext.ljust((len(ciphertext) + 15) // 16 * 16, b'\x00')
cipher = AES.new(key, AES.MODE_ECB)
decrypted = cipher.decrypt(ciphertext)
return decrypted

def main():
file_path = "capture.pcapng"
crc_list = []

for src_port in range(30012, 30092):
print(f"[+] Processing source port: {src_port}")
aes_key = md5(str(src_port).encode())
print(f"[+] aes_key: {aes_key}")
filter_str = f"tcp.srcport == {src_port}"

command = [
"tshark",
'-r', file_path,
'-T', 'fields',
'-Y', filter_str,
'-e', 'tcp.payload'
]
# print(f"[+] cmd: {command}")
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output = result.stdout.split()
if not output:
continue

hex_data = "".join(output)
# print(f"[+] hex_data: {hex_data}")
zip_data = aes_ecb_decrypt(bytes.fromhex(hex_data), aes_key.encode())
# print(zip_data)

zip_filename = f"{src_port}.zip"
with open(zip_filename, 'wb') as f:
f.write(zip_data)
try:
with zipfile.ZipFile(zip_filename, 'r') as zf:
file_list = zf.namelist()
if file_list:
target_file = file_list[0]
info = zf.getinfo(target_file)
crc_list.append(hex(info.CRC))
except:
print(f"\033[1;31m[-] Failed to process zip for port {src_port}\033[0m")

print(f"CRC32 list: {crc_list}")

if __name__ == "__main__":
main()

得到80个zip文件,都能crc爆破,写脚本批量爆破 crc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import binascii

class CRC32Reverse:
def __init__(self, crc32, length, tbl=bytes(range(256)), poly=0xEDB88320, accum=0):
self.char_set = set(tbl) # 支持所有字节
self.crc32 = crc32
self.length = length
self.poly = poly
self.accum = accum
self.table = []
self.table_reverse = []

def init_tables(self, poly, reverse=True):
"""构建 CRC32 表及其反向查找表"""
# CRC32 表构建
for i in range(256):
for j in range(8):
if i & 1:
i >>= 1
i ^= poly
else:
i >>= 1
self.table.append(i)

assert len(self.table) == 256, "CRC32 表的大小错误"

# 构建反向查找表
if reverse:
for i in range(256):
found = [j for j in range(256) if self.table[j] >> 24 == i]
self.table_reverse.append(tuple(found))

assert len(self.table_reverse) == 256, "反向查找表的大小错误"

def calc(self, data, accum=0):
"""计算 CRC32 校验值"""
accum = ~accum
for b in data:
accum = self.table[(accum ^ b) & 0xFF] ^ ((accum >> 8) & 0x00FFFFFF)
accum = ~accum
return accum & 0xFFFFFFFF

def find_reverse(self, desired, accum):
"""查找反向字节序列"""
solutions = set()
accum = ~accum
stack = [(~desired,)]

while stack:
node = stack.pop()
for j in self.table_reverse[(node[0] >> 24) & 0xFF]:
if len(node) == 4:
a = accum
data = []
node = node[1:] + (j,)
for i in range(3, -1, -1):
data.append((a ^ node[i]) & 0xFF)
a >>= 8
a ^= self.table[node[i]]
solutions.add(tuple(data))
else:
stack.append(((node[0] ^ self.table[j]) << 8,) + node[1:] + (j,))

return solutions

def dfs(self, length, outlist=[b'']):
"""深度优先搜索生成字节序列"""
if length == 0:
return outlist
tmp_list = [item + bytes([x]) for item in outlist for x in self.char_set]
return self.dfs(length - 1, tmp_list)

def run_reverse(self):
"""执行 CRC32 反向查找"""
self.init_tables(self.poly)

desired = self.crc32
accum = self.accum
result_list = []

# 处理至少为 4 字节的情况
if self.length >= 4:
patches = self.find_reverse(desired, accum)
for patch in patches:
checksum = self.calc(patch, accum)
# print(f"verification checksum: 0x{checksum:08x} ({'OK' if checksum == desired else 'ERROR'})")
for item in self.dfs(self.length - 4):
patch = list(item)
patches = self.find_reverse(desired, self.calc(patch, accum))
for last_4_bytes in patches:
patch.extend(last_4_bytes)
checksum = self.calc(patch, accum)
if checksum == desired:
result_list.append(bytes(patch)) # 添加符合条件的字节序列
else:
for item in self.dfs(self.length):
if self.calc(item) == desired:
result_list.append(bytes(item)) # 添加符合条件的字节序列
return result_list

def crc32_reverse(crc32, length, char_set=bytes(range(256)), poly=0xEDB88320, accum=0):
obj = CRC32Reverse(crc32, length, char_set, poly, accum)
return obj.run_reverse() # 返回所有结果

def crc32(s):
return binascii.crc32(s) & 0xFFFFFFFF

if __name__ == "__main__":
crc_values =[0xcf60023f, 0x61d8a4f3, 0xb15f099f, 0xb93935f3, 0x56263d91, 0x7c9a17, 0x324af895, 0x64105f13, 0x7aae1d0a, 0x616c6729, 0x2b51c9d4, 0xb6e26299, 0xdff453c4, 0x9331116d, 0x324af895, 0x7c9a17, 0x7e2361b8, 0x7c65dfe1, 0x9e4be534, 0x324af895, 0x821fc2f0, 0x78e8a353, 0xac828282, 0x9e4be534, 0x6504596, 0xcaa8f9df, 0x64dc7498, 0x779f2fbd, 0x7b27b1d3, 0xcb9596dc, 0xaab6455d, 0xb72008ae, 0xb3ad741c, 0xa10773ef, 0x2b9de25f, 0x9b04f3b1, 0x48f88f87, 0xac828282, 0x821fc2f0, 0x6a2254af, 0xcaa8f9df, 0x3e3e4d70, 0x7b91940, 0x3c78f329, 0xe435b9ad, 0x6847643, 0x2c944a83, 0xc4a9dcdc, 0x9c0d5b6d, 0xa341cdb6, 0x358f7bc2, 0x13f3eab9, 0x6193621d, 0x159ed4a, 0x69a680c1, 0x4fb4a8, 0x70968761, 0x2f60fc5, 0x3937e5ac, 0x9b04f3b1, 0x1d26fc6f, 0x95fad386, 0x3937e5ac, 0x37c9c59b, 0xa341cdb6, 0xeb09f3ad, 0xa448656a, 0xab742f6a, 0x2090af1, 0xe435b9ad, 0xb26f1e2b, 0xecd468a4, 0x26cc20e7, 0x1ea22801, 0x64dc7498, 0x638602f4, 0x26b4c8b6, 0x61d8a4f3, 0xb15f099f, 0x88a078ba]

# print(len(crc_list))
res = b""
for item in crc_values:
res += crc32_reverse(item, 4)[0]
print(res)
with open("res.bin",'wb') as f:
f.write(res)

这里crc_values是上一个脚本得到的数据

就是这里要注意最后一个压缩包中的 txt 是 3 字节,需要单独处理一下,所有CRC爆破结果合起来是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
53 29 00 00 CB AE 02 00 CB 2C 00 00 53 31 04 00
D3 32 04 00 D3 32 02 00 D3 32 00 00 D3 32 06 00
33 D5 02 00 33 B2 04 00 D3 32 01 00 33 34 06 00
33 4D 04 00 33 4F 03 00 D3 32 00 00 D3 32 02 00
33 D3 02 00 33 D0 02 00 33 36 05 00 D3 32 00 00
33 31 04 00 33 D6 02 00 4B B6 00 00 33 36 05 00
B3 48 06 00 4B B5 04 00 4B 35 03 00 B3 30 05 00
B3 48 03 00 33 34 03 00 33 33 07 00 33 35 06 00
33 33 06 00 33 4F 01 00 4B 35 04 00 33 31 05 00
4B 31 00 00 4B B6 00 00 33 31 04 00 4B 4E 05 00
4B B5 04 00 4B 4D 03 00 4B 31 07 00 4B 4E 03 00
33 32 00 00 33 B0 00 00 4B 31 04 00 33 4E 05 00
33 35 05 00 33 4C 01 00 4B 31 05 00 B3 30 01 00
4B 32 03 00 B3 4C 06 00 4B 4C 05 00 33 B5 00 00
B3 34 05 00 4B 36 07 00 4B 49 03 00 33 31 05 00
4B 33 06 00 33 4A 03 00 4B 49 03 00 4B 32 05 00
33 4C 01 00 33 48 06 00 33 48 01 00 33 32 07 00
33 B6 00 00 33 32 00 00 33 32 06 00 B3 B4 00 00
B3 34 03 00 4B 31 06 00 4B 35 03 00 D3 52 01 00
D3 2F 00 00 CB AE 02 00 CB 2C 00 00 53 01 00

把上面的数据按每4字节一组解压,可以得到一个zip的hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import zlib

hex_data = "53290000CBAE0200CB2C000053310400D3320400D3320200D3320000D332060033D5020033B20400D332010033340600334D0400334F0300D3320000D332020033D3020033D0020033360500D33200003331040033D602004BB6000033360500B34806004BB504004B350300B3300500B348030033340300333307003335060033330600334F01004B350400333105004B3100004BB60000333104004B4E05004BB504004B4D03004B3107004B4E03003332000033B000004B310400334E050033350500334C01004B310500B33001004B320300B34C06004B4C050033B50000B33405004B3607004B490300333105004B330600334A03004B4903004B320500334C010033480600334801003332070033B600003332000033320600B3B40000B33403004B3106004B350300D3520100D32F0000CBAE0200CB2C0000530100"

data = bytes.fromhex(hex_data)

chunks = [data[i:i+4] for i in range(0, len(data), 4)]

out = b""
for c in chunks:
try:
out += zlib.decompress(c, -zlib.MAX_WBITS)
except:
print("bad chunk:", c.hex())

print(out.decode())

hash为:

1
$pkzip$1*1*2*0*35*29*4135a7f*0*26*0*35*0413*c8358ce9e6858f166753637de145d0c841cee9efd7cf2008d13e551dd584b69cae5895c7df45f32fdfb51d0c0d273820239896d3e6*$/pkzip$

参考2025 buckeyeCTF-zip2john2zip的wp:

https://github.com/cscosu/buckeyectf-2025-public/blob/master/forensics/zip2john2zip/solve/solve.py

已知三个内部密钥和哈希值,还原 zip 并解压缩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env python3

def pkcrc(x, b):
x = (x ^ b) & 0xFFFFFFFF
for _ in range(8):
if x & 1:
x = (x >> 1) ^ 0xedb88320
else:
x >>= 1
return x & 0xFFFFFFFF


def decrypt_stream_with_keys(enc, key0, key1, key2):

def _update_keys(byte_val):
nonlocal key0, key1, key2
key0 = pkcrc(key0, byte_val)
temp = (key1 + (key0 & 0xff)) & 0xFFFFFFFF
key1 = (((temp * 0x08088405) & 0xFFFFFFFF) + 1) & 0xFFFFFFFF
key2 = pkcrc(key2, (key1 >> 24) & 0xff)

def _get_keystream_byte():
nonlocal key2
temp = (key2 & 0xFFFF) | 3
return (((temp * (temp ^ 1)) & 0xFFFF) >> 8) & 0xff

out = bytearray()
for e in enc:
ks = _get_keystream_byte()
d = e ^ ks
out.append(d)
_update_keys(d)

return bytes(out)


if __name__ == "__main__":
# your recovered keys:
k0 = 0xb47e923c
k1 = 0x5aeb49a7
k2 = 0xa3cd7af0

# encrypted data from $pkzip$ hash
hash_text = "$pkzip$1*1*2*0*35*29*4135a7f*0*26*0*35*0413*c8358ce9e6858f166753637de145d0c841cee9efd7cf2008d13e551dd584b69cae5895c7df45f32fdfb51d0c0d273820239896d3e6*$/pkzip$"
enc_hex = hash_text.split('*')[12] # not 13 !
enc = bytes.fromhex(enc_hex)

plain = decrypt_stream_with_keys(enc, k0, k1, k2)
print(plain)
#b'\xcf\xecP\x1f\x89\x9e\x83q1"\xa4\x04flag{W0ww_th3_C@ske7|s_Tre4sur3_unl0cke9}'

得到flag{W0ww_th3_C@ske7|s_Tre4sur3_unl0cke9}