╔══════════════════════════════════════════════════════════════════════════════════╝ ║REV MYSECRETRECIPE HER0CTF WRITEUP 13/12/2025 ╚══════════════════════════════════════════════════════════════════════════════════╗┏━━┓ BACK┗━━┛ we are given the following: "You will never guess the secret recipe for my secret flag-cake !" and a my_secret_recipe file ==================================================================================================== first impressions: looks like an ELF file, lets try running it in WSL: -> [-] Missing arguments, usage ./my_secret_recipeokay so we need to give it the flag as a string lets try: ./my_secret_recipe secret ->🍰 The Chef’s Secret Recipe: To bake the perfect flag-cake: sift the flour, add sugar, crack some eggs, melt the butter, blend in vanilla and milk, whisk the cocoa, fold in the baking powder, swirl in the cream, chop some cherry, toss on sprinkles, preheat the oven, grease the pan, line it with parchment, set the timer, light a candle, serve on a plate, and garnish with frosting, a pinch of salt, and crushed nuts for that final touch of sweetness. [-] Nope open in ghidra: main takes our two args (as expected) then it initialises a local_48 with 0x28 items (40) next, it calls a function called "parse_recipe" that takes the recipe printed before as well as local_48 as arguments after this function has done something to local_48, it is then compared to our string input (argv[1]) and if they match, it prints local_48 (which is presumably the flag) lets look inside the parse_recipie function: ==================================================================================================== has a stack canary at the top -> local_20 (renamed to canary) strcpy is called with 3 arguments - not sure what that third arg is okay its just size, so secret_recipe is copied into local_428 with size 0x400 (1024) local_438 is then taken as the tokened list from local_428 (with argv[1] = " \n" which I assume means a list of items that are seperated by a newline char or space?) documentation calls it a "delimiter" its then passed to a function called "normalise word" - which I think removes all uppercase chars then we get a loop: counter = 0 while counter < 41: (one for each of the items in local_48 - plus a null terminator) iVar2 = strcmp(recipe_tokens, *(char **)(ingredients + counter * 16)); I think what this is doing is seeing if each token matches an "ingredient" list and then setting the corresponding local_48[counter] to be equal to some function call contained in the "ingredients" in ghidra, "ingredients" is just a list of bytes - lets try binja yeah thats better (though not perfect) lists a bunch of ingredients, as well as function references: ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- 00404060 char const (* ingredients)[0x5] = data_402008 {"bake"} 00404068 void* data_404068 = bake 004011a9 int64_t bake() __pure 004011b3 return 0x48 ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- so if "bake" was the first word in the 40 token list, then the returned token list would contain 0x48 as its first item (which is "H" in ASCII) lets see how long it is going to take to make a parsing script for this... (a long time it turns out) ==================================================================================================== ==================================================================================================== recipe = "To bake the perfect flag-cake: sift the flour, add sugar, crack some eggs, melt the butter, blend in vanilla and milk, whisk the cocoa, fold in the baking powder, swirl in the cream, chop some cherry, toss on sprinkles, preheat the oven, grease the pan, line it with parchment, set the timer, light a candle, serve on a plate, and garnish with frosting, a pinch of salt, and crushed nuts for that final touch of sweetness." def tokenise(string): tokens = string.split() return tokens ingredients = { "bake":0x48, "perfect":0x65, "sift":0x72, "flour":0x6f, "sugar":0x7b, "crack":0x30, "eggs":0x68, "melt":0x5f, "butter":0x4e, "blend":0x30, "vanilla":0x5f, "milk":0x79, "whisk":0x30, "cocoa":0x75, "fold":0x5f, "baking":0x36, "powder":0x30, "swirl":0x54, "cream":0x5f, "chop":0x4d, "cherry":0x79, "toss":0x5f, "sprinkles":0x53, "preheat":0x33, "oven":0x63, "grease":0x52, "pan":0x65, "line":0x54, "parchment":0x5f, "timer":0x43, "light":0x34, "candle":0x6b, "plate":0x33, "garnish":0x5f, "frosting":0x52, "pinch":0x33, "salt":0x63, "crushed":0x31, "nuts":0x70, "touch":0x65, "sweetness":0x7d, } def decode(tokens): counter = 0 decoded_tokens = [] while counter < len(tokens): word = tokens[counter] if word in ingredients: decoded_tokens.append(ingredients[word]) counter += 1 return decoded_tokens def from_hex(tokens): ascii_string = "" for i in tokens: char = chr(i) ascii_string = ascii_string + char return ascii_string if __name__ == "__main__": tokens = tokenise(recipe) decoded = decode(tokens) ascii_chars = from_hex(decoded) print(ascii_chars) ==================================================================================================== ==================================================================================================== ruinning this we get: -> Her0_0_0_6TM_3RT4_31pe the first bit looks legit for sure, but not sure about the rest? - i guess its kind of leet-speak lets just try it, flag format is: ^Hero{\S+}$ try: ^Hero{Her0_0_0_6TM_3RT4_31pe}$ -> invalid try: Hero{Her0_0_0_6TM_3RT4_31pe} -> invalid hmmm that feels like way less than 40 characters... -> its only 22 so im missing a bunch, the first few work fine (given the first 4 chars are Her0 - that seems correct) added some debug stuff to the code - and found its skipping some tokens maybe it's because they contain punctuation? - so "x," is not recognised as "x" because of the comma? yeah that was it, here is the fixed code: ==================================================================================================== ==================================================================================================== debug = 0 recipe = "\tTo bake the perfect flag-cake: sift the flour, add sugar, crack some eggs,\n \tmelt the butter, blend in vanilla and milk, whisk the cocoa, fold in the baking powder,\n \tswirl in the cream, chop some cherry, toss on sprinkles, preheat the oven, grease the pan,\n \tline it with parchment, set the timer, light a candle, serve on a plate, and garnish with frosting,\n \ta pinch of salt, and crushed nuts for that final touch of sweetness. \n\n" def tokenise(string): tokens = string.split() counter = 0 while counter < len(tokens): tokens[counter] = tokens[counter].strip(".,:;!?-[]{}()") counter = counter + 1 return tokens ingredients = { "bake":0x48, "perfect":0x65, "sift":0x72, "flour":0x6f, "sugar":0x7b, "crack":0x30, "eggs":0x68, "melt":0x5f, "butter":0x4e, "blend":0x30, "vanilla":0x5f, "milk":0x79, "whisk":0x30, "cocoa":0x75, "fold":0x5f, "baking":0x36, "powder":0x30, "swirl":0x54, "cream":0x5f, "chop":0x4d, "cherry":0x79, "toss":0x5f, "sprinkles":0x53, "preheat":0x33, "oven":0x63, "grease":0x52, "pan":0x65, "line":0x54, "parchment":0x5f, "timer":0x43, "light":0x34, "candle":0x6b, "plate":0x33, "garnish":0x5f, "frosting":0x52, "pinch":0x33, "salt":0x63, "crushed":0x31, "nuts":0x70, "touch":0x65, "sweetness":0x7d, } def decode(tokens): counter = 0 decoded_tokens = [] debug_tokens = [] while counter < len(tokens): word = tokens[counter] if word in ingredients: decoded_tokens.append(ingredients[word]) debug_tokens.append(tokens[counter]) counter += 1 if debug == True: print(debug_tokens) return decoded_tokens def from_hex(tokens): ascii_string = "" for i in tokens: char = chr(i) ascii_string = ascii_string + char return ascii_string if __name__ == "__main__": tokens = tokenise(recipe) decoded = decode(tokens) ascii_chars = from_hex(decoded) print(ascii_chars) ==================================================================================================== ==================================================================================================== running this yields: -> Hero{0h_N0_y0u_60T_My_S3cReT_C4k3_R3c1pe} which is valid :) ┏━━┓ BACK┗━━┛