Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Tesi di Laurea
matteo mauro
3
4 Indice
5
6 i concetti chiave: dependability, security e static analysis
11
12 la static analysis
L’approccio più semplice tra le tecniche di analisi statica consiste nel rice-
vere in input un modulo sorgente e “parsare” (esaminare lessicalmente)
il contenuto carattere per carattere, con l’obiettivo di rilevare pattern di
istruzioni che possono attivare difetti (intesi come bugs di programma-
zione); ovviamente trattare il contenuto sorgente come uno stream di
caratteri non permette di distinguere semanticamente commenti, stringhe
di letterali, dichiarazioni, funzioni. . . e rappresenta quindi un approccio
poco efficace.
Il passo successivo consiste nell’eseguire un’analisi lessicale (come
avviene nei compilatori con l’ausilio di strutture dati a dizionario) per
individuare categorie diverse di costrutti sintattici (assegnazioni, dichia-
razioni, espressioni. . . ); sebbene uno stream di “token semantici” sia
migliore di uno stream di caratteri, tutto ciò porta ad un elevato numero
di “falsi positivi”, ovvero di rilevamenti sbagliati di difetti inesistenti. Ciò
è causato dal fatto che l’analizzatore ancora non comprende il comporta-
mento del sistema durante una reale esecuzione, perché non considera
le relazioni tra funzioni e/o moduli diversi. Il miglioramento successivo
implica la costruzione di un “albero sintattico astratto” (AST Abstract
Syntax Tree) , ovvero una struttura dati che consente di esplorare la strut-
2.3 considerazioni sull’uso dei tools 13
La static analysis non può rilevare ogni genere di vulnerabilità del soft-
ware sottoposto ad esame, in quanto il tool verificherà esclusivamente
la presenza di pattern e/o regole previste dall’analizzatore stesso. Nel
caso in cui non siano rilevate ulteriori vulnerabilità, un buon analiz-
zatore dovrebbe comunicare un messaggio tipo :“Spiacente, nessuna
ulteriore vulnerabilità individuata.” piuttosto che :”Il software è privo di
vulnerabilità.”. [8]
Il problema del rilevamento di errori attraverso la static analysis è
indecidibile, ovvero non è sempre possibile realizzare un algoritmo che
soddisfi la garanzia di trovare tutte e sole le vulnerabilità di ogni pro-
gramma; ogni tool realizza delle approssimazioni che generano falsi
positivi e falsi negativi: questi ultimi sono sicuramente più pericolosi in
quanto generano una falsa idea di sicurezza.
Un buon tool di analisi statica dovrebbe essere user-friendly, anche
per un programmatore con una superficiale preparazione nel campo del
secure coding, guidandolo nel corretto utilizzo delle best practices (lin-
guaggi come il C prevedono infatti un importante ed affermato insieme
di regole).
Tra i principali vantaggi nell’uso dei tools di static analysis elenchiamo:
1. Type Checking
La forma più largamente usata di static analysis riguarda il control-
lo del tipo, ovvero impedire al programmatore di associare tipi di
variabili incoerenti tra loro. Linguaggi fortemente tipizzati come
Java sfruttano largamente questo concetto, offrendo al programma-
tore la sicurezza che (a meno di downcast espliciti!) il programma
non subirà crash dovuti ad incoerenze tra tipi; altri linguaggi in-
terpretati (come Python) possono essere preventivamente soggetti
ad una verifica di static analysis, così da rilevare subito potenziali
problemi.
2. Style checking
Insieme di regole che suggeriscono utili informazioni per aiutare la
leggibilità e manutenibilità del codice: nomi di funzioni e variabili,
spaziatura, elementi deprecati, commenti ecc. Come esempio signi-
ficativo si pensi al compilatore gcc: se utilizzato con il flag “-Wall”
nella compilazione del seguente pezzo di codice:
typedef enum { red, green, blue } Color;
char* getColorString(Color c) {
char* ret = NULL;
switch (c) {
2.4 tipi di tools di static analysis 15
case red:
printf("red");
}
return ret;
}
in questo caso il tool non è a conoscenza del fatto che, essendo inBuf
uguale a NULL, non c’è nessuna memoria da liberare e quindi non
rappresenta un errore.
I tools di static analysis più precisi (ovvero quelli che verificano la pre-
senza di una specifica vulnerabilità) sono capaci di computare decine di
migliaia di righe di codice, prima che le risorse in termini di tempo e me-
moria diventino ingestibili. Al contrario, i tools più semplici analizzano
18 la static analysis
poche righe di codice per volta, rilevando per esempio funzioni deprecate
o exploitabili in pochi millisecondi, ma risultano inevitabilmente meno
efficienti. Spesso la precisione viene ridotta per aumentare la scalabilità,
ovvero la possibilità di rilevare una classe di vulnerabilità più ampia.
La profondità di un tool è direttamente proporzionale alla visibilità
(scope) di ricerca: il controllo di poche righe di codice permette di ricevere
risultati più rapidamente ma più superficiali, mentre l’analisi globale di
più moduli può richiedere ore di computazione per progetti complessi.
3
C L A S S I F I C A Z I O N E D E L L E V U L N E R A B I L I TA’
19
20 classificazione delle vulnerabilita’
Tabella 4.1
6. qualità del codice: una scarsa qualità del codice sviluppa comporta-
menti inaspettati: dal punto di vista dell’utente può risultare in una
scarsa usabilità, mentre da quello dell’attaccante rappresenta una
facile opportunità di forzare le funzionalità del software. Dereferen-
ziare un puntatore nullo o consentire un ciclo infinito rappresenta
una mancanza di buona qualità del codice;
Tabella 4.2
gie e consigli atti a fornire consapevolezza dei rischi nei confronti delle
aziende.
Le regole sono divise in specifiche categorie, che vengono esposte qui
di seguito:
1. Autenticazione utente
L’autenticazione è il processo di verifica dell’identità di una perso-
na/ente/sito web ecc; nel contesto delle applicazioni web è comune-
mente realizzata tramite l’invio di credenziali (username, password
o altre informazioni private) che solo l’utente originale dovrebbe
conoscere. Le regole OWASP si concentrano su:
a) implementazione efficace dei controlli delle credenziali;
b) policy di scelta di user ID e password (lunghezza, caratteri
alfanumerici ecc);
c) meccanismi sicuri di recovery delle credenziali;
d) trasmissione sicura su connessioni cifrate (TSL o altri protocol-
li);
e) memorizzazione sicura delle password e prevenzione da attac-
chi a “dizionario” (brute force);
3.2 owasp: secure coding rules 23
2. Session Management
Una sessione di comunicazione è rappresentata da una serie di ri-
chieste e risposte (solitamente HTTP) associate allo stesso utente. Le
moderne web applications possono fornire servizi che richiedono
la conservazione (temporanea o persistente) di informazioni sullo
stato dell’utente, dal primo all’ultimo messaggio ricevuto; perciò
la sessione utente racchiude la presenza di variabili e parametri di
identificazione, come ad esempio i diritti di accesso, localizzazione
ecc. Una sessione può esistere anche senza un meccanismo di au-
tenticazione: basti pensare al protocollo HTTP (che è stateless) ed
al meccanismo dei cookie, che consentono il riconoscimento di un
utente che ha già visitato il sito precedentemente. Le regole OWASP
si concentrano su:
a) meccanismi efficienti e sicuri di creazione e validazione dei
token usati dall’utente;
b) implementazioni delle procedure di login/logout;
c) nascondere la sessione privata di un utente lato server;
d) generare periodicamente nuovi identificatori di sessione dopo
un tempo di espirazione o in seguito ad ogni autenticazione.
3. Controllo dell’accesso
Qualora un servizio web dovesse essere accessibile previa auto-
rizzazione, questa categoria rappresenta l’insieme delle regole da
implementare per l’accesso sicuro alle risorse del sistema software:
files, URLs protetti, funzionalità, dati e metadati degli utenti ecc. Le
pratiche di secure coding di controllo dell’accesso spaziano da:
a) separazione della logica di criptazione dei dati e validazione
degli utenti;
b) gestione sicura e comunicazione dei tentativi falliti di accesso;
c) limitazione al numero di transazioni eseguite in un arco di
tempo stabilito;
d) negazione di accesso ad ogni utente non autenticato secondo
specifici requisiti.
4. Validazione dell’input
24 classificazione delle vulnerabilita’
25
26 gestione dell’input (input handling)
Il giusto approccio nella verifica dei dati in ingresso risiede nel control-
larne l’appartenenza ad un sicuro insieme di valori, tecnica conosciuta
come whitelisting. Quando l’insieme di valori è piuttosto ridotto, allora
può essere efficace usare la “selezione indiretta”, ovvero offrire all’utente
una lista di legittime opzioni identificabili da un valore numerico, una
stringa, un’interfaccia grafica con dei pulsanti di selezione ecc.
In molte situazioni però la selezione indiretta non è fattibile in quan-
to l’insieme di possibili opzioni è potenzialmente infinito (per esempio
l’insieme dei possibili indirizzi email o dei numeri di telefono). In questi
casi si adotta il whitelisting, ovvero si specifica un pattern di rappresen-
tazione corretta dei dati (per esempio attraverso espressioni regolari); un
valore in input viene accettato esclusivamente se è conforme al pattern
28 gestione dell’input (input handling)
33
34 buffer overflow
Allocazione statica
Il più grande vantaggio si esprime nella semplicità di poter sempre
essere a conoscenza della dimensione del buffer: ciò semplifica le pro-
cedure di controllo a costo di una minore flessibilità del programma,
in particolare non sempre il troncamento dei dati è accettabile; un altro
svantaggio risiede nel determinare lo spazio di allocazione iniziale, in
quanto è sempre auspicabile utilizzare la minore quantità di spazio in
memoria per salvare le informazioni, evitando il fenomeno della "fram-
mentazione interna" (esempio: la dimensione di un buffer che conterrà
5.1 tattiche di prevenzione 35
Allocazione dinamica
Disaccoppiare la definizione del buffer dall’effettiva quantità di alloca-
zione di memoria permette un utilizzo più flessibile del sistema, a costo
di una più severa e scrupolosa verifica di sicurezza delle operazioni. E’
riportato un esempio che mette a confronto lo stesso codice implementato
tramite allocazione statica e con allocazione dinamica.
// Soluzione: allocazione statica
/* Inserisce una stringa in str, fino a un massimo di BUFSIZE-1
caratteri (con eventuale troncamento della stringa originaria */
int main(int argc, char **argv) {
char str[BUFSIZE];
int len;
len = snprintf(str, BUFSIZE, "%s(%d)", argv[0], argc);
printf("%s\n", str);
if (len >= BUFSIZE) {
printf("length truncated (from \%d)\n", len);
}
return SUCCESS;
}
}
printf("%s\n", str);
free(str);
str = NULL;
return SUCCESS;
}
portato nel corso degli anni ad accettare nello standard versioni modi-
ficate di funzioni preesistenti (vulnerabili) tra le quali: scanf(), strcpy(),
sprintf(), gets() (quest’ultima è stata direttamente rimossa dalla libreria
<stdio.h> nell’ultima versione del linguaggio C11), ecc. Segue una breve
panoramica di alcune di queste funzioni.
gets(char *ptr)
Analizziamo come esempio la funzione gets(char *ptr): essa consente
di copiare nel buffer, puntato da ptr, tutto ciò che viene scritto dall’utente
fino al carattere di invio, senza considerare alcun limite di capienza;
l’estrema vulnerabilità di questa funzione è stata stranamente mantenu-
ta anche nella creazione del linguaggio C++, in quanto risulta ancora
possibile utilizzatore l’operatore » per ricevere input da stdin senza
controlli.
//operazione non sicura in C++
char line[512];
cin >> (line);
Per fornire API più sicure sono state introdotte nuove funzioni che
simulano il servizio fornito da alcune delle funzioni sopra descritte, ma
permettono di specificare il numero massimo di caratteri che devono
essere scritti: alcuni esempi sono strncpy(), strncat(), fgets() ecc; altre
38 buffer overflow
45
46 errori ed eccezioni
L’uso di static analysis può risultare utile per identificare quelle ec-
cezioni che non dovrebbero essere catturate: in particolare eccezioni
come NullPointerException, OutOfMemoryError o StackOverflowError
possono essere incoscientemente "controllate" (ovvero deliberatamente
ignorate, ad esempio stampando solo il log del messaggio di errore) per
impedire che il programma termini forzatamente la sua esecuzione; ciò è
ovviamente una pratica di programmazione scorretta, ma questi controlli
48 errori ed eccezioni
49
50 la suite juliet
I test cases sono utilizzati per dimostrare le capacità dei tools di static
analysis: in particolare risulta interessante studiare l’abilità nel seguire
flussi di dati e di controllo diversificati, verificando se il tool riesca in
contesti diversi a rilevare la falla. I tipi di data flows e/o control flows
7.3 progettazione dei test cases 51
3. good1, good2, good3, etc. – nomi generici per i casi esclusi rispetto
a sopra.
I test cases sono stati progettati per rendere facilmente ricavabili i risultati
della valutazione di analisi statica.
Quando un tool analizza un singolo test case, il tool dovrebbe auspica-
bilmente individuare almeno una vulnerabilità all’interno della funzione
cattiva (nel senso definito nel paragrafo 4), ciò è considerato un “vero
positivo”; nel caso in cui il tool non riesca a tracciare la falla presente, ab-
biamo un “falso negativo”. Analogamente, un “falso positivo” si ottiene
se il tool rileva una vulnerabilità in una funzione buona.
Un tool potrebbe inoltre rilevare vulnerabilità non direttamente corre-
late al CWE di riferimento del test case. Ci sono due occasioni in cui ciò
può accadere:
53
54 misra-c 2004
1. statements di controllo;
2. controllo di flusso;
ESEMPIO 1:
printLine(dest);
}
ESEMPIO 2:
{
/* By initializing dataBuffer, we ensure this will not be the
* CWE 690 (Unchecked Return Value To NULL Pointer) flaw for
fgets() */
char dataBuffer[100] = "";
char * data = dataBuffer;
printLine("Please enter a string: ");
/* FLAW: check the return value, but do nothing if there is an
error */
if (fgets(data, 100, stdin) == NULL)
{
/* do nothing */
}
printLine(data);
}
exit(1);
}
9
CONCLUSIONI
La static analysis dei codici vulnerabili della suite Juliet richiedeva delle
fondamenta teoriche propedeutiche: nella prima parte di questa tesi sono
state esplorate le proprietà e le classificazioni dei sistemi safety-related,
esplorando i concetti di Dependability e Security. Approfondendo le
caratteristiche di quest’ultime, si è passati al tema della static analysis: le
tecniche di rilevazione degli errori di sviluppo (che possono diventare
vulnerabilità soggette ad exploits) e l’importanza di applicare tools di
static analysis ai processi di validazione sono tasselli fondamentali nel
ciclo di vita del software.
Successivamente sono state approfondite le principali vulnerabilità
software legate alla sicurezza: tecniche di gestione sicura dell’input (whi-
telisting/blacklisting, controlli sulla lunghezza, metacaratteri e codifiche),
buffer overflows, integer overflows, gestione sicura degli errori, infine
esempi pratici di funzioni e costrutti da evitare (nei linguaggi C e Java).
Per ogni vulnerabilità, si è riflettuto sull’efficacia che un tool di static ana-
lysis possa avere nel riuscire a rilevarla e/o prevenirla, in considerazione
del lavoro successivo riportato nell’ultima sezione della tesi.
Nell’ultima parte della tesi sono state introdotte le best practices
MISRA-C 2004: è stato interessante mettere in contrapposizione lo stan-
dard OWASP, legato profondamente alla prevenzione di vulnerabilità (da
un punto di vista agnostico rispetto alla tecnologia) rispetto alle regole
MISRA, compendio di buone regole di programmazione nel dominio
software automotive (specifico del linguaggio C).
59
60 conclusioni
for systems that have to safe and/or secure, should be using the MISRA
C Guidelines, [. . . ].” [11]
Avendo libero accesso alle guide-lines del 2004 (la versione precedente
e gratuitamente accessibile), mi sono chiesto quanto esse fossero efficaci
nell’affrontare vulnerabilità software: adottando il tool Understand v5.0
per sottoporre i codici della suite Juliet a verifiche di static analysis, ho
comparato le regole MISRA-C 2004 violate ed ho esaminato l’impatto
sulla prevenzione che esse avrebbero potuto avere se legittimamente
rispettate. L’appendice B espone tutti i test di static analysis, compiuti su
40 diversi programmi appartenenti a diverse categorie di vulnerabilità.
La conclusione dei risultati ottenuti conferma che le regole MISRA-C
2004 sono completamente inadatte alla rilevazione e/o prevenzione di
vulnerabilità software, non riuscendo quasi mai ad identificare l’errore
oppure proponendo soluzioni troppo generiche ed inefficaci.
BIBLIOGRAFIA
[3] Gary McGraw, Brian Chess - Static Analysis for Security - published
by IEEE COMPUTER SOCIETY - anno: 2007
[5] Brian Chess, Jacob West - Secure programming with Static Analysis -
“Static analysis in the big picture” pg. 11, pubblicato da Addison-
Wesley, giugno 2007
[6] Brian Chess, Jacob West - Secure programming with Static Analysis -
“Static analysis in the big picture” pg. 71, pubblicato da Addison-
Wesley, giugno 2007
[7] Brian Chess, Jacob West - Secure programming with Static Analysis -
“Static analysis in the big picture” pgg. 76-78, pubblicato da Addison-
Wesley, giugno 2007
[9] Juliet Test Suite v1.2 for C/C++ User Guide - do-
cumentazione ufficiale Dicembre 2012 - URL:
https://samate.nist.gov/SRD/resources/Juliet_Test_Suite_v1.2_for_C_Cpp_-
_User_Guide.pdf
61
62 Bibliografia
[10] Brian Chess, Jacob West: Static Analysis for Security, Editor: Gary
McGraw, novembre/dicembre 2004
[12] Brian Chess, Jacob West - Secure programming with Static Analysis -
“Static analysis in the big picture” pg. 178, pubblicato da Addison-
Wesley, giugno 2007
Appendice A
- Regola 1.2 (required) “Non deve essere riposta nessuna fiducia sui comportamenti
non definiti dalle funzioni utilizzate”
- Regola 1.3 (required) “Un uso combinato di compilatori e/o linguaggi diversi
dovrebbe essere permesso solo se esiste un’interfaccia comune di interazione tra i file
oggetto, ai quali i compilatori/linguaggi siano conformi”
Diverse API fornite dalle librerie standard non prevedono una descrizione totale del loro
funzionamento, diversi aspetti (spesso riguardanti le condizioni di errore) sono lasciati
volontariamente non definiti e, di conseguenza, sono dipendenti dal compilatore e/o
dall’architettura sottostante. Esempi di questi comportamenti non definiti sono: utilizzo dello
stack, passaggio di parametri e modalità di caricamento dei dati (dimensioni dei tipi,
allineamento in memoria, aliasing ecc).
- Regola 2.1 (required) “I pezzi di codice Assembly devono essere incapsulati e isolati”
Qualora sia necessario utilizzare istruzioni in-line assembly, esse devono essere rifattorizzate
in funzioni separate o macro. Per ragioni di efficienza, è spesso necessario utilizzare tali
approcci a basso livello (es: controllo sugli interrupts), ciò deve essere comunque
documentato e validato, in quanto rappresenta un’estensione del linguaggio e viola la regola
1.1.
- Regola 4.1 (required) “Solo i caratteri di escape definiti nello standard ISO C devono
essere utilizzati”
Tutte le sequenze di escape esadecimali e ottali (tranne ‘\0’) sono proibite.
- Regola 5.2 (required) “Due o più identificatori distinti non devono possedere lo stesso
nome, qualora il primo occulti lo scope del secondo”
- Regola 5.3 (required) “Un typedef dovrebbe possedere un nome univoco all’interno
del file sorgente”
Ogni blocco di codice (generalmente individuato da parentesi graffe) aggiunge un livello
interno di visibilità (scope): gli identificatori (nomi di variabili, funzioni ecc) ad un livello più
interno nascondono le dichiarazioni degli identificatori più esterni; ciò deve essere evitato
per evitare ambiguità sia nella leggibilità del codice che nella programmazione.
Lo stesso ragionamento si applica alla regola 5.3.
- Regola 6.1 (required) “Il tipo char deve contenere ed utilizzare esclusivamente valori
alfanumerici”
- Regola 6.2 (required) “I tipi signed char e unsigned char devono esser usati
esclusivamente per valori numerici”
Ci sono 3 tipi distinti:
- char
- signed char
- unsigned char
Il tipo char deve essere usato esclusivamente per rappresentare caratteri alfanumerici: il
technical corrigendum emanato a luglio 2017 identifica come valori alfanumerici:
‘A’,’5’,’\n’,”a” ecc. La sua rappresentazione (che determina proprietà come il segno) è
dipendente dall’architettura.
Gli operatori che si possono usare con il tipo char sono l’assegnazione (=), le operazioni di
confronto (==, !=) e le operazioni esplicite di cast.
- Regola 6.3 (advisory) “I typedefs che indicano la dimensione dei tipi dovrebbero essere
usati al posto dei tipi numerici di base forniti dal linguaggio”
I tipi base char, short, int, long, float, double (sia in versione signed che unsigned) non
devono essere usati, ma si devono sfruttare dei typedefs che documentino la dimensione, ad
esempio per una macchina a 32 bit:
typedef char char_t;
typedef signed char int8_t;
typedef signed short int16_t;
typedef signed int int32_t;
typedef signed long int64_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long uint64_t;
typedef float float32_t;
typedef double float64_t;
typedef long double float128_t;
Ciò consente di aumentare la leggibilità del codice e a porre consapevolezza circa la
dimensione dei tipi usati: ovviamente ciò non rende automaticamente il codice portabile su
più architetture, ma aiuta a prevedere possibili incoerenze tra l’aritmetica del calcolatore ed il
programma.
- Regola 6.4 (required) “I campi bit devono essere dichiarati come signed int o unsigned
int”
- Regola 6.5 (required) “Un campo bit di tipo signed deve essere grande almeno 2 bit”
Dato che il segno del tipo int è dipendente dall’architettura, è necessario sempre specificarne
l’appartenenza. Inoltre un campo signed int deve essere sempre di almeno 2 bit, altrimenti
signed risulta una dichiarazione inutile.
- Regola 1.1 (required) “Le costanti e le escape sequences ottali non devono essere
usate”
Ogni costante intera che inizia con 0 è considerata ottale. Ciò può causare un errore, da parte
del programmatore, qualora vengano inizializzate delle variabili a lunghezza fissa, come nel
seguente esempio:
code[0] = 109; /* 109 in base 10*/
code[1] = 100; /* 100 in base 10 */
code[2] = 052; /* 42 in base 10 */
code[3] = 071; /* 57 in base 10 */
L’array code[] contiene codici di 3 cifre, ma il valore “052” è trattato in base 8 (quindi 42
decimale) e ciò può generare confusione nella scrittura del codice.
Le sequenze di escape ottali possono essere problematiche a causa della possibilità di
introdurre cifre decimali nella sequenza stessa, evento che porta ad un differente
interpretazione da quella desiderata: ad esempio la sequenza “\109” viene considerata
l’unione dei caratteri “\10” e “9”, mentre “\100” è un’unico carattere. Non essendo possibile
impedire l’introduzione di cifre decimali in sequenze ottali, quest’ultime sono proibite
(eccetto l’unica sequenza “\0”).
Contesto
Nel linguaggio C, dichiarazione e definizione sono 2 concetti distinti:
- dichiarazione: introduce un identificatore in un programma, ovvero il nome di variabili,
namespaces, funzioni ecc. Le dichiarazioni specificano anche le informazioni sul tipo. Un
nome deve essere dichiarato prima di poter essere usato: in C il punto in cui viene
dichiarato un nome determina il suo scope nei confronti del compilatore, perciò non è
possibile fare riferimento ad un identificatore in un punto successivo alla dichiarazione
(nel caso delle variabili si può aggirare tale comportamento con il modificatore “extern”).
- definizione: specifica il codice di una funzione o i dati di una variabile: consiste quindi
nell’effettiva autorizzazione ad allocare spazio in memoria per tale elemento.
- Regola 8.3 (required) “Per ogni argomento formale e tipo di ritorno di una funzione,
il tipo deve sempre coincidere con quello esplicitato nella dichiarazione”
- Regola 8.6 (required) “Le funzioni devono essere dichiarate con visibilità globale (file
scope)”
Dichiarare funzioni localmente in blocchi interni può portare a codice poco leggibile e
comportamenti inaspettati (ad esempio nascondere dichiarazioni più esterne
inconsapevolmente).
- Regola 8.7 (required) “Le variabili usate esclusivamente all’interno di una funzione
devono essere dichiarate localmente a quest’ultima”
E’ necessario ridurre al minimo la visibilità degli identificatori quando necessario, così da
evitare comportamenti inaspettati; qualora una variabile venga usata solo all’interno di una
variabile, non ha senso dichiararla globale nel file sorgente (ciò ha senso quando la variabile
deve possedere un linkage interno o esterno).
- Regola 8.8 (required) “Una funzione o una variabile esterna deve essere dichiarata
esclusivamente una volta”
Seppure sia lecito dichiarare multiple volte la stessa variabile/funzione, ciò è proibito per
questioni di leggibilità e manutenibilità del codice. Generalmente ciò si conclude nel porre
tale dichiarazione in un file header, che verrà incluso ove necessario.
- Regola 8.10 (required) “Tutte le funzioni/variabili dichiarate con file scope devono
possedere internal linkage, a meno che l’external non sia strettamente necessario”
- Regola 8.11 (required) “Ogni funzione/variabile con internal linkage deve essere
esplicitamente dichiarato con la parola chiave static”
Ogni variabile/funzione che deve essere utilizzata esclusivamente dalle funzioni interne al
file sorgente di dichiarazione deve prevedere la parola chiave static: ciò permette di eliminare
ambiguità di omonimia tra identificatori quando si includono delle librerie.
Prima di definire le regole legate alle conversioni, si introducono alcuni concetti importanti.
Cast esplicito
Un cast esplicito può essere effettuato per uno dei seguenti motivi:
- per modificare il tipo con cui un’espressione aritmetica viene eseguita;
- per troncare deliberatamente il valore di un dato;
- per rendere più chiara la conversione di tipo che un dato assumerebbe implicitamente.
Cast implicito
Si riportano 2 classi principali di cast implicito.
1) Promozione ad intero (integral promotion)
La promozione ad intero descrive il processo con il quale ogni operazione aritmetica è
condotta sui tipi int o long (signed o unsigned): tutte le classi di variabili come short e char
sono adeguatamente convertite in tipi int o unsigned int prima di essere utilizzate in
espressioni algebriche: questi tipi vengono chiamati small integer types; la regola impone che
ogni small integer venga convertito in un int se i valori del tipo originario possono essere
effettivamente rappresentati senza perdite, altrimenti si converte in un unsigned int. Tale
promozione si effettua:
- solo agli small integers;
- si applica agli operandi degli operatori unari, binari e ternari;
- non si applica agli operatori logici;
- si applica al controllo di selezione degli switch.
Attenzione a non confondere la promozione ad intero con il “bilanciamento” (approfondito in
seguito): la promozione avviene quando i due operandi all’interno di una espressione sono
dello stesso tipo.
La promozione ad intero è il motivo per cui, ad esempio, la somma di 2 variabili di tipo
unsigned short restituiscono un oggetto di tipo signed/unsigned int; eseguire queste
operazioni attraverso tale processo di conversione consente di evitare alcuni casi di integer
overflow: si supponga che un int sia grande 32 bit, la moltiplicazione tra 2 variabili short di
16 bit permette di generare senza perdita di dati un numero a 32 bit (ovviamente su
un’architettura in cui la dimensione di un int risulti minore, tale procedura non risolve il
problema).
2) Conversioni di bilanciamento
Nello standard ISO C, il bilanciamento è definito come “conversione aritmetica ordinaria
(Usual Arithmetic Conversion)”: questo insieme di regole definisce un meccanismo di
conversione per il quale 2 operandi di tipo diverso vengono convertiti in un tipo comune; essa
è sempre espletata quando sono presenti espressioni che coinvolgono 2 variabili di tipi
diversi, inoltre entrambe le variabili possono essere soggette a cast implicito.
Il processo di bilanciamento è preceduto dal processo di promozione ad intero (descritto
precedentemente) anche qualora gli operandi coinvolti siano dello stesso small integer types.
Gli operatori che innescano un bilanciamento sono:
- moltiplicativi/additivi: *, /, %, +, -
- bitwise: &, |, ^
- relazionali: <, <=, >, >=, ==, !=
Tali operatori restituiscono un risultato appartenente al tipo definito dal bilanciamento (tranne
i relazionali che restituiscono un valore bollano codificato su un int).
2) Espressioni complesse
Il termine “espressione complessa” si riferisce a qualunque operazione aritmetica diversa da:
- un’espressione costante;
- un lvalue;
- il valore di ritorno di una funzione.
Esempi di espressioni complesse:
s8a + s8b
~u16a
u16a >> 2
foo(2) + u8a
*ppc + 1
++u8a
Le seguenti espressioni non sono complesse (ma possono contenere sotto-espressioni
complesse):
pc[u8a] //lvalue
foo(u8a + u8b) //valore di ritorno di foo()
**ppuc //dereferenziazione = lvalue
*(ppc + 1) //dereferenziazione = lvalue
pcbuf[s16a * 2] //lvalue
- Regola 10.1 (required) “Il valore di un'espressione di tipo intera non deve subire un
cast implicito ad un tipo sottostante (guardare definizione sopra) diverso se:
• non è una conversione ad un tipo intero più grande dello stesso segno;
• l'espressione è complessa;
• l'espressione non è costante ed è l'argomento di una funzione;
• l'espressione non è costante ed è il ritorno di una funzione.”
- Regola 10.3 (required) “Il valore di un'espressione complessa di tipo intera può essere
convertita esclusivamente con un tipo dello stesso segno e non più largo del tipo
sottostante dell'espressione eseguita.”
- Regola 10.4 (required) “Il valore di un'espressione complessa di tipo reale può essere
convertita esclusivamente ad un tipo non più largo del tipo sottostante
dell'espressione eseguita.”
Come descritto precedentemente, i cast all'interno delle espressioni complesse sono fonte di
confusione e risulta necessario applicare delle limitazioni. Per essere conformi a queste
regole, può risultare necessario utilizzare variabili temporanee e/o statements aggiuntivi.
- Regola 10.5 (required) “Quando l'argomento degli operatori ~ e << sono di tipo
unsigned short o unsigned char, il risultato dell'operazione deve essere
immediatamente castato con il tipo sottostante”
Quando vengono utilizzati gli operatori ~, << e >> sugli small integer types, a quest'ultimi
viene applicata la promozione ad intero, processo che può generare un estensione del segno
sui bit più significativi ad insaputa del programmatore.
Nel seguente esempio:
uint8_t port = 0x5aU;
uint8_t result_8;
uint16_t result_16;
uint16_t mode;
- Regola 10.6 (required) “Il suffisso U deve essere applicato alle costanti di tipo
unsigned”
Solo alcuni tipi di conversione sono definiti dal linguaggio C, altri sono dipendenti
dall’implementazione.
- Regola 11.1 (required) “I puntatori a funzioni possono essere convertiti
esclusivamente verso tipi di puntatore ad intero”
- Regola 11.2 (required) “Un puntatore ad oggetto può essere convertito esclusivamente
verso uno dei seguenti tipi di puntatore:
• puntatore ad intero;
• puntatore ad oggetto dello stesso tipo;
• puntatore a void. “
- Regola 11.3 (advisory) “Non è permesso eseguire un cast tra un tipo puntatore ed un
tipo intero”
La dimensione dell’intero richiesta quando si esegue un cast di un puntatore è dipendente
dall’architettura sottostante. Nonostante ciò, la regola è solo advisory dal momento che non è
possibile evitare tale comportamento quando si indirizzano registri di memoria o altre
caratteristiche dell’hardware specifiche.
- Regola 11.4 (advisory) “Non è permesso eseguire un cast tra tipi diversi di puntatori
ad oggetti”
Conversioni di questo tipo possono portare ad incoerenze qualora il nuovo tipo richieda un
allineamento più rigoroso in memoria. Esempio:
uint8_t * p1;
uint32_t * p2;
p2 = (uint32_t *)p1;
- Regola 11.5 (required) “I cast che tentano di eliminare o aggirare i modificatori const
e volatile non sono permesse”
- Regola 12.1 (required) “E’ necessario fare poca affidabilità sulle precedenze degli
operatori in C, esplicitando quando possibile l’ordine delle operazioni attraverso l’uso
di parentesi tonde”
Le parentesi tonde devono essere utilizzate per rendere esplicito l’ordine di esecuzione di
determinate operazioni, nonostante sia importante trovare un compromesso tra leggibilità ed
inutile ingombro di parentesi all’interno codice.
- Regola 12.2 (required) “Il valore restituito da un’espressione deve essere sempre lo
stesso , indipendentemente dall’ordine di valutazione che lo standard C consente”
A parte per determinati operatori ( l’operatore di chiamata (), &&, ||, ?: ecc), l’ordine di
valutazione di alcune sotto-espressioni non è definito formalmente, in particolare questi casi
esulano dalla regola precedente perché non possono essere semplicemente “forzati”
dall’utilizzo di parentesi:
- incremento/decremento di operatori: considerare il seguente codice:
x = b[i] + i++;
evidentemente il risultato cambia in base alla precedenza che la valutazione di b[i] ha rispetto
agli altri operatori;
- argomenti di funzione:
x = func( i++, i );
le espressioni degli argomenti passai sono dipendenti, quindi l’ordine di valutazione modifica
la chiamata.
- assegnamenti innestati:
l’ordine di valutazione delle espressioni in assegnamenti multipli può provocare effetti
indesiderati, esempi come i seguenti sono da evitare:
x = y = y = z / 3 ;
x = y = y++;
- Regola 12.3 (required) “L’operatore sizeof non deve essere usato in congiunzione ad
espressioni innestate”
L’operatore sizeof restituisce la dimensione del tipo dell’espressione, ma non assicura che
l’espressione venga valutata, ad esempio:
int32_t i;
int32_t j;
j = sizeof(i = 1234); /* j ottiene la dimensione del tipo di i, ovvero un
int*/
/* i NON ottiene il valore 1234. */
- Regola 12.7 (required) “Gli operatori di bitwise non devono essere usati con operandi
il cui tipo sottostante è rappresentato con segno”
- Regola 13.1 (required) “Gli operatori di assegnamento non devono essere usati in
espressioni che devono restituire un valore booleano”
Per evitare errori logici ed ambiguità (come ad esempio confondere “==“ con “=“), ogni
espressione che restituisce un valore booleano non può contenere operazioni di
assegnamento. Il seguente esempio risulta pertanto non lecito:
if ( ( x = y ) != 0 ){
foo();
}
- Regola 13.3 (required) “Le espressioni floating-point non devono essere direttamente
coinvolte in operazioni di uguaglianza, ma è necessario calcolare una soglia di
tolleranza con cui comparare il risultato”
A causa dell’aritmetica finita del calcolatore, la rappresentazione floating-point di un numero
reale non può assumere determinati valori e ciò può causare degli errori di confronto; i
seguenti esempi mostrano operazioni non lecite:
float32_t x, y;
if ( x == y ) /* not compliant */
if ( x == 0.0f) /* not compliant */
if ( ( x <= y ) && ( x >= y ) ) /* not compliant */
Il metodo raccomandato per affrontare questi calcoli è quello di implementare una libreria di
funzioni specifica per eseguire operazioni di confronto tra variabili reali, le cui API esposte
permettano di accettare una soglia di tolleranza (ovvero una stima della precisione richiesta
per accettare l’uguaglianza).
- Regola 13.5 (required) “Le tre espressioni di un ciclo for devono essere coerenti con la
semantica del costrutto for”
Un ciclo for è un costrutto ben definito semanticamente: il suo scopo è realizzare un ciclo
contatore; pertanto le tre espressioni devono seguire le seguenti indicazioni:
- 1° espressione: inizializza uno o più contatori;
- 2° espressione: effettua il test sulla condizione di uscita, sulla base della variabile contatore
(ed eventualmente altri flag);
- 3° espressione: incrementa/decrementa il contatore.
- Regola 14.1 (required) “Non deve essere presente codice non raggiungibile”
Questa regola si applica a quelle porzioni di codice per le quali non esistono flussi di
esecuzione che le raggiungano; presentano eccezioni quei contesti in cui il codice può essere
raggiunto ma mai eseguito (ad esempio sezioni di codice inerenti alla sicurezza del
programma).
Linee di codice che seguono un’istruzione break e funzioni mai richiamate fanno all’interno
del programma sono esempi di codice mai raggiunto.
- Regola 14.7 (required) “Una funzione deve prevedere un’unico punto di uscita, posta
come ultima istruzione”
- Regola 14.8 (required) “Il corpo dei costrutti switch, do…while, while e for deve
sempre essere racchiuso tra parentesi graffe”
Tale regola si applica anche a statement di un’unica istruzione, ciò impedisce di ottenere
comportamenti inaspettati e di essere fuorviati da indentazioni scorrette.
- Regola 14.10 (required) “Tutti i costrutti if…else if devono includere una clausola
finale else”
Analogamente al default case di uno switch, in una serie di if…else if a cascata può essere
utile individuare un blocco finale che rappresenta l’opzione non riconosciuta dagli altri
statement: anche quando non è prevista nessuna specifica azione, l’ultima clausola else è
obbligatoria, eventualmente accompagnata da un commento descrittivo.
- Regola 16.1 (required) “Le funzioni non devono prevedere un numero variabile di
argomenti”
A causa dei molteplici rischi legati all’uso di variadic functions, è proibito definire interfacce
con un numero variabile di argomenti. Come riportato qui, non è sempre possibile per il
compilatore determinare se il tipo degli argomenti passati è coerente con l’interfaccia della
funzione (incappando quindi in comportamenti indefiniti e non previsti).
- Regola 16.2 (required) “La programmazione ricorsiva, sia diretta che indiretta, è
vietata”
Nella progettazione di sistemi affidabili, la tecnica della ricorsione introduce problemi di
utilizzo elevato delle risorse, che possono portare a cali delle prestazioni considerevoli fino
all’eccedere i limiti di allocamento di spazio in memoria.
void a(void);
int main(void){
a(5);
return 0;}
}
void a(int n){ //stampa n;} //non compila perché non è coerente con la
dichiarazione
- Regola 16.10 (required) “Se una funzione restituisce un codice di errore, allora tale
risultato deve essere controllato”
Per i motivi descritti e approfonditi nel capitolo 6 “Errori ed eccezioni”, ogni possibile
informazione di errore restituita deve essere verificata e risolta, prima di ogni avanzamento
del programma. Ignorare i codici di ritorno non è conforme alle regole.
- Regola 17.2 (required) “Sottrazioni tra due puntatori devono essere effettuate su
puntatori che indirizzano elementi appartenenti allo stesso array”
- Regola 17.3 (required) “Gli operatori <,<=,>,>= non devono essere usati sui tipi
puntatori, eccetto quando puntano agli elementi di uno stesso array”
La disposizione degli elementi in memoria non è deterministica, pertanto effettuare le
operazioni descritte non è sempre coerente con le aspettative (tranne per gli elementi di un
array che sono sempre allocati consecutivamente).
- Regola 17.6 (required) “L’indirizzo di una variabile locale non deve essere assegnato
ad una variabile con uno scope più elevato”
Ovviamente l’indirizzo di una variabile locale diventa non valido dal momento in cui essa
cessa di esistere.
- Regola 20.3 (required) “La validità dei valori passati alle funzioni delle librerie
standard deve sempre essere verificata”
Molte funzioni appartenenti alla librerie standard non prevedono un accurato controllo dei
parametri d’ingresso (alcune non ne prevedono affatto), tra le quali i seguenti esempi:
- libreria math.h : sqrt() e log() non prevedono un controllo sui numeri negativi;
- Regola 20.4 (required) “L’allocazione dinamica della memoria non deve essere
utilizzata”
La regola preclude l’utilizzo di funzioni come malloc(), calloc(), realloc() e free().
Esiste un numero molto ampio di comportamenti non definiti o dipendenti dall’architettura
circa l’uso di memoria heap, i quali possono condurre ad errori quali memory leaks,
inconsistenza dei dati ed esaurimento della memoria.
Anche le funzioni che utilizzano tale tecnica (come alcune presenti nella libreria string.h)
devono essere evitate.
- Regola 20.5 (required) “La variabile errno non deve essere usata”
errno rappresenta un feature del linguaggio C utile per la rilevazione di errori, ma risulta
formalmente poco definita dallo standard: ad esempio un valore diverso da zero potrebbe
indicare o meno un errore dipendentemente dal contesto di esecuzione.
1° programma:
“CWE121_Stack_Based_Buffer_Overflow__char_type_overrun_memcpy_01.c"
void CWE121_Stack_Based_Buffer_Overflow__char_type_overrun_memcpy_01_bad()
{
{
charVoid structCharVoid;
structCharVoid.voidSecond = (void *)SRC_STR;
/* Print the initial block pointed to by structCharVoid.voidSecond */
printLine((char *)structCharVoid.voidSecond);
/* FLAW: Use the sizeof(structCharVoid) which will overwrite the pointer
voidSecond */
memcpy(structCharVoid.charFirst, SRC_STR, sizeof(structCharVoid));
structCharVoid.charFirst[(sizeof(structCharVoid.charFirst)/
sizeof(char))-1] = '\0'; /* null terminate the string */
printLine((char *)structCharVoid.charFirst);
printLine((char *)structCharVoid.voidSecond);
}
}
8.11: variabili globali e funzioni che vengono usate solo 2 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
correct_length = sizeof(structCharVoid.charFirst);
memcpy(structCharVoid.charFirst, SRC_STR, correct_length);
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
La regola 20.4 non risulta pienamente pertinente alla vulnerabilità introdotta: essa pone
consapevolezza, nei confronti del programmatore, riguardo i rischi dell’allocazione
dinamica, ma non risulta efficace nell’individuare la vulnerabilità perché non si accorge
della mancanza di liberazione di memoria, piuttosto consiglia a priori di non utilizzare tale
Matteo Mauro Matricola: 5971842 !9
tecnica di programmazione (a seconda del software da sviluppare, l’allocazione dinamica
potrebbe essere obbligatoriamente necessaria).
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
void CWE476_NULL_Pointer_Dereference__binary_if_01_bad()
{
{
twoIntsStruct *twoIntsStructPtr = NULL;
/* FLAW: Using a single & in the if statement will cause both sides of
the expression to be evaluated thus causing a NPD */
if ((twoIntsStructPtr != NULL) & (twoIntsStructPtr->intOne == 5))
{
printLine("intOne == 5");
}
}
}
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
void CWE78_OS_Command_Injection__char_connect_socket_execl_01_bad() {
char * data;
char dataBuffer[100] = COMMAND_ARG2;
data = dataBuffer;
{
int recvResult;
struct sockaddr_in service;
char *replace;
SOCKET connectSocket = INVALID_SOCKET;
size_t dataLen = strlen(data);
do {
/* POTENTIAL FLAW: Read data using a connect socket */
connectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (connectSocket == INVALID_SOCKET){
break;
}
memset(&service, 0, sizeof(service));
service.sin_family = AF_INET;
service.sin_addr.s_addr = inet_addr(IP_ADDRESS);
service.sin_port = htons(TCP_PORT);
if (connect(connectSocket, (struct sockaddr*)&service,
sizeof(service)) == SOCKET_ERROR){
break;
}
/* Abort on error or the connection was closed, make sure to recv
one
* less char than is in the recv_buf in order to append a terminator
*/
/* Abort on error or the connection was closed */
recvResult = recv(connectSocket, (char *)(data + dataLen),
sizeof(char) * (100 - dataLen - 1), 0);
if (recvResult == SOCKET_ERROR || recvResult == 0){
break;
}
/* Append null terminator */
data[dataLen + recvResult / sizeof(char)] = '\0';
/* Eliminate CRLF */
replace = strchr(data, '\r');
if (replace){
*replace = '\0';
}
replace = strchr(data, '\n');
if (replace){
*replace = '\0';
}
} while (0);
if (connectSocket != INVALID_SOCKET){
CLOSE_SOCKET(connectSocket);
}
}
/* execl - specify the path where the command is located */
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
void CWE90_LDAP_Injection__w32_char_connect_socket_01_bad(){
char * data;
char dataBuffer[256] = "";
data = dataBuffer;
int recvResult;
struct sockaddr_in service;
char *replace;
SOCKET connectSocket = INVALID_SOCKET;
size_t dataLen = strlen(data);
{
LDAP* pLdapConnection = NULL;
ULONG connectSuccess = 0L;
ULONG searchSuccess = 0L;
LDAPMessage *pMessage = NULL;
char filter[256];
/* POTENTIAL FLAW: data concatenated into LDAP search, which could
result in LDAP Injection*/
snprintf(filter, 256-1, "(cn=%s)", data);
pLdapConnection = ldap_initA("localhost", LDAP_PORT);
if (pLdapConnection == NULL)
{
printLine("Initialization failed");
exit(1);
}
connectSuccess = ldap_connect(pLdapConnection, NULL);
if (connectSuccess != LDAP_SUCCESS)
{
printLine("Connection failed");
exit(1);
}
searchSuccess =
ldap_search_ext_sA(pLdapConnection,"base",LDAP_SCOPE_SUBTREE,filter,&pMessage);
if (searchSuccess != LDAP_SUCCESS)
{
printLine("Search failed");
if (pMessage != NULL)
{
ldap_msgfree(pMessage);
}
exit(1);
}
/* Typically you would do something with the search results, but this is
a test case and we can ignore them */
/* Free the results to avoid incidentals */
if (pMessage != NULL)
{
ldap_msgfree(pMessage);
}
/* Close the connection */
ldap_unbind(pLdapConnection);
}
}
2) Analisi della vulnerabilità
Matteo Mauro Matricola: 5971842 !17
LDAP (Lightweight Directory Access Protocol) è un protocollo di comunicazione client-
server per l’interazione con un servizio di directory centralizzato. Attraverso delle API
consente agli utenti di accedere in lettura a file condivisi all’interno di una rete; nel
programma presentato i parametri di ricerca vengono recuperati da una socket (quindi
sono forniti in remoto da un utente), per poi essere concatenati senza validazione alla
query di ricerca (istruzione -> snprintf(filter, 256-1, "(cn=%s)", data); ).
Ciò può modificare illegittimamente il comportamento della query, portando a “leggere e/
o modificare dati applicativi” (fonte: https://cwe.mitre.org/data/definitions/90.html).
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
struct {
char charFirst;
int intSecond;
} structCharInt;
char *charPtr;
structCharInt.charFirst = 1;
charPtr = &structCharInt.charFirst;
/* FLAW: Attempt to modify intSecond assuming intSecond comes after
charFirst and is aligned on an int-boundary after charFirst */
*(int*)(charPtr + sizeof(int)) = 5;
printIntLine(structCharInt.charFirst);
printIntLine(structCharInt.intSecond);
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
E’ importante notare che l’analisi di questo test case non ha rilevato la violazione della
regola 16.10, che si applica alla vulnerabilità rilevata.
Dato che l’obiettivo è rilevare le regole MISRA-C 2004 inerenti alla security, questa viene
riportata nella tabella finale come regola non rilevata ma evidentemente violata.
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
char * filename;
char tmpl[] = "fnXXXXXX";
int fileDesc;
filename = mktemp(tmpl);
if (filename == NULL)
{
exit(1);
}
printLine(filename);
/* FLAW: Open a temporary file using open() and flags that do not
prevent a race condition */
fileDesc = open(filename, O_RDWR|O_CREAT, S_IREAD|S_IWRITE);
if (fileDesc != -1)
{
printLine("Temporary file was opened...now closing file");
close(fileDesc);
}
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
void CWE561_Dead_Code__return_before_code_01_bad()
{
bad_function();
}
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
14.7: una funzione deve avere una singola istruzione di uscita 1 Required PERTINENTE
posta alla fine della funzione stessa
void CWE562_Return_of_Stack_Variable_Address__return_buf_01_bad()
{
printLine(helperBad());
}
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
void CWE570_Expression_Always_False__global_01_bad()
{
/* FLAW: This expression is always false */
if (globalFalse)
{
printLine("Never prints");
}
}
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
void CWE571_Expression_Always_True__global_01_bad()
{
/* FLAW: This expression is always true */
if (globalTrue)
{
printLine("Always prints");
}
}
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
void CWE674_Uncontrolled_Recursion__infinite_recursive_call_01_bad()
{
helperBad();
}
8.11: variabili globali e funzioni che vengono usate solo 1 Required NON
PERTINENTE
all’interno del file in cui sono dichiarate, dovrebbero essere
dichiarate static
Legenda:
(*) := regola violata, ma non rilevata dal tool Understand v5.0
14.7 - Control flow 36° programma - CWE561: Dead code - return before code
Conclusioni dell’Appendice B
Il sottoinsieme di regole violate che sono state rilevate dal tool Understand v5.0 sono
complessivamente poche, rispetto al totale fornito dalle MISRA-C 2004 (due regole sono
state inoltre aggiunte manualmente poiché attinenti ma non identificate).
Come si può evincere dalla tabella riepilogativa, le regole pertinenti alle vulnerabilità
software presentate dai testcases rappresentano una netta minoranza: la maggior parte
delle regole non prevengono né identificano direttamente gli errori, nonostante la grande
varietà di classi di vulnerabilità proposte dalla CWE e implementate nella suite Juliet. Le
poche regole effettivamente attinenti sono legate all’uso di funzioni deprecate e statements
di controllo mal codificati.