In C, non usando 'void' se una funzione non accetta alcun argomento è una potenziale vulnerabilità

8

Nello standard di codifica sicura CERT, vi è una raccomandazione che " Specifica sempre void anche se una funzione non accetta argomenti ". In esso viene proposta una possibile vulnerabilità di sicurezza.

/* Compile using gcc4.3.3 */
   void foo() {
        /* Use asm code to retrieve i
         * implicitly from caller
         * and transfer it to a less privileged file */
       }

   /* Caller */
   foo(i); /* i is fed from user input */

In this noncompliant code example, a user with high privileges feeds some secret input to the caller that the caller then passes to foo(). Because of the way foo() is defined, we might assume there is no way for foo() to retrieve information from the caller. However, because the value of i is really passed into a stack (before the return address of the caller), a malicious programmer can change the internal implementation and copy the value manually into a less privileged file.

C'è qualche exploit che usa questa pratica di codifica "non sicura". Se no, qualcuno può spiegare o fornire un codice di esempio su come questo può essere usato. Inoltre, se questo è davvero difficile da sfruttare o non è possibile, potresti spiegare perché?

    
posta Jor-el 27.09.2013 - 14:41
fonte

2 risposte

13

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.

    
risposta data 27.09.2013 - 15:22
fonte
5

In C, la funzione prototipo foo(); dichiara una funzione che accetta un numero imprecisato di parametri. Questo differisce da molti altri linguaggi (incluso C ++) in cui tale prototipo indicherebbe una funzione che richiede parametri no .

I due potenziali problemi elencati nella pagina a cui ti colleghi sono indicati come Interfaccia ambigua e Uscita di informazioni .

A partire da quest'ultimo, si potrebbe immaginare di lavorare per un'organizzazione che gestisce le informazioni classificate. È possibile eseguire audit del codice per garantire che le informazioni TOP SECRET non possano essere trasferite in un file classificato come solo SECRET. Un tale controllo del codice potrebbe facilmente perdere un prototipo come foo(); , che consentirebbe effettivamente alle informazioni di fluire in esso sotto forma di un parametro non dichiarato. Se foo(); è qualcosa come update_log(); e il nostro doppio agente è in grado di modificare il codice per update_log(); per ricevere un parametro e scriverlo come una voce di registro, potresti vedere come è possibile raggiungere l'outflow.

L'altro potenziale problema, un'interfaccia ambigua, si riferisce al fatto che il prototipo foo(); può essere chiamato in modi diversi. È un po 'inverosimile, ma non impossibile, che tale ambiguità possa portare a una vulnerabilità del software.

Se un'implementazione di foo (); sta facendo qualcosa di molto non standard, come usare l'aritmetica del puntatore per raggiungere il frame dello stack dei chiamanti, quindi passare i parametri a foo(); potrebbe interrompere quell'aritmetica del puntatore che porta a problemi arbitrari.

    
risposta data 27.09.2013 - 15:25
fonte

Leggi altre domande sui tag