Clock 🌡 20 °C
..

BrunnerCTF 2025 - Bakeware (RE)

Intro

The BrunnerCTF is a 48h online CTF created by the Brunnerne team.

For this challenge we get a file named Grandmas_Secret_Baking_Family_Recipe.enc wich contains encrypted datas and an ELF named Bakeware. The goal of this challenge is to decrypt the encrypted datas. The difficulty for me is that I’ve almost never coded in Rust, I can only read the binary code and improvise.

The Write-up

With a file command we notice that the binary isn’t stripped, which is cool.

To begin, I inspected the Rust main function with the r2 disassembler.

[0x00007bc0]> s sym.Bakeware::main::h837f6b7d44b9b487
[0x00008c60]> pdf

The first thing we notice is that the ELF try to open a file named Grandmas_Secret_Baking_Family_Recipe.txt.

... args
0x00008ca0      488d15c27c..   lea rdx, [0x00050969]       ; "Grandmas_Secret_Baking_Family_Recipe.txtFile not found. Nothing to steal
... more args
0x00008cbc      ff153e9b0500   call qword [sym.std::fs::OpenOptions::\_open::hce0f5e8979d4b5a1]
...

I guess that the file will be encrypted and that the output will be the .enc file. That’s why I create a backup of the encrypted file.

user_one@machine:bakeware $ cp Grandmas_Secret_Baking_Family_Recipe.enc Grandmas_Secret_Baking_Family_Recipe.enc.bak

If the file opening is a success the binary perfoms a SHA256 on the content of the .txt file.

            0x00008d12      a801           test al, 1
│      ┌──< 0x00008d14      0f85a60b0000   jne 0x98c0
│      ││   0x00008d1a      488dbc2490..   lea rdi, [var_190h]
│      ││   0x00008d22      488db42460..   lea rsi, [var_560h]
│      ││   0x00008d2a      ff1500a10500   call qword [sym.__alloc::vec::Vec_u8__as_sha256::Sha256Digest_::digest::h94a9f04017ae2932]

A bit further, we can see a call to the bcmp libc function. One of the arguments is a long string starting by what’s seems to be a SHA256 hash. The second argument passed to the func is 0x40 (64 in decimal notation). A SHA256 hash being 64 bytes long, it looks like an hash comparison.

0x00008d8a      488d35247c..   lea rsi, [0x000509b5]       ; "502ff05a7b51b76e740b19cc4957ad118897a25becbb87fcb662a14b2e56a5d9Sec..."
0x00008d91      ba40000000     mov edx, 0x40               ; elf_phdr
0x00008d96      4c89f7         mov rdi, r14
0x00008d99      ff15299f0500   call qword [reloc.bcmp]     ; [0x62cc8:8]=0

The solution to bypass this is obviously to change the value of the hash during the execution or read the code to see if this verification really matters.

I search a way to obtain a file content that validate the hash even though I knew that it’s almost impossible to get a collision. I still don’t know why I did that.

Once the hash verification done, there is several calls to a get_key_part function.

...
0x00008db6      e895fdffff     call sym Bakeware::get_key_part::hd4d7de7168456fd6
...
0x00008dcd      e87efdffff     call sym Bakeware::get_key_part::hd4d7de7168456fd6
...
0x00008de4      e867fdffff     call sym Bakeware::get_key_part::hd4d7de7168456fd6
...
0x00008dfb      e850fdffff     call sym Bakeware::get_key_part::hd4d7de7168456fd6
...
0x00008e12      e839fdffff     call sym Bakeware::get_key_part::hd4d7de7168456fd6
...
0x00008e29      e822fdffff     call sym Bakeware::get_key_part::hd4d7de7168456fd6
...

A bit further there is calls to AES related functions. We can guess that get_key_part is used to build the key for decryption.

Because I’m not able to reverse the get_key_part function statically, I used GDB. I search a function where the key could be passed as an argument and this seems to be a good candidate:

0x0000c4b0    1   1355 sym.__aes::ni::Aes256Enc_as_crypto_common::KeyInit_::new::haaa3a7e26e4efd28 

The first breakpoint is set a the bcmp call, the second at the KeyInit line.

(gdb) b* 0x100008d99
Breakpoint 1 at 0x100008d99
(gdb) b* 0x100009410
Breakpoint 2 at 0x10000c4b0
(gdb) r
...
Breakpoint 1, 0x0000000100008d99 in Bakeware::main ()
(gdb) set *(char *)$rdi@0x40=*(char *)$rsi@0x40
(gdb) c
Continuing.
(gdb) c
Continuing.

Breakpoint 2, 0x000000010000c4b0 in <aes::ni::Aes256Enc as crypto_common::KeyInit>::new ()
(gdb) x/s $rsi
0x100064b50:    "OTHellOTotallyStealGoodRecipes!!"
(gdb) c
Continuing.
Data exfiltrated to: Grandmas_Secret_Baking_Family_Recipe.enc
[Inferior 1 (process 18491) exited normally]

Some explanation about the set *(char *)$rdi@0x40=*(char *)$rsi@0x40 command. I didn’t used this command to resolve the challenge during the CTF, I found it afterward. Here $rdi contains the address of the array that contains the hash of the .txt file content. The (char *) part is a regular cast, as we do in C. This makes $rdi point to an array of char. The * placed before (char *) say that we refer to the value stored at the address pointed by $rdi (exactly like in C). The @0x40 refers to the numbers of datas we want to take in consideration. Here 0x40 char. GDB documentation related to that here.

It looks like OTHellOTotallyStealGoodRecipes!! is our key ! Later in the code there is calls to functions used to make the padding necessary to every AES implementations but I decided to decrypt the file without investigating this part. I don’t know which AES mode is used but It won’t be hard to test every of them anyway. I first tried with the CBC mode because it’s one of the most popular.

from Crypto.Cipher import AES

key = b'OTHellOTotallyStealGoodRecipes!!'
cipher = AES.new(key, AES.MODE_CBC)
f = open("Grandmas_Secret_Baking_Family_Recipe.enc.bak","rb")
ciphertext = bytes(f.read())

plaintext = cipher.decrypt(ciphertext)

print(plaintext)
user_one@machine:bakeware $ python decrypt.py
b"P\x9d\x95`\xa0\x91\xd7\xcd\x9e\xe8s\xa9\xb3\xa5iq days of baking and eatin' cookies and cake all day long. I finally think you are old enough for me to share my favourite recipe with you.\nPlease keep this safe for generations to come.\n\nThe recipe for the perfect brunsviger:\n- 20g yeast\n- 1dl milk\n- 40g butter\n- 1 egg\n- 40g sugar\n- 0.5 tsp salt\n- 250g flour\n\nTo bake it you simply just:\nbrunner{Gr4ndm4_sh0u1d_R34lL7_l34rn_b3tt3r_0ps3c}\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c"

We get the flag: brunner{Gr4ndm4_sh0u1d_R34lL7_l34rn_b3tt3r_0ps3c}.