Stonks
Goal: a fake trading service asks for an API token and prints it back. The flag is read into a buffer in the same function and is never printed by anything.
Approach
Section titled “Approach”The source is the whole briefing.
┌──(Idan@Kali)-[~/Stonks]└─> wget https://mercury.picoctf.net/static/fdf270d959fa5231e180e2bd11421d0c/vuln.cbuy_stonks() does three things in order that matter. It opens a file called api and reads the flag into char api_buf[128], a local. It allocates a 301 byte buffer on the heap and fills it with scanf("%300s", user_buf). Then it calls printf(user_buf) with no format string of its own, which makes whatever you typed the format string.
That last detail decides which attack is available. Because the payload lives on the heap rather than the stack, the usual move of planting an address in your own buffer and dereferencing it with %s is off the table. What works instead is simply reading: %x consumes one four byte slot from where printf expects its variadic arguments, and on a 32 bit binary those come off the stack in order, so a long run of them walks straight up the frame that api_buf is sitting in.
A run of 130 %x is 260 characters, comfortably inside the 300 that scanf accepts, which caps the payload at 150 specifiers. The reply is 817 hex digits, saved to token.txt.
The full leak, 817 hex digits
8bc03b0804b00080489c3f7fbed80ffffffff18bbe160f7fcc110f7fbedc708bbf18018bc03908bc03b06f6369707b465443306c5f49345f74356d5f6c6c306d5f795f79336e6263376365616336ff98007df7ff9af8f7fcc440aaadc40010f7e5bce9f7fcd0c0f7fbe5c0f7fbe000ff9845b8f7e4c68df7fbe5c08048ecaff9845c40f7fe0f09804b000f7fbe000f7fbee20ff9845f8f7fe6d50f7fbf890aaadc400f7fbe000804b000ff9845f88048c868bbe160ff9845e4ff9845f88048be9f7fbe3fc0ff9846acff9846a4118bbe160aaadc400ff98461000f7e01fa1f7fbe000f7fbe0000f7e01fa11ff9846a4ff9846acff98463410f7fbe000f7fe170af7ff90000f7fbe000002af6a390da444580000180486300f7fe6d50f7fe1960804b00018048630080486628048b851ff9846a48048cd08048d30f7fe1960ff98469cf7ff99401ff985e7a0ff985eb3ff985ec0ff985ec9ff985ef8ff985f30ff985f4cff985f6bff985f73020f7fd1b5021f7fd1000101f8bfbff61000116438048034420597f7fd20008098048630b412c412d413e41317Decoding the leak
Section titled “Decoding the leak”Converted back to bytes, the leak is mostly unreadable, which is expected: the stack holds pointers, not text. One run of printable characters stands out.
┌──(Idan@Kali)-[~/Stonks]└─> xxd -r -p token.txt > tokenHexdump┌──(Idan@Kali)-[~/Stonks]└─> bat -A tokenHexdump | grep -oE 'ocip\{[^}]*\}' --color=noneocip{FTC0l_I4_t5m_ll0m_y_y3nbc7ceac6\xFF\x98␀}ocip{FTC is pico and CTF{ with each group of four turned around, and the same holds all the way along. Forty bytes, ten groups, reversed in place.
scrambled = b"ocip{FTC0l_I4_t5m_ll0m_y_y3nbc7ceac6\xff\x98\x00}"fixed = b""for i in range(0, len(scrambled), 4): fixed += scrambled[i:i+4][::-1]print(fixed.decode(errors="ignore").rstrip("\x00"))picoCTF{I_l05t_4ll_my_m0n3y_c7cb6cae} The eight characters before the brace belong to the instance, not to the challenge, and mercury.picoctf.net no longer resolves.
Why it works
Section titled “Why it works”printf cannot tell that its format string came from the user, so every %x is an instruction it follows honestly: take the next four byte slot where a variadic argument should be, and print it. Nothing is being overflowed. The frame is simply being read out in order, and api_buf sits fourteen words past the argument area, so the flag begins at the fifteenth conversion and runs for ten.
The scrambling is endianness meeting notation, and neither half causes it alone. %x prints a word as a number, most significant digit first, while on a little endian machine the most significant byte is the one stored at the highest address. So the printed text spells each word out in descending address order. xxd -r -p then turns that text back into bytes knowing nothing about where one word ends and the next begins, which is what leaves each of the flag’s ten words back to front. The last group is worth reading slowly: a closing brace, the NUL that fgets wrote after it, and then two bytes of the stack address that was living there beforehand.