hacktricks/src/binary-exploitation/libc-heap/unsorted-bin-attack.md

14 KiB
Raw Blame History

Unsorted Bin Attack

{{#include ../../banners/hacktricks-training.md}}

Basic Information

Pour plus d'informations sur ce qu'est un unsorted bin, consultez cette page :

{{#ref}} bins-and-memory-allocations.md {{#endref}}

Unsorted lists sont capables d'écrire l'adresse de unsorted_chunks (av) dans le champ bk du chunk. Par conséquent, si un attaquant peut modifier l'adresse du pointeur bk dans un chunk à l'intérieur de l'unsorted bin, il pourrait être capable d'écrire cette adresse à une adresse arbitraire, ce qui peut être utile pour leak des adresses Glibc ou contourner certaines protections.

Donc, essentiellement, cette attaque permet de poser un grand nombre à une adresse arbitraire. Ce grand nombre est une adresse, qui peut être une adresse du heap ou une adresse Glibc. Une cible traditionnelle était global_max_fast pour permettre de créer des fast bin avec des tailles plus grandes (et passer d'un unsorted bin attack à un fast bin attack).

  • Modern note (glibc ≥ 2.39): global_max_fast est devenu un global 8 bits. Écrire aveuglément un pointeur là via une écriture unsorted-bin corrompra les données libc adjacentes et ne relèvera plus de manière fiable la limite des fastbin. Préférez d'autres cibles ou d'autres primitives contre glibc 2.39+. Voir "Modern constraints" cidessous et envisagez de combiner avec d'autres techniques comme un large bin attack ou un fast bin attack une fois que vous avez une primitive stable.

Tip

T> aking a look to the example provided in https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#principle and using 0x4000 and 0x5000 instead of 0x400 and 0x500 as chunk sizes (to avoid Tcache) it's possible to see that nowadays the error malloc(): unsorted double linked list corrupted is triggered.

Therefore, this unsorted bin attack now (among other checks) also requires to be able to fix the doubled linked list so this is bypassed victim->bk->fd == victim or not victim->fd == av (arena), which means that the address where we want to write must have the address of the fake chunk in its fd position and that the fake chunk fd is pointing to the arena.

Caution

Note that this attack corrupts the unsorted bin (hence small and large too). So we can only use allocations from the fast bin now (a more complex program might do other allocations and crash), and to trigger this we must allocate the same size or the program will crash.

Note that overwriting global_max_fast might help in this case trusting that the fast bin will be able to take care of all the other allocations until the exploit is completed.

Le code de guyinatuxedo l'explique très bien, bien que si vous modifiez les mallocs pour allouer assez grand afin de ne pas finir dans une Tcache vous pouvez voir que l'erreur précédemment mentionnée apparaît empêchant cette technique : malloc(): unsorted double linked list corrupted

How the write actually happens

  • The unsorted-bin write is triggered on free when the freed chunk is inserted at the head of the unsorted list.
  • During insertion, the allocator performs bck = unsorted_chunks(av); fwd = bck->fd; victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;
  • If you can set victim->bk to (mchunkptr)(TARGET - 0x10) before calling free(victim), the final statement will perform the write: *(TARGET) = victim.
  • Later, when the allocator processes the unsorted bin, integrity checks will verify (among other things) that bck->fd == victim and victim->fd == unsorted_chunks(av) before unlinking. Because the insertion already wrote victim into bck->fd (our TARGET), these checks can be satisfied if the write succeeded.

Modern constraints (glibc ≥ 2.33)

To use unsortedbin writes reliably on current glibc:

  • Tcache interference: for sizes that fall into tcache, frees are diverted there and wont touch the unsorted bin. Either
  • make requests with sizes > MAX_TCACHE_SIZE (≥ 0x410 on 64bit by default), or
  • fill the corresponding tcache bin (7 entries) so that additional frees reach the global bins, or
  • if the environment is controllable, disable tcache (e.g., GLIBC_TUNABLES glibc.malloc.tcache_count=0).
  • Integrity checks on the unsorted list: on the next allocation path that examines the unsorted bin, glibc checks (simplified):
  • bck->fd == victim and victim->fd == unsorted_chunks(av); otherwise it aborts with malloc(): unsorted double linked list corrupted.
  • This means the address you target must tolerate two writes: first *(TARGET) = victim at freetime; later, as the chunk is removed, *(TARGET) = unsorted_chunks(av) (the allocator rewrites bck->fd back to the bin head). Choose targets where simply forcing a large nonzero value is useful.
  • Typical stable targets in modern exploits
  • Application or global state that treats "large" values as flags/limits.
  • Indirect primitives (e.g., set up for a subsequent fast bin attack or to pivot a later writewhatwhere).
  • Avoid __malloc_hook/__free_hook on new glibc: they were removed in 2.34. Avoid global_max_fast on ≥ 2.39 (see next note).
  • About global_max_fast on recent glibc
  • On glibc 2.39+, global_max_fast is an 8bit global. The classic trick of writing a heap pointer into it (to enlarge fastbins) no longer works cleanly and is likely to corrupt adjacent allocator state. Prefer other strategies.

Minimal exploitation recipe (modern glibc)

Goal: achieve a single arbitrary write of a heap pointer to an arbitrary address using the unsortedbin insertion primitive, without crashing.

  • Layout/grooming
  • Allocate A, B, C with sizes large enough to bypass tcache (e.g., 0x5000). C prevents consolidation with the top chunk.
  • Corruption
  • Overflow from A into Bs chunk header to set B->bk = (mchunkptr)(TARGET - 0x10).
  • Trigger
  • free(B). At insertion time the allocator executes bck->fd = B, therefore *(TARGET) = B.
  • Continuation
  • If you plan to continue allocating and the program uses the unsorted bin, expect the allocator to later set *(TARGET) = unsorted_chunks(av). Both values are typically large and may be enough to change size/limit semantics in targets that only check for "big".

Pseudocode skeleton:

// 64-bit glibc 2.352.38 style layout (tcache bypass via large sizes)
void *A = malloc(0x5000);
void *B = malloc(0x5000);
void *C = malloc(0x5000); // guard

// overflow from A into Bs metadata (prev_size/size/.../bk). You must control B->bk.
*(size_t *)((char*)B - 0x8) = (size_t)(TARGET - 0x10); // write fake bk

free(B); // triggers *(TARGET) = B (unsorted-bin insertion write)

Note

• Si vous ne pouvez pas bypasser tcache avec la taille, remplissez le tcache bin pour la taille choisie (7 frees) avant de free le chunk corrompu afin que le free aille dans unsorted. • Si le programme abort immédiatement sur la prochaine allocation à cause des checks unsorted-bin, re-vérifiez que victim->fd vaut toujours la head du bin et que votre TARGET contient le pointeur exact victim après le premier write.

Unsorted Bin Infoleak Attack

C'est en réalité un concept très basique. Les chunks dans l'unsorted bin vont contenir des pointeurs. Le premier chunk dans l'unsorted bin aura en fait les liens fd et bk pointant vers une partie du main arena (Glibc).
Donc, si vous pouvez mettre un chunk dans un unsorted bin et le lire (use after free) ou le réallouer sans écraser au moins 1 des pointeurs puis le lire, vous pouvez obtenir un Glibc info leak.

Une attaque similaire attack used in this writeup utilisait une structure de 4 chunks (A, B, C et D - D est juste pour empêcher la consolidation avec le top chunk) : un null byte overflow dans B était utilisé pour faire indiquer à C que B était unused. Aussi, dans B le champ prev_size a été modifié pour que la taille au lieu d'être la taille de B soit A+B.
Ensuite C a été deallocated, et consolidé avec A+B (mais B était toujours in use). Un nouveau chunk de taille A a été alloué puis les adresses libc leakées ont été écrites dans B d'où elles ont été leakées.

Références & Autres exemples

  • https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#hitcon-training-lab14-magic-heap
  • L'objectif est d'overwriter une variable globale avec une valeur supérieure à 4869 pour pouvoir récupérer le flag et PIE n'est pas activé.
  • Il est possible de générer des chunks de tailles arbitraires et il y a un heap overflow avec la taille désirée.
  • L'attaque commence en créant 3 chunks : chunk0 pour abuser de l'overflow, chunk1 pour être overflowed et chunk2 pour que le top chunk ne consolide pas les précédents.
  • Ensuite, chunk1 est freed et chunk0 est overflowed pour que le bk pointer de chunk1 pointe vers : bk = magic - 0x10
  • Puis, chunk3 est alloué avec la même taille que chunk1, ce qui déclenchera l'unsorted bin attack et modifiera la valeur de la variable globale, permettant d'obtenir le flag.
  • https://guyinatuxedo.github.io/31-unsortedbin_attack/0ctf16_zerostorage/index.html
  • La fonction merge est vulnérable parce que si les deux indexes passés sont identiques elle fera un realloc dessus puis le free mais retournera un pointeur vers cette région freed qui peut être utilisée.
  • Donc, 2 chunks sont créés : chunk0 qui sera merged avec lui-même et chunk1 pour empêcher la consolidation avec le top chunk. Ensuite, la fonction merge est appelée avec chunk0 deux fois ce qui provoque un use after free.
  • Ensuite, la fonction view est appelée avec l'index 2 (qui est l'index du chunk use after free), ce qui va leak une adresse libc.
  • Comme le binaire a des protections qui n'autorisent que des malloc de tailles supérieures à global_max_fast donc aucun fastbin n'est utilisé, une unsorted bin attack sera utilisée pour overwriter la variable globale global_max_fast.
  • Ensuite, il est possible d'appeler la fonction edit avec l'index 2 (le pointeur use after free) et d'overwrite le pointeur bk pour qu'il pointe vers p64(global_max_fast-0x10). Puis, créer un nouveau chunk utilisera l'adresse freed compromise précédemment (0x20) et déclenchera l'unsorted bin attack écrasant global_max_fast avec une très grande valeur, permettant maintenant de créer des chunks dans les fast bins.
  • Maintenant une fast bin attack est réalisée :
  • Tout d'abord on découvre qu'il est possible de travailler avec des fast chunks de taille 200 à l'emplacement de __free_hook :
  • gef➤  p &__free_hook
    

$1 = (void (**)(void *, const void *)) 0x7ff1e9e607a8 <__free_hook> gef➤ x/60gx 0x7ff1e9e607a8 - 0x59 0x7ff1e9e6074f: 0x0000000000000000 0x0000000000000200 0x7ff1e9e6075f: 0x0000000000000000 0x0000000000000000 0x7ff1e9e6076f <list_all_lock+15>: 0x0000000000000000 0x0000000000000000 0x7ff1e9e6077f <_IO_stdfile_2_lock+15>: 0x0000000000000000 0x0000000000000000

  • Si on parvient à obtenir un fast chunk de taille 0x200 à cet emplacement, il sera possible d'overwrite un pointeur de fonction qui sera exécuté.
  • Pour cela, un nouveau chunk de taille 0xfc est créé et la fonction merged est appelée avec ce pointeur deux fois, ainsi on obtient un pointeur vers un chunk freed de taille 0xfc*2 = 0x1f8 dans le fast bin.
  • Ensuite, la fonction edit est appelée sur ce chunk pour modifier l'adresse fd de ce fast bin pour qu'elle pointe vers l'emplacement de __free_hook.
  • Puis, un chunk de taille 0x1f8 est créé pour récupérer du fast bin le chunk précédent inutile, puis un autre chunk de taille 0x1f8 est créé pour obtenir un fast bin chunk dans __free_hook qui est overwrité avec l'adresse de la fonction system.
  • Et enfin un chunk contenant la string /bin/sh\x00 est freed en appelant la fonction delete, déclenchant __free_hook qui pointe vers system avec /bin/sh\x00 comme paramètre.
  • CTF https://guyinatuxedo.github.io/33-custom_misc_heap/csaw19_traveller/index.html
  • Un autre exemple d'abus d'un overflow 1B pour consolider des chunks dans l'unsorted bin et obtenir un libc infoleak puis effectuer une fast bin attack pour overwrite malloc hook avec une one gadget address
  • Robot Factory. BlackHat MEA CTF 2022
  • On ne peut allouer que des chunks de taille plus grande que 0x100.
  • Overwrite global_max_fast en utilisant une Unsorted Bin attack (fonctionne 1/16 fois à cause de l'ASLR, car il faut modifier 12 bits, mais on doit modifier 16 bits).
  • Fast Bin attack pour modifier un tableau global de chunks. Cela donne un primitive arbitrary read/write, ce qui permet de modifier la GOT et de faire pointer une fonction vers system.

Références