Vazando libc com malloc_consolidate (Todo)
O fastbin não se consolida automaticamente, apenas quando a função malloc_consolidate() é chamada. A função pode parecer complicada, mas seu papel é percorrer todos os chunks nas fastbins, unir aqueles que são adjacentes na memória para formar chunks maiores e, em seguida, inserir esses chunks consolidados na unsorted bin.
Um Read-After-Free (leitura após free) em um chunk na fastbin pode vazar o endereço da heap, mas não da libc. Pois as listas de fastbins são simplesmente encadeadas usando apenas o ponteiro fd, que aponta para outro chunk livre na heap ou é NULL.
Para obter um vazamento da libc, precisamos acessar ponteiros da unsorted bin, que é uma lista duplamente encadeada. Isso pois quando há só um chunk na unsorted bin, seu ponteiros fd/bk apontam para a libc (que está dentro do main_arena). main_arena é uma variável global da libc que contém todo o estado do alocador. Vou explicar com mais detalhes adiante.
O segredo para vazar a libc será ativar malloc_consolidate() para levar um chunk da fastbin para a unsorted bin e acessar seu fd/bk que apontam para a libc.
main_arena e unsorted bin
Observemos a struct malloc_state (main_arena é uma instância dessa struct):
struct malloc_state { // main_arena é uma INSTÂNCIA disso
mutex_t mutex;
int flags;
// Várias "bins" (listas) de chunks livres
mfastbinptr fastbinsY[NFASTBINS]; // Fastbins
mchunkptr top; // Top chunk
mchunkptr last_remainder;
// Bins normais (unsorted, small, large)
mchunkptr bins[NBINS * 2 - 2]; // <--- IMPORTANTE!
// ...
};
Cada bin ocupa DOIS SLOTS no array bins:
Índices no array bins[]:
[0] = fd do unsorted bin (aponta para primeiro chunk)
[1] = bk do unsorted bin (aponta para último chunk)
[2] = fd do smallbin[1] (chunks de ~32 bytes)
[3] = bk do smallbin[1]
[4] = fd do smallbin[2] (chunks de ~48 bytes)
[5] = bk do smallbin[2]
...
E lembre-se que mchunkptr é uma ponteiro para um chunk:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
typedef struct malloc_chunk* mchunkptr // PONTEIRO para chunk;
Os bins ficam em main_arena.bins (mesma coisa que malloc_chunk.bins). O array bins guarda os ponteiros para o primeiro/último chunk de cada bin/lista.
Bom, tomemos a unsorted bin. É uma lista duplamente encadeada (fd e bk), certo? Em uma lista encadeada circular, o último elemento aponta para o primeiro. Na implementação da libc, o HEAD possui dois ponteiros: um que aponta para o primeiro chunk e outro que aponta para o último (por que o HEAD aponta para os dois? Para fins de otimização).
NAVEGANDO POR fd
HEAD -> a -> b -> c -> HEAD
NAVEGANDO POR bk
HEAD -> c -> b -> a -> HEAD
Assim:
HEAD.fd = Primeiro
HEAD.bk = Último
main_arena.bins[0] é um ponteiro para o primeiro chunk ("fd" do HEAD) e main_arena.bins[1] é um ponteiro para o último chunk (bk do HEAD):
main_arena.bins[0] (HEAD da unsorted bin):
- É um PONTEIRO DUPLO (armazena dois valores)
- bins[0] = fd do HEAD (aponta para primeiro chunk)
- bins[1] = bk do HEAD (aponta para último chunk)
Quando unsorted bin está VAZIO, por lógica da libc, main_arena.bins[0] = &bins[0] e main_arena.bins[1] = &bins[1] (apontam para si mesmos).
main_arena.bins[0]: [fd = &bins[0]] ← aponta para SI MESMO!
main_arena.bins[1]: [bk = &bins[0]] ← aponta para SI MESMO!
Quando unsorted bin possui UM chunk, main_arena.bins[0] = &chunk e main_arena.bins[1] = &chunk (apontam para chunk):
Memória da libc (main_arena):
bins[0] (fd do HEAD) → aponta para CHUNK
bins[1] (bk do HEAD) → aponta para CHUNK
Memória da heap (CHUNK):
chunk->fd → aponta para bins[0] (na libc)
chunk->bk → aponta para bins[1] (na libc)
Círculo completo:
bins[0] → CHUNK → bins[0]
bins[1] → CHUNK → bins[1]
E aqui está O VAZAMENTO QUE QUERÍAMOS! Com UM CHUNK, fd e bk desse mesmo chunk apontam para dentro da libc!
main_arena na LIBC (&bins = 0x7ffff7dd1b00):
┌─────────────────────────────┐
│ bins[0] (fd): 0x55555555a010│← aponta para CHUNK
│ bins[1] (bk): 0x55555555a010│← aponta para CHUNK
└─────────────────────────────┘
CHUNK na HEAP (0x55555555a010):
┌─────────────────────────────┐
│ fd: 0x7ffff7dd1b00 │← aponta para LIBC! (bins[0])
│ bk: 0x7ffff7dd1b08 │← aponta para LIBC! (bins[1])
│ ... dados do chunk ... │
└─────────────────────────────┘
Ativando malloc_conosolidate
Se liberarmos chunks adjacentes na fastbin e ativarmos uma chamada a malloc_consolidate(), eles serão consolidados e formarão um chunk maior que será movido para a unsorted bin. Se o chunk movido for o único elemento da lista, ambos fd e bk apontarão para uma localização dentro da estrutura malloc_state, que reside na memória da libc.
Portanto, o essencial é saber como ativar a consolidação do fastbin. Alguns gatilhos comuns incluem:
-
Fornecer uma entrada muito grande para funções como
scanf(por exemplo, algo em torno de0x400caracteres ou mais). Isso funciona porque a implementação interna doscanfem algumas libc usa umscratch_bufferalocado na heap para entradas grandes. Se a entrada exceder certo tamanho, o código pode acionarmallocourealloc, o que por sua vez pode levar à chamada demalloc_consolidatepara lidar com fragmentação e otimizar a alocação de grandes blocos. -
Usar uma string de formato como
%10000cem uma vulnerabilidade de format string também pode desencadear a alocação de um buffer grande, já que a funçãoprintfprecisa alocar espaço para a saída expandida, potencialmente ativandomalloc_consolidateindiretamente.
Ambos os métodos funcionam porque alocações que caem na categoria de largebin (ou que exigem manipulação especial de blocos grandes) frequentemente invocam malloc_consolidate para fundir blocos livres e reduzir a fragmentação antes de atender a uma requisição grande. Ao inspecionar o código-fonte do malloc.c na glibc, é possível identificar outros gatilhos específicos, como chamadas a malloc com tamanhos muito grandes, uso de realloc em certas condições, ou até mesmo a liberação massiva de memória em situações de pressão de heap.
Consolidação automática no free() comum
Vimos só sobre fastbin, mas a consolidação acontece SEMPRE ao liberar chunks small/large:
char *a = malloc(0x100); // Small chunk (não fastbin!)
char *b = malloc(0x100);
char *c = malloc(0x100);
free(a); // Já vai para unsorted bin
free(c); // Já vai para unsorted bin
free(b); // CONSOLIDA AUTOMATICAMENTE com 'a' e 'c'!
// Resultado: um chunk de ~0x300 na unsorted bin
Isso não
Resumo
- Chunks na fastbin → só vazam endereços da heap
- Chunks na unsorted bin → vazam endereços da libc
- Para mover chunks da fastbin para unsorted bin → chame
malloc_consolidate()(consolida chunks na fastbin e move para unsorted bin) - Para chamar
malloc_consolidate()→ use funções que alocam muito de uma só vez (scanf/printf grandes) - Vaze
fdoubkdo chunk que agora está na unsorted bin com UAF, Heap Overflow ou Double Free.