mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
601 lines
44 KiB
Markdown
601 lines
44 KiB
Markdown
# Bins & Memory Allocations
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|
||
|
||
## Basic Information
|
||
|
||
Για να βελτιωθεί η αποδοτικότητα της αποθήκευσης των chunks, κάθε chunk δεν είναι απλώς σε μία συνδεδεμένη λίστα, αλλά υπάρχουν διάφοροι τύποι. Αυτοί είναι οι bins και υπάρχουν 5 τύποι bins: [62](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l1407) μικροί bins, 63 μεγάλοι bins, 1 αταξινόμητος bin, 10 γρήγοροι bins και 64 tcache bins ανά νήμα.
|
||
|
||
Η αρχική διεύθυνση για κάθε αταξινόμητο, μικρό και μεγάλο bin είναι μέσα στην ίδια σειρά. Ο δείκτης 0 είναι ανενεργός, 1 είναι ο αταξινόμητος bin, οι bins 2-64 είναι μικροί bins και οι bins 65-127 είναι μεγάλοι bins.
|
||
|
||
### Tcache (Per-Thread Cache) Bins
|
||
|
||
Ακόμα και αν τα νήματα προσπαθούν να έχουν το δικό τους heap (βλ. [Arenas](bins-and-memory-allocations.md#arenas) και [Subheaps](bins-and-memory-allocations.md#subheaps)), υπάρχει η πιθανότητα μια διαδικασία με πολλά νήματα (όπως ένας web server) **να μοιραστεί το heap με άλλα νήματα**. Σε αυτή την περίπτωση, η κύρια λύση είναι η χρήση **locker**, που μπορεί να **επιβραδύνει σημαντικά τα νήματα**.
|
||
|
||
Επομένως, ένα tcache είναι παρόμοιο με έναν γρήγορο bin ανά νήμα με τον τρόπο που είναι μια **μοναδική συνδεδεμένη λίστα** που δεν συγχωνεύει chunks. Κάθε νήμα έχει **64 απλά συνδεδεμένα tcache bins**. Κάθε bin μπορεί να έχει μέγιστο [7 chunks ίδιου μεγέθους](https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=2527e2504761744df2bdb1abdc02d936ff907ad2;hb=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc#l323) που κυμαίνονται από [24 έως 1032B σε συστήματα 64-bit και 12 έως 516B σε συστήματα 32-bit](https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=2527e2504761744df2bdb1abdc02d936ff907ad2;hb=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc#l315).
|
||
|
||
**Όταν ένα νήμα απελευθερώνει** ένα chunk, **αν δεν είναι πολύ μεγάλο** για να κατανεμηθεί στο tcache και ο αντίστοιχος tcache bin **δεν είναι γεμάτος** (ήδη 7 chunks), **θα κατανεμηθεί εκεί**. Αν δεν μπορεί να πάει στο tcache, θα χρειαστεί να περιμένει για το κλείδωμα του heap για να μπορέσει να εκτελέσει την παγκόσμια λειτουργία απελευθέρωσης.
|
||
|
||
Όταν ένα **chunk κατανεμηθεί**, αν υπάρχει ένα ελεύθερο chunk του απαιτούμενου μεγέθους στο **Tcache θα το χρησιμοποιήσει**, αν όχι, θα χρειαστεί να περιμένει για το κλείδωμα του heap για να μπορέσει να βρει ένα στους παγκόσμιους bins ή να δημιουργήσει ένα νέο.\
|
||
Υπάρχει επίσης μια βελτιστοποίηση, σε αυτή την περίπτωση, ενώ έχει το κλείδωμα του heap, το νήμα **θα γεμίσει το Tcache του με chunks heap (7) του ζητούμενου μεγέθους**, έτσι σε περίπτωση που χρειαστεί περισσότερα, θα τα βρει στο Tcache.
|
||
|
||
<details>
|
||
|
||
<summary>Προσθήκη παραδείγματος chunk tcache</summary>
|
||
```c
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *chunk;
|
||
chunk = malloc(24);
|
||
printf("Address of the chunk: %p\n", (void *)chunk);
|
||
gets(chunk);
|
||
free(chunk);
|
||
return 0;
|
||
}
|
||
```
|
||
Συγκεντρώστε το και αποσφαλματώστε το με ένα σημείο διακοπής στον κωδικό op της συνάρτησης main. Στη συνέχεια, με το gef μπορείτε να δείτε το tcache bin σε χρήση:
|
||
```bash
|
||
gef➤ heap bins
|
||
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
|
||
Tcachebins[idx=0, size=0x20, count=1] ← Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
```
|
||
</details>
|
||
|
||
#### Δομές & Συναρτήσεις Tcache
|
||
|
||
Στον παρακάτω κώδικα είναι δυνατόν να δούμε τα **max bins** και **chunks per index**, τη δομή **`tcache_entry`** που δημιουργήθηκε για να αποφεύγονται οι διπλές απελευθερώσεις και τη **`tcache_perthread_struct`**, μια δομή που χρησιμοποιεί κάθε νήμα για να αποθηκεύει τις διευθύνσεις σε κάθε δείκτη του bin.
|
||
|
||
<details>
|
||
|
||
<summary><code>tcache_entry</code> και <code>tcache_perthread_struct</code></summary>
|
||
```c
|
||
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c
|
||
|
||
/* We want 64 entries. This is an arbitrary limit, which tunables can reduce. */
|
||
# define TCACHE_MAX_BINS 64
|
||
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1)
|
||
|
||
/* Only used to pre-fill the tunables. */
|
||
# define tidx2usize(idx) (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)
|
||
|
||
/* When "x" is from chunksize(). */
|
||
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
|
||
/* When "x" is a user-provided size. */
|
||
# define usize2tidx(x) csize2tidx (request2size (x))
|
||
|
||
/* With rounding and alignment, the bins are...
|
||
idx 0 bytes 0..24 (64-bit) or 0..12 (32-bit)
|
||
idx 1 bytes 25..40 or 13..20
|
||
idx 2 bytes 41..56 or 21..28
|
||
etc. */
|
||
|
||
/* This is another arbitrary limit, which tunables can change. Each
|
||
tcache bin will hold at most this number of chunks. */
|
||
# define TCACHE_FILL_COUNT 7
|
||
|
||
/* Maximum chunks in tcache bins for tunables. This value must fit the range
|
||
of tcache->counts[] entries, else they may overflow. */
|
||
# define MAX_TCACHE_COUNT UINT16_MAX
|
||
|
||
[...]
|
||
|
||
typedef struct tcache_entry
|
||
{
|
||
struct tcache_entry *next;
|
||
/* This field exists to detect double frees. */
|
||
uintptr_t key;
|
||
} tcache_entry;
|
||
|
||
/* There is one of these for each thread, which contains the
|
||
per-thread cache (hence "tcache_perthread_struct"). Keeping
|
||
overall size low is mildly important. Note that COUNTS and ENTRIES
|
||
are redundant (we could have just counted the linked list each
|
||
time), this is for performance reasons. */
|
||
typedef struct tcache_perthread_struct
|
||
{
|
||
uint16_t counts[TCACHE_MAX_BINS];
|
||
tcache_entry *entries[TCACHE_MAX_BINS];
|
||
} tcache_perthread_struct;
|
||
```
|
||
</details>
|
||
|
||
Η συνάρτηση `__tcache_init` είναι η συνάρτηση που δημιουργεί και εκχωρεί τον χώρο για το αντικείμενο `tcache_perthread_struct`
|
||
|
||
<details>
|
||
|
||
<summary>κώδικας tcache_init</summary>
|
||
```c
|
||
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L3241C1-L3274C2
|
||
|
||
static void
|
||
tcache_init(void)
|
||
{
|
||
mstate ar_ptr;
|
||
void *victim = 0;
|
||
const size_t bytes = sizeof (tcache_perthread_struct);
|
||
|
||
if (tcache_shutting_down)
|
||
return;
|
||
|
||
arena_get (ar_ptr, bytes);
|
||
victim = _int_malloc (ar_ptr, bytes);
|
||
if (!victim && ar_ptr != NULL)
|
||
{
|
||
ar_ptr = arena_get_retry (ar_ptr, bytes);
|
||
victim = _int_malloc (ar_ptr, bytes);
|
||
}
|
||
|
||
|
||
if (ar_ptr != NULL)
|
||
__libc_lock_unlock (ar_ptr->mutex);
|
||
|
||
/* In a low memory situation, we may not be able to allocate memory
|
||
- in which case, we just keep trying later. However, we
|
||
typically do this very early, so either there is sufficient
|
||
memory, or there isn't enough memory to do non-trivial
|
||
allocations anyway. */
|
||
if (victim)
|
||
{
|
||
tcache = (tcache_perthread_struct *) victim;
|
||
memset (tcache, 0, sizeof (tcache_perthread_struct));
|
||
}
|
||
|
||
}
|
||
```
|
||
</details>
|
||
|
||
#### Δείκτες Tcache
|
||
|
||
Το tcache έχει αρκετούς κάδους ανάλογα με το μέγεθος και τους αρχικούς δείκτες στο **πρώτο κομμάτι κάθε δείκτη και την ποσότητα κομματιών ανά δείκτη που βρίσκονται μέσα σε ένα κομμάτι**. Αυτό σημαίνει ότι εντοπίζοντας το κομμάτι με αυτές τις πληροφορίες (συνήθως το πρώτο), είναι δυνατόν να βρεθούν όλα τα αρχικά σημεία tcache και η ποσότητα των κομματιών Tcache.
|
||
|
||
### Γρήγοροι κάδοι
|
||
|
||
Οι γρήγοροι κάδοι έχουν σχεδιαστεί για να **ταχύτητα την κατανομή μνήμης για μικρά κομμάτια** διατηρώντας πρόσφατα απελευθερωμένα κομμάτια σε μια δομή γρήγορης πρόσβασης. Αυτοί οι κάδοι χρησιμοποιούν μια προσέγγιση Last-In, First-Out (LIFO), που σημαίνει ότι το **πιο πρόσφατα απελευθερωμένο κομμάτι είναι το πρώτο** που θα ξαναχρησιμοποιηθεί όταν υπάρχει ένα νέο αίτημα κατανομής. Αυτή η συμπεριφορά είναι ευνοϊκή για την ταχύτητα, καθώς είναι πιο γρήγορο να εισάγεις και να αφαιρείς από την κορυφή μιας στοίβας (LIFO) σε σύγκριση με μια ουρά (FIFO).
|
||
|
||
Επιπλέον, **οι γρήγοροι κάδοι χρησιμοποιούν απλές συνδεδεμένες λίστες**, όχι διπλά συνδεδεμένες, γεγονός που βελτιώνει περαιτέρω την ταχύτητα. Δεδομένου ότι τα κομμάτια στους γρήγορους κάδους δεν συγχωνεύονται με γείτονες, δεν υπάρχει ανάγκη για μια περίπλοκη δομή που να επιτρέπει την αφαίρεση από τη μέση. Μια απλή συνδεδεμένη λίστα είναι πιο απλή και γρήγορη για αυτές τις λειτουργίες.
|
||
|
||
Βασικά, αυτό που συμβαίνει εδώ είναι ότι η κεφαλίδα (ο δείκτης στο πρώτο κομμάτι που θα ελεγχθεί) δείχνει πάντα στο πιο πρόσφατα απελευθερωμένο κομμάτι αυτού του μεγέθους. Έτσι:
|
||
|
||
- Όταν ένα νέο κομμάτι κατανεμηθεί αυτού του μεγέθους, η κεφαλίδα δείχνει σε ένα ελεύθερο κομμάτι προς χρήση. Καθώς αυτό το ελεύθερο κομμάτι δείχνει στο επόμενο προς χρήση, αυτή η διεύθυνση αποθηκεύεται στην κεφαλίδα ώστε η επόμενη κατανομή να γνωρίζει πού να βρει ένα διαθέσιμο κομμάτι
|
||
- Όταν ένα κομμάτι απελευθερωθεί, το ελεύθερο κομμάτι θα αποθηκεύσει τη διεύθυνση του τρέχοντος διαθέσιμου κομματιού και η διεύθυνση αυτού του νέου απελευθερωμένου κομματιού θα τοποθετηθεί στην κεφαλίδα
|
||
|
||
Το μέγιστο μέγεθος μιας συνδεδεμένης λίστας είναι `0x80` και οργανώνονται έτσι ώστε ένα κομμάτι μεγέθους `0x20` να βρίσκεται στον δείκτη `0`, ένα κομμάτι μεγέθους `0x30` να βρίσκεται στον δείκτη `1`...
|
||
|
||
> [!CAUTION]
|
||
> Τα κομμάτια στους γρήγορους κάδους δεν ορίζονται ως διαθέσιμα, οπότε διατηρούνται ως κομμάτια γρήγορου κάδου για κάποιο χρονικό διάστημα αντί να μπορούν να συγχωνευτούν με άλλα ελεύθερα κομμάτια που τα περιβάλλουν.
|
||
```c
|
||
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
|
||
|
||
/*
|
||
Fastbins
|
||
|
||
An array of lists holding recently freed small chunks. Fastbins
|
||
are not doubly linked. It is faster to single-link them, and
|
||
since chunks are never removed from the middles of these lists,
|
||
double linking is not necessary. Also, unlike regular bins, they
|
||
are not even processed in FIFO order (they use faster LIFO) since
|
||
ordering doesn't much matter in the transient contexts in which
|
||
fastbins are normally used.
|
||
|
||
Chunks in fastbins keep their inuse bit set, so they cannot
|
||
be consolidated with other free chunks. malloc_consolidate
|
||
releases all chunks in fastbins and consolidates them with
|
||
other free chunks.
|
||
*/
|
||
|
||
typedef struct malloc_chunk *mfastbinptr;
|
||
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])
|
||
|
||
/* offset 2 to use otherwise unindexable first 2 bins */
|
||
#define fastbin_index(sz) \
|
||
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
|
||
|
||
|
||
/* The maximum fastbin request size we support */
|
||
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
|
||
|
||
#define NFASTBINS (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)
|
||
```
|
||
<details>
|
||
|
||
<summary>Προσθήκη παραδείγματος fastbin chunk</summary>
|
||
```c
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *chunks[8];
|
||
int i;
|
||
|
||
// Loop to allocate memory 8 times
|
||
for (i = 0; i < 8; i++) {
|
||
chunks[i] = malloc(24);
|
||
if (chunks[i] == NULL) { // Check if malloc failed
|
||
fprintf(stderr, "Memory allocation failed at iteration %d\n", i);
|
||
return 1;
|
||
}
|
||
printf("Address of chunk %d: %p\n", i, (void *)chunks[i]);
|
||
}
|
||
|
||
// Loop to free the allocated memory
|
||
for (i = 0; i < 8; i++) {
|
||
free(chunks[i]);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
Σημειώστε πώς κατανέμουμε και απελευθερώνουμε 8 κομμάτια του ίδιου μεγέθους ώστε να γεμίσουν το tcache και το όγδοο αποθηκεύεται στο γρήγορο κομμάτι.
|
||
|
||
Συγκεντρώστε το και αποσφαλματώστε το με ένα breakpoint στον `ret` opcode από τη συνάρτηση `main`. Στη συνέχεια, με το `gef` μπορείτε να δείτε ότι το tcache bin είναι γεμάτο και ένα κομμάτι είναι στο γρήγορο bin:
|
||
```bash
|
||
gef➤ heap bins
|
||
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
|
||
Tcachebins[idx=0, size=0x20, count=7] ← Chunk(addr=0xaaaaaaac1770, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1750, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1730, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1710, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac16f0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac16d0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
|
||
Fastbins[idx=0, size=0x20] ← Chunk(addr=0xaaaaaaac1790, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
Fastbins[idx=1, size=0x30] 0x00
|
||
```
|
||
</details>
|
||
|
||
### Αταξινόμητος κάδος
|
||
|
||
Ο αταξινόμητος κάδος είναι μια **κρυφή μνήμη** που χρησιμοποιείται από τον διαχειριστή σωρού για να επιταχύνει την κατανομή μνήμης. Να πώς λειτουργεί: Όταν ένα πρόγραμμα απελευθερώνει ένα κομμάτι, και αν αυτό το κομμάτι δεν μπορεί να κατανεμηθεί σε tcache ή γρήγορο κάδο και δεν συγκρούεται με το κορυφαίο κομμάτι, ο διαχειριστής σωρού δεν το τοποθετεί αμέσως σε έναν συγκεκριμένο μικρό ή μεγάλο κάδο. Αντίθετα, πρώτα προσπαθεί να **συγχωνεύσει το με οποιαδήποτε γειτονικά ελεύθερα κομμάτια** για να δημιουργήσει ένα μεγαλύτερο μπλοκ ελεύθερης μνήμης. Στη συνέχεια, το τοποθετεί σε έναν γενικό κάδο που ονομάζεται "αταξινόμητος κάδος."
|
||
|
||
Όταν ένα πρόγραμμα **ζητά μνήμη**, ο διαχειριστής σωρού **ελέγχει τον αταξινόμητο κάδο** για να δει αν υπάρχει ένα κομμάτι επαρκούς μεγέθους. Αν βρει ένα, το χρησιμοποιεί αμέσως. Αν δεν βρει ένα κατάλληλο κομμάτι στον αταξινόμητο κάδο, μεταφέρει όλα τα κομμάτια σε αυτή τη λίστα στους αντίστοιχους κάδους τους, είτε μικρούς είτε μεγάλους, με βάση το μέγεθός τους.
|
||
|
||
Σημειώστε ότι αν ένα μεγαλύτερο κομμάτι χωριστεί σε 2 μισά και το υπόλοιπο είναι μεγαλύτερο από το MINSIZE, θα τοποθετηθεί ξανά στον αταξινόμητο κάδο.
|
||
|
||
Έτσι, ο αταξινόμητος κάδος είναι ένας τρόπος για να επιταχυνθεί η κατανομή μνήμης επαναχρησιμοποιώντας γρήγορα πρόσφατα απελευθερωμένη μνήμη και μειώνοντας την ανάγκη για χρονοβόρες αναζητήσεις και συγχωνεύσεις.
|
||
|
||
> [!CAUTION]
|
||
> Σημειώστε ότι ακόμη και αν τα κομμάτια είναι διαφορετικών κατηγοριών, αν ένα διαθέσιμο κομμάτι συγκρούεται με ένα άλλο διαθέσιμο κομμάτι (ακόμη και αν ανήκουν αρχικά σε διαφορετικούς κάδους), θα συγχωνευτούν.
|
||
|
||
<details>
|
||
|
||
<summary>Προσθήκη παραδείγματος αταξινόμητου κομματιού</summary>
|
||
```c
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *chunks[9];
|
||
int i;
|
||
|
||
// Loop to allocate memory 8 times
|
||
for (i = 0; i < 9; i++) {
|
||
chunks[i] = malloc(0x100);
|
||
if (chunks[i] == NULL) { // Check if malloc failed
|
||
fprintf(stderr, "Memory allocation failed at iteration %d\n", i);
|
||
return 1;
|
||
}
|
||
printf("Address of chunk %d: %p\n", i, (void *)chunks[i]);
|
||
}
|
||
|
||
// Loop to free the allocated memory
|
||
for (i = 0; i < 8; i++) {
|
||
free(chunks[i]);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
Σημειώστε πώς κατανέμουμε και απελευθερώνουμε 9 κομμάτια του ίδιου μεγέθους ώστε να **γεμίσουν το tcache** και το όγδοο αποθηκεύεται στο unsorted bin επειδή είναι **πολύ μεγάλο για το fastbin** και το ένατο δεν έχει απελευθερωθεί, οπότε το ένατο και το όγδοο **δεν συγχωνεύονται με το κορυφαίο κομμάτι**.
|
||
|
||
Συγκεντρώστε το και αποσφαλματώστε το με ένα breakpoint στον `ret` opcode από τη συνάρτηση `main`. Στη συνέχεια, με το `gef` μπορείτε να δείτε ότι το tcache bin είναι γεμάτο και ένα κομμάτι είναι στο unsorted bin:
|
||
```bash
|
||
gef➤ heap bins
|
||
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
|
||
Tcachebins[idx=15, size=0x110, count=7] ← Chunk(addr=0xaaaaaaac1d10, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1c00, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1af0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac19e0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac18d0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac17c0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac12a0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
|
||
Fastbins[idx=0, size=0x20] 0x00
|
||
Fastbins[idx=1, size=0x30] 0x00
|
||
Fastbins[idx=2, size=0x40] 0x00
|
||
Fastbins[idx=3, size=0x50] 0x00
|
||
Fastbins[idx=4, size=0x60] 0x00
|
||
Fastbins[idx=5, size=0x70] 0x00
|
||
Fastbins[idx=6, size=0x80] 0x00
|
||
─────────────────────────────────────────────────────────────────────── Unsorted Bin for arena at 0xfffff7f90b00 ───────────────────────────────────────────────────────────────────────
|
||
[+] unsorted_bins[0]: fw=0xaaaaaaac1e10, bk=0xaaaaaaac1e10
|
||
→ Chunk(addr=0xaaaaaaac1e20, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
[+] Found 1 chunks in unsorted bin.
|
||
```
|
||
</details>
|
||
|
||
### Μικρές Θήκες
|
||
|
||
Οι μικρές θήκες είναι ταχύτερες από τις μεγάλες θήκες αλλά πιο αργές από τις γρήγορες θήκες.
|
||
|
||
Κάθε θήκη από τις 62 θα έχει **κομμάτια του ίδιου μεγέθους**: 16, 24, ... (με μέγιστο μέγεθος 504 bytes σε 32 bits και 1024 σε 64 bits). Αυτό βοηθά στην ταχύτητα εύρεσης της θήκης όπου θα πρέπει να γίνει η κατανομή χώρου και στην εισαγωγή και αφαίρεση καταχωρήσεων σε αυτές τις λίστες.
|
||
|
||
Αυτός είναι ο τρόπος υπολογισμού του μεγέθους της μικρής θήκης σύμφωνα με τον δείκτη της θήκης:
|
||
|
||
- Μικρότερο μέγεθος: 2\*4\*δείκτης (π.χ. δείκτης 5 -> 40)
|
||
- Μεγαλύτερο μέγεθος: 2\*8\*δείκτης (π.χ. δείκτης 5 -> 80)
|
||
```c
|
||
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
|
||
#define NSMALLBINS 64
|
||
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
|
||
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > CHUNK_HDR_SZ)
|
||
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)
|
||
|
||
#define in_smallbin_range(sz) \
|
||
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)
|
||
|
||
#define smallbin_index(sz) \
|
||
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
|
||
+ SMALLBIN_CORRECTION)
|
||
```
|
||
Συνάρτηση για την επιλογή μεταξύ μικρών και μεγάλων κάδων:
|
||
```c
|
||
#define bin_index(sz) \
|
||
((in_smallbin_range (sz)) ? smallbin_index (sz) : largebin_index (sz))
|
||
```
|
||
<details>
|
||
|
||
<summary>Προσθέστε ένα μικρό παράδειγμα κομματιού</summary>
|
||
```c
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *chunks[10];
|
||
int i;
|
||
|
||
// Loop to allocate memory 8 times
|
||
for (i = 0; i < 9; i++) {
|
||
chunks[i] = malloc(0x100);
|
||
if (chunks[i] == NULL) { // Check if malloc failed
|
||
fprintf(stderr, "Memory allocation failed at iteration %d\n", i);
|
||
return 1;
|
||
}
|
||
printf("Address of chunk %d: %p\n", i, (void *)chunks[i]);
|
||
}
|
||
|
||
// Loop to free the allocated memory
|
||
for (i = 0; i < 8; i++) {
|
||
free(chunks[i]);
|
||
}
|
||
|
||
chunks[9] = malloc(0x110);
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
Σημειώστε πώς κατανέμουμε και απελευθερώνουμε 9 κομμάτια του ίδιου μεγέθους ώστε να **γεμίσουν το tcache** και το όγδοο αποθηκεύεται στο unsorted bin επειδή είναι **πολύ μεγάλο για το fastbin** και το ένατο δεν έχει απελευθερωθεί, οπότε το ένατο και το όγδοο **δεν συγχωνεύονται με το κορυφαίο κομμάτι**. Στη συνέχεια, κατανέμουμε ένα μεγαλύτερο κομμάτι 0x110 που κάνει **το κομμάτι στο unsorted bin να πηγαίνει στο small bin**.
|
||
|
||
Συγκεντρώστε το και αποσφαλματώστε το με ένα breakpoint στην εντολή `ret` από τη συνάρτηση `main`. Στη συνέχεια, με το `gef` μπορείτε να δείτε ότι το tcache bin είναι γεμάτο και ένα κομμάτι είναι στο small bin:
|
||
```bash
|
||
gef➤ heap bins
|
||
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
|
||
Tcachebins[idx=15, size=0x110, count=7] ← Chunk(addr=0xaaaaaaac1d10, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1c00, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1af0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac19e0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac18d0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac17c0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac12a0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
|
||
Fastbins[idx=0, size=0x20] 0x00
|
||
Fastbins[idx=1, size=0x30] 0x00
|
||
Fastbins[idx=2, size=0x40] 0x00
|
||
Fastbins[idx=3, size=0x50] 0x00
|
||
Fastbins[idx=4, size=0x60] 0x00
|
||
Fastbins[idx=5, size=0x70] 0x00
|
||
Fastbins[idx=6, size=0x80] 0x00
|
||
─────────────────────────────────────────────────────────────────────── Unsorted Bin for arena at 0xfffff7f90b00 ───────────────────────────────────────────────────────────────────────
|
||
[+] Found 0 chunks in unsorted bin.
|
||
──────────────────────────────────────────────────────────────────────── Small Bins for arena at 0xfffff7f90b00 ────────────────────────────────────────────────────────────────────────
|
||
[+] small_bins[16]: fw=0xaaaaaaac1e10, bk=0xaaaaaaac1e10
|
||
→ Chunk(addr=0xaaaaaaac1e20, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
[+] Found 1 chunks in 1 small non-empty bins.
|
||
```
|
||
</details>
|
||
|
||
### Μεγάλες δεξαμενές
|
||
|
||
Σε αντίθεση με τις μικρές δεξαμενές, οι οποίες διαχειρίζονται κομμάτια σταθερού μεγέθους, κάθε **μεγάλη δεξαμενή διαχειρίζεται μια σειρά από μεγέθη κομματιών**. Αυτό είναι πιο ευέλικτο, επιτρέποντας στο σύστημα να φιλοξενεί **διάφορα μεγέθη** χωρίς να χρειάζεται μια ξεχωριστή δεξαμενή για κάθε μέγεθος.
|
||
|
||
Σε έναν αλγόριθμο κατανομής μνήμης, οι μεγάλες δεξαμενές ξεκινούν όπου τελειώνουν οι μικρές δεξαμενές. Οι περιοχές για τις μεγάλες δεξαμενές μεγαλώνουν σταδιακά, πράγμα που σημαίνει ότι η πρώτη δεξαμενή μπορεί να καλύπτει κομμάτια από 512 έως 576 byte, ενώ η επόμενη καλύπτει από 576 έως 640 byte. Αυτό το μοτίβο συνεχίζεται, με τη μεγαλύτερη δεξαμενή να περιέχει όλα τα κομμάτια πάνω από 1MB.
|
||
|
||
Οι μεγάλες δεξαμενές είναι πιο αργές στη λειτουργία τους σε σύγκριση με τις μικρές δεξαμενές, διότι πρέπει να **ταξινομήσουν και να αναζητήσουν μέσα σε μια λίστα με ποικίλα μεγέθη κομματιών για να βρουν την καλύτερη εφαρμογή** για μια κατανομή. Όταν ένα κομμάτι εισάγεται σε μια μεγάλη δεξαμενή, πρέπει να ταξινομηθεί, και όταν η μνήμη κατανεμηθεί, το σύστημα πρέπει να βρει το σωστό κομμάτι. Αυτή η επιπλέον εργασία τις καθιστά **αργές**, αλλά καθώς οι μεγάλες κατανομές είναι λιγότερο συχνές από τις μικρές, είναι μια αποδεκτή ανταλλαγή.
|
||
|
||
Υπάρχουν:
|
||
|
||
- 32 δεξαμενές εύρους 64B (συγκρούονται με μικρές δεξαμενές)
|
||
- 16 δεξαμενές εύρους 512B (συγκρούονται με μικρές δεξαμενές)
|
||
- 8 δεξαμενές εύρους 4096B (μερικώς συγκρούονται με μικρές δεξαμενές)
|
||
- 4 δεξαμενές εύρους 32768B
|
||
- 2 δεξαμενές εύρους 262144B
|
||
- 1 δεξαμενή για τα υπόλοιπα μεγέθη
|
||
|
||
<details>
|
||
|
||
<summary>Κωδικός μεγάλων δεξαμενών</summary>
|
||
```c
|
||
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
|
||
|
||
#define largebin_index_32(sz) \
|
||
(((((unsigned long) (sz)) >> 6) <= 38) ? 56 + (((unsigned long) (sz)) >> 6) :\
|
||
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
|
||
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
|
||
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
|
||
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
|
||
126)
|
||
|
||
#define largebin_index_32_big(sz) \
|
||
(((((unsigned long) (sz)) >> 6) <= 45) ? 49 + (((unsigned long) (sz)) >> 6) :\
|
||
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
|
||
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
|
||
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
|
||
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
|
||
126)
|
||
|
||
// XXX It remains to be seen whether it is good to keep the widths of
|
||
// XXX the buckets the same or whether it should be scaled by a factor
|
||
// XXX of two as well.
|
||
#define largebin_index_64(sz) \
|
||
(((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) :\
|
||
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
|
||
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
|
||
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
|
||
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
|
||
126)
|
||
|
||
#define largebin_index(sz) \
|
||
(SIZE_SZ == 8 ? largebin_index_64 (sz) \
|
||
: MALLOC_ALIGNMENT == 16 ? largebin_index_32_big (sz) \
|
||
: largebin_index_32 (sz))
|
||
```
|
||
</details>
|
||
|
||
<details>
|
||
|
||
<summary>Προσθήκη ενός μεγάλου παραδείγματος</summary>
|
||
```c
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *chunks[2];
|
||
|
||
chunks[0] = malloc(0x1500);
|
||
chunks[1] = malloc(0x1500);
|
||
free(chunks[0]);
|
||
chunks[0] = malloc(0x2000);
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
2 μεγάλες κατανομές εκτελούνται, στη συνέχεια μία απελευθερώνεται (βάζοντάς την στο αταξινόμητο δοχείο) και μια μεγαλύτερη κατανομή γίνεται (μετακινώντας την ελεύθερη από το αταξινόμητο δοχείο στο μεγάλο δοχείο).
|
||
|
||
Συγκεντρώστε το και αποσφαλματώστε το με ένα σημείο διακοπής στον `ret` opcode από τη συνάρτηση `main`. Στη συνέχεια, με το `gef` μπορείτε να δείτε ότι το δοχείο tcache είναι γεμάτο και ένα κομμάτι είναι στο μεγάλο δοχείο:
|
||
```bash
|
||
gef➤ heap bin
|
||
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
|
||
All tcachebins are empty
|
||
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
|
||
Fastbins[idx=0, size=0x20] 0x00
|
||
Fastbins[idx=1, size=0x30] 0x00
|
||
Fastbins[idx=2, size=0x40] 0x00
|
||
Fastbins[idx=3, size=0x50] 0x00
|
||
Fastbins[idx=4, size=0x60] 0x00
|
||
Fastbins[idx=5, size=0x70] 0x00
|
||
Fastbins[idx=6, size=0x80] 0x00
|
||
─────────────────────────────────────────────────────────────────────── Unsorted Bin for arena at 0xfffff7f90b00 ───────────────────────────────────────────────────────────────────────
|
||
[+] Found 0 chunks in unsorted bin.
|
||
──────────────────────────────────────────────────────────────────────── Small Bins for arena at 0xfffff7f90b00 ────────────────────────────────────────────────────────────────────────
|
||
[+] Found 0 chunks in 0 small non-empty bins.
|
||
──────────────────────────────────────────────────────────────────────── Large Bins for arena at 0xfffff7f90b00 ────────────────────────────────────────────────────────────────────────
|
||
[+] large_bins[100]: fw=0xaaaaaaac1290, bk=0xaaaaaaac1290
|
||
→ Chunk(addr=0xaaaaaaac12a0, size=0x1510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
[+] Found 1 chunks in 1 large non-empty bins.
|
||
```
|
||
</details>
|
||
|
||
### Κύριο Τμήμα
|
||
```c
|
||
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
|
||
|
||
/*
|
||
Top
|
||
|
||
The top-most available chunk (i.e., the one bordering the end of
|
||
available memory) is treated specially. It is never included in
|
||
any bin, is used only if no other chunk is available, and is
|
||
released back to the system if it is very large (see
|
||
M_TRIM_THRESHOLD). Because top initially
|
||
points to its own bin with initial zero size, thus forcing
|
||
extension on the first malloc request, we avoid having any special
|
||
code in malloc to check whether it even exists yet. But we still
|
||
need to do so when getting memory from system, so we make
|
||
initial_top treat the bin as a legal but unusable chunk during the
|
||
interval between initialization and the first call to
|
||
sysmalloc. (This is somewhat delicate, since it relies on
|
||
the 2 preceding words to be zero during this interval as well.)
|
||
*/
|
||
|
||
/* Conveniently, the unsorted bin can be used as dummy top on first call */
|
||
#define initial_top(M) (unsorted_chunks (M))
|
||
```
|
||
Βασικά, αυτό είναι ένα κομμάτι που περιέχει όλα τα διαθέσιμα heap. Όταν εκτελείται μια malloc, αν δεν υπάρχει διαθέσιμο ελεύθερο κομμάτι προς χρήση, αυτό το κορυφαίο κομμάτι θα μειώνει το μέγεθός του δίνοντας τον απαραίτητο χώρο.\
|
||
Ο δείκτης προς το Top Chunk αποθηκεύεται στη δομή `malloc_state`.
|
||
|
||
Επιπλέον, στην αρχή, είναι δυνατόν να χρησιμοποιηθεί το αταξινόμητο κομμάτι ως το κορυφαίο κομμάτι.
|
||
|
||
<details>
|
||
|
||
<summary>Παρατηρήστε το παράδειγμα του Top Chunk</summary>
|
||
```c
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *chunk;
|
||
chunk = malloc(24);
|
||
printf("Address of the chunk: %p\n", (void *)chunk);
|
||
gets(chunk);
|
||
return 0;
|
||
}
|
||
```
|
||
Μετά την εκτέλεση και την αποσφαλμάτωσή του με ένα σημείο διακοπής στον `ret` opcode του `main`, είδα ότι η malloc επέστρεψε τη διεύθυνση `0xaaaaaaac12a0` και αυτοί είναι οι θρόισμοι:
|
||
```bash
|
||
gef➤ heap chunks
|
||
Chunk(addr=0xaaaaaaac1010, size=0x290, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
[0x0000aaaaaaac1010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
|
||
Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
[0x0000aaaaaaac12a0 41 41 41 41 41 41 41 00 00 00 00 00 00 00 00 00 AAAAAAA.........]
|
||
Chunk(addr=0xaaaaaaac12c0, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
[0x0000aaaaaaac12c0 41 64 64 72 65 73 73 20 6f 66 20 74 68 65 20 63 Address of the c]
|
||
Chunk(addr=0xaaaaaaac16d0, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
|
||
[0x0000aaaaaaac16d0 41 41 41 41 41 41 41 0a 00 00 00 00 00 00 00 00 AAAAAAA.........]
|
||
Chunk(addr=0xaaaaaaac1ae0, size=0x20530, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← top chunk
|
||
```
|
||
Μπορεί να παρατηρηθεί ότι το κορυφαίο κομμάτι βρίσκεται στη διεύθυνση `0xaaaaaaac1ae0`. Αυτό δεν είναι έκπληξη γιατί το τελευταίο κατανεμημένο κομμάτι ήταν στη διεύθυνση `0xaaaaaaac12a0` με μέγεθος `0x410` και `0xaaaaaaac12a0 + 0x410 = 0xaaaaaaac1ae0`.\
|
||
Είναι επίσης δυνατό να δούμε το μήκος του κορυφαίου κομματιού στην κεφαλίδα του κομματιού του:
|
||
```bash
|
||
gef➤ x/8wx 0xaaaaaaac1ae0 - 16
|
||
0xaaaaaaac1ad0: 0x00000000 0x00000000 0x00020531 0x00000000
|
||
0xaaaaaaac1ae0: 0x00000000 0x00000000 0x00000000 0x00000000
|
||
```
|
||
</details>
|
||
|
||
### Τελευταίο Υπόλοιπο
|
||
|
||
Όταν χρησιμοποιείται το malloc και ένα κομμάτι διαιρείται (από το unsorted bin ή από το top chunk για παράδειγμα), το κομμάτι που δημιουργείται από το υπόλοιπο του διαιρεμένου κομματιού ονομάζεται Τελευταίο Υπόλοιπο και ο δείκτης του αποθηκεύεται στη δομή `malloc_state`.
|
||
|
||
## Ροή Κατανομής
|
||
|
||
Δείτε:
|
||
|
||
{{#ref}}
|
||
heap-memory-functions/malloc-and-sysmalloc.md
|
||
{{#endref}}
|
||
|
||
## Ροή Απελευθέρωσης
|
||
|
||
Δείτε:
|
||
|
||
{{#ref}}
|
||
heap-memory-functions/free.md
|
||
{{#endref}}
|
||
|
||
## Έλεγχοι Ασφαλείας Συναρτήσεων Heap
|
||
|
||
Ελέγξτε τους ελέγχους ασφαλείας που εκτελούνται από τις πολύ χρησιμοποιούμενες συναρτήσεις στο heap στο:
|
||
|
||
{{#ref}}
|
||
heap-memory-functions/heap-functions-security-checks.md
|
||
{{#endref}}
|
||
|
||
## Αναφορές
|
||
|
||
- [https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/](https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/)
|
||
- [https://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/](https://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/)
|
||
- [https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/core_functions](https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/core_functions)
|
||
- [https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/implementation/tcache/](https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/implementation/tcache/)
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|