Challenge Description

In this challenge, we will attempt to decode the contents of the given file that has been encrypted with a Python script.

We first begin by downloading the 2 files, enc_flag and custom_encryption.py, by running wget <link1> <link2>.

I proceeding to use cat to display the contents of enc_flag file:

a = 97
b = 22
cipher is: [151146, 1158786, 1276344, 1360314, 1427490, 1377108, 1074816, 1074816, 386262, 705348, 0, 1393902, 352674, 83970, 1141992, 0, 369468, 1444284, 16794, 1041228, 403056, 453438, 100764, 100764, 285498, 100764, 436644, 856494, 537408, 822906, 436644, 117558, 201528, 285498]

We see that there are 2 variables given, a and b. There’s also a list of seemingly random numbers.

To understand the contents of this file, we need to check the Python script provided, which I’ve included below as well:

from random import randint
import sys
 
 
def generator(g, x, p):
    return pow(g, x) % p
 
 
def encrypt(plaintext, key):
    cipher = []
    for char in plaintext:
        cipher.append(((ord(char) * key*311)))
    return cipher
 
 
def is_prime(p):
    v = 0
    for i in range(2, p + 1):
        if p % i == 0:
            v = v + 1
    if v > 1:
        return False
    else:
        return True
 
 
def dynamic_xor_encrypt(plaintext, text_key):
    cipher_text = ""
    key_length = len(text_key)
    for i, char in enumerate(plaintext[::-1]):
        key_char = text_key[i % key_length]
        encrypted_char = chr(ord(char) ^ ord(key_char))
        cipher_text += encrypted_char
    return cipher_text
 
 
def test(plain_text, text_key):
    p = 97
    g = 31
    if not is_prime(p) and not is_prime(g):
        print("Enter prime numbers")
        return
    a = randint(p-10, p)
    b = randint(g-10, g)
    print(f"a = {a}")
    print(f"b = {b}")
    u = generator(g, a, p)
    v = generator(g, b, p)
    key = generator(v, a, p)
    b_key = generator(u, b, p)
    shared_key = None
    if key == b_key:
        shared_key = key
    else:
        print("Invalid key")
        return
    semi_cipher = dynamic_xor_encrypt(plain_text, text_key)
    cipher = encrypt(semi_cipher, shared_key)
    print(f'cipher is: {cipher}')
 
 
if __name__ == "__main__":
    message = sys.argv[1]
    test(message, "trudeau")
 

There are a total of 5 functions used to encrypt the original flag. After reading the code, we will understand why the values of a and b were given. It’s because a and b are random values between 87 - 97 and 21 - 31 respectively.

We are also given the list of numbers named “cipher” as we will need to do some reverse-engineering using these values to find the original flag.

To decode the encrypted file contents of enc_flag, I wrote this Python script:

'''
p = 97
g = 31
 
a = 97 (random number from p-10 to p, both included)
b = 22 (random number from g-10 to g, both included)
 
u = g^a mod p
u = 31
 
v = g^b mod p
v = 54
 
key = v^a mod p
key = 54
 
b_key = u^b mod p
b_key = 54
 
So, shared_key = key = b_key = 54
 
semi_cipher = dynamic_xor_encrypt(flag, "trudeau")
cipher = encrypt(semi_cipher, shared_key)
'''
cipher = [151146, 1158786, 1276344, 1360314, 1427490, 1377108, 1074816, 1074816, 386262, 705348, 0, 
          1393902, 352674, 83970, 1141992, 0, 369468, 1444284, 16794, 1041228, 403056, 453438, 100764, 
          100764, 285498, 100764, 436644, 856494, 537408, 822906, 436644, 117558, 201528, 285498]
 
def decrypt_semi_flag(key):
    semi_flag = ""
    for chr_cipher in cipher:
        x = chr(chr_cipher // (key*311))
        semi_flag += x
    return semi_flag
 
def dynamic_xor_decrypt(semi_flag, key): 
    flag = ""
    key_length = len(key)
    for i, char in enumerate(semi_flag):
        key_char = key[i % key_length]
        decrypted_char = chr(ord(char) ^ ord(key_char))
        flag += decrypted_char
    return flag
 
semi_flag = decrypt_semi_flag(54)
flag = dynamic_xor_decrypt(semi_flag, "trudeau")
print(flag[::-1]) # unreverse the flag

Running this script allows us to retrieve the flag.

Flag

picoCTF{custom_d2cr0pt6d_e4530597}