Translated ['binary-exploitation/heap/README.md'] to it

This commit is contained in:
Translator 2024-05-09 18:07:46 +00:00
parent d313286adc
commit f765ed8b16
7 changed files with 210 additions and 1 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View file

@ -1,3 +1,212 @@
# Heap
Il **heap** è una struttura dati utilizzata per l'allocazione dinamica della memoria durante l'esecuzione di un programma. Gli attacchi al **heap** sono comuni nelle sfide di sicurezza informatica e possono portare a vulnerabilità come **heap overflow**, **use-after-free** e **double free**. Imparare a sfruttare queste vulnerabilità è essenziale per diventare un hacker esperto nel campo della sicurezza informatica.
## Concetti di Heap
L'heap è essenzialmente il luogo in cui un programma sarà in grado di memorizzare dati quando richiede dati chiamando funzioni come **`malloc`**, `calloc`... Inoltre, quando questa memoria non è più necessaria, viene resa disponibile chiamando la funzione **`free`**.
Come mostrato, si trova subito dopo dove il binario viene caricato in memoria (controlla la sezione `[heap]`):
<figure><img src="../../.gitbook/assets/image (1241).png" alt=""><figcaption></figcaption></figure>
### Allocazione di Chunk di Base
Quando viene richiesto di memorizzare alcuni dati nell'heap, viene allocato uno spazio nell'heap. Questo spazio apparterrà a un bin e solo i dati richiesti + lo spazio degli header del bin + l'offset della dimensione minima del bin verranno riservati per il chunk. L'obiettivo è riservare solo la memoria minima possibile senza rendere complicato trovare dove si trova ciascun chunk. Per questo, le informazioni sui metadati del chunk vengono utilizzate per sapere dove si trova ciascun chunk utilizzato/libero.
Ci sono diversi modi per riservare lo spazio principalmente a seconda del bin utilizzato, ma una metodologia generale è la seguente:
* Il programma inizia richiedendo una certa quantità di memoria.
* Se nella lista dei chunk c'è qualcuno abbastanza grande da soddisfare la richiesta, verrà utilizzato.
* Questo potrebbe significare che parte del chunk disponibile verrà utilizzata per questa richiesta e il resto verrà aggiunto alla lista dei chunk.
* Se non c'è alcun chunk disponibile nella lista ma c'è ancora spazio nella memoria dell'heap allocata, il gestore dell'heap crea un nuovo chunk.
* Se non c'è abbastanza spazio nell'heap per allocare il nuovo chunk, il gestore dell'heap chiede al kernel di espandere la memoria allocata all'heap e quindi utilizza questa memoria per generare il nuovo chunk.
* Se tutto fallisce, `malloc` restituisce null.
Nota che se la memoria richiesta supera una soglia, verrà utilizzato **`mmap`** per mappare la memoria richiesta.
### Aree
Nelle applicazioni **multithread**, il gestore dell'heap deve prevenire le **condizioni di gara** che potrebbero portare a crash. Inizialmente, ciò veniva fatto utilizzando un **mutex globale** per garantire che solo un thread potesse accedere all'heap alla volta, ma ciò causava **problemi di prestazioni** a causa del collo di bottiglia indotto dal mutex.
Per affrontare questo problema, l'allocatore di heap ptmalloc2 ha introdotto "arene", dove **ogni arena** funge da **heap separato** con le sue **proprie** strutture **dati** e **mutex**, consentendo a più thread di eseguire operazioni sull'heap senza interferire l'uno con l'altro, purché utilizzino arene diverse.
L'arena "principale" predefinita gestisce le operazioni sull'heap per le applicazioni single-threaded. Quando vengono aggiunti **nuovi thread**, il gestore dell'heap assegna loro **arene secondarie** per ridurre la contesa. Prima tenta di collegare ciascun nuovo thread a un'arena inutilizzata, creandone di nuove se necessario, fino a un limite di 2 volte i core della CPU per i sistemi a 32 bit e 8 volte per i sistemi a 64 bit. Una volta raggiunto il limite, i **thread devono condividere le arene**, portando a una potenziale contesa.
A differenza dell'arena principale, che si espande utilizzando la chiamata di sistema `brk`, le arene secondarie creano "subheap" utilizzando `mmap` e `mprotect` per simulare il comportamento dell'heap, consentendo flessibilità nella gestione della memoria per operazioni multithread.
### Subheap
I subheap fungono da riserve di memoria per le arene secondarie nelle applicazioni multithread, consentendo loro di crescere e gestire le proprie regioni di heap separatamente dall'heap principale. Ecco come i subheap differiscono dall'heap iniziale e come operano:
1. **Heap Iniziale vs. Subheap**:
* L'heap iniziale si trova direttamente dopo il binario del programma in memoria e si espande utilizzando la chiamata di sistema `sbrk`.
* I subheap, utilizzati dalle arene secondarie, vengono creati tramite `mmap`, una chiamata di sistema che mappa una regione di memoria specificata.
2. **Riserva di Memoria con `mmap`**:
* Quando il gestore dell'heap crea un subheap, riserva un grande blocco di memoria tramite `mmap`. Questa riserva non alloca immediatamente memoria; designa semplicemente una regione che altri processi di sistema o allocazioni non dovrebbero utilizzare.
* Per impostazione predefinita, la dimensione riservata per un subheap è di 1 MB per i processi a 32 bit e di 64 MB per i processi a 64 bit.
3. **Espansione Graduale con `mprotect`**:
* La regione di memoria riservata è inizialmente contrassegnata come `PROT_NONE`, indicando che il kernel non ha bisogno di allocare memoria fisica per questo spazio ancora.
* Per "far crescere" il subheap, il gestore dell'heap utilizza `mprotect` per cambiare le autorizzazioni delle pagine da `PROT_NONE` a `PROT_READ | PROT_WRITE`, spingendo il kernel ad allocare memoria fisica agli indirizzi precedentemente riservati. Questo approccio passo dopo passo consente al subheap di espandersi secondo necessità.
* Una volta esaurito l'intero subheap, il gestore dell'heap crea un nuovo subheap per continuare l'allocazione.
### Metadati
Come già commentato, questi chunk hanno anche alcuni metadati, molto ben rappresentati in questa immagine:
<figure><img src="../../.gitbook/assets/image (1242).png" alt=""><figcaption><p><a href="https://azeria-labs.com/wp-content/uploads/2019/03/chunk-allocated-CS.png">https://azeria-labs.com/wp-content/uploads/2019/03/chunk-allocated-CS.png</a></p></figcaption></figure>
Di solito i metadati sono 0x08B, indicando la dimensione corrente del chunk utilizzando gli ultimi 3 bit per indicare:
* `A`: Se è 1 proviene da un subheap, se è 0 è nell'arena principale
* `M`: Se è 1, questo chunk fa parte di uno spazio allocato con mmap e non fa parte di un heap
* `P`: Se è 1, il chunk precedente è in uso
Quindi, lo spazio per i dati dell'utente, e infine 0x08B per indicare la dimensione del chunk precedente quando il chunk è disponibile (o per memorizzare i dati dell'utente quando viene allocato).
Inoltre, quando disponibile, i dati dell'utente vengono utilizzati per contenere anche alcuni dati:
* Puntatore al chunk successivo
* Puntatore al chunk precedente
* Dimensione del chunk successivo nella lista
* Dimensione del chunk precedente nella lista
<figure><img src="../../.gitbook/assets/image (1243).png" alt=""><figcaption><p><a href="https://azeria-labs.com/wp-content/uploads/2019/03/chunk-allocated-CS.png">https://azeria-labs.com/wp-content/uploads/2019/03/chunk-allocated-CS.png</a></p></figcaption></figure>
Nota come organizzare la lista in questo modo evita la necessità di avere un array in cui ogni singolo chunk viene registrato.
## Protezioni di Free
Per proteggersi dall'abuso accidentale o intenzionale della funzione free, prima di eseguire le sue azioni vengono effettuati alcuni controlli:
* Controlla che l'indirizzo [sia allineato](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4182) su un limite di 8 byte o 16 byte su un limite di 64 bit (`(indirizzo % 16) == 0`), poiché _malloc_ garantisce che tutte le allocazioni siano allineate.
* Controlla che il campo di dimensione del chunk non sia impossibile - sia perché è [troppo piccolo](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4318), troppo grande, non di dimensioni allineate, o [sovrapporrebbe la fine dello spazio degli indirizzi del processo](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4175).
* Controlla che il chunk si trovi [all'interno dei limiti dell'arena](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4318).
* Controlla che il chunk non sia [già contrassegnato come libero](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4182) controllando il bit "P" corrispondente che si trova nei metadati all'inizio del chunk successivo.
## Bins
Per migliorare l'efficienza di come vengono memorizzati i chunk, ogni chunk non è solo in una lista concatenata, ma ci sono diversi tipi. Questi sono i bins e ci sono 5 tipi di bins: [62](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l1407) small bins, 63 large bins, 1 unsorted bin, 10 fast bins e 64 tcache bins per thread.
L'indirizzo iniziale di ogni bin non ordinato, small e large è all'interno dello stesso array. L'indice 0 non è utilizzato, 1 è l'unsorted bin, i bins da 2 a 64 sono small bins e i bins da 65 a 127 sono large bins.
### Small Bins
I small bins sono più veloci dei large bins ma più lenti dei fast bins.
Ogni bin dei 62 avrà **chunk della stessa dimensione**: 16, 24, ... (con una dimensione massima di 504 byte in 32 bit e 1024 in 64 bit). Questo aiuta nella velocità nel trovare il bin in cui dovrebbe essere allocato uno spazio e nell'inserimento e rimozione delle voci in queste liste.
### Large Bins
A differenza dei small bins, che gestiscono chunk di dimensioni fisse, ogni **large bin gestisce un intervallo di dimensioni di chunk**. Questo è più flessibile, permettendo al sistema di ospitare **varie dimensioni** senza necessità di un bin separato per ogni dimensione.
In un allocatore di memoria, i large bins iniziano dove finiscono i small bins. Gli intervalli per i large bins crescono progressivamente, il che significa che il primo bin potrebbe coprire chunk da 512 a 576 byte, mentre il successivo copre da 576 a 640 byte. Questo modello continua, con il bin più grande che contiene tutti i chunk sopra 1MB.
I large bins sono più lenti da gestire rispetto ai small bins perché devono **ordinare e cercare in una lista di dimensioni di chunk variabili per trovare la migliore corrispondenza** per un'allocazione. Quando un chunk viene inserito in un large bin, deve essere ordinato e quando viene allocata memoria, il sistema deve trovare il chunk giusto. Questo lavoro aggiuntivo li rende **più lenti**, ma poiché le allocazioni di grandi dimensioni sono meno comuni di quelle piccole, è un compromesso accettabile.
Ci sono:
* 32 bins di intervallo 64B
* 16 bins di intervallo 512B
* 8 bins di intervallo 4096B
* 4 bins di intervallo 32768B
* 2 bins di intervallo 262144B
* 1 bin per dimensioni rimanenti
### Unsorted bin
L'unsorted bin è una **cache veloce** utilizzata dal gestore dell'heap per rendere più veloce l'allocazione di memoria. Ecco come funziona: quando un programma libera memoria, il gestore dell'heap non la mette immediatamente in un bin specifico. Invece, cerca prima di **fonderla con eventuali chunk liberi adiacenti** per creare un blocco più grande di memoria libera. Quindi, inserisce questo nuovo chunk in un bin generale chiamato "unsorted bin".
Quando un programma **richiede memoria**, il gestore dell'heap **controlla prima l'unsorted bin** per vedere se c'è un chunk della dimensione corretta. Se ne trova uno, lo usa immediatamente, il che è più veloce rispetto alla ricerca in altri bins. Se non trova un chunk adatto, sposta i chunk liberati nei rispettivi bins corretti, piccoli o grandi, in base alla loro dimensione.
Quindi, l'unsorted bin è un modo per velocizzare l'allocazione di memoria riutilizzando rapidamente la memoria liberata di recente e riducendo la necessità di ricerche e fusioni dispendiose di tempo.
{% hint style="danger" %}
Nota che anche se i chunk sono di categorie diverse, di tanto in tanto, se un chunk disponibile entra in collisione con un altro chunk disponibile (anche se sono di categorie diverse), verranno fusi.
{% endhint %}
### Fast bins
I fast bins sono progettati per **accelerare l'allocazione di memoria per chunk piccoli** mantenendo i chunk liberati di recente in una struttura di accesso rapido. Questi bins utilizzano un approccio Last-In, First-Out (LIFO), il che significa che il **chunk liberato più di recente è il primo** ad essere riutilizzato quando c'è una nuova richiesta di allocazione. Questo comportamento è vantaggioso per la velocità, poiché è più veloce inserire e rimuovere dalla cima di uno stack (LIFO) rispetto a una coda (FIFO).
Inoltre, **i fast bins utilizzano liste collegate singolarmente**, non doppie, il che migliora ulteriormente la velocità. Poiché i chunk nei fast bins non vengono fusi con i vicini, non c'è bisogno di una struttura complessa che permetta la rimozione dal mezzo. Una lista collegata singolarmente è più semplice e veloce per queste operazioni.
Fondamentalmente, ciò che accade qui è che l'intestazione (il puntatore al primo chunk da controllare) punta sempre all'ultimo chunk liberato di quella dimensione. Quindi:
* Quando viene allocato un nuovo chunk di quella dimensione, l'intestazione punta a un chunk libero da utilizzare. Poiché questo chunk libero punta al successivo da utilizzare, questo indirizzo viene memorizzato nell'intestazione in modo che la prossima allocazione sappia dove ottenere un chunk disponibile
* Quando un chunk viene liberato, il chunk libero salverà l'indirizzo al chunk disponibile corrente e l'indirizzo a questo nuovo chunk liberato verrà messo nell'intestazione
{% hint style="danger" %}
I chunk nei fast bins non vengono automaticamente impostati come disponibili, quindi rimangono come chunk fast bin per un po' di tempo invece di poter essere fusi con altri chunk.
{% endhint %}
### Tcache (Per-Thread Cache) Bins
Anche se i thread cercano di avere il proprio heap (vedi [Arenas](./#arenas) e [Subheaps](./#subheaps)), c'è la possibilità che un processo con molti thread (come un server web) **finisca per condividere l'heap con altri thread**. In questo caso, la soluzione principale è l'uso di **lock**, che potrebbero **sopprimere significativamente i thread**.
Pertanto, una tcache è simile a un fast bin per thread nel senso che è una **lista collegata singolarmente** che non fonde i chunk. Ogni thread ha **64 tcache bins collegati singolarmente**. Ogni bin può avere un massimo di [7 chunk della stessa dimensione](https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=2527e2504761744df2bdb1abdc02d936ff907ad2;hb=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc#l323) che vanno da [24 a 1032B nei sistemi a 64 bit e da 12 a 516B nei sistemi a 32 bit](https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=2527e2504761744df2bdb1abdc02d936ff907ad2;hb=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc#l315).
Quando un **thread libera** un chunk, **se non è troppo grande** per essere allocato nella tcache e il rispettivo bin tcache **non è pieno** (già 7 chunk), **verrà allocato lì**. Se non può andare nella tcache, dovrà attendere il lock dell'heap per poter eseguire l'operazione di liberazione globalmente.
Quando viene **allocato un chunk**, se c'è un chunk libero della dimensione necessaria nella **tcache lo utilizzerà**, altrimenti dovrà attendere il lock dell'heap per poterne trovare uno nei bin globali o crearne uno nuovo.\
C'è anche un'ottimizzazione, in questo caso, mentre si ha il lock dell'heap, il thread **riempirà la sua tcache con chunk dell'heap (7) della dimensione richiesta**, quindi nel caso ne abbia bisogno di più, li troverà nella tcache.
### Ordine dei Bins
#### Per l'allocazione:
1. Se è disponibile un chunk nel Tcache di quella dimensione, utilizzalo
2. Se è molto grande, utilizza mmap
3. Ottieni il lock dell'heap dell'arena e:
1. Se c'è abbastanza spazio di dimensioni ridotte, utilizza un chunk fast bin disponibile della dimensione richiesta, utilizzalo e riempi il tcache dal fast bin
2. Controlla ogni voce nella lista non ordinata cercando un chunk sufficientemente grande e riempi il tcache se possibile
3. Controlla i bin di dimensioni ridotte o grandi (in base alla dimensione richiesta) e riempi il tcache se possibile
4. Crea un nuovo chunk dalla memoria disponibile
1. Se non c'è memoria disponibile, ottienine di più usando `sbrk`
2. Se la memoria principale dell'heap non può crescere ulteriormente, crea uno nuovo spazio utilizzando mmap
5. Se niente ha funzionato, restituisci null
**Per la liberazione:**
1. Se il puntatore è Null, termina
2. Esegui controlli di coerenza `free` nel chunk per cercare di verificarne la legittimità
1. Se è abbastanza piccolo e il tcache non è pieno, mettilo lì
2. Se il bit M è impostato (non nell'heap), utilizza `munmap`
3. Ottieni il lock dell'heap dell'arena:
1. Se si adatta a un fastbin, mettilo lì
2. Se il chunk è > 64KB, consolidare immediatamente i fastbins e mettere i chunk uniti risultanti nel bin non ordinato.
3. Unisci il chunk all'indietro e in avanti con i chunk liberati vicini nei bin di dimensioni ridotte, grandi e non ordinati se presenti.
4. Se è in cima alla testa, uniscilo alla memoria non utilizzata
5. Se non è tra quelli precedenti, memorizzalo nella lista non ordinata
\
Esempio rapido di heap da [https://guyinatuxedo.github.io/25-heap/index.html](https://guyinatuxedo.github.io/25-heap/index.html) ma in arm64:
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(void)
{
char *ptr;
ptr = malloc(0x10);
strcpy(ptr, "panda");
}
```
Imposta un breakpoint alla fine della funzione principale e scopriamo dove sono state memorizzate le informazioni:
<figure><img src="../../.gitbook/assets/image (1239).png" alt=""><figcaption></figcaption></figure>
È possibile vedere che la stringa panda è stata memorizzata a `0xaaaaaaac12a0` (che è stato l'indirizzo restituito da malloc all'interno di `x0`). Controllando 0x10 byte prima è possibile vedere che il `0x0` rappresenta che il **chunk precedente non è in uso** (lunghezza 0) e che la lunghezza di questo chunk è `0x21`.
Gli spazi extra riservati (0x21-0x10=0x11) provengono dagli **header aggiunti** (0x10) e 0x1 non significa che sono stati riservati 0x21 byte ma gli ultimi 3 bit della lunghezza dell'header corrente hanno alcuni significati speciali. Poiché la lunghezza è sempre allineata su 16 byte (nelle macchine a 64 bit), questi bit non verranno mai utilizzati dal numero di lunghezza.
```
0x1: Previous in Use - Specifies that the chunk before it in memory is in use
0x2: Is MMAPPED - Specifies that the chunk was obtained with mmap()
0x4: Non Main Arena - Specifies that the chunk was obtained from outside of the main arena
```
##
## Riferimenti
* [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/)