Unlink Attack

Basic Information

Historically, this attack used to give a very strong WWW (Write-What-Where) primitive. Modern glibc added integrity checks, so the technique is no longer the old "write-anything-anywhere by corrupting fd/bk" bug class. However, unsafe unlink is still relevant as a way to obtain a relative pointer overwrite, create overlapping chunks, or pivot a pointer table into a more useful primitive.

Code Example:

Code
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

// Altered from https://github.com/DhavalKapil/heap-exploitation/tree/d778318b6a14edad18b20421f5a06fa1a6e6920e/assets/files/unlink_exploit.c to make it work

struct chunk_structure {
  size_t prev_size;
  size_t size;
  struct chunk_structure *fd;
  struct chunk_structure *bk;
  char buf[10];               // padding
};

int main() {
  unsigned long long *chunk1, *chunk2;
  struct chunk_structure *fake_chunk, *chunk2_hdr;
  char data[20];

  // First grab two chunks (non fast)
  chunk1 = malloc(0x8000);
  chunk2 = malloc(0x8000);
  printf("Stack pointer to chunk1: %p\n", &chunk1);
  printf("Chunk1: %p\n", chunk1);
  printf("Chunk2: %p\n", chunk2);

  // Assuming attacker has control over chunk1's contents
  // Overflow the heap, override chunk2's header

  // First forge a fake chunk starting at chunk1
  // Need to setup fd and bk pointers to pass the unlink security check
  fake_chunk = (struct chunk_structure *)chunk1;
  fake_chunk->size = 0x8000;
  fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
  fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

  // Next modify the header of chunk2 to pass all security checks
  chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
  chunk2_hdr->prev_size = 0x8000;  // chunk1's data region size
  chunk2_hdr->size &= ~1;        // Unsetting prev_in_use bit

  // Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
  // This results in chunk1 pointer pointing to chunk1 - 3
  // i.e. chunk1[3] now contains chunk1 itself.
  // We then make chunk1 point to some victim's data
  free(chunk2);
  printf("Chunk1: %p\n", chunk1);
  printf("Chunk1[3]: %x\n", chunk1[3]);

  chunk1[3] = (unsigned long long)data;

  strcpy(data, "Victim's data");

  // Overwrite victim's data using chunk1
  chunk1[0] = 0x002164656b636168LL;

  printf("%s\n", data);

  return 0;
}

Modern notes

  • The primitive is not dead after tcache. The main problem is that if a chunk is handled by tcache or fastbins, unlink_chunk() is never reached. Therefore, modern PoCs usually use sizes outside tcache (for example 0x420 in the current how2heap unsafe_unlink.c) or first fill the target tcache bin.
  • Safe-linking protects the singly linked lists used by tcache and fastbins, but it does not protect the doubly linked fd/bk pointers used by the unlink checks. It still matters in practice because many modern exploits use unsafe unlink only to get an overlap and then finish with Tcache Bin Attack.
  • In modern challenges this technique is often just the first stage: create an overlap / move a pointer table / corrupt a known pointer, and then chain that into a libc leak, heap leak, GOT overwrite, FSOP, or tcache poisoning.

Goal

This attack allows an attacker to change a pointer to a chunk so it points 3 qwords before its original storage location. If that new location contains interesting data (other heap pointers, stack values, globals, or a pointer table), it may be possible to read or overwrite it and pivot into a stronger primitive.

  • If this pointer is stored in the stack, and the user can later read/write through it, it may be possible to leak sensitive stack data or even modify nearby saved state without directly touching the canary.
  • In several CTF examples this pointer is stored inside an array of heap pointers instead of the stack. Then, moving the pointer 3 qwords backwards is enough to retarget adjacent entries and turn the bug into arbitrary read/write against GOT entries or other application structures.

Requirements

  • Control over one chunk's contents and the ability to corrupt the next chunk header.
  • A fake chunk that can satisfy the modern unlink checks:
  • chunksize(P) == prev_size(next_chunk(P))
  • P->fd->bk == P
  • P->bk->fd == P
  • A known writable location containing the pointer you want to corrupt (stack slot, global pointer, pointer array, heap metadata controlled by the program, ...).
  • The target free must actually reach backward consolidation. If the chunk goes to tcache/fastbins, the unlink path is not triggered.
  • In many modern exploit chains, a heap leak is also needed later because the overlap obtained with unlink is commonly chained with House of Einherjar or Tcache Bin Attack.

Attack

  • There are a couple of chunks (chunk1 and chunk2).
  • The attacker controls the content of chunk1 and the headers of chunk2.
  • Inside chunk1 the attacker creates a fake free chunk:
  • The fake chunk size must match the forged prev_size that will later be read from the next chunk. Otherwise glibc aborts with corrupted size vs. prev_size while consolidating.
  • The fake chunk fd and bk are made to point near the storage of the real chunk1 pointer, with offsets -3 and -2, so that both integrity checks are true and both writes land on the same pointer-sized slot.

https://heap-exploitation.dhavalkapil.com/attacks/unlink_exploit

  • The header of chunk2 is corrupted to indicate that the previous chunk is free:
  • clear PREV_INUSE
  • forge prev_size so it points backwards to the fake chunk
  • When chunk2 is freed, glibc performs backward consolidation and unlink_chunk() processes the fake chunk:
  • fake_chunk->fd->bk = fake_chunk->bk
  • fake_chunk->bk->fd = fake_chunk->fd
  • Because fake_chunk->fd->bk and fake_chunk->bk->fd were arranged to reference the same memory slot, the second write wins and the stored chunk1 pointer is changed to the address located 3 qwords before it.
  • Once the program uses chunk1 again, the attacker now reads/writes through a misdirected pointer. If the corrupted pointer lives near other attacker-controlled pointers, stack variables, or an object table, this often becomes the real exploitation pivot.
  • A very common modern continuation is:
  • use unlink to obtain an overlap or corrupt a pointer table,
  • leak heap/libc pointers from unsorted or overlapped chunks,
  • finish with House of Einherjar or Tcache Bin Attack.

https://heap-exploitation.dhavalkapil.com/attacks/unlink_exploit

References