C Memória Segurança
C

Ponteiros & Memória

Um ponteiro é um valor que contém um endereço de memória — nada mais, nada menos. Esta simplicidade é ao mesmo tempo o que torna C poderoso e o que torna C perigoso: com acesso directo a qualquer endereço de memória, um erro de programação pode corromper dados arbitrários, executar código injectado, ou expor informação sensível. Perceber ponteiros é perceber porque buffer overflows existem, porque use-after-free é explorável, e o que a JVM sacrifica em troca de eliminar estas classes de bugs.

O Que é um Ponteiro

Toda a variável em C ocupa um endereço de memória. Um ponteiro é uma variável que guarda esse endereço — em vez de guardar um valor directamente, guarda a localização onde o valor está. Em sistemas 64-bit, todos os ponteiros têm 8 bytes, independentemente do tipo apontado.

// Declaração e uso básico de ponteiros:

int x = 42;         // variável inteira — valor 42, algures na memória
int* p = &x;        // ponteiro para int — guarda o endereço de x
                    // &x = "endereço de x" = ex: 0x00007FFE_A3B2C1D0

// Os três operadores fundamentais:
// &  — address-of: obtém o endereço de uma variável
// *  — dereference: acede ao valor no endereço guardado pelo ponteiro
// [] — indexação: açúcar sintáctico para aritmética de ponteiros

printf("%p\n",  p);   // imprime o endereço: 0x7ffe a3b2c1d0
printf("%d\n",  *p);  // dereferencia: imprime o valor em p → 42
printf("%d\n",  x);   // acesso directo → 42 (mesmo valor)

*p = 99;              // escreve 99 no endereço guardado por p
printf("%d\n",  x);   // → 99 — x foi modificado através do ponteiro

// Tamanho de um ponteiro:
printf("%zu\n", sizeof(int*));    // → 8 bytes (sistema 64-bit)
printf("%zu\n", sizeof(char*));   // → 8 bytes (todos os ponteiros têm 8 bytes em 64-bit)
printf("%zu\n", sizeof(void*));   // → 8 bytes

// Declaração — a posição do * é irrelevante para o compilador mas convencional:
int* p1;   // estilo C++ — o * "pertence ao tipo"
int *p2;   // estilo C   — o * "pertence à variável"
// int* a, b;  — ATENÇÃO: apenas a é ponteiro, b é int normal
// int *a, *b; — ambos são ponteiros

Aritmética de Ponteiros

Em C, é possível fazer aritmética sobre ponteiros — somar ou subtrair inteiros para avançar ou recuar na memória. A aritmética é automaticamente escalada pelo tamanho do tipo apontado. Esta funcionalidade é a base de como arrays funcionam em C — e a fonte de uma classe inteira de vulnerabilidades quando os limites não são verificados.

// Arrays em C são ponteiros para o primeiro elemento:

int arr[5] = {10, 20, 30, 40, 50};
//  arr[0]=10  arr[1]=20  arr[2]=30  arr[3]=40  arr[4]=50
//  endereço:  +0         +4         +8         +12        +16  (bytes, sizeof(int)=4)

int* p = arr;   // p aponta para arr[0]

// Indexação e aritmética são equivalentes:
arr[2]    == *(arr + 2)   // → 30 — notação de array = aritmética de ponteiro
p[2]      == *(p + 2)     // → 30 — mesma coisa

// p + 1 não avança 1 byte — avança sizeof(int) = 4 bytes
// p + 2 avança 8 bytes — aponta para arr[2]

// Percorrer um array com aritmética de ponteiros:
int* end = arr + 5;       // ponteiro "past the end" — endereço após o último elemento
for (int* it = arr; it != end; it++) {
    printf("%d ", *it);   // dereferencia cada posição
}
// → 10 20 30 40 50

// O que acontece sem verificação de limites — out-of-bounds:
printf("%d\n", arr[5]);   // ❌ undefined behaviour — acesso fora do array
printf("%d\n", arr[10]);  // ❌ undefined behaviour — pode ler memória de outra variável,
                          //    retornar lixo, ou causar segmentation fault
                          // Em Java: ArrayIndexOutOfBoundsException — determinístico
                          // Em C: comportamento indefinido — silencioso e perigoso

Stack vs Heap — Gestão de Memória em C

Em C, o programador controla explicitamente onde cada dado é alocado. A stack é gerida automaticamente pelo compilador — rápida, tamanho fixo por frame. O heap é gerido manualmente — malloc para alocar, free para libertar. Esquecer o free é um memory leak; usar memória depois do free é use-after-free.

// ── Stack — alocação automática ──────────────────────────────────────────────

void funcao_stack() {
    int x = 42;           // alocado na stack — libertado quando a função retorna
    char buf[256];        // 256 bytes na stack — libertados automaticamente
    int arr[10];          // 40 bytes na stack

    // x, buf, arr são válidos enquanto estamos dentro de funcao_stack()
}   // aqui: x, buf, arr são automaticamente libertados (stack pointer recua)

// Tamanho da stack: tipicamente 1-8 MB por thread
// Stack overflow: exceder o tamanho da stack — recursão infinita, arrays grandes
void recursao_infinita() {
    int arr[100000];      // 400 KB na stack a cada chamada
    recursao_infinita();  // → stack overflow em poucos níveis de recursão
}


// ── Heap — alocação dinâmica ──────────────────────────────────────────────────

#include <stdlib.h>
#include <string.h>

void funcao_heap() {
    // malloc — aloca n bytes no heap, retorna void* (ponteiro genérico)
    // retorna NULL se a alocação falhar — SEMPRE verificar
    int* arr = malloc(10 * sizeof(int));
    if (arr == NULL) {
        // tratamento de erro — malloc falhou (memória esgotada)
        return;
    }

    // calloc — aloca E inicializa a zero (malloc não inicializa — contém lixo)
    int* zeros = calloc(10, sizeof(int));

    // realloc — redimensionar uma alocação existente
    arr = realloc(arr, 20 * sizeof(int));
    if (arr == NULL) { /* erro */ }

    // usar o array...
    arr[0] = 42;
    memset(arr, 0, 20 * sizeof(int));  // inicializar a zero manualmente

    // free — libertar a memória alocada
    free(arr);     // obrigatório — não libertar = memory leak
    free(zeros);
    arr = NULL;    // boa prática: NULL após free — evita dangling pointer acidental

}   // a função retorna — arr e zeros são variáveis da stack (libertadas automaticamente)
    // mas a memória que elas apontavam estava no heap — por isso o free é necessário
// Visualização da memória durante funcao_heap():

// Stack (endereços altos → baixos):
// ┌─────────────────────────────────────┐  ← endereço alto
// │  arr   = 0x0000561A_B3C4D5E0  (8B) │  ← ponteiro — guarda endereço do heap
// │  zeros = 0x0000561A_B3C4F600  (8B) │  ← outro ponteiro para o heap
// └─────────────────────────────────────┘
//
// Heap (endereços baixos → altos):
// ┌─────────────────────────────────────┐  ← endereço baixo
// │  [lixo][lixo][lixo]...[lixo]  (40B)│  ← malloc(10 * sizeof(int)) — não inicializado
// │  addr: 0x0000561A_B3C4D5E0          │
// ├─────────────────────────────────────┤
// │  [0][0][0][0][0][0][0][0][0][0](40B)│  ← calloc(10, sizeof(int)) — inicializado a 0
// │  addr: 0x0000561A_B3C4F600          │
// └─────────────────────────────────────┘

Os Bugs de Memória Mais Comuns

Bugs de gestão de memória em C são a fonte de décadas de CVEs críticos. Ao contrário dos erros lógicos, estes bugs têm consequências de segurança directas — um atacante que controla os dados de input pode frequentemente transformar um bug de memória em execução de código arbitrário.

// ── 1. Memory Leak — esquecer o free ─────────────────────────────────────────

void processar_pedido(Request* req) {
    char* buffer = malloc(req->size);
    if (req->tipo == TIPO_INVALIDO) {
        return;   // ❌ memory leak — buffer nunca é libertado neste caminho
    }
    processar(buffer);
    free(buffer);  // só chega aqui se req->tipo != TIPO_INVALIDO
}

// Consequência: cada pedido inválido vaza req->size bytes
// Em servidores de longa duração: uso de memória cresce indefinidamente → crash
// Detecção: Valgrind, AddressSanitizer


// ── 2. Use-After-Free — usar memória depois de libertar ───────────────────────

int* criar() {
    int* p = malloc(sizeof(int));
    *p = 42;
    return p;
}

void bug_use_after_free() {
    int* p = criar();
    free(p);        // memória libertada — devolvida ao allocator

    printf("%d\n", *p);   // ❌ use-after-free — undefined behaviour
    // O allocator pode ter reutilizado esta memória para outra alocação
    // Ler: retorna lixo ou dados de outra estrutura
    // Escrever: corrompe dados de outra estrutura — potencialmente explorável

    *p = 99;        // ❌ use-after-free com escrita — ainda mais perigoso
}

// Exploit clássico:
// 1. Atacante faz free de um objecto com callback function pointer
// 2. Aloca novo objecto do mesmo tamanho com dados controlados pelo atacante
//    (o allocator reutiliza o mesmo bloco de memória)
// 3. Escreve um endereço malicioso na posição do function pointer
// 4. O código legítimo chama o callback → executa código do atacante


// ── 3. Dangling Pointer — ponteiro para memória inválida ─────────────────────

int* ponteiro_dangling() {
    int x = 42;      // variável na stack
    return &x;       // ❌ retorna endereço de variável local
}                    // x é destruído aqui — o endereço retornado é inválido

void usar_dangling() {
    int* p = ponteiro_dangling();
    printf("%d\n", *p);  // ❌ undefined behaviour — lê memória da stack que foi reutilizada
    // pode retornar 42 "por acidente" ou lixo total — não determinístico
}

// O compilador moderno avisa: warning: address of local variable 'x' returned
// Com -Wall este warning é imediato — mais uma razão para -Wall ser obrigatório


// ── 4. Double Free — libertar o mesmo bloco duas vezes ───────────────────────

void double_free_bug() {
    int* p = malloc(sizeof(int));
    free(p);
    free(p);    // ❌ double free — corrupção das estruturas internas do allocator
    // O allocator mantém uma lista de blocos livres
    // Double free insere o mesmo bloco duas vezes na lista
    // A alocação seguinte pode retornar o mesmo bloco para dois chamadores diferentes
    // → dois ponteiros para a mesma memória → race condition / corrupção
}

// CVE-2019-11477 (Linux kernel), CVE-2022-0185 — double free em código kernel


// ── 5. Buffer Overflow — escrever além dos limites ───────────────────────────

void buffer_overflow() {
    char buf[8];                  // 8 bytes na stack
    strcpy(buf, "AAAAAAAAAAAAA"); // copia 13 bytes (+ null) para 8 bytes
    // os 6 bytes extra sobrescrevem:
    // - variáveis locais adjacentes na stack
    // - o saved RBP (base pointer do frame anterior)
    // - o endereço de retorno da função
    // Quando a função retorna: CPU salta para o endereço sobrescrito pelo atacante
}

// gets() e strcpy() são as funções mais perigosas da libc — nunca usar
// Substituições seguras:
fgets(buf, sizeof(buf), stdin);          // em vez de gets()
strncpy(buf, src, sizeof(buf) - 1);      // em vez de strcpy()
buf[sizeof(buf) - 1] = '\0';             // garantir null-termination
snprintf(buf, sizeof(buf), "%s", src);   // em vez de sprintf()

Ponteiros para Funções

Em C é possível ter ponteiros para funções — variáveis que guardam o endereço de uma função e permitem chamá-la indirectamente. São a base de callbacks, polimorfismo em C, e tabelas de dispatch — e um vector de ataque crítico em exploits de use-after-free.

// Ponteiros para funções — declaração e uso:

// Declaração: tipo_retorno (*nome)(tipos_parametros)
int (*operacao)(int, int);   // ponteiro para função que recebe dois int e retorna int

int somar(int a, int b) { return a + b; }
int multiplicar(int a, int b) { return a * b; }

operacao = somar;             // atribuir endereço da função
printf("%d\n", operacao(3, 4)); // → 7 — chamada indirecta

operacao = multiplicar;
printf("%d\n", operacao(3, 4)); // → 12

// Uso prático — callback:
void processar_array(int* arr, int n, int (*transformar)(int)) {
    for (int i = 0; i < n; i++) {
        arr[i] = transformar(arr[i]);
    }
}

int dobrar(int x)  { return x * 2; }
int quadrado(int x) { return x * x; }

processar_array(arr, 10, dobrar);    // passa dobrar como callback
processar_array(arr, 10, quadrado);  // passa quadrado como callback

// Virtual dispatch em C — simular polimorfismo com tabela de funções:
typedef struct {
    void (*render)(void* self);    // função virtual "render"
    void (*update)(void* self);    // função virtual "update"
    void (*destroy)(void* self);   // função virtual "destroy"
} VTable;

typedef struct {
    VTable* vtable;   // ponteiro para a tabela de funções
    // dados do objecto...
} Componente;

// Em C++, o compilador gera esta VTable automaticamente para classes com virtual
// Em Java, a JVM gere a dispatch table — o programador não tem acesso directo

// Relevância para segurança:
// Use-after-free num objecto com VTable → atacante sobrescreve vtable pointer
// → próxima chamada virtual executa endereço controlado pelo atacante

Como a JVM Elimina (e Não Elimina) Estes Bugs

A JVM foi desenhada explicitamente para eliminar as classes de bugs de gestão de memória que assolam C. Mas esta eliminação tem um custo — e não cobre tudo.

// O que a JVM elimina comparado com C:

// ✅ Buffer overflow de array:
// C:    arr[10] em array de 5 → undefined behaviour silencioso
// Java: arr[10] em array de 5 → ArrayIndexOutOfBoundsException determinística

// ✅ Use-after-free:
// C:    free(p); *p = 42; → corrupção de memória
// Java: não existe free manual — o GC liberta quando não há mais referências
//       enquanto existe uma referência para o objecto, o GC não o liberta

// ✅ Dangling pointer:
// C:    retornar endereço de variável local → acesso a memória inválida
// Java: referências para objectos são sempre válidas enquanto existem
//       a JVM garante que o objecto não é recolhido enquanto é referenciado

// ✅ Double free:
// C:    free(p); free(p); → corrupção do heap allocator
// Java: não existe free manual — o GC gere a libertação automaticamente

// ✅ Aritmética de ponteiros arbitrária:
// C:    int* p = (int*)0xDEADBEEF; *p = 42; → escreve em endereço arbitrário
// Java: não existem ponteiros raw — referências são verificadas pelo runtime

// ❌ O que a JVM NÃO elimina:
// Null pointer dereference → NullPointerException (melhor que UB, mas ainda é erro)
// Integer overflow         → silencioso em Java, como em C
// Logic errors             → a JVM não verifica a correcção do algoritmo
// Memory leaks lógicos     → guardar referências desnecessariamente impede o GC
//                            ex: adicionar a uma List estática sem nunca remover

// O custo da segurança da JVM:
// - Overhead de verificação de limites em cada acesso a array (~1-5% em JIT optimizado)
// - Overhead do Garbage Collector — pausas GC, uso de memória 2-5× maior
// - Sem acesso a endereços de memória raw — impossível implementar certas optimizações
// - Startup lento — JVM precisa de inicializar antes de executar código
Rust — a terceira via Rust elimina as classes de bugs de memória de C sem o overhead de runtime da JVM. Em vez de verificações em runtime (JVM) ou sem verificações (C), Rust usa um sistema de tipos e um borrow checker para provar em tempo de compilação que o programa não tem use-after-free, dangling pointers, ou data races. Zero overhead de runtime — o código máquina gerado é equivalente a C bem escrito. O custo é uma curva de aprendizagem significativamente maior.

Ferramentas de Detecção

Em C, bugs de memória são frequentemente silenciosos em desenvolvimento e exploráveis em produção. Um conjunto de ferramentas padrão da indústria detecta estes bugs antes que cheguem a produção.

// Valgrind — detector de erros de memória em runtime
valgrind --leak-check=full --show-leak-kinds=all ./programa
// Detecta: memory leaks, use-after-free, leituras de memória não inicializada
// Overhead: ~20-50× mais lento — usar apenas em testes, nunca em produção

// AddressSanitizer (ASan) — mais rápido que Valgrind, integrado no compilador
gcc -fsanitize=address -g -o programa programa.c
./programa
// Detecta: buffer overflow (stack e heap), use-after-free, double free
// Overhead: ~2× mais lento, ~3× mais memória — usar em CI e testes

// UBSanitizer (UBSan) — detecta undefined behaviour
gcc -fsanitize=undefined -g -o programa programa.c
// Detecta: integer overflow signed, null dereference, desalinhamento, ...
// Pode combinar com ASan: -fsanitize=address,undefined

// Static analysis — sem executar o programa
clang --analyze programa.c     // Clang Static Analyzer
cppcheck programa.c            // CPPCheck — open source
// Detecta padrões de bug sem executar — útil para code review automatizado

// Exemplo de saída do ASan para use-after-free:
// ==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
// READ of size 4 at 0x602000000010 thread T0
//     #0 0x401234 in usar_dangling programa.c:42
//     #1 0x401456 in main programa.c:60
// 0x602000000010 was freed in:
//     #0 0x7f... in free (...)
//     #1 0x401123 in funcao programa.c:38

Checklist Ponteiros & Memória