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.
In this write-up, I’ll walk through some of the challenges I solved.
Let’s dive in right away:
Forensics
Phantom Connection
Extracting the zip file resulted to a cache folder with 2 files “bcache24.bmc” and “Cache000.bin”
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.
This last part I solved after the CTF was over
Scrolling through:
Also noticed -b from BMC-tools later on:
Opening the merged collage file:
1
apoorvctf{CAcH3_Wh4T_YoU_sE3}
Cryptography
Kowareta Cipher
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.
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:
Changing the target to the remote server:
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
1
apoorvctf{3cb_345y_crypt0_br34k}
Till the next one.
And remember, there are many ways of killing Jerry!