Home ApoorvCTF 2025 Write-up
Post
Cancel

ApoorvCTF 2025 Write-up

On Fri, 28 Feb. 2025 — Sun, 02 March 2025, Fr334aks-mini participated in ApoorvCTF, competing against 776 teams and securing 187th place. The competition featured a diverse set of challenges, testing our skills in multiple domains, from reverse engineering to forensics, OSINT, web, pwn, and more.

image.png

fmini.png

In this write-up, I’ll walk through some of the challenges I solved.

Let’s dive in right away:

Forensics

Phantom Connection

ph1.png

Extracting the zip file resulted to a cache folder with 2 files “bcache24.bmc” and “Cache000.bin”

ph2.png

Looking further, I found nothing much until I checked on the “bmc” file extension.

BMC files are cached screen fragments(bitmaps) used by Microsoft’s Windows Remote Desktop Client (RDC) caching system. They are stored for quick retrieval during remote desktop sessions.

Searching on how to open a bmc file I stumbled on BMC-tools designed for RDP Bitmap Cache analysis. After tickling it’s syntax, I was able to extract the bitmap files from the bin file.

ph3.png

This last part I solved after the CTF was over

Scrolling through:

ph4.png

Also noticed -b from BMC-tools later on:

ph5.png

Opening the merged collage file:

ph6.png

1
apoorvctf{CAcH3_Wh4T_YoU_sE3}

Cryptography

Kowareta Cipher

crypt0.png

The challenge description hints on AES usage without an initialization vector(IV), some patterns and cracks - some observable and a weakness.

We are given a zip file that extracts to challenge.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from random import randbytes

def main():
    key = randbytes(16)
    cipher = AES.new(key, AES.MODE_ECB)
    flag = b'apoorvctf{fake_flag_123}'

    print("Welcome to the ECB Oracle challenge!")
    print("Enter your input in hex format.")

    try:
        while True:
            print("Enter your input: ", end="", flush=True)
            userinput = sys.stdin.readline().strip()

            if not userinput:
                break

            try:
                userinput = bytes.fromhex(userinput)
                ciphertext = cipher.encrypt(pad(userinput + flag + userinput, 16))
                print("Ciphertext:", ciphertext.hex())

            except Exception as e:
                print(f"Error: {str(e)}")

    except KeyboardInterrupt:
        print("Server shutting down.")

if __name__ == "__main__":
    main()
    

Analyzing the script, it implements an AES ECB encryption oracle where a user can send hex-encoded input, and the server responds with an AES-encrypted ciphertext.

The ECB (Electronic Codebook) mode used is insecure because it encrypts identical plaintext blocks into identical ciphertext blocks. Since there is no IV, this kicks out chances of randomness on the cyphertext.

crypt1a.png

The flag is sandwiched between two copies of user input. We can leak the flag using an ECB Oracle Attack.

To get the flag here we can:

  • Control input placement to reveal one byte of the flag at a time.
  • Compare ciphertext blocks to deduce the flag content.
  • Use a byte-by-byte brute-force.

Let’s make life easier by creating a script to:

  • Send controlled input (padding) to align the flag.
  • Capture the reference ciphertext (baseline).
  • Brute-force one byte at a time by comparing ciphertext blocks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Author: D_C4ptain

from pwn import *

BLOCK_SIZE = 16
known_flag = b""

# Connect to the challenge process or server
#p = remote("chals1.apoorvctf.xyz", 4001)
p = process(["python3", "Chall.py"])

# Consume the initial welcome message
print(p.recvuntil(b"Enter your input: "))

print(" Starting ECB Oracle Attack...\n")

for i in range(BLOCK_SIZE):  # Assuming the flag is at least 16 bytes
    pad_length = BLOCK_SIZE - (i + 1)
    prefix = b"A" * pad_length  # Align the flag in the block

    # Send the controlled input to align the flag
    p.sendline(prefix.hex().encode())  
    p.recvuntil(b"Ciphertext: ")
    ciphertext_ref = bytes.fromhex(p.recvline().strip().decode())

    for b in range(256):  # Brute-force each byte
        test_input = prefix + known_flag + bytes([b])
        p.sendline(test_input.hex().encode())
        p.recvuntil(b"Ciphertext: ")
        ciphertext_test = bytes.fromhex(p.recvline().strip().decode())

        # Compare encrypted blocks
        if ciphertext_test[:BLOCK_SIZE] == ciphertext_ref[:BLOCK_SIZE]:
            known_flag += bytes([b])
            print(f"Recovered: {known_flag.decode(errors='ignore')}")
            break

print("\n **Recovered Flag:**", known_flag.decode(errors='ignore'))

Running solv.py simultaneously on the chall.py:

soln1.png

Changing the target to the remote server:

crypt1.png

For some reason the script would stop abruptly. I had to change the blocksize(forgot about my assumption) to 48 bytes and it worked perfectly - thanks to a team mate; mburka

soln2.png

1
apoorvctf{3cb_345y_crypt0_br34k}

Till the next one.

And remember, there are many ways of killing Jerry!

jerry.webp

This post is licensed under CC BY 4.0 by the author.

UofTCTF 2025 Write up

-