Fundamentos Hardware Segurança
Fundamentos

Binário & Assembly

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.

Bits, Bytes e Bases Numéricas

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

Representação de Inteiros em Memória

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)
Integer Overflow — CVE-2021-21985 e similares Integer overflow ocorre quando uma operação aritmética produz um resultado fora do intervalo representável pelo tipo. Em C, overflow de inteiros signed é undefined behaviour — o compilador pode optimizar assumindo que nunca ocorre, produzindo código com comportamento imprevisível. Em Java, overflow é silencioso — o valor simplesmente "volta ao início" sem excepção. Em ambos os casos, um atacante que controla os valores de entrada pode usar overflow para contornar verificações de segurança: if (size + offset < MAX) pode ser true mesmo com valores maliciosos se a soma sofrer overflow para um valor pequeno.

Memória: Stack vs Heap

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
}

Registos e Instruções Assembly

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

De Assembly a Linguagens de Alto Nível

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

Representação de Texto: ASCII, UTF-8 e Encoding

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

Endianness: Ordem dos Bytes

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]

A Ligação à Segurança Web

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)

Checklist Binário & Assembly