Variáveis Não Inicializadas

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Informações Básicas

A ideia central aqui é entender o que acontece com variáveis não inicializadas, pois elas terão o valor que já estava na área de memória atribuída a elas. Exemplo:

  • Function 1: initializeVariable: Declaramos uma variável x e atribuímos um valor, digamos 0x1234. Essa ação é semelhante a reservar um espaço na memória e colocar um valor específico nele.
  • Function 2: useUninitializedVariable: Aqui, declaramos outra variável y mas não atribuímos nenhum valor a ela. Em C, variáveis não inicializadas não são automaticamente zeradas. Em vez disso, elas retêm o valor que foi armazenado por último na sua posição de memória.

Quando executamos essas duas funções sequencialmente:

  1. Em initializeVariable, x recebe um valor (0x1234), que ocupa um endereço de memória específico.
  2. Em useUninitializedVariable, y é declarada mas não recebe valor, então ocupa a posição de memória imediatamente após x. Por não inicializar y, ela acaba “herdando” o valor da mesma posição de memória usada por x, porque esse foi o último valor presente ali.

Esse comportamento ilustra um conceito chave em programação de baixo nível: o gerenciamento de memória é crucial, e variáveis não inicializadas podem levar a comportamentos imprevisíveis ou vulnerabilidades de segurança, pois podem reter inadvertidamente dados sensíveis deixados na memória.

Variáveis de stack não inicializadas podem representar vários riscos de segurança, como:

  • Data Leakage: Informações sensíveis, como senhas, chaves de criptografia ou dados pessoais, podem ser expostas se armazenadas em variáveis não inicializadas, permitindo que atacantes potencialmente leiam esses dados.
  • Information Disclosure: O conteúdo de variáveis não inicializadas pode revelar detalhes sobre o layout de memória do programa ou operações internas, auxiliando atacantes a desenvolver exploits direcionados.
  • Crashes and Instability: Operações envolvendo variáveis não inicializadas podem resultar em comportamento indefinido, levando a falhas do programa ou resultados imprevisíveis.
  • Arbitrary Code Execution: Em certos cenários, atacantes poderiam explorar essas vulnerabilidades para alterar o fluxo de execução do programa, possibilitando a execução de código arbitrário, o que pode incluir ameaças de execução remota de código.

Exemplo

#include <stdio.h>

// Function to initialize and print a variable
void initializeAndPrint() {
int initializedVar = 100; // Initialize the variable
printf("Initialized Variable:\n");
printf("Address: %p, Value: %d\n\n", (void*)&initializedVar, initializedVar);
}

// Function to demonstrate the behavior of an uninitialized variable
void demonstrateUninitializedVar() {
int uninitializedVar; // Declare but do not initialize
printf("Uninitialized Variable:\n");
printf("Address: %p, Value: %d\n\n", (void*)&uninitializedVar, uninitializedVar);
}

int main() {
printf("Demonstrating Initialized vs. Uninitialized Variables in C\n\n");

// First, call the function that initializes its variable
initializeAndPrint();

// Then, call the function that has an uninitialized variable
demonstrateUninitializedVar();

return 0;
}

Como Isso Funciona:

  • initializeAndPrint Function: Esta função declara uma variável inteira initializedVar, atribui a ela o valor 100 e então imprime tanto o endereço de memória quanto o valor da variável. Este passo é simples e mostra como uma variável inicializada se comporta.
  • demonstrateUninitializedVar Function: Nesta função, declaramos uma variável inteira uninitializedVar sem inicializá-la. Ao tentar imprimir seu valor, a saída pode mostrar um número aleatório. Esse número representa quaisquer dados que estavam previamente naquela posição de memória. Dependendo do ambiente e do compilador, a saída real pode variar, e às vezes, por segurança, alguns compiladores podem inicializar automaticamente variáveis com zero, embora não se deva confiar nisso.
  • main Function: A função main chama ambas as funções acima em sequência, demonstrando o contraste entre uma variável inicializada e uma não inicializada.

Padrões práticos de exploração (2024–2025)

O clássico bug “read-before-write” continua relevante porque mitigações modernas (ASLR, canaries) frequentemente dependem do sigilo. Superfícies de ataque típicas:

  • Structs parcialmente inicializados copiados para userland: Kernel ou drivers frequentemente memset apenas um campo de comprimento e então copy_to_user(&u, &local_struct, sizeof(local_struct)). Padding e campos não usados leak metade(s) do stack canary, saved frame pointers ou kernel pointers. Se o struct contém um function pointer, deixá-lo não inicializado pode também permitir controlled overwrite quando for reutilizado mais tarde.
  • Buffers de stack não inicializados reutilizados como indexes/lengths: Um size_t len; não inicializado usado para limitar read(fd, buf, len) pode dar aos atacantes leituras/escritas out-of-bounds ou permitir contornar checagens de tamanho quando a posição na stack ainda contém um valor grande de uma chamada anterior.
  • Padding adicionado pelo compilador: Mesmo quando membros individuais são inicializados, bytes de padding implícitos entre eles não são. Copiar o struct inteiro para userland leak padding que frequentemente contém conteúdo anterior da stack (canaries, pointers).
  • ROP/Canary disclosure: Se uma função copia um struct local para stdout para debugging, padding não inicializado pode revelar o stack canary, permitindo exploração de stack overflow subsequente sem brute-force.

Padrão mínimo de PoC para detectar esses problemas durante a revisão:

struct msg {
char data[0x20];
uint32_t len;
};

ssize_t handler(int fd) {
struct msg m;              // never fully initialized
m.len = read(fd, m.data, sizeof(m.data));
// later debug helper
write(1, &m, sizeof(m));   // leaks padding + stale stack
return m.len;
}

Mitigações & opções do compilador (tenha em mente ao contornar)

  • Clang/GCC auto-init: Toolchains recentes expõem -ftrivial-auto-var-init=zero ou -ftrivial-auto-var-init=pattern, preenchendo toda variável automática (stack) na entrada da função com zeros ou um padrão de poison (0xAA / 0xFE). Isso fecha a maioria dos uninitialized-stack info leaks e torna a exploração mais difícil ao converter segredos em valores conhecidos.
  • Linux kernel hardening: Kernels compilados com CONFIG_INIT_STACK_ALL ou o mais novo CONFIG_INIT_STACK_ALL_PATTERN inicializam com zero/padrão cada slot da stack na entrada da função, apagando canaries/pointers que, de outra forma, leak. Procure por distros que entreguem kernels compilados com Clang com essas opções habilitadas (comum em configs de hardening 6.8+).
  • Opt-out attributes: Clang agora permite __attribute__((uninitialized)) em locais/structs específicos para manter áreas críticas para performance não inicializadas mesmo quando o auto-init global está ativado. Revise essas anotações cuidadosamente — elas frequentemente marcam superfície de ataque deliberada para side channels.

Do ponto de vista do atacante, saber se o binário foi construído com essas flags determina se stack-leak primitives são viáveis ou se você deve pivotar para divulgações do heap/seção-de-dados.

Finding uninitialized-stack bugs quickly

  • Compiler diagnostics: Compile com -Wall -Wextra -Wuninitialized (GCC/Clang). Para código C++, clang-tidy -checks=cppcoreguidelines-init-variables irá auto-fixar muitos casos para zero-init e é útil para localizar locais esquecidos durante a auditoria.
  • Dynamic tools: -fsanitize=memory (MSan) no Clang ou o Valgrind com --track-origins=yes sinalizam de forma confiável leituras de bytes de stack uninitialized durante fuzzing. Instrumente test harnesses com essas ferramentas para evidenciar padding leaks sutis.
  • Grepping patterns: Em revisões, procure por chamadas copy_to_user / write de structs inteiros, ou memcpy/send de dados da stack onde somente parte do struct é preenchida. Preste atenção especial a caminhos de erro onde a inicialização é pulada.

Exemplo ARM64

This doesn’t change at all in ARM64 as local variables are also managed in the stack, you can check this example were this is shown.

Referências

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks