hacktricks/binary-exploitation/heap/bins-and-memory-allocations.md

21 KiB

Bins & Alocações de Memória

Aprenda hacking AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras maneiras de apoiar o HackTricks:

Informações Básicas

Para melhorar a eficiência na forma como os chunks são armazenados, cada chunk não está apenas em uma lista encadeada, mas existem vários tipos. Estes são os bins e existem 5 tipos de bins: 62 small bins, 63 large bins, 1 unsorted bin, 10 fast bins e 64 tcache bins por thread.

O endereço inicial para cada bin não ordenado, small e large está dentro do mesmo array. O índice 0 não é usado, 1 é o bin não ordenado, os bins 2-64 são os small bins e os bins 65-127 são os large bins.

Small Bins

Os small bins são mais rápidos do que os large bins, mas mais lentos do que os fast bins.

Cada bin dos 62 terá chunks do mesmo tamanho: 16, 24, ... (com um tamanho máximo de 504 bytes em 32 bits e 1024 em 64 bits). Isso ajuda na velocidade para encontrar o bin onde um espaço deve ser alocado e inserir e remover entradas nessas listas.

Large Bins

Ao contrário dos small bins, que gerenciam chunks de tamanhos fixos, cada large bin lida com uma faixa de tamanhos de chunk. Isso é mais flexível, permitindo que o sistema acomode vários tamanhos sem precisar de um bin separado para cada tamanho.

Em um alocador de memória, os large bins começam onde os small bins terminam. As faixas para os large bins crescem progressivamente, o que significa que o primeiro bin pode cobrir chunks de 512 a 576 bytes, enquanto o próximo cobre de 576 a 640 bytes. Esse padrão continua, com o maior bin contendo todos os chunks acima de 1MB.

Os large bins são mais lentos de operar em comparação com os small bins porque eles precisam ordenar e pesquisar em uma lista de tamanhos de chunk variados para encontrar o melhor encaixe para uma alocação. Quando um chunk é inserido em um large bin, ele precisa ser ordenado, e quando a memória é alocada, o sistema deve encontrar o chunk certo. Esse trabalho extra os torna mais lentos, mas como alocações grandes são menos comuns do que as pequenas, é uma troca aceitável.

Existem:

  • 32 bins de faixa de 64B
  • 16 bins de faixa de 512B
  • 8 bins de faixa de 4096B
  • 4 bins de faixa de 32768B
  • 2 bins de faixa de 262144B
  • 1 bin para tamanhos restantes

Unsorted Bin

O unsorted bin é um cache rápido usado pelo gerenciador de heap para tornar a alocação de memória mais rápida. Veja como funciona: Quando um programa libera memória, o gerenciador de heap não a coloca imediatamente em um bin específico. Em vez disso, primeiro tenta fundir com quaisquer chunks livres vizinhos para criar um bloco maior de memória livre. Em seguida, coloca esse novo chunk em um bin geral chamado "unsorted bin".

Quando um programa solicita memória, o gerenciador de heap verifica o unsorted bin para ver se há um chunk de tamanho suficiente. Se encontrar um, ele o usa imediatamente. Se não encontrar um chunk adequado, move os chunks liberados para seus bins correspondentes, seja small ou large, com base em seus tamanhos.

Portanto, o unsorted bin é uma maneira de acelerar a alocação de memória reutilizando rapidamente a memória liberada recentemente e reduzindo a necessidade de pesquisas e fusões demoradas.

{% hint style="danger" %} Observe que mesmo que os chunks sejam de categorias diferentes, se um chunk disponível estiver colidindo com outro chunk disponível (mesmo que sejam de categorias diferentes), eles serão mesclados. {% endhint %}

Fast Bins

Os fast bins são projetados para acelerar a alocação de memória para pequenos chunks mantendo chunks liberados recentemente em uma estrutura de acesso rápido. Esses bins usam uma abordagem Last-In, First-Out (LIFO), o que significa que o chunk liberado mais recentemente é o primeiro a ser reutilizado quando há uma nova solicitação de alocação. Esse comportamento é vantajoso para a velocidade, pois é mais rápido inserir e remover do topo de uma pilha (LIFO) em comparação com uma fila (FIFO).

Além disso, os fast bins usam listas encadeadas simples, não duplamente encadeadas, o que melhora ainda mais a velocidade. Como os chunks nos fast bins não são mesclados com vizinhos, não há necessidade de uma estrutura complexa que permita a remoção do meio. Uma lista encadeada simples é mais simples e rápida para essas operações.

Basicamente, o que acontece aqui é que o cabeçalho (o ponteiro para o primeiro chunk a ser verificado) está sempre apontando para o chunk liberado mais recentemente desse tamanho. Portanto:

  • Quando um novo chunk é alocado desse tamanho, o cabeçalho está apontando para um chunk livre para usar. Como esse chunk livre está apontando para o próximo a ser usado, esse endereço é armazenado no cabeçalho para que a próxima alocação saiba onde obter um chunk disponível.
  • Quando um chunk é liberado, o chunk livre salvará o endereço para o chunk disponível atual e o endereço para esse chunk recém-liberado será colocado no cabeçalho.

{% hint style="danger" %} Chunks nos fast bins não são definidos automaticamente como disponíveis, então eles permanecem como chunks de fast bin por algum tempo em vez de poderem ser mesclados com outros chunks. {% endhint %}

Tcache (Cache por Thread) Bins

Mesmo que as threads tentem ter sua própria heap (veja Arenas e Subheaps), há a possibilidade de que um processo com muitas threads (como um servidor web) acabe compartilhando a heap com outras threads. Nesse caso, a solução principal é o uso de lockers, que podem desacelerar significativamente as threads.

Portanto, um tcache é semelhante a um fast bin por thread no sentido de que é uma lista encadeada simples que não mescla chunks. Cada thread tem 64 tcache bins encadeados simples. Cada bin pode ter um máximo de 7 chunks do mesmo tamanho variando de 24 a 1032B em sistemas de 64 bits e de 12 a 516B em sistemas de 32 bits.

Quando uma thread libera um chunk, se não for muito grande para ser alocado no tcache e o respectivo bin do tcache não estiver cheio (já com 7 chunks), ele será alocado lá. Se não puder ir para o tcache, precisará esperar o bloqueio da heap para poder realizar a operação de liberação globalmente.

Quando um chunk é alocado, se houver um chunk livre do tamanho necessário no Tcache, ele será usado, caso contrário, precisará esperar o bloqueio da heap para poder encontrar um nos bins globais ou criar um novo.
Há também uma otimização, nesse caso, enquanto tiver o bloqueio da heap, a thread preencherá seu Tcache com chunks da heap (7) do tamanho solicitado, para que, caso precise de mais, os encontre no Tcache.

Fluxo de Alocação

{% hint style="success" %} (Esta explicação atual é de https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/core_functions. TODO: Verificar última versão e atualizá-la) {% endhint %}

As alocações são finalmente realizadas com a função: void * _int_malloc (mstate av, size_t bytes) e seguem esta ordem:

  1. Atualiza bytes para cuidar dos alinhamentos, etc.
  2. Verifica se av é NULL ou não.
  3. No caso de ausência de arena utilizável (quando av é NULL), chama sysmalloc para obter um chunk usando mmap. Se bem-sucedido, chama alloc_perturb. Retorna o ponteiro.
  4. Dependendo do tamanho:
  • [Adição ao original] Usa tcache antes de verificar o próximo fastbin.
  • [Adição ao original] Se não houver tcache, mas um bin diferente for usado (ver etapa posterior), tenta preencher a tcache a partir desse bin.
  • Se o tamanho estiver na faixa do fastbin:
  1. Obtém o índice na matriz fastbin para acessar um bin apropriado de acordo com o tamanho solicitado.
  2. Remove o primeiro chunk nesse bin e faz com que victim aponte para ele.
  3. Se victim for NULL, passa para o próximo caso (smallbin).
  4. Se victim não for NULL, verifica o tamanho do chunk para garantir que pertença a esse bin específico. Caso contrário, é lançado um erro ("malloc(): corrupção de memória (fast)").
  5. Chama alloc_perturb e então retorna o ponteiro.
  • Se o tamanho estiver na faixa do smallbin:
  1. Obtém o índice na matriz smallbin para acessar um bin apropriado de acordo com o tamanho solicitado.
  2. Se não houver chunks neste bin, passa para o próximo caso. Isso é verificado comparando os ponteiros bin e bin->bk.
  3. victim é igual a bin->bk (o último chunk no bin). Se for NULL (ocorre durante a inicialização), chama malloc_consolidate e pula esta etapa completa de verificação em bins diferentes.
  4. Caso contrário, quando victim não for NULL, verifica se victim->bk->fd e victim são iguais ou não. Se não forem iguais, é lançado um erro (malloc(): lista duplamente vinculada smallbin corrompida).
  5. Define o bit PREV_INSUSE para o próximo chunk (na memória, não na lista duplamente vinculada) para victim.
  6. Remove este chunk da lista do bin.
  7. Define o bit de arena apropriado para este chunk dependendo de av.
  8. Chama alloc_perturb e então retorna o ponteiro.
  • Se o tamanho não estiver na faixa do smallbin:
  1. Obtém o índice na matriz largebin para acessar um bin apropriado de acordo com o tamanho solicitado.
  2. Verifica se av possui fastchunks ou não. Isso é feito verificando o FASTCHUNKS_BIT em av->flags. Se sim, chama malloc_consolidate em av.
  3. Se nenhum ponteiro foi retornado ainda, isso significa um ou mais dos seguintes casos:
  4. O tamanho está na faixa do 'fastbin' mas nenhum fastchunk está disponível.
  5. O tamanho está na faixa do 'smallbin' mas nenhum smallchunk está disponível (chama malloc_consolidate durante a inicialização).
  6. O tamanho está na faixa do 'largebin'.
  7. Em seguida, os chunks não ordenados são verificados e os chunks percorridos são colocados em bins. Este é o único lugar onde os chunks são colocados em bins. Itera o bin não ordenado a partir do 'TAIL'.
  8. victim aponta para o chunk atual em consideração.
  9. Verifica se o tamanho do chunk de victim está dentro da faixa mínima (2*SIZE_SZ) e máxima (av->system_mem). Lança um erro (malloc(): corrupção de memória) caso contrário.
  10. Se (o tamanho do chunk solicitado está na faixa do smallbin) e (victim é o último chunk restante) e (é o único chunk no bin não ordenado) e (o tamanho dos chunks >= o solicitado): Divide o chunk em 2 chunks:
  • O primeiro chunk corresponde ao tamanho solicitado e é retornado.
  • O chunk restante se torna o novo último chunk restante. Ele é reinserido no bin não ordenado.
  1. Define os campos chunk_size e chunk_prev_size apropriadamente para ambos os chunks.
  2. O primeiro chunk é retornado após chamar alloc_perturb.
  3. Se a condição acima for falsa, o controle chega aqui. Remove victim do bin não ordenado. Se o tamanho de victim corresponder exatamente ao tamanho solicitado, retorne este chunk após chamar alloc_perturb.
  4. Se o tamanho de victim estiver na faixa do smallbin, adicione o chunk no smallbin apropriado no HEAD.
  5. Caso contrário, insira no largebin apropriado mantendo a ordem classificada:
  6. Primeiro verifica o último chunk (menor). Se victim for menor que o último chunk, insere-o no final.
  7. Caso contrário, faça um loop para encontrar um chunk com tamanho >= tamanho de victim. Se o tamanho for exatamente o mesmo, sempre insira na segunda posição.
  8. Repita toda essa etapa um máximo de MAX_ITERS (10000) vezes ou até que todos os chunks no bin não ordenado se esgotem.
  9. Após verificar os chunks não ordenados, verifique se o tamanho solicitado não está na faixa do smallbin, se sim, verifique os largebins.
  10. Obtém o índice na matriz largebin para acessar um bin apropriado de acordo com o tamanho solicitado.
  11. Se o tamanho do maior chunk (o primeiro chunk no bin) for maior que o tamanho solicitado:
  12. Itera a partir do 'TAIL' para encontrar um chunk (victim) com o menor tamanho >= o tamanho solicitado.
  13. Chama unlink para remover o chunk victim do bin.
  14. Calcula remainder_size para o chunk de victim (este será o tamanho do chunk de victim - tamanho solicitado).
  15. Se este remainder_size >= MINSIZE (o tamanho mínimo do chunk incluindo os cabeçalhos), divide o chunk em dois chunks. Caso contrário, o chunk inteiro de victim será retornado. Insira o chunk restante no bin não ordenado (no final do 'TAIL'). É feita uma verificação no bin não ordenado se unsorted_chunks(av)->fd->bk == unsorted_chunks(av). Um erro é lançado caso contrário ("malloc(): chunks não ordenados corrompidos").
  16. Retorna o chunk victim após chamar alloc_perturb.
  17. Até agora, verificamos o bin não ordenado e também o respectivo fast, small ou large bin. Observe que um único bin (fast ou small) foi verificado usando o tamanho exato do chunk solicitado. Repita as seguintes etapas até que todos os bins se esgotem:
  18. O índice na matriz bin é incrementado para verificar o próximo bin.
  19. Usa o mapa av->binmap para pular os bins vazios.
  20. victim aponta para o 'TAIL' do bin atual.
  21. Usando o binmap garante que se um bin for pulado (no segundo passo acima), ele está definitivamente vazio. No entanto, não garante que todos os bins vazios serão pulados. Verifique se o victim está vazio ou não. Se estiver vazio, pule novamente o bin e repita o processo acima (ou 'continue' este loop) até chegarmos a um bin não vazio.
  22. Divida o chunk (victim aponta para o último chunk de um bin não vazio) em dois chunks. Insira o chunk restante no bin não ordenado (no final do 'TAIL'). É feita uma verificação no bin não ordenado se unsorted_chunks(av)->fd->bk == unsorted_chunks(av). Um erro é lançado caso contrário ("malloc(): chunks não ordenados corrompidos 2").
  23. Retorna o chunk victim após chamar alloc_perturb.
  24. Se ainda nenhum bin vazio for encontrado, o chunk 'top' será usado para atender à solicitação:
  25. victim aponta para av->top.
  26. Se o tamanho do chunk 'top' >= 'tamanho solicitado' + MINSIZE, divida-o em dois chunks. Neste caso, o chunk restante se torna o novo chunk 'top' e o outro chunk é retornado ao usuário após chamar alloc_perturb.
  27. Verifica se av possui fastchunks ou não. Isso é feito verificando o FASTCHUNKS_BIT em av->flags. Se sim, chama malloc_consolidate em av. Retorna para a etapa 6 (onde verificamos o bin não ordenado).
  28. Se av não tiver fastchunks, chama sysmalloc e retorna o ponteiro obtido após chamar alloc_perturb.

Fluxo Livre

{% hint style="success" %} (Esta explicação atual é de https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/core_functions. TODO: Verificar última versão e atualizá-la) {% endhint %}

A função final que libera pedaços de memória é _int_free (mstate av, mchunkptr p, int have_lock):

  1. Verificar se p está antes de p + chunksize(p) na memória (para evitar embrulhar). Um erro (free(): ponteiro inválido) é lançado caso contrário.
  2. Verificar se o pedaço tem pelo menos o tamanho MINSIZE ou é um múltiplo de MALLOC_ALIGNMENT. Um erro (free(): tamanho inválido) é lançado caso contrário.
  3. Se o tamanho do pedaço estiver na lista fastbin:
  4. Verificar se o tamanho do próximo pedaço está entre o tamanho mínimo e máximo (av->system_mem), lançar um erro (free(): tamanho próximo inválido (rápido)) caso contrário.
  5. Chamar free_perturb no pedaço.
  6. Definir FASTCHUNKS_BIT para av.
  7. Obter índice na matriz fastbin de acordo com o tamanho do pedaço.
  8. Verificar se o topo do bin não é o pedaço que estamos adicionando. Caso contrário, lançar um erro (dupla liberação ou corrupção (topo rápido)).
  9. Verificar se o tamanho do pedaço fastbin no topo é o mesmo que o pedaço que estamos adicionando. Caso contrário, lançar um erro (entrada fastbin inválida (liberação)).
  10. Inserir o pedaço no topo da lista fastbin e retornar.
  11. Se o pedaço não estiver mapeado:
  12. Verificar se o pedaço é o pedaço superior ou não. Se sim, um erro (dupla liberação ou corrupção (topo)) é lançado.
  13. Verificar se o próximo pedaço (por memória) está dentro dos limites da arena. Se não estiver, um erro (dupla liberação ou corrupção (fora)) é lançado.
  14. Verificar se o bit anterior em uso do próximo pedaço (por memória) está marcado ou não. Se não estiver, um erro (dupla liberação ou corrupção (!prev)) é lançado.
  15. Verificar se o tamanho do próximo pedaço está entre o tamanho mínimo e máximo (av->system_mem). Se não estiver, um erro (free(): tamanho próximo inválido (normal)) é lançado.
  16. Chamar free_perturb no pedaço.
  17. Se o pedaço anterior (por memória) não estiver em uso, chamar unlink no pedaço anterior.
  18. Se o próximo pedaço (por memória) não for o pedaço superior:
  19. Se o próximo pedaço não estiver em uso, chamar unlink no próximo pedaço.
  20. Mesclar o pedaço com o anterior, próximo (por memória), se algum estiver livre e adicioná-lo ao início do bin não ordenado. Antes de inserir, verificar se unsorted_chunks(av)->fd->bk == unsorted_chunks(av) ou não. Se não, um erro ("free(): pedaços não ordenados corrompidos") é lançado.
  21. Se o próximo pedaço (por memória) era um pedaço superior, mesclar os pedaços apropriadamente em um único pedaço superior.
  22. Se o pedaço estava mapeado, chamar munmap_chunk.

Verificações de Segurança das Funções de Heap

Verifique as verificações de segurança realizadas por funções amplamente utilizadas no heap em:

{% content-ref url="heap-functions-security-checks.md" %} heap-functions-security-checks.md {% endcontent-ref %}

Referências

Aprenda hacking AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras maneiras de apoiar o HackTricks: