28th 2022 / Document No. D22.102.16
Prepared By: aris
Challenge Author(s): aris
Difficulty: Easy
Classification: Official
- This challenges introduces the player to the concept of Legendre Symbol. The task is to test whether the encrypted values
$g^r$ are quadratic residues or not. If it is a quadratic residue then we know that the corresponding flag bit is$0$ , otherwise it is$1$ .
- The exam season is coming up, and you have to study the encryption used in malwares. The class structure involves the professors providing you with an encryption function, and your task is to find a way to decrypt the data without knowing the key. Practicing this will lead you to becoming proficient in cryptography, making data recovery by humans nearly impossible.
- Basic knowledge of SageMath.
- Know how to check whether a prime or a generator can make DLP easier to solve.
- Basic number theory knowledge.
- Learn about the Legendre symbol and quadratic residues.
In this challenge we are provided with two files:
source.py
: This is the main script that encrypts the flag and writes the data to the output file.output.txt
: This is the output file that contains the data that we have to use to solve this challenge.
The flow of the main function is very easy to follow:
def main():
flag = int.from_bytes(FLAG, 'big')
encrypted_flag = encrypt(flag)
with open('output.txt', 'w') as f:
f.write(str(encrypted_flag))
The function encrypt
is called that encrypts the flag and then the ciphertext is outputted to output.txt
. Let us take a closer look at the encrypt
function.
p = 307163712384204009961137975465657319439
g = 1337
def encrypt(m):
bits = bin(m)[2:]
encrypted = []
for b in bits:
r = (randint(2, p) << 1) + int(b)
encrypted.append(pow(g, r, p))
return encrypted
The flow of the encrypt
function can be summarized as:
- The message is converted into bits.
- There is a loop iterating the bits of the message.
- In each iteration, a random number
$r$ is uniformly selected in the range$[2,\ p]$ . - The current bit of the message, say
$b$ , is appended in the end of$r$ , therefore making it an even number if$b = 0$ , else odd.
The encrypted bit is computed as the result of the modular exponentiation
As the flag is encrypted bit-by-bit, our goal is to recover the bits given the encrypted values and since
Let us first write a function that loads the encrypted values from output.txt
.
def load_values():
with open('output.txt') as f:
encrypted_flag = eval(f.read())
return encrypted_flag
First thing that comes to mind is solving the discrete logarithm problem in order to recover the unknown exponent
p = 307163712384204009961137975465657319439
print(factor(p-1))
2 * 5923 * 1478672857 * 17535819988380789524657429
We can see that the size of the factors do not enable us to apply the PH attack. Our last opportunity is to verify that
p = 307163712384204009961137975465657319439
g = 1337
F = GF(p)
print(F(g).multiplicative_order())
307163712384204009961137975465657319438
Sadly,
This challenge aims at introducing the player to the concept of the Legendre Symbol and its significance in asymmetric cryptography.
Let
-
$-1$ if$a$ is not a perfect square modulo$p$ . -
$1$ if$a$ is a perfect square modulo$p$ . -
$0$ if$a = 0 \pmod p$ .
Perfect square means that
Back to our challenge, remember that our task is to determine whether the exponent
If the result is
Let us write a function that checks whether a value is a quadratic residue or not.
def is_quadratic_residue(a, p):
return pow(a, (p-1)//2, p) == 1
By implementing the idea above, we can recover the flag bit-by-bit.
from Crypto.Util.number import long_to_bytes
def recover_flag_bits(encrypted_flag):
flag = ''
for e in encrypted_flag:
if is_quadratic_residue(e, p):
flag += '0'
else:
flag += '1'
flag = long_to_bytes(int(flag, 2))
return flag
A final summary of all that was said above:
- Notice that our task is to find the LSB of
$r$ ; in other words, determine whether$r$ is odd or even. - Inspect
$p$ and$g$ and test whethre solving DLP is feasible in a logical amount of time. - This inspection fails so we think of Legendre symbol.
- We can determine the flag bits one-by-one by checking whether the encrypted values are quadratic residues or not.
This recap can be represented by code with the pwn()
function:
def pwn():
encrypted_flag = load_values()
flag = recover_flag_bits(encrypted_flag)
print(flag)
if __name__ == '__main__':
pwn()