TCP1P CTF 2024 - Writeups
This is a writeup for some forensics challenges from TCP1P CTF 2024. I didn’t have much time to play this CTF, so I only solved a few challenges. I hope you enjoy it!
Skibidi Format [Forensics]
Description: So my friend just made a new image format and asked me to give him a test file, so I gave him my favorite png of all time. But the only thing I receive back is just my image with his new format and its “specification” file, don’t know what that is. Can you help me read this file?
We are given a .skibidi image and a spec.html file listing the format of the new image format. The structure is as follows:
A Skibidi file is composed of two main sections:
Header: Contains metadata about the image, compression, and encryption details.
Data Section: Holds the encrypted and compressed pixel data.
1
2
3
4
5
6
7
8
9
10
11
+----------------------+-----------------------+
| Header | Data Section |
+----------------------+-----------------------+
| Magic Number (4B) | Encrypted Data |
| Width (4B) | |
| Height (4B) | |
| Channels (1B) | |
| Compression ID (1B) | |
| AES Key (32B) | |
| AES IV (12B) | |
+----------------------+-----------------------+
We can extract the key and IV from the header and decrypt the data section using AES in GCM mode. The flag is the decrypted data.
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import struct
import zstandard as zstd
from Crypto.Cipher import AES
from PIL import Image
import numpy as np
HEADER_SIZE = 58
def save_as_image(data, width, height, channels, output_path):
if channels == 3:
mode = 'RGB'
elif channels == 4:
mode = 'RGBA'
else:
raise ValueError(f"Unsupported number of channels: {channels}")
image_data = np.frombuffer(data, dtype=np.uint8).reshape((height, width, channels))
image = Image.fromarray(image_data, mode=mode)
image.save(output_path)
print(f"Image saved to {output_path}")
def parse_header(file_data):
header = {}
header['magic_number'] = file_data[:4].decode('ascii')
header['width'], header['height'] = struct.unpack('<II', file_data[4:12])
header['channels'] = struct.unpack('<B', file_data[12:13])[0]
header['compression_id'] = struct.unpack('<B', file_data[13:14])[0]
header['aes_key'] = file_data[14:46]
header['aes_iv'] = file_data[46:58]
return header
def decrypt_data(encrypted_data, aes_key, aes_iv):
cipher = AES.new(aes_key, AES.MODE_GCM, nonce=aes_iv)
decrypted_data = cipher.decrypt(encrypted_data)
return decrypted_data
def decompress_data(compressed_data):
dctx = zstd.ZstdDecompressor()
try:
decompressed_data = dctx.decompress(compressed_data, max_output_size=1024*1024*100) # Assume max 100MB output
return decompressed_data
except zstd.ZstdError as e:
print(f"Zstd decompression error: {e}")
return None
def analyze_zstd_frame(compressed_data):
try:
frame_info = zstd.get_frame_parameters(compressed_data)
print(f"Frame info:")
print(f" Content size: {frame_info.content_size}")
print(f" Window size: {frame_info.window_size}")
print(f" Has checksum: {frame_info.has_checksum}")
# Print all available attributes
print("All frame info attributes:")
for attr in dir(frame_info):
if not attr.startswith('__'):
value = getattr(frame_info, attr)
print(f" {attr}: {value}")
except zstd.ZstdError as e:
print(f"Error analyzing Zstd frame: {e}")
def parse_skibidi(file_path):
with open(file_path, 'rb') as f:
file_data = f.read()
# Read and parse the header
header = parse_header(file_data[:HEADER_SIZE])
if header['magic_number'] != 'SKB1':
raise ValueError("Invalid magic number. Not a valid Skibidi file.")
print(f"Image dimensions: {header['width']}x{header['height']}, Channels: {header['channels']}")
# Extract the encrypted data (after header)
encrypted_data = file_data[HEADER_SIZE:]
print(f"Size of encrypted data: {len(encrypted_data)}")
# Decrypt the data using AES-256-GCM
try:
decrypted_data = decrypt_data(encrypted_data, header['aes_key'], header['aes_iv'])
print("Decryption successful.")
#print(f"Size of decrypted data: {len(decrypted_data)}")
#print(decrypted_data[:64].hex()) # Inspect first 64 bytes
except Exception as e:
print(f"Decryption error: {e}")
return None
# Decompress the data using Zstandard
if header['compression_id'] == 1:
#analyze_zstd_frame(decrypted_data)
decompressed_data = decompress_data(decrypted_data)
if decompressed_data:
save_as_image(decompressed_data, header['width'], header['height'], header['channels'], 'output_image.png')
else:
print("Decompression failed.")
else:
raise ValueError("Unsupported compression method.")
return decompressed_data
decompressed_image_data = parse_skibidi('suisei.skibidi')
The analyze_zstd_frame
function can be used to inspect the Zstandard frame parameters. I originally wrote this function to debug the decompression process, but it’s not necessary for solving the challenge.
The script will decrypt the data and save the image to output_image.png
.
Flag: TCP1P{S3ems_L1k3_Sk1b1dI_T0il3t_h4s_C0nsUm3d_My_fr13nD_U72Syd6}
Encrypt Decrypt File [Forensics]
Description: My brother deleted an important file from the encrypt-decrypt-file repository
We are given a .zip file which contains a file named main.py
which is used to encrypt the flag using AES-CBC mode and the rest of the files are part of a mercurial repository.
The main.py
script is as follows:
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import argparse
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
key = bytes.fromhex('00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff')
iv = bytes.fromhex('0102030405060708090a0b0c0d0e0f10')
BLOCK_SIZE = 16
def encrypt_file(input_file, output_file):
with open(input_file, 'rb') as f:
plaintext = f.read()
cipher = AES.new(key, AES.MODE_CBC, iv)
padded_plaintext = pad(plaintext, BLOCK_SIZE)
ciphertext = cipher.encrypt(padded_plaintext)
with open(output_file, 'wb') as f:
f.write(ciphertext)
print(f'File encrypted successfully and saved as {output_file}')
def decrypt_file(input_file, output_file):
with open(input_file, 'rb') as f:
ciphertext = f.read()
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = cipher.decrypt(ciphertext)
plaintext = unpad(decrypted_data, BLOCK_SIZE)
with open(output_file, 'wb') as f:
f.write(plaintext)
print(f'File decrypted successfully and saved as {output_file}')
def main():
parser = argparse.ArgumentParser(description="Encrypt or decrypt a file using AES-256-CBC.")
parser.add_argument('--encrypt', action='store_true', help="Encrypt the file.")
parser.add_argument('--decrypt', action='store_true', help="Decrypt the file.")
parser.add_argument('--input', type=str, required=True, help="Input file path.")
parser.add_argument('--output', type=str, required=True, help="Output file path.")
args = parser.parse_args()
if args.encrypt:
encrypt_file(args.input, args.output)
elif args.decrypt:
decrypt_file(args.input, args.output)
else:
print("Please specify --encrypt or --decrypt.")
if __name__ == "__main__":
main()
The script uses a hardcoded key and IV to encrypt and decrypt the file. We can use the main.py
script to decrypt the flag file. However, the flag.enc file is missing from the repository. We can use the mercurial repository to recover the deleted file.
1
2
$ hg cat -r 0 flag.enc > flag.enc
$ python3 main.py --decrypt --input flag.enc --output flag.txt
Inspecting the first few bytes of flag.txt reveals that it is infact a PNG file. We can rename the file to flag.png and open it to get the flag.
Flag: TCP1P{introduction_to_hg_a82ffbe612}
Conclusion
Overall, it was quite a difficult CTF for me, although I am far from an advanced player. My team (of 2 more) managed to solve a few OSINT and miscellaneous challenges, but we didn’t have the time or the necessary skills to solve the more difficult challenges. That is why we learn and then try again. I hope you enjoyed the writeups and learned something new. If you have any questions or suggestions, feel free to contact me on LinkedIn. Until next time, happy hacking!