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@0x40command. I didn’t used this command to resolve the challenge during the CTF, I found it afterward. Here$rdicontains the address of the array that contains the hash of the.txtfile content. The(char *)part is a regular cast, as we do in C. This makes$rdipoint to an array ofchar. The*placed before(char *)say that we refer to the value stored at the address pointed by$rdi(exactly like in C). The@0x40refers to the numbers of datas we want to take in consideration. Here0x40char. 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}.