Quello che succede qui è che la funzione foo()
usa una cosiddetta dichiarazione vecchio stile , cioè come le cose sono state fatte in C prima della prima normalizzazione (aka "ANSI C" dal 1989) . In pre-ANSI C, una funzione bar()
che accetta due argomenti di tipi int
e char *
sarebbe definita in questo modo:
void bar()
int i;
char *p;
{
/* do some stuff */
}
e sarebbe dichiarato come segue (di solito in un file di intestazione):
void bar();
Ciò significa che il codice che utilizza la funzione includerebbe il file di intestazione, che quindi trasmetterebbe informazioni sull'esistenza della funzione, e sul suo tipo di ritorno (qui, void
), ma nulla sul numero di argomenti e i loro tipi. Pertanto, al momento dell'utilizzo, il chiamante deve fornire i parametri e sperare che abbia inviato il numero e i tipi appropriati . Se il codice del chiamante non funziona correttamente, il compilatore non emetterà avvisi.
Come vulnerabilità della sicurezza , non è molto convincente. Ha senso solo in un contesto di ispezione del codice. Alcuni revisori stanno riesaminando un sacco di codice sorgente. Uno sviluppatore malvagio sta cercando di fare cose malvagie che il revisore non noterà (questo è lo scenario che viene esplorato nel Concorso C sottoterra ) . Presumibilmente, l'auditor guarderà i file header e anche al start dell'implementazione della funzione. Nel tuo esempio, vedrà:
void foo()
{
/* some stuff */
}
quindi il revisore può semplicemente presumere che foo()
non abbia alcun parametro, poiché la parentesi di apertura segue immediatamente foo()
. Tuttavia, il codice del chiamante (che è altrove) chiama foo()
con alcuni parametri. Il compilatore C non può avvisare: poiché la funzione è dichiarata "vecchio stile", il compilatore C non sa, durante la compilazione del codice chiamante, che foo()
non utilizza effettivamente alcun parametro (o almeno così sembra). Il codice del chiamante farà spingerà gli argomenti sullo stack (e li rimuoverà al ritorno). Il programmatore malvagio quindi include nella definizione di foo()
un assembly fatto a mano per recuperare gli argomenti dallo stack, anche se, a livello di sintassi C, non esistono.
Quindi, un canale di comunicazione semi-nascosto tra due codici malvagi (il codice del chiamante e la funzione chiamata), in un modo che non è visibile da un'ispezione superficiale della dichiarazione di funzione e l'inizio della definizione, e, soprattutto, non avvertito anche dal compilatore C.
Come vulnerabilità , la trovo piuttosto debole. Lo scenario è abbastanza poco plausibile.
Il problema riguarda maggiormente la garanzia della qualità. Le dichiarazioni vecchio stile sono pericolose , non a causa dei programmatori malvagi, ma a causa dei programmatori umani , chi non può pensare a tutto e deve essere aiutato dagli avvertimenti del compilatore. Questo è tutto il punto dei prototipi di funzioni , introdotti in ANSI C, che includono informazioni sul tipo per i parametri di funzione. Nei nostri esempi, ecco due prototipi:
void foo(void);
void bar(int, char *);
Con queste dichiarazioni, il compilatore C noterà che il codice del chiamante sta tentando di inviare parametri a una funzione che non ne usa, e interromperà la compilazione o almeno emetterà un avvertimento scritto in modo severo.
Un tipico problema con i prototipi vecchio stile è l'incapacità di eseguire cast di tipo automatico. Ad esempio, con questa funzione:
void qux()
char *p;
int i;
{
/* some stuff */
}
e questa chiamata:
qux(0, 42);
Il compilatore, vedendo la chiamata, crederà che i due parametri siano due valori di int
. Ma la funzione si aspetta davvero un puntatore e quindi un int
. Se l'architettura è tale che un puntatore prende la stessa dimensione nello stack come int
(e anche tale che un puntatore NULL è codificato allo stesso modo di un intero di valore 0, che è una caratteristica piuttosto comune), quindi le cose sembra funzionare. Quindi compila quello su un'architettura in cui i puntatori sono due volte più grandi degli interi: il codice fallirà perché il 42
sarà interpretato come parte del valore del puntatore.
(I dettagli dipendono dall'architettura, ma questo sarebbe tipico del codice C a 16 bit compilato su un'architettura 16/32-bit con allineamento a 16 bit, ad esempio una CPU 68000. Su architetture moderne a 64 bit,% i valori diint
tendono ad essere allineati a 64 bit nello stack, il che risparmia la pelle di molti programmatori incurati, ma il problema è più generale: problemi simili si verificano con i tipi a virgola mobile.)
Dovresti usare i prototipi; non perché le funzioni vecchio stile inducono vulnerabilità, ma perché inducono bug .
Nota a margine: in C ++, i prototipi sono obbligatori, quindi la dichiarazione void foo();
significa in realtà "nessun argomento di alcun tipo", come quello che void foo(void);
significherebbe in ANSI C.