Skip to main content
  1. ctf-writeups/

E CTF 2025

·6 mins
E CTF
  • Welcome to my write-up for the E CTF 2025! In this post, I’ll walk through the challenges I tackled, sharing insights into my approach, analysis, and how I cracked the flags. Let’s dive straight into the action!

Challenge 1: Cracking the Vault: #

E CTF

Unpacking the Challenge #

E CTF
  • I downloaded the provided zipped file, unzipped it and got two files: Encryption.py and VaultKey_encrypted.txt.
  • VaultKey_encrypted.txt contained the ciphertext.
  • Encryption.py was a Python script with the code to encrypt the plaintext.

The Encryption Script #

import secrets
import hashlib
def encryption(text):
encrypted = []
random = secrets.SystemRandom()
padding_length = 256 - len(text) % 256
raw_padding = [chr(random.randint(32, 126)) for _ in range(padding_length)]
scrambled_padding = [chr((ord(c) * 3 + 7) % 94 + 32) for c in raw_padding]
shifted_padding = scrambled_padding[::-1]
padded_text = ''.join(shifted_padding) + text
final_padded_text = ''.join(
chr((ord(c) ^ 42) % 94 + 32) if i % 2 == 0 else c
for i, c in enumerate(padded_text)
)
secret_key = str(sum(ord(c) for c in text))
secret_key = secret_key[::-1]
hashed_key = hashlib.sha256(secret_key.encode()).hexdigest()
seed = int(hashed_key[:16], 16)
random = secrets.SystemRandom(seed)
for i, char in enumerate(text):
char_code = ord(char)
shift = (i + 1) * 3
transformed = (char_code + shift + 67) % 256
encrypted.append(chr(transformed))
return ''.join(encrypted), seed
with open('VaultKey.txt', 'r') as f:
text = f.read()
encrypted_text, seed = encryption(text)
with open('VaultKey_encrypted.txt', 'w') as f:
f.write(encrypted_text)
print("The file has been successfully encrypted!")
view raw Encryption.py hosted with ❤ by GitHub
  • The code was heavily obfuscated! So let’s dive into the deobfuscation.

Deobfuscating the Encryption Code #

1. Unused XOR Transformation #

  • One of the first things I noticed was this XOR-based transformation on line 15:
final_padded_text = ''.join(
    chr((ord(c) ^ 42) % 94 + 32) if i % 2 == 0 else c
    for i, c in enumerate(padded_text)
)
  • At first, this looked like an attempt to alter the text further using XOR. But after tracing the variables, I realized this transformed text string (final_padded_text) was never actually used anywhere.
Note: This doesn't change the functionality of the encryption process, it will run just fine but it just makes it a lot harder to reverse engineer the code.

Red flag: It was simply a distraction.

2. Unused Padding and Scrambling #

  • Next, I saw a weird section adding padding before encryption: line 7 to line 13
padding_length = 256 - len(text) % 256
raw_padding = [chr(random.randint(32, 126)) for _ in range(padding_length)]

scrambled_padding = [chr((ord(c) * 3 + 7) % 94 + 32) for c in raw_padding]
shifted_padding = scrambled_padding[::-1]

padded_text = ''.join(shifted_padding) + text
  • This made it look like the encryption relied on some kind of structured padding. But after carefully following the code, I realized this padding was never used in the actual encryption process.

Red flag: More junk code to make things appear more complicated.

3. Generating Unused Random Numbers #

  • Another major distraction was line 20 - 25 of the code.
    secret_key = str(sum(ord(c) for c in text))
    secret_key = secret_key[::-1]

    hashed_key = hashlib.sha256(secret_key.encode()).hexdigest()

    seed = int(hashed_key[:16], 16)

    random = secrets.SystemRandom(seed)
  • The above code creates a secure random number generator (random) that is deterministically based on the input text. The process involves:

  • Summing the Unicode values of the characters in text.

  • Reversing and hashing the resulting sum.

  • Using the first 16 characters of the hash as a seed for the secure RNG.

  • Generating random numbers based on the seed

Red flag: Complete misdirection though. Just there to waste time.

Actual Encryption Logic #

  • Once I eliminated all the unnecessary parts of the code, what was left was surprisingly simple.
    def encryption(text):
    encrypted = []
    for i, char in enumerate(text):
    char_code = ord(char)
    shift = (i + 1) * 3
    transformed = (char_code + shift + 67) % 256
    encrypted.append(chr(transformed))
    return ''.join(encrypted)
  • This was the core encryption logic that was actually responsible for transforming the text. Let’s break it down:

1. Character Conversion: Each character of the text is converted to its ASCII code using ord(char).

2. Shifting: For each character, the code applies a shift that increases with the character’s position. Specifically, it multiplies the character’s index (i + 1) by 3, which means the shift increases as we move through the string. This shifting helps obscure the original text.

  • Example: The first character gets a shift of 3 ((1 + 1) * 3), the second character gets a shift of 6 ((2 + 1) * 3), and so on.

3. Transformation: After applying the shift, 67 is added to the result. Then, the modulo 256 operation ensures that the result is within the valid range for ASCII characters. The chr() function is used to convert the result back to a character.

This transformation essentially scrambles the original text based on the position of each character, but with a predictable pattern.

Reversing the Encryption #

  • To decrypt the encrypted text and retrieve the original data, we need to reverse the steps in the encryption function.
# decryption function to reverse the encryption
def decryption(encrypted_text):
decrypted = []
for i, char in enumerate(encrypted_text):
char_code = ord(char)
shift = (i + 1) * 3
original_code = (char_code - shift - 67) % 256
decrypted.append(chr(original_code))
return ''.join(decrypted)
view raw Decryption.py hosted with ❤ by GitHub
  • For each character in the encrypted text:

1. Reverse the Shift: The shift is calculated the same way as in encryption, so we subtract the same amount to reverse it.

2. Subtract the Constant: The 67 that was added during encryption is subtracted here.

3. Apply Modulo 256: To ensure we get the correct ASCII value, we use modulo 256.

  • We return the decrypted text
# Read the encrypted text
with open('VaultKey_encrypted.txt', 'r', encoding='utf-8') as f:
encrypted_text = f.read()
# Decrypt the text
decrypted_text = decryption(encrypted_text)
# Write the decrypted text to a new file
with open('VaultKey_decrypted.txt', 'w') as f:
f.write(decrypted_text)
print("The file has been successfully decrypted!")
view raw decrypt.py hosted with ❤ by GitHub

1. Read the encrypted text: We read the entire content of the VaultKey_encrypted.txt and store it in the variable encrypted_text.

2. Decrypting the Encrypted Text: After loading the encrypted content, we pass it through the decryption function that we previously wrote. This function takes the encrypted text, applies the reverse operations, and returns the original plain text.

3. Writing the Decrypted Text to a New File: Once we have the decrypted text, the next step is to save it into a new file, VaultKey_decrypted.txt, so we can review the original data.

4. Finally, we print a message to confirm that the decryption process was successful.

Final Decryption Script #

# decryption function to reverse the encryption
def decryption(encrypted_text):
decrypted = []
for i, char in enumerate(encrypted_text):
char_code = ord(char)
shift = (i + 1) * 3
original_code = (char_code - shift - 67) % 256
decrypted.append(chr(original_code))
return ''.join(decrypted)
# Read the encrypted text
with open('VaultKey_encrypted.txt', 'r', encoding='utf-8') as f:
encrypted_text = f.read()
# Decrypt the text
decrypted_text = decryption(encrypted_text)
# Write the decrypted text to a new file
with open('VaultKey_decrypted.txt', 'w') as f:
f.write(decrypted_text)
print("The file has been successfully decrypted!")

Getting the Flag #

We run the final script we get the flag :)

  • E CTF
    Flag: Well done! I bet you're great at math. Here's your flag, buddy: ectf{1t_W45_ju5T_4_m1nu5}

  • That was indeed a piece of cake :)

Challenge 2: ASCII me anything but not the flag #

E CTF
  • The challenge dropped a pretty solid hint right off the bat: the text was ASCII-encoded.

Converting ASCII to Readable Text #

  • So, my first move was obvious – take those numbers and convert them into something readable.
108 100 111 109 123 85 99 49 122 95 106 53 95 79 111 51 95 88 52 116 95 48 109
95 51 111 88 121 90 107 97 106 48 105 125 10 10 69 98 111 98 32 102 112 32 118
108 114 111 32 104 98 118 44 32 100 108 108 97 32 105 114 122 104 32 58 32 72
66 86 72 66 86 10 10 87 101 108 108 32 100 111 110 101 44 32 98 117 116 32 110
111 119 32 100 111 32 121 111 117 32 107 110 111 119 32 97 98 111 117 116 32
116 104 101 32 103 117 121 32 119 104 111 32 103 111 116 32 115 116 97 98 98
101 100 32 50 51 32 116 105 109 101 115 32 63
view raw ascii.txt hosted with ❤ by GitHub
  • I plugged these into an ASCII-to-text converter, and here’s what I got:
ldom{Uc1z_j5_Oo3_X4t_0m_3oXyZkaj0i}
Ebob fp vlro hbv, dlla irzh : HBVHBV
Well done, but now do you know about the guy who got stabbed 23 times?
  • First off, we got the flag format: ldom{Uc1z_j5_Oo3_X4t_0m_3oXyZkaj0i}.
  • But that second part? Ebob fp vlro hbv, dlla irzh : HBVHBV… definitely some sort of cipher.
  • And the guy who got stabbed 23 times? That was a clue. Julius Caesar, anyone?

Brute-Forcing the Caesar Cipher #

  • The approach here was straightforward: brute force all the possible shifts until something readable came out.
A Caesar cipher works by shifting each letter in the message by a fixed number of positions in the alphabet.
  • Since there are 26 letters in the alphabet, that means there are only 25 possible shifts (ignoring a shift of 0, which would just be the original text).

  • I wrote up a quick Python script (but you can use CyberChef) to shift each letter through all 25 possibilities. The goal was to see which shift made sense in the context of the message.

def caesar_cipher(text, shift):
result = ''
for char in text:
if char.isalpha():
offset = 65 if char.isupper() else 97
result += chr((ord(char) - offset + shift) % 26 + offset)
else:
result += char
return result
ciphertext = "Ebob fp vlro hbv, dlla irzh : HBVHBV"
for shift in range(1, 26):
print(f"Shift {shift}: {caesar_cipher(ciphertext, shift)}")

After running it, I found that shift 3 gave me: Here is your key, good luck : KEYKEY

  • That gave me the key for the Vigenère cipher: KEYKEY

Decrypting the Vigenère Cipher #

  • With the key KEYKEY from the Caesar shift, it was time to decrypt the Vigenère cipher. The ciphertext to crack was: ldom{Uc1z_j5_Oo3_X4t_0m_3oXyZkaj0i}

  • I could have gone through the manual steps, but why bother when CyberChef has your back? It’s one of my go-to tools for decryption.

    E CTF

  • Hit the “Bake” button, I got this: bzqc{Qe1p_f5_Qe3_T4v_0c_3kZoVmqf0k}

  • The output was still scrambled, but it was definitely progress.

Applying the Caesar Cipher Again #

  • Since the Caesar cipher shift was a known factor, I decided to take that 3 shift and apply it again to tidy things up. You can use CyberChef
def caesar_cipher(text, shift):
result = ''
for char in text:
if char.isalpha():
offset = 65 if char.isupper() else 97
result += chr((ord(char) - offset + shift) % 26 + offset)
else:
result += char
return result
ciphertext = "bzqc{Qe1p_f5_Qe3_T4v_0c_3kZoVmqf0k}"
decrypted_text = caesar_cipher(ciphertext, 3)
print(decrypted_text)
  • Running the script and there we get our flag!:
    E CTF
  • Flag: ectf{Th1s_i5_Th3_W4y_0f_3nCrYpti0n}