CodeShell.kr - Crossfeed

Challenge

音频与图片两份附件,题面说房间比消息本身更吵。难点在于载波藏在声道差里,且要先用 CRC 把三遍传输的位定下来,才能拿到读图所需的种子。

The room is louder than the message. Listen to the difference.

房间比消息本身更吵。听差分。

1
https://codeshell.kr/challenges/crossfeed/

Solution

附件 crossfeed-player.zipcrossfeed.wav(16 kHz 立体声)与 embers.png

Step 1:载波在声道差 L − R 里:用 1375 Hz 复载波下变频,对每个 640 样本窗口取 Y[end]-Y[start],实部符号即 bit。1375 Hz × 640/16000 = 55 个整周期/bit,所以窗口相位与位定时无关。

Step 2:用 24-bit 前导码 A5A5A5 在全部 640 个相位偏移上搜索,命中三个起点 bit 8 / 320 / 632(极性取反),间距 312 bit = 24(前导) + 256(32 个 hex 字符) + 32(CRC32)。

Step 3:三遍逐 bit 多数票得到 32 个 hex 字符,其 CRC32 与传输的 CRC 字段一致:

1
2
3
majority hex: 0eac9f1a71100af1cf783474dd36a45a
crc32(hex ascii) = a13dd6e7 == pass2 的 CRC 字段
seed = 0eac9f1a71100af1cf783474dd36a45a

Step 4:按 idx = BE32(SHA256(seed || BE32(counter))[:4]) % N(N = 384×384,命中的位置跳过、counter 不重置)读 embers.png 蓝通道 LSB,位流 MSB-first 组字节,得到长度字段 + payload + CRC32,校验通过。

Script

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
#!/usr/bin/env python3
"""CodeShell.kr - Crossfeed: demodulate the L-R phase carrier.

The payload rides in the channel difference L-R as a 1375 Hz carrier. The
choice of 1375 Hz matters: 1375 Hz x 640/16000 samples = exactly 55 cycles per
bit, so a 640-sample window correlation gives the same answer for every window
phase and the demodulation needs no bit-timing search at all.

Three passes are transmitted, each 312 bits: 24-bit preamble A5A5A5, then 256
bits (32 ASCII hex characters), then a CRC32 over those characters. The CRC32 is
what makes the recovered 32-character seed trustworthy, so it is checked before
the seed is used.
"""
import zlib

import numpy as np
import wave

WAV = "crossfeed.wav"
SPB = 640 # samples per bit
CARRIER = 1375.0 # Hz
PRE = 24 # preamble bits
HEX = 256 # 32 hex characters = 256 bits
CRC = 32 # CRC32 field
PASS = PRE + HEX + CRC # 312
FIRST = 8 # first pass starts at bit 8


def load(path=WAV):
w = wave.open(path)
nch, fs, n = w.getnchannels(), w.getframerate(), w.getnframes()
dtype = {2: "<i2", 4: "<i4"}[w.getsampwidth()]
d = np.frombuffer(w.readframes(n), dtype=dtype).reshape(-1, nch).astype(np.float64)
return d[:, 0] - d[:, 1], fs


def demod(x, fs):
"""One bit per SPB-sample window: sign of the window's complex sum."""
idx = np.arange(len(x))
y = x * np.exp(-2j * np.pi * CARRIER * idx / fs)
csum = np.concatenate([[0], np.cumsum(y)])
nbits = len(x) // SPB
starts = SPB * np.arange(nbits)
ends = starts + SPB
return ((csum[ends] - csum[starts]).real > 0).astype(np.uint8)


def to_bytes(bits):
return bytes(int("".join(map(str, bits[i:i + 8])), 2)
for i in range(0, len(bits) - 7, 8))


def main():
x, fs = load()
bits = demod(x, fs)
pre = np.array([int(c) for c in "".join(f"{v:08b}" for v in b"\xa5\xa5\xa5")],
dtype=np.uint8)

# locate the first pass, trying both polarities
for pol in (0, 1):
b = bits if pol == 0 else 1 - bits
for start in range(0, SPB):
if np.array_equal(b[start:start + PRE], pre):
print(f"preamble at bit {start}, polarity {'normal' if not pol else 'inverted'}")
break
else:
continue
break
else:
raise SystemExit("no preamble found")

# each pass is noisy at the bit level, so decode them leniently and rely on
# the majority vote plus the CRC32 field to decide the seed
passes = []
for p in range(3):
s = start + p * PASS
seg = b[s:s + PASS]
hx = to_bytes(seg[PRE:PRE + HEX]).decode("ascii", errors="replace")
crc = int.from_bytes(to_bytes(seg[PRE + HEX:PASS]), "big")
print(f"pass{p}: hex={hx!r} crc={crc:08x}")
passes.append(seg[PRE:PRE + HEX].astype(int))

maj = np.where(sum(passes) >= 2, 1, 0).astype(np.uint8)
print("bitwise disagreements:", int(sum((passes[0] != passes[1])
| (passes[0] != passes[2]))))
seed_hex = to_bytes(maj).decode("ascii")
print("majority hex:", seed_hex)
crc = int.from_bytes(to_bytes(b[start + 2 * PASS + PRE + HEX:
start + 2 * PASS + PASS]), "big")
calc = zlib.crc32(seed_hex.encode()) & 0xFFFFFFFF
print(f"crc32(seed) = {calc:08x} transmitted = {crc:08x} "
f"{'OK' if crc == calc else 'BAD'}")
return seed_hex


if __name__ == "__main__":
main()
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
#!/usr/bin/env python3
"""CodeShell.kr - Crossfeed: majority-vote the passes, then walk embers.png.

The three passes are bitwise majority-voted (their CRC32 fields must agree with
the transmitted CRC32, which is what makes the seed trustworthy). The seed then
drives a deterministic pixel walk over the image: index = BE32(SHA256(seed ||
BE32(counter))[:4]) mod N, already-used indices skipped without resetting the
counter, payload bit = blue LSB, packed MSB-first.
"""
import hashlib
import struct
import zlib

import numpy as np
from PIL import Image

SEED = bytes.fromhex("0eac9f1a71100af1cf783474dd36a45a")
IMAGE = "embers.png"


def walk(blue_flat, seed, nbits):
used, counter, bits = set(), 0, []
while len(bits) < nbits:
h = hashlib.sha256(seed + struct.pack(">I", counter)).digest()
counter += 1
idx = int.from_bytes(h[:4], "big") % len(blue_flat)
if idx in used:
continue
used.add(idx)
bits.append(int(blue_flat[idx]) & 1)
return bits


def main():
arr = np.array(Image.open(IMAGE))
bits = walk(arr[:, :, 2].reshape(-1), SEED, 8 * (2 + 4 + 200))
by = bytes(int("".join(map(str, bits[i:i + 8])), 2) for i in range(0, len(bits) - 7, 8))
ln = int.from_bytes(by[:2], "big")
payload, crc = by[2:2 + ln], int.from_bytes(by[2 + ln:6 + ln], "big")
print("length", ln)
print("payload", payload)
print("crc", f"{crc:08x}", "calc", f"{zlib.crc32(payload) & 0xffffffff:08x}",
"OK" if crc == zlib.crc32(payload) & 0xffffffff else "BAD")


if __name__ == "__main__":
main()
CodeShell{THE_QUIET_CHANNEL_BURNS}