Série: Exploração de Binários

Visão Geral de Segurança de Memória: Stack e Heap.

Postado em

Onde os dados vivem

Entender a exploração de binários começa por onde os dados realmente vivem, a stack e o heap. Na prática, a stack segue o rigor do LIFO, onde cada chamada de uma função empilha um frame com variáveis locais, registradores salvos e o endereço de retorno. Quando a função termina, o frame simplesmente desaparece, nada de free manual, nada de “esquecer de desalocar”.

Já o heap é outra história. Ele funciona como um gerenciador de blocos dinâmicos (malloc, new) que aceita alocações e liberações em ordem arbitrária. Essa liberdade traz um monte de dor de cabeça, fragmentação interna, necessidade de chamar free ou delete explicitamente e, claro, a festa começa para o atacante assim que alguém deixa um ponteiro solto ou escreve fora dos limites.

Use‑after‑free, heap spraying e outras formas de corrupção de memória são quase garantidas quando a superfície de exposição aumenta.


Stack vs. Heap: modelo de alocação e vetores de ataque

A disposição física desses segmentos numa máquina x86‑64 deixa a teoria de lado e encara a realidade. No mapeamento de memória abaixo (vmmap), observe a diferença nos endereços, a Stack (em roxo) reside no topo da memória (endereços altos iniciando em 0x7fff…), enquanto o Heap (em verde) começa lá embaixo, próximo ao código (endereços 0x5555…). Esse abismo entre eles é onde a batalha pela memória acontece.

Mapeamento de memória no GDB evidenciando Stack e Heap

Em C, o programador mexe nos ponteiros como quem brinca com fósforos.

#include <stdio.h>

int main() {
    // 1. Declaramos uma variável normal chamada 'var' e iniciamos com 0
    int var = 0;
 
    // Criamos um ponteiro 'p' que guarda o endereço de memória de 'var'
    int *p = &var; 
    
    // Usamos o ponteiro para ir até esse endereço e alterar o valor para 42
    *p = 42;

    // 3. Imprimimos o valor de 'var' para comprovar que foi alterado
    printf("O valor de var agora é: %d\n", var);

    return 0; // Indica que o programa terminou com sucesso
}

Pode fazer aritmética de ponteiros, acessar qualquer região de memória e, consequentemente, abrir um leque enorme de vetores de ataque.

A prova dessa “proximidade perigosa” com o hardware está na imagem abaixo. Observe o destaque em azul, o valor 0x44434241 (o hexadecimal para "ABCD") está gravado diretamente no topo da Stack. Não há mágica nem proteção oculta, a variável está lá, exposta e editável.

Variável ABCD no topo da Stack visualizada no GDB

A sobrescrita de return address, corrupção de metadados do heap, injeção de shellcode… tudo isso é trivial se o código falhar ao validar limites ou ao liberar recursos. Um simples write‑out‑of‑bounds pode gerar um segmentation fault imediato, mas também pode passar despercebido até que o programa retorne a um ponto crítico, disparando um stack smashing detectado apenas por canários de integridade.


C versus Python: controle de ponteiros e superfície de exposição

Python, por outro lado, esconde os ponteiros atrás de um coletor de lixo que combina contagem de referências com coleta de ciclos. O programador raramente vê um pointer direto, mas os objetos são referenciados por handles internos mantidos pela VM. Essa abstração corta a superfície de exposição a use‑after‑free visível, porque a liberação de memória acontece automaticamente. Mas convenhamos, a segurança não é absoluta.

Vulnerabilidades ainda surgem em extensões escritas em C ou em partes críticas do próprio interpretador. Quando isso acontece, o atacante acaba seguindo os mesmos caminhos de C, corrompendo estruturas internas do heap do interpretador ou explorando falhas de validação em módulos nativos, só que agora o caminho até lá costuma ser mais longo, exigindo um gadget chain que atravessa a camada de abstração do Python.

Mas para resumir cada abordagem, vou colocar 3 três pontos que surgem naturalmente:

  • Controle direto vs. indireto. C dá controle total, mas cada operação insegura abre um canal de ataque. Python delega esse controle ao runtime, isso mitiga erros comuns, mas transfere a responsabilidade para a robustez da própria VM.
  • Visibilidade de falhas. Em C, um acesso ilegal dispara SIGSEGV, facilitando a detecção nos testes. Em Python, a mesma situação pode acabar em uma exceção genérica ou, pior ainda, em um crash silencioso do interpretador quando código nativo viola invariantes.
  • Superfície de ataque. C traz buffer overflow, format string, integer overflow e heap corruption. Python restringe a superfície a bugs em extensões C, deserialização insegura e falhas de isolamento entre objetos gerenciados.

Mitigações recomendadas

Agora, como reduzir a exposição? A prática recomendada começa na fase de compilação e na configuração do ambiente.

Primeiro, ative ASLR (sysctl kernel.randomize_va_space=2). Isso embaralha as bases da stack e do heap, dificultando a predição de endereços críticos. Depois, compile com -z noexecstack para garantir que a pilha seja marcada como não executável, nada de injetar shellcode direto na stack.

Flags como -fstack-protector-strong ou -fstack-clash-protection inserem canários que detectam sobrescritas antes do retorno da função, e -D_FORTIFY_SOURCE=2 adiciona checagens de limites nas chamadas de biblioteca padrão.

Do lado do heap, habilite tcache hardening (MALLOC_CONF="tcache:true,lg_tcache_max:0") e ative malloc_check (export MALLOC_CHECK_=3). Essas opções reforçam a integridade dos metadados alocados dinamicamente, tornando mais difícil corromper a lista livre.

Para entender o que estamos protegendo, veja a inspeção abaixo. O comando x/4gx revela o cabeçalho do nosso chunk alocado. O valor 0x291 não é mágico, ele indica o tamanho do bloco e a flag PREV_INUSE ativa. É alterando esses bits que a maioria dos exploits de heap modernos começa.

Cabeçalho de chunk heap examinado com x/4gx no GDB

Mas, vamos ser honestos, nenhuma dessas medidas é bala de prata. ASLR pode ser burlado por vazamentos de ponteiros, canários podem ser pulados se o atacante conseguir um write‑what‑where preciso, e tcache hardening tem limites conhecidos. Por isso, a camada de isolamento de processos também entra em cena. Executar serviços críticos dentro de containers ou namespaces limita o alcance de um eventual comprometimento de memória, impedindo que um vetor de ataque q explore corrupção de heap se propague para outros componentes do sistema.


Considerações finais

Com essa combinação de mitigação em tempo de compilação, randomização de endereços e isolamento de execução forma a base sobre a qual se constroem análises mais avançadas, seja para demonstrar um clássico stack overflow ou para encadear gadgets em um interpretador Python vulnerável. E, como sempre, teste tudo na prática, a teoria ajuda, mas o verdadeiro aprendizado vem quando você vê aquele segfault inesperado aparecer na sua tela depois de horas de depuração.

Afinal, como dizem por aí, “se o código não quebra, ele ainda não foi testado suficientemente”.

Categoria: Engenharia Reversa, Security, Backend
Gostou do conteúdo?

Se este artigo foi útil para você, considere apoiar meu trabalho!

Me compre um café

Comentários