check-little

侥幸拿到一血

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

from Crypto.Util.Padding import pad

from Crypto.Cipher import AES

import os

flag, key = open('secret').read().split('\n')

e = 3

while 1:

p = getPrime(1024)

q = getPrime(1024)

phi = (p - 1) * (q - 1)

if phi % e != 0:

break

N = p * q

c = pow(key, e, N)

iv = os.urandom(16)

ciphertext = AES.new(key = long_to_bytes(key)[:16], iv = iv, mode = AES.MODE_CBC).encrypt(pad(flag.encode(),16)).hex()

f = open('output.txt', 'w')

f.write(f'N = {N}\n')

f.write(f'c = {c}\n')

f.write(f'iv = {iv}\n')

f.write(f'ciphertext = {ciphertext}\n')
  1. 计算 g = gcd(c, N)。若 1 < g < N,则 gpq,直接分解出 N = g * (N // g)
  2. 计算 phi = (p - 1) * (q - 1),并求私钥 d = inverse(e, phi)
  3. 恢复对称密钥:key_int = pow(c, d, N),再转为字节 key_bytes = long_to_bytes(key_int)
  4. 使用 aes_key = key_bytes[:16] 与给定的 iv 解密 AES-CBC ciphertext(PKCS#7),得到明文 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
from Crypto.Util.number import long_to_bytes, inverse
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import math



N = 18795243691459931102679430418438577487182868999316355192329142792373332586982081116157618183340526639820832594356060100434223256500692328397325525717520080923556460823312550686675855168462443732972471029248411895298194999914208659844399140111591879226279321744653193556611846787451047972910648795242491084639500678558330667893360111323258122486680221135246164012614985963764584815966847653119900209852482555918436454431153882157632072409074334094233788430465032930223125694295658614266389920401471772802803071627375280742728932143483927710162457745102593163282789292008750587642545379046283071314559771249725541879213
c = 10533300439600777643268954021939765793377776034841545127500272060105769355397400380934565940944293911825384343828681859639313880125620499839918040578655561456321389174383085564588456624238888480505180939435564595727140532113029361282409382333574306251485795629774577583957179093609859781367901165327940565735323086825447814974110726030148323680609961403138324646232852291416574755593047121480956947869087939071823527722768175903469966103381291413103667682997447846635505884329254225027757330301667560501132286709888787328511645949099996122044170859558132933579900575094757359623257652088436229324185557055090878651740
iv = b'\x91\x16\x04\xb9\xf0RJ\xdd\xf7}\x8cW\xe7n\x81\x8d'
ct = bytes.fromhex("bf87027bc63e69d3096365703a6d47b559e0364b1605092b6473ecde6babeff2")



p = math.gcd(c, N)
q = N // p



phi = (p - 1) * (q - 1)
d = inverse(3, phi)
key_int = pow(c, d, N)
key_bytes = long_to_bytes(key_int)



aes_key = key_bytes[:16]



cipher = AES.new(aes_key, AES.MODE_CBC, iv=iv)
pt = unpad(cipher.decrypt(ct), 16)
print(pt.decode())

ezran

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

from random import *

f = open('flag.txt', 'r')

flag = f.read().encode()

gift=b''

for i in range(3108):

r1 = getrandbits(8)

r2 = getrandbits(16)

x=(pow(r1, 2*i, 257) & 0xff) ^ r2

c=long_to_bytes(x, 2)

gift+=c

m = list(flag)

for i in range(2025):

shuffle(m)

c = "".join(list(map(chr,m)))

f = open('output.txt', 'w')

f.write(f"gift = {bytes_to_long(gift)}\n")

f.write(f"c = {c}\n")

这里有一道很类似的题目2024-同济大学第二届网络安全新生赛CatCTF-wp-crypto | 糖醋小鸡块的blog

虽然十分类似,但是用里面的exp打不出来,这里来分析一下题目的代码

首先这里加密的过程分为两步:

  1. 生成gift序列
  2. 打乱flag
1
2
3
4
5
6
for i in range(3108):
r1 = getrandbits(8)
r2 = getrandbits(16)
x = (pow(r1, 2*i, 257) & 0xff) ^ r2
c = long_to_bytes(x, 2)
gift += c

这里代码很好理解

循环3108次,每次都生成一个8位的随机数和一个16位的随机数

然后计算 x = (pow(r1, 2*i, 257) & 0xff) ^ r2

这里需要注意& 0xff,那么就是取到结果的低8位,再与r2进行异或

最后将x转为字节放入gift中

1
2
3
4
m = list(flag)
for i in range(2025):
shuffle(m)
c = "".join(list(map(chr, m)))

这里是打乱flag的代码

先是将flag转为列表然后用函数打乱2025次

这道的漏洞就在于x和r2的高位是相同的

通过上面的分析我们知道

x是由(pow(r1, 2*i, 257) & 0xff) ^ r2计算得到的,既然这里只取低8位那么结果肯定是在0-255的

r2是是一个16位的随机数,范围是0-65536

两个数异或后也就不影响r2的高8位

所以我们x和r2的高8位是相同的

现在只需要提取出x的高8位就要我们r2的高8位了

因为这里提供的样本数据有3108个足够恢复了

接下来我们使用gf2bv库来进行状态恢复

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
from gf2bv import LinearSystem

from gf2bv.crypto.mt import MT19937

from Crypto.Util.number import *

from tqdm import *

from random import *

def mt19937(out):

lin = LinearSystem([32] * 624)

mt = lin.gens()

rng = MT19937(mt)

zeros = []

for i in range(len(out)):

​ r1 = rng.getrandbits(8)

​ r2 = rng.getrandbits(16)

​ zeros.append((r2 >> 8) ^ int(out[i]))

zeros.append(mt[0] ^ int(0x80000000))

sols = lin.solve_all(zeros)

return sols

gift = ""

gift = int(gift)

c = ""

gift = long_to_bytes(gift)

gifts = []

for i in range(0, len(gift), 2):

word = gift[i:i+2]

gifts.append(bytes_to_long(word) >> 8)

sols = mt19937(gifts)

for sol in sols:

rng = MT19937(sol)

pyrand = rng.to_python_random()

RNG = pyrand

STATE = RNG.getstate()[1][:-1]

STATE = STATE + (len(STATE),)

RNG.setstate((3,STATE,None))

for i in range(3108):

​ RNG.getrandbits(8)

​ RNG.getrandbits(16)

x = [i for i in range(len(c))]

for i in range(2025):

​ RNG.shuffle(x)

flag = ""

for i in range(len(c)):

​ flag += c[x.index(i)]

print(flag)

sk

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
from random import randint
from Crypto.Util.number import getPrime, inverse, long_to_bytes, bytes_to_long
from math import gcd
import signal
from secret import flag

def gen_coprime_num(pbits):
lbits = 2 * pbits + 8
lb = 2**lbits
ub = 2**(lbits + 1)
while True:
r = randint(lb, ub)
s = randint(lb, ub)
if gcd(r, s) == 1:
return r, s

def mult_mod(A, B, p):
result = [0] * (len(A) + len(B) - 1)
for i in range(len(A)):
for j in range(len(B)):
result[i + j] = (result[i + j] + A[i] * B[j]) % p


return result

def gen_key(p):
f = [randint(1, 2**128) for i in ":)"]
h = [randint(1, 2**128) for i in ":("]


R1, S1 = gen_coprime_num(p.bit_length())
R2, S2 = gen_coprime_num(p.bit_length())

B = [[randint(1, p - 1) for i in ":("] for j in ":)"]

P = []
for b in B:
P.append(mult_mod(f, b, p))

Q = []
for b in B:
Q.append(mult_mod(h, b, p))

for i in range(len(P)):
for j in range(len(P[i])):
P[i][j] = P[i][j] * R1 % S1
Q[i][j] = Q[i][j] * R2 % S2

sk = [(R1, S1), (R2, S2), f, h, p]
pk = [P, Q, p]

return sk, pk

def encrypt(pk, pt):
P, Q, p = pk
pt = bytes_to_long(pt)

PP = 0
QQ = 0

for i in range(len(P)):
u = randint(1, p)
for j in range(len(P[0])):
PP = PP + P[i][j] * (u * pt**j % p)
QQ = QQ + Q[i][j] * (u * pt**j % p)

return PP, QQ

def decrypt(sk, ct):
RS1, RS2, f, h, p = sk
R1, S1 = RS1
R2, S2 = RS2

P, Q = ct
invR1 = inverse(R1, S1)
invR2 = inverse(R2, S2)
P = (P * invR1 % S1) % p
Q = (Q * invR2 % S2) % p

f0q = f[0] * Q % p
f1q = f[1] * Q % p
h0p = h[0] * P % p
h1p = h[1] * P % p

a = f1q + p - h1p % p
b = f0q + p - h0p % p

pt = -b * inverse(a, p) % p
pt = long_to_bytes(pt)

return pt

signal.alarm(30)
p = getPrime(256)
sk, pk = gen_key(p)
ticket = long_to_bytes(randint(1, p))
enc_ticket = encrypt(pk, ticket)
print(pk)
print(enc_ticket)

for i in range(2):
op = int(input("op>").strip())
if op == 1:
msg = input("pt:").strip().encode()
ct = encrypt(pk, msg)
print(f"ct: {ct}")
elif op == 2:
user_input = input("ct:").strip().split(" ")
if len(user_input) == 2:
ct = [int(user_input[0]), int(user_input[1])]
else:
print("invalid ct.")
break


user_input = input("your f:").strip().split(" ")
if len(user_input) == 2:
user_f = [int(user_input[0]), int(user_input[1])]
else:
print("invalid f.")
break

user_input = input("your h:").strip().split(" ")
if len(user_input) == 2:
user_h = [int(user_input[0]), int(user_input[1])]
else:
print("invalid h.")
break

user_input = input("your R1 S1:").strip().split(" ")
if len(user_input) == 2:
user_r1s1 = [int(user_input[0]), int(user_input[1])]
else:
print("invalid R1 S1.")
break

user_input = input("your R2 S2:").strip().split(" ")
if len(user_input) == 2:
user_r2s2 = [int(user_input[0]), int(user_input[1])]
else:
print("invalid R2 S2.")
break

pt = decrypt((user_r1s1, user_r2s2, user_f, user_h, p), ct)
if pt == ticket:
print(flag)
else:
print(pt.hex())
else:
print("invalid op.")
break

print("bye!")

这里我们直接看到加密过程

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
def encrypt(pk, pt):

P, Q, p = pk

pt = bytes_to_long(pt)

PP = 0

QQ = 0



for i in range(len(P)):

u = randint(1, p)

for j in range(len(P[0])):

PP = PP + P[i][j] * (u * pt**j % p)

QQ = QQ + Q[i][j] * (u * pt**j % p)



return PP, QQ

我们看到这里给出的等式

1
2
3
4
5
PP = P[0][0]·u₁ + P[0][1]·(u₁·pt) + P[0][2]·(u₁·pt²)
+ P[1][0]·u₂ + P[1][1]·(u₂·pt) + P[1][2]·(u₂·pt²)

QQ = Q[0][0]·u₁ + Q[0][1]·(u₁·pt) + Q[0][2]·(u₁·pt²)
+ Q[1][0]·u₂ + Q[1][1]·(u₂·pt) + Q[1][2]·(u₂·pt²)

这里不妨将后面的u * pt**j % p看作一个整体

1
2
3
4
5
6
x1 = u1
x2 = u1 * pt (mod p)
x3 = u1 * pt^2 (mod p)
x4 = u2
x5 = u2 * pt (mod p)
x6 = u2 * pt^2 (mod p)

那么就会得到

1
2
3
4
5
PP = P[0][0]·x₁ + P[0][1]·x₂ + P[0][2]·x₃  
+ P[1][0]·x₄ + P[1][1]·x₅ + P[1][2]·x₆

QQ = Q[0][0]·x₁ + Q[0][1]·x₂ + Q[0][2]·x₃
+ Q[1][0]·x₄ + Q[1][1]·x₅ + Q[1][2]·x₆

看到这里就有两种做法了一个是格来打,一个是Coppersmith

这里新学会的CUSO可以说太有帮助了,最后只需要走一遍流程对服务器传入相应的参数即可

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
from sage.all import *
from Crypto.Util.number import *
from cuso import find_small_roots
from pwn import process, remote
from chal import gen_key, encrypt, decrypt

pt_2_u1, pt_2_u2, pt_u1, pt_u2, u1, u2 = var('pt_2_u1, pt_2_u2, pt_u1, pt_u2, u1, u2')

W = [pt_2_u1, pt_2_u2, pt_u1, pt_u2, u1, u2]

PP = 0
QQ = 0

PP = P[0][0] * u1 + P[0][1] * pt_u1 + P[0][2] * pt_2_u1 + P[1][0] * u2 + P[1][1] * pt_u2 + P[1][2] * pt_2_u2 - Pc
QQ = Q[0][0] * u1 + Q[0][1] * pt_u1 + Q[0][2] * pt_2_u1 + Q[1][0] * u2 + Q[1][1] * pt_u2 + Q[1][2] * pt_2_u2 - Qc

bounds = {}
for w in W:
bounds[w] = (0, p)

roots = find_small_roots([PP, QQ], bounds)

for root in roots:
u1_, u2_, pt_u1_, pt_u2_ = root[u1], root[u2], root[pt_u1], root[pt_u2]
pt1 = pt_u1_ * inverse(u1_, p) % p
pt2 = pt_u2_ * inverse(u2_, p) % p
if pt1 == pt2:
pt = pt1
break

print(pt)

ticket = long_to_bytes(pt)
sk, pk = gen_key(p)
(R1, S1), (R2, S2), f, h, p = sk
new_ticket = encrypt(pk, ticket)
assert decrypt(sk, new_ticket) == ticket

r.sendlineafter(b'op>', b'2')
r.sendlineafter(b'ct:', str(new_ticket[0]) + ' ' + str(new_ticket[1]))
r.sendlineafter(b'your f:', str(f[0]) + ' ' + str(f[1]))
r.sendlineafter(b'your h:', str(h[0]) + ' ' + str(h[1]))
r.sendlineafter(b'your R1 S1:', str(R1) + ' ' + str(S1))
r.sendlineafter(b'your R2 S2:', str(R2) + ' ' + str(S2))

r.interactive()