Introdução

Antes de nos aprofundarmos na heap da glibc, vamos dar uma olhada rápida na pilha. Depois disso, poderemos explorar a heap.
Na parte 2, vamos abordar a função free e suas bins.

A stack

Nos sistemas operacionais modernos, cada thread de uma aplicação possui sua própria stack. A stack tem um tamanho fixo, que é alocado pelo sistema operacional e atribuído ao processo quando ele é iniciado.
Ela funciona no modelo LIFO e, geralmente, armazena variáveis locais, ponteiros para endereços de retorno, argumentos de funções, etc.

A heap

A heap é um pouco diferente. Ela lida com a memória alocada dinamicamente. Isso significa que a heap não possui um tamanho fixo, pois cresce conforme mais memória é necessária. A maneira como a heap é gerenciada pode variar dependendo da implementação utilizada: a implementação da heap e seu funcionamento no Linux podem diferir da implementação no Windows.

Vamos ver o que acontece quando você realiza uma solicitação de malloc (supondo que você esteja usando a glibc) e ainda não há nenhuma região da heap mapeada (geralmente acontece quando você acabou de iniciar um programa). Este é o código-fonte do programa que vou mostrar:

1
2
3
4
5
6
#include<stdlib.h>  
  
int main(void) {  
	malloc(24);
	return 0;  
}

Se você apenas executar o programa, nada acontecerá (como esperado). Vamos analisá-lo usando o gdb. Vou usar o pwndbg, mas você pode usar qualquer ferramenta de sua escolha (ou até mesmo só o gdb puro). Contudo, recomendo fortemente o pwndbg.

Colocando um breakpoint na função main e executando o programa, use o comando vmmap:

no_mappings

Se você observar com atenção, ainda não há nenhum mapeamento de heap. Vamos usar o comando n para executar a função malloc e, em seguida, verificar novamente o vmmap:

mappings

Como você pode ver, agora existe uma região da heap! Isso ocorre porque acabamos de executar a função malloc. Legal, certo?

Com o pwndbg, agora use o comando vis (que é um atalho para vis_heap_chunks):

vis

Isso mostrará todos os chunks localizados na heap. Há um grande chunk no início (sobre o qual não entraremos em detalhes agora), e há um chunk logo após ele (colorido de roxo) que tem 0x0000000000000021 escrito nele. No final, há um 0x0000000000020d51 (sobre o qual também não entraremos em detalhes agora).

Por que não usar algo como mmap?

O maior problema com isso é que não teríamos um tamanho flexível de memória para trabalhar (pois ele deve ser um múltiplo de 4096, o tamanho de uma página).

Além disso, isso requer envolvimento do kernel. Isso significa que não será uma operação extremamente rápida.

A implementação da heap fará, mais ou menos, uma grande alocação de memória e, então, a distribuirá em chunks menores. Isso elimina os problemas mencionados anteriormente, pois não precisaremos do envolvimento do kernel o tempo todo e teremos a possibilidade de distribuir chunks menores quando uma alocação for solicitada.

Um chunk da heap

Metadados e dados

Vamos simplificar olhando apenas para o chunk roxo. Este é o chunk que obtivemos quando a função malloc(24) foi chamada. Há 0x21 escrito nele. Isso é, na verdade, o tamanho do chunk que acabamos de obter. Isso inclui os primeiros 8 bytes (que contam como os metadados) e a região de dados real do chunk. 8 bytes (metadados) + 24 bytes (dados reais) = 32 bytes (0x20). Mas para que servem esses metadados?

Os metadados

Nos primeiros 8 bytes, há um 0x21 escrito. Por que é 0x21 em vez de 0x20 então?
Os chunks não devem estar desalinhados, pois isso não é bom por uma série de razões. Assim, eles sempre precisam ser múltiplos de 8 (em sistemas de 32 bits) ou 16 (em sistemas de 64 bits). O que isso significa então? Os últimos 3 bits serão sempre zero.

Vamos ver isso ao nível dos bits. Imagine que você tem um número estranho como 397:
0b110001101
Agora, multiplique por 8:
0b110001101000
Multiplicar por 8 significa simplesmente adicionar 3 zeros ao final de um número binário.

Como os últimos bits sempre serão zero, os usaremos para outros propósitos, como salvar flags. Há 0x21 escrito porque o último bit é definido para uma flag chamada prev_inuse. Essa flag terá algumas funcionalidades que veremos mais adiante nesta série de blog posts.

Vamos continuar explorando os metadados com outras coisas. Altere o malloc(24) no código para um malloc(8) e execute outro comando vis:

4

O tamanho nos metadados do nosso chunk não mudou, mesmo que tenhamos mudado o tamanho solicitado na função malloc. Isso acontece porque a implementação da heap usará tamanhos de chunks “fixos” para otimizações (especialmente para quando liberarmos o chunk com free). Vamos explorar melhor a função free e as free bins no próximo post.

Top chunk

Lembra daquele valor no final dos chunks do heap que eu disse que abordaríamos mais tarde? Bem, vamos vê-lo agora!

Este valor é o tamanho do top chunk. Quando emitimos a função malloc pela primeira vez, ela solicitará ao kernel memória para a região do heap. No entanto, ela pedirá muito mais memória do que estamos solicitando. O principal motivo é que não quer pedir novamente, pois isso não é tão rápido. Isso significa que podemos solicitar mais chunks com malloc sem precisar solicitar mais memória ao kernel.

Vamos dar uma olhada melhor nisso. Este é o código-fonte que vou debuggar:

1
2
3
4
5
6
7
8
#include<stdlib.h>  
  
int main(void) {  
	void *a = malloc(0x8);
	void *b = malloc(0x8);
	void *c = malloc(0x8);
	return 0;  
}

Coloque um breakpoint na função main e execute. Recomendo compilar o código-fonte com debug informations (você pode fazer isso usando a flag -g).

Execute o primeiro malloc e execute um comando vis: 5

Isso está no final dos chunks.

Ok, tudo está como de costume e já vimos isso.

Agora, emita outro malloc usando o comando n. Use vis para ver o que acontece:
6
Como você pode ver, conseguimos outro chunk. Mas, se prestar muita atenção, o último valor (o tamanho do top chunk) agora é 0x20d31 em vez de 0x20d51. Ele realmente encolheu! Em vez de precisar solicitar mais memória ao kernel, simplesmente usamos o top chunk no final da região para atender à solicitação de malloc.

Este processo seria diferente se tivéssemos um chunk disponível em uma das listas livres, mas ainda não cobrimos isso. Então, vamos analisar apenas este caso por enquanto.

Interessante, né? Vamos fazer outra alocação então:
7

Nada de novo por aqui. Vamos tornar as coisas um pouco mais interessantes.

Solicitando mais memória do que o top chunk

No último exemplo, havia um valor de 0x20d11 no top chunk. O que acontece se usarmos tudo?

Este é o código-fonte modificado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include<stdlib.h>  
  
int main(void) {  
	void *a = malloc(0x8);  
	void *b = malloc(0x8);  
	void *c = malloc(0x8);  
	void *d = malloc(0x20ce0);  
	void *e = malloc(0x8);  
	return 0;  
}

Adicionei 2 chamadas malloc nele. Veja os chunks logo após o malloc na variável d:

8

O chunk superior agora tem tamanho 0x20. Fazendo outra solicitação de malloc:
9

Podemos ver que a heap aumentou de tamanho, pois obteve um novo top chunk (você pode verificar isso usando o comando vmmap logo após a última alocação e imediatamente antes dela). Vamos analisar melhor o top chunk e como algumas coisas funcionam ao redor dele quando explorarmos os ataques House of Force e House of Orange, que serão abordados em um post futuro.

Arenas

Há um conceito importante ao falar sobre a heap, chamado “arena”. Basicamente, uma arena é uma região de memória relacionada a cada thread e contém uma referência a uma ou mais heaps (cada heap pode estar em apenas uma arena). Há esta imagem interessante que vi neste incrível blog post. 10

Antes do uso de arenas, a forma de impedir que mais de uma thread usasse a mesma heap era através do uso de um mutex. Atualmente, cada arena ainda utiliza um mutex, mas agora as threads podem realizar operações na heap sem se preocuparem umas com as outras (já que estão interagindo com outras arenas). Quando não há como criar mais arenas, algumas threads terão que compartilhar a mesma arena, e o desempenho será mais lento devido ao uso de mutexes para bloquear threads acessando a mesma heap.

Referências

Azeria Labs
https://ir0nstone.gitbook.io/notes/binexp/heap/
The toddler’s introduction to Heap exploitation (Part 1) | by +Ch0pin🕷️ | InfoSec Write-ups
Introduction To GLIBC Heap Exploitation - Max Kamper
Heap Exploitation - Nightmare
Heap exploitation, glibc internals and nifty tricks. - Quarkslab’s blog