.. visibility



 ██▓     ██▓ ▄▄▄▄    ▄████▄      ██▓███   ▒█████   ██▓ ███▄    █ ▄▄▄█████▓▓█████  ██▀███      ██▓███   ██▀███   ▒█████  ▄▄▄█████▓▓█████  ▄████▄  ▄▄▄█████▓ ██▓ ▒█████   ███▄    █   ██████ 
▓██▒    ▓██▒▓█████▄ ▒██▀ ▀█     ▓██░  ██▒▒██▒  ██▒▓██▒ ██ ▀█   █ ▓  ██▒ ▓▒▓█   ▀ ▓██ ▒ ██▒   ▓██░  ██▒▓██ ▒ ██▒▒██▒  ██▒▓  ██▒ ▓▒▓█   ▀ ▒██▀ ▀█  ▓  ██▒ ▓▒▓██▒▒██▒  ██▒ ██ ▀█   █ ▒██    ▒ 
▒██░    ▒██▒▒██▒ ▄██▒▓█    ▄    ▓██░ ██▓▒▒██░  ██▒▒██▒▓██  ▀█ ██▒▒ ▓██░ ▒░▒███   ▓██ ░▄█ ▒   ▓██░ ██▓▒▓██ ░▄█ ▒▒██░  ██▒▒ ▓██░ ▒░▒███   ▒▓█    ▄ ▒ ▓██░ ▒░▒██▒▒██░  ██▒▓██  ▀█ ██▒░ ▓██▄   
▒██░    ░██░▒██░█▀  ▒▓▓▄ ▄██▒   ▒██▄█▓▒ ▒▒██   ██░░██░▓██▒  ▐▌██▒░ ▓██▓ ░ ▒▓█  ▄ ▒██▀▀█▄     ▒██▄█▓▒ ▒▒██▀▀█▄  ▒██   ██░░ ▓██▓ ░ ▒▓█  ▄ ▒▓▓▄ ▄██▒░ ▓██▓ ░ ░██░▒██   ██░▓██▒  ▐▌██▒  ▒   ██▒
░██████▒░██░░▓█  ▀█▓▒ ▓███▀ ░   ▒██▒ ░  ░░ ████▓▒░░██░▒██░   ▓██░  ▒██▒ ░ ░▒████▒░██▓ ▒██▒   ▒██▒ ░  ░░██▓ ▒██▒░ ████▓▒░  ▒██▒ ░ ░▒████▒▒ ▓███▀ ░  ▒██▒ ░ ░██░░ ████▓▒░▒██░   ▓██░▒██████▒▒
░ ▒░▓  ░░▓  ░▒▓███▀▒░ ░▒ ▒  ░   ▒▓▒░ ░  ░░ ▒░▒░▒░ ░▓  ░ ▒░   ▒ ▒   ▒ ░░   ░░ ▒░ ░░ ▒▓ ░▒▓░   ▒▓▒░ ░  ░░ ▒▓ ░▒▓░░ ▒░▒░▒░   ▒ ░░   ░░ ▒░ ░░ ░▒ ▒  ░  ▒ ░░   ░▓  ░ ▒░▒░▒░ ░ ▒░   ▒ ▒ ▒ ▒▓▒ ▒ ░
░ ░ ▒  ░ ▒ ░▒░▒   ░   ░  ▒      ░▒ ░       ░ ▒ ▒░  ▒ ░░ ░░   ░ ▒░    ░     ░ ░  ░  ░▒ ░ ▒░   ░▒ ░       ░▒ ░ ▒░  ░ ▒ ▒░     ░     ░ ░  ░  ░  ▒       ░     ▒ ░  ░ ▒ ▒░ ░ ░░   ░ ▒░░ ░▒  ░ ░
  ░ ░    ▒ ░ ░    ░ ░           ░░       ░ ░ ░ ▒   ▒ ░   ░   ░ ░   ░         ░     ░░   ░    ░░         ░░   ░ ░ ░ ░ ▒    ░         ░   ░          ░       ▒ ░░ ░ ░ ▒     ░   ░ ░ ░  ░  ░  
    ░  ░ ░   ░      ░ ░                      ░ ░   ░           ░             ░  ░   ░                    ░         ░ ░              ░  ░░ ░                ░      ░ ░           ░       ░  
                  ░ ░                                                                                                                   ░                                                  
                                                                                              
                                                                               
                      @@@@@@@@                                                       
                  @@@@@@@@@@@@@@@@                                                   
                @@@@@@@      @@@@@@@@@@@@@@@@@                                       
               @@@@           @@@@@@@@  @@@@@@@@@                                    
                            @@@@@  @@@@       @@@@@                                  
            @@@           @@@@@     @@@@        @@@@                                 
            @@@@         @@@@        @@@         @@@@                                
            @@@        @@@@@         @@@          @@@@@@@@@@@@                       
            @@@@      @@@@           @@@        @@@@@@@  @@@@@@@                     
            @@@@     @@@@        @@@@@@@@@@@@  @@@@ @@@      @@@@                    
             @@@@  @@@@@     @@@@@@@@@@@@@@@@@@@@@  @@@        @@                    
              @@@@@@@@@@@@@@@@@@@@  @@@@    @@@@@@@ @@@                              
              @@@@@@@@@@@@@@@@@@@@@@@@@      @@@@@@@@@@@         @@@                 
            @@@@@@@@@@ @@@@@    @@@@@@@      @@@  @@@@@@         @@@@                
          @@@@@    @@@@@@@         @@@@@    @@@@    @@@@@         @@@                
         @@@@       @@@@@          @@@@@@@@@@@@@     @@@@         @@@@               
        @@@@        @@@@@@      @@@@@@@@@@@@@@@@@@@   @@@          @@@               
       @@@@        @@@@@@@@@@@@@@@@@@  @@@@  @@@@@@@@@@@@@         @@@@    @@@       
      @@@@        @@@@@@@@@@@@@@  @@@   @@@   @@@ @@@@@@@@@        @@@@    @@@@@     
      @@@@        @@@@@@@@@@      @@@   @@@   @@@@   @@@@@@@       @@@@      @@@@    
      @@@     @@@@@@@@@  @@@@    @@@@   @@@@   @@@@     @@@@@@@    @@@@       @@@@@  
@@@@@@@@@@@@@@@@@@@@      @@@@   @@@@    @@@    @@@     @@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@  @@@@      @@@@   @@@@    @@@    @@@@@  @@@@     @@@@@@@@@@@@@@@@@@@@@
      @@@       @@@@       @@@   @@@@    @@@      @@@@ @@@@        @@@@        @@@@@ 
      @@@@      @@@@       @@@@  @@@@   @@@@       @@@@@@@         @@@       @@@@@   
       @@@      @@@@       @@@    @@@   @@@         @@@@@         @@@@     @@@@@     
       @@@@@    @@@@      @@@@    @@@ @@@@@          @@@@         @@@      @@@       
        @@@@@    @@@      @@@@    @@@@@@@           @@@@@        @@@@                
          @@@@@@ @@@@   @@@@@      @@@@@          @@@@@@@@       @@@@                
            @@@@@@@@@@@@@@@      @@@@@@          @@@@ @@@@      @@@@                 
               @@@@@@@@@@       @@@@@@@@@     @@@@@@  @@@@     @@@@                  
                   @@@@        @@@@   @@@@@@@@@@@@    @@@     @@@@                   
                    @@@@       @@@       @@@@@      @@@@@    @@@@                    
                     @@@@      @@@@               @@@@@     @@@@                     
                      @@@@@     @@@@@@@        @@@@@@     @@@@@                      
                        @@@@@     @@@@@@@@@@@@@@@@@    @@@@@@                        
                         @@@@@@@       @@@@@@@      @@@@@@@                          
                            @@@@@@@@@             @@@@@@                             
                               @@@@@@@@@@@@@@@@   @@                                 
                                     @@@@@@@@@                                       
                                                                            

Introduction

I was playing a CTF a couple weekends ago and I stumbled upon this heap-style challenge that was linked to a “modern” libc. This was my first time dealing with modern libc versions, so naturally I faced a couple new (to me) protections.

Unfortunately, I couldn’t finish the challenge in time and that spiked my curiosity about them.

So in this blog post, I’ll talk about my research about the libc pointer protections introduced in 2 fields:

Heap pointers

Commit a1a486d70ebcc47a686ff5846875eacad0940e41 introduced Safe-Linking into the heap single-linked lists, in particular to the tcache and the fastbins in 2020.

From the commit message:

Safe-Linking is a security mechanism that protects single-linked
lists (such as the fastbin and tcache) from being tampered by attackers.
The mechanism makes use of randomness from ASLR (mmap_base), and when
combined with chunk alignment integrity checks, it protects the "next"
pointers from being hijacked by an attacker.
...
The design assumes an attacker doesn't know where the heap is located,
and uses the ASLR randomness to "sign" the single-linked pointers. We
mark the pointer as P and the location in which it is stored as L, and
the calculation will be:
  * PROTECT(P) := (L >> PAGE_SHIFT) XOR (P)
  * *L = PROTECT(P)

This translate in two macros, PROTECT_PTR and REVEAL_PTR, defined in malloc.c that are called whenever a pointer is written and retrieved, respectively.

#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

Let’s break down this process, first by understantding how a pointer is encoded:

pos represents, as the commit message says, the address where pointer will be stored and ptr is the pointer address itself.

First, we get the address (pos) and shift it 12 bits. If assuming a page size of 4096, this effectively extracts the page of the memory address.

0x555555559af0 will turn into 0x555555559

After than, we xor the value with the pointer value to obtain the protected pointer.

Revealing the original pointer works the same way, by simply rerunning the operation.

Example

Let’s look at an example. We have a heap at 0x555555559000 with 2 free chunks:

0x555555559000	0x0000000000000000	0x0000000000000291	................
0x555555559010	0x0000000000020000	0x0000000000000000	................
...
0x555555559ab0	0x0000000000000000	0x0000000000000031	........1.......
0x555555559ac0	0x0000000555555559	0x990bee95285d21f8	YUUU.....!](....	 <-- tcachebins[0x30][1/2]
0x555555559ad0	0x0000000000000000	0x0000000000000000	................
0x555555559ae0	0x0000000000000000	0x0000000000000031	........1.......
0x555555559af0	0x000055500000cf99	0x990bee95285d21f8	....PU...!](....	 <-- tcachebins[0x30][0/2]
0x555555559b00	0x0000000000000000	0x0000000000000000	................
0x555555559b10	0x0000000000000000	0x00000000000204f1	................	 <-- Top chunk

Picking the last one, we can see an FD of 0x55500000cf99, that we know points to 0x555555559ac0. Let’s reveal it:

Exploitation perspective

From an exploitation perspective, this means we need to leak a heap address to defeat this protection.

I’m sure you noticed the different value in tcachebins[0x30][1/2] and this is a very useful one. The first one to be linked into the fastbins or the tcache will have a forward pointer with value 0. When we perform the PROTECT_PTR, we are performing xor over 0, which results in the original address.

All we need to do is shift the bytes back to obtain the base of the heap! 0x555555559 -> 0x555555559000. This is not always 100% true, if the chunk sits in a different page then that’s what we will retrieve. Nevertheless, doing the math to figure out where the chunk is compared to the beginning of the heap shouldn’t be too hard, if controlling the allocations.

With this information, preparing our own pointers is just a matter of keeping track where our chunk will live. Since we only care about the page address, we only need to worry about increasing the leak if our chunk is placed in a new page.

Following the example before, and for the sake of showing how it works, let’s say we want to write in the stack, at 0x7ffffffde100.

We can protect our fake pointer: 0x555555559 ^ 0x7ffffffde100 -> 0x7ffaaaa8b459. Now we write that into the FD:

0x555555559ae0	0x0000000000000000	0x0000000000000031	........1.......
0x555555559af0	0x00007ffaaaa8b459	0x990bee95285d21f8	Y........!](....	 <-- tcachebins[0x30][0/2]
0x555555559b00	0x0000000000000000	0x0000000000000000	................
0x555555559b10	0x0000000000000000	0x00000000000204f1	................	 <-- Top chunk
pwndbg> bins
tcachebins
0x30 [  2]: 0x555555559af0 —▸ 0x7ffffffde100 ◂— 0x7ffffffde

And as you can see, with pwndbg we can already check that it indeed points to 0x7ffffffde100. Otherwise, allocating one for the first on the list and another one finally for the one we are targetting will reveal that we indeed allocate a chunk in our target address.

As mentioned, fastbins will apply too:

0x555555559ab0	0x0000000000000000	0x0000000000000021	........!.......
0x555555559ac0	0x0000000555555559	0xc4d708284c02606d	YUUU....m`.L(...	 <-- tcachebins[0x20][1/2]
0x555555559ad0	0x0000000000000000	0x0000000000000021	........!.......
0x555555559ae0	0x000055500000cf99	0xc4d708284c02606d	....PU..m`.L(...	 <-- tcachebins[0x20][0/2]
0x555555559af0	0x0000000000000000	0x0000000000000021	........!.......	 <-- fastbins[0x20][0]
0x555555559b00	0x0000000555555559	0x0000000000000000	YUUU............
0x555555559b10	0x0000000000000000	0x00000000000204f1	................	 <-- Top chunk

Note that 0x0000000555555559 is both in fastbins list and tcachebins list, as it is 0 in both cases (and the page address is still the same)

If you noticed that in my program the last chunk went to fastbins instead of the tcache for 0x20 (since the max amount is 7), this is because you can influence tcache parameters at run time like this one, with GLIBC_TUNABLES (remember CVE-2023-4911? ;) For this particular case, I used: export GLIBC_TUNABLES=glibc.malloc.tcache_count=2

Now, what if we cannot retrieve the special 0 case? Do not worry, it is not much a problem either, as long as we know how far the address is from the actual pointer.

Let’s use a different case, now with ASLR enabled:

0x5ceaf258aac0	0x00000005ceaf258a	0x7045b85a9fba521d	.%.......R..Z.Ep	 <-- tcachebins[0x20][1/2]
0x5ceaf258aad0	0x0000000000000000	0x0000000000000021	........!.......
0x5ceaf258aae0	0x00005cef3cf78f4a	0x7045b85a9fba521d	J..<.\...R..Z.Ep	 <-- tcachebins[0x20][0/2]
0x5ceaf258aaf0	0x0000000000000000	0x0000000000020511	................	 <-- Top chunk

Let’s see if we can crack 0x00005cef3cf78f4a. If we know the offset, we know that the value (next chunk address) is the same as the address where the value is stored - 0x20 (0x5ceaf258aae0 - 0x20)

We know:

With this information, we can assume that, when we REVEAL_PTR the operation is as follows:

I will use letters to better ilustrate the concept

PTR_ADDRESS: 0xAAABBBCCCDDD
VALUE:       0xAAABBBCCCDEE

    |    AAABBBCCC|DDD
    | AAABBBCCCDEE|
XOR-|-------------|------
      5cef3cf78f4a

The trick here is, since we know the first one and a half bytes, we can decipher the next one and a half, as we can reuse the previous ones we know to decypher it:

AAA -> we know it is 0x5ce

AAA XOR BBB = 0xf3c => 0xf3c XOR AAA = BBB => 0xf3c XOR 0x5ce => 0xaf2

BBB XOR CCC = 0xf78 => 0xaf2 XOR BBB = CCC => 0xaf2 XOR 0xf78 => 0x58a

CCC XOR DEE = 0xf4a => 0x58a XOR CCC = DEE => 0x58a XOR 0xf4a => 0xac0

With 0x5ceaf258aac0 we would have enough, as we only need 0x5ceaf258a to obfuscate any further pointers we want. Either way, we can compute the original address by adding the known offset, 0x20.

A quick python script to prove the point, so you can try too:

def decipher_values(value: int):
    val_parts = [
        (value >> 36) & 0xFFF,
        (value >> 24) & 0xFFF,
        (value >> 12) & 0xFFF,
        value & 0xFFF
    ]
    
    # Decipher using known values
    part0 = val_parts[0]
    part1 = val_parts[1] ^ part0
    part2 = val_parts[2] ^ part1
    part3 = val_parts[3] ^ part2
    
    return (part0 << 36) + (part1 << 24) + (part2 << 12) + part3

# Example usage
result = decipher_values(0x00005cef3cf78f4a)

print(hex(result)) # => 0x5ceaf258aac0

General libc addresses

Pointer mangling is used to obfuscate dynamic pointers stored in memory that are used by some libc functions. This is handled in libc by the macros PTR_DEMANGLE and PTR_MANGLE. They were introduced a long time ago, commit 3467f5c369a10ef19c8df38fb282c7763f36d66f introduced the mangling operations for i386 and x86_64 on Dec 2005. Other commits followed up to use these macros roughly at the same time, like a3c88553729c1c4dcd4f893a96b4668bce640ee5 or 915a6c51c5d8127e87ef797ee23e04e4f92b4c4f.

This approach basically aims to defend those addresses that are initially written in memory by libc functions and will later be used again by other libc functions.

exit is a very good example of one of those functions. At the beginning, __on_exit is called to register a function + args to be called at the end of execution, when exit is called.

__on_exit will PTR_MANGLE the address of the function and exit (well, actually __run_exit_handlers) will PTR_DEMANGLE the function.

PTR_DEMANGLE/MANGLE are provided for x86_64 in sysdeps/unix/sysv/linux/x86_64/pointer_guard.h, in assembly, so the path (and code) will vary based on the architecture:

#  define PTR_MANGLE(reg)       xor %fs:POINTER_GUARD, reg;                   \
                                rol $2*LP_SIZE+1, reg
#  define PTR_DEMANGLE(reg)     ror $2*LP_SIZE+1, reg;                        \
                                xor %fs:POINTER_GUARD, reg

By disassembling __on_exit we can confirm PTR_MANGLE:

   0x00007ffff7c496c5 <+85>:	xor    rdi,QWORD PTR fs:0x30
   0x00007ffff7c496ce <+94>:	rol    rdi,0x11
   0x00007ffff7c496d2 <+98>:	mov    QWORD PTR [rax+0x8],rdi

and __run_exit_handlers shows PTR_DEMANGLE:

   0x00007ffff7c47ada <+154>:	ror    rax,0x11
   0x00007ffff7c47ade <+158>:	xor    rax,QWORD PTR fs:0x30
   ...
   0x00007ffff7c47af5 <+181>:	call   rax

Exploitation perspective

From an exploitation perspective, we need to know the key used for the xor operation. This value is a randomized value set in the Thread Control Block, especifically at offset 0x30 in x86_64.

Not that it matter from an exploitation perspective, but if you are curious, this value is originally set in __libc_start_main, as you can see here:

 // csu/libc-start.c #L299
 /* Set up the pointer guard value.  */
 uintptr_t pointer_chk_guard = _dl_setup_pointer_guard (_dl_random,
							 stack_chk_guard);
# ifdef THREAD_SET_POINTER_GUARD
 THREAD_SET_POINTER_GUARD (pointer_chk_guard);
# else
 __pointer_chk_guard_local = pointer_chk_guard;
# endif

Once we are able to obtain this value, we can write our own mangled pointers. Lucky for us, this area is also writable, which means we can also overwrite it:

pwndbg> xinfo $fs_base
Extended information for virtual address 0x7ffff7fa3740:

  Containing mapping:
    0x7ffff7fa3000     0x7ffff7fa6000 rw-p     3000      0 [anon_7ffff7fa3]

  Offset information:
         Mapped Area 0x7ffff7fa3740 = 0x7ffff7fa3000 + 0x740
pwndbg> 

So we have two options:

First option is probably the cleaneast one since that wouldn’t corrupt any other demangle operations that may happen before libc uses the pointer we want to overwrite.

In any case, let’s see an example with exit. First, lets see the value of the key used with the xor operation fs:0x30:

pwndbg> x/1gx $fs_base + 0x30
0x7ffff7fa3770:	0xd7e12bf4cba2d603

__exit_funcs are the list of functions we will be calling, you can see it on exit:

/*
*   __run_exit_handlers (int status, struct exit_function_list **listp,
		     bool run_list_atexit, bool run_dtors)
*/
  __run_exit_handlers (status, &__exit_funcs, true, true);

And the actual structure in gdb:

pwndbg> p *__exit_funcs
$3 = {
  next = 0x0,
  idx = 1,
  fns = {
      flavor = 4,
      func = {
        at = 0xa81678bd4487afc2,
        on = {
          fn = 0xa81678bd4487afc2,
          arg = 0x0
        },
        cxa = {
          fn = 0xa81678bd4487afc2,
          arg = 0x0,
          dso_handle = 0x0
        }
      }
    },
    ...

As you can see, currently registered target is 0xa81678bd4487afc2. Let’s perform manually the PTR_DEMANGLE and PTR_MANGLE operation:

def ror(value, bits, size=64):
    """ Rotate right (ROR) operation. 
        value: Integer to rotate
        bits: Number of bits to rotate by
        size: Bit size (default 64-bit for rax)
    """
    mask = (1 << size) - 1  # Create a mask for the given size
    return ((value >> bits) | (value << (size - bits))) & mask

def rol(value, bits, size=64):
    """ Rotate left (ROL) operation.
        value: Integer to rotate
        bits: Number of bits to rotate by
        size: Bit size (default 64-bit for rax)
    """
    mask = (1 << size) - 1  # Create a mask for the given size
    return ((value << bits) | (value >> (size - bits))) & mask

# Demangle operation
mangled = 0xa81678bd4487afc2
rax = mangled
rax = ror(rax, 0x11) 
demangled = rax ^ 0xd7e12bf4cba2d603
print(hex(demangled)) # -> 0x7ffff7fc7440

# Mangle operation
rax = demangled
rax = rax ^ 0xd7e12bf4cba2d603
rax = rol(rax, 0x11)
assert(rax == mangled)

We can check this on gdb and confirm it’s a valid address, and see that it matches _dl_fini:

pwndbg> x/1i 0x7ffff7fc7440
   0x7ffff7fc7440 <_dl_fini>:	endbr64

If you are curious about how _dl_fini ends up there, this happens through __libc_start_main -> __cxa_atexit -> __internal_atexit

Now for the sake of showing the concept, let’s point it to system:

# Mangle operation
rax = 0x7ffff7c5af30 # system addr
rax = rax ^ 0xd7e12bf4cba2d603
rax = rol(rax, 0x11)
print(hex(rax)) # => 0xa81678cef267afc2
  ...
        cxa = {
          fn = 0xa81678cef267afc2,
          arg = 0x555555559c80,
          dso_handle = 0x0
        }
  ...

being 0x555555559c80 an address were I wrote /bin/sh.

Let’s finish the program (so exit is called). In __run_exit_handlers, we can see that after performing the PTR_DEMANGLE operation, we see our system address:

*RAX 0x7ffff7c5af30 (system) ◂— endbr64

And finally we can see system is called with our string:

  0x7ffff7c47bef <__run_exit_handlers+431>    call   rax  <system>
        command: 0x555555559c80 ◂— 0x68732f6e69622f /* '/bin/sh' */
pwndbg> c
Continuing.
[Attaching after Thread 0x7ffff7fa3740 (LWP 10964) vfork to child process 12772]
[New inferior 2 (process 12772)]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[Detaching vfork parent process 10964 after child exec]
[Inferior 1 (process 10964) detached]
process 12772 is executing new program: /usr/bin/dash
...

Feel free to reach out if you spot any mistakes!

Happy hacking!