第八届强网拟态决赛-Misc

本文最后更新于 2025年11月30日 下午

Misc

泄漏的时间与电码(三血)

把log和chal的加密脚本直接扔给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
def solve():
timings = [
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
]

class LFSR:
def __init__(self):
self.lfsr = 0x92

def step(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

machine = LFSR()
flag = ""
inv_31 = pow(31, -1, 256)

print(f"{'Time':<12} | {'Ops':<5} | {'Char':<5}")
print("-" * 30)

for t in timings:
ops = round(t / 0.005)

if ops % 2 == 0:
base_ops = ops - 10
else:
base_ops = ops - 10 - 30

val = ((base_ops - 85) * inv_31) & 0xFF

k = machine.step()
char_code = val ^ k
char = chr(char_code)

flag += char
# 打印时间对应关系
print(f"{t:<12.6f} | {ops:<5} | {char:<5}")

print("-" * 30)
print(f"[+]Recovered: \n{flag}")


if __name__ == "__main__":
solve()

2025-11-28123536

之后没思路无能狂怒了一天

官方hint的内容是ModR/M

已知给的chal的elf文件就说加密脚本直接在linux环境编译的,尝试反编译未找到可利用的内容,利用ai检索信息ModR/M,问了隐写得到了一个项目

woodruffw/steg86: Hiding messages in x86 programs using semantic duals

猜测可能是,装一下环境

2025-11-28091510

得到了这个内容,把密文和字符表扔个ai

2025-11-28123949

Vim 编辑器进行宏操作验证的一个题目

标准的绝密压缩(三血)

题目给了一个流量包,

简单的tcp分析发现是有png的hex流的

1
tshark -r capture.pcapng -Y "tcp" -T fields -e data >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
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 binascii
import re

def extract_pngs(input_file):
with open(input_file, 'r') as f:
lines = f.readlines()

reassembled_hex_string = ""

for line in lines:
line = line.strip()
if not line:
continue

try:
payload_bytes = binascii.unhexlify(line)
payload_str = payload_bytes.decode('utf-8')

if re.match(r'^[0-9a-fA-F]+$', payload_str):
reassembled_hex_string += payload_str

except (UnicodeDecodeError, binascii.Error):

continue

try:
full_binary = binascii.unhexlify(reassembled_hex_string)
except binascii.Error:
print("Error converting reassembled string to binary. Check data integrity.")
return


png_signature = b'\x89PNG\r\n\x1a\n'


parts = full_binary.split(png_signature)

count = 0
for i, part in enumerate(parts):
if i == 0:

continue

png_data = png_signature + part

iend_marker = b'IEND'
iend_index = png_data.find(iend_marker)

if iend_index != -1:
end_of_file = iend_index + 4 + 4
png_data = png_data[:end_of_file]


filename = f'extracted_{count}.png'
with open(filename, 'wb') as out:
out.write(png_data)
print(f'[+] Extracted: {filename}')
count += 1

extract_pngs('1.txt')

提取出来发现是1kb规则的图片,看不出任何名堂,于是放弃(中途三个小时去看了其他的)

最终还是回归png的,尝试了

1
pngcheck extracted_*

30张图片都是有问题的,且都是可打印字符,那么就是朝着png想,翻了翻lunatic师傅的博客,检索到了可能是idat隐写,手动调整一下

2025-11-27184405

超绝震撼,把30条信息都提取出来,最后的信息是

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
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.

中间的解码可以拿到压缩包

2025-11-27184642

一眼明文攻击,这里回归文本

1
2
3
4
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.

那么sample.txt就是需要社工的地方,找到对应文本内容

Him.Html | Minecraft CreepyPasta Wiki | Fandom

这里就是拷打ai进行网页社工找到了对应内容

1
Him.HTML was a website that a Steve face with realistic eyes and a jumble of numbers and letters. When sorted it said this: 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.

文件大小略有出路,最终的内容是

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.

这里利用7z制作压缩包

2025-11-27185222

利用ARCHPR进行攻击解压同时得到密钥

1
[ b47e923c 5aeb49a7 a3cd7af0 ]

rule文本的内容是

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

仔细分析一下内容(这里有点脑王争霸),发现端口30012-30091之间的区别与前面png传输的内容,拼接起来是乱码,尝试发现是AES加密

2025-11-27145000

那么就可以写个脚本提取出所有的压缩包了

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
118
119
120
121
122
123
124
125
126
127
128
129
130
import sys
import hashlib
from Crypto.Cipher import AES


def aes_ecb_decrypt(key_utf8, cipher_hex):
"""
使用 UTF-8 编码的密钥字符串进行 AES ECB 解密
key_utf8: 普通字符串(如MD5的hex结果)
cipher_hex: 十六进制密文字符串
"""
key_bytes = key_utf8.encode('utf-8') # 直接转成字节
cipher_bytes = bytes.fromhex(cipher_hex) # 密文hex转字节

cipher = AES.new(key_bytes, AES.MODE_ECB)
plaintext = cipher.decrypt(cipher_bytes)
return plaintext


def unpad(s):
"""去除 PKCS7 填充"""
padding_len = s[-1]
if padding_len < 1 or padding_len > 16:
return s
return s[:-padding_len]


def parse_traffic_data(filename):
results = {}

try:
with open(filename, 'r', encoding='utf-8') as f:
lines = f.readlines()

print(f"读取文件: {filename}, 共 {len(lines)} 行\n")

for i, line in enumerate(lines, 1):
line = line.strip()
if not line:
continue

parts = line.split()

if len(parts) < 3:
continue

try:
src_port = int(parts[0])
if 30012 <= src_port <= 30091:
hex_data = parts[2]

if src_port not in results:
results[src_port] = []

results[src_port].append(hex_data)

except ValueError:
continue

print(f"找到 {len(results)} 个端口的数据\n")
print("=" * 70)

# 解密并保存
for port in sorted(results.keys()):
encrypted_hex = ''.join(results[port])
decrypt_and_save(port, encrypted_hex)
print("=" * 70)

except FileNotFoundError:
print(f"错误: 找不到文件 {filename}")
except Exception as e:
print(f"错误: {type(e).__name__}: {str(e)}")
import traceback
traceback.print_exc()


def decrypt_and_save(port, encrypted_hex):
"""解密单个端口的数据并保存为 ZIP"""
try:
print(f"\n端口: {port}")

# 1. 端口号 → MD5 → hex字符串作为密钥
port_str = str(port)
md5_hash = hashlib.md5(port_str.encode('utf-8')).hexdigest()
key_utf8 = md5_hash # MD5的hex结果作为UTF-8字符串密钥

print(f"端口字符串: '{port_str}'")
print(f"MD5密钥 (hex): {key_utf8}")
print(f"密钥长度: {len(key_utf8)} 字符")

# 2. 解密
print(f"密文长度: {len(encrypted_hex)} 字符 ({len(encrypted_hex) // 2} 字节)")

decrypted = aes_ecb_decrypt(key_utf8, encrypted_hex)
print(f"解密后长度: {len(decrypted)} 字节")

# 3. 去除填充
decrypted = unpad(decrypted)
print(f"去填充后长度: {len(decrypted)} 字节")

# 4. 检查 ZIP 文件头
if decrypted[:4] == b'PK\x03\x04':
print("✓ 检测到 ZIP 文件头")
else:
print(f"⚠ 前4字节: {decrypted[:4].hex()} (ZIP应为 504b0304)")

# 5. 保存为 ZIP 文件
output_filename = f"{port}.zip"
with open(output_filename, 'wb') as f:
f.write(decrypted)

print(f"✓ 成功保存: {output_filename}")

except Exception as e:
print(f"✗ 解密端口 {port} 失败: {type(e).__name__}: {str(e)}")
import traceback
traceback.print_exc()


if __name__ == "__main__":
target_file = '1.txt'
if len(sys.argv) > 1:
target_file = sys.argv[1]

print("=" * 70)
print("AES 流量数据解密工具")
print("=" * 70)
print()

parse_traffic_data(target_file)

得到对应的压缩包,这里仔细分析压缩包里的内容是4字节,应该是CRC爆破,有不可见字符参考lunatic师傅的脚本

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
118
119
120
121
122
import os
import zipfile
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):
for i in range(256):
for j in range(8):
if i & 1:
i >>= 1
i ^= poly
else:
i >>= 1
self.table.append(i)
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))

def calc(self, data, accum=0):
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):
self.init_tables(self.poly)
desired = self.crc32
accum = self.accum
result_list = []

if self.length >= 4:
patches = self.find_reverse(desired, accum)
for item in self.dfs(self.length - 4):
patch = list(item)
patches = self.find_reverse(desired, self.calc(patch, accum))
for last4 in patches:
patch.extend(last4)
if self.calc(patch, accum) == 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):
return CRC32Reverse(crc32, length, char_set, poly, accum).run_reverse()


def scan_zip_crc(dirname, output_bin="output.bin", brute_length=4):
results = []

for root, dirs, files in os.walk(dirname):
for fname in files:
if fname.lower().endswith(".zip"):
zip_path = os.path.abspath(os.path.join(root, fname))
zip_path = zip_path.replace("\\", "/")

print(f"[+] 发现压缩包:{zip_path}")

try:
with zipfile.ZipFile(zip_path) as z:
for info in z.infolist():
crc = info.CRC
print(f" → 文件头:{info.filename} CRC32={crc:08X}")

res = crc32_reverse(crc, brute_length)
print(f" 逆推得到 {len(res)} 条结果")

results.extend(res)
except Exception as e:
print(f"[-] 无法读取 ZIP:{zip_path}, 错误:{e}")

# 写入 bin
with open(output_bin, "wb") as f:
for r in results:
f.write(r)

print(f"[+] 已写入 {len(results)} 条结果 → {output_bin}")


if __name__ == "__main__":
scan_zip_crc(".", output_bin="final_output.bin", brute_length=4)

然后根据一开始的提示deflated了,那么我们inflate一下,搓个脚本得到了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import zlib

with open("final_output.bin", "rb") as f:
data = f.read()

all_out = b""

# 假设每块是 4 字节
for i in range(0, len(data), 4):
block = data[i:i+4]
try:
# raw deflate 解压
out = zlib.decompress(block, wbits=-15)
all_out += out
except Exception:
pass # 有些组合可能不是真正的 deflate 数据

# 最终拼接成一句话
try:
print(all_out.decode()) # 如果是 ASCII/UTF-8 文本
except UnicodeDecodeError:
print(all_out) # 如果包含非文本字节,直接打印 bytes

得到了一个hash的值

分析一下hash,算是zipCrypto的特性,里面文本内容较短,在进行hash转储的时候会将内容全部放入到hash中进行加密,从开始的文本聊天得知,这边的密钥可以进行复用,在已知hash和key后可以hash反推压缩包得到flag的内容,脚本如下

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import binascii
import zlib
from typing import Tuple, Optional


class ZipCrypto:
CRC32_POLYNOMIAL = 0xEDB88320
MAGIC_MULT = 134775813
MAGIC_ADD = 1

def __init__(self, key0: int, key1: int, key2: int):
self.keys = [key0, key1, key2]
self.crc_table = self._build_crc_table()

def _build_crc_table(self) -> list:
table = []
for i in range(256):
crc = i
for _ in range(8):
if crc & 1:
crc = (self.CRC32_POLYNOMIAL ^ (crc >> 1)) & 0xFFFFFFFF
else:
crc = (crc >> 1) & 0xFFFFFFFF
table.append(crc)
return table

def _crc32(self, old_crc: int, byte: int) -> int:
return (self.crc_table[(old_crc ^ byte) & 0xFF] ^ (old_crc >> 8)) & 0xFFFFFFFF

def _update_keys(self, plaintext_byte: int):
self.keys[0] = self._crc32(self.keys[0], plaintext_byte)
self.keys[1] = (self.keys[1] + (self.keys[0] & 0xFF)) & 0xFFFFFFFF
self.keys[1] = (self.keys[1] * self.MAGIC_MULT + self.MAGIC_ADD) & 0xFFFFFFFF
self.keys[2] = self._crc32(self.keys[2], (self.keys[1] >> 24) & 0xFF)

def _decrypt_byte(self) -> int:
temp = (self.keys[2] | 3) & 0xFFFFFF
return ((temp * (temp ^ 1)) >> 8) & 0xFF

def decrypt(self, ciphertext: bytes) -> bytes:
plaintext = bytearray()
for cipher_byte in ciphertext:
keystream_byte = self._decrypt_byte()
plain_byte = cipher_byte ^ keystream_byte
self._update_keys(plain_byte)
plaintext.append(plain_byte)
return bytes(plaintext)


def extract_encrypted_data(pkzip_hash: str) -> Optional[bytes]:
clean = pkzip_hash.replace("$pkzip$", "").replace("$/pkzip$", "").strip()
parts = clean.split('*')

hex_data = max(
(p for p in parts if all(c in '0123456789abcdefABCDEF' for c in p)),
key=len,
default=""
)

if not hex_data:
return None

try:
return binascii.unhexlify(hex_data)
except binascii.Error:
return None


def try_decompress(data: bytes) -> Tuple[bool, bytes]:

try:
# -15 表示 raw deflate(不含 zlib 头)
decompressed = zlib.decompress(data, -15)
return True, decompressed
except zlib.error:
return False, data


def display_result(data: bytes, label: str = "内容"):
print(f"\n{'=' * 60}")
print(f"[{label}]")
print('=' * 60)

try:
text = data.decode('utf-8')
print(text)
except UnicodeDecodeError:
print(f"Raw: {repr(data)}")
print(f"Hex: {binascii.hexlify(data).decode()}")

print('=' * 60)


def main():
TARGET_HASH = "$pkzip$1*1*2*0*35*29*4135a7f*0*26*0*35*0413*c8358ce9e6858f166753637de145d0c841cee9efd7cf2008d13e551dd584b69cae5895c7df45f32fdfb51d0c0d273820239896d3e6*$/pkzip$"

K0 = 0xb47e923c
K1 = 0x5aeb49a7
K2 = 0xa3cd7af0

print("ZIP Crypto 解密工具")
print(f"密钥状态: K0={hex(K0)}, K1={hex(K1)}, K2={hex(K2)}")

encrypted = extract_encrypted_data(TARGET_HASH)
if encrypted is None:
print("\n[错误] 无法从 hash 中提取加密数据")
return

print(f"\n[*] 提取到 {len(encrypted)} 字节加密数据")
print(f" 前 20 字节: {binascii.hexlify(encrypted[:20]).decode()}...")

crypto = ZipCrypto(K0, K1, K2)
decrypted = crypto.decrypt(encrypted)

if len(decrypted) <= 12:
print("\n[错误] 解密数据过短(需要 >12 字节)")
return

actual_data = decrypted[12:]
print(f"[*] 移除加密头后剩余 {len(actual_data)} 字节")

display_result(actual_data, "解密内容 (Stored)")

is_compressed, final_data = try_decompress(actual_data)
if is_compressed:
print("\n[!] 检测到 DEFLATE 压缩数据")
display_result(final_data, "解压后内容")
else:
print("\n[*] 数据未压缩(使用 Stored 方法)")


if __name__ == "__main__":
main()

2025-11-28173456

返璞归真(一血)

给的压缩包开头有提示hashisk3y

这里直接伪加密解压得到图片,利用foremost进行分离得到dmp,利用项目

PaperBack

进行分离拿到文本内容,这里是23字节,卡了一会,最后是猜测是RC4,将可能的hash值都运行下来,直接扔个ai分析得到flag

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
import base64
import binascii

# 1. 密文
cipher_b64 = "jNX+xu2QKBm23AUlwClt+3xDkQcJGjM="
cipher_data = base64.b64decode(cipher_b64)

# 2. 密钥池 (来源于你提供的文件 Hash)
# 格式: (描述, MD5 Hex字符串)
candidates = [
("Hint String (hashisk3y)", "298e85501e91238426e2e542d9925232"), # MD5("hashisk3y")
("00000000.jpg", "001a62ee54d1c28a8b769ab5499011cb"),
("00000698.bmp", "5166b497bb66913fc0e23fd82980538f"),
("image.jpg", "652365effd5138036c26382004b8d6bd"),
("返璞归真.zip", "369f792f2edf63102fe12e1951ec899f")
]


# RC4 实现
def rc4(data, key):
# 确保 key 是 bytes
if isinstance(key, str):
key = key.encode()

S = list(range(256))
j = 0
out = []

# KSA
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]

# PRGA
i = j = 0
for char in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
out.append(char ^ S[(S[i] + S[j]) % 256])

return bytes(out)


print(f"--- 开始 RC4 批量爆破 (尝试 {len(candidates) * 2} 种组合) ---")

found = False

for label, md5_hex in candidates:
# 方式 A: 密钥就是 32位的 Hex 字符串 (例如 "001a...")
key_str = md5_hex
res_a = rc4(cipher_data, key_str)

# 方式 B: 密钥是 Hex 转换后的 16位 原始字节 (例如 \x00\x1a...)
try:
key_bytes = binascii.unhexlify(md5_hex)
res_b = rc4(cipher_data, key_bytes)
except:
res_b = b""

# 检查结果 A
try:
text_a = res_a.decode('utf-8')
if "flag" in text_a.lower() or "ctf" in text_a.lower():
print(f"\n🔥🔥🔥 [SUCCESS] 找到 Flag! 🔥🔥🔥")
print(f"来源: {label}")
print(f"密钥类型: Hex String (32 chars)")
print(f"Key : {key_str}")
print(f"Flag: {text_a}")
found = True
break
except:
pass # 解码失败说明不是 flag

# 检查结果 B
try:
text_b = res_b.decode('utf-8')
if "flag" in text_b.lower() or "ctf" in text_b.lower():
print(f"\n🔥🔥🔥 [SUCCESS] 找到 Flag! 🔥🔥🔥")
print(f"来源: {label}")
print(f"密钥类型: Raw Bytes (16 bytes)")
print(f"Key (Hex): {md5_hex}")
print(f"Flag: {text_b}")
found = True
break
except:
pass

if not found:
print("未能解出包含 'flag' 字样的结果。")
print("调试输出 (看看有没有类似 flag 的乱码):")
# 打印一下 hashisk3y 的结果看看样子
print(f"Try 1: {rc4(cipher_data, candidates[0][1]).decode('utf-8', 'ignore')}")

2025-11-28125207

猫咪电台(一血)

题目给了一个图片和一个wav

2025-11-28133936

图片里面应该有东西,分析一下lsb隐写,提取出来

2025-11-28164835

去掉开头fffe得到图片

1

2025-11-28165227


wav用010分析后面有一个巨大的压缩包,分离出来。

根据文件夹的提示,以及尝试发现1.wav不是sstv后联想到是无线电的音频

问ai有哪些工具可以进行尝试

2025-11-28153035

2025-11-28152910

得到了part1 R77YM30W1SFUN

这个密码可以解压得到2.wav

2.wav是个很杂的音,双声道

2025-11-28153304

1
2
# 这条命令的意思是:新建一个文件,内容是 (通道1 - 通道2)
sox 2.wav -c 1 recovered.wav remix 1v0.5 2v-0.5

但还是杂音

回头看了一下文件的hz

1
2
file 2.wav
2.wav: MBWF/RF64 audio, stereo 2400000 Hz

2025-11-28153715

跑出个脚本

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
import numpy as np
from scipy.io import wavfile
from scipy.signal import resample_poly, decimate


def auto_tune_and_demod(filename):
print(f"[*] 正在读取高采样率文件 {filename} (可能需要一点时间)...")
fs, data = wavfile.read(filename)

# 确保是浮点数并归一化
data = data.astype(np.float32)
data /= np.max(np.abs(data))

# 组合成 IQ 信号
# 注意:视不同设备,Q 可能需要反转。如果听不清,可以把下行改为 data[:, 0] - 1j * data[:, 1]
iq = data[:, 0] + 1j * data[:, 1]

print(f"[*] 原始采样率: {fs} Hz. 正在寻找最强信号频率...")

# 1. 寻找最强频率 (FFT)
# 取中间一段样本分析,避免头部尾部噪音
n_fft = 1024 * 16
start = len(iq) // 2
slice_iq = iq[start: start + n_fft]

spectrum = np.fft.fftshift(np.fft.fft(slice_iq))
freqs = np.fft.fftshift(np.fft.fftfreq(len(slice_iq), 1 / fs))

# 忽略 DC (0Hz) 附近的峰值,因为那通常是硬件直流偏置
dc_mask = (np.abs(freqs) > 2000)
valid_indices = np.where(dc_mask)

peak_idx = valid_indices[0][np.argmax(np.abs(spectrum)[valid_indices])]
target_freq = freqs[peak_idx]

print(f"[!] 锁定最强信号频率: {target_freq:.2f} Hz")

# 2. 移频 (Frequency Shift)
# 将目标频率移动到 0 Hz
print("[*] 正在调谐 (移频)...")
t = np.arange(len(iq)) / fs
shifted_iq = iq * np.exp(-1j * 2 * np.pi * target_freq * t)

# 3. 降采样 (Decimation)
# 目标是 48kHz 音频,所以降采样因子 = 2.4M / 48k = 50
target_fs = 48000
decimation_factor = int(fs / target_fs)

print(f"[*] 正在降采样 (因子: {decimation_factor})...")
# 使用 decimate 进行低通滤波 + 抽取
# complex=True 处理复数
downsampled_iq = decimate(shifted_iq, decimation_factor, ftype='fir')

# 4. NFM 解调
print("[*] 正在进行 NFM 解调...")
phase = np.angle(downsampled_iq)
audio = np.diff(np.unwrap(phase))

# 保存
out_name = "final_decoded.wav"
audio = audio / np.max(np.abs(audio)) * 32767
wavfile.write(out_name, target_fs, audio.astype(np.int16))

print(f"[+] 成功!已保存为: {out_name}")
print("[!] 请播放该文件,如果是人声/拨号声,Flag就在其中!")


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

发现中间是有类似sstv的东西的,用工具跑看看

2025-11-28154949

是图片不过需要去噪啊

然后写了一个稍微能看到一点的

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
import numpy as np
from scipy.io import wavfile
from scipy.signal import resample_poly, decimate, butter, lfilter

def butter_bandpass(lowcut, highcut, fs, order=5):
nyq = 0.5 * fs
low = lowcut / nyq
high = highcut / nyq
b, a = butter(order, [low, high], btype='band')
return b, a

def butter_bandpass_filter(data, lowcut, highcut, fs, order=5):
b, a = butter_bandpass(lowcut, highcut, fs, order=order)
y = lfilter(b, a, data)
return y

def auto_tune_and_demod(filename):
print(f"[*] 正在读取高采样率文件 {filename} ...")
fs, data = wavfile.read(filename)

# 转为浮点并归一化
data = data.astype(np.float32)
data /= np.max(np.abs(data))

# 组合成 IQ 信号
iq = data[:, 0] + 1j * data[:, 1]

print(f"[*] 原始采样率: {fs} Hz. 正在寻找最强信号频率...")

# 1. 寻找最强频率
n_fft = 1024 * 16
start = len(iq) // 2
slice_iq = iq[start: start + n_fft]
spectrum = np.fft.fftshift(np.fft.fft(slice_iq))
freqs = np.fft.fftshift(np.fft.fftfreq(len(slice_iq), 1 / fs))

# 忽略 DC 附近的噪音
dc_mask = (np.abs(freqs) > 2000)
valid_indices = np.where(dc_mask)
peak_idx = valid_indices[0][np.argmax(np.abs(spectrum)[valid_indices])]
target_freq = freqs[peak_idx]

print(f"[!] 锁定最强信号频率: {target_freq:.2f} Hz")

# 2. 移频
print("[*] 正在调谐 (移频)...")
t = np.arange(len(iq)) / fs
shifted_iq = iq * np.exp(-1j * 2 * np.pi * target_freq * t)

# 3. 降采样
target_fs = 48000
decimation_factor = int(fs / target_fs)
print(f"[*] 正在降采样 (因子: {decimation_factor})...")
downsampled_iq = decimate(shifted_iq, decimation_factor, ftype='fir')

# 4. NFM 解调
print("[*] 正在进行 NFM 解调...")
phase = np.angle(downsampled_iq)
audio = np.diff(np.unwrap(phase))

# 5. SSTV 专用去噪 (带通滤波 1100Hz - 2500Hz)
print("[*] 正在应用 SSTV 专用滤波器 (1100Hz - 2500Hz)...")
audio_filtered = butter_bandpass_filter(audio, 1100, 2500, target_fs, order=6)

# 保存
out_name = "final_sstv_clean.wav"
# 重新归一化
audio_filtered = audio_filtered / np.max(np.abs(audio_filtered)) * 32000
wavfile.write(out_name, target_fs, audio_filtered.astype(np.int16))

print(f"[+] 成功!已生成干净的 SSTV 音频: {out_name}")
print("[!] 提示: 解码时如果图片还是斜的,请尝试在软件里调整 'Slant Correction' 或 'Auto Slant'。")

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

2025-11-28155414

出的时候试了好几次,最后应该是2nd part _C4T9L1V3S

1
flag{Ci4l10~R77YM30W1SFUN_C4T9L1V3S}

后话

赛中还研学了一下web层层加码,n4cl师傅payload一穿三太强了,可惜3.2过于恶心。

主办方过于富裕,线下比赛体感最好的一集,和很多SU的伙伴见面约饭了。这里还是宣传一波,欢迎各位有意加入战队的师傅们投递简历到SUsu-team。战队采用的是candidate过渡考察的机制,进入我们的Discord打几周之后考察转正(问了一下处在candidate的师傅们起初觉得我们可能审核比较严,闹大误会了),祝SU全年起飞,偶尔摸鱼。


第八届强网拟态决赛-Misc
https://0ran9ewww.github.io/2025/11/30/qwnt/wp/
作者
orange
发布于
2025年11月30日
更新于
2025年11月30日
许可协议