Programação defensiva e memória
Format strings, memória e ROP, canários, taint, cópia segura, TOCTOU e mitigações.
Uma função que aceita nomes precisa de um contrato. Chama-se contrato às regras sobre que tipo de dados recebe, que tamanho admite e que faz quando a entrada é inválida. Uma entrada vinda da rede não passa a ser confiável só porque o formulário a validou no navegador, porque o atacante pode enviar o pedido diretamente.
Validar e interpretar
Valida tipo, tamanho, intervalo e significado no servidor. Uma lista de permissão define o conjunto aceite; uma lista de negação tenta excluir padrões perigosos. Uma lista de negação pode complementar um controlo, mas tende a falhar quando existem muitas representações equivalentes.
A validação não substitui a separação entre dados e instruções. Um apelido legítimo pode conter uma aspa. Rejeitar aspas não é a solução geral para SQL: os parâmetros separam os valores da estrutura da consulta. Para HTML, a codificação depende do contexto de saída.
Ao validar caminhos, normalizar e verificar uma pasta permitida pode detetar ../. Porém, verificar o caminho e abri-lo depois ainda pode permitir uma troca de ligações simbólicas. As operações de abertura e verificação têm de preservar a identidade do objeto, conforme as regras do sistema operativo.
Format strings: o formato é uma instrução
Em printf(formato, ...), o primeiro argumento descreve como interpretar os seguintes. Por isso, printf(entrada) deixa a entrada escolher essa interpretação. Mesmo sem overflow, uma entrada com especificadores pode pedir leituras ou escritas para argumentos que o programa não forneceu.
| Especificador | Argumento esperado | Efeito |
|---|---|---|
%d | int | Imprime um inteiro decimal |
%s | Ponteiro para uma string acessível terminada em nulo | Lê e imprime os caracteres |
%n | int *, sem modificador de comprimento | Guarda nesse endereço a contagem de caracteres já impressos |
%% | Nenhum | Imprime o caráter % |
%n não imprime o inteiro apontado: é uma escrita. Argumentos ausentes, tipos incompatíveis ou ponteiros inválidos produzem comportamento indefinido; não permitem prever uma saída portátil. Por exemplo, não executes %s com um ponteiro nulo para tentar obter um resultado garantido.
Uma entrada com formato controlado também é uma primitiva de leitura potencial: pode levar a implementação a interpretar argumentos inexistentes e, nesta máquina de laboratório x86, a devolver valores lidos de posições da pilha, que podem incluir segredos ou o canário da secção seguinte. Em C, argumentos ausentes ou incompatíveis dão comportamento indefinido, sem saída portátil garantida, como exige printf(3). Por isso uma fuga de formato pode anular uma defesa que dependa de um valor imprevisível, sem prometer fuga automática em qualquer ABI.
No programa seguinte só há formatos literais e argumentos válidos: o % da entrada é dado, não um especificador, e no segundo printf o %n guarda 3 antes de serem impressos ! e a mudança de linha.
#include <stdio.h>
int main(void) {
const char *entrada = "100% concluido";
int escritos = -1;
printf("%s\n", entrada);
printf("abc%n!\n", &escritos);
printf("Contagem guardada: %d\n", escritos);
return 0;
}Executa o código para ver a saída e os gráficos.
Para mostrar texto externo, usa printf("%s", entrada) ou uma API de texto apropriada. Isto não valida tamanho, ponteiro ou contexto HTML: cada problema exige o seu contrato.
Chamada normal antes da corrupção
Numa chamada típica, o chamador coloca argumentos, executa call, que guarda o endereço de retorno, e o chamado cria o seu frame. Variáveis locais ficam nesse frame. Ao terminar, o chamado repõe o frame e executa ret, que retoma o endereço guardado. Uma corrupção só faz sentido contra este percurso: algum passo escreve fora do objeto e alcança dados de controlo como o endereço de retorno.
Buffer overflow
Um buffer de 8 bytes permite uma string de até 7 bytes mais o terminador nulo. strcpy não conhece a capacidade de destino. Copiar 12 letras e o terminador escreve 13 bytes, cinco para além do buffer.
void saudar(const char *nome) {
char buf[8];
strcpy(buf, nome); /* não verifica a capacidade de buf */
printf("%s\n", buf);
}
Isto é comportamento indefinido: pode corromper dados, falhar ou permitir desvio de execução, conforme a disposição da memória e as proteções, por isso não deduzas da ordem das declarações qual variável será alterada.
Seguir a pilha no modelo x86
Os exemplos das aulas usam um modelo Linux/x86. Num processo, código, dados estáticos, heap e pilha têm papéis diferentes. Globais e estáticos não passam a estar na pilha só por serem usados numa função. A heap guarda alocações dinâmicas; a pilha guarda contextos de chamadas segundo a convenção adotada.
Num exemplo convencional de x86 de 32 bits com frame pointer, a pilha cresce para endereços menores. ebp referencia o frame; variáveis locais podem estar em offsets negativos, e o endereço de retorno em ebp+4. Esta disposição não vale para todos os binários, otimizações ou arquiteturas.
Suponhamos que o enunciado fixa buf em ebp-16, com 12 bytes, e o endereço de retorno em ebp+4. A cópia da string escreve em endereços crescentes. A diferença entre os endereços é bytes. O byte no índice 20 da cópia atinge o primeiro byte do endereço de retorno; no mapa seguinte, buf ocupa de ebp-16 a ebp-5, os bytes 16 a 19 cobrem o valor guardado de ebp e o índice 20 alcança o retorno.
| Endereços | Índices da cópia | Objeto no modelo |
|---|---|---|
ebp+4 a ebp+7 | 20 a 23 | Endereço de retorno |
ebp a ebp+3 | 16 a 19 | ebp guardado |
ebp-4 a ebp-1 | 12 a 15 | Espaço intermédio |
ebp-16 a ebp-5 | 0 a 11 | Buffer de 12 bytes |
A cópia avança para endereços maiores: o índice 12 já sai do buffer e o índice 20 alcança o retorno. A pilha cresce para endereços menores ao criar frames, por isso não confundas os dois sentidos.
Com vinte caracteres de um byte, esse byte seria o terminador; não precisamos de copiar um endereço completo para ter uma escrita indevida.
A little-endian, o inteiro de 32 bits 0x11223344 ocupa, em endereços crescentes, os bytes 44 33 22 11. A ordem dos bytes é diferente da direção em que a pilha cresce, por isso distingue endianness, offsets e sentido da cópia antes de analisar um desenho.
Este exemplo analisa uma disposição dada, sem executar código injetado nem pressupor que o programa real terá esses offsets. Linguagens com verificação de memória reduzem estas classes de erro, mas bibliotecas nativas e componentes inferiores continuam a precisar de análise.
Corrigir a cópia
Se nome é uma string terminada em nulo válida, mede antes de copiar e rejeita o que não cabe, porque n >= sizeof buf inclui o caso em que falta espaço para o terminador.
#include <stdio.h>
#include <string.h>
static void saudar(const char *nome) {
char buf[8];
size_t n = strlen(nome);
if (n >= sizeof buf) {
puts("Nome demasiado comprido.");
return;
}
memcpy(buf, nome, n + 1);
printf("Olá, %s!\n", buf);
}
int main(void) {
saudar("Ana");
saudar("1234567");
saudar("12345678");
return 0;
}Executa o código para ver a saída e os gráficos.
Estamos a contar bytes, não letras Unicode: uma letra acentuada pode ocupar vários bytes em UTF-8. Para dados recebidos como bloco binário, não uses strlen sem garantir um terminador dentro de memória acessível. Nesse caso, recebe o comprimento e valida-o explicitamente.
strncpy pode não escrever um terminador quando a origem é longa. Também pode truncar silenciosamente um identificador e fazê-lo colidir com outro. Escolhe entre rejeitar e truncar de acordo com o contrato, em vez de assumir que qualquer função com “n” no nome resolve o problema.
Outros erros de memória
Um overflow na heap ultrapassa uma alocação dinâmica. Duas chamadas consecutivas a malloc não garantem que os objetos sejam adjacentes, nem permitem deduzir qual objeto será corrompido. Os slides mostram alocadores com metadados e ponteiros de controlo, mas a disposição e as verificações dependem do alocador. NX não impede alterar dados ou um ponteiro de função.
Um use-after-free usa uma referência depois de libertar o objeto. Segue o estado, sem executar o acesso inválido:
| Passo | Estado de p e da memória |
|---|---|
p = malloc(8) com sucesso | p dá acesso ao objeto de 8 bytes. |
free(p) | O objeto deixou de estar disponível através de p; o valor do ponteiro pode continuar guardado. |
| Outra alocação | O alocador pode reutilizar a região, mas não tem de o fazer. |
Usar p para ler ou escrever | O acesso é inválido, mesmo que os bytes pareçam intactos. |
Um double free tenta libertar o mesmo objeto duas vezes. Anular um ponteiro depois de libertar pode evitar o seu uso, mas não anula outras referências copiadas antes.
Um integer overflow pode transformar um tamanho grande num tamanho pequeno antes da alocação. Por exemplo, com size_t de 32 bits e itens de 8 bytes, este cálculo reserva pouco e escreve muito quando quantidade é grande:
typedef struct { unsigned char b[8]; } item;
size_t total = quantidade * sizeof(item);
item *p = malloc(total); /* se a multiplicação excede SIZE_MAX, total envolve */
for (size_t i = 0; i < quantidade; i++)
p[i] = itens[i];
quantidade conta itens e total conta bytes; o ciclo copia item a item. Verifica a multiplicação antes de alocar, por exemplo rejeitando quando quantidade > SIZE_MAX / sizeof(item).
Leituras fora dos limites
Uma leitura indevida pode divulgar dados sem alterar memória. Considera uma mensagem com quatro bytes de conteúdo que declara ter 32. Se o servidor confia no comprimento declarado e devolve 32 bytes a partir desse conteúdo, tenta ler 28 bytes que não pertencem à mensagem. Não podemos prever quais seriam esses bytes, mas a ausência de escrita não elimina o risco de confidencialidade.
Antes de usar o comprimento, compara-o com os bytes realmente disponíveis. Num vetor de comprimento len, um índice inteiro com sinal precisa de satisfazer 0 <= indice < len. Só testar indice < len deixa passar valores negativos; usar indice <= len deixa passar o primeiro índice para além do fim.
Conversões de tamanho
Um tamanho pode perder informação antes da verificação. Suponhamos um comprimento sem sinal de 65 540 que é guardado num campo sem sinal de 16 bits. O campo fica com . Verificar que o campo é menor que 100 não demonstra que o conteúdo original cabe num buffer de 100 bytes.
Conserva a representação do comprimento efetivamente usado e rejeita valores que não cabem no tipo de destino antes de converter. Em C, a aritmética sem sinal envolve segundo o módulo do tipo; overflow de inteiros com sinal tem regras diferentes e pode ser comportamento indefinido. As promoções de inteiros também contam: uma operação com operandos de tipo pequeno pode ser calculada em int. Declara os tipos antes de prever o resultado.
Injeção e reutilização de código
Heap spraying coloca muitas cópias de dados numa região para aumentar a hipótese de um desvio a atingir. JIT spraying procura influenciar bytes de código gerados pelo compilador em execução. São estratégias diferentes: a primeira, por si, não transforma páginas não executáveis em código executável. Em ambos os casos ainda é necessário um caminho que viole a proteção ou desvie o fluxo. Não são efeitos garantidos de qualquer overflow.
As proteções têm objetivos distintos. Um canário pode detetar algumas alterações na pilha antes do retorno. NX impede executar certas páginas de dados. ASLR dificulta prever endereços. Nenhuma destas proteções corrige o acesso fora dos limites, porque ataques que reutilizam código existente podem evitar a necessidade de executar bytes na pilha.
Reutilização de código e ROP
NX impede executar páginas marcadas como não executáveis. Não impede executar instruções que já pertencem a páginas de código. Return-oriented programming, ou ROP, encadeia pequenos trechos existentes, chamados gadgets, usando desvios como ret. Reutilizar funções completas é um caso mais simples para seguir a ideia de controlo por endereços de retorno.
Neste modelo didático (x86 de 32 bits, argumentos na pilha, funções que não retiram os argumentos ao retornar, sem canário nem rearranjo da pilha), queremos visitar etapaA() e depois etapaB(7); os nomes são símbolos, não endereços de um programa explorável.
Segue a sequência de retornos no modelo didático:
- O primeiro
retconsomeetapaAe deixa o topo emetapaB. - A entrada de
etapaAvêetapaBcomo endereço de retorno. - O retorno de
etapaAconsome esse endereço.etapaBvê agoracontinuaçãocomo retorno e 7 como argumento. - Quando
etapaBretorna, a execução segue paracontinuação; o valor 7 ainda exige tratamento conforme a convenção.
Não podemos acrescentar livremente argumentos às etapas anteriores: sem ajuste, esses valores ficariam no caminho dos retornos. Cadeias gerais usam instruções que ajustam a pilha e, noutras arquiteturas, preparam registos. Não extrapoles esta disposição para x86-64 ou para qualquer convenção de chamada.
ASLR dificulta localizar código; uma fuga de endereços pode reduzir essa proteção. Verificação de integridade do fluxo de controlo e mecanismos de retorno protegido procuram restringir os destinos. A correção continua a ser remover a corrupção de memória que permite controlar o fluxo.
Control-Flow Integrity, ou CFI, verifica em execução se certos desvios seguem destinos admitidos pelo modelo de fluxo de controlo. Não é uma verificação de todos os índices ou comprimentos. Se um acesso fora dos limites corrompe apenas dados, sem violar um desvio verificado, pode continuar a escapar a CFI. O conjunto de destinos, os desvios cobertos e a precisão da política determinam o limite da proteção. A distinção face a SFI e interposição de chamadas fica para sistemas seguros.
Canários: o que está a ser verificado?
Um canário fica numa posição que certas escritas entre um buffer e o retorno têm de atravessar. Antes de retornar, o programa verifica se o valor mudou. Uma alteração provoca a interrupção, em vez de continuar com o retorno corrompido. Não protege todos os objetos nem escritas que saltem diretamente para outra posição.
Um canário terminador, em construções históricas, contém bytes que terminam operações de texto, como nulo e mudança de linha. Uma cópia que para no nulo não consegue transportar esse byte e continuar com o resto da entrada. A justificação depende da função: uma cópia binária com comprimento explícito pode transportar bytes nulos.
Um canário aleatório depende de o valor ser imprevisível e não ser exposto. Uma fuga que o revele pode permitir reproduzi-lo numa escrita. Uma leitura com formato controlado sobre a pilha é uma forma concreta de obter essa fuga nesta máquina de laboratório, por isso as duas secções analisam-se em conjunto. Construções históricas XOR também ligam o valor protegido ao endereço de retorno. Nenhum tipo é sempre superior: especifica a primitiva de cópia, a informação disponível e a disposição da pilha. Implementações atuais podem combinar aleatoriedade com bytes terminadores.
Análise estática, dinâmica e taint
Taint analysis acompanha dados de origens não confiáveis (sources) até operações sensíveis (sinks). Ler uma linha da rede é uma origem; usá-la como formato de printf é um sink. Concatenação e atribuição podem propagar a marca. Uma transformação só é uma sanitização se estabelecer a condição exigida por aquele sink.
| Análise | Como trabalha | Limite a justificar |
|---|---|---|
| Estática | Analisa código e caminhos possíveis sem executar cada caso | Aproximações de ponteiros e caminhos podem gerar falsos positivos ou omitir fluxos |
| Dinâmica | Instrumenta uma execução e acompanha os valores que realmente passam | Um caminho não executado pelos dados de teste não foi observado |
Suponhamos linha = ler_rede(); copia = linha; printf(copia);. A marca chega ao formato através da cópia, mesmo que a variável tenha outro nome. Mudar para printf("%s", copia) remove o controlo do formato. Nesse caso, copia continua externa mas passa como dado. Isto não prova que é uma string válida ou que outro uso será seguro.
Um teste dinâmico com apenas Ana pode não revelar um problema que depende de %. A análise estática pode identificar o fluxo potencial, mas precisa de modelar ler_rede e a chamada de impressão. Usa as duas técnicas como evidências complementares, não como provas automáticas de ausência de falhas.
TOCTOU
Uma race condition depende da ordem de operações concorrentes. TOCTOU ocorre quando uma condição muda entre a verificação e o uso. Verificar um nome e abrir esse nome depois resolve a referência duas vezes: o mesmo texto pode designar objetos diferentes. Uma segunda verificação pelo nome apenas cria outro intervalo se o nome continuar mutável.
Assume que outro processo pode alterar a entrada de diretório relatorio. A e B são ficheiros diferentes, e só A foi autorizado:
| Momento | Programa | Outro processo | Objeto referido |
|---|---|---|---|
| 1 | Verifica relatorio e autoriza A | Não altera o nome | A |
| 2 | Ainda não abriu o ficheiro | Troca o nome por uma ligação para B | B |
| 3 | Abre relatorio e usa B | Não precisa de outra alteração | B |
O texto do nome ficou igual. A identidade do objeto mudou entre a verificação e a abertura.
Quando o sistema permite, abre com as restrições adequadas, verifica o objeto pelo descritor e usa esse mesmo descritor. Para uma atualização de saldo, verificar e alterar deve ser uma operação atómica ou uma transação com isolamento adequado. Um mutex numa única aplicação não coordena automaticamente outros processos ou servidores.
Atualizações e resposta
Uma atualização pode corrigir uma vulnerabilidade conhecida. Antes de a aplicar, verifica dependências e compatibilidade; depois confirma a versão e o comportamento. Fixar uma versão permite reproduzir o sistema, mas exige um processo de atualização. Um componente sem manutenção continua a precisar de isolamento, substituição ou uma decisão sobre o risco residual.
Exercícios
O byte que falta
Um buffer tem 16 bytes. Copiamos uma string ASCII com 20 caracteres usando strcpy, incluindo o terminador final. Quantos bytes são escritos para além da capacidade?
Pista
Conta também o terminador nulo.
Solução
bytes excedem a capacidade: a cópia escreve 21 bytes num objeto de 16. A escrita é comportamento indefinido, não uma alteração garantida de uma variável específica. Não deduzas a vítima pela ordem das declarações.
Erros frequentes
Não confundir comprimento da string com bytes copiados incluindo terminador.
O caminho mudou
Um programa verifica que um caminho aponta para um ficheiro autorizado e abre esse caminho mais tarde. Outro processo pode trocar o nome por uma ligação simbólica. Explica o intervalo vulnerável e uma direção de correção.
Pista
Segue a identidade do objeto, não só o texto do caminho.
Mais uma pista
Resolver o mesmo nome duas vezes pode dar objetos diferentes.
Solução
A verificação não está ligada ao objeto efetivamente usado. O atacante altera a referência no intervalo entre a verificação e a abertura. A solução deve preservar essa identidade e aplicar restrições adequadas à abertura, depois verificar e operar pelo descritor.
Confere a tua resposta:
- Identifica a troca entre verificação e abertura.
- Explica que o objeto usado pode ser diferente do verificado.
- Propõe abertura com restrições e verificação/uso do mesmo descritor, segundo a API adequada.
Erros frequentes
Canonicalizar uma vez e abrir pelo nome depois não elimina a corrida.
O valor escrito por %n
Assume argumentos válidos. Em printf("ola: %nfeito\n", &n), que valor é guardado em n? O espaço depois dos dois pontos pertence ao texto.
Pista
Conta só os caracteres impressos antes de %n.
Solução
Fica 5. %n não acrescenta caracteres à saída; guarda a contagem nesse momento. O texto posterior e a mudança de linha não alteram o valor já escrito.
Erros frequentes
Não contar o texto depois de %n nem tratar n como um inteiro a imprimir.
Seguir dois retornos
No modelo x86 de 32 bits da explicação, o topo antes do primeiro ret contém, em endereços crescentes, alfa, beta, terminar, 12. alfa não recebe argumentos e retorna sem ajustar a pilha. Indica o retorno visto por alfa e o retorno e primeiro argumento vistos por beta.
Pista
Cada ret consome uma posição de quatro bytes.
Mais uma pista
Depois de consumir alfa, o topo é beta. Depois de consumir beta, é terminar.
Solução
À entrada de alfa, beta é o endereço de retorno. À entrada de beta, terminar é o retorno e a posição seguinte, 12, é o primeiro argumento. A conta não serve para arquiteturas com argumentos em registos ou funções que ajustem a pilha de outra forma. Inserir valores de argumentos entre alfa e beta quebraria este percurso sem ajuste adicional.
Confere a tua resposta:
- Identifica beta como retorno visto por alfa.
- Identifica terminar como retorno visto por beta e 12 como o argumento.
- Justifica que a conclusão depende da convenção e do ajuste da pilha.
Erros frequentes
Não confundir o destino consumido pelo ret com o retorno que a função encontra à entrada.
Cópia binária, canário e NX
Um overflow resulta de uma cópia binária com comprimento explícito. Entre o buffer e o retorno há um canário com um byte nulo, verificado antes de retornar. A pilha não é executável (NX). A equipa afirma que o nulo impede a cópia e que NX corrige qualquer corrupção restante. Avalia ambas as afirmações. Distingue uma escrita que altera o canário de outra que corrompe dados sem o atravessar.
Pista
Analisa escrita, verificação e execução como operações distintas.
Solução
A cópia binária é controlada pelo comprimento e pode transportar nulos; o argumento de terminação de strings não se aplica. Se atravessa e altera o canário, a verificação pode interromper o programa antes do retorno. Uma escrita noutro objeto pode não o alterar e escapar a essa deteção. NX impede executar páginas marcadas como não executáveis, mas não impede a cópia, corrupção de dados ou toda reutilização de código existente. A correção é validar a capacidade e eliminar o acesso indevido.
Confere a tua resposta:
- Expliquei que a cópia pode transportar bytes nulos.
- Separei deteção por canário de permissão de execução NX.
- Não concluí segurança só porque um dos mecanismos está ativo.
Erros frequentes
Não afirmar que o canário é inútil por aceitar nulos, nem que NX elimina o overflow.
A execução não passou por lá
Um serviço lê nome da rede. Só no ramo modo_debug chama printf(nome). A análise dinâmica testou apenas o modo normal. Pode concluir que não existe fluxo externo até um formato? Explica como a análise estática pode ajudar.
Pista
Que ramo foi efetivamente observado?
Solução
Não. A análise dinâmica não observou o ramo debug. O fluxo potencial liga a leitura da rede ao primeiro argumento de printf. Uma análise estática que modele a origem, a propagação e o sink pode assinalá-lo sem executar esse ramo, mas aproximações e modelos incompletos continuam a limitar a conclusão.
Confere a tua resposta:
- Identifica entrada externa como source e formato como sink.
- Explica que dinâmica só observou os caminhos executados.
- Reconhece análise estática como evidência potencial com limites de modelação.
Erros frequentes
Não confundir ausência de um alerta no teste com prova de ausência de uma vulnerabilidade.
Multiplicar antes de alocar
Com size_t de 32 bits, quantidade = 600 000 000 e sizeof(item) = 8, o programa calcula total = quantidade * 8 e aloca total bytes. Quantos bytes reserva e porquê? Propõe a verificação.
Pista
Compara o produto com o máximo de 32 bits.
Mais uma pista
e .
Solução
O produto é bytes, mas em 32 bits envolve para bytes. O programa reserva cerca de 505 MB e o uso escreve 4,8 GB. Rejeita antes de multiplicar quando quantidade > SIZE_MAX / sizeof(item).
Confere a tua resposta:
- Calcula o produto e deteta o envolvimento em 32 bits.
- Explica que a alocação reserva menos do que o uso.
- Propõe verificação antes da multiplicação com SIZE_MAX.
Erros frequentes
Verificar o resultado depois de envolver não corrige a alocação curta.
Bytes intactos, referência inválida
Um programa guarda q = p, liberta o objeto de oito bytes apontado por p e faz outra alocação de oito bytes. Depois lê pelo ponteiro q e não observa um crash. A leitura é válida? Podemos afirmar que a nova alocação ocupa a região antiga? Noutro ponto, um vetor de cinco elementos usa apenas indice < 5 antes de ler. Identifica o caso inválido que passa essa verificação e a condição completa, assumindo índice inteiro com sinal.
Pista
Distingue o tempo de vida do objeto, a disposição do alocador e o intervalo de índices.
Solução
A leitura por q é use-after-free: copiar o ponteiro não prolonga a vida do objeto. Não haver crash não torna o acesso válido. O alocador pode reutilizar a região, mas o mesmo tamanho não garante essa escolha. No vetor, um índice negativo passa indice < 5; a condição completa é 0 <= indice && indice < 5. O índice 5 já está fora do vetor e deve ser negado.
Confere a tua resposta:
- Identifiquei use-after-free mesmo sem escrita ou crash.
- Não deduzi a reutilização obrigatória da região pela nova alocação.
- Justifiquei o limite inferior e superior do índice.
Ler a pilha antes de escrever
Um serviço imprime com printf(entrada) texto externo e protege a pilha com um canário aleatório. Um atacante envia uma entrada com especificadores %x e observa os valores impressos. Depois envia um overflow que inclui um desses valores na posição do canário. Explica por que a segunda etapa pode passar a verificação e que correção atua na causa.
Pista
O que é que os valores impressos revelam sobre a pilha?
Solução
A entrada controla o formato e pode levar a implementação a ler posições da pilha nesta máquina de laboratório, o que pode expor o próprio canário. Com o valor conhecido, o atacante escreve-o na posição esperada e a verificação antes do retorno não deteta a alteração. Em C, esse comportamento é indefinido e depende de ABI e ambiente, sem garantia portátil. A correção é tratar a entrada como dado, com formato literal, em vez de confiar apenas no canário: um valor imprevisível deixa de proteger quando existe uma primitiva de leitura.
Confere a tua resposta:
- Identifica formato controlado como leitura potencial de posições da pilha nesta máquina.
- Explica que o valor revelado pode ser reutilizado na escrita.
- Separa corrigir a fuga de formato de confiar apenas no canário.
Erros frequentes
Não afirmar que um canário impede a leitura da pilha por outra vulnerabilidade.