0%

2024-强网杯-final-wp-crypto

最想给st出题人一拳的一集,赛中没做出来的会打*。

shopping

题目:

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
flag = b"flag{test}"
from Crypto.Util.number import *
from gmpy2 import *
def next_prime(x):
while not isPrime(x):
x += 1
return x

success = 0

for i in range(20):
coins = 27
space = 0
p = getPrime(64)
delta = getRandomNBitInteger(30)
q = next_prime(p + delta)
N = p*q
print("Welcome to my supermarket\n")
while coins > 0:
choice = input('give me your choice\n')
if choice == '1':
space = int(input("What size of house would you like to purchase?\n"))
assert 1 <= space <= 10
ls = [0] * space
coins -= space * 5
print(f'{coins} coins left\n')
elif choice == '2':
op = input()
assert op in ['+', '-', '*', '//', '%', 'root']
a, b, c= input().split('.')
try:
if op == 'root':
exec(f'{a}=iroot({b},{c})[0]')
else:
exec(f'{a}={b}{op}{c}')
except:exit
if op in '+-':
coins -= 1
elif op in '*//%':
coins -= 3
else:
coins -= 5
print(f'{coins} coins left\n')

elif choice == '3':
state = 0
print("One coin to check\n")
coins -= 1
print("You must have decorated a beautiful house.\n")
assert coins >= 0
for i in ls:
if i > 1 and i < N and N%i == 0:
success += 1
state = 1
print(f'wonderful!, still {coins} coins left\n')
break
if state:
break

if success == 20:
print(f'Congratulations! Here is your flag:{flag}\n')

题目基于素数分解,对于两个64bit、且相差30bit以内的素数p、q乘积N,要在有限的步数内把他分解掉,并把结果存在ls数组里,从而获取success值。积累20点success值最终拿到flag。

聚焦一下运算执行步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
elif choice == '2':
op = input()
assert op in ['+', '-', '*', '//', '%', 'root']
a, b, c= input().split('.')
try:
if op == 'root':
exec(f'{a}=iroot({b},{c})[0]')
else:
exec(f'{a}={b}{op}{c}')
except:exit
if op in '+-':
coins -= 1
elif op in '*//%':
coins -= 3
else:
coins -= 5
print(f'{coins} coins left\n')

预期里p、q高位接近,应该是要对N开根然后加速费马分解之类。但既然用了exec,那么能用的方法就很多了:

  • 既然能输入N,那么自然也可以输入p、q啥的,所以拿满success压根不是问题

  • 可以给coins赋值,之后慢慢操作

  • 再比如可以给success赋值,直接达到flag要求

  • 最简单的话可以直接执行print(flag)语句,比如下面这个exp:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from pwn import *

    sh = process(["python3", "task.py"])
    sh.recvuntil(b"Welcome to my supermarket\n")
    sh.recvuntil(b'give me your choice\n')
    sh.sendline(b"2")
    sh.sendline(b"+")
    sh.sendline(b"a.print(flag).1")
    print(sh.recvline().strip().decode())



fak1

题目:

task.py:

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
from sage.all import *
from Crypto.Util.number import long_to_bytes
import os
import signal

def _handle_timeout(signum, frame):
raise TimeoutError('function timeout')

timeout = 60
signal.signal(signal.SIGALRM, _handle_timeout)
signal.alarm(timeout)

load("ntru.sage")
p,n,q,dg,df = 3,117,1091,35,37


cs = []
def Game():
while True:
pk,sk = genKeys()
try:
1 / pk
print(pk.list())
break
except:
pass

secret = os.urandom(23)
secretPoly = MsgtoPoly(secret)
print("Welcome to fak1. You can send some message to me. Have fun!")
msgs = [[0 for _ in range(116)]]

for _ in range(10):
msg = [round(int(i,16) % 3329) for i in input("Give me a list of message (hex):").split(" ")]
if len(msg) != 116 or (msg in msgs):
print("You bad!")
return
msgs.append(msg)
m = secretPoly + QR([round(i / 3329 * q) for i in msg])
c = encrypt(pk,m)
try:
assert m == decrypt(sk,c)
except:
print("You bad!!")
return
print(c.list())

if long_to_bytes(int(input("You wanner say something? :"), 16)) == secret:
print(flag)
print("You good!")
else:
print("OK, bye bye~")

Game()

ntru.sage:

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
import random
from Crypto.Util.number import bytes_to_long,long_to_bytes

p,n,q,dg,df = 3,117,1091,35,37
R.<x> = PolynomialRing(Zmod(q))
RR.<xx> = PolynomialRing(Zmod(p))
QR = R.quotient(x ^ n + 1)
QRR = RR.quotient(xx ^ n + 1)
flag = b"flag{*******************************************}"

def balancemodlist(List,p):
for i in range(len(List)):
if int(List[i]) > p / 2:
List[i] = int(List[i]) - p
return List

def randomPoly(d = 0):
if d == 0:
poly = QR([random.randint(0,1) for _ in range(n)])
else:
L_0 = [0] * (n - 2 * d - 1)
L_1 = (d + 1) * [1]
L_2 = d * [-1]
L = L_0 + L_1 + L_2
for i in range(randint(5,10)):
random.shuffle(L)
poly = QR(L)
return poly

def genPrivKey():
while True:
poly = randomPoly(df)
try:
poly_q = (1 / QR(poly))
poly_p = (1 / QRR(RR(balancemodlist(poly.list(),q))))
break
except:
pass
return poly, poly_q, poly_p

def MsgtoPoly(msg):
msg = Integer(bytes_to_long(msg))
m = msg.digits(p)
for i in range(len(m)):
if m[i] > p / 2:
m[i] -= p
return QR(m)

def genKeys():
f, f_q, f_p = genPrivKey()
g = randomPoly(dg)
pk = QR(p * g * f_q)
sk = (f,f_q,f_p,g)
return pk,sk

def encrypt(pk, m):
r = randomPoly()
return pk * r + m

def decrypt(sk,c):
f,_,f_p,_ = sk
m = QR(balancemodlist((QRR(balancemodlist((f * c).list(),q)) * f_p).list(),p))
return m

题目基于如下环上的ntru:

在连接上靶机之后,靶机会先生成:

  • 一组公私钥$pk,sk$,其中$pk$一定是可逆的
  • 一个secret_poly,记为$s$,$s$是在模3下balance过的,也就是说它由1、0、-1组成。

之后有10次,每次加密可以选择一个消息$m_i$,靶机会生成一个系数由0、1组成的多项式$r_i$,并计算:

并且在decrypt测试成功之后返回$c_i$给我们。

由于$m_i$是我们自己选的,所以不妨令$c_i = c_i - m_i$,那么每次加密本质上也就是对$s$的加密:

而多组加密用的是同一组公钥,那么两两作差可以消去s,得到:

由于$pk$一定可逆,那么把$pk$求逆移项就有:

那么对于向量$r_i - r_j$来说,其每个位置系数代表:

  • 系数为1,说明当前位置$r_i$分量为1,$r_j$分量为0
  • 系数为-1,说明当前位置$r_i$分量为0,$r_j$分量为1
  • 系数为0则不能做出判断

因此10组均用上作差,理论上来说很大概率可以恢复所有$r_i$,但是实际上只和$r_0$作差已经可以较大概率恢复出$r_0$,然后解出$s$就好。

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
from Crypto.Util.number import *
from random import *
from pwn import *

p,n,q,dg,df = 3,117,1091,35,37
R.<x> = PolynomialRing(Zmod(q))
RR.<xx> = PolynomialRing(Zmod(p))
PRq = R.quotient(x ^ n + 1)
PRp = RR.quotient(xx ^ n + 1)

sh = process(["sage", "task.py"])
pk = PRq(eval(sh.recvline().strip().decode()))
sh.recvuntil(b"Welcome to fak1. You can send some message to me. Have fun!")

msgs = [[randint(0, 1) for _ in range(116)] for i in range(10)]
msgs_send = [" ".join([hex(i)[2:].zfill(2) for i in msg]).encode() for msg in msgs]
ms = [PRq([round(i / 3329 * q) for i in msg]) for msg in msgs]

cs = []
for i in range(10):
sh.recvuntil(b"Give me a list of message (hex):")
sh.sendline(msgs_send[i])
cs.append(PRq(eval(sh.recvline().strip().decode())))

r0_ri = []
for i in range(1, 10):
r0_ri.append((cs[0]-cs[i] - (ms[0]-ms[i]))*pk^(-1))

r0 = ["*" for i in range(117)]
for i in r0_ri:
temp = i.list()
for j, t in enumerate(temp):
if(t == 1):
r0[j] = 1
if(t == 1090):
r0[j] = 0
r0 = PRq(r0)

sh.recvuntil(b"You wanner say something? :")
s = cs[0] - ms[0] - r0*pk
s = s.list()
ss = []

for i in s:
if(i == 1090):
ss.append(2)
else:
ss.append(i)
secret = 0
for i in range(len(ss)):
secret += int(3^i*int(ss[i]))

sh.sendline(hex(secret)[2:].encode())
print(sh.recvline())



bl0ck

题目:

aes.py:

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
#!/usr/bin/env python3

# Implementation based on https://github.com/boppreh/aes/blob/master/aes.py
"""
This is an exercise in secure symmetric-key encryption, implemented in pure
Python (no external libraries needed).

Original AES-128 implementation by Bo Zhu (http://about.bozhu.me) at
https://github.com/bozhu/AES-Python . PKCS#7 padding, CBC mode, PKBDF2, HMAC,
byte array and string support added by me at https://github.com/boppreh/aes.
Other block modes contributed by @righthandabacus.


Although this is an exercise, the `encrypt` and `decrypt` functions should
provide reasonable security to encrypted messages.
"""

import random


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

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


def sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = s_box[s[i][j]]


def inv_sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = inv_s_box[s[i][j]]


def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]


def inv_shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]

def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]


# learned from https://web.archive.org/web/20100626212235/http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)


def mix_single_column(a):
# see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)


def mix_columns(s):
for i in range(4):
mix_single_column(s[i])


def inv_mix_columns(s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
u = xtime(xtime(s[i][0] ^ s[i][2]))
v = xtime(xtime(s[i][1] ^ s[i][3]))
s[i][0] ^= u
s[i][1] ^= v
s[i][2] ^= u
s[i][3] ^= v

mix_columns(s)


r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)


def bytes2matrix(text):
""" Converts a 16-byte array into a 4x4 matrix. """
return [list(text[i:i+4]) for i in range(0, len(text), 4)]

def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return bytes(sum(matrix, []))

def xor_bytes(a, b):
""" Returns a new byte array with the elements xor'ed. """
return bytes(i^j for i, j in zip(a, b))


class AES:
"""
Class for AES-128 encryption with CBC mode and PKCS#7.

This is a raw implementation of AES, without key stretching or IV
management. Unless you need that, please use `encrypt` and `decrypt`.
"""
rounds_by_key_size = {16: 10, 24: 12, 32: 14}
def __init__(self, master_key, backdoor = None):
"""
Initializes the object with a given key.
"""
assert len(master_key) in AES.rounds_by_key_size
self.n_rounds = AES.rounds_by_key_size[len(master_key)]

# 😈 backdoored by crazyman
# ===
if backdoor is not None:
self.n_rounds = (self.n_rounds & 0b1010) - 1

self._key_matrices = self._expand_key(master_key)


operations = []

operations.extend([
(add_round_key, self._key_matrices[0])
])

for i in range(1, self.n_rounds):
operations.extend([
(sub_bytes, ),
(shift_rows, ),
(mix_columns, ),
(add_round_key, self._key_matrices[i])
])

operations.extend([
(sub_bytes, ),
(shift_rows, ),
(add_round_key, self._key_matrices[-1])
])

self.operations = operations

def _expand_key(self, master_key):
"""
Expands and returns a list of key matrices for the given master_key.
"""
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4

i = 1
while len(key_columns) < (self.n_rounds + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])

# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]

# XOR with equivalent word from previous iteration.
word = xor_bytes(word, key_columns[-iteration_size])
key_columns.append(word)

# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]

def encrypt_block(self, plaintext):
"""
Encrypts a single block of 16 byte long plaintext.
"""
assert len(plaintext) == 16

plain_state = bytes2matrix(plaintext)

for operation, *args in self.operations:
operation(plain_state, *args)

return matrix2bytes(plain_state)

server.py:

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
from aes import AES
from secret import flag

def xor(a, b):
return bytes([x ^ y for x, y in zip(a * 2, b)])

class HAeSH:
def __init__(self, key):
self.state = b'\x00' * 16
self.key = key
self.blocks = []

def update(self, block, backdoor = None):
assert len(block) == 16 or len(block) == 24 or len(block) == 32, "🚫 Block length should be 16,24,32"
self.blocks.append(block)
mid_state = xor(self.state, block)
if len(self.blocks) > 1:
assert mid_state not in self.blocks, "🚫 Block repetition detected"
self.state = xor(self.state,
AES(mid_state, backdoor).
encrypt_block(self.key))

def digest(self):
return self.state

def blocks_length_check(blocks):
if len(blocks) <= 1: return True
return len(blocks[-1]) == len(blocks[0])

def main():
key = bytes.fromhex(input('🔑: '))
assert len(key) == 16, "Key length should be 16"
hash = HAeSH(key)
blocks = []
for _ in "HAeSH"[2:4]:
block = bytes.fromhex(input(f"🥢[{_}]: "))
hash.update(block, True)
blocks.append(block)
if not blocks_length_check(blocks):
print("🚫 Block length mismatch")
return
quit_option = input("🔚 Quit? (y/n): ")
if quit_option == "y":
break

if hash.digest() == b'\x00' * 16:
print("🔓 Success!")
print(f'🏁 {flag}')
else:
print("🔒 Failed!")

if __name__ == '__main__':
main()

题目基于加了后门的AES。首先通过测试可以发现,不加后门的话,题目是标准的AES实现,而后门的作用是减少AES轮数:

而交互流程是:

  • 输入长16字节的key

  • 之后靶机初始化一个HAeSH类hash,其内部state初始值为全0

  • 可以有最多两轮的交互,每次交互可以输入一个block,并更新hash的内部state值

    如果交互两次的话,需要满足以下几个要求才行:

    • 输入的两个block需要等长
    • AES用于作为加密密钥的midstate不能相同
  • 如果交互结束后,hash的state值依然是16字节全0,就可以获得flag

对于每一次交互来说,输入一个block后,hash的state更新是这样的(state记为s,block记为b,key记为k):

由于s初始值为16字节全0,所以对于两次交互来说应该是:

所以我们有以下两种可能可以获得flag的方法:

  • 只交互一轮,那么就要满足:

  • 交互两轮,那么就要满足:

    且:

显然第一种会简单很多,我们可以本地先任意输入$b_1$,然后对0做解密,最后把得到的结果作为key发送并发送$b_1$即可。

预期是用两轮的,是用论文数据来作为9轮AES的碰撞,可以参考dbt的wp

exp(先把题目附件的aes.py加上decrypt block,直接用题目附件给出的网址对应实现即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from aes import AES
from pwn import *

b1 = b"tangcuxiaojikuai"
key = AES(b1, True).decrypt_block(b"\x00"*16)

sh = process(["python3", "server.py"])
sh.sendline(key.hex().encode())
sh.recvuntil(b"]: ")
sh.sendline(b1.hex().encode())
sh.recvuntil(b"(y/n): ")
sh.sendline(b"y")

sh.recvline()
print(sh.recvline())



signin

题目:

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
from hashlib import sha256
from Crypto.Util.number import inverse, bytes_to_long
from random import randint
from os import urandom
from secret import flag
import signal

p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
a = 0
b = 7
gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

zero = (0, 0)
G = (gx, gy)


def add(p1, p2):
if p1 == zero:
return p2
if p2 == zero:
return p1
(p1x, p1y), (p2x, p2y) = p1, p2
if p1x == p2x and (p1y != p2y or p1y == 0):
return zero
if p1x == p2x:
tmp = (3 * p1x * p1x + a) * inverse(2 * p1y, p) % p
else:
tmp = (p2y - p1y) * inverse(p2x - p1x, p) % p
x = (tmp * tmp - p1x - p2x) % p
y = (tmp * (p1x - x) - p1y) % p
return (int(x), int(y))


def mul(n, p):
r = zero
tmp = p
while 0 < n:
if n & 1 == 1:
r = add(r, tmp)
n, tmp = n >> 1, add(tmp, tmp)
return r


def sign(msg, q, d, G):
z = int(sha256(msg).hexdigest(), 16)
while True:
k = randint(1, q - 1)
P = mul(k, G)
r = P[0]
s = inverse(k, q) * (z + r * d) % q
if r != 0 and s != 0:
return r, s, k.bit_length()


def verify(sig, msg, q, G, Q):
r, s = sig
print(r, s)
if r == 0 or s == 0:
return False
z = int(sha256(msg).hexdigest(), 16)
u1 = z * inverse(s, q) % q
u2 = r * inverse(s, q) % q
P = add(mul(u1, G), mul(u2, Q))
return P[0] == r


signal.alarm(240)
menu = \
'''
1.sign
2.verify
3.exit
'''

secret_key = bytes_to_long(urandom(27) + b"qwbs8")
Q = mul(secret_key, G)

count = 678
for i in range(count):
print(menu)
op = int(input(">").strip())
if op == 1:
msg = input("msg: ").strip().encode()
if msg == b"qwbs8_send_flag_to_me":
print("another msg plz.")
else:
r, s, kbits = sign(msg, order, secret_key, G)
print(r, s, kbits)
elif op == 2:
msg = input("msg: ").strip().encode()
ur = int(input("r: ").strip())
us = int(input("s: ").strip())
sig = (ur, us)
if verify(sig, msg, order, G, Q):
if msg == b"qwbs8_send_flag_to_me":
print(flag)
else:
print(True)
else:
print("malformed signature.")
else:
exit(0)

题目基于ECDSA,目标是伪造指定消息qwbs8_send_flag_to_me的签名值,为此题目给了我们678次签名机会,特殊的地方在于以下几个:

  • 私钥$d$的低5字节固定为qwbs8
  • 每次签名除了给出$(r,s)$对,还会额外给出作为nonce的$k$的比特数kbits

赛中思路

给了kbits这一信息,那很显然是要做HNP,同时又因为有$d$的低位,因此考虑不作差消去$d$,而直接把他的低位代入签名式:

既然是要做HNP,那么$k_i$当然要选kbits比较小的,而搜了一下论文发现关于256bit的曲线,2bit以内的泄露做HNP会比较难,于是考虑选择kbits小于等于253的$k_i$,一般来说符合条件的签名会有70-110组。

然而本地造格测了一下发现这样直接HNP,目标向量差的还是有点多,于是考虑做balance优化。balance优化很简单,比如对于当前式子,规约出来的目标向量中的值范围应该是:

那么通过减去1bit的MSB,范围大小可以变成:

这个优化使得目标向量所有值的数量级都下降了1bit,利好规约。此外为了提高成功率还可以做的是:

  • 反复重连靶机,直到满足要求的$k_i$有100组往上

  • 用BKZ进行规约

    但这个很慢,时间限制240s,跑不完。所以最后还是选了flatter,大不了多开几个靶机多连几次

比赛中rec就用这个思路,多开了几个靶机反复交互最后出了flag。

赛后思路

然而仔细看,上面其实有一个细节问题,就是对于kbit的$k$,他的范围其实应该是:

所以减去MSB(也就是$2^{kbit-1}$)后范围其实变成了:

这其实并不充分,我们还可以减去一次$2^{kbit-2}$得到:

这样相当于利用上了kbit所透露的下界,如此一来相当于一次签名可以多偷两bit,这样就稳定多了,测试了一下发现说60组kbit小于等于253+BKZ已经可以做到几乎百分百出,并且也很快。

用LLL都行,因为一次偷2bit真的很宽松了就

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
from Crypto.Util.number import *
from hashlib import *
from pwn import *

context.log_level = "critical"

sh = process(["python3", "task.py"])

############################################################################## part1 get data
msg = b"tangcuxiaojikuai"
sigs = []
maxbits = 253
for i in range(677):
sh.recvuntil(b">")
sh.sendline(b"1")
sh.recvuntil(b"msg: ")
sh.sendline(msg)
sig = list(map(int, sh.recvline().strip().decode().split(" ")))
if(sig[2] <= maxbits):
sigs.append(sig)
nums = 60
assert nums <= len(sigs)


############################################################################## part2 BKZ
dl = bytes_to_long(b"qwbs8")
q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
z = int(sha256(msg).hexdigest(), 16)

L = Matrix(ZZ, 2*(nums+1), 2*(nums+1))
for i in range(nums):
ri, si, kbitsi = sigs[i]
delta = 2^(kbitsi-1) + 2^(kbitsi-2)
ci = delta*si - z - ri*2^255 - ri*dl
L[0, nums+2+i] = -ri*2^40
L[1+i, nums+2+i] = si
L[nums+1, nums+2+i] = ci
for i in range(nums+2):
L[i, i] = 1
for i in range(nums):
L[-i-1, -i-1] = q

K = diagonal_matrix([2^maxbits]*(nums+2) + [2^300]*nums)
Q = diagonal_matrix([2^215] + [2^(sig[-1]-1) for sig in sigs[:nums]] + [1] + [1]*nums)
L = L * (K / Q)
L = L.BKZ()
L = L / (K / Q)


############################################################################## part3 get d
for i in L:
if(i[nums+1] == 1 and all(j == 0 for j in i[nums+2:])):
d = 2^40*(i[0] + 2^215) + dl
break
elif(i[nums+1] == -1 and all(j == 0 for j in i[nums+2:])):
d = 2^40*(-i[0] + 2^215) + dl
break


############################################################################## part4 forge
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
a = 0
b = 7
gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

zero = (0, 0)
G = (gx, gy)
def add(p1, p2):
if p1 == zero:
return p2
if p2 == zero:
return p1
(p1x, p1y), (p2x, p2y) = p1, p2
if p1x == p2x and (p1y != p2y or p1y == 0):
return zero
if p1x == p2x:
tmp = (3 * p1x * p1x + a) * inverse(2 * p1y, p) % p
else:
tmp = (p2y - p1y) * inverse(p2x - p1x, p) % p
x = (tmp * tmp - p1x - p2x) % p
y = (tmp * (p1x - x) - p1y) % p
return (int(x), int(y))


def mul(n, p):
r = zero
tmp = p
while 0 < n:
if n & 1 == 1:
r = add(r, tmp)
n, tmp = n >> 1, add(tmp, tmp)
return r

def sign(msg, q, d, G):
z = int(sha256(msg).hexdigest(), 16)
while True:
k = randint(1, q - 1)
P = mul(k, G)
r = P[0]
s = inverse(k, q) * (z + r * d) % q
if r != 0 and s != 0:
return r, s

r, s = sign(b"qwbs8_send_flag_to_me", q, d, G)


############################################################################## part5 get flag
sh.recvuntil(b">")
sh.sendline(b"2")
sh.recvuntil(b"msg: ")
sh.sendline(b"qwbs8_send_flag_to_me")
sh.recvuntil(b"r: ")
sh.sendline(str(r).encode())
sh.recvuntil(b"s: ")
sh.sendline(str(s).encode())
sh.recvline()
print(sh.recvline())



*f2ke

题目:

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
from sage.all import *
from Crypto.Util.number import long_to_bytes
import os
import signal

def _handle_timeout(signum, frame):
raise TimeoutError('function timeout')

timeout = 70
signal.signal(signal.SIGALRM, _handle_timeout)
signal.alarm(timeout)

load("ntru.sage")
p,n,q,dg,df = 3,117,1091,35,37


def Game():
secret = os.urandom(23)
secretpoly = MsgtoPoly(secret)
print("Welcome to f2ke. You can send some message to me. Have fun!")
msgs = [[0 for _ in range(116)]]

for _ in range((n + 3) // 2):
while True:
pk,sk = genKeys()
try:
1 / pk
break
except:
pass
msg = [round(int(i,16) % 3329) for i in input("Give me a list of message (hex):").split(" ")]
if len(msg) != 116 or (msg in msgs):
print("You bad!")
return
msgs.append(msg)
m = secretpoly + QR([round(i / 3329 * q) for i in msg])
c = encrypt(pk,m)
try:
assert m == decrypt(sk,c)
except:
print("You bad!!")
return
print(c.list())
print(pk.list())

if long_to_bytes(int(input("You wanner say something? :"), 16)) == secret:
print(flag)
print("You good!")
else:
print("OK, bye bye~")

Game()

和fak1相比:

  • ntru.sage文件没有什么变化
  • 交互次数从10次提高到了60次
  • 每一次交互都会重新生成一组公钥

相当于对于本题来说,我们最多可以获取60组如下形式的密文(仍然做差消去$m_i$):

依然需要解出s来得到flag。

赛中思路

题目情境很符合ntru的广播攻击,广播攻击的核心思路为:

  • 处理密文等式得到:

  • 把等式化为矩阵方程:

  • 如果能够确定$\textbf{r}_i$的内积(也就是$\textbf{r}_i^T\textbf{r}_i$的值),记为$t_i$,那么可以构建一个矩阵方程为:

  • 这其实是个二次等式,所以对应的应该有$n^2$个变量,消去对称的项应该有$\frac{n^2}{2}$个变量

  • 由于在环$R_q$的卷积矩阵的特殊性,所以大多数变量其实系数相同,在模q下又可以合并起来,最终得到的变量个数是$n + \lceil \frac{n}{2} \rceil$个

  • 因此如果能搜集到$n + \lceil \frac{n}{2} \rceil$就可以解线性方程得到$s$

而对于这个题来讲,想要应用广播攻击的思路,有几个问题需要解决:

  • $\textbf{r}_i$的内积并不知道
  • 等式数量仅有$\frac{n+3}{2}$个,不够$n + \lceil \frac{n}{2} \rceil$,得不到线性方程唯一解

对于第一个问题,由于$\textbf{r}_i$是由01组成的,因此可以对他做$y=2x-1$的线性变换,此时得到的$\textbf{r}’_i$就是一个由1、-1组成的向量,那么其内积显然就是它的长度,也就是n。

而对于第二个问题,对于60次交互,我们能构造60个方程,而实际变量有$117 + \lceil \frac{117}{2} \rceil = 176$个,因此我们能构造的实际上是一个求不出唯一解的不满秩矩阵方程:

而由于我们目标的解x是s的平方变量合并后的结果,其前面176-117个变量是二次项的合并,而后117个变量就是s本身,因此解x并不大,所以期望可以用LLL求一下试试看有没有结果。

但是很遗憾规约结果似乎总是差点,所以这个思路走不通。

非预期解

赛后问了一下解出来的师傅的做法,发现说在环$R_q$下,$x^n+1$其实有如下小系数+稀疏系数的分解:

所以可以在两个子环上对公钥做BKZ,之后crt就可以把私钥求出来,然后解密就得到$s$

其实这个确实应该留个心眼,比如之前折磨过我的一次DASCTF就有一个分解这种不太容易看出来子环的RLWE:

2024-DASCTF-GFCTF-wp-crypto | 糖醋小鸡块的blog (tangcuxiaojikuai.xyz)

hashhash说直接对在$R_q$上BKZ打公钥也可以,不过需要七分钟,超过了限时

甚至听说用g6k可以七八十秒完成,黑科技就是黑科技

预期

赛后rec问了下k1r的预期,思路实在是相当巧妙。

漏洞点依然在于$\textbf{r}_i$是由01组成的,那么回顾刚才的矩阵方程:

$\textbf{r}_i$中的每个分量都是0或1,而0、1是方程$x(x-1)=0$的解,所以对于每个分量,都可以用方程$x(x-1)=0$构造一条等式,因此一次交互实际上来说我们能获得的是n个等式。

而变量有多少个呢?我们每次构造的方程其实也就是关于向量$\textbf{s}$的二次方程,因此去除对称项,变量数量就是$\frac{n^2+n}{2}$个二次项加上$n$个一次项,所以对于$\frac{n+3}{2}$次交互来说刚刚好。

然而一个显著的问题在于时间,题目限时70s,而要完成题目的时间包括:

  • 连靶机拿数据的时间,本地打没啥延迟,线下的话可能得好几秒才行

  • 建矩阵的时间,这一部分我没有怎么优化过,大概要一分钟

    这里应该有些办法优化的

  • 求解矩阵方程的时间,这个我就需要70s,所以根本过不了题目

所以也许需要点算力或者改写成cpp实现啥的才行,我是本地把timeout改成300s然后打的。

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
from pwn import *
from tqdm import *

p,n,q,dg,df = 3,117,1091,35,37

R.<x> = PolynomialRing(Zmod(q))
RR.<xx> = PolynomialRing(Zmod(p))
PRq = R.quotient(x ^ n + 1)
PRp = RR.quotient(xx ^ n + 1)

############################################################################### part1 get data
nums = (n+3)//2
sh = process(["sage", "task.py"])
sh.recvuntil(b"Welcome to f2ke. You can send some message to me. Have fun!")

msgs = [[randint(0, 1) for _ in range(116)] for i in range(nums)]
msgs_send = [" ".join([hex(i)[2:].zfill(2) for i in msg]).encode() for msg in msgs]
ms = [PRq([round(i / 3329 * q) for i in msg]) for msg in msgs]

pk = []
c = []
for i in range(nums):
sh.recvuntil(b"Give me a list of message (hex):")
sh.sendline(msgs_send[i])
c.append(PRq(eval(sh.recvline().strip().decode())) - ms[i])
pk.append(PRq(eval(sh.recvline().strip().decode())))


############################################################################### part2 construct matrix
def construct_poly_mul_mat(n,v,b):
assert v[-1] == 1 #use this after monic

#step1 construct a matrix of d=a*b
mat1 = Matrix(ZZ,n,2*n-1)
for i in range(n):
for j in range(n):
mat1[i,j+i] = b[j]

#step2 construct a matrix of c=d%v
mat2 = Matrix(ZZ,2*n-1,n)
for i in range(n):
mat2[i,i] = 1
for i in range(n,2*n-1):
for j in range(i-n,n):
mat2[i,j] = -v[j-(i-n)]

init_row = vector(ZZ,n*[0])
for j in range(i-n):
temp = -v[n-1-j]*vector(ZZ,mat2[i-j-1])
init_row += temp
for j in range(n):
mat2[i,j] += init_row[j]

#step3 multiply them and return
return(mat1*mat2)

PRp.<x> = PolynomialRing(Zmod(q))
f = x^n + 1
fv = f.list()


L = []
R = []
for i in trange(nums):
ci = vector(Zmod(q), c[i].list())
Hi = Matrix(Zmod(q), construct_poly_mul_mat(n, fv, (pk[i]^(-1)).list()))
ai = (ci*Hi).list()


for j in range(n):
aij = ai[j]
coeffj = (-Hi[:, j:j+1]).list()

vec1 = [aij] + coeffj
vec2 = [aij-1] + coeffj
vec = [0 for _ in range(n*(n+3)//2)]

R.append(-(aij * (aij-1)) % q)
for u in range(n):
vec[u] = (aij*coeffj[u] + (aij-1)*coeffj[u]) % q
vec[n+u] = coeffj[u]^2 % q

ind = 2*n
for u in range(n):
for v in range(u+1, n):
vec[ind] = 2*coeffj[u]*coeffj[v] % q
ind += 1
L.append(vec)


############################################################################### part3 get s and flag
L = Matrix(Zmod(q), L)
R = vector(Zmod(q), R)
s = L.solve_right(R).list()[:n]

ss = []
for i in s:
if(i == 1090):
ss.append(2)
else:
ss.append(i)
secret = 0
for i in range(len(ss)):
secret += int(3^i*int(ss[i]))

sh.sendline(hex(secret)[2:].encode())
print(sh.recvline())



*b1ock

题目:

des.py:

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
import random
# DES S-boxes
S_BOXES = [
# S1
[
[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7],
[0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8],
[4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0],
[15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13]
],
# S2
[
[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10],
[3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5],
[0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15],
[13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9]
],
# S3
[
[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8],
[13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1],
[13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7],
[1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12]
],
# S4
[
[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15],
[13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9],
[10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4],
[3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14]
],
# S5
[
[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9],
[14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6],
[4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14],
[11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3]
],
# S6
[
[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11],
[10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8],
[9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6],
[4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13]
],
# S7
[
[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1],
[13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6],
[1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2],
[6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12]
],
# S8
[
[13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7],
[1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2],
[7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8],
[2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11]
]
]

# Initial and Final Permutation tables
IP = [58, 50, 42, 34, 26, 18, 10, 2,
60, 52, 44, 36, 28, 20, 12, 4,
62, 54, 46, 38, 30, 22, 14, 6,
64, 56, 48, 40, 32, 24, 16, 8,
57, 49, 41, 33, 25, 17, 9, 1,
59, 51, 43, 35, 27, 19, 11, 3,
61, 53, 45, 37, 29, 21, 13, 5,
63, 55, 47, 39, 31, 23, 15, 7]

FP = [40, 8, 48, 16, 56, 24, 64, 32,
39, 7, 47, 15, 55, 23, 63, 31,
38, 6, 46, 14, 54, 22, 62, 30,
37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28,
35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 42, 10, 50, 18, 58, 26,
33, 1, 41, 9, 49, 17, 57, 25]

# PC-1 - Permuted choice 1
PC1 = [57, 49, 41, 33, 25, 17, 9,
1, 58, 50, 42, 34, 26, 18,
10, 2, 59, 51, 43, 35, 27,
19, 11, 3, 60, 52, 44, 36,
63, 55, 47, 39, 31, 23, 15,
7, 62, 54, 46, 38, 30, 22,
14, 6, 61, 53, 45, 37, 29,
21, 13, 5, 28, 20, 12, 4]

# PC-2 - Permuted choice 2
PC2 = [14, 17, 11, 24, 1, 5,
3, 28, 15, 6, 21, 10,
23, 19, 12, 4, 26, 8,
16, 7, 27, 20, 13, 2,
41, 52, 31, 37, 47, 55,
30, 40, 51, 45, 33, 48,
44, 49, 39, 56, 34, 53,
46, 42, 50, 36, 29, 32]

# Left shifts for each round
SHIFTS = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]

E = [32, 1, 2, 3, 4, 5,
4, 5, 6, 7, 8, 9,
8, 9, 10, 11, 12, 13,
12, 13, 14, 15, 16, 17,
16, 17, 18, 19, 20, 21,
20, 21, 22, 23, 24, 25,
24, 25, 26, 27, 28, 29,
28, 29, 30, 31, 32, 1]

P = [16, 7, 20, 21, 29, 12, 28, 17,
1, 15, 23, 26, 5, 18, 31, 10,
2, 8, 24, 14, 32, 27, 3, 9,
19, 13, 30, 6, 22, 11, 4, 25]

def _initial_permutate(block):
# Apply initial permutation
return ''.join(block[i-1] for i in IP)

def _final_permutate(block):
# Apply final permutation
return ''.join(block[i-1] for i in FP)

def _exchange_blocks(block):
# Exchange left and right halves
return block[32:64] + block[:32]

def _pbox_permutate(block):
# P-box permutation
return block[0:64] + "".join(block[64:].zfill(32)[i-1] for i in P).zfill(32)

def _sbox_permutate(block):
# Apply S-boxes
l, r, x = block[0:32], block[32:64], block[64:].zfill(48)

output = ''
for i in range(8):
chunk = x[i*6:(i+1)*6]
row = int(chunk[0] + chunk[5], 2)
col = int(chunk[1:5], 2)
output += bin(S_BOXES[i][row][col])[2:].zfill(4)
return l + r + output

def _add_round_key(block, key):
# XOR block with key
l, r, er = block[0:32], block[32:64], block[64:].zfill(48)
return l + r + bin(int(er, 2) ^ key)[2:].zfill(48)

def _extend_block(block):
# Extend block to 48 bits
l, r = block[0:32], block[32:64]
return l + r + ''.join(r[i-1] for i in E)

def _xor_blocks(block):
# XOR left and right halves
l, r, rr = block[0:32], block[32:64], block[64:96].zfill(32)
return bin(int(l,2) ^ int(rr,2))[2:].zfill(32) + r

class DES:
def __init__(self, master_key, rounds=16, backdoor = None):
R = random.Random()

self.rounds = rounds
self._subkeys = self._expand_key(master_key)

operations = []
operations.extend([
(_initial_permutate, ),
])

for i in range(self.rounds):
operations.extend([
(_extend_block, ),
(_add_round_key, self._subkeys[i]),
(_sbox_permutate, ),
(_pbox_permutate, ),
(_xor_blocks, ),
(_exchange_blocks, ),
])


operations.extend([
(_exchange_blocks, ),
(_final_permutate, ),
])

# 😈 backdoored by CRAZYMAN!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
if backdoor:
R.seed(backdoor)
operations = R.choices(operations, k = len(operations))

self.operations = operations

def _expand_key(self, master_key):
key_bin = bin(int.from_bytes(master_key, 'big'))[2:].zfill(64)

# Apply PC-1
key = ''.join([key_bin[i - 1] for i in PC1])

# Split into left and right halves
c = key[:28]
d = key[28:]

subkeys = []
# Generate subkeys
for i in range(16):
# Left shift
c = c[SHIFTS[i]:] + c[:SHIFTS[i]]
d = d[SHIFTS[i]:] + d[:SHIFTS[i]]

# Combine C and D
cd = c + d

# Apply PC-2
subkey = ''.join([cd[i - 1] for i in PC2])
subkeys.append(int(subkey,2))

for i in range(16, self.rounds):
subkeys.append(subkeys[i % 16])

return subkeys[:self.rounds]

def encrypt_block(self, plaintext):
assert len(plaintext) == 8

state = bin(int.from_bytes(plaintext, 'big'))[2:].zfill(64)

for operation, *args in self.operations:
state = operation(state, *args)

return int(state[:64],2).to_bytes(8, 'big')

server.py:

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
import os
import signal
from Crypto.Cipher import DES as pyDES
from secret import flag
from des import DES

def _handle_timeout(signum, frame):
raise TimeoutError('function timeout')

timeout = 15
signal.signal(signal.SIGALRM, _handle_timeout)
signal.alarm(timeout)

def _test():
key = os.urandom(8)
des = pyDES.new(key, pyDES.MODE_ECB)
mydes = DES(key, rounds=16)
mydes_backdoored = DES(key, rounds=16, backdoor = 0xdeadbeef)
for _ in range(200):
plaintext = os.urandom(8)
ciphertext = des.encrypt(plaintext)
assert (mydes.encrypt_block(plaintext) == ciphertext)
assert (mydes_backdoored.encrypt_block(plaintext) != ciphertext)

def main():
seed = int(input('🌱 '))
key = os.urandom(8)

cipher = DES(key, rounds = 48, backdoor = seed)

m0 = os.urandom(8)
c0 = cipher.encrypt_block(m0)
print(f'🤐 {c0.hex()}')

for _ in "🥵 You Only Live Once!"[0]:
m = bytes.fromhex(input('💬 '))
if m == m0:
print(f'🏁 {flag}')
exit()
c = cipher.encrypt_block(m)
print(f'🤫 {c.hex()}')

if __name__ == '__main__':
_test()
main()

第一天结束的时候这个题似乎是0解,所以第一天晚上的时间都花在了白天花了比较多功夫的st那一坨上。第二天回来的时候,这个题解变多了不少,但是又上了两个新题目,以及还剩下一坨st,所以也没有回头看这一道。

再加上确实不太会对称,看到对称就很头大,想逃跑

赛后看了dbt的wp发现这个题很巧妙且有趣,所以简单记一记思路。

题目基于带有后门的48轮DES,其后门在于可以指定一个种子,由种子固定的random会选择和DES正常流程不一样的操作序列,并按这个序列执行加密得到结果。题目允许自己制定种子,并给出一组密文要求还原明文,得到flag。

可以想到这样操作的话,按照普通的des应该会报错,因为输入输出的比特数对于某些操作来说都不一样,所以必须要用某些操作让他们对齐。

而这对齐的操作就有可能出现问题,出现的问题可能会导致DES的加密完全不受密钥影响,因此可以逐步解密还原。而比赛中就停在了这一步,因为分析究竟在什么情况下,轮密钥加这个操作不会影响到加密过程,这个对我来说难度有点太大。

而实际上这一步在wp里用巧妙的方式bypass掉了,题目的test函数就做了个暗示——可以把带后门的DES当作一个黑盒,测试其在哪些种子下对于相同的明文能输出相同的密文,那么这些种子对应的操作序列就是不受轮密钥加影响的。

能想到这一步之后,剩下的步骤就是苦力活,对于找到的种子对应的操作序列写逆操作解密即可,详情可以参考dbt的wp



$st

这不算个题,至少绝对不算个crypto题,所以不写了。

浪费了一晚上时间(╬゚д゚)▄︻┻┳═一