CodeShell.kr - Parallax

Challenge

64 个 48×48 plate,自带完整的组装契约。难点不在组装而在载荷:数据位被写成奇数值,按常规读 LSB 只会得到全零。

Nothing is aligned

什么都没对齐。

1
https://codeshell.kr/challenges/infinity-parallax/

Solution

64 个 48×48 plate,自带完整 contract.txt

1
2
3
4
5
6
7
8
64 plates; rotations only. Match equal edge signatures; zero is outside.
Read sides N,E,S,W in increasing image-coordinate order, 36 symbols per edge,
3 two-bit symbols per byte (low first). For each assembled plate use
random.Random(SHA256(concatenated edges).digest()) to shuffle range(256);
first 16 indices address a 16x16 patch at (8,8). Read red LSB, most
significant bit first, then XOR with SHA256(edges+b"ink")[:2]. Join plates in
row-major order. The valid orientation starts with INFINITY, then 32 bytes,
then their SHA256.

Step 1:边符号的位置与字母表。逐像素 dump 一侧发现每条边只有索引 6..41(36 个)取特殊值,两端 0–5 与 42–47 是装饰渐变:

1
2
N row0: 94 90 88 84 82 78 | 112 64 112 112 160 ... 160 64 | 96 92 90 86 84 80
\____ frame _____/ \_____ 36 symbols ______/ \____ frame _____/

字母表恰好 4 个值 {64,112,160,208} → 2 bit。

Step 2:组装。统计确认 32 条全零边(正是 8×8 网格外框的 32 条),且每个 非零签名恰好出现 2 次(896 条边配对无歧义)→ 组装被完全约束,DFS 逐格放置即可。

Step 3:关键突破:数据位是「奇数」。按 contract 直读 (8,8) 的 16×16 补丁 第一次得到全 0,因为那片区域的红通道全是偶数。逐像素扫全图才看到真相:每片 红通道只有 6–12 个奇数值,且它们全落在某个 16×16 窗口内:

1
2
3
07df7112c77282ca: 窗口内奇数个数 [0, 0, 0, 6]  -> 唯一旋转 = 3
0e0f818c3b3449d2: [0, 0, 8, 0] -> 唯一旋转 = 2
24b02a0b657d8146: [8, 0, 0, 0] -> 唯一旋转 = 0

背景渐变全偶、数据写成奇数,所以每片的旋转由它自身内容唯一确定,不用从边 匹配去猜。旋转直方图 {0:13, 1:17, 2:19, 3:15} 分布均匀。

Step 4:用子集判据定死映射与打包。数据位必须是 shuffle 选中的那 16 个补丁 位置;256 个位置里只有 6–12 个奇数,随机 16 子集恰好覆盖它们概率可忽略,所以这是 判决性检验:

1
2
3
rank per_byte=3  -> 所有奇数像素都在选中的 16 个之内   ✓
rank per_byte=4 -> 失败 hi4 per_byte=3 -> 失败
hi4 per_byte=4 -> 失败

一次锁定:映射 = rank 序(64→0, 112→1, 160→2, 208→3),打包 = 每字节 3 个 2-bit 符号、低位在前(每边 12 字节)。

Step 5:提取。每片:四边字节拼成 blob(48 字节)→ random.Random(SHA256(blob).digest())range(256) → 前 16 个索引定位补丁像素 → 红 LSB 按 MSB-first 组 2 字节 → 与 SHA256(blob + b"ink")[:2] 异或。行优先拼接, 全局朝向 4 种里只有一种给出 INFINITY

1
$ uv run python solvers/parallax2.py
1
2
3
4
gdelta=0: b'INFINITY\xbe\xf8Nn'      <- 唯一命中
payload: bef84e6e5bfbf4ceac37fa07e39af5b2272f730f5a743973bd4a2e7285a89f8e
sha256 : f7f7b273c2e227d1e2566d8f647241cb48a4049f1afcad683427df85639db053
check : True (流末尾 32 字节 == payload 的 SHA256)

两个坑(8,8) 补丁只有在正确旋转之后的坐标系里才落着数据,直接读原始 像素只会得到全 0;另外 hi4 映射((v>>4)&3)看起来更"自然"(64/112/160/208 的 高 nibble 是 4/7/10/13),但它与 rank 序只差一个 bit 反转,只有子集判据能区分。

Crypto

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
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/usr/bin/env python3
"""CodeShell.kr — infinity-parallax: solve with data-located rotations.

Decisive finding (parallax_oddbits.py): the red channel of every plate is an
otherwise-all-EVEN gradient, and the data bits are written as ODD red values.
Each plate has exactly ONE rotation that puts those odd pixels inside the 16x16
patch at (8,8), and with symbol mapping = rank of {64,112,160,208} and packing =
3 two-bit symbols per byte, those odd pixels are exactly a subset of the 16
shuffle-selected patch positions. (per_byte=4 and the (v>>4)&3 mapping both
fail that test.)

So each plate's rotation is determined by its own content -- no need to guess it
from edge matching. Edge matching only has to place the plates in the 8x8 grid.

Run:
uv run python solvers/parallax2.py
"""

import hashlib
import random
from pathlib import Path

import numpy as np
from PIL import Image

ROOT = Path('codeshell')
PL = ROOT / 'extracted' / 'infinity-parallax' / 'plates'
ZERO = (0,) * 36


def edges_raw(a):
g = a[:, :, 0]
return (g[0, 6:42], g[6:42, 47], g[47, 6:42], g[6:42, 0])


def pack3(sym):
"""36 two-bit symbols -> 12 bytes, 3 symbols per byte, low first."""
out = bytearray()
for i in range(0, len(sym), 3):
v = 0
for j, s in enumerate(sym[i:i + 3]):
v |= s << (2 * j)
out.append(v)
return bytes(out)


def main():
plates = {f.stem: np.asarray(Image.open(f).convert('RGB')).astype(int)
for f in sorted(PL.glob('*.png'))}
vals = []
for a in plates.values():
vals.extend(int(v) for e in edges_raw(a) for v in e)
srt = sorted(set(vals))
smap = {v: i for i, v in enumerate(srt)}
print(f"alphabet {srt} -> {sorted(smap.values())}")

# rotation of each plate = the one whose (8..23,8..23) window holds the data
rot, sigs = {}, {}
for name, a in plates.items():
cands = [k for k in range(4)
if int((np.rot90(a, -k)[8:24, 8:24, 0] & 1).sum()) > 0]
if len(cands) != 1:
print(f" !! {name}: {len(cands)} candidate rotations {cands}")
k = cands[0]
rot[name] = k
b = np.rot90(a, -k)
e = edges_raw(b)
sigs[name] = tuple(tuple(smap[int(v)] for v in e[s]) for s in range(4))
from collections import Counter
print("rotation histogram:", dict(Counter(rot.values())))

# edge matching: every non-zero signature must appear exactly twice
cnt = Counter()
for name, s in sigs.items():
for side in range(4):
cnt[s[side]] += 1
bad = {k: v for k, v in cnt.items() if k != ZERO and v != 2}
print(f"non-zero signatures with count != 2: {len(bad)}")
print(f"zero-signature edges: {cnt[ZERO]}")

# assemble with rotations FIXED
names = sorted(plates)
grid, used = {}, set()

def fits(i, j, name):
N, E, S, W = sigs[name]
if (i, j - 1) in grid:
if sigs[grid[(i, j - 1)]][1] != W:
return False
elif W != ZERO:
return False
if (i - 1, j) in grid:
if sigs[grid[(i - 1, j)]][2] != N:
return False
elif N != ZERO:
return False
if i == 7 and S != ZERO:
return False
if j == 7 and E != ZERO:
return False
return True

def dfs(pos):
if pos == 64:
return True
i, j = divmod(pos, 8)
for name in names:
if name in used or not fits(i, j, name):
continue
grid[(i, j)] = name
used.add(name)
if dfs(pos + 1):
return True
del grid[(i, j)]
used.discard(name)
return False

if not dfs(0):
print("assembly failed with fixed rotations")
return
print("assembled 8x8 with fixed rotations")

# extract, trying the 4 global grid rotations
for gdelta in range(4):
st = bytearray()
for i in range(8):
for j in range(8):
name = grid[(i, j)]
k = (rot[name] + gdelta) % 4
b = np.rot90(plates[name], -k)
e = edges_raw(b)
blob = b''.join(pack3(tuple(smap[int(v)] for v in e[s]))
for s in range(4))
rng = random.Random(hashlib.sha256(blob).digest())
idx = list(range(256))
rng.shuffle(idx)
bits = 0
for t in idx[:16]:
r, c = 8 + t // 16, 8 + t % 16
bits = (bits << 1) | (int(b[r, c, 0]) & 1)
mask = hashlib.sha256(blob + b'ink').digest()[:2]
st += bytes(x ^ y for x, y in zip(bits.to_bytes(2, 'big'),
mask))
print(f" gdelta={gdelta}: {bytes(st[:12])!r}")
if bytes(st).startswith(b'INFINITY'):
payload, chk = bytes(st[8:40]), bytes(st[40:72])
h = hashlib.sha256(payload).hexdigest()
print(f"\n *** INFINITY found at gdelta={gdelta}")
print(f" payload: {payload.hex()}")
print(f" sha256 : {h}")
print(f" check : {chk == hashlib.sha256(payload).digest()}")
print(f"\n ANSWER : CodeShell{{{h}}}")


if __name__ == '__main__':
main()
CodeShell{f7f7b273c2e227d1e2566d8f647241cb48a4049f1afcad683427df85639db053}