Ingegneria civile e strutturale
Come Scrivere il Codice C sicuro per prevenire i buffer overflows
Table of Contents
Nonostante siano ben documentati per decenni, continuano a causare gravi problemi come la corruzione dei dati, crash di sistema e l'esecuzione di codice remoto. La scrittura di codice sicuro richiede una profonda comprensione di come i sovraflussi del buffer avvengono e un approccio disciplinato per prevenirli. Questo articolo fornisce una guida completa per la scrittura di robusti, sovraccarichi C codice, coprendo concetti fondamentali, elaborazioni moderne.
Comprendere i buffer overflows
Un buffer overflow avviene quando un programma scrive più dati a un blocco contiguo di memoria (un buffer) di quanto il buffer è stato assegnato per tenere. Poiché i buffer risiedono in stack o in una memoria di mucchio, superando i loro confini sovrascrive le posizioni di memoria adiacenti. Questa corruzione può alterare lo stato del programma, introdurre comportamenti imprevedibili, o essere sfruttati da un aggressore per iniettare ed eseguire codice arbitrario.
L'overwriting di un indirizzo di ritorno sullo stack può reindirizzare l'esecuzione al codice controllato dagli aggressori. I puntatori di sovrascrittura possono portare a arbitrari scrive memoria. Anche i semplici crash possono essere sfruttati per attacchi disinvolti. Capire la meccanica è il primo passo per la prevenzione.
Overflows basati su pinze
Le variabili locali, inclusi i buffer dichiarati all'interno delle funzioni, vengono memorizzate sullo stack. Lo stack contiene anche l'indirizzo di ritorno, i puntatori di frame salvati e altri dati di controllo. Quando un buffer lineare come []] è invaso, i dati si riversano nell'indirizzo di ritorno e oltre.
Overflows basati su Heap
I buffer dinamici (tramite ], [, ecc.) risiedono sul mucchio. I flussi di sovrappeso qui possono corrompere i metadati utilizzati dall'allocatore, portando a crash o lo sfruttamento tramite attacchi di spruzzo o di uso-dopo-free.
Funzioni vulnerabili comuni e le loro alternative sicure
La libreria standard C fornisce diverse funzioni che non eseguono il controllo dei limiti. L'utilizzo di essi è la causa più comune dei flussi di buffer.
Copia e concatenazione di stringa
- ][] — Non sicuro: copie fino a un terminatore nullo, nessun limite di lunghezza. [
[]]]Alternativa sicura:[] []]]]] — copie alla maggior parte dei caratteri; si noti che non annulla-terminate se la sorgente è più lunga, quindi null- - Meglio ancora: — disponibile su BSD e molti sistemi Linux; sempre null-terminates e restituisce la lunghezza della stringa di origine per il rilevamento della tronca.
- ][] — Non sicuro: concatena senza limiti. [
]]Alternativa sicura:] []] — aggiunge alla maggior parte dei n caratteri e sempre null-termina.
Uscita formattata e ingresso
- ][ — Non sicuro: scrive l'output formattato a un buffer senza controllo di dimensioni. [
[]]]Alternativa sicura:[[]]] — limiti di uscita a caratteri di dimensione-1 più terminatore nullo. - [][]] — Rischio simile; usare ] invece.
- ][] — Estremamente pericoloso; rimosso dallo standard C11.
- [][] — Nessun controllo dei limiti. Usa [] o ] con specifier di larghezza del campo.
Copia e Spostamento della memoria
- [][] — Sicuro solo se n è verificato di non superare la dimensione del buffer di destinazione. [
]]] []]] [[[[
]]]]]]]]]]] [[[[[[[[[[[[[[FLT]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[FLT]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] - Alcune piattaforme forniscono dall'allegato K (opzionale in C11), ma l'adozione è limitata.
Gestione delle dimensioni e della convalida
Anche con funzioni sicure, è necessario convalidare le lunghezze di input, garantire dimensioni tampone adeguate e gestire la potenziale tronca con grazia.
Controllare le lunghezze di ingresso
Prima di copiare o elaborare input esterni (ingresso utente, dati di rete, contenuti di file), determinare la sua lunghezza massima accettabile e rifiutare o troncare i dati che lo supera.
#define MAX_INPUT 255
char buffer[MAX_INPUT + 1]; // +1 for null
if (strlen(user_input) > MAX_INPUT) {
// Handle error: reject or truncate
fputs("Input too long", stderr);
return -1;
}
strncpy(buffer, user_input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
Utilizzare Buffer di dimensione fissa con limiti noti
Evita i array di lunghezza variabile (VLA) che possono causare sovrappeso di stack se vengono fornite grandi dimensioni. Invece, assegnare dinamicamente con controlli di dimensioni esplicite.
Trucco maniglia Esplicitamente
Funzioni come e [] possono troncare i dati. Sii consapevole del valore di ritorno per rilevare la tronca e decidere se i dati troncati sono accettabili o se si dovrebbe sollevare un errore.
Bandiere di sicurezza e protezioni runtime
I compilatori moderni offrono bandiere che aggiungono il rilevamento e la mitigazione del overflow del buffer senza modifiche di codice.
- []][[] — Inserire i canari di stack (valori casuali) prima degli indirizzi di ritorno. Se un buffer overflow sovrascrive il canarino prima di modificare l'indirizzo di ritorno, il programma si interrompe prima che l'exploit completi.
- [][]] — Sostituisce chiamate a funzioni non sicure come [ e ]] con versioni controllate che si interrompeno se il buffer di destinazione è troppo piccolo.
- ][] — Avverte sulle vulnerabilità della stringa di formato che possono portare a overflow buffer o perdite di informazioni.
- ][[] — codice degli strumenti AddressSanitizer (ASan) per rilevare i flussi di buffer, l'uso-after-free e altri errori di memoria a tempo di esecuzione.
- [][]] – Evitare di ottimizzare i controlli di overflow (utilizzare con cautela).
Protezione del sistema operativo
I canari di stack sono solo uno strato. Le tecnologie di mitigazione di Exploit nei sistemi operativi moderni includono:
- Prevenzione di esecuzione dei dati (DEP) / NX bit[ — Marchi stack e heap come non eseguibile, impedendo l'esecuzione dei codici di shell.
- Address Space Layout Randomization (ASLR) – Randomizza gli indirizzi di memoria (stack, heap, librerie condivise) per rendere più difficile prevedere gli obiettivi.
- Rilocalizzazione Read-Only (RELRO)[ — Protegge il GOT (Tabella di Offset Globale) dalla sovrascrittura.
Abilitando queste protezioni (solitamente predefinite) solleva la barra per lo sfruttamento, ma non sostituisce la codifica sicura.
Audit del codice e analisi statica
La revisione umana combinata con l'analisi statica automatizzata può catturare i problemi di overflow del buffer presto.
- Rivista del codice manuale[[] — Cercare usi di funzioni non sicure, mancanti controlli di dimensione e loop che scrivono oltre i confini del buffer.
- Strumenti di analisi statici[[] — Strumenti come , [], [[], e rilevano potenziali overflow, uso di funzioni pericolose e errori off-by-one.
- Fuzzing[] — Utilizza libFuzzer, AFL o altri fuzzeri per testare automaticamente la gestione degli input con dati inaspettati che possono innescare overflow.
Esempi pratici di codice sicuro
Copia di stringa sicura con i bounds
#include <stdio.h>
#include <string.h>
int safe_string_copy(char *dest, size_t dest_size, const char *src) {
if (!dest || !src || dest_size == 0) {
return -1; // Invalid parameters
}
size_t src_len = strlen(src);
if (src_len >= dest_size) {
// Source too large; truncation or error
// Option: copy what fits and null-terminate
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
return 1; // Truncation occurred
}
strncpy(dest, src, dest_size);
// strncpy fills remaining with null, so dest_size fits; no need to null-terminate if src shorter
return 0; // Success, no truncation
}
Gestione sicura Integer per dimensioni Buffer
Il sovraflusso del buffer può anche derivare da overflow di interi quando le dimensioni del calcolo.
#include <stdlib.h>
#include <limits.h>
#include <errno.h>
void *safe_malloc_array(size_t nmemb, size_t size) {
if (nmemb == 0 || size == 0) {
return NULL; // Or handle zero-size allocation
}
if (nmemb > SIZE_MAX / size) {
// Integer overflow would occur
errno = ENOMEM;
return NULL;
}
return malloc(nmemb * size);
}
Utilizzo di snprintf per le stringhe formattate
char log_message[256];
int ret = snprintf(log_message, sizeof(log_message),
"User %s logged in from %s", username, ip_address);
if (ret < 0) {
// Output error
} else if ((size_t)ret >= sizeof(log_message)) {
// Truncation occurred; handle if needed
}
Migliori Pratiche aggiuntive
- Iniziaalizzare i buffer[ — Sempre zero-initializzare i buffer per evitare perdite di memoria non inizializzata.
- Avoid recursion with unbounded deep[[] — I sovrapposti di stack possono verificarsi da profonda ricorsione; usare iterazione o profondità limite.
- Utilizza ] qualificante[ — Aiuta il compilatore ad ottimizzare e può catturare problemi di alias, anche se non direttamente prevenire i trabocchetti.
- Preferire [-correttatezza[] – Previene la modifica accidentale delle stringhe di input e fa rispettare l'intento.
- Implementa la gestione degli errori[[] — Non ignorare i valori di ritorno dalle funzioni come , [, , ecc.
Risorse per ulteriori apprendimento
- CERT C Coding Standard[[] — Regole complete per la codifica C sicura.
- CWE-120: Copia buffer senza controllare la dimensione dell'input[[ — classificazione di MITRE di punti di sovraflusso del buffer.
- OWASP Buffer Overflow[[] — Guida pratica dal progetto di sicurezza delle applicazioni Web aperto.
- GNU C Manuale della Biblioteca: Strumenti e utilità di Array[ — Documentazione per funzioni di stringa sicure.
- AddressSanitizer[ — Un rivelatore di errore di memoria veloce.
Conclusioni
Prevenire overflow buffer in C non è facoltativo; è una responsabilità fondamentale di qualsiasi sviluppatore che lavora con la lingua. Comprendendo i meccanismi di overflow, la sostituzione di funzioni pericolose con alternative più sicure, convalidando rigorosamente input e dimensioni, consentendo protezioni di compilatore, e utilizzando analisi statiche e test, è possibile ridurre drasticamente il rischio di queste vulnerabilità.