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

15 KiB
Raw Blame History

Unsorted Bin Attack

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

Basic Information

For more information about what is an unsorted bin check this page:

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

Unsorted lists are able to write the address to unsorted_chunks (av) in the bk address of the chunk. Therefore, if an attacker can modify the address of the bk pointer in a chunk inside the unsorted bin, he could be able to write that address in an arbitrary address which could be helpful to leak a Glibc addresses or bypass some defense.

So, basically, this attack allows to set a big number at an arbitrary address. This big number is an address, which could be a heap address or a Glibc address. A traditional target was global_max_fast to allow to create fast bin bins with bigger sizes (and pass from an unsorted bin attack to a fast bin attack).

  • Modern note (glibc ≥ 2.39): global_max_fast became an 8bit global. Blindly writing a pointer there via an unsorted-bin write will clobber adjacent libc data and will not reliably raise the fastbin limit anymore. Prefer other targets or other primitives when running against glibc 2.39+. See "Modern constraints" below and consider combining with other techniques like a large bin attack or a fast bin attack once you have a stable primitive.

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.

The code from guyinatuxedo explains it very well, although if you modify the mallocs to allocate memory big enough so don't end in a Tcache you can see that the previously mentioned error appears preventing this 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

• Якщо ви не можете обійти tcache за розміром, заповніть tcache bin для обраного розміру (7 frees) перед тим, як звільнити пошкоджений chunk, щоб free пішов у unsorted. • Якщо програма негайно аварійно завершується на наступному виділенні через перевірки unsorted-bin, ще раз перевірте, що victim->fd досі дорівнює голові bin і що ваш TARGET містить точний вказівник victim після першого запису.

Unsorted Bin Infoleak Attack

Насправді це доволі проста концепція. Чанки в unsorted bin будуть містити вказівники. Перший chunk в unsorted bin фактично має fd і bk посилання, які вказують на частину main arena (Glibc).
Отже, якщо ви можете помістити chunk в unsorted bin і прочитати його (use after free) або заново allocate його без перезапису принаймні одного з вказівників, щоб потім прочитати його, ви можете отримати Glibc info leak.

Схожий attack used in this writeup полягав у зловживанні структурою з 4 chunks (A, B, C і D — D потрібен лише, щоб запобігти консолідації з top chunk), тому null byte overflow в B використовувався, щоб змусити C вказувати, що B не використовується. Також в B дані prev_size були змінені так, що розмір замість розміру B став A+B.
Потім C було deallocated і консолідовано з A+B (але B залишався у використанні). Було виділено новий chunk розміру A, і в B записали libc leaked addresses, звідки вони були leaked.

Посилання та інші приклади

  • https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#hitcon-training-lab14-magic-heap

  • Мета — перезаписати глобальну змінну значенням більшим за 4869, щоб стати можливим отримання flag і PIE не увімкнено.

  • Можна згенерувати chunks довільних розмірів і існує heap overflow потрібного розміру.

  • Атака починається зі створення 3 chunks: chunk0 для зловживання overflow, chunk1 який буде overflowed і chunk2 щоб top chunk не консолідував попередні.

  • Потім chunk1 звільняють і chunk0 переповнюють до bk вказівника chunk1, який тепер вказує на: bk = magic - 0x10

  • Далі виділяють chunk3 того ж розміру, що й chunk1, що викликає unsorted bin attack і змінює значення глобальної змінної, що робить можливим отримання flag.

  • https://guyinatuxedo.github.io/31-unsortedbin_attack/0ctf16_zerostorage/index.html

  • Функція merge вразлива, тому що якщо передані обидва індекси однакові, вона зробить realloc на ньому, потім free, але поверне вказівник на ту freed область, яку можна використати.

  • Тому створюються 2 chunks: chunk0, який буде змержений сам з собою, і chunk1, щоб запобігти консолідації з top chunk. Потім викликають merge з chunk0 двічі, що викликає use after free.

  • Далі викликають функцію view з індексом 2 (індекс use after free chunk), яка leaks libc address.

  • Оскільки бінар має обмеження, що malloc дозволений лише на розміри більші за global_max_fast, тому fastbin не використовується, застосовується unsorted bin attack для перезапису глобальної змінної global_max_fast.

  • Потім можна викликати функцію edit з індексом 2 (вказівник use after free) і перезаписати bk вказівник, щоб він вказував на p64(global_max_fast-0x10). Створення нового chunk використає попередньо скомпрометовану freed адресу (0x20) і trigger the unsorted bin attack, перезаписавши global_max_fast на дуже велике значення, що дозволяє тепер створювати chunks у fast bins.

  • Тепер виконується fast bin attack:

  • По-перше встановлено, що можна працювати з fast chunks розміром 200 у локації __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

  • Якщо вдасться отримати fast chunk розміру 0x200 у цій локації, буде можливість перезаписати function pointer, який буде виконано.

  • Для цього створюють новий chunk розміру 0xfc і викликають merge з цим вказівником двічі, таким чином отримують вказівник на freed chunk розміру 0xfc*2 = 0x1f8 у fast bin.

  • Потім викликають edit для цього chunk, щоб змінити адресу fd fast bin і вказати її на попередній __free_hook.

  • Далі створюють chunk розміру 0x1f8, щоб отримати з fast bin попередній непотрібний chunk, після чого створюють ще один chunk розміру 0x1f8, щоб отримати fast bin chunk у __free_hook, який перезаписується адресою функції system.

  • І нарешті chunk, що містить рядок /bin/sh\x00, звільняється викликом delete, що trigger'ить __free_hook, який тепер вказує на system з параметром /bin/sh\x00.

  • CTF https://guyinatuxedo.github.io/33-custom_misc_heap/csaw19_traveller/index.html

  • Ще один приклад зловживання 1B overflow для консолідації chunks в unsorted bin і отримання libc infoleak, а потім виконання fast bin attack для перезапису malloc hook адресою one gadget.

  • Robot Factory. BlackHat MEA CTF 2022

  • Можна виділяти лише chunks розміром більше ніж 0x100.

  • Перезапис global_max_fast за допомогою Unsorted Bin attack (працює 1/16 разів через ASLR, бо потрібно модифікувати 12 біт, але треба змінити 16 біт).

  • Fast Bin attack для зміни глобального масиву chunks. Це дає довільні read/write примітиви, що дозволяє змінювати GOT і вказати деякі функції на system.

Посилання