Post

qrime - AlpacaHack Round 3 (Crypto)

qrime - AlpacaHack Round 3 (Crypto)

Flag : Alpaca{q_and_r_have_nothing_to_do_with_QR_code}

  • Difficulty: Easy/Medium

We got the Python source code used to encrypt 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
import os
from Crypto.Util.number import bytes_to_long, getRandomNBitInteger, isPrime

def nextPrime(n):
    while not isPrime(n := n + 1):
        continue
    return n

def gen():
    while True:
        q = getRandomNBitInteger(256)
        r = getRandomNBitInteger(256)
        p = q * nextPrime(r) + nextPrime(q) * r
        if isPrime(p) and isPrime(q):
            return p, q, r

flag = os.environ.get("FLAG", "fakeflag").encode()
m = bytes_to_long(flag)

p, q, r = gen()
n = p * q

phi = (p - 1) * (q - 1)
e = 0x10001
d = pow(e, -1, phi)
c = pow(m, e, n)

print(f"{n=}")
print(f"{e=}")
print(f"{c=}")
print(f"{r=}")

and we also have the output of the encryption:

1
2
3
4
n=200697881793620389197751143658858424075492240536004468937396825699483210280999214674828938407830171522000573896259413231953182108686782019862906633259090814783111593304404356927145683840948437835426703183742322171552269964159917779
e=65537
c=77163248552037496974551155836778067107086838375316358094323022740486805320709021643760063197513767812819672431278113945221579920669369599456818771428377647053211504958874209832487794913919451387978942636428157218259130156026601708
r=30736331670163278077316573297195977299089049174626053101058657011068283335270

Source code analysis

The code is generating an RSA key pair, with 2 primes $p$ and $q$, and encrypting the flag using the public exponent $e$ and the modulus $n$. The code shows the modulus $n$, the public exponent $e$, the $ciphertext$, and the $r$ value. $p$ is generated as $q * nextPrime(r) + nextPrime(q) * r$.

Solution 1

I have $r$ value, and I can calculate $next_r$ value, I saw the difference between $r$ and $nextPrime(r)$ is small and I think the diffrence between $q$ and $nextPrime(q)$ is small too. So, I noticed $next_q = q + k $, where $k$ is a small number.

\[\begin{cases} n = p *q \\ p = q * r\_next + (q + k) * r \\ n = q^2 * r\_next + q * (q + k) * r = q ^ 2 * (r\_next + r) + q * (k * r) \end{cases}\]

Let’s note polynomial $P(x) = x^2 * (r_next + r) + x * (k * r) - n$ and $P(q) = 0$ (for some small $k$)

$q = (-(k * r) ± \sqrt{((k * r) ** 2 + 4 * (r_next + r) * n))} / (2 * (r_next + r))$, but the $q$ is positive, so \(q = (-(k * r) + \sqrt{((k * r) ** 2 + 4 * (r\_next + r) * n))} / (2 * (r\_next + r))\)

Solve script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from Crypto.Util.number import long_to_bytes, inverse, isPrime
import math

n = 200697881793620389197751143658858424075492240536004468937396825699483210280999214674828938407830171522000573896259413231953182108686782019862906633259090814783111593304404356927145683840948437835426703183742322171552269964159917779
e = 65537
c = 77163248552037496974551155836778067107086838375316358094323022740486805320709021643760063197513767812819672431278113945221579920669369599456818771428377647053211504958874209832487794913919451387978942636428157218259130156026601708
r = 30736331670163278077316573297195977299089049174626053101058657011068283335270

def nextPrime(n):
    while not isPrime(n):
        n += 1
    return n

r_next = nextPrime(r)
print(f"nextPrime(r)={r_next}")

# p * q = n
# p = q * r_next + (q + k) * r

# n = q ** 2 * r_next + q * (q + k) * r
#   = q ** 2 * (r_next + r) + q * (k * r)

# P(x) = x ** 2 * (r_next + r) + x * (k * r) - n
# P(q) = 0 (for some small k)

# q = (-(k * r) ± √((k * r) ** 2 + 4 * (r_next + r) * n)) / (2 * (r_next + r))
# But q > 0 , it's a getRandomNBitInteger(256) 

k = 1
while True:
    q = (- (k * r) + math.isqrt((k * r) ** 2 + 4 * (r_next + r) * n)) // (2 * (r_next + r))
    if n % q == 0 and q > 1:
        break
    k = k + 1


print(f"q={q}")
p = n // q
print(f"p={p}")

phi = (p - 1) * (q - 1)
e = 0x10001
d = inverse(e, phi)
m = pow(c, d, n)
print(bytes.fromhex(hex(m)[2:]))

Solution 2

For second solution, I took another approach, I will use the binary search to find the $q$ value.

Solve script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from Crypto.Util.number import long_to_bytes, inverse, isPrime
import sympy

n = 200697881793620389197751143658858424075492240536004468937396825699483210280999214674828938407830171522000573896259413231953182108686782019862906633259090814783111593304404356927145683840948437835426703183742322171552269964159917779
e = 65537
c = 77163248552037496974551155836778067107086838375316358094323022740486805320709021643760063197513767812819672431278113945221579920669369599456818771428377647053211504958874209832487794913919451387978942636428157218259130156026601708
r = 30736331670163278077316573297195977299089049174626053101058657011068283335270


def bin_search(n: int, r: int) -> int:
    low = 0
    high = 1 << 256
    while low < high:
        mid = (low + high) // 2
        q = mid
        p: int = q * sympy.nextprime(r) + sympy.nextprime(q) * r
        if p * q >= n:
            high = mid
        else:
            low = mid + 1
    return mid


q = bin_search(n, r)
assert n % q == 0
p = n // q
# And from this is trivial to decrypt the flag
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
This post is licensed under CC BY 4.0 by the author.