CodeShell.kr - Glass Register

Challenge

同模板第三题,多了一条链式累加,而且最终判据是反相的。难度在于只匹配期望表会在特定情况下误判。

A register mixer with no useful output until the state is reconstructed.

一台寄存器混合器,在状态被重建前没有任何有用输出。

1
https://codeshell.kr/challenges/glass-register/

Solution

Step 1:与 Opcode Picnic 同一生成器模板,常数不同:strlen == 33.rodata @ 0x2020 是 33 字节逆序索引表(32..0),0x2060 是 33 字节期望累加器低字节。

与 01 的差异:内层 4 → 11 轮;种子 0x4a9a4ce9shr edx,clcl = (edi&3)*8rol dl,clcl = ((edi+i) mod 7)+1(用 magic 0x24924925 实现除 7);r8/r13 每外层轮清零;r10 = i*0x45d9f3b

Step 2:本题独有:外层每轮之后有一条链式累加 r14 = rol32(r14 ^ i*0x9e37 ^ acc_byte, ((i+3) mod 29)+1)(除 29 用 magic 0x469ee58469ee5847),最终判据是 cmp r15d,r13dje rejected反相,必须 r15d != r13d 才 accepted。只匹配期望表而忽略这条,会在 r15d == r13d 时误判。

Step 3:33 轮暴力全部唯一命中(无 0 解、无多解),链式判据对候选成立。寄存器模型用 GDB 单步校验:33 轮逐轮 sil 与模型完全一致,最终 r15d=62r13d=4194943236 两边同值;随机输入对照同样一致。

1
2
3
$ ./glass.bin 'CodeShell{registers_leave_traces}'      # accepted   rc=0
$ ./glass.bin 'CodeShell{registers_leave_traceX}' # rejected rc=1
$ ./glass.bin 'short' # 无输出 rc=2

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
#!/usr/bin/env python3
"""CodeShell.kr - Glass Register (Reversing, 200p) solver.

Recovers the accepted 33-byte input from 03_glass_register_Ytqr6gK.bin.

Same family as opcode-picnic / switchyard:
- strlen(argv[1]) == 0x21 (33)
- .rodata @0x2020: 33-byte reversed index table (32,31,...,1,0)
- .rodata @0x2060: 33 expected accumulator low-bytes
- per outer round i the accumulator low byte depends only on (i, input[32-i])
so every input byte is brute-forced independently over 0..255

Register-level model validated against a live GDB trace (see findings).
Differences vs opcode-picnic:
- inner loop runs 11 rounds (cmp edi,0xb), not 4
- seed constant 0x4a9a4ce9 instead of 0x9f80c83a
- final acceptance additionally requires acc_low(last) != r13 (inverted cmp)
"""

import sys

M32 = 0xFFFFFFFF


def rol32(x, c):
c &= 31
return ((x << c) | (x >> (32 - c))) & M32 if c else x & M32


def rol8(x, c):
c &= 7
return ((x << c) | (x >> (8 - c))) & 255 if c else x & 255


def acc(i, b):
"""Full 32-bit accumulator low byte for outer round i with input byte b."""
r10 = (i * 0x45D9F3B) & M32
eax = (r10 ^ 0x4A9A4CE9) & M32
esi = b & 0xFF
r8 = 0
r13 = 0
edi = 0
while True:
edx = eax
ecx = (eax >> 7) & M32
edx = (rol32(edx, 13) ^ ecx) & M32
eax = (eax ^ edx) & M32
eax = (eax ^ r8) & M32
r8 = (r8 + 0x7F4A7C15) & M32
edx = (eax >> ((edi & 3) * 8)) & M32
edx = (edx ^ esi) & M32
x = (edi + i) & M32
esi = ((eax >> 19) + r13 + rol8(edx & 0xFF, (x % 7) + 1)) & M32
r13 = (r13 + 0xB) & M32
edi += 1
if edi == 11:
break
return esi & 0xFF


def chain(accs):
"""Return (r15_last, r13_at_exit) for the final inverted comparison."""
r14 = 0x31415926
r11 = 0
r13 = 0
for i, a in enumerate(accs):
r14 = (r14 ^ r11) & M32 # xor r14d,r11d (r11d == i*0x9e37)
r13 = r14
r14 = a
r11 = (r11 + 0x9E37) & M32
r14 = (r14 ^ r13) & M32
r14 = rol32(r14, ((i + 3) % 29) + 1)
return accs[-1], r13


def solve(path):
data = open(path, "rb").read()
table = list(data[0x2020:0x2020 + 33]) # 32..0
target = list(data[0x2060:0x2060 + 33])

out = [None] * 33
accs = [None] * 33
for i, idx in enumerate(table):
hits = [b for b in range(256) if acc(i, b) == target[i]]
if len(hits) != 1:
print(f"[!] round {i}: {len(hits)} candidates {hits}")
out[idx] = hits[0]
accs[i] = target[i]
flag = bytes(out)
r15, r13 = chain(accs)
print(f"[i] final cmp: r15d={r15} r13d={r13} accepted_extra={r15 != r13}")
return flag


if __name__ == "__main__":
p = sys.argv[1] if len(sys.argv) > 1 else \
"assets/challenge-files/03_glass_register_Ytqr6gK.bin"
f = solve(p)
print("recovered:", f.decode("latin-1"))
CodeShell{registers_leave_traces}