Tuần vừa rồi, mình có chơi giải Bauhinia CTF với team @phis1Ng_. Mình thấy các challenge Crypto được đánh giá cao, với lại nhạc cũng hay nữa :))), mình có làm được một ~~vài~~ challenge về Crypto nên muốn chia sẻ với mọi người
## grhkm's babyRSA
`chall.py`
```python
from math import gcd
from Crypto.Util.number import getPrime, getRandomNBitInteger, bytes_to_long
from secret import flag
lcm = lambda u, v: u*v//gcd(u, v)
bits = 1024
given = bits // 5
e_bits = bits // 12
mask = (1 << given) - 1
while True:
p = getPrime(bits // 2)
q = getPrime(bits // 2)
N = p * q
if N.bit_length() != bits:
continue
l = lcm(p - 1, q - 1)
e = getRandomNBitInteger(e_bits)
if gcd(e, l) > 1:
continue
d = pow(e, -1, l)
dp = int(d % (p - 1))
dq = int(d % (q - 1))
break
l_dp = dp & mask
l_dq = dq & mask
print(f'{N = }')
print(f'{e = }')
print(f'{l_dp = }')
print(f'{l_dq = }')
flag = bytes_to_long(flag)
ct = pow(flag, e, N)
print(f'{ct = }')
```
`output.txt`
```
N = 96446191626393604009054111437713980755082681332020571709789032122186639773874753631630024642568257679734714430483780317122960230235124140242511126339536047435591010087751700582288534654352742251068909342986464462021206713195415006300821397979265537607226612724482984235104418995222711966835565604156795231519
e = 21859725745573183363159471
l_dp = 5170537512721293911585823686902506016823042591640808668431139
l_dq = 2408746727412251844978232811750068549680507130361329347219033
ct = 22853109242583772933543238072263595310890230858387007784810842667331395014960179858797539466440641309211418058958036988227478000761691182791858340813236991362094115499207490244816520864518250964829219489326391061014660200164748055767774506872271950966288147838511905213624426774660425957155313284952800718636
```
Đây là một bài liên quan về RSA-CRT, challenge cho ta LSB của `dp` và `dq` và mục tiêu của ta sẽ là phân tích `n`. Nếu như các bạn không hiểu mục đích của `dp` và `dq` là gì thì có thể đọc qua về [RSA-CRT](https://en.wikipedia.org/wiki/RSA_(cryptosystem)#Using_the_Chinese_remainder_algorithm)
Bằng một chút osint, mình đã tìm ra một paper liên quan đến bài này: [Factoring with Only a Third of the Secret CRT-Exponents](https://eprint.iacr.org/2022/271.pdf). Mình sẽ giải thích chi tiết hơn về paper này
Ta có các equation sau: $$\begin{align} dp &= h_{dp} * 2^{given} + l_{dp} \\ dq &= h_{dq} * 2^{given} + l_{dp} \end{align} \\ e * dp = k * (p - 1) + 1 \\ e * dq = l * (q - 1) + 1$$
Với $k, l, h_{dp}, h_{dq}$ là các ẩn mà ta chưa biết
Đọc phần 3.2, ta có thể xây dựng được một đa thức 2 biến có nghiệm là $(k, l)$
$$\begin{align} A &= e * (l_{dp} + l_{dq}) - e^2 * l_{dp} * l_{dq} - 1 \\ f(x, y) &= (N-1)*x*y - (e*l_{dq}-1)*x - (e*l_{dp}-1)*y + A \ (\ mod \ e * 2^i) \end{align}$$
Với chú ý rằng 2 giá trị $k, l < e$, ta sẽ tìm nghiệm của đa thức này bằng Coppersmith. Các bạn có thể kiếm python script của Coppersmith ở trên mạng khá nhiều. Ở đây, mình sử dụng code của [Defund](https://github.com/defund/coppersmith/blob/master/coppersmith.sage)
`find_k_l.sage`
```python
import itertools
import sys
bits = 1024
given = bits // 5
e_bits = bits // 12
mask = (1 << given) - 1
N = ..
e = ..
l_dp = ..
l_dq = ..
ct = ..
A = e*(l_dp + l_dq) - e**2 * l_dp * l_dq - 1
PR.<x, y> = PolynomialRing(Zmod(e * 2**given), 2)
f = (N-1)*x*y - (e*l_dq-1)*x - (e*l_dp-1)*y + A
def small_roots(f, bounds, m=1, d=None):
if not d:
d = f.degree()
R = f.base_ring()
N = R.cardinality()
#f /= f.coefficients().pop(0)
f = f.change_ring(ZZ)
G = Sequence([], f.parent())
for i in range(m+1):
base = N^(m-i) * f^i
for shifts in itertools.product(range(d), repeat=f.nvariables()):
g = base * prod(map(power, f.variables(), shifts))
G.append(g)
B, monomials = G.coefficient_matrix()
monomials = vector(monomials)
factors = [monomial(*bounds) for monomial in monomials]
for i, factor in enumerate(factors):
B.rescale_col(i, factor)
B = B.dense_matrix().LLL()
B = B.change_ring(QQ)
for i, factor in enumerate(factors):
B.rescale_col(i, 1/factor)
H = Sequence([], f.parent().change_ring(QQ))
for h in filter(None, B*monomials):
H.append(h)
I = H.ideal()
if I.dimension() == -1:
H.pop()
elif I.dimension() == 0:
roots = []
for root in I.variety(ring=ZZ):
root = tuple(R(root[var]) for var in f.variables())
roots.append(root)
return roots
return []
k, l = small_roots(f, (e,e), m=3, d=4)[0]
print(k, l)
```
Ta sẽ tìm được $(k, l) = (12177905682444242771542873, 4277124735150641724212759)$
Theo phần 3.3 của paper, sau khi đã có $k, l$, ta có thể xây dựng một đa thức $g$ trên $Z_N$ có nghiệm là $h_{dp}$:
$$\begin{align} a &= (e*l_{dp} + k - 1) * (e*2^{-i} \mod k*N) \\ g(x) &= x + a \end{align}$$
Ta tiếp tục dùng Coppersmith để tìm nghiệm của $g$, tuy nhiên, không hiểu sao script ở trên không tìm được nghiệm của $g$
Vì thế nên mình đã đọc kĩ hơn paper, và tìm được [script](https://github.com/juliannowakowski/crtrsa-small-e-pke/blob/main/implementation_new_attack.sage) của tác giả cái paper này. Ở đây, tác giả có xây dựng hàm [Coppersmith mở rộng](http://hyperelliptic.org/tanja/teaching/crypto20/20200922-lll.pdf) và nó hiệu quả nên mình đã sử dụng nó với một chút hiệu chỉnh
`find_h_dp.sage`
```py
m_2 = 40 #Parameter for 2nd Lattice
t_2 = 20 #Parameter for 2nd Lattice
k = 12177905682444242771542873
l = 4277124735150641724212759
n = 512
dSize = 512
alpha = 85
Unknown_MSB = 512 - given
LSB_dp = Integer(l_dp)
TWO_POWER = 2^(dSize - Unknown_MSB)
R.<x>=QQ[]
f = (e*(TWO_POWER*x+LSB_dp)-1+k)
IN_k = (e*TWO_POWER).inverse_mod(k*N)
f = x+IN_k*(e*LSB_dp-1+k) # Make f monic by inverting the coefficient of x
X = 2^Unknown_MSB
#Generate shift polynomials and store these polynomials in F. Store monomials of shift polynomials in S
F = []
S = []
for i in range(m_2+1):
h = f^i*k^(m_2-i)*N^(max(0,t_2-i))
F.append(h)
S.append(x^i)
"""
Form a matrix MAT. Entries of MAT are coming from the coefficient
vector from shift polynomials which are stored in F
"""
print('2nd lattice dimension', len(F))
MAT = Matrix(ZZ, len(F))
for i in range(len(F)):
f = F[i]
f = f(x*X)
coeffs = (f.coefficients(sparse=False))
for j in range(len(coeffs), len(F)):
coeffs.append(0)
coeffs = vector(coeffs)
MAT[i] = coeffs
from time import process_time
TIME_Start = process_time()
tt = cputime()
MAT = MAT.LLL()
TIME_Stop = process_time()
print('2nd LLL time', TIME_Stop-TIME_Start)
#Get all polynomial
A = []
for j in range(len(F)):
f = 0
for i in range(len(S)):
cij = MAT[j,i]
cij = cij/S[i](X)
cj = ZZ(cij)
f = f + cj*S[i]
A.append(f)
else:
break
for f in A:
print(f.roots())
```
Ở đây, mình sẽ kiểm tra xem nghiệm $c$ của đa thức nào trong $A$ là $h_{dp}$ bằng cách tính $gcd(g(c), N) = d$. Nếu $d > 1$ thì đó chính là $p$ mà ta cần tìm
Ta tìm được $h_{dp} = 180951980763775058492815911873237082514145707000972099423553967985124001563643605807070492022$ qua đó tìm được $p = 8351309129105229708154695602362829027250608775685390771007529034429910339403472223935574054850623494610995086524639065024717100915125468017775773884252621$
Khi đã có $p$, mọi chuyện là đơn giản:
`solve.py`
```py
from Crypto.Util.number import *
ct = ..
N = ..
p = ..
q = N // p
e = ..
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
flag = pow(ct, d, N)
print(long_to_bytes(flag))
```
> flag: b6actf{y0u_mu5t_b3_c0nv1nc3d_th4t_lgn/5_is_gr34t3r_th4n_lgn/4_n0w}
## * How to stop time
`chall.py`
```python
# Since the script is simple, I will just add a lot of useless information to
# distract you all. Good luck identifying whether one is a red herring!
# Although I am using the "os" package, I don't call "os.system". Now give up on
# that wacky thoughts.
import os
# I am using the `random` package, which is known to be using MT19937, a
# reversable pseudorandom number generator. Maybe you can make use of them?
import random
# I sometime receive complaints from players regarding installing random Python
# packages all around. I will refrain from using third-party packages for this
# challenge. Hope that helps!
from mathy import is_prime
def main():
# Please do not submit "flag{this_is_a_fake_flag}" as the flag! This is only
# a placeholder and this is not the REAL flag for the challenge. Go nc to
# the actual server for the actual flag!
flag = os.environ.get('FLAG', 'flag{this_is_a_fake_flag}')
# "Once is happenstance. Twice is coincidence...
# Sixteen times is a recovery of the pseudorandom number generator."
# - "Maybe Someday" on Google CTF 2022
#
# But... How about 256 times? Prediction of pseudorandom number generator?
for _ in range(256):
# I will pregenerate those values ahead. Can you read that before
# sending q?
g = random.getrandbits(512)
x = random.getrandbits(512)
# Yeah, come send me a large-enough prime q!
q = int(input('[<] q = '))
# Told you I need a large-enough prime.
assert q.bit_length() == 512 and is_prime(q)
# I was going to set "p = 2*q + 1" to make p a safe prime... I will just
# changing that to "p = 4*q + 1" to pretend that there is a bug. Let's
# call that a... pseudo-safe prime?
p = 4*q + 1
assert is_prime(p)
print(f'[>] {g = }')
# I intentionally computes g^x mod p between printing g and h. Good luck
# unleashing a timing attack!
h = pow(g, x, p)
print(f'[>] {h = }')
# You have to recover me the "x". Quickly.
_x = int(input('[<] x = '))
assert x == _x
# How should I innotate this? Go grab the flag!
print(f'[*] {flag}')
if __name__ == '__main__':
try:
main()
except:
print('[!] Well played.')
```
`mathy.py`
```python
# Functions copied from "That-crete log" from UIUCTF 2022. Thanks!
def miller_rabin(bases, n):
# I don't know how to annotate this because it involves of a bunch of
# mathematics that I could not understand, but I still want to be verbose.
# Maybe I should link you the wiki page so you could read that...
# https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test
if n == 2 or n == 3:
return True
r, s = 0, n - 1
while s % 2 == 0:
r += 1
s //= 2
for b in bases:
x = pow(b, s, n)
if x == 1 or x == n-1:
continue
for _ in range(r - 1):
x = x * x % n
if x == n-1:
break
else:
return False
return True
def is_prime(n):
# bases = [2, 3, 5, 7, 11, 13, 17, 19, 31337] are used by the challenge from
# UIUCTF. That isn't good enough...
# I learned from ICPC that those seven bases blocks every number below 2^64.
bases = [2, 325, 9375, 28178, 450775, 9780504, 1795265022]
# I will also add a bunch of random bases. Well, I really meant it. This is
# how I generate those bases:
# sorted([random.randint(2, 1000000)*2+1 for _ in range(200)])
bases += [
20669, 48929, 57021, 63569, 73307, 86815, 93495, 101303,
124851, 126617, 164415, 171811, 184653, 219385, 221067, 223499,
229897, 234477, 251893, 264151, 295599, 299453, 308525, 316135,
318467, 319081, 326169, 341721, 343699, 351743, 374223, 378375,
387703, 387807, 390443, 417763, 430031, 438233, 440079, 441259,
444591, 465613, 475205, 485841, 501341, 509761, 515577, 528775,
533381, 536401, 558123, 562419, 583397, 606965, 617121, 619821,
625787, 632805, 650751, 689307, 695181, 695725, 704809, 706557,
720371, 729335, 737269, 741827, 743969, 745609, 750425, 764843,
768725, 782945, 789713, 794851, 832829, 849477, 849917, 872481,
880381, 880601, 882991, 891339, 892581, 897917, 900497, 902791,
907839, 908069, 910733, 936747, 945849, 952533, 965837, 967739,
1007573, 1018197, 1022845, 1027277, 1027963, 1044711, 1050091, 1050839,
1053395, 1060643, 1070551, 1080385, 1087593, 1095565, 1111439, 1141847,
1146745, 1168487, 1176229, 1180219, 1187279, 1203567, 1204739, 1207205,
1212905, 1233043, 1252625, 1256889, 1272399, 1298475, 1302085, 1305033,
1309991, 1325833, 1334399, 1340793, 1355737, 1365593, 1376389, 1381963,
1390677, 1405539, 1421269, 1426487, 1433469, 1448275, 1458545, 1462879,
1464553, 1482773, 1486655, 1504839, 1512277, 1517895, 1526807, 1532327,
1543995, 1545351, 1553127, 1563397, 1572205, 1573891, 1583443, 1595567,
1603263, 1609551, 1631223, 1633943, 1650589, 1677741, 1681935, 1696649,
1713355, 1715365, 1730819, 1741045, 1745279, 1751007, 1758715, 1778157,
1779521, 1785051, 1789451, 1789671, 1790781, 1791763, 1812959, 1823427,
1824907, 1842549, 1846559, 1847019, 1865431, 1879215, 1895455, 1930981,
1932295, 1940509, 1957911, 1976957, 1986973, 1992813, 1993333, 1994939
]
# We should be able to find all composite numbers smaller than 65536 with
# this sieve. Well, we don't do Miller-Rabin for every numbers; or it will
# be too time-consuming.
for i in range(2, min(256, n)):
if n % i == 0:
return False
# Although I don't know why they used 256 (instead of 65536) here, I will
# just stick to that.
if n < 256:
return True
# Now we use Miller-Rabin for the large numbers.
return miller_rabin(bases, n)
```
Trong bài này, ta được yêu cầu phải tính $log_g(h) \mod p$ 256 lần liên tiếp. Ta được phép nhập số $q$ sao cho `q.bit_length() = 512`, `is_prime(q)` và `is_prime(4*q - 1)`
Trong lúc giải đang diễn ra thì mình không giải được câu này :((. Với việc tác giả có nhắc đến một challenge trong `UIUCTF 2022` nên mình đã làm theo và bị mắc kẹt trong suốt giải
Sau giải, mình mới để ý dòng này ở trong file `mathy.py`
```python
# Although I don't know why they used 256 (instead of 65536) here, I will
# just stick to that.
if n < 256:
return True
```
và:
```python
# Yeah, come send me a large-enough prime q!
q = int(input('[<] q = '))
# Told you I need a large-enough prime.
assert q.bit_length() == 512 and is_prime(q)
```
trong file `chall.py`
Ta có thể bypass hàm `is_prime()` bằng cách chọn ra số **nguyên** `x < 256`. Server không hề check xem số ta gửi có phải là một số âm không, vì thế ta hoàn toàn có thể chọn một số âm $p = 1 \mod 4$ và hàm `is_prime()` sẽ luôn trả về True. Từ đó, ta có thể tính $q = (p - 1)/4$
Để tính dlog cho đơn giản, ta nên chọn $p$ sao cho $p - 1$ có thể phân tích thành các số nguyên tố nhỏ hơn, và hàm `.log()` trong sage sẽ tính được `x` rất nhanh
Vì mình không làm được trong giải, với lại vừa xong giải thì tác giả gửi writeup luôn nên chắc là mình sẽ lấy [code của tác giả](https://github.com/blackb6a/blackb6a-ctf-2023-challenges/blob/main/01-how-to-stop-time/src/solve.sage) vậy:D
Trong code, tác giả cũng có giải thích cách tìm $p$ nên nếu không hiểu các bạn có thể đọc qua
`solve.sage`
```python
from pwn import *
from tqdm import trange
# context.log_level = 'debug'
# r = process(['python3', 'chall.py'])
r = remote('localhost', 28101)
for i in trange(256):
log.info(f'=== Round {i+1}/256 ===')
p = -97523^31
assert p % 4 == 1
q = (p - 1) // 4
r.sendlineafter(b'[<] q = ', str(q).encode())
r.recvuntil(b'[>] g = ')
g = int(r.recvline().decode())
r.recvuntil(b'[>] h = ')
h = int(r.recvline().decode())
# Note: p is not a prime!
Zp = Zmod(p)
g, h = Zp(g), Zp(h)
log.info(f'order(g) = {g.multiplicative_order()}')
log.info(f'2**512 = {2**512}')
log.info(f'{g = }')
log.info(f'{h = }')
x = h.log(g)
log.info(f'{x = }')
r.sendlineafter(b'[<] x = ', str(x).encode())
r.interactive()
```
> flag: b6actf{h0w_c4n_y0u_c0mpu73_d109s_s00ooo_qu1ckly}