Toda a programação — independentemente da linguagem, framework, ou abstracção — executa no mesmo substrato: transístores que representam 0 e 1, e instruções que manipulam esses valores em registos de 64 bits. Perceber este substrato não é arqueologia — é o que te permite compreender integer overflow, buffer overflow, representação de dados em memória, e porque certas vulnerabilidades existem independentemente da linguagem que usas por cima.
Um bit é a unidade mínima de informação — pode representar dois estados: 0 ou 1. Oito bits formam um byte. Um byte pode representar 256 valores distintos (2⁸ = 256). Toda a informação num computador — texto, imagens, código, chaves criptográficas — é, no final, uma sequência de bytes.
// As três bases numéricas relevantes em programação:
// Base 10 (decimal) — o que usamos no dia-a-dia
// Dígitos: 0-9
255
// Base 2 (binário) — o que o hardware usa
// Dígitos: 0-1
// Prefixo em código: 0b
0b11111111 // = 255 em decimal
// Base 16 (hexadecimal) — compacto para representar bytes
// Dígitos: 0-9, A-F (A=10, B=11, C=12, D=13, E=14, F=15)
// Prefixo em código: 0x
// 1 byte = 2 dígitos hex (cada dígito hex representa 4 bits)
0xFF // = 255 em decimal = 11111111 em binário
// Conversão:
// 0xFF = 15×16 + 15×1 = 240 + 15 = 255
// 0x1A = 1×16 + 10×1 = 16 + 10 = 26
// 0x7F = 7×16 + 15×1 = 112 + 15 = 127
// Porquê hexadecimal é ubíquo em programação de sistemas:
// 1 byte = 8 bits = 2 dígitos hex
// 4 bytes (int 32-bit) = 8 dígitos hex
// 8 bytes (long 64-bit) = 16 dígitos hex
// Endereço de memória típico (64-bit):
0x00007FFE_A3B2C1D0
// muito mais legível que 140731569660368 em decimal
// ou 0000000000000000011111111111111010100011101100101100000111010000 em binário
Um inteiro de 8 bits sem sinal (unsigned) pode representar valores de 0 a 255. Um inteiro de 8 bits com sinal (signed) usa o bit mais significativo para o sinal e pode representar -128 a 127. A forma como os valores negativos são codificados chama-se two's complement — e é a fonte de uma classe inteira de vulnerabilidades de segurança.
// Inteiros de 8 bits:
// Unsigned (sem sinal) — 0 a 255
uint8_t u = 255; // 0b11111111 = 0xFF
u + 1; // → 0 (overflow silencioso — volta ao início)
// 0xFF + 1 = 0x100, mas só temos 8 bits → 0x00
// Signed (com sinal, two's complement) — -128 a 127
int8_t s = 127; // 0b01111111 = 0x7F (bit mais significativo = 0 → positivo)
s + 1; // → -128 (overflow — 0x7F + 1 = 0x80 = -128 em signed)
// Two's complement — como representar negativos:
// Para obter -N: inverter todos os bits de N e somar 1
//
// 1 = 00000001
// -1 = 11111110 + 1 = 11111111 = 0xFF
// -2 = 11111101 + 1 = 11111110 = 0xFE
// -128 = 10000000 = 0x80
// A elegância do two's complement:
// adição funciona da mesma forma para signed e unsigned
// 127 + 1 = 128 em unsigned (correcto)
// 127 + 1 = -128 em signed (overflow — visualmente inesperado)
// Tamanhos típicos em C e Java:
//
// C (tamanhos mínimos — podem variar por plataforma):
// char — 8 bits — -128 a 127 (signed) ou 0 a 255 (unsigned)
// short — 16 bits — -32768 a 32767
// int — 32 bits — -2147483648 a 2147483647
// long — 32 ou 64 bits (depende da plataforma)
// long long— 64 bits
//
// Java (tamanhos fixos — independentes da plataforma):
// byte — 8 bits
// short — 16 bits
// int — 32 bits
// long — 64 bits
// (Java não tem tipos unsigned nativos)
if (size + offset < MAX) pode ser true
mesmo com valores maliciosos se a soma sofrer overflow para um valor pequeno.
Um programa em execução tem acesso a duas regiões de memória com características muito diferentes. Compreender a distinção é essencial para perceber buffer overflows, gestão de memória em C, e porque a JVM existe como abstracção sobre este modelo.
// Stack — memória automática, gerida pelo compilador
// - Alocação e libertação automáticas (entra na função → aloca, sai → liberta)
// - Tamanho fixo por frame (determinado em compile-time)
// - LIFO — Last In, First Out
// - Muito rápida — apenas move um ponteiro (stack pointer)
// - Típicamente 1-8 MB por thread
void exemplo() {
int x = 42; // alocado na stack — libertado quando a função retorna
char buf[256]; // 256 bytes na stack
} // x e buf são libertados automaticamente aqui
// Heap — memória dinâmica, gerida pelo programador (em C)
// - Alocação e libertação explícitas (malloc/free em C, new/delete em C++)
// - Tamanho determinado em runtime — pode crescer
// - Fragmentação possível
// - Mais lenta que stack — o allocator precisa de encontrar espaço livre
// - Gerida automaticamente pela JVM em Java (Garbage Collector)
void exemplo_heap() {
int* arr = malloc(256 * sizeof(int)); // aloca 1024 bytes no heap
// usar arr...
free(arr); // libertação manual obrigatória — esquecê-la = memory leak
// em Java: o GC trata disto automaticamente
}
// O Buffer Overflow clássico — stack smashing:
void vulnerable(char* input) {
char buffer[64]; // 64 bytes na stack
strcpy(buffer, input); // copia input para buffer SEM verificar tamanho
// se input tiver 100 bytes, os 36 bytes extra escrevem por cima
// do endereço de retorno da função na stack
// um atacante controla para onde a função "retorna" — execução arbitrária
}
O processador não opera directamente sobre memória RAM — opera sobre registos, um conjunto pequeno de localizações de armazenamento dentro do próprio CPU, extremamente rápidas. As instruções Assembly movem dados entre memória e registos, e executam operações aritméticas e lógicas sobre os registos.
// Registos principais em x86-64 (arquitectura dos processadores modernos):
//
// Registos de uso geral (64-bit):
// RAX, RBX, RCX, RDX — dados gerais
// RSI, RDI — source index, destination index (operações de memória)
// RSP — stack pointer (aponta para o topo da stack)
// RBP — base pointer (base do frame actual da stack)
// R8-R15 — registos adicionais (x86-64)
//
// Registos especiais:
// RIP — instruction pointer (endereço da próxima instrução a executar)
// FLAGS — resultado de comparações (zero, carry, overflow, sign...)
// Instruções Assembly x86-64 (sintaxe AT&T):
// mov src, dst — copiar valor
// add src, dst — dst = dst + src
// sub src, dst — dst = dst - src
// cmp a, b — comparar a com b (actualiza FLAGS)
// jmp label — saltar para label
// je label — saltar se igual (jump if equal)
// call label — chamar função (push RIP na stack, saltar)
// ret — retornar de função (pop RIP da stack)
// Exemplo: função simples em C e o Assembly resultante
// C:
int soma(int a, int b) {
return a + b;
}
// Assembly x86-64 gerado (simplificado):
soma:
mov %edi, %eax // mover argumento a (em EDI) para EAX
add %esi, %eax // EAX = EAX + argumento b (em ESI)
ret // retornar — valor de retorno está em EAX por convenção
// A convenção de chamada x86-64 (System V ABI — Linux/macOS):
// Argumentos inteiros: RDI, RSI, RDX, RCX, R8, R9 (depois: stack)
// Valor de retorno: RAX
// O compilador C e a JVM (via JIT) seguem esta convenção automaticamente
// Como uma chamada de função funciona ao nível da stack:
// Estado da stack durante a execução de main() → soma() → retorno
// main() em execução:
// [stack frame de main]
// variáveis locais de main
// endereço de retorno de main ← RSP aponta aqui
//
// main chama soma(3, 5):
// argumentos em RDI=3, RSI=5 (por convenção x86-64)
// CALL soma → push RIP+1 na stack (endereço da instrução após CALL)
//
// stack durante soma():
// [stack frame de soma]
// RBP anterior (salvo)
// variáveis locais de soma ← RSP aponta aqui
// [stack frame de main]
// ...
// endereço de retorno para main
//
// soma executa: EAX = 3 + 5 = 8
// RET → pop endereço de retorno da stack → RIP = endereço após CALL em main
// controlo regressa a main, RAX=8 contém o resultado
// O buffer overflow explora exactamente esta estrutura:
// se escreveres além do buffer local, sobrescreves o endereço de retorno
// quando RET executa, o CPU salta para o endereço que o atacante escreveu
Nenhum programador escreve Assembly para aplicações modernas — os compiladores fazem essa tradução. Mas o Assembly que o compilador gera é o que realmente executa, e as suas características determinam o comportamento do programa em casos extremos.
// O que o compilador faz com o teu código C:
// Código C — alto nível, legível
int verificar_acesso(int user_id, int resource_id) {
if (user_id < 0 || user_id > MAX_USERS) return 0;
if (resource_id < 0) return 0;
return db_check(user_id, resource_id);
}
// 1. Preprocessamento — expandir macros, includes
// 2. Compilação — C → Assembly (gcc -S)
// 3. Assembling — Assembly → código máquina binário (objectos .o)
// 4. Linking — combinar objectos + bibliotecas → executável
// O compilador pode optimizar eliminando verificações que considera "impossíveis":
// Se int é signed e o compilador assume que overflow nunca ocorre,
// pode eliminar verificações de overflow como "always true" — undefined behaviour.
// Java segue um percurso diferente:
// Código Java → bytecode JVM (.class) → JIT compiler → código máquina nativo
// A JVM verifica limites de arrays, não tem ponteiros expostos, gere memória automaticamente
// Troca: menos vulnerabilidades de memória, mais overhead de runtime
Texto é bytes — mas a mesma sequência de bytes pode representar texto diferente dependendo do encoding. Confusões de encoding são fonte de bugs subtis e, em contextos de segurança, de vulnerabilidades de bypass de validação.
// ASCII — 7 bits, 128 caracteres (inglês básico)
// 'A' = 65 = 0x41
// 'a' = 97 = 0x61
// '0' = 48 = 0x30
// ' ' = 32 = 0x20
// Latin-1 (ISO-8859-1) — 8 bits, 256 caracteres (estende ASCII com acentos europeus)
// 'á' = 225 = 0xE1
// UTF-8 — encoding variável, compatível com ASCII, suporta todos os caracteres Unicode
// Caracteres ASCII (0-127): 1 byte — idêntico a ASCII
// Caracteres latinos acentuados: 2 bytes
// Caracteres asiáticos: 3 bytes
// Emojis e caracteres raros: 4 bytes
// 'A' = 0x41 (1 byte — ASCII)
// 'á' = 0xC3 0xA1 (2 bytes — UTF-8)
// '€' = 0xE2 0x82 0xAC (3 bytes — UTF-8)
// '😀' = 0xF0 0x9F 0x98 0x80 (4 bytes — UTF-8)
// Implicação para programação:
// String.length() em Java retorna número de chars (code units UTF-16), não bytes
// "á".length() = 1 mas "á".getBytes(UTF_8).length = 2
// Validar tamanho máximo de input: usar bytes, não chars
// Implicação de segurança — bypass de filtros:
// Um filtro que bloqueia "/" pode ser contornado com a representação UTF-8:
// "/" = 0x2F (bloqueado)
// "%2F" (URL encoding de /) — pode passar se o filtro não normalizar antes de validar
// A normalização deve acontecer ANTES da validação, nunca depois
Quando um inteiro de 32 bits é guardado em memória, a ordem dos seus bytes pode variar por arquitectura. Isto é raramente relevante em desenvolvimento de aplicações — mas torna-se crítico em protocolos de rede, ficheiros binários, e criptografia.
// O inteiro 0x01020304 em memória:
// Big-endian (byte mais significativo primeiro — "natural")
// Endereço: 0x00 0x01 0x02 0x03
// Valor: 0x01 0x02 0x03 0x04
// Little-endian (byte menos significativo primeiro — x86, ARM moderno)
// Endereço: 0x00 0x01 0x02 0x03
// Valor: 0x04 0x03 0x02 0x01
// x86/x86-64 (Intel/AMD) — little-endian
// ARM — configurável, tipicamente little-endian em modo moderno
// Protocolos de rede (IP, TCP, HTTP) — big-endian ("network byte order")
// Em C, converter entre ordens:
#include <arpa/inet.h>
uint32_t host_value = 0x01020304;
uint32_t network_value = htonl(host_value); // host to network long
uint32_t back = ntohl(network_value); // network to host long
// Em Java: java.nio.ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(4);
buf.order(ByteOrder.BIG_ENDIAN); // network byte order
buf.putInt(0x01020304);
byte[] bytes = buf.array(); // [0x01, 0x02, 0x03, 0x04]
Os conceitos deste artigo não são abstractos — materializam-se directamente nas vulnerabilidades mais comuns em aplicações modernas.
// Integer overflow em verificação de tamanho (C)
// CVE pattern — alocação insuficiente por overflow na multiplicação
size_t count = attacker_controlled_value; // ex: 0x40000001
size_t size = count * 4; // overflow: 0x100000004 → 0x04 (apenas 4 bytes)
char* buf = malloc(size); // aloca 4 bytes
memcpy(buf, data, count * 4); // copia 0x100000004 bytes → heap overflow
// Buffer overflow em leitura de input (C)
// A causa raiz de décadas de CVEs: gets(), strcpy(), sprintf() sem limite de tamanho
char buffer[64];
gets(buffer); // lê até newline SEM limite — clássico buffer overflow
// strncpy(buffer, input, sizeof(buffer) - 1) é a versão segura
// Em Java — a JVM elimina estas classes de vulnerabilidades:
// ✅ Arrays têm tamanho verificado — ArrayIndexOutOfBoundsException em vez de overflow
// ✅ Sem aritmética de ponteiros — não existe acesso directo a endereços de memória
// ✅ Strings são imutáveis e geridas pelo GC — sem dangling pointers
// ❌ Integer overflow ainda existe em Java — silencioso, sem excepção
// int maxValue = Integer.MAX_VALUE; // 2147483647
// maxValue + 1; // → -2147483648 (overflow silencioso)
String.length() conta code units, não bytes. Validar tamanho de input em bytes.