Type to search posts, pages, and more...

V1t Ctf 2025

Welcome to a comprehensive guide through five interesting cryptographic and steganographic challenges! Each one teaches a different technique for hiding or protecting information. Whether you’re new to CTFs or looking to expand your toolkit, these challenges showcase common patterns you’ll encounter in cybersecurity competitions.

Challenge 1: Whitespace Steganography - When Nothing is Something

Flag: v1t{1_c4nt_s33_4nyth1ng}

Ever opened a file that looked completely blank? Don’t be fooled - sometimes the most invisible data hiding techniques are right in front of your eyes. This challenge demonstrates whitespace steganography, where secret messages are encoded using only spaces and tabs.

The Mystery of the “Empty” File

We’re given a file called txt that appears completely empty when opened in any normal text editor. The challenge title and description were also empty, which is actually a huge hint! When you see “nothing,” think whitespace steganography.

The key insight is that while the file looks empty, it actually contains invisible characters:

  • Spaces (0x20)
  • Tabs (0x09)
  • Carriage returns (0x0D)
  • Line feeds (0x0A)

Cracking the Code

The technique is surprisingly simple once you know what to look for:

  • Each space represents a binary 0
  • Each tab represents a binary 1
  • Line breaks separate different symbols

Here’s how to decode it:

  1. Read each line of the file
  2. Convert spaces to 0 and tabs to 1
  3. Ignore lines that are just separators (single 1) or end markers (00)
  4. Convert each binary string to its ASCII character
from pathlib import Path
raw = Path('txt').read_bytes()
lines = raw.splitlines()

out = []
for line in lines:
    bits = ''.join('0' if b==0x20 else '1' if b==0x09 else '' for b in line)
    if not bits or bits in ('1','00'):
        continue
    out.append(chr(int(bits, 2)))

print(''.join(out))

This reveals the hidden message: v1t{1_c4nt_s33_4nyth1ng}

Pro Tips

  • Always examine files with a hex editor if they look “empty”
  • Be careful with editors that strip trailing whitespace
  • Zero-width Unicode characters (U+200B, U+200C, etc.) are another variant of this technique

Challenge 2: LSB Steganography - The Devil’s in the Details

Flag: v1t{LSB:>}

This challenge teaches Least Significant Bit (LSB) steganography, a technique that hides data in the tiny details that our eyes can’t easily detect. It’s like hiding a secret message in the last letter of every word in a paragraph.

The Decoy Message

We’re given a file containing space-separated binary data that looks like this:

01001000 01101001 01101001 01101001 00100000 01101101 01100001 01101110...

If you convert this binary directly to ASCII, you get:

Hiii man,how r u ?Is it :))))Rawr-^^[]  LSB{><}!LSB~~LSB~~---v1t  {135900_13370}

This readable text is actually a decoy! Notice how it mentions “LSB” multiple times - that’s our hint. The real message is hidden in the least significant bits.

The LSB Extraction

LSB steganography works by taking just the last bit from each byte and reassembling them into a new message:

  1. For each 8-bit binary number, take only the rightmost bit
  2. Concatenate all these bits together
  3. Group them into new 8-bit chunks
  4. Convert each chunk to an ASCII character
# Take the least significant bit from each byte
lsb_bits = ''.join(b[-1] for b in bytes_list)

# Repack into bytes and decode to ASCII
hidden = ''.join(
    chr(int(lsb_bits[i:i+8], 2))
    for i in range(0, len(lsb_bits), 8)
    if len(lsb_bits[i:i+8]) == 8
)

This extraction reveals the true flag: v1t{LSB:>}


Challenge 3: Broken RSA - When Math Goes Wrong

Flag: v1t{f3rm4t_l1ttl3_duck}

RSA encryption is supposed to be secure, but what happens when someone makes a fundamental mistake in setting it up? This challenge shows how a single configuration error can completely break cryptographic security.

The Critical Mistake

Normal RSA uses a modulus n that’s the product of two large prime numbers: n = p × q. This challenge gives us:

  • n = a very large number
  • e = 65537 (standard)
  • c = encrypted message

The problem? The value of n is actually a prime number itself, not the product of two primes!

Why This Breaks Everything

In proper RSA, the security comes from the difficulty of factoring n into its prime components. But if n is already prime, there’s nothing to factor - we can directly compute what we need.

For a prime modulus n, the size of the multiplicative group is simply n - 1. This means:

  • φ(n) = n - 1 (instead of (p-1)(q-1))
  • We can directly compute the private key: d = e⁻¹ mod (n-1)

The Simple Solution

# Since n is prime, phi(n) = n - 1
phi = n - 1

# Compute the private exponent using the extended Euclidean algorithm
d = inverse_mod(e, phi)

# Decrypt the message
m = pow(c, d, n)

# Convert to readable text
plaintext = m.to_bytes((n.bit_length()+7)//8, 'big').lstrip(b'\x00')
print(plaintext.decode())

This reveals: v1t{f3rm4t_l1ttl3_duck} - a reference to Fermat’s Little Theorem, which is the mathematical principle that makes this attack possible.


Challenge 4: Modular Arithmetic Puzzle - Finding the Key

Flag: v1t{m0dul0_pr1z3}Key: 51

Sometimes the simplest ciphers can be the trickiest to break. This challenge uses modular arithmetic - basically fancy division with remainders - to hide a message. The twist? We need to figure out both the secret key and the hidden message.

Understanding the Cipher

The encryption is straightforward: take each character’s ASCII value and find its remainder when divided by a secret number (the key). For example, if the key is 51 and we have the letter ‘v’ (ASCII 118), we get: 118 % 51 = 16.

We’re given the encrypted data as a list of these remainders:

16 49 14 21 7 48 49 15 6 48 44 10 12 49 20 0 23

Finding the Secret Number

The clever part is deducing the key from the encrypted data. Since remainders are always less than the divisor, the largest remainder (49 in our case) tells us the key must be at least 50.

We can test possible keys by seeing which ones could produce valid flag characters. Assuming the flag uses typical characters (lowercase letters, digits, underscore, braces), we can check each possible key:

  • For each remainder, the original character could be remainder, remainder + key, remainder + 2×key, etc.
  • Only one key (51) produces sensible characters for all positions.

from pathlib import Path
res = list(map(int, Path('flag.enc').read_text().strip().split()))

# only test keys > max(residue) up to 100
last = res[-1]
K_CANDIDATES = [k for k in range(max(res)+1, 101) if (125 % k) == last]
if not K_CANDIDATES:
    raise SystemExit('No key candidates found; check assumptions.')
# the surviving candidate is the key
k = K_CANDIDATES[0]
print('Recovered key:', k)

  • Read the residues: the script loads flag.enc and parses the space-separated integers into res, the list of remainders produced by ord(ch) % k.
  • Bound the search: a remainder is always in [0, k-1], so the key must be larger than the largest residue; we therefore only test k starting at max(res)+1 (up to 100 per the challenge).
  • Use the trailing-brace heuristic: most CTF flags end with } (ASCII 125). If the last plaintext character is } then 125 % k must equal the final residue last. The comprehension [k for k in range(max(res)+1, 101) if (125 % k) == last] keeps only keys that satisfy that condition.
  • Number-theory view: 125 % k == last is equivalent to k dividing (125 - last). So we are looking for divisors of 125 - last that lie in the allowed range.
  • Handle failure: if K_CANDIDATES is empty then the } assumption (or the file) is wrong and the script exits.
  • Pick the key: in this dataset the filter yields a single surviving key (51), so the code takes k = K_CANDIDATES[0] and prints it.
  • Verify: you should (and can) verify the key by re-encrypting any candidate plaintext with k and checking the produced residues match flag.enc.

Reconstructing the Message

With key = 51, each remainder maps to a small set of possible characters. By choosing the most natural-looking combination that fits the flag format, we get:

v1t{m0dul0_pr1z3}

The Verification Step

Always double-check your work by re-encrypting your answer:

plaintext = 'v1t{m0dul0_pr1z3}'
encrypted = [ord(char) % 51 for char in plaintext]
# Should match the original: [16, 49, 14, 21, 7, 48, 49, 15, 6, 48, 44, 10, 12, 49, 20, 0, 23]

Why Multiple Solutions Exist

Modular arithmetic is “lossy” - different inputs can produce the same output. That’s why we had to make educated guesses about the flag format. The key, however, is unique and mathematically determined.


Challenge 5: Shamir’s Secret Sharing

Flag: v1t{555_s3cr3t_sh4r1ng}

The final challenge demonstrates Shamir’s Secret Sharing, a clever system where a secret is split into multiple pieces, and you need a minimum number of piecs to reconstruct it. It’s like having a treasure map torn into pieces - you need enough pieces to see the whole picture.

Understanding the Shares

We’re given six “shares” from different people:

Bob-ef73fe834623128e6f43cc923927b33350314b0d08eeb386
Sang-2c17367ded0cd22e15220a2b2a6cede16e2ed64d1898bbad
Khoi-e05fd9646ff27414510dec2e46032469cd60d632606c8181
Long-0c4de736ced3f8412307729b8bea56cc6dc74abce06a0373
Dung-afe15ff509b15eb48b0e9d72fc2285094f6233ec98914312
Steve-cb1a439f208aa76e89236cb496abaf20723191c188e23f54

Each hex string represents a point on a mathematical polynomial. The description hints that any 3 of the 6 “ducks” can reconstruct the secret.

The Mathematical Magic

Shamir’s scheme works by:

  1. Creating a polynomial where the secret is the y-intercept (the value when x=0)
  2. Giving each participant a point on this polynomial
  3. Using polynomial interpolation to reconstruct the original equation

Since we need 3 points to define a degree-2 polynomial, any 3 shares are sufficient.

Reconstructing the Secret

We interpret the data as:

  • x-coordinates: 1, 2, 3, 4, 5, 6 (Bob through Steve)
  • y-coordinates: the hex values converted to large integers

Using Lagrange interpolation to find the polynomial value at x=0:

# For points (x₁,y₁), (x₂,y₂), (x₃,y₃), evaluate at x=0:
secret = y * (0-x)(0-x)/((x-x)(x-x)) + 
         y * (0-x)(0-x)/((x-x)(x-x)) + 
         y * (0-x)(0-x)/((x-x)(x-x))

The Hidden Message

Converting the reconstructed secret to bytes reveals:

*v1t{555_s3cr3t_sh4r1ng}

The leading * is just padding, so our flag is: v1t{555_s3cr3t_sh4r1ng}

The “555” cleverly represents “SSS” (Shamir’s Secret Sharing) in leetspeak!

Why It Works

The beautiful thing about Shamir’s scheme is that it’s mathematically guaranteed: with exactly the threshold number of shares, the polynomial is uniquely determined. Any combination of 3 shares gives the same result, demonstrating the robustness of the system.

Happy hacking, and remember: in cybersecurity, curiosity and persistence are your best tools!