完整 WP
这题按你给的思路,核心链路是:
ZIP 伪加密 -> 修标志位 -> 解出 task.mp4 -> 提取 audio.wav -> Echo Hiding 解码 -> Morse -> Atbash -> ISCC{...}
这份新附件最终跑出的结果是:
ISCC{GNU6+/W0KKF2(B/}
1. 识别 ZIP 伪加密
附件是 attachment-4 (1).zip。
它不是“真加密”,而是 ZIP 头里的加密标志位被置了。
我检查到:
- 本地文件头
PK\x03\x04的加密位是正常的 - 中央目录头
PK\x01\x02的加密位被置成了1
这会导致很多解压工具误以为它需要密码。
修法就是把这两个位置的最低位清零:
- 本地文件头:偏移
+6 - 中央目录头:偏移
+8
可用代码:
from pathlib import Path
def clear_zip_fake_encrypt(src: Path, dst: Path) -> None:
data = bytearray(src.read_bytes())
i = 0
while True:
j = data.find(b'PK\x03\x04', i)
if j < 0:
break
data[j + 6] &= 0xFE
i = j + 4
i = 0
while True:
j = data.find(b'PK\x01\x02', i)
if j < 0:
break
data[j + 8] &= 0xFE
i = j + 4
dst.write_bytes(data)
修完后就能正常解压,里面只有一个 task.mp4。
2. 从 MP4 提取音频
直接用 ffmpeg 抽出 PCM:
ffmpeg -y -i task.mp4 -vn -acodec pcm_s16le audio.wav
我实际提取到的是:
- 采样率:
44100 Hz - 双声道
int16- 长度约
1764352个采样点
3. Echo Hiding 解码
题目真正的信息不在画面,而在音频回声隐写里。
按你给的 wp 参数:
- 段长
ws = 2205 d0 = 100d1 = 130
处理方式:
- 左右声道取平均
- 每
2205个采样切一段 - 对每段做倒谱
- 比较
100附近和130附近哪个峰更高 - 高者对应
0/1
代码如下:
import numpy as np
import scipy.io.wavfile as wav
from pathlib import Path
def echo_decode(audio: Path, ws: int = 2205, d0: int = 100, d1: int = 130):
sr, x = wav.read(audio)
if x.ndim == 2 and x.shape[1] == 2:
sig = (x[:, 0].astype(np.float64) + x[:, 1].astype(np.float64)) / 2.0
else:
sig = x.astype(np.float64)
bits = []
for off in range(0, len(sig) - ws + 1, ws):
seg = sig[off:off + ws]
seg = seg - seg.mean()
cep = np.fft.irfft(np.log(np.abs(np.fft.rfft(seg)) ** 2 + 1e-10))
p0 = float(np.max(cep[max(1, d0 - 2):d0 + 3]))
p1 = float(np.max(cep[max(1, d1 - 2):d1 + 3]))
bits.append(0 if p0 > p1 else 1)
return bits
这份附件跑出来:
- 比特数:
800
4. 比特流转 ASCII / Morse
把比特按 8 位一组还原成字节:
def bits_to_raw(bits):
return bytes(
int(''.join(map(str, bits[i:i + 8])), 2)
for i in range(0, len(bits) - 7, 8)
)
题目里前面一段是纯 Morse 字符集,只包含:
.-- 空格
/
所以直接截到第一个非法字符前:
def extract_morse_prefix(raw: bytes) -> str:
end = 0
while end < len(raw) and raw[end] in b'.- /':
end += 1
return raw[:end].decode('ascii')
这份附件得到的 Morse 串是:
- -- ..-. -.... .-.-. -..-. -.. ----- .--. .--. ..- ..--- -.--. -.-- -..-.
5. Morse 解码
需要完整 Morse 表,尤其注意 .-... -> & 这一项有时会坑人。
不过这份附件实际没用到 &,但字典还是建议补全。
MORSE = {
'.-':'A','-...':'B','-.-.':'C','-..':'D','.':'E','..-.':'F','--.':'G',
'....':'H','..':'I','.---':'J','-.-':'K','.-..':'L','--':'M','-.':'N',
'---':'O','.--.':'P','--.-':'Q','.-.':'R','...':'S','-':'T','..-':'U',
'...-':'V','.--':'W','-..-':'X','-.--':'Y','--..':'Z',
'-----':'0','.----':'1','..---':'2','...--':'3','....-':'4','.....':'5',
'-....':'6','--...':'7','---..':'8','----.':'9',
'.-.-.-':'.','--..--':',','..--..':'?','-.-.--':'!','-....-':'-',
'-..-.':'/','-.--.':'(','-.--.-':')','.----.':"'",'---...':':',
'-.-.-.':';','.-.-.':'+','-...-':'=','..--.-':'_','.-..-.':'"',
'...-..-':'$','.--.-.':'@','.-...':'&',
}
def morse_decode(morse: str) -> str:
return ''.join(MORSE[tok] for tok in morse.split() if tok in MORSE)
解出来是:
TMF6+/D0PPU2(Y/
6. Atbash
按 wp 的方式,只对字母做 Atbash,其它字符原样保留:
def atbash_letters_only(s: str) -> str:
out = []
for c in s:
if 'A' <= c <= 'Z':
out.append(chr(ord('Z') - (ord(c) - ord('A'))))
elif 'a' <= c <= 'z':
out.append(chr(ord('z') - (ord(c) - ord('a'))))
else:
out.append(c)
return ''.join(out)
对
TMF6+/D0PPU2(Y/
做 Atbash 得到:
GNU6+/W0KKF2(B/
最后套壳:
ISCC{GNU6+/W0KKF2(B/}
7. 一把梭脚本
from __future__ import annotations
from pathlib import Path
import shutil
import zipfile
import tempfile
import subprocess
import numpy as np
import scipy.io.wavfile as wav
MORSE = {
'.-':'A','-...':'B','-.-.':'C','-..':'D','.':'E','..-.':'F','--.':'G',
'....':'H','..':'I','.---':'J','-.-':'K','.-..':'L','--':'M','-.':'N',
'---':'O','.--.':'P','--.-':'Q','.-.':'R','...':'S','-':'T','..-':'U',
'...-':'V','.--':'W','-..-':'X','-.--':'Y','--..':'Z',
'-----':'0','.----':'1','..---':'2','...--':'3','....-':'4','.....':'5',
'-....':'6','--...':'7','---..':'8','----.':'9',
'.-.-.-':'.','--..--':',','..--..':'?','-.-.--':'!','-....-':'-',
'-..-.':'/','-.--.':'(','-.--.-':')','.----.':"'",'---...':':',
'-.-.-.':';','.-.-.':'+','-...-':'=','..--.-':'_','.-..-.':'"',
'...-..-':'$','.--.-.':'@','.-...':'&',
}
FFMPEG = r"C:\Users\ericgao\AppData\Local\Programs\Python\Python313\Lib\site-packages\imageio_ffmpeg\binaries\ffmpeg-win-x86_64-v7.1.exe"
def clear_zip_fake_encrypt(src: Path, dst: Path) -> None:
data = bytearray(src.read_bytes())
i = 0
while True:
j = data.find(b'PK\x03\x04', i)
if j < 0:
break
data[j + 6] &= 0xFE
i = j + 4
i = 0
while True:
j = data.find(b'PK\x01\x02', i)
if j < 0:
break
data[j + 8] &= 0xFE
i = j + 4
dst.write_bytes(data)
def extract_audio(zip_path: Path, outdir: Path) -> Path:
fixed_zip = outdir / "fixed.zip"
mp4 = outdir / "task.mp4"
audio = outdir / "audio.wav"
clear_zip_fake_encrypt(zip_path, fixed_zip)
with zipfile.ZipFile(fixed_zip) as zf:
with zf.open("task.mp4") as src, open(mp4, "wb") as dst:
shutil.copyfileobj(src, dst)
subprocess.run(
[FFMPEG, "-y", "-v", "error", "-i", str(mp4), "-vn", "-acodec", "pcm_s16le", str(audio)],
check=True,
)
return audio
def echo_decode(audio: Path, ws: int = 2205, d0: int = 100, d1: int = 130):
sr, x = wav.read(audio)
if x.ndim == 2 and x.shape[1] == 2:
sig = (x[:, 0].astype(np.float64) + x[:, 1].astype(np.float64)) / 2.0
else:
sig = x.astype(np.float64)
bits = []
for off in range(0, len(sig) - ws + 1, ws):
seg = sig[off:off + ws]
seg = seg - seg.mean()
cep = np.fft.irfft(np.log(np.abs(np.fft.rfft(seg)) ** 2 + 1e-10))
p0 = float(np.max(cep[max(1, d0 - 2):d0 + 3]))
p1 = float(np.max(cep[max(1, d1 - 2):d1 + 3]))
bits.append(0 if p0 > p1 else 1)
return bits
def bits_to_morse(bits):
raw = bytes(
int(''.join(map(str, bits[i:i + 8])), 2)
for i in range(0, len(bits) - 7, 8)
)
end = 0
while end < len(raw) and raw[end] in b'.- /':
end += 1
return raw[:end].decode("ascii")
def morse_decode(morse: str) -> str:
return ''.join(MORSE[tok] for tok in morse.split() if tok in MORSE)
def atbash_letters_only(s: str) -> str:
out = []
for c in s:
if 'A' <= c <= 'Z':
out.append(chr(ord('Z') - (ord(c) - ord('A'))))
elif 'a' <= c <= 'z':
out.append(chr(ord('z') - (ord(c) - ord('a'))))
else:
out.append(c)
return ''.join(out)
def solve_one(zip_path: Path) -> str:
with tempfile.TemporaryDirectory() as td:
td = Path(td)
audio = extract_audio(zip_path, td)
bits = echo_decode(audio)
morse = bits_to_morse(bits)
decoded = morse_decode(morse)
body = atbash_letters_only(decoded)
return f"ISCC{{{body}}}"
if __name__ == "__main__":
zip_path = Path(r"C:\Users\ericgao\Desktop\test\attachment-4 (1).zip")
print(solve_one(zip_path))
8. 最终答案
ISCC{GNU6+/W0KKF2(B/}
评论