Francesco Abate - Computer engineer

Home    CTFs  Misc  Rocketry  About  Contacts       

Welcome to my website! Here you can find CTF writeups, embedded projects, desktop and web applications, tutorials and much more.

View on GitHub
12 April 2025

Endless Emails

by Francesco Abate

Endless Emails is an RSA challenge from CryptoHack

🗳️ CTF Writeup: Endless Emails

CTF Challenge Analysis: RSA Low Exponent Broadcast

🔍 Challenge Overview

You’re given access to:

🧠 Goal

This document analyzes the provided files (johan.py and output.txt) from the cryptography CTF challenge. The goal is to understand the setup and identify the intended vulnerability and solution path.


📄 Challenge Files

johan.py

This Python script generated the challenge data found in output.txt.

#!/usr/bin/env python3

from Crypto.Util.number import bytes_to_long, getPrime # Imports functions for number theory: converting bytes to integers and generating prime numbers.
from secret import messages # Imports a variable 'messages' from a file named secret.py. This likely contains the flag or plaintext messages to be encrypted.


def RSA_encrypt(message):
    m = bytes_to_long(message) # Converts the input message (bytes) into a large integer 'm'.
    p = getPrime(1024) # Generates a 1024-bit prime number 'p'.
    q = getPrime(1024) # Generates another 1024-bit prime number 'q'.
    N = p * q # Calculates the RSA modulus 'N' by multiplying p and q. N will be approximately 2048 bits long.
    e = 3 # Sets the public exponent 'e' to 3. This is a small, common value, often used in CTFs.
    c = pow(m, e, N) # Performs the RSA encryption: c = m^e mod N.
    return N, e, c # Returns the modulus N, public exponent e, and the ciphertext c.


for m in messages: # Loops through each message in the 'messages' list/tuple imported from secret.py.
    N, e, c = RSA_encrypt(m) # Encrypts the current message 'm'. Crucially, a *new* pair of primes (p, q) and thus a *new* modulus N are generated for *each* message.
    print(f"n = {N}") # Prints the generated modulus N.
    print(f"e = {e}") # Prints the public exponent e (which is always 3).
    print(f"c = {c}") # Prints the resulting ciphertext c.

Key Observations:

output.txt

The output.txt file contains the RSA modulus (n), public exponent (e), and ciphertext (c) for each message. The format is as follows:

n = 22266616657574989868109324252160663470925207690694094953312891282341426880506924648525181014287214350136557941201445475540830225059514652125310445352175047408966028497316806142156338927162621004774769949534239479839334209147097793526879762417526445739552772039876568156469224491682030314994880247983332964121759307658270083947005466578077153185206199759569902810832114058818478518470715726064960617482910172035743003538122402440142861494899725720505181663738931151677884218457824676140190841393217857683627886497104915390385283364971133316672332846071665082777884028170668140862010444247560019193505999704028222347577
e = 3
c = 6965891612987861726975066977377253961837139691220763821370036576350605576485706330714192837336331493653283305241193883593410988132245791554283874785871849223291134571366093850082919285063130119121338290718389659761443563666214229749009468327825320914097376664888912663806925746474243439550004354390822079954583102082178617110721589392875875474288168921403550415531707419931040583019529612270482482718035497554779733578411057633524971870399893851589345476307695799567919550426417015815455141863703835142223300228230547255523815097431420381177861163863791690147876158039619438793849367921927840731088518955045807722225

Key Observations:

Solution


The core steps of the strategy are:

  # Import necessary libraries
import gmpy2 as gmpy # Used for high-precision integer arithmetic, especially the integer root function
from Cryptodome.Util import number # Used for number theoretic functions like modular inverse and byte/long conversion
from itertools import combinations # Used to generate combinations of (n, c) pairs

# Function to load n and c values from the output file
def load_output():
    # Initialize a dictionary to store lists of 'n' and 'c' values
    ret = {'n':[], 'c':[]}
    # Open the specified output file in binary read mode
    # NOTE: Ensure the filename "output_0ef6d6343784e59e2f44f61d2d29896f.txt" matches your actual file.
    with open("output_0ef6d6343784e59e2f44f61d2d29896f.txt", 'rb') as fd:
        # Read the file line by line
        while True:
            line = fd.readline()
            # Break the loop if end of file is reached
            if not line: break
            # Remove leading/trailing whitespace and decode from bytes to string
            line = line.strip().decode()
            # Skip empty lines
            if not line: continue
            
            # Split the line into key ('n', 'e', or 'c') and value parts based on '='
            k, v = line.split('=')
            # Remove whitespace from the key
            k = k.strip()
            # Skip the line if the key is 'e' (we already know e=3)
            if k == 'e':
                continue
            # Convert the value part to an integer and append it to the corresponding list in the dictionary
            ret[k].append(int(v))

    # Return the dictionary containing the lists of n and c values
    return ret

# Function to perform Hastad's attack
def decrypt(grps, e):
    # Use itertools.combinations to iterate through all unique combinations of size 'e' (which is 3)
    # from the available (n, c) pairs. zip(grps['n'], grps['c']) creates pairs like (n1, c1), (n2, c2), ...
    for grp in combinations(zip(grps['n'], grps['c']), e):
        # grp will be a tuple like ((n1, c1), (n2, c2), (n3, c3)) for e=3

        # Calculate the product of the moduli in the current group
        N = 1
        for x in grp: N *= x[0] # N = n1 * n2 * n3

        # Apply the Chinese Remainder Theorem (CRT) to find M such that M = c_i mod n_i for all i
        # M = sum( c_i * N/n_i * inverse(N/n_i, n_i) ) mod N
        M = 0
        for x in grp:
            # x[0] is n_i, x[1] is c_i
            term = N // x[0] # This is N/n_i
            # Calculate modular inverse: inverse(N/n_i, n_i)
            inv = number.inverse(term, x[0])
            # Add the component for this congruence to the total sum M
            M += x[1] * inv * term
        
        # Ensure M is within the range [0, N-1]
        M %= N

        # Calculate the integer e-th root (cube root since e=3) of M using gmpy2
        # gmpy.mpz(M) converts M to a gmpy2 multi-precision integer for efficiency/correctness
        # gmpy.iroot returns the integer root 'm' and a boolean 'exact'
        # 'exact' is True if M is a perfect e-th power, False otherwise
        m, exact = gmpy.iroot(gmpy.mpz(M), e)

        # Check if M was a perfect e-th power
        if exact:
            # If it was, 'm' is our original message integer m = M^(1/e)
            # Convert the integer 'm' back to bytes
            flag = number.long_to_bytes(m)
            # Print the recovered flag
            print(flag)


# --- Main execution ---
# Load the n and c values from the file
grps = load_output()
# Call the decrypt function with the loaded groups and the known public exponent e=3
decrypt(grps, 3)

Eventually you obtain the flag :

 crypto{1f_y0u_d0nt_p4d_y0u_4r3_Vuln3rabl3}
tags: CTF - cryptography - Cryptohack - Cybersecurity