N1junior2025-Crypto-WP

sign in the ca7s

题目描述

1
2
3
Sign in the cats, so that you can cat the flag! 🐱

Note: This is the easy version of "sign the ca7s".

题目附件

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
# server.sage
from Crypto.Util.number import bytes_to_long
from hashlib import md5
import os

FLAG = os.environ.get("FLAG", "flag{**redacted**}")

E = EllipticCurve(GF(0x1337_ca7_eae368ff5d702e6067aaaa77ca_ca7_1337), [0, 3])
G, n = E(1, 2), E.order()

def sign(priv, ctx, msg):
k = bytes_to_long(ctx + md5(str(priv).encode() + msg).digest())
z = bytes_to_long(md5(ctx + msg).digest())
r = int((k * G).x()) % n
s = (pow(k, -1, n) * (z + r * priv)) % n
return r, s

def verify(pub, ctx, msg, sig):
z = bytes_to_long(md5(ctx + msg).digest())
r, s = sig
if 0 < r < n and 0 < s < n:
return r == int((pow(s, -1, n) * (z * G + r * pub)).x()) % n

def chall(level, flag):
priv = randint(1, n - 1)
pub = priv * G
msg = os.urandom(64)

print(f"=== level {level} ===")
for _ in range(catalan_number(level)):
ctx = bytes.fromhex(input('context: '))
r, s = sign(priv, ctx, msg)
assert verify(pub, ctx, msg, (r, s))
if level <= 1: print('message:', msg.hex())
if level <= 2: print('sign:', r)
if level <= 3: print('ature:', s)

r, s = map(int, input('signature: ').split())
assert verify(pub, b'n1junior_2025', f'cat /flag{level}'.encode(), (r, s))
print(f'flag{level}:', flag)

if __name__ == "__main__":
chall(0, "💧")
chall(1, "🐱")
chall(2, FLAG)

解题思路

level 1 & 0

曲线是anomalous的,所以可以用SmartAttack求出 k,但是这里 k 的正负不清楚,爆就完了,然后 ctx 和 msg 都知道,所以直接求私钥就行。

level 2

这次msg不知道了,但是有2对签名值。

可以用 fastcoll 生成2个md5一样的 ctx,由于md5是从前往后读取消息并更新内部hash值的,所以两次 md5(ctx + msg) 也是一样的,这样 z 就是一样的。这样未知数就只有z和priv,刚好有2个签名方程。拿到私钥正常签就行。

Exp

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
# exp.sage
from pwn import process, remote, context
from ctf_tools.crypto.ECC.SmartAttack import SmartAttack
from Crypto.Util.number import *
from hashlib import md5
from tqdm import trange

context.log_level = "error"

p = 0x1337_ca7_eae368ff5d702e6067aaaa77ca_ca7_1337
E = EllipticCurve(GF(p), [0, 3])
G, n = E(1, 2), E.order()

def sign(priv, ctx, msg):
k = bytes_to_long(ctx + md5(str(priv).encode() + msg).digest())
z = bytes_to_long(md5(ctx + msg).digest())
r = int((k * G).x()) % n
s = (pow(k, -1, n) * (z + r * priv)) % n
return r, s

def verify(pub, ctx, msg, sig):
z = bytes_to_long(md5(ctx + msg).digest())
r, s = sig
if 0 < r < n and 0 < s < n:
return r == int((pow(s, -1, n) * (z * G + r * pub)).x()) % n

def phi(P):
return - P.x() / P.y()

def playonce():
io = remote("60.205.163.215", 11776)
for level in range(2):
io.recvuntil(b"context: ")
ctx = b"\x00"
io.sendline(ctx.hex().encode())
msg = bytes.fromhex(io.recvline().decode().split(": ")[1])
r = int(io.recvline().decode().split(": ")[1])
s = int(io.recvline().decode().split(": ")[1])
k = int(SmartAttack(G, E.lift_x(GF(p)(r)), p))
z = bytes_to_long(md5(ctx + msg).digest())
pri = (s * k - z) * pow(r, -1, n) % n
r, s = sign(pri, b'n1junior_2025', f'cat /flag{level}'.encode())
io.recvuntil(b"signature: ")
io.sendline((str(r) + " " + str(s)).encode())
recv = io.recvline().decode()
if "Traceback" in recv:
io.close()
return 1
# generated by fastcoll
ctx1 = b'J\xc6 \x14\xe9a\xd6)"\xeb\xc7\t%\xae\x95\xf3\xa3\xee\xc7\xfe&\xce\xd4\xae#\x02\x81}\xc5\x1f\x9a\xc8\xa0\x05\xf2\xe2\xba\x0b\xc4,\xe7#q<\xc5A\xbe\x86\x85pv\x88\xe1/H\xdb_\x9a\x1anZ\x02\xdd\xcf_\xbbXz\xc56\x1c\xd2\xe9\x82R\xb7\xa6\xfe\x07a\xcbu\xd8\x8bX\x19\xf3\xd0\xc9\xc5\xf0\xce\xb6_H\xc7\xd3s\xca\xf7\xaf\x86r\x1e\xfbD\x8a;\xbbc\x02\x10\xabg\xdc|\xe0|\xe6o6\xe9\xfa\xac\x93\xb0\x9d\x07'
ctx2 = b'J\xc6 \x14\xe9a\xd6)"\xeb\xc7\t%\xae\x95\xf3\xa3\xee\xc7~&\xce\xd4\xae#\x02\x81}\xc5\x1f\x9a\xc8\xa0\x05\xf2\xe2\xba\x0b\xc4,\xe7#q<\xc5\xc1\xbe\x86\x85pv\x88\xe1/H\xdb_\x9a\x1a\xeeZ\x02\xdd\xcf_\xbbXz\xc56\x1c\xd2\xe9\x82R\xb7\xa6\xfe\x07a\xcbu\xd8\x0bX\x19\xf3\xd0\xc9\xc5\xf0\xce\xb6_H\xc7\xd3s\xca\xf7\xaf\x86r\x1e\xfbD\x8a;\xbb\xe3\x01\x10\xabg\xdc|\xe0|\xe6o6\xe9\xfa,\x93\xb0\x9d\x07'
CTX = [ctx1, ctx2]
rl = []
sl = []
for i in range(2):
io.recvuntil(b"context: ")
io.sendline(CTX[i].hex().encode())
r = int(io.recvline().decode().split(": ")[1])
s = int(io.recvline().decode().split(": ")[1])
rl.append(r)
sl.append(s)
r1, r2 = rl
s1, s2 = sl
k1 = int(SmartAttack(G, E.lift_x(GF(p)(r1)), p))
k2 = int(SmartAttack(G, E.lift_x(GF(p)(r2)), p))
print(k1, k2)
pri = (s1 * k1 - s2 * k2) * pow(r1 - r2, -1, n) % n
r, s = sign(pri, b'n1junior_2025', f'cat /flag2'.encode())
io.recvuntil(b"signature: ")
io.sendline((str(r) + " " + str(s)).encode())
recv = io.recvline().decode()
if "Traceback" in recv:
io.close()
return 1
print(recv)
return 0

for i in trange(100):
if playonce() == 0:
break
# flag{good_job_k33p_g01n9_for_level_3__TMXMhh6T}

赛后讨论

比赛结束后和 shin 交流了一下,发现这里 k 其实是可以check的,直接比较 k 和 ctx 的高位就行。

sign the ca7s

题目描述

1
Sign the cats, so that you can cat the 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
# server.sage
from Crypto.Util.number import bytes_to_long
from hashlib import md5
import os

FLAG = os.environ.get("FLAG", "flag{**redacted**}")

E = EllipticCurve(GF(0x1337_ca7_eae368ff5d702e6067aaaa77ca_ca7_1337), [0, 3])
G, n = E(1, 2), E.order()

def sign(priv, ctx, msg):
k = bytes_to_long(ctx + md5(str(priv).encode() + msg).digest())
z = bytes_to_long(md5(ctx + msg).digest())
r = int((k * G).x()) % n
s = (pow(k, -1, n) * (z + r * priv)) % n
return r, s

def verify(pub, ctx, msg, sig):
z = bytes_to_long(md5(ctx + msg).digest())
r, s = sig
if 0 < r < n and 0 < s < n:
return r == int((pow(s, -1, n) * (z * G + r * pub)).x()) % n

def chall(level, flag):
priv = randint(1, n - 1)
pub = priv * G
msg = os.urandom(64)

print(f"=== level {level} ===")
for _ in range(catalan_number(level)):
ctx = bytes.fromhex(input('context: '))
r, s = sign(priv, ctx, msg)
assert verify(pub, ctx, msg, (r, s))
if level <= 1: print('message:', msg.hex())
if level <= 2: print('sign:', r)
if level <= 3: print('ature:', s)

r, s = map(int, input('signature: ').split())
assert verify(pub, b'n1junior_2025', f'cat /flag{level}'.encode(), (r, s))
print(f'flag{level}:', flag)

if __name__ == "__main__":
chall(1, "💧")
chall(2, "🐱")
chall(3, FLAG)

解题思路

sign in the ca7s 多了一个level 3,这次 r 也没有了, 只有一个 s,但签名次数有5次。

输入还是找 5 个有相同md5的 ctx,这样 z = bytes_to_long(md5(ctx + msg).digest()) 就是一样的,对于不同的k = bytes_to_long(ctx + md5(str(priv).encode() + msg).digest()),他们只差了一个常数,所以 5 个不同的 k 可以用一个变量表示。最后是 r,他是 k * G 的横坐标,设横坐标是 x0,对应的纵坐标是 y0,由于不同的 k 就差了一个常数,所以不同的 k * G 就差了一个点,用上点加法公式,就可以把所有的 r 用 x0,y0 表示。

所以我们一共有 5 个未知数:k, x0, y0, z, priv。 而我们有 5 个签名方程和 1 个曲线方程,所以是可解的。

最后还有一个问题是如何找到 5 个具有相同 md5 的 ctx。可以利用 fastcoll 这样操作:首先生成两个具有相同 md5 的消息,记作 \((A_1,A_2)\),然后随便选一个,比如 \(A_1\),以他为前缀再生成 2 个相同 md5 的数据,记作 \((A_1||B_1,A_1||B_2)\),然后再随机选一个,比如 \(A_1||B_1\),以他为前缀再生成 2 个相同 md5 的数据,记作 \((A_1||B_1||C_1,A_1||B_1||C_2)\),这样 \(A_i||B_j||C_k,i,j,k \in \{1, 2\}\) 的md5就都是一样的。

Exp

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
# exp.sage
from pwn import process, remote, context
from ctf_tools.crypto.ECC.SmartAttack import SmartAttack
from Crypto.Util.number import *
from hashlib import md5
from tqdm import trange

context.log_level = "error"

p = 0x1337_ca7_eae368ff5d702e6067aaaa77ca_ca7_1337
E = EllipticCurve(GF(p), [0, 3])
G, n = E(1, 2), E.order()

def sign(priv, ctx, msg):
k = bytes_to_long(ctx + md5(str(priv).encode() + msg).digest())
z = bytes_to_long(md5(ctx + msg).digest())
r = int((k * G).x()) % n
s = (pow(k, -1, n) * (z + r * priv)) % n
return r, s

def verify(pub, ctx, msg, sig):
z = bytes_to_long(md5(ctx + msg).digest())
r, s = sig
if 0 < r < n and 0 < s < n:
return r == int((pow(s, -1, n) * (z * G + r * pub)).x()) % n

def level1(io):
io.recvuntil(b"context: ")
ctx = b"\x00"
io.sendline(ctx.hex().encode())
msg = bytes.fromhex(io.recvline().decode().split(": ")[1])
r = int(io.recvline().decode().split(": ")[1])
s = int(io.recvline().decode().split(": ")[1])
k = int(SmartAttack(G, E.lift_x(GF(p)(r)), p))
z = bytes_to_long(md5(ctx + msg).digest())
pri = (s * k - z) * pow(r, -1, n) % n
r, s = sign(pri, b'n1junior_2025', f'cat /flag1'.encode())
io.recvuntil(b"signature: ")
io.sendline((str(r) + " " + str(s)).encode())
recv = io.recvline().decode()
if "Traceback" in recv:
io.close()
return 1
return 0

def level2(io):
ctx1 = b'J\xc6 \x14\xe9a\xd6)"\xeb\xc7\t%\xae\x95\xf3\xa3\xee\xc7\xfe&\xce\xd4\xae#\x02\x81}\xc5\x1f\x9a\xc8\xa0\x05\xf2\xe2\xba\x0b\xc4,\xe7#q<\xc5A\xbe\x86\x85pv\x88\xe1/H\xdb_\x9a\x1anZ\x02\xdd\xcf_\xbbXz\xc56\x1c\xd2\xe9\x82R\xb7\xa6\xfe\x07a\xcbu\xd8\x8bX\x19\xf3\xd0\xc9\xc5\xf0\xce\xb6_H\xc7\xd3s\xca\xf7\xaf\x86r\x1e\xfbD\x8a;\xbbc\x02\x10\xabg\xdc|\xe0|\xe6o6\xe9\xfa\xac\x93\xb0\x9d\x07'
ctx2 = b'J\xc6 \x14\xe9a\xd6)"\xeb\xc7\t%\xae\x95\xf3\xa3\xee\xc7~&\xce\xd4\xae#\x02\x81}\xc5\x1f\x9a\xc8\xa0\x05\xf2\xe2\xba\x0b\xc4,\xe7#q<\xc5\xc1\xbe\x86\x85pv\x88\xe1/H\xdb_\x9a\x1a\xeeZ\x02\xdd\xcf_\xbbXz\xc56\x1c\xd2\xe9\x82R\xb7\xa6\xfe\x07a\xcbu\xd8\x0bX\x19\xf3\xd0\xc9\xc5\xf0\xce\xb6_H\xc7\xd3s\xca\xf7\xaf\x86r\x1e\xfbD\x8a;\xbb\xe3\x01\x10\xabg\xdc|\xe0|\xe6o6\xe9\xfa,\x93\xb0\x9d\x07'
CTX = [ctx1, ctx2]
rl = []
sl = []
for i in range(2):
io.recvuntil(b"context: ")
io.sendline(CTX[i].hex().encode())
r = int(io.recvline().decode().split(": ")[1])
s = int(io.recvline().decode().split(": ")[1])
rl.append(r)
sl.append(s)
r1, r2 = rl
s1, s2 = sl
k1 = int(SmartAttack(G, E.lift_x(GF(p)(r1)), p))
k2 = int(SmartAttack(G, E.lift_x(GF(p)(r2)), p))
# print(k1, k2)
pri = (s1 * k1 - s2 * k2) * pow(r1 - r2, -1, n) % n
r, s = sign(pri, b'n1junior_2025', f'cat /flag2'.encode())
io.recvuntil(b"signature: ")
io.sendline((str(r) + " " + str(s)).encode())
recv = io.recvline().decode()
if "Traceback" in recv:
io.close()
return 1
return 0

def solve_equtions(sl, CTX):
CTX_num = [bytes_to_long(i) for i in CTX]
PR.<k0, z, x0, y0, d> = PolynomialRing(GF(p))
# k0, z, x0, y0, d = var("k0, z, x0, y0, d")
eqs = [
y0**2 - (x0**3 + 3),
sl[0] * k0 - (z + x0 * d)
]

for i in range(1, 5):
ki = k0 + (CTX_num[i] - CTX_num[0]) * 256 ** 16
delta = (CTX_num[i] - CTX_num[0]) * 256 ** 16 * G
delta_x, delta_y = delta.xy()
slope = (int(delta_y) - y0) / (int(delta_x) - x0)
xi = slope**2 - x0 - int(delta_x)
eqs.append((sl[i] * ki - (z + xi * d)).numerator())
I = PR.ideal(eqs)
return int(I.groebner_basis()[-1].univariate_polynomial().roots()[0][0])

def level3(io):
CTX = [b'J\xc6 \x14\xe9a\xd6)"\xeb\xc7\t%\xae\x95\xf3\xa3\xee\xc7\xfe&\xce\xd4\xae#\x02\x81}\xc5\x1f\x9a\xc8\xa0\x05\xf2\xe2\xba\x0b\xc4,\xe7#q<\xc5A\xbe\x86\x85pv\x88\xe1/H\xdb_\x9a\x1anZ\x02\xdd\xcf_\xbbXz\xc56\x1c\xd2\xe9\x82R\xb7\xa6\xfe\x07a\xcbu\xd8\x8bX\x19\xf3\xd0\xc9\xc5\xf0\xce\xb6_H\xc7\xd3s\xca\xf7\xaf\x86r\x1e\xfbD\x8a;\xbbc\x02\x10\xabg\xdc|\xe0|\xe6o6\xe9\xfa\xac\x93\xb0\x9d\x07\xc6\xbf\xe8\xd8\xce\xcf@j\xbb\xf8\x06\x1c^\x0e\x18\xf1\x9c\xdf?1]\xc7R\xfdH\xea:\xfej\x14"\x0e\xa1\xbb\xad\xcc\xea\xc1\xec_g*\xd6\x9a\xae5\x14\x1b\xc0\xad\xf3h\x8e\x00M\x90#b\x81\xd5x<,e\xae,\xc9\xb5\xdcX\x8e_\xaf\x0e$\xe1;\xcc\xd7\x1bR\x86\x9d\x13\x0e\xfb\xe0\x1c\xcd\xd7\x14\xc1\xd9ay.M1\x96\xbae\xf9\x8eSw\x91\xc1\xaf\x8d\xd6L\x86\x84Q\x03.\xdb5\xa1\xe5G\xde6\x8c\xec\xd4\x8c\x85\xf2\xd6Ec\xcc\xf9\xee\xef\xeb6\xf5U^+\x93\xc3P\x8a}\xf4\xd2{\xe2@5\xb2:>C\xb8\t\r\xf2\xc9\xca\xe2L\xaf+=\x87H\xf8\xe4\r0\xd6Q\x07\x91\xd3\x8b5\xda\x9c]\x131l{\x9a\x16\xceC\x0bz>\xdd\x84\x0c\x83\xa8S\xb2\x8e9\x99\x19K\xdc\xb0\r\x002\xf9\x05&m\xd1Z\xfe\xc6\xec\x06\xe2\xebx\x8e&X\xf6I\xe8\xbexHDO\xa1\x84\xd1*S\xde\xbb\r\xb4<_{2\xdf\x83M\x8a\xb9\x12e', b'J\xc6 \x14\xe9a\xd6)"\xeb\xc7\t%\xae\x95\xf3\xa3\xee\xc7\xfe&\xce\xd4\xae#\x02\x81}\xc5\x1f\x9a\xc8\xa0\x05\xf2\xe2\xba\x0b\xc4,\xe7#q<\xc5A\xbe\x86\x85pv\x88\xe1/H\xdb_\x9a\x1anZ\x02\xdd\xcf_\xbbXz\xc56\x1c\xd2\xe9\x82R\xb7\xa6\xfe\x07a\xcbu\xd8\x8bX\x19\xf3\xd0\xc9\xc5\xf0\xce\xb6_H\xc7\xd3s\xca\xf7\xaf\x86r\x1e\xfbD\x8a;\xbbc\x02\x10\xabg\xdc|\xe0|\xe6o6\xe9\xfa\xac\x93\xb0\x9d\x07\xc6\xbf\xe8\xd8\xce\xcf@j\xbb\xf8\x06\x1c^\x0e\x18\xf1\x9c\xdf?1]\xc7R\xfdH\xea:\xfej\x14"\x0e\xa1\xbb\xad\xcc\xea\xc1\xec_g*\xd6\x9a\xae5\x14\x1b\xc0\xad\xf3h\x8e\x00M\x90#b\x81\xd5x<,e\xae,\xc9\xb5\xdcX\x8e_\xaf\x0e$\xe1;\xcc\xd7\x1bR\x86\x9d\x13\x0e\xfb\xe0\x1c\xcd\xd7\x14\xc1\xd9ay.M1\x96\xbae\xf9\x8eSw\x91\xc1\xaf\x8d\xd6L\x86\x84Q\x03.\xdb5\xa1\xe5G\xde6\x8c\xec\xd4\x8c\x85\xf2\xd6Ec\xcc\xf9\xee\xef\xeb6\xf5U^+\x93\xc3P\x8a}t\xd2{\xe2@5\xb2:>C\xb8\t\r\xf2\xc9\xca\xe2L\xaf+=\x87H\xf8\xe4\r\xb0\xd6Q\x07\x91\xd3\x8b5\xda\x9c]\x131l\xfb\x9a\x16\xceC\x0bz>\xdd\x84\x0c\x83\xa8S\xb2\x8e9\x99\x19K\xdc\xb0\r\x00\xb2\xf9\x05&m\xd1Z\xfe\xc6\xec\x06\xe2\xebx\x8e&X\xf6I\xe8\xbexHDO\xa1\x04\xd1*S\xde\xbb\r\xb4<_{2\xdf\x83\xcd\x8a\xb9\x12e', b'J\xc6 \x14\xe9a\xd6)"\xeb\xc7\t%\xae\x95\xf3\xa3\xee\xc7\xfe&\xce\xd4\xae#\x02\x81}\xc5\x1f\x9a\xc8\xa0\x05\xf2\xe2\xba\x0b\xc4,\xe7#q<\xc5A\xbe\x86\x85pv\x88\xe1/H\xdb_\x9a\x1anZ\x02\xdd\xcf_\xbbXz\xc56\x1c\xd2\xe9\x82R\xb7\xa6\xfe\x07a\xcbu\xd8\x8bX\x19\xf3\xd0\xc9\xc5\xf0\xce\xb6_H\xc7\xd3s\xca\xf7\xaf\x86r\x1e\xfbD\x8a;\xbbc\x02\x10\xabg\xdc|\xe0|\xe6o6\xe9\xfa\xac\x93\xb0\x9d\x07\xc6\xbf\xe8\xd8\xce\xcf@j\xbb\xf8\x06\x1c^\x0e\x18\xf1\x9c\xdf?\xb1]\xc7R\xfdH\xea:\xfej\x14"\x0e\xa1\xbb\xad\xcc\xea\xc1\xec_g*\xd6\x9a\xae\xb5\x14\x1b\xc0\xad\xf3h\x8e\x00M\x90#b\x81Ux<,e\xae,\xc9\xb5\xdcX\x8e_\xaf\x0e$\xe1;\xcc\xd7\x1bR\x86\x9d\x93\x0e\xfb\xe0\x1c\xcd\xd7\x14\xc1\xd9ay.M1\x96\xbae\xf9\x8eSw\x91\xc1\xaf\x8dVL\x86\x84Q\x03.\xdb5\xa1\xe5G\xde6\x0c\xec\xd4\x8c\x85\xf2\xd6Ec\xcc\xf9\xee\xef\xeb6\xf5U^+\x93\xc3P\x8a}\xf4\xd2{\xe2@5\xb2:>C\xb8\t\r\xf2\xc9\xca\xe2L\xaf+=\x87H\xf8\xe4\r0\xd6Q\x07\x91\xd3\x8b5\xda\x9c]\x131l{\x9a\x16\xceC\x0bz>\xdd\x84\x0c\x83\xa8S\xb2\x8e9\x99\x19K\xdc\xb0\r\x002\xf9\x05&m\xd1Z\xfe\xc6\xec\x06\xe2\xebx\x8e&X\xf6I\xe8\xbexHDO\xa1\x84\xd1*S\xde\xbb\r\xb4<_{2\xdf\x83M\x8a\xb9\x12e', b'J\xc6 \x14\xe9a\xd6)"\xeb\xc7\t%\xae\x95\xf3\xa3\xee\xc7\xfe&\xce\xd4\xae#\x02\x81}\xc5\x1f\x9a\xc8\xa0\x05\xf2\xe2\xba\x0b\xc4,\xe7#q<\xc5A\xbe\x86\x85pv\x88\xe1/H\xdb_\x9a\x1anZ\x02\xdd\xcf_\xbbXz\xc56\x1c\xd2\xe9\x82R\xb7\xa6\xfe\x07a\xcbu\xd8\x8bX\x19\xf3\xd0\xc9\xc5\xf0\xce\xb6_H\xc7\xd3s\xca\xf7\xaf\x86r\x1e\xfbD\x8a;\xbbc\x02\x10\xabg\xdc|\xe0|\xe6o6\xe9\xfa\xac\x93\xb0\x9d\x07\xc6\xbf\xe8\xd8\xce\xcf@j\xbb\xf8\x06\x1c^\x0e\x18\xf1\x9c\xdf?\xb1]\xc7R\xfdH\xea:\xfej\x14"\x0e\xa1\xbb\xad\xcc\xea\xc1\xec_g*\xd6\x9a\xae\xb5\x14\x1b\xc0\xad\xf3h\x8e\x00M\x90#b\x81Ux<,e\xae,\xc9\xb5\xdcX\x8e_\xaf\x0e$\xe1;\xcc\xd7\x1bR\x86\x9d\x93\x0e\xfb\xe0\x1c\xcd\xd7\x14\xc1\xd9ay.M1\x96\xbae\xf9\x8eSw\x91\xc1\xaf\x8dVL\x86\x84Q\x03.\xdb5\xa1\xe5G\xde6\x0c\xec\xd4\x8c\x85\xf2\xd6Ec\xcc\xf9\xee\xef\xeb6\xf5U^+\x93\xc3P\x8a}t\xd2{\xe2@5\xb2:>C\xb8\t\r\xf2\xc9\xca\xe2L\xaf+=\x87H\xf8\xe4\r\xb0\xd6Q\x07\x91\xd3\x8b5\xda\x9c]\x131l\xfb\x9a\x16\xceC\x0bz>\xdd\x84\x0c\x83\xa8S\xb2\x8e9\x99\x19K\xdc\xb0\r\x00\xb2\xf9\x05&m\xd1Z\xfe\xc6\xec\x06\xe2\xebx\x8e&X\xf6I\xe8\xbexHDO\xa1\x04\xd1*S\xde\xbb\r\xb4<_{2\xdf\x83\xcd\x8a\xb9\x12e', b'J\xc6 \x14\xe9a\xd6)"\xeb\xc7\t%\xae\x95\xf3\xa3\xee\xc7~&\xce\xd4\xae#\x02\x81}\xc5\x1f\x9a\xc8\xa0\x05\xf2\xe2\xba\x0b\xc4,\xe7#q<\xc5\xc1\xbe\x86\x85pv\x88\xe1/H\xdb_\x9a\x1a\xeeZ\x02\xdd\xcf_\xbbXz\xc56\x1c\xd2\xe9\x82R\xb7\xa6\xfe\x07a\xcbu\xd8\x0bX\x19\xf3\xd0\xc9\xc5\xf0\xce\xb6_H\xc7\xd3s\xca\xf7\xaf\x86r\x1e\xfbD\x8a;\xbb\xe3\x01\x10\xabg\xdc|\xe0|\xe6o6\xe9\xfa,\x93\xb0\x9d\x07\xc6\xbf\xe8\xd8\xce\xcf@j\xbb\xf8\x06\x1c^\x0e\x18\xf1\x9c\xdf?1]\xc7R\xfdH\xea:\xfej\x14"\x0e\xa1\xbb\xad\xcc\xea\xc1\xec_g*\xd6\x9a\xae5\x14\x1b\xc0\xad\xf3h\x8e\x00M\x90#b\x81\xd5x<,e\xae,\xc9\xb5\xdcX\x8e_\xaf\x0e$\xe1;\xcc\xd7\x1bR\x86\x9d\x13\x0e\xfb\xe0\x1c\xcd\xd7\x14\xc1\xd9ay.M1\x96\xbae\xf9\x8eSw\x91\xc1\xaf\x8d\xd6L\x86\x84Q\x03.\xdb5\xa1\xe5G\xde6\x8c\xec\xd4\x8c\x85\xf2\xd6Ec\xcc\xf9\xee\xef\xeb6\xf5U^+\x93\xc3P\x8a}\xf4\xd2{\xe2@5\xb2:>C\xb8\t\r\xf2\xc9\xca\xe2L\xaf+=\x87H\xf8\xe4\r0\xd6Q\x07\x91\xd3\x8b5\xda\x9c]\x131l{\x9a\x16\xceC\x0bz>\xdd\x84\x0c\x83\xa8S\xb2\x8e9\x99\x19K\xdc\xb0\r\x002\xf9\x05&m\xd1Z\xfe\xc6\xec\x06\xe2\xebx\x8e&X\xf6I\xe8\xbexHDO\xa1\x84\xd1*S\xde\xbb\r\xb4<_{2\xdf\x83M\x8a\xb9\x12e']
sl = []
for i in range(5):
io.recvuntil(b"context: ")
io.sendline(CTX[i].hex().encode())
s = int(io.recvline().decode().split(": ")[1])
sl.append(s)
priv = solve_equtions(sl, CTX)
r, s= sign(priv, b'n1junior_2025', f'cat /flag3'.encode())
io.recvuntil(b"signature: ")
io.sendline((str(r) + " " + str(s)).encode())
recv = io.recvline().decode()
if "Traceback" in recv:
io.close()
return 1
print(recv)
return 0

for i in trange(100):
io = remote("60.205.163.215", 38176)
if level1(io) == 1:
continue
if level2(io) == 1:
continue
if level3(io) == 1:
continue
print("Successfully")
break
io.close()
# flag{wow_such_smart_very_MD5_much_r3la7ed_n0nc3s_kn9xyf8b}

sign one m0re

1
2
3
4
5
6
Can you break the one-more unforgeability of this signature scheme? 🖊

hint
1. Can you break the one-more **unforgeability** of this **provably secure partially blind signature** scheme?
hint
2. eROSion

题目附件

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
# server.py
from fastecdsa.curve import secp256k1
from fastecdsa.point import Point
from secrets import randbelow
from hashlib import sha512
import signal
import os

FLAG = os.environ.get("FLAG", "flag{**redacted**}")

MAX_SESSIONS = 192

p, q, G = secp256k1.p, secp256k1.q, secp256k1.G
info = int.from_bytes(b"[N1CTF Junior 2025]", "big")
z = Point(info, pow(info ** 3 + 7, (p + 1) // 4, p), secp256k1)

class Signer:
def __init__(self):
self.x = randbelow(q)
self.y = self.x * G
self.sessions = {sid: (0, ) for sid in range(MAX_SESSIONS)}

def get_public_key(self):
return self.y

def commit(self, sid):
assert sid in range(MAX_SESSIONS), "Invalid session"
assert self.sessions[sid][0] == 0, "Invalid state"
u, s, d = [randbelow(q) for _ in range(3)]
a, b = u * G, s * G + d * z
self.sessions[sid] = (1, u, s, d)
return a, b

def sign(self, sid, e):
assert sid in range(MAX_SESSIONS), "Invalid session"
assert self.sessions[sid][0] == 1, "Invalid state"
assert 1 < e < q, "Invalid query"
_, u, s, d = self.sessions[sid]
c = (e - d) % q
r = (u - c * self.x) % q
self.sessions[sid] = (2, )
return r, c, s, d

class Verifier:
def __init__(self):
self.messages = set()

def oracle(self, α, β, z, msg):
to_hash = "||".join(map(str, [α.x, α.y, β.x, β.y, z.x, z.y, msg]))
return int.from_bytes(sha512(to_hash.encode()).digest(), "big") % q

def verify(self, pub, sig, msg):
assert all(1 < x < q for x in sig), "Invalid signature"
ρ, ω, σ, δ = sig
if (ω + δ) % q == self.oracle(ρ * G + ω * pub, σ * G + δ * z, z, msg):
self.messages.add(msg)
if len(self.messages) == MAX_SESSIONS + 1:
return FLAG
return "Good signature"
return "Bad signature"

signal.alarm(300)
signer = Signer()
verifier = Verifier()

while True:
cmd, *args = input("> ").split()
if cmd == "get_key":
y = signer.get_public_key()
print(f"y = ({y.x}, {y.y})")
elif cmd == "commit":
sid = int(args[0])
a, b = signer.commit(sid)
print(f"a = ({a.x}, {a.y})")
print(f"b = ({b.x}, {b.y})")
elif cmd == "sign":
sid, e = int(args[0]), int(args[1])
r, c, s, d = signer.sign(sid, e)
print(f"{r = }")
print(f"{c = }")
print(f"{s = }")
print(f"{d = }")
elif cmd == "verify":
sig, msg = tuple(map(int, args[:4])), args[4]
print(verifier.verify(signer.get_public_key(), sig, msg))

解题思路

根据题目给的hint,找到这篇文章[1]。发现论文的参数和题目的参数十分吻合。之后@LOV3把这篇论文交给AI后成功复现了出来,而且下面的Exp也是AI写的。等有空了再来记录下原理吧(逃

Exp

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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# exp.py
#!/usr/bin/env python3
"""
One-shot ROS attack for 'sign one m0re' challenge
- Collect 192 commits (a_i, b_i)
- For each i, precompute multiple candidate e_{i,j} = H(a_i, b_i, z, m_{i,j}) by varying messages
- Build per-dimension lattices and solve via Babai to get mu_i
- Decompose target to choose j_i per dimension so that sum K_i * e_{i,j_i} == e_forge
- Query sign once per session with e_{i,j_i}, collect (r_i, c_i, s_i, d_i)
- Forge (rho, omega, sigma, delta) = sum K_i * (r_i, c_i, s_i, d_i)
- Verify forged signature on forged message to get flag

Notes:
- All modular arithmetic is done mod n (curve order)
- EC point ops use Sage on secp256k1 over GF(p)
- Requires running with `sage -python one_shot_ros.py`
"""

from pwn import *
import hashlib
import os
from sage.all import *
from sage.modules.free_module_integer import IntegerLattice

# secp256k1 params
FIELD_P = Integer(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f) # field prime
ORDER_N = Integer(0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141) # group order
Fp = GF(FIELD_P)
Zn = GF(ORDER_N)
E = EllipticCurve(Fp, [0, 7])
G = E(Integer(0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798),
Integer(0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8))

MAX_SESSIONS = 192

# Fixed z from challenge
INFO = int.from_bytes(b"[N1CTF Junior 2025]", "big")
z_point = E(Integer(INFO), Integer(pow(INFO ** 3 + 7, (FIELD_P + 1) // 4, FIELD_P)))

# Oracle identical to challenge (mod n)
def oracle(alpha_pt, beta_pt, z_pt, msg_str):
to_hash = "||".join(map(str, [int(alpha_pt[0]), int(alpha_pt[1]), int(beta_pt[0]), int(beta_pt[1]), int(z_pt[0]), int(z_pt[1]), msg_str]))
return Integer(int.from_bytes(hashlib.sha512(to_hash.encode()).digest(), "big")) % ORDER_N

# Utilities

def inner_product(coeffs, vals):
return sum(c * v for c, v in zip(coeffs, vals))


def scale_to_Zn(vec):
# Convert a vector of rationals in Zn to a single Zn element (first entry) after solving
assert all(gcd(ORDER_N, el.denominator()) == 1 for el in vec)
return vector(Zn, [Zn(el.numerator()) / Zn(el.denominator()) for el in vec])


def pows_gen(n=7, group_bit_len=256, extra_digits=0):
# Same as provided Sage, but using ORDER_N
max_number = Integer(2) ** group_bit_len
assert n >= 2
factored = []
k = n - 1
while k >= 1:
B = QQ(1) / QQ(500) # practical tweak
max_k = ceil(log(max_number, k + 1))
if k == 1:
e_k = 0
else:
e_k = ceil(log(B * log(ORDER_N, k + 1) * ORDER_N ** ((k - 1) / k), k + 1)) + extra_digits
factored = [(k + 1, i) for i in range(e_k, max_k)] + factored
max_number = (k + 1) ** e_k
k -= 1
return factored # list of (base, exponent)


def multibase(num_int, pows):
# pows is list of integers (bases already exponentiated)
temp = Integer(num_int)
digits = []
for base in pows[::-1]:
digits = [temp // base] + digits
temp = temp % base
assert inner_product(digits, pows) == num_int
return digits


def choose_pows_up_to(max_sessions=192, start_basis=7, max_basis_cap=12):
# Try to get len(pows) as close to max_sessions without exceeding
best = None
for mb in range(start_basis, max_basis_cap + 1):
factored = pows_gen(n=mb + 1, group_bit_len=ceil(log(ORDER_N, 2)), extra_digits=0)
pows = [Integer(b) ** Integer(e) for (b, e) in factored]
if best is None or (len(pows) <= max_sessions and len(pows) > len(best[0])):
best = (pows, [b for (b, e) in factored])
if len(pows) == max_sessions:
break
# If still shorter, pad with base=2 powers of 1 to reach max_sessions (benign extra dims)
pows, bases = best
if len(pows) < max_sessions:
pad = [Integer(2)] * (max_sessions - len(pows))
pows = pows + pad
bases = bases + [2] * len(pad)
if len(pows) > max_sessions:
pows = pows[:max_sessions]
bases = bases[:max_sessions]
return pows, bases


def main():
context = contextlib = None # avoid lint
print("[*] One-shot ROS attack starting...")

# Connect to challenge
io = process(['python', '/d/CTF/Chall/2025/N1junior_2/sign_one_m0re/attachment/server.py'])
# io = remote("60.205.163.215", 33073)

# Get public key
io.sendline(b"get_key")
line = io.recvline().decode().strip()
y_coords = line.split("y = (")[1].rstrip(")")
yx, yy = map(int, y_coords.split(", "))
Y = E(Integer(yx), Integer(yy))
print(f"[*] Public key Y.x={yx}")

# Choose bases and pows
pows, pows_bases = choose_pows_up_to(MAX_SESSIONS, start_basis=7, max_basis_cap=12)
for powi in pows:
print(factor(powi))
print(f"{pows_bases = }")
ell = len(pows)
assert ell == MAX_SESSIONS, f"ell={ell} != {MAX_SESSIONS}, adjust choose_pows_up_to"
print(f"[*] ell = {ell} dimensions")

# Collect commits (a_i, b_i)
A_pts = []
B_pts = []
a_coords_list = []
b_coords_list = []

for sid in range(ell):
io.sendline(f"commit {sid}".encode())
la = io.recvline().decode().strip()
lb = io.recvline().decode().strip()
ax_s = la.split("a = (")[1].rstrip(")")
bx_s = lb.split("b = (")[1].rstrip(")")
ax, ay = map(int, ax_s.split(", "))
bx, by = map(int, bx_s.split(", "))
a_pt = E(Integer(ax), Integer(ay))
b_pt = E(Integer(bx), Integer(by))
A_pts.append(a_pt)
B_pts.append(b_pt)
a_coords_list.append((ax, ay))
b_coords_list.append((bx, by))
if sid % 32 == 0:
print(f"[*] commit {sid}/{ell}")

# Generate candidate messages and e-values per dimension
# For each i: generate pows_bases[i] candidates indexed 0..B_i-1
msgs = []
e_values = [] # list of lists of integers mod n

for i in range(ell):
Bi = int(pows_bases[i])
mi_list = []
ei_list = []
for b in range(Bi):
m = f"m{i}_{b}_{os.urandom(8).hex()}"
eib = oracle(A_pts[i], B_pts[i], z_point, m)
mi_list.append(m)
ei_list.append(int(eib))
msgs.append(mi_list)
e_values.append(ei_list)
if i % 32 == 0:
print(f"[*] candidates {i}/{ell}")

# Build qi and lattices M_i
qi = [] # differences list for each i: [e_i_b - e_i_0] for b>=1
M = []

for i in range(ell):
base_e0 = Integer(e_values[i][0])
diffs = [Integer((Integer(e_values[i][b]) - base_e0) % ORDER_N) for b in range(1, int(pows_bases[i]))]
qi.append(diffs)
if len(diffs) > 0:
top = Matrix(ZZ, [diffs]) # 1 x (Bi-1)
bottom = ORDER_N * matrix.identity(ZZ, len(diffs)) # (Bi-1) x (Bi-1)
Mi = block_matrix([[top], [bottom]])
else:
Mi = matrix(ZZ, [[int(ORDER_N)]])
M.append(Mi)

# Solve CVP with Babai per dimension to obtain mu_i in Zn
mu = []
for i in range(ell):
Bi = int(pows_bases[i])
if Bi > 1:
tgt = vector(ZZ, [int(j * Integer(pows[i])) for j in range(1, Bi)])
lattice = IntegerLattice(M[i])
closest = vector(ZZ, lattice.babai(tgt))
sol = M[i].solve_left(closest) # rational vector (length 1)
mu_i = (Zn(1) / Zn(int(pows[i]))) * scale_to_Zn(sol)[0]
mu.append(mu_i)
else:
mu.append(Zn.random_element())
print("[*] mu computed")

# Forge target alpha,beta points (base sums)
# alpha_base = sum pows[i]*mu[i] * A_pts[i]
# beta_base = sum pows[i]*mu[i] * B_pts[i]
alpha_base = None
beta_base = None
for i in range(ell):
coeff = int((Zn(int(pows[i])) * mu[i]).lift()) # 0..n-1
if coeff % int(ORDER_N) == 0:
continue
term_a = Integer(coeff) * A_pts[i]
term_b = Integer(coeff) * B_pts[i]
alpha_base = term_a if alpha_base is None else alpha_base + term_a
beta_base = term_b if beta_base is None else beta_base + term_b
if alpha_base is None:
print("[!] alpha_base is None, abort")
io.close(); return

# Try decomposition attempts
attempts = 0
success = False

while attempts < 80 and not success:
attempts += 1
extra_alpha = Zn.random_element()
if int(extra_alpha) == 0:
continue
inv_extra = Zn(int(pow(int(extra_alpha), -1, int(ORDER_N))))

alpha_forge = int(extra_alpha) * alpha_base
beta_forge = int(extra_alpha) * beta_base
msg_forge = f"forge_msg_{os.urandom(6).hex()}"
c_forge = Integer(oracle(alpha_forge, beta_forge, z_point, msg_forge)) # in [0,n)

# NUM = extra_alpha^{-1} * c_forge + sum pows*mu*(-e_i0) mod n
NUM = (inv_extra * Zn(int(c_forge)))
for i in range(ell):
NUM += (Zn(0) - (Zn(int(pows[i])) * mu[i] * Zn(int(e_values[i][0]))))
NUM_int = Integer(int(NUM.lift())) # 0..n-1

# greedy digit by digit
digits = [0] * ell
NUM_cur = NUM_int
ok = True
for i in range(ell - 1, -1, -1):
try:
cur_digits = multibase(NUM_cur, [Integer(x) for x in pows])
except AssertionError:
ok = False
break
new_digit = int(cur_digits[i])
digits[i] = new_digit
if new_digit >= int(pows_bases[i]):
ok = False
break
if new_digit != 0:
# NUM_cur -= pows[i] * mu[i] * qi[i][new_digit-1] (mod n) as integer representative
delta = (Zn(int(pows[i])) * mu[i] * Zn(int(qi[i][new_digit - 1])))
NUM_cur = Integer((NUM_cur - int(delta.lift())) % int(ORDER_N))
if NUM_cur < 0:
ok = False
break
if NUM_cur == 0:
# can break early only if remaining higher digits are 0
pass
if ok and NUM_cur == 0:
success = True
print(f"[+] Decomposition success in {attempts} attempts")
break
if attempts % 10 == 0:
print(f"[*] attempt {attempts} failed")

if not success:
print("[!] Decomposition failed. Consider rerunning.")
io.close(); return

# With digits selected, we now sign once per session using e_i = e_values[i][digits[i]]
# and immediately verify to accumulate 192 messages in the verifier
signed = [] # tuples (r,c,s,d,msg)
for i in range(ell):
e_sel = int(e_values[i][digits[i]])
msg_i = msgs[i][digits[i]]
io.sendline(f"sign {i} {e_sel}".encode())
r_line = io.recvline().decode().strip(); r = int(r_line.split("r = ")[1])
c_line = io.recvline().decode().strip(); c = int(c_line.split("c = ")[1])
s_line = io.recvline().decode().strip(); s = int(s_line.split("s = ")[1])
d_line = io.recvline().decode().strip(); d = int(d_line.split("d = ")[1])
signed.append((r, c, s, d, msg_i))
# verify this legit signature to populate set
io.sendline(f"verify {r} {c} {s} {d} {msg_i}".encode())
vr = io.recvline().decode().strip()
if "Good signature" not in vr and "flag{" not in vr:
print(f"[!] Legit signature verify failed for session {i}: {vr}")
io.close(); return
if i % 32 == 0:
print(f"[*] verified legit {i}/{ell}")

# Compute forged signature as linear combination with K_i = extra_alpha * pows[i] * mu[i]
rho_f = 0; omega_f = 0; sigma_f = 0; delta_f = 0
for i in range(ell):
Ki = int((extra_alpha * Zn(int(pows[i])) * mu[i]).lift()) % int(ORDER_N)
r, c, s, d, _ = signed[i]
rho_f = (rho_f + Ki * r) % int(ORDER_N)
omega_f = (omega_f + Ki * c) % int(ORDER_N)
sigma_f = (sigma_f + Ki * s) % int(ORDER_N)
delta_f = (delta_f + Ki * d) % int(ORDER_N)

# Final verify forged signature to trigger the flag
io.sendline(f"verify {rho_f} {omega_f} {sigma_f} {delta_f} {msg_forge}".encode())
res = io.recvline().decode().strip()
print(res)

io.close()

if __name__ == "__main__":
main()
# flag{ROS_attack__with_dimensional_eROSion_t9uKVDXg}

SM1¼

题目描述

1
SM4 is a secure block cipher. So is SM1¼. 🔒

题目附件

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
from Crypto.Util.Padding import pad
from Crypto.Util.strxor import strxor
import os

FLAG = os.environ.get("FLAG", "flag{**redacted**}")

S = (
0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05,
0x2B, 0x67, 0x9A, 0x76, 0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A, 0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62,
0xE4, 0xB3, 0x1C, 0xA9, 0xC9, 0x08, 0xE8, 0x95, 0x80, 0xDF, 0x94, 0xFA, 0x75, 0x8F, 0x3F, 0xA6,
0x47, 0x07, 0xA7, 0xFC, 0xF3, 0x73, 0x17, 0xBA, 0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0x4F, 0xA8,
0x68, 0x6B, 0x81, 0xB2, 0x71, 0x64, 0xDA, 0x8B, 0xF8, 0xEB, 0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35,
0x1E, 0x24, 0x0E, 0x5E, 0x63, 0x58, 0xD1, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21, 0x78, 0x87,
0xD4, 0x00, 0x46, 0x57, 0x9F, 0xD3, 0x27, 0x52, 0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E,
0xEA, 0xBF, 0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE, 0xF9, 0x61, 0x15, 0xA1,
0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34, 0x1A, 0x55, 0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3,
0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29, 0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F,
0xD5, 0xDB, 0x37, 0x45, 0xDE, 0xFD, 0x8E, 0x2F, 0x03, 0xFF, 0x6A, 0x72, 0x6D, 0x6C, 0x5B, 0x51,
0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F, 0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8,
0x0A, 0xC1, 0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD, 0x2D, 0x74, 0xD0, 0x12, 0xB8, 0xE5, 0xB4, 0xB0,
0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96, 0x77, 0x7E, 0x65, 0xB9, 0xF1, 0x09, 0xC5, 0x6E, 0xC6, 0x84,
0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20, 0x79, 0xEE, 0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48,
)

FK = (
0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC,
)

CK = (
0x00070E15, 0x1C232A31, 0x383F464D, 0x545B6269,
0x70777E85, 0x8C939AA1, 0xA8AFB6BD, 0xC4CBD2D9,
0xE0E7EEF5, 0xFC030A11, 0x181F262D, 0x343B4249,
0x50575E65, 0x6C737A81, 0x888F969D, 0xA4ABB2B9,
0xC0C7CED5, 0xDCE3EAF1, 0xF8FF060D, 0x141B2229,
0x30373E45, 0x4C535A61, 0x686F767D, 0x848B9299,
0xA0A7AEB5, 0xBCC3CAD1, 0xD8DFE6ED, 0xF4FB0209,
0x10171E25, 0x2C333A41, 0x484F565D, 0x646B7279,
)

l2b = lambda x: x.to_bytes(4, "big")
b2l = lambda x: int.from_bytes(x, "big")
τ = lambda x: b2l(bytes(S[y] for y in l2b(x)))
rol = lambda x, y: ((x << y) | (x >> (32 - y))) & 0xFFFFFFFF
L = lambda x: x ^ rol(x, 2) ^ rol(x, 10) ^ rol(x, 18) ^ rol(x, 24)
L_ = lambda x: x ^ rol(x, 13) ^ rol(x, 23)
T = lambda x: L(τ(x))
T_ = lambda x: L_(τ(x))

def enc_block(key, block, nr=32):
K = [b2l(key[i : i + 4]) ^ FK[i // 4] for i in range(0, 16, 4)]
X = [b2l(block[i : i + 4]) for i in range(0, 16, 4)]
for i in range(nr):
K += [K[i] ^ T_(CK[i] ^ K[i + 1] ^ K[i + 2] ^ K[i + 3])]
X += [X[i] ^ T(X[i + 1] ^ X[i + 2] ^ X[i + 3] ^ K[i + 4])]
return b"".join(l2b(X[nr + 3 - i]) for i in range(4))

def enc(key, pt, nr=32):
pt = pad(pt, 16)
ct = bytes(16)
for i in range(0, len(pt), 16):
ct += enc_block(key, strxor(pt[i : i + 16], ct[-16 : ]), nr)
return ct[16 : ]

key = os.urandom(16)
print(enc(key, FLAG.encode()).hex())
while True:
pt = bytes.fromhex(input("> "))
print(enc(key, pt, 10).hex())

解题思路

搜到这篇文章[2],依旧是@LOV3把这篇论文交给AI,写了一份Exp,我就改了一下并行交互。

Exp

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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
恢复 10 轮最后一轮密钥 K13(oracle 版)
- 直接对接本目录的 server.py(固定密钥,10轮oracle)
- 使用论文扩展的 2^16 结构化积分:
S0 = (Δ(a), (C,C,C,δ), (C,C,C,δ), (C,C,C,δ)),a,δ∈[0,255]
其中 Δ(a) = L(0,0,0,a) ⊕ C1
- 计算:
t = L^{-1}( ⊕ C0 )
U = C3 ⊕ C2 ⊕ C1
对每个字节 b,找到所有 guess g 满足 ⊕ S[ U_b ⊕ g ] == t_b
- 多基底可进一步交叉筛选(若需要)。本脚本先用单基底验证在当前实现下可唯一化。
"""
from pwn import process, remote
from Crypto.Util.strxor import strxor
import sys, re, time
from tqdm import trange

# --- SMS4 primitives ---
S = (
0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05,
0x2B, 0x67, 0x9A, 0x76, 0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A, 0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62,
0xE4, 0xB3, 0x1C, 0xA9, 0xC9, 0x08, 0xE8, 0x95, 0x80, 0xDF, 0x94, 0xFA, 0x75, 0x8F, 0x3F, 0xA6,
0x47, 0x07, 0xA7, 0xFC, 0xF3, 0x73, 0x17, 0xBA, 0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0x4F, 0xA8,
0x68, 0x6B, 0x81, 0xB2, 0x71, 0x64, 0xDA, 0x8B, 0xF8, 0xEB, 0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35,
0x1E, 0x24, 0x0E, 0x5E, 0x63, 0x58, 0xD1, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21, 0x78, 0x87,
0xD4, 0x00, 0x46, 0x57, 0x9F, 0xD3, 0x27, 0x52, 0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E,
0xEA, 0xBF, 0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE, 0xF9, 0x61, 0x15, 0xA1,
0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34, 0x1A, 0x55, 0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3,
0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29, 0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F,
0xD5, 0xDB, 0x37, 0x45, 0xDE, 0xFD, 0x8E, 0x2F, 0x03, 0xFF, 0x6A, 0x72, 0x6D, 0x6C, 0x5B, 0x51,
0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F, 0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8,
0x0A, 0xC1, 0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD, 0x2D, 0x74, 0xD0, 0x12, 0xB8, 0xE5, 0xB4, 0xB0,
0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96, 0x77, 0x7E, 0x65, 0xB9, 0xF1, 0x09, 0xC5, 0x6E, 0xC6, 0x84,
0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20, 0x79, 0xEE, 0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48,
)

l2b = lambda x: x.to_bytes(4, 'big')
b2l = lambda x: int.from_bytes(x, 'big')
rol = lambda x, y: ((x << y) | (x >> (32 - y))) & 0xFFFFFFFF
L = lambda x: x ^ rol(x, 2) ^ rol(x, 10) ^ rol(x, 18) ^ rol(x, 24)

def L_inv(x: int) -> int:
y = x
for _ in range(32):
y = x ^ (rol(y,2) ^ rol(y,10) ^ rol(y,18) ^ rol(y,24))
return y & 0xFFFFFFFF

# byte-wise S-box on 32-bit
τ = lambda x: b2l(bytes(S[b] for b in l2b(x)))
T = lambda x: L(τ(x))

# Key schedule constants and transform
FK = (0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC)
CK = (
0x00070E15, 0x1C232A31, 0x383F464D, 0x545B6269,
0x70777E85, 0x8C939AA1, 0xA8AFB6BD, 0xC4CBD2D9,
0xE0E7EEF5, 0xFC030A11, 0x181F262D, 0x343B4249,
0x50575E65, 0x6C737A81, 0x888F969D, 0xA4ABB2B9,
0xC0C7CED5, 0xDCE3EAF1, 0xF8FF060D, 0x141B2229,
0x30373E45, 0x4C535A61, 0x686F767D, 0x848B9299,
0xA0A7AEB5, 0xBCC3CAD1, 0xD8DFE6ED, 0xF4FB0209,
0x10171E25, 0x2C333A41, 0x484F565D, 0x646B7279,
)
T_ = lambda x: (x ^ rol(x,13) ^ rol(x,23)) # used on τ(x)
T_key = lambda x: T_(τ(x))

def key_schedule(master_key: bytes, nr: int=32):
K = [b2l(master_key[i:i+4]) ^ FK[i//4] for i in range(0,16,4)]
rks = []
for i in range(nr):
Ki = K[i] ^ T_key(CK[i] ^ K[i+1] ^ K[i+2] ^ K[i+3])
K.append(Ki)
rks.append(Ki)
return rks

# 构造 2^16 结构
def build_block(a: int, d: int, base=(0x11223344,0x55667788,0x99aabbcc,0xddeeff00), active_byte_index=3) -> bytes:
# Δ(a) = L(0,0,0,a) ⊕ base[0]
shift = (3 - active_byte_index) * 8
w = (a & 0xFF) << shift
W1 = L(w) ^ base[0]
m = (d & 0xFF) << shift
W2 = base[1] ^ m
W3 = base[2] ^ m
W4 = base[3] ^ m
return b''.join(l2b(x) for x in (W1,W2,W3,W4))


HEX_RE = re.compile(r"^[0-9a-fA-F]+$")

def recv_hex_line(io, timeout=5.0):
line = io.recvline(timeout=timeout)
if not line:
raise EOFError("No line from oracle")
s = line.strip().decode()
# Some servers may print banners; skip non-hex lines
while not HEX_RE.match(s):
line = io.recvline(timeout=timeout)
if not line:
raise EOFError("No hex line from oracle")
s = line.strip().decode()
return s

def send_pt_get_ct(io, pt_hex: str, expect_prompt=True, timeout=5.0):
if expect_prompt:
io.sendlineafter(b"> ", pt_hex.encode())
else:
io.sendline(pt_hex.encode())
return recv_hex_line(io, timeout=timeout)

def query_base_collect(io: remote, base, active_byte_index=3, expect_prompt=True):
sum_C0 = 0
U_cols = [[],[],[],[]]
# 65,536 queries

ptl = [None for _ in range(256 * 256)]
for a in range(256):
for d in range(256):
pt = build_block(a, d, base=base, active_byte_index=active_byte_index)
ptl[a*256 + d] = pt.hex()
ctl = [None for _ in range(256 * 256)]

gap = 32
for i in trange(0, 256 * 256, gap):
io.send(("\n".join(ptl[i:i+gap]) + "\n").encode())
recv = io.recvlines(gap)
for j in range(gap):
ctl[i + j] = recv[j].split(b" ")[1].decode()

for a in range(256):
for d in range(256):
pt = bytes.fromhex(ptl[a*256 + d])
ct = bytes.fromhex(ctl[a*256 + d])
C0 = b2l(ct[0:4]); C1 = b2l(ct[4:8]); C2 = b2l(ct[8:12]); C3 = b2l(ct[12:16])
sum_C0 ^= C0
U = C3 ^ C2 ^ C1
Ub = l2b(U)
for i in range(4):
U_cols[i].append(Ub[i])
t = L_inv(sum_C0)
return t, U_cols

# 已优化
def recover_k13_from_oracle_and_intersect(io, bases, active_byte_index=3, expect_prompt=True):
# For each base, compute candidate sets per byte
cand_sets = [set(range(256)) for _ in range(4)]
print(f"{len(bases) = }")
for base in bases:
t, U_cols = query_base_collect(io, base, active_byte_index, expect_prompt=expect_prompt)
t_b = l2b(t)
print(f"[oracle] base={tuple(hex(x) for x in base)} -> t=0x{t:08x}")
for bpos in range(4):
new_cand = set()
col = U_cols[bpos]
want = t_b[bpos]
for g in cand_sets[bpos]:
acc = 0
for v in col:
acc ^= S[v ^ g]
if acc == want:
new_cand.add(g)
cand_sets[bpos] = new_cand
if not cand_sets[bpos]:
print(f"[oracle] byte[{bpos}] 候选为空,需增加更多基底")
rec = [min(cs) if cs else 0 for cs in cand_sets]
for i,cs in enumerate(cand_sets):
if len(cs)>1:
print(f"[oracle] byte[{i}] 多解 {sorted(map(hex,cs))},取最小 {rec[i]:#04x}")
print(f"[oracle] byte[{i}] = {rec[i]:#04x}")
return b2l(bytes(rec))

# 已优化
def recover_k12_with_k13(io, K13, bases, active_byte_index=3, expect_prompt=True):
# Recover last round of 9-round using known K13
cand_sets = [set(range(256)) for _ in range(4)]
print(f"{len(bases) = }")
for base in bases:
sum_X12 = 0
U_cols = [[],[],[],[]]

ptl = [None for _ in range(256 * 256)]
for a in range(256):
for d in range(256):
pt = build_block(a, d, base=base, active_byte_index=active_byte_index)
ptl[a*256 + d] = pt.hex()
ctl = [None for _ in range(256 * 256)]

gap = 32
for i in trange(0, 256 * 256, gap):
io.send(("\n".join(ptl[i:i+gap]) + "\n").encode())
recv = io.recvlines(gap)
for j in range(gap):
ctl[i + j] = recv[j].split(b" ")[1].decode()

for a in range(256):
for d in range(256):
pt = bytes.fromhex(ptl[a*256 + d])
ct = bytes.fromhex(ctl[a*256 + d])
X13 = b2l(ct[0:4]); X12 = b2l(ct[4:8]); X11 = b2l(ct[8:12]); X10 = b2l(ct[12:16])
U10 = X10 ^ X11 ^ X12
X9 = X13 ^ T(U10 ^ K13)
# 9-round target sums S9,4 = X12
sum_X12 ^= X12
# 9-round U = X9 ^ X10 ^ X11
U9 = X9 ^ X10 ^ X11
Ub = l2b(U9)
for i in range(4):
U_cols[i].append(Ub[i])
t9 = L_inv(sum_X12)
t9_b = l2b(t9)
print(f"[oracle] (K13 known) base={tuple(hex(x) for x in base)} -> t9=0x{t9:08x}")
for bpos in range(4):
new_cand = set()
col = U_cols[bpos]
want = t9_b[bpos]
for g in cand_sets[bpos]:
acc = 0
for v in col:
acc ^= S[v ^ g]
if acc == want:
new_cand.add(g)
cand_sets[bpos] = new_cand
if not cand_sets[bpos]:
print(f"[oracle] (K13) byte[{bpos}] 候选为空,需要更多基底")
rec = [min(cs) if cs else 0 for cs in cand_sets]
for i,cs in enumerate(cand_sets):
if len(cs)>1:
print(f"[oracle] (K13) byte[{i}] 多解 {sorted(map(hex,cs))},取最小 {rec[i]:#04x}")
print(f"[oracle] (K13) byte[{i}] = {rec[i]:#04x}")
return b2l(bytes(rec))

# # 已优化
def recover_k11_with_k12_k13(io, K12, K13, bases, active_byte_index=3, expect_prompt=True):
# Recover last round of 8-round using known K12,K13
cand_sets = [set(range(256)) for _ in range(4)]
for base in bases:
sum_X11 = 0
U_cols = [[],[],[],[]]

ptl = [None for _ in range(256 * 256)]
for a in range(256):
for d in range(256):
pt = build_block(a, d, base=base, active_byte_index=active_byte_index)
ptl[a*256 + d] = pt.hex()
ctl = [None for _ in range(256 * 256)]

gap = 32
for i in trange(0, 256 * 256, gap):
io.send(("\n".join(ptl[i:i+gap]) + "\n").encode())
recv = io.recvlines(gap)
for j in range(gap):
ctl[i + j] = recv[j].split(b" ")[1].decode()

for a in range(256):
for d in range(256):
pt = bytes.fromhex(ptl[a*256 + d])
ct = bytes.fromhex(ctl[a*256 + d])
X13 = b2l(ct[0:4]); X12 = b2l(ct[4:8]); X11 = b2l(ct[8:12]); X10 = b2l(ct[12:16])
# step back to X9 with K13
U10 = X10 ^ X11 ^ X12
X9 = X13 ^ T(U10 ^ K13)
# step back to X8 with K12
U9 = X9 ^ X10 ^ X11
X8 = X12 ^ T(U9 ^ K12)
# 8-round target S8,4 = X11
sum_X11 ^= X11
# 8-round U = X8 ^ X9 ^ X10
U8 = X8 ^ X9 ^ X10
Ub = l2b(U8)
for i in range(4):
U_cols[i].append(Ub[i])
t8 = L_inv(sum_X11)
t8_b = l2b(t8)
print(f"[oracle] (K12,K13 known) base={tuple(hex(x) for x in base)} -> t8=0x{t8:08x}")
for bpos in range(4):
new_cand = set()
col = U_cols[bpos]
want = t8_b[bpos]
for g in cand_sets[bpos]:
acc = 0
for v in col:
acc ^= S[v ^ g]
if acc == want:
new_cand.add(g)
cand_sets[bpos] = new_cand
if not cand_sets[bpos]:
print(f"[oracle] (K12,K13) byte[{bpos}] 候选为空,需要更多基底")
rec = [min(cs) if cs else 0 for cs in cand_sets]
for i,cs in enumerate(cand_sets):
if len(cs)>1:
print(f"[oracle] (K12,K13) byte[{i}] 多解 {sorted(map(hex,cs))},取最小 {rec[i]:#04x}")
print(f"[oracle] (K12,K13) byte[{i}] = {rec[i]:#04x}")
return b2l(bytes(rec))

def recover_k10_with_k11_k12_k13(io, K11, K12, K13, bases, active_byte_index=3, expect_prompt=True):
# Recover last round of 7-round using known K11,K12,K13
cand_sets = [set(range(256)) for _ in range(4)]
for base in bases:
sum_X10 = 0
U_cols = [[],[],[],[]]

ptl = [None for _ in range(256 * 256)]
for a in range(256):
for d in range(256):
pt = build_block(a, d, base=base, active_byte_index=active_byte_index)
ptl[a*256 + d] = pt.hex()
ctl = [None for _ in range(256 * 256)]

gap = 32
for i in trange(0, 256 * 256, gap):
io.send(("\n".join(ptl[i:i+gap]) + "\n").encode())
recv = io.recvlines(gap)
for j in range(gap):
ctl[i + j] = recv[j].split(b" ")[1].decode()

for a in range(256):
for d in range(256):
pt = bytes.fromhex(ptl[a*256 + d])
ct = bytes.fromhex(ctl[a*256 + d])
X13 = b2l(ct[0:4]); X12 = b2l(ct[4:8]); X11 = b2l(ct[8:12]); X10 = b2l(ct[12:16])
# X9
U10 = X10 ^ X11 ^ X12
X9 = X13 ^ T(U10 ^ K13)
# X8
U9 = X9 ^ X10 ^ X11
X8 = X12 ^ T(U9 ^ K12)
# X7
U8 = X8 ^ X9 ^ X10
X7 = X11 ^ T(U8 ^ K11)
# 7-round target S7,4 = X10
sum_X10 ^= X10
# 7-round U = X7 ^ X8 ^ X9
U7 = X7 ^ X8 ^ X9
Ub = l2b(U7)
for i in range(4):
U_cols[i].append(Ub[i])
t7 = L_inv(sum_X10)
t7_b = l2b(t7)
print(f"[oracle] (K11..K13 known) base={tuple(hex(x) for x in base)} -> t7=0x{t7:08x}")
for bpos in range(4):
new_cand = set()
col = U_cols[bpos]
want = t7_b[bpos]
for g in cand_sets[bpos]:
acc = 0
for v in col:
acc ^= S[v ^ g]
if acc == want:
new_cand.add(g)
cand_sets[bpos] = new_cand
if not cand_sets[bpos]:
print(f"[oracle] (K11..K13) byte[{bpos}] 候选为空,需要更多基底")
rec = [min(cs) if cs else 0 for cs in cand_sets]
for i,cs in enumerate(cand_sets):
if len(cs)>1:
print(f"[oracle] (K11..K13) byte[{i}] 多解 {sorted(map(hex,cs))},取最小 {rec[i]:#04x}")
print(f"[oracle] (K11..K13) byte[{i}] = {rec[i]:#04x}")
return b2l(bytes(rec))


def reverse_master_from_k10_13(K10,K11,K12,K13):
# Recreate K[0..13] backwards
K = [0]*14
K[10],K[11],K[12],K[13] = K10,K11,K12,K13
# invert recurrence: K[i] = K[i+4] ^ T_key(CK[i] ^ K[i+1] ^ K[i+2] ^ K[i+3])
for i in range(9,-1,-1):
K[i] = K[i+4] ^ T_key(CK[i] ^ K[i+1] ^ K[i+2] ^ K[i+3])
# master key words = K[0..3] ^ FK
w0 = K[0] ^ FK[0]; w1 = K[1] ^ FK[1]; w2 = K[2] ^ FK[2]; w3 = K[3] ^ FK[3]
return b"".join(l2b(w) for w in (w0,w1,w2,w3))

# Decryption utilities to verify end-to-end

def enc_block_with_roundkeys(block_words, rk):
X = list(block_words)
for i in range(len(rk)):
X.append(X[i] ^ T(X[i+1] ^ X[i+2] ^ X[i+3] ^ rk[i]))
return (X[len(rk)], X[len(rk)+1], X[len(rk)+2], X[len(rk)+3])

def dec_block(key, block, nr=32):
rks = key_schedule(key, nr)
X = [b2l(block[i:i+4]) for i in range(0,16,4)]
# produce reversed rounds by feeding rk in reverse
for i in range(nr):
X.append(X[i] ^ T(X[i+1] ^ X[i+2] ^ X[i+3] ^ rks[nr-1-i]))
return b"".join(l2b(X[nr+3 - i]) for i in range(4))

def dec_cbc_zero_iv(key, ct):
from Crypto.Util.Padding import unpad
from Crypto.Util.strxor import strxor as bxor
iv = bytes(16)
pt = b""
prev = iv
for i in range(0, len(ct), 16):
block = ct[i:i+16]
m = dec_block(key, block, 32)
m = bxor(m, prev)
pt += m
prev = block
return unpad(pt, 16)


def main():
bases = [
(0x11223344,0x55667788,0x99aabbcc,0xddeeff00),
(0x01020304,0x05060708,0x0a0b0c0d,0x0e0f1011),
(0x89abcdef,0x01234567,0xfedcba98,0x76543210),
]
expect_prompt = True

# io = remote("60.205.163.215", 51556)
io = process(["python", "/d/CTF/Chall/2025/N1junior_2/SM4/attachment/server.py"])
flag_ct_hex = io.recvline().strip().decode()
print(f"[oracle] 32-round FLAG ct = {flag_ct_hex}")

# Recover K13 with intersection
K13 = recover_k13_from_oracle_and_intersect(io, bases, active_byte_index=3, expect_prompt=expect_prompt)
K13 = recover_k13_from_oracle_and_intersect(io, bases, active_byte_index=3)
print(f"[oracle] K13 = 0x{K13:08x}")

# Recover K12 using virtual 9-round
K12 = recover_k12_with_k13(io, K13, bases, active_byte_index=3, expect_prompt=expect_prompt)
print(f"[oracle] K12 = 0x{K12:08x}")

# Recover K11, K10
K11 = recover_k11_with_k12_k13(io, K12, K13, bases, active_byte_index=3, expect_prompt=expect_prompt)
print(f"[oracle] K11 = 0x{K11:08x}")
K10 = recover_k10_with_k11_k12_k13(io, K11, K12, K13, bases, active_byte_index=3, expect_prompt=expect_prompt)
print(f"[oracle] K10 = 0x{K10:08x}")

# Reverse to master key
master_key = reverse_master_from_k10_13(K10,K11,K12,K13)
print(f"[oracle] recovered master key = {master_key.hex()}")

# Use recovered master key to decrypt FLAG
flag_ct = bytes.fromhex(flag_ct_hex)
rec_flag = dec_cbc_zero_iv(master_key, flag_ct)
print(f"[oracle] FLAG (recovered key): {rec_flag.decode(errors='ignore')}")

# For verification compute true keys
true_master = bytes.fromhex("0123456789abcdeffedcba9876543210")
true_rks = key_schedule(true_master, 32)
print(f"[oracle] true K10..K13 = 0x{true_rks[7]:08x},0x{true_rks[8]:08x},0x{true_rks[9]:08x},0x{true_rks[10]:08x}")

io.close()

if __name__ == '__main__':
main()

References

  1. A. Joux, J. Loss, and G. Santato, “Dimensional eROSion: Improving the ROS Attack with Decomposition in Higher Bases,” Cryptology ePrint Archive, Paper 2025/306, 2025. ↩︎️
  2. F. Liu et al., “Analysis of the SMS4 Block Cipher,” in Proc. 12th Australasian Conf. on Information Security and Privacy (ACISP 2007), Lecture Notes in Computer Science, vol. 4586, J. Pieprzyk, H. Ghodosi, and E. Dawson, Eds. Berlin, Heidelberg: Springer-Verlag, 2007, pp. 158–170. ↩︎️