Vous êtes sur la page 1sur 140

Scuola di Scienze Matematiche, Fisiche e Naturali

Corso di Laurea in Informatica

Tesi di Laurea

REGOLE DI PROGRAMMAZIONE PER LA


S A F E T Y E S E C U R I T Y: A N A L I S I , S T R U M E N T I
E RELAZIONI

PROGRAMMING RULES FOR SAFETY AND


S E C U R I T Y: A N A LY S I S , T O O L S A N D
R E L AT I O N S

matteo mauro

Relatore: Paolo Lollini


Correlatore: Andrea Ceccarelli

Anno Accademico 2017-2018


Matteo Mauro: Regole di programmazione per la Safety e Security: analisi,
strumenti e relazioni, Corso di Laurea in Informatica, © Anno Accademico
2017-2018
INDICE

1 I concetti chiave: Dependability, Security e Static Analysis 5


1.1 Fallimenti, errori e difetti (failures, errors, faults) 5
1.2 Dependability e Security: proprieta’ caratteristiche 6
1.3 Mezzi per affrontare le vulnerabilita’ 7
2 La Static Analysis 11
2.1 Panoramica su altre metodologie di testing 11
2.2 Evoluzione dei tools di analisi statica 12
2.3 Considerazioni sull’uso dei tools 13
2.4 Tipi di tools di static analysis 14
2.5 Compromessi tra precisione, profondita’ e scalabilita’ 17
3 Classificazione delle vulnerabilita’ 19
3.1 "The Seven Pernicious Kingdoms" 19
3.2 OWASP: Secure Coding Rules 21
4 Gestione dell’input (Input Handling) 25
4.1 Validazione dell’input: cosa verificare? 26
4.2 Validazione dell’input: come verificare? 27
4.3 Standardizzare le procedure di verifica dei dati 29
5 Buffer Overflow 33
5.1 Tattiche di prevenzione 34
5.2 Stringhe e buffer overflow 36
5.3 Taint propagation 38
5.4 Rappresentazione dei caratteri, codifica e decodifica 39
5.5 Integer overflow 40
6 Errori ed eccezioni 45
6.1 Gestire errori attraverso i codici di ritorno 45
6.2 Gestire errori attraverso le eccezioni 46
7 La suite Juliet 49
7.1 Struttura dei nomi dei test cases 50
7.2 Flow variants 50
7.3 Progettazione dei test cases 51
7.4 Considerazioni sull’uso di analisi statica sulla suite Ju-
liet 52
8 MISRA-C 2004 53
9 Conclusioni 59
2 Indice
P R E FA Z I O N E

Le moderne realtà industriali affidano l’esecuzione dei propri processi


aziendali a sistemi informatici, i quali devono garantire agli utenti il cor-
retto espletamento dei servizi richiesti nelle modalità e nelle tempistiche
necessarie: si introduce quindi il concetto di “sistema critico”, ovvero
un software che consenta alle persone di provare un legittimo senso di
“fiducia” nell’utilizzarlo. Tale proprietà si identifica come Dependability.
Parallelamente si introduce la definizione di Security, ovvero la capa-
cità di un sistema di prevenire l’insorgere di vulnerabilità che possano
portare ad una serie di imprevedibili e pericolose conseguenze: blocco o
rallentamento del sistema informatico, accesso non autorizzato a risorse
HW/SW ecc; tale proprietà può essere garantita dall’utilizzo di standard
di "Secure Coding”, ovvero best practices di sviluppo del software pro-
gettate per questo specifico compito. Nel corso degli anni i professionisti
del settore hanno identificato la sorgente di tali problematiche nell’in-
troduzione di comuni errori di programmazione, causati spesso da una
parziale conoscenza e cattiva interpretazione del linguaggio utilizzato.
La seguente tesi esplora i concetti di Dependability e Security di un
sistema software, evidenziando le differenze sostanziali nel processo di
corretta validazione e garanzia delle suddette proprietà. In particolare si
è interessati a studiare l’efficacia che le best practices di programmazione
MISRA-C 2004 possono avere nel garantire un alto livello di security:
quest’ultime sono specificatamente dedicate alla certificazione di qualità
dei sistemi embedded scritti in linguaggio C e sono intrinsecamente
legate al concetto di dependability, ma non sono state progettate con l’o-
biettivo di assicurare anche la security, pertanto l’obiettivo è determinare
l’impatto che tali regole possiedono nel prevenire e/o rilevare l’insorgere
di vulnerabilità latenti.
La tesi è strutturata come segue.
Il capitolo 1 si concentra sullo studio delle definizioni di Dependability
e Security, classificandone le proprietà e le tecniche per garantirle. La
terminologia qui introdotta sarà alla base della trattazione di tutta la tesi.
Il capitolo 2 introduce il tema della static analysis, ovvero la meto-
dologia di verifica della qualità del codice utilizzata in questo lavoro
di tesi. In questo capitolo si descrive le tecniche di verifica che i tools

3
4 Indice

adottano, le differenze sostanziali con gli altri modelli di validazione e si


ragiona sull’utilità di impiegare processi di static analysis durante il ciclo
di sviluppo di un sistema informatico.
Il capitolo 3 descrive la principali categorie di vulnerabilità software
che minacciano la proprietà di security di un sistema. Tale sezione rap-
presenta un’introduzione fondamentale per i capitoli successivi nei quali
si esaminano dettagliatamente le implementazioni ed i rischi delle prin-
cipali vulnerabilità (l’analisi sarà limitata alle categorie più frequenti e
che saranno oggetto di test nella fase finale della tesi). Coerentemente
con i test effettuati per studiare l’efficacia delle regole MISRA-C 2004
(come accennato precedentemente) i capitoli 4, 5 e 6 sono incentrati sullo
studio di vulnerabilità note e frequentemente riscontrabili nella progetta-
zione di varie tipologie di sistemi informatici: ciò aiuterà ad avere una
comprensione migliore nell’analisi dei software campione vulnerabili.
Il capitolo 4 espone i rischi alla sicurezza provocati da una scorret-
ta gestione dell’input fornito al sistema, enfatizzando le tecniche di
individuazione delle sorgenti non sicure e di corretta validazione.
Il capitolo 5 tratta i rischi legati a buffer overflows, le tattiche di pre-
venzione nei confronti sia della gestione delle stringhe, sia di buffer di
variabili generiche (riportando anche un esempio specifico di possibile
exploit).
Il capitolo 6 tratta il tema della gestione degli errori, in particolare pone
consapevolezza sulle metodologie ed accortezze necessarie per fornire
all’utente il minor numero di informazioni necessarie a comunicare
l’errore verificatosi in maniera esaustiva.
Il capitolo 7 espone la struttura della suite Juliet, una collezione di
software (realizzato dall’NSA/CAS) con vulnerabilità note, introdotte
intenzionalmente per essere oggetto di studio della static analysis.
Il capitolo 8 riporta lo studio effettuato sulle regole MISRA-C 2004 e le
possibili influenze che possiedono sulla prevenzione e/o rilevazione di
vulnerabilità software (mostrando alcuni esempi di test di static analysis).
L’appendice A riepiloga lo studio riportato sulle regole MISRA-C 2004,
approfondendo nel dettaglio tutte le regole potenzialmente inerenti al
tema della security.
L’appendice B riporta l’insieme di test di static analysis effettuati sulla
suite Juliet, con l’obiettivo di rilevare quante regole MISRA-C 2004 sono
state violate e quante di queste sarebbero attinenti al tema della security.
1
I C O N C E T T I C H I AV E : D E P E N D A B I L I T Y, S E C U R I T Y E
S TAT I C A N A LY S I S

Le moderne realtà industriali ed aziendali prevedono l’utilizzo di com-


plessi sistemi informatici, impiegati nella gestione dei dati (elaborazione,
condivisione, conservazione. . . ) e nell’erogazione di servizi. Nel corso
degli ultimi decenni si è sviluppata la rilevante questione del concetto di
Dependability nei confronti di un sistema informatico e su come garantir-
la. La presente sezione propone definizioni e classificazioni dei concetti
di base, su cui poggerà la trattazione dei temi di static analysis e secure
coding: prima di studiare tali argomenti è necessario definire un contesto
di concetti univoci e ben delineati, frutto dello studio degli ultimi decenni
di ricerca nei campi della sicurezza ed affidabilità dei sistemi.
Un sistema dependable è un sistema su cui riporre fiducia in maniera
giustificata: dal punto di vista dell’utente, tale proprietà riflette il grado
di fiducia che viene riposta nella corretta esecuzione delle operazioni
previste e/o nei servizi ricevuti. A tal proposito, è necessario definire
cosa si intende con “servizio corretto” e “fallimento”.

1.1 fallimenti, errori e difetti (failures, errors, faults)

Ogni sistema informatico descrive l’insieme delle funzioni offerte attraver-


so le “specifiche funzionali”, ovvero una raccolta di descrizioni dei servizi
proposti in termini di funzionalità e performance; un servizio, accessibile
attraverso un’interfaccia, è pertanto definibile come il comportamento
(ovvero lo stato) percepito dall’utilizzatore [1] .
Un servizio è “corretto” quando la sua implementazione è coerente con
le specifiche funzionali, ma se devia da tali requisiti allora si definisce
“non corretto”: tale evento si definisce come “fallimento” del servizio
(failure).
La transizione di un sistema in uno stato in cui il servizio offerto è
“non corretto” è definito “errore” (error), tale deviazione è causata dalla

5
6 i concetti chiave: dependability, security e static analysis

presenza (interna o esterna al sistema) di un “difetto” (fault), che può


risultare latente fino alla sua attivazione (deliberata o involontaria).
Un sistema informatico di grande dimensione prevede diversi moduli
che interagiscono tra loro, un difetto presente in un componente interno
può attivarsi e propagarsi (attraverso l’interfaccia fornita) ad altre entità:
quando giunge infine all’interfaccia esterna (visibile all’utente) si verifica
un fallimento che può portare ad un’interruzione del servizio; la difficoltà
nell’individuare la causa è spesso ricondotta alla “catena di propagazio-
ne” che si innesca sotto determinate condizioni. I metodi studiati per
affrontare i difetti di sviluppo sono descritti nel paragrafo 1.3.

1.2 dependability e security: proprieta’ caratteristiche

Il concetto di Dependability è rappresentabile dalle seguenti proprietá [1] :

1. reliability: continuità nel tempo del servizio;

2. availability: prontezza del servizio;

3. safety: assenza di conseguenze catastrofiche per gli utenti o per


l’ambiente d’uso;

4. integrity: protezione da alterazioni improprie del sistema;

5. maintainability: possibilità di applicare modifiche e correzioni cor-


rette al sistema.

Definizione formale delle proprietà [2] :


1. Reliability: rappresenta l’abilità del software di riuscire a espletare
i propri servizi, sotto determinate condizioni, per un periodo di
tempo garantito.
Formalmente, sia t un punto sull’asse temporale:
Reliability-> R(t) è la probabilità P([0,t]) che il sistema riesca a
eseguire le proprie funzioni in modo continuativo per il tempo
specificato.

2. Availability: è la misura con cui il sistema risulta accessibile e


operativo al momento della richiesta.
Formalmente:
Availability ->A(t) è la probabilità P(t) che il sistema sia operativo
all’istante t (oppure entro un range di tempo garantito).
1.3 mezzi per affrontare le vulnerabilita’ 7

Notare la differenza tra reliability e availability: un sistema che


subisce frequentemente un fallimento ma che riesce automatica-
mente a ripristinare il servizio in tempi molto brevi, è tecnicamente
un sistema poco reliable, ma generalmente possiede un’elevata
availability.
3. Safety: un sistema safe garantisce l’assenza di conseguenze gravi
per gli utenti o l’ambiente di utilizzo. Un sistema implementato
sotto tale criterio può comprometterne la disponibilità, per esem-
pio potrebbe prevedere uno shutdown forzato (con conseguente
chiusura del servizio) quando rileva una violazione di safety.
4. Maintainability: riguarda non solo la correzione e riparazione do-
po il rilevamento di un bug, ma anche l’introduzione di nuove
funzionalità per ampliare i servizi del sistema.
Formalmente:
Maintainability -> M(t) è la probabilità P([0,t]) che il sistema possa
essere correttamente riparato (oppure che possa essere aggiornato
per fornire nuovi e/o migliori servizi) entro l’intervallo di tempo
specificato.
5. Integrity: impedisce al sistema di subire alterazioni improprie, ov-
vero di passare ad uno stato inconsistente in cui i dati sono struttu-
ralmente e/o semanticamente non corretti (per esempio le transi-
zioni in un database devono essere consistenti, ovvero mantenere
l’integrità dei record memorizzati anche a fronte di imprevisti).
Analogamente si descrive il tema della Security, costituito dai seguenti
attributi:
1. confidentiality: la protezione dalla divulgazione non autorizzata di
informazioni riservate;
2. availability: intesa come la possibilità di accedere ad un contenuto
riservato solo previa autorizzazione;
3. integrity: azioni non autorizzate non compromettono il valore dei
dati.

1.3 mezzi per affrontare le vulnerabilita’

E’ possibile individuare 4 principali categorie di tecniche per garantire


dependability e security:
8 i concetti chiave: dependability, security e static analysis

1. Prevenzione (Fault prevention): mezzi per prevenire il verificarsi


o l’introduzione di difetti di sviluppo. Nel campo delle metodo-
logie software si parla di strumenti come l’information hiding, la
modularizzazione del codice, l’uso di linguaggi strong-typed. . .

2. Tolleranza (Fault tolerance): l’obiettivo è evitare l’insorgere di un


fallimento del servizio a seguito dell’attivazione di un guasto,
attraverso tecniche di “rilevamento dell’errore” e “ripristino del
sistema”.
Solitamente la gestione dell’errore è seguita dalla “manutenzione
correttiva”, ovvero la fase di aggiornamento mirata alla rimozione
del guasto e ripristino dallo stato di errore.
Mentre “Rollback” e “Rollforward” sono richieste in seguito alla
rilevazione di uno stato di errore, la “Compensazione” risulta invece
adatta ad essere eseguita sia su domanda che sistematicamente
nel tempo, indipendentemente dalla presenza o rilevazione di un
guasto; tuttavia attenzione: il mascheramento che perviene dalla
ridondanza di controlli può celare un progressivo decadimento
della protezione del sistema nel lungo termine.
Notare che i guasti intermittenti (che si verificano con casualità)
non necessitano per forza di isolamento o riconfigurazione: identifi-
care se un guasto è sporadico o meno può essere fatto attraverso
tecniche di gestione dell’errore (l’obiettivo è analizzare la frequenza
dell’errore).
Infine è utile sottolineare che spesso la “fault tolerance” è sinonimo
di “resilience” (elasticità del sistema), ovvero la capacità di riuscire
ad erogare il servizio corretto (eventualmente con performance
minori) anche a seguito dell’attivazione di un guasto che porta ad
uno stato di errore.

3. Gestione/rimozione del guasto (fault removal): la rimozione del


guasto durante la fase di sviluppo del codice prevede la sequenza
di 3 operazioni: verifica, diagnosi e correzione; se durante la fase
di verifica il sistema non risulta aderente alle specifiche funzionali,
si deve ricorrere alla diagnosi degli errori, in cui si identifica il
problema per correggerlo successivamente. E’ fondamentale rie-
seguire tale sequenza ciclicamente per controllare di non avere
introdotto altri errori, procedimento che viene definito “verifica
della non-regressione del sistema”.
1.3 mezzi per affrontare le vulnerabilita’ 9

Una classificazione importante delle tecniche di rilevazione dell’er-


rore riguarda la necessità di esecuzione del sistema:
a) la “verifica statica” non comporta l’esecuzione a regime del
sistema, si parla di “static analysis” (ispezione del codice, ana-
lisi data-flow, compiler checks. . . ), oppure di “model checking”
(reti di Petri, automi a stati finiti o infiniti. . . );
b) la “verifica dinamica” necessita di un sistema che possa essere
eseguito, con lo scopo di eseguire fasi di testing.
Per sistemi complessi è impossibile eseguire una verifica dinamica
attraverso testing sottoponendo ogni possible input, pertanto si deve
eseguire una selezione di input fondamentali (deterministic testing)
oppure fornire un insieme di modelli casuali (random/statistical
testing).

4. Previsione (fault forecasting): esecuzione di una valutazione del


comportamento del sistema, sulla base dei seguenti approcci:
a) qualitativo: tentare di identificare e classificare le potenziali
modalità di fallimento del sistema;
b) quantitativo: determinare la misura di gravità che un certo
fallimento comporterebbe all’erogazione del servizio.
2
L A S TAT I C A N A LY S I S

La static analysis rappresenta l’insieme delle tecniche di verifica di un


software senza la necessità di lanciare in esecuzione tale sistema, introdu-
cendo strategie di rilevamento di potenziali difetti di programmazione
che agiscono direttamente sul codice sorgente.
Ogni programmatore si imbatte sporadicamente in errori di scrittura
del codice: dimenticanza di punti e virgola, errori di battitura nel nome
di variabili e funzioni ecc; in questi casi interviene il compilatore, ovvero
il software che analizza il codice sorgente con l’obiettivo di creare un file
oggetto privo di errori sintattici. Analogamente a questa soluzione, un
tool di static analysis interviene sul codice sorgente per rilevare possibili
errori logici, ovvero errori o vulnerabilità introdotte dal programmatore
che non contraddicono la sintassi del linguaggio, ma possono introdurre
problemi al momento dell’esecuzione: overflow di array, cicli infiniti,
mancata liberazione di memoria heap acquisita etc.
Il livello di qualità di un tool di static analysis può spaziare dal semplice
controllo del comportamento di statements individuali (ad esempio evi-
denziare l’utilizzo di funzioni deprecate) fino alla completa validazione
del codice sorgente (perfino se suddiviso su più moduli) [3] .

2.1 panoramica su altre metodologie di testing

Prima di parlare di static analysis, può essere utile comprenderne i


vantaggi in contrapposizione ad altre metodologie.
Molte realtà industriali tentano di affrontare la ricerca di vulnerabilità
attraverso la tecnica di “penetration testing”: al momento del rilascio
del prodotto sul mercato, un gruppo di analisti esegue una serie di
test per rintracciare l’esistenza di falle nella sicurezza, senza avere la
possibilità di studiare il codice sorgente (black-box testing) [6] ; ma tale
procedura (che solitamente avviene anche grazie all’utilizzo di tools)
non è sempre efficace: anche eventuali attaccanti hanno la possibilità di

11
12 la static analysis

lavorare sull’exploiting del software, inoltre è convinzione comune ed


accertata che risolvere un bug nelle fasi finali di sviluppo richieda una
quantità di lavoro e di soldi maggiore rispetto a risolvere la falla nelle
fasi iniziali.
Un altro approccio esistente è il “fuzzing testing”: esso consiste nel
fornire al programma una serie di input generati casualmente, in una
quantità elevata e con la speranza di ricreare situazioni estreme in cui le
vulnerabilità vengano innescate; tale metodo risulta però statisticamente
inefficiente, in quanto è spesso necessario eseguire una certa personaliz-
zazione dei valori generati per evitare lunghe e dispendiose sessioni di
testing.
Storicamente hanno avuto molta importanza l’adozione di firewall,
sistemi di rilevamento di intrusioni ecc: tali strumenti identificano un’ap-
proccio di sicurezza “a posteriori”, ovvero realizzato solo a prodotto finito
e utilizzato a regime. Le moderne organizzazioni di sviluppo software
devono prevedere la necessità di eseguire verifiche di difetti a partire
dalle prime fasi di realizzazione, in maniera trasversale al ciclo di vita
del software.

2.2 evoluzione dei tools di analisi statica

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

tura sintattica del programma in composizione con il flusso di esecuzione


simulata. [7]
E’ possibile definire 3 livelli di visibilità:

1. analisi locale: esamina una funzione per volta senza considerare


relazioni inter-procedurali;

2. analisi del modulo: lo scope è esteso all’insieme di funzioni che


collaborano tra loro all’interno di un singolo modulo;

3. analisi globale: considera le relazioni tra entità (funzioni e variabili)


appartenenti a moduli distinti.

2.3 considerazioni sull’uso dei tools

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. viene effettuata in maniera completa e automatizzata su tutto il


codice fornito, in particolare senza influenze date dall’esperienza
e/o conoscenze pregresse dello sviluppatore;

2. la verifica del codice porta all’individuazione della “radice” della


vulnerabilità, non si concentra sugli effetti: se uno sviluppatore
scopre che un certo input provoca un crash del sistema, potrebbe
14 la static analysis

aggiungere un controllo per evitare che quella specifica sequenza di


ingresso non venga digitata, ma ovviamente ciò non risolve comple-
tamente il problema; al contrario un tool di static analysis potrebbe
direttamente rilevare la vulnerabilità originaria (per esempio un
buffer overflow);

3. un tool può individuare un difetto di implementazione alle prime


fasi di sviluppo, permettendo una riduzione significativa dei costi
nella riparazione e permettendo al programmatore di imparare e
prendere coscienza di vulnerabilità a lui sconosciute: sotto questo
punto di vista la static analysis rappresenta un mezzo per veicolare
un know-how più specifico.

2.4 tipi di tools di static analysis

La static analysis prevede diversi tipi di interazione con il codice sorgente


per rilevare diverse categorie di vulnerabilità:

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;
}

mostra su schermo i seguenti messaggi:


enum.c:5: warning: enumeration value ’green’ not handled in
switch
enum.c:5: warning: enumeration value ’blue’ not handled in
switch

infatti il programma non esegue un’azione per ogni valore dell’e-


num, ciò potrebbe essere una svista del programmatore come una
scelta intenzionale, ma in ogni caso aggiungere un case vuoto per
ogni valore potrebbe rendere il codice più intuibile (a meno di non
ledere la leggibilità).

3. Comprensione della struttura del programma


Strumenti come gli IDE prevedono alcune delle funzionalità di
questa categoria: “trova tutte le chiamate di questo metodo” oppure
“trova la dichiarazione di questa variabile globale” sono tutti servizi
resi al programmatore per facilitare la comprensione del codice.
Ci sono poi sistemi più avanzati che permettono il refactoring dei
metodi, il rename delle variabili ecc.
Esistono programmi di alto livello che consentono di estrapolare
schemi concettuali dal codice, come il progetto opensource “Fujaba”,
un tool che consente di ottenere uno schema UML dal codice Java
e viceversa, individuando e riportando determinati design pattern
adottati.

4. Verifica del programma e verifica delle proprietà


Un tool di “verifica del programma” richiede in input un insieme di
specifiche e un codice da analizzare, successivamente applica mo-
delli di simulazione di esecuzione ed infine restituisce una risposta
indicando se l’implementazione rispetta o meno tali requisiti. Una
specifica completa e formale che rappresenti ogni singolo aspetto
del sistema, porterebbe il tool ad eseguire ciò che si definisce la
“verifica di equivalenza”, ma nella pratica è difficile e dispendioso
16 la static analysis

arrivare ad un tale livello di accuratezza nel campo del software


(tale approccio è molto più utilizzato nella progettazione hardware).
Da un punto di vista pragmatico questo tipo di tool esegue gene-
ralmente la “verifica delle proprietà”, ovvero analizza un singolo
comportamento del codice tentando di trovare un controesempio
che violi la specifica fornita, come mostrato nell’esempio:
inBuf = (char*) malloc(bufSz);
if (inBuf == NULL)
return -1;
outBuf = (char*) malloc(bufSz);
if (outBuf == NULL)
return -1;

Se l’allocazione di inBuf va a buon fine mentre quella di outBuf è


respinta, la funzione termina senza deallocare il primo buffer e si
ha un memory leak. Un tool che verifica l’occorrenza di tale evento
potrebbe generare un avvertimento tipo:
Violation of property "allocated memory should always be
freed":
line 2: inBuf != NULL
line 5: outBuf == NULL
line 6: function returns (-1) without freeing inBuf

Un tool si definisce “sicuro rispetto alla specifica” se è in grado


di rilevare in ogni contesto la vulnerabilità introdotta, ovvero se
non produce falsi negativi; per conseguire tale risultato può essere
necessario imporre alcuni vincoli di sviluppo del codice, come
ad esempio non introdurre puntatori a funzioni, indirizzare due
puntatori verso la stessa area di memoria (alias), non usare la
memoria dinamica ecc. Ovviamente un tale approccio è utile da
un punto di vista teorico, ma nella pratica risulta poco adattabile
al software prodotto a livello industriale, per il quale si accetta il
compromesso di ottenere sporadicamente dei falsi positivi, come il
seguente esempio:
Violation of property "allocated memory should always be
freed":
line 2: inBuf == NULL
line 3: function returns (-1) without freeing inBuf
2.5 compromessi tra precisione, profondita’ e scalabilita’ 17

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.

5. Rilevamento dei bug


L’obiettivo è quello di rilevare determinati pezzi di codice o pattern
che spesso sfociano in vulnerabilità del sistema: l’implementazione
del codice sorgente fornito viene comparata all’insieme di regole
(“bug idioms”) che costituiscono le potenziali vulnerabilità; ad esem-
pio un buon tool opensource chiamato “FindBugs” (per linguaggio
Java) potrebbe rilevare un double-check locking, una tecnica utiliz-
zata per migliorare la sincronizzazione tra thread ma che funziona
solo con l’impiego di Java1.5 e con l’utilizzo della keyword volatile.
In altre parole il rilevamento di bug non coinvolge tecniche di style
checking, ma riguarda direttamente l’individuazione di sequenze
di codice che potrebbero portare ad un comportamento inaspettato.

6. Verifica della sicurezza


I tools di static analysis incentrati sulla sicurezza ereditano molte
tecniche di rilevamento di bug e di style checking, ma applicano
metodologie diverse; una proprietà di sicurezza (come per esempio
prevenire buffer overflow per impedire l’accesso non autorizzato
ad aree di memoria) può essere generalizzata attraverso tecniche
di verifica delle proprietà (come accennato ai punti 4 e 5), contem-
poraneamente ciò implica dover riconoscere potenziali istruzioni
“exploitabili” (come la funzione strcpy()del linguaggio C) .
Mentre le tecniche di rilevamento di bug tendono a minimizzare
i falsi positivi (e di conseguenza aumentare la possibilità di falsi
negativi), i tools di verifica della sicurezza seguono un approccio
più severo, riportando tutto ciò che potrebbe risultare come difetti
di sviluppo ed è quindi necessaria una massiccia supervisione dello
sviluppatore per verificare l’attendibilità dei risultati.

2.5 compromessi tra precisione, profondita’ e scalabilita’

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’

L’uso appropriato di tools di static analysis richiede una conoscenza


delle classi di vulnerabilità esistenti. Un’utile classificazione si basa sul
concetto di “generalità”:

1. una vulnerabilità si definisce “generica” se può essere introdotta


indipendentemente dall’applicazione che si sta sviluppando e/o
dal linguaggio di programmazione utilizzato;

2. altrimenti si definisce “specifica del contesto” (context-specific fault),


qualora risieda nella complessità progettuale del software (inte-
sa come insieme delle tecnologie HW/SW coinvolte) che si sta
sviluppando.

La prima classe riguarda errori sufficientemente facili da rilevare, pro-


prio perché possono apparire indistintamente in diversi contesti (per
esempio un buffer overflow).
Ciò apre la strada anche alle vulnerabilità che sono visibili direttamente
dal codice sorgente rispetto a quelle che sono intrinsecamente legate alle
decisioni progettuali impiegate, la tabella 4.1 fornisce degli esempi in cui
si legano i due approcci.

3.1 "the seven pernicious kingdoms"

Un’utile classificazione delle vulnerabilità software prende il nome di


“Seven Pernicious Kingdoms”, ovvero sette domini (più uno extra) che
offrono una panoramica generale ma piuttosto completa da cui partire[8] :

1. Validazione e rappresentazione dell’input: vulnerabilità causate


dall’uso inappropriato di metacaratteri, codifiche/decodifiche, rap-
presentazioni numeriche ecc. Le possibili conseguenze includono
buffer overflow, cross-site scripting (noto come XSS, affligge siti

19
20 classificazione delle vulnerabilita’

Tabella 4.1

web dinamici che impiegano un insufficiente controllo dell’input


nei form), SQL injection (sottomissione di query mascherate da
informazioni di un utente) e molte altre; attualmente rappresentano
le più classiche e prevalenti falle sfruttabili nei moderni sistemi
informatici;

2. uso scorretto di API: un API rappresenta un’interfaccia attraverso la


quale viene fornito un servizio tra chiamante e chiamato, pertanto
definisce una serie di regole che devono essere rispettate affinché
la funzione erogata venga espletata correttamente. Se una delle
due parti non rispetta tale accordo di utilizzo possono verificarsi
transizioni in stati di errori inconsistenti (per esempio una classe
Java che estende la classe java.util.Random e la cui implementazione
prevede di restituire valori non casuali, rappresenta una violazione
delle API stipulate dall’interfaccia della superclasse);

3. meccanismi di sicurezza: aspetti riguardanti l’autenticazione, con-


trollo degli accessi, confidenzialità, crittografia e gestione dei privi-
legi. Per esempio, includere password hard-coded nel file sorgente
è un’evidente vulnerabilità appartenente a questa macro-categoria;

4. temporizzazione e stato: l’uso di programmazione multithread o


3.2 owasp: secure coding rules 21

l’adozione di processi concorrenti può portare a problemi di concor-


renza, a causa di interazioni non previste nel modello progettato;

5. gestione degli errori: la gestione degli errori appartiene tecnicamen-


te all’insieme delle API, ma è contemporaneamente un insieme così
importante da essere legittimamente considerato a parte: una pove-
ra ed inefficiente gestione delle situazioni di errore (oppure rivelare
all’utente troppe informazioni quando si verifica una transizione
in uno stato non corretto) è sintomo di una debole gestione degli
errori;

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;

7. incapsulamento: consiste nel progettare e implementare forti re-


strizioni su dati e/o servizi, con cui un utente deve o meno poter
interagire;

8. * ambiente: riguarda tutto ciò che non è direttamente collegato al


codice del software quanto piuttosto all’ambiente di sviluppo o di
esecuzione che lo interessa.

Per ratificare l’effettiva validità di tale classificazione può essere utile


compararla con quella della OWASP top 10 dell’anno 2004 (il prossimo
paragrafo approfondirà il contesto delle secure coding OWASP): si può
facilmente vedere che quest’ultima è perfettamente inglobata nella prima
attraverso la tabella 4.2.

3.2 owasp: secure coding rules

L’"Open Web Application Security Project” è una community online che


produce report, documentazione, metodologie di sviluppo, tools e proget-
ti open-source per la sicurezza delle applicazioni. Un importante risultato
di questo ente è la pubblicazione di regole di secure coding, una raccolta
di pratiche di programmazione mirate a prevenire vulnerabilità software.
E’ importante notare che tali pratiche sono indipendenti dalle tecnologie
e/o linguaggi di programmazione adottati, piuttosto rivelano metodolo-
22 classificazione delle vulnerabilita’

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

f) strategie intelligenti di logging e messaggi di errore.

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’

Come discusso più approfonditamente nel capitolo 4, la validazione


dell’input garantisce che solo i dati ben formati, adeguatamente
strutturati e codificati possano entrare all’interno del ciclo di esecu-
zione del sistema, prevenendo incongruenze nei database o le ese-
cuzioni impreviste di funzioni non sicure. Il controllo si concentra
su:
a) sintassi: verifica della struttura logica dei campi, coerenza dal
punto di vista dell’univocità o dell’obbligatorietà dei dati;
b) semantica: i valori dei dati devono essere coerenti con il concet-
to che rappresentano (esempio: una variabile che rappresenta
un prezzo dovrà essere maggiore o uguale a zero).
Le regole OWASP si concentrano su:
a) tecniche di implementazione dei controlli: whitelisting/blac-
klisting, validazione free-form text, espressioni regolari;
b) ridondanza di controlli sia su lato client che lato server;
c) validazione dei file inviati in upload.

Molte altre categorie infine riguardano la prevenzione da attacchi comuni


(XSS-Cross Site Scripting, SQL Injection, Cross Site Request Forgery. . . ) e
le operazioni di crittografia.
4
G E S T I O N E D E L L’ I N P U T ( I N P U T H A N D L I N G )

La corretta e completa validazione dell’input è la chiave per prevenire le


vulnerabilità, ma può diventare un’impresa ardua nel contesto di progetti
critici e di rilevanti dimensioni che ottengono dati da diversi canali; è
importante definire le seguenti idee:
1. Cosa validare:
a) validare ogni input ricevuto: effettuare controlli su ogni dato
fornito in ingresso, senza assumere che un determinato input
sia certamente sicuro;
b) identificare ogni sorgente di dati: parametri forniti da com-
mand line, file di configurazione o di log, connessioni internet,
variabili d’ambiente ecc sono tutte sorgenti di dati che possono
essere exploitate;
c) stabilire confini di sicurezza: implementare strutture dati e
algoritmi di controllo, che consentano in modo efficace di
separare i dati potenzialmente dannosi da quelli verificati;
2. Come validare:
a) selezione indiretta e whitelisting (approfonditi nel prossimo
paragrafo);
b) evitare tecniche di blacklisting;
c) rifiutare dati identificati come pericolosi, evitando procedure
di correzione automatica;
d) realizzazione di API di verifica: standardizzare un layer di
astrazione che consenta di imporre la validazione dei dati in
ingresso in ogni contesto;
e) controllare sempre la lunghezza dei dati in ingresso, verificare
sempre l’appartenenza dei dati numerici all’insieme dei valori
rappresentabili;

25
26 gestione dell’input (input handling)

4.1 validazione dell’input: cosa verificare?

Un tool di analisi statica deve individuare ogni istruzione che prevede


l’ingresso di dati in input, facendo emergere quella che viene chiamata
“superficie d’attacco” (attack’s surface).
Lo scopo è quello di chiarire allo sviluppatore quali possibili punti di
ingresso devono essere controllati, in particolare adottando i seguenti
due approcci:

1. controllo della sintassi: verificare il corretto formato dell’input,


inteso come ordine e quantità di dati ricevuti (esempio: rispettare
la struttura formale di un messaggio HTTP/1.0);

2. controllo semantico: consiste nel determinare se l’input ricevuto


contiene dati appropriati e conformi alla logica dell’applicazione.

E’ importante considerare ogni forma di sorgente dati nelle procedure


di verifica:

1. file di configurazione: Apache v1.2.29 soffriva di buffer overflow


causato da uno sbagliato controllo del numero di argomenti forniti;

2. parametri forniti da linea di comando: Hibernate v2.1.9 (framework


opensource che offre servizi di ORM “Object-Relational Mapping”)
soffriva di SQL injection permettendo all’utente di specificare il
carattere delimitatore tra query;

3. database: anche se tecniche come i triggers, stored procedures e


vincoli dovrebbero assicurare una forma di consistenza dei dati, ciò
non esonera il programmatore dall’eseguire procedure di testing:
ad esempio record che dovrebbero essere univoci devono effettiva-
mente risultare tali quando recuperati attraverso una query, inoltre
è consigliato verificare la presenza indesiderata di metacaratteri al-
l’interno dei campi recuperati dal database e che potrebbero portare
a exploit come cross-site scripting ecc;

4. network services: qualsiasi forma di dato ricevuto da una rete deve


essere controllato, non solo quelli provenienti da applicazioni di
terze parti, ma anche servizi comuni quali DNS possono nascondere
dati pericolosi (hostname DNS e indirizzi IP non garantiscono
un’autenticazione sicura, DNS poisoning/ IP spoofing).
4.2 validazione dell’input: come verificare? 27

I dati devono essere gestiti attraverso strutture dati che consentano


una ferrea separazione tra gli input verificati come sicuri da quelli non
ancora controllati. Un esempio esplicativo è quello rappresentato da un
campo proveniente da una HTTP request che viene aggiunto alla sessione
dell’utente senza essere verificato, come nel seguente esempio Java:
status = request.getParameter("status");
if (status != null && status.length() > 0) {
session.setAttribute("USER_STATUS", status);
}

Un possibile exploit causato da tale vulnerabilità è esercitabile per esem-


pio attraverso un cross-site scripting usando una pagina JSP (Java Server
Pages):
<\%
String user_state = "Unknown";
try {
HttpSession user_session = Init.sessions.get(tmpUser.getUser());
user_state = user_session == null ? "Unknown" :
(String)user_session.getAttribute("USER_STATUS");
user_state = user_state == null ? "Available" : user_state;
}
...
\%>
<\%=user_state \%>

4.2 validazione dell’input: come verificare?

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)

fornito, altrimenti viene scartato: l’ultima affermazione è importante in


quanto spesso l’intento del programmatore è quello di applicare delle
trasformazioni all’input difettoso ricevuto, con lo scopo di aumentare
l’usabilità del sistema. Un utente normale che fornisce un input sbagliato
può apprezzare che il sistema riesca comunque a trattarlo, ma ciò non
deve rendere facile ad un utente malintenzionato la possibilità di fregare
i controlli di sicurezza.

Il whitelisting può implicare una certa difficoltà progettuale o imple-


mentativa, ma in ogni caso è importante non ricorrere al blacklisting:
quest’ultima tecnica identifica un’insieme di valori non accettati e ai quali
i dati ricevuti vengono comparati. La vulnerabilità intrinseca di questa
procedura è che spesso risulta impossibile determinare ogni possibile
valore non corretto, come nell’esempio seguente (Tomcat v5.1.31):
for (int i = 0; i < content.length; i++) {
switch (content[i]) {
case ’<’:
result.append("&lt;");
break;
case ’>’:
result.append("&gt;");
break;
case ’&’:
result.append("&amp;");
break;
case ’"’:
result.append("&quot;");
break;
default:
result.append(content[i]);
}
}

Il codice riportato rivela l’intento del programmatore di prevenire il cross-


site scripting, ma ciò non risulta efficace qualora si dovesse aggiungere
altri metacaratteri, ad esempio quelli di Javascript (dove virgola ‘,’ e
parentesi “( )” hanno significati specifici) oppure file CSS (stessa ragio-
namento per ‘:’ o ‘;’). Metacaratteri distinti possono assumere diversi
significati in base al contesto; anche l’utilizzo di codifiche e compres-
sioni possono alterare la struttura dell’input, rendendo impossibile al
blacklisting identificare potenziali pericoli.
4.3 standardizzare le procedure di verifica dei dati 29

4.3 standardizzare le procedure di verifica dei dati

Solitamente un linguaggio di programmazione non offre funzionalità


built-in di controllo dei dati, ciò vuol dire che il programmatore è respon-
sabile di implementarle; invece di reinventare la ruota ad ogni passaggio,
può risultare conveniente realizzare un layer di astrazione che preveda,
in maniera chiara e consistente, delle procedure di verifica: chiamiamo
tali strumenti “API di sicurezza avanzata” (security-enhanced API).
La progettazione di queste API risulta difficile nel trovare il compro-
messo più adatto tra generalità (ovvero numero di parametri da fornire in
ingresso) ed efficienza: in molti casi è impossibile implementare controlli
di sintassi e di semantica avendo a disposizione solo il contesto front-end
dell’applicazione (questo è uno dei motivi primari per cui strumenti
come firewalls non sono in grado di risolvere ogni problema inerente alla
sicurezza).
Tra i vantaggi delle API di sicurezza troviamo:

1. applicare in maniera standardizzata e condivisa dal team di svilup-


po un insieme di criteri di verifica dei dati;

2. comprendere e mantenere consistente nel corso del tempo la logica


di validazione, durante tutto il ciclo di sviluppo del software: la
verifica degli input può essere macchinosa ed è importante preveni-
re la proliferazione di implementazioni diverse (alcune delle quali
potrebbero risultare difettose);

3. facilitare l’aggiornamento delle politiche di validazione, rifattoriz-


zando e centralizzando il codice in librerie specifiche.

Controllo della lunghezza dell’input


Verificare che la dimensione dei dati ricevuti non sia troppo elevata è
il minimo livello di sicurezza indispensabile da realizzare. Una conside-
razione interessante che può essere sollevata riguarda la ridondanza di
controlli per linguaggi safe come Java, che non portano con sé problemi
intrinsechi di buffer overflow come il C/C++ (nel senso che Java lancia
sempre un’eccezione interrompendo l’esecuzione del programma). Ciò è
solo parzialmente vero, ma prima di trattare tale argomento analizziamo
prima un esempio classico del C. La funzione gets() del linguaggio C è
una funzione deprecata, a causa del fatto che non prevede limitazioni
sulla quantità di dati letti da stdin e che vengono immessi nel buffer di
destinazione. Il problema con gets() è talmente riconosciuto che anche
30 gestione dell’input (input handling)

diversi compilatori emettono automaticamente un warning quando rile-


vano tale istruzione. Allocare un array sullo stack e richiamare gets() può
portare ad uno stack buffer overflow:
// vulnerabile qualora l’utente digiti piu’ di 127 caratteri
char buf[128];
gets(buf);

Per questioni di compatibilità, anche il C++ prevede la funzione gets()


(con le stesse falle di sicurezza), ma prevede una soluzione migliore
usando la classe string:
// buf aumenta automaticamente la quantita’ di spazio necessaria e
previene l’overflow
string buf;
cin >> buf;

Ciò non significa che un controllo dimensionale non sia necessario: un


elevata quantità di dati può portare ad esaurire la quantità di memoria
riservata al processo, causandone il crash. Detto questo anche Java può
soffrire dello stesso problema qualora si utilizzasse la funzione readLine()
per accettare dati in ingresso.

Controllo di integer overflows


L’aritmetica del calcolatore può portare a subdoli problemi di rappre-
sentazione, che possono conseguentemente portare ad errori di buffer
overflow. Si analizzi il seguente problema (http://www.di-srv.unisa.it/ ads/corso-
security/www/CORSO-0304/SuperbugWindows/7%20-%20Integer%20overflow.htm):
int bad_copy(char* buf1, char* buf2, int lengthBuf1, int
lengthBuf2){
char buffer[256];
if( (lengthBuf1 + lengthBuf2) < 256) {
// la somma dei dati e’ contenibile in buffer
memcpy(buffer, buf1, lengthBuf1);
memcpy(buffer + lengthBuf1, buf2, lengthBuf2);
}
}

Tale controllo non tiene conto di un possibile overflow della somma


delle lunghezze ricevute: in particolare se un int fosse rappresentabile su
32 bit in complemento a 2 avrebbe un range da [-231 . . . (231 )-1], se inoltre
avessimo
4.3 standardizzare le procedure di verifica dei dati 31

lengthBuf1 = lengthBuf2 = 2147483648 = 2\textsuperscript{31}


lengthBuf1 + lengthBuf2 = 4294967296 = 2\textsuperscript{32}

la somma non sarebbe rappresentabile ed in particolare il calcolatore


interpreterebbe il risultato come 0, quindi il controllo verrebbe illegittima-
mente superato e si otterrebbe una sovrascrittura di un’area di memoria
sbagliata.

Prevenire vulnerabilità legate all’uso di metacaratteri


Ogni qualvolta sia fornita all’utente la possibilità di inserire dati (per
esempio in un form) e di inviarli (per esempio attraverso la rete nei
confronti di un Web server) è necessario prevedere la possibilità che i
dati possano essere intenzionalmente manipolati per essere interpretati
erroneamente dal sistema. Linguaggi di scripting o di markup forniscono
al programmatore di specificare una fluida combinazione di strutture
sintattiche legate tra loro; per esempio una query SQL:
SELECT * FROM <table> WHERE <field> = <value>;

E’ importante non dare la capacità all’utente di inserire, rimuovere ed


alterare le strutture sintattiche utilizzate: il problema intrinseco è che
i campi vuoti hanno ognuno un particolare significato ed è necessario
che il programmatore si assicuri che i valori inseriti siano conformi. La
presenza di metacaratteri deve essere verificata in quanto possono portare
a manipolazioni intenzionali delle istruzioni. In sintesi le soluzioni da
adottare sono le tecniche di stored procedures e parametrizzazione delle
query:

1. parametrizzazione: usare wildcards all’interno delle query consente


di prevenire attacchi di tipo SQL injection, in quanto i campi inseriti
saranno effettivamente trattati come valori e non come comandi (in
generale è quindi sconsigliato usare tecniche di concatenazione di
stringhe per costruire le query);

2. stored procedures: programma scritto in SQL, generalmente salvato


nel database data dictionary, consente di imporre limitazioni sui
tipi di statements eseguiti e di interpretare correttamente i valo-
ri passati (trattazione ed esempio utile sull’argomento reperibile qui:
https://blogs.msdn.microsoft.com/brian_swan/2011/02/16/do-stored-
procedures-protect-against-sql-injection/);
5
BUFFER OVERFLOW

Un buffer overflow si verifica quando un programma scrive una quantità


di dati che eccede i limiti di memoria fisica della variabile di destinazione,
provocando potenzialmente una sovrascrittura illegittima di informazioni.
Prima di passare alla rassegna di tecniche di prevenzione, può essere utile
studiare gli exploits (intenzionali) più comuni legati ai buffer overflows.
Nel caso più classico si può avere un “stack smashing attack”, dove
una porzione di memoria appartenente allo stack (ad esempio lo stack
frame di una funzione) viene intenzionalmente sovrascritto dalle seguenti
informazioni:
1. una serie di NOP (No-OPeration, ovvero un’istruzione vuota atta a
riempire spazio, la cui utilità è discussa più avanti);

2. un codice malevolo (che l’attaccante vuole sia illegittimamente


eseguito);

3. un serie di indirizzi di ritorno che puntano al codice malevolo.


L’ultima informazione viene scritta più volte consecutivamente con lo
scopo di piazzare il dato nella cella del “return value”, ovvero la variabile
che rappresenta l’indirizzo di ritorno alla funzione “chiamante” (come
mostrato in figura 6.1).

Figura 6.1: rappresentazione in memoria di un tentativo di exploit attraverso


stack overflow [12]

33
34 buffer overflow

La premessa di funzionamento di tale exploit consiste nel sapere qua-


le sia l’indirizzo di memoria che conterrà il codice malevolo (nell’im-
magine tale indirizzo corrisponde a 0xNN): ciò può diventare un’im-
presa più complicata grazie all’introduzione di tecniche tipo ASLR
(https://en.wikipedia.org/wiki/Address_space_layout_randomization),
le quali permettono di allocare nella RAM, in maniera pseudo-casuale,
gli elementi dello spazio di indirizzamento del processo (heap, stack ecc);
lo scopo delle istruzioni NOP è proprio quello di aumentare il range di
indirizzi validi per lanciare il codice malevolo. Un’altra strategia consiste
nel progettare un’architettura che non consenta l’esecuzione di codice
proveniente da zone di memoria stack (NX-bit), oppure utilizzare una
variabile sentinella (canary word) piazzata tra variabili locali e return
address (la sua sovrascrittura consente di identificare l’occorrenza di un
buffer overflow).
Non è solo lo stack ad essere vulnerabile, ma anche la memoria
heap: l’indirizzo di un puntatore a funzione potrebbe essere egual-
mente modificato, oppure attraverso attacchi di tipo “return-to-libc”
(https://en.wikipedia.org/wiki/Return-to-libc_attack) è possibile far ese-
guire codice di sistema senza che l’attaccante abbia necessità di introdurre
codice malevolo.

5.1 tattiche di prevenzione

Quando un’operazione di scrittura richiede una quantità maggiore di


memoria, è possibile adottare due strategie:

1. mantenere inalterata la dimensione del buffer, respingendo l’opera-


zione di scrittura oppure espletandone solo una parte;

2. allocare dinamicamente una quantità di memoria adatta a contenere


i dati richiesti.

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

un indirizzo email dovrà essere capiente come il più grande indirizzo


email possibile, ma presumibilmente molti indirizzo salvati saranno ben
più corti, rendendo alcune porzioni di memoria non riutilizzabili).

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;
}

// Soluzione: allocazione dinamica


/* Alloca spazio (BUFSIZE bytes) usando il puntatore str, esegue la
copia della stringa in argv[0] e se la quantita’
precedentemente allocata non risulta sufficiente, allora ne
rialloca un altro segmento */
int main(int argc, char **argv) {
char *str;
int len;
if ((str = (char *)malloc(BUFSIZE)) == NULL) {
return FAILURE_MEMORY;
}
len = snprintf(str, BUFSIZE, "%s(%d)", argv[0], argc);
if (len >= BUFSIZE) {
free(str);
if ((str = (char *)malloc(len + 1)) == NULL)
return FAILURE_MEMORY;
snprintf(str, len + 1, "%s(%d)", argv[0], argc);
36 buffer overflow

}
printf("%s\n", str);
free(str);
str = NULL;
return SUCCESS;
}

Nel primo codice la funzione snprintf() copia il contenuto di argv[0]


e argc (opportunamente formattati) nel buffer di nome str fino ad un
massimo di BUFSIZE -1 caratteri, in quanto la funzione inserisce auto-
maticamente il carattere terminatore di stringa. La funzione restituisce
il numero di caratteri che sarebbero stati complessivamente scritti, in-
dipendentemente dal valore di BUFSIZE: ciò consente di capire se è
avvenuto un troncamento dei dati qualora la variabile len fosse maggiore
di BUFSIZE, così da poter avvertire l’utente.
Il secondo codice presenta più verbosità in quanto richiede di allocare
dinamicamente una quantità di spazio prefissata, dopodiché si tenta di
copiare una stringa che potrebbe risultare troppo grande: in tal caso si
libera il puntatore str, si tenta di riallocare abbastanza spazio e si procede
con la scrittura. E’ importante seguire tutti questi passi per evitare insidie
pericolose, ad esempio il seguente codice potrebbe portare ad un memory
leak qualora la malloc() non riuscisse ad allocare spazio la seconda volta:
notare infatti che anche se il puntatore str venisse posto a NULL, la
memoria non verrebbe effettivamente liberata.
if (len >= BUFSIZE) {
//free(str); non libero il puntatore a str
if ((str = (char *)malloc(len + 1)) == NULL)
return FAILURE_MEMORY;
snprintf(str, len + 1, "%s(%d)", argv[0], argc);
}

Ciò dimostra la scrupolosità necessaria per utilizzare in maniera sicura


l’allocazione dinamica, ma che indubbiamente presenta una maggior
flessibilità nei confronti dell’utilizzo del sistema.

5.2 stringhe e buffer overflow

La rappresentazione di una stringa nel linguaggio C consiste in un array


di caratteri che terminano con un carattere terminatore: questa strategia
è suscettibile di diversi errori di programmazione, situazione che ha
5.2 stringhe e buffer overflow 37

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);

E’ raccomandabile usare la classe std::string che permette di operare


in sicurezza.

scanf(const char *format [, <arguments>])


Sebbene leggermente più complicata, la funzione scanf() è ugualmente
vulnerabile: quando le specifiche di formato introducono un ‘%s’ è per-
messo all’utente introdurre un numero arbitrario di caratteri che possono
portare ad un overflow del buffer.

strcpy(char *dst, const char *src)


La funzione agisce su stringhe già immagazzinate in memoria, copian-
do il contenuto di src nel buffer puntato da dst. L’operazione risulta sicura
fintanto che strlen(src) < strlen(dst), ma se la condizione non è verificata
allora si ottiene un buffer overflow. Si consideri anche il caso in cui le
lunghezze delle due stringhe sia esattamente la stessa: si ottiene un “off-
by-one overflow”, ovvero la scrittura del carattere terminatore avviene
nel byte successivo al buffer di ricezione e quindi fuori dal range previsto.

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

versioni aggiornate come le funzioni strlcpy() e strlcat() prevedono inoltre


la confermata immissione del carattere terminatore.
Da un punto di vista della static analysis, è importante notare la
maggiore facilità di verifica che le funzioni “bounded” forniscono rispetto
alle altre: esse si basano infatti su un vincolo esplicito passato come
argomento, mentre le unbounded possono affidarsi solo al limite imposto
dal contenuto effettivo; ciò significa che un tool di analisi statica può
verificare un potenziale overflow direttamente nel contesto della funzione
richiamata, senza dover occuparsi di determinare l’origine da cui vengono
prodotti quei dati (che potrebbero essere l’input dell’utente, connessione
di rete, lettura da un file, ecc).

5.3 taint propagation

La taint propagation è la tecnica utilizzata per “tracciare” l’utilizzo di


determinati dati lungo il flusso di esecuzione di un programma, i quali
potrebbero provocare delle vulnerabilità se usati impropriamente. Risul-
ta una tecnica molto efficace per prevenire l’insorgere di errori, perché
consente di rilevare subito il momento in cui variabili contenenti infor-
mazioni sensibili varcano una soglia pericolosa: il tool può rimuovere il
flag di taint solo se la variabile in questione passa attraverso operazioni
di controllo e validazione (specificate dal programmatore stesso).
Nel contesto delle stringhe e dei buffer overflow, è interessante analiz-
zare l’utilizzo della taint propagation per evitare che una stringa venga
correttamente terminata con il carattere terminatore a seguito di una
operazione di manipolazione.
L’esempio proposto prevede una chiamata alla funzione readRawIn-
put(), che restituisce una stringa ma non prevede di aggiungere il carattere
terminatore alla fine. La funzione concatStrs() funziona come strncat().
/* codice esempio: vulnerabile a causa di mancata terminazione
della stringa */
char buffer[MAX];
int last = readRawInput(buffer, MAX-1);
if(false)
buffer[last] = ’\0’;
for(int i = 1; i < argv; i++){
int remaining_space = sizeof(buffer) - strlen(buffer);
concatStrs(buffer, argc[i],remaining_space);
}
5.4 rappresentazione dei caratteri, codifica e decodifica 39

Notare che la condizione if(false) non permette di aggiungere il ca-


rattere terminatore a buffer, di conseguenza l’operazione concatStrs()
probabilmente non individuerà correttamente il punto in cui concatenare
i dati forniti e realizzerà un buffer overflow.
Per prevenire tale errore, è possibile specificare regole di verifica basate
sulla taint propagation del seguente tipo (pseudo-linguaggio):
Source rule:
Function: readRawInput()
Postcondition: first argument is tainted and carries the
NOT_NULL_TERMINATED taint flag

Ciò specifica che il primo argomento passato a readRawInput() (ovvero


buffer) dovrà essere considerato vulnerabile prima di coinvolgerlo in
successive manipolazioni.
Sink rule:
Function: concatStrs()
Precondition: the first and second arguments must not carry the
NOT_NULL_TERMINATED taint flag

Quest’ultima regola consente di vincolare il passaggio di parametri a


concatStrs() alle sole variabili non marchiate: il tool di analisi statica può
rilevare una vulnerabilità qualora questa regola venga violata.

5.4 rappresentazione dei caratteri, codifica e decodifica

Un “character set” rappresenta un insieme di caratteri stampabili a scher-


mo, i quali vengono rappresentati in base alla codifica utilizzata (character
encoding forms). Ogni carattere viene mappato con un codice numerico
(code point) che lo identifica univocamente, ma ogni codifica propone
strategie di corrispondenza diverse, in particolare esistono:

1. codifiche a lunghezza fissa: utilizzano code point con un numero


fisso di bit, facili da manipolare a livello programmativo ma poco
efficienti per grandi insiemi di caratteri (es. ISO-8859-1 e UTF-32);

2. codifiche a lunghezza variabile: consentono di associare code point


più brevi per caratteri che ricorrono più frequentemente nel linguag-
gio comune, mentre caratteri più rari corrisponderanno a codifiche
più lunghe (es. UTF-8 e UTF-16).
40 buffer overflow

Quest’ultime possono provocare errori logici, in quanto una serie ar-


bitraria di bit può rappresentare un code point valido oppure solo par-
ziale e ciò ha provocato diverse vulnerabilità nello sviluppo di software
importanti.
Il linguaggio Java prevede di rappresentare le stringhe in memoria con
caratteri UTF-16, i tipi di dati char e la classe Character utilizzano sempre
UTF-16: così anche le funzioni che manipolano i caratteri attraverso il
tipo int possono efficacemente interpretare i code points di tale codifica.
I linguaggi C e C++ prevedono una maggiore indipendenza nel for-
mato di rappresentazione dei caratteri, lasciando l’implementazione al
programmatore in base alle esigenze. Il tipo di dato char può contenere
una codifica di un byte e per ovviare a questa limitazione (per esempio
UTF-16 richiede almeno 16 bit per carattere) fu introdotto il tipo wchar_t
che nella pratica prevede un’allocazione di 16 bit su piattaforme Windo-
ws e 32 bit su quelle UNIX-like: queste informazioni mostrano quanto
possa essere facile mal interpretare il significato di una stringa, che non
si limita solo alla cattiva rappresentazione a schermo di un carattere, ma
può coinvolgere exploits di buffer overflow: una funzione che richiede
un’unità di misura nella lettura di un buffer si comporterebbe in maniera
diversa qualora si passasse sizeof(wchar_t) piuttosto che sizeof(char).

5.5 integer overflow

Un “integer overflow” si manifesta quando non è possibile rappresentare


il valore di un numero con i bit a disposizione. Tutti i tipi primitivi (char,
short, int ecc) sono individuati da un numero fisso di bit: quando il valore
da rappresentare eccede il range di numeri possibili (per esempio a causa
operazioni aritmetiche) avviene un “ribaltamento”, come mostrato in
figura 6.2:

Figura 6.2: range di int a 4 bit (con segno e senza segno)


5.5 integer overflow 41

esempio: unsigned int a 4 bit


decimale: 15+1 = 16
binario: 1111+0001=10000 —> troncamento —> 0000 ERRORE!

Il troncamento di un intero avviene in seguito ad un conversione ad


un tipo di dato con un insufficiente numero di bit, mentre il caso opposto
è rappresentato dall’estensione del segno:

esempio: signed int a 4 bit


decimale: -4, in complemento a 2 diventa: 1100
se esteso a 8 bit diventa: 11111100

L’errore comune è quello utilizzare erroneamente un numero con segno


successivamente ad un’operazione di estensione (per esempio passando
tale variabile come argomento di una funzione): in tal caso il valore
rappresentato potrebbe essere valutato come unsigned. Il codice seguente
identifica un possibile exploit di buffer overflow che sfrutta un errore di
estensione del segno:
short length = -1;
if(len <= 512)
memcpy(buf, strm, len);

Dato che short non è esplicitamente dichiarato unsigned, può con-


tenere valori negativi. La condizione nell’if serve a controllare di non
copiare un’eccessiva quantità di memoria, ma non controlla che il valore
sia anche maggiore di zero: quest’ultimo viene implicitamente convertito
ad unsigned int durante la chiamata di funzione ed il valore risultante è
evidentemente molto più grande.

Metodi per rilevare e prevenire integer overflow


1. usare tipi unsigned: usare sempre tipi unsigned quando si deve
trattare dati che logicamente non possono assumere valori negativi
(per esempio indirizzi di memoria), ciò implica doversi preoccupare
di controllare solo il limite superiore nel range di valori possibili;

2. documentarsi sulle specifiche del linguaggio/sistema di esecuzione:


sulla base del linguaggio utilizzato e/o della piattaforma di rilascio
del software, i tipi primitivi possono assumere diverse dimensioni
(basti pensare ad un software compilato sia per sistemi a 32 bit che
a 64 bit);
42 buffer overflow

3. enfatizzare i controlli inerenti ad accessi alla memoria: operazioni


sensibili come gli accessi alla memoria devono sempre essere verifi-
cati attraverso test di sicurezza, le variabili che vengono controllate
non devono essere modificate successivamente ai controlli;

4. warnings dei compilatori: un compilatore può fornire indicazioni


utili sull’introduzione involontaria di integer overflow ed in genera-
le su errori di programmazione; quando è necessario introdurre tan-
ti cast espliciti per evitare warning di compilazione, probabilmente
si è sbagliato qualcosa nella soluzione di progettazione.

Quando i controlli da effettuare diventano pressanti e numerosi, può


essere una buona scelta affidarsi a librerie specifiche che prevengono gli
integer overflow: ad esempio Microsoft ha rilasciato “SafeInt”(C++) e
“IntSafe”(C), due librerie particolarmente utili in quanto consentono di
utilizzare classi e funzioni “overflow-safe”.
esempio linguaggio C++ con SafeInt
void* AllocateMemForStructs(int StructSize, int HowMany){
SafeInt<unsigned long> s(StructSize);
s *= HowMany;
return malloc(s.Value());
}

Notare che l’override dell’operatore di moltiplicazione specifico del-


la classe SafeInt consente di eseguire in maniera sicura l’operazione
aritmetica, senza definire controlli condizionali specifici.
Linguaggi "safe" come Java, C# ecc eliminano teoricamente il rischio di
buffer overflow attraverso il controllo runtime delle operazioni eseguite,
garantendo due proprietà:

1. “memory safety”: il programma non legge né scrive in regioni di


memoria non legittime;

2. “type safety”: identifica ogni variabile attraverso un tipo di dato


che permette di tracciare costantemente le dimensioni massime del
valore memorizzato, oltre alle operazioni consentite; senza type
safety, ogni variabile potrebbe essere arbitrariamente usata per
contenere valori eterogenei durante l’esecuzione e provocare stati
di esecuzione inconsistenti.

I linguaggi “safe” accettano dei compromessi per garantire tali caratte-


ristiche, come limitare le capacità e/o possibilità di progettazione di una
5.5 integer overflow 43

soluzione software, al contrario di linguaggi “unsafe” (come C/C++) che


consentono una più libera manipolazione dell’architettura sottostante: ciò
si traduce spesso in una lotta di supremazia tra performance e sicurezza,
che devono essere considerate nella scelta degli strumenti da utilizzare.
6
ERRORI ED ECCEZIONI

Il linguaggio utilizzato determina l’approccio con cui il programma evi-


denzia e gestisce condizioni inaspettate. E’ uso comune che con il C si
utilizzino valori numerici di ritorno delle funzioni, mentre Java dispone
di costrutti sintattici precisi chiamati “Eccezioni” e C++ sfrutta entrambi
gli approcci. Indipendentemente dalla tecnica, una cattiva gestione del-
l’errore porta spesso a perdite di risorse del sistema ed a transizioni verso
stati inconsistenti, ma è bene precisare che:
1. i codici di ritorno (C-style) sono un approccio poco intelligente che
favorisce la nascita di diverse complicazioni;

2. le eccezioni (Java-style) nascono appositamente per questo scopo e


risultano più efficaci e user-friendly.
Nei prossimi paragrafi si studiano entrambi gli approcci.

6.1 gestire errori attraverso i codici di ritorno

Seppur questa rappresenti l’idea più facilmente attuabile, può introdurre


seri problemi:
1. è facile ignorare (volontariamente o meno) situazioni di errore,
semplicemente non acquisendo i valori di ritorno delle funzioni;

2. collegare il codice numerico allo stato di errore non è immediato


e richiede una precisa conoscenza del programma, inoltre rende
meno leggibile il codice in quanto è necessario quantomeno creare
uno statement per ogni possibile codice di errore (per esempio
switch-case molto grandi);

3. non esiste una convenzione comune sulla comunicazione degli


errori attraverso valori numerici, ogni programmatore adotta la
propria soluzione in base al contesto.

45
46 errori ed eccezioni

Si riportano alcuni esempi.


//Esempio 1: BAD CODE
char buf[10], cp_buf[10];
fgets(buf, 10, stdin);
strcpy(cp_buf, buf);

La funzione fgets() copia il testo digitato dall’utente in buf, per un


totale di massimo 9 caratteri ai quali viene aggiunto automaticamente il
carattere terminatore. Se un errore di I/O dovesse verificarsi, allora fgets()
non aggiungerebbe alla stringa il carattere terminatore e restituirebbe
NULL; ciò viene ignorato nel codice sopra riportato, ma è bene notare che
l’uso di strcpy() su un buffer non correttamente terminato può introdurre
un buffer overflow, rilevabile solo controllando il valore di ritorno di
fgets(), come nel seguente esempio.
//Esempio 2: GOOD CODE but BAD RETURN
char buf[10], cp_buf[10];
char* ret = fgets(buf, 10, stdin);
if (ret != buf) {
report_error(errno);
return;
}
strcpy(cp_buf, buf);

Un difetto dell’esempio 2 riguarda l’utilizzo del return nel mezzo del


codice, ciò è pericoloso in quanto può introdurre la mancata liberazione
di risorse in contesti differenti di implementazione: ogni return dovrebbe
risultare come ultima istruzione di qualsiasi funzione, in modo da non
deviare mai il flusso di esecuzione; il codice di controllo e di eventuale
liberazione di risorse dovrebbe sempre essere presente in funzioni e/o
moduli ben definiti per tale scopo.

6.2 gestire errori attraverso le eccezioni

Un’eccezione è un costrutto Java che permette di segnalare la presenza


di uno stato di errore ed eventualmente deviare il flusso di esecuzione.
Alcune API Java richiedono anch’esse una valutazione del valore di
ritorno delle funzioni, come ad esempio l’istruzione read(): essa restituisce
il numero di caratteri effettivamente letti dallo stream ricevuto, quantità
non necessariamente pari a quella massima fissata dal programmatore;
pertanto un efficiente controllo prevede di analizzare il valore di ritorno
6.2 gestire errori attraverso le eccezioni 47

e lanciare le corrette eccezioni se necessario: l’esempio seguente prevede


la lettura di esattamente 1KB di dati da alcuni file; se ne viene rilevata
una quantità minore allora è necessario lanciare un’eccezione.
// Esempio Java: GOOD CODE
FileInputStream fis;
byte[] byteArray = new byte[1024];
for (Iterator i=users.iterator(); i.hasNext();) {
String userName = (String) i.next();
String pFileName = PFILE_ROOT + "/" + userName;
fis = new FileInputStream(pFileName);
try {
int bRead = 0;
while (bRead < 1024) {
int rd = fis.read(byteArray, bRead, 1024 - bRead);
if (rd == -1)
throw new IOException("file is unusually small");
bRead += rd;
}
} finally {
fis.close();
}
// could add check to see if file is too large here
processPFile(userName, byteArray) ;
}

Le eccezioni si dividono in 2 categorie: checked e unchecked. Se un


metodo dichiara il possibile insorgere di un’eccezione checked, allora
ogni altro metodo che lo richiama dovra’ scegliere se inoltrarla a sua volta
o controllarla: ogni violazione viene identificata direttamente a livello
di compilazione; le eccezioni unchecked rappresentano una categoria
ben distinta: non sono obbligatoriamente controllate da uno statement
definito dal programmatore e, quando insorgono a runtime, possono far
crashare il programma.

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

possono essere introdotti durante le fasi di debug e poi essere dimenticate.


Una regola strutturale per identificare una NullPointerException è:
CatchBlock:
exception.type.name == "java.lang.NullPointerException"
and not enclosingClass.supers contains [name ==
"junit.framework.Test"]

Le nozioni introdotte negli ultimi capitoli hanno permesso di approfon-


dire tematiche di sicurezza legate ai sistemi informatici: in ogni capitolo
si è cercato di mettere in relazione la possibile fonte di vulnerabilità con
il tema della static analysis, definendo regole di best practices e secure
coding che saranno esaminate nella successiva ed ultima parte di questa
tesi.
7
LA SUITE JULIET

Juliet versione 1.2 è una suite di programmi (realizzati dall’NSA/CAS


- “National Security Agency / Center of Assured Software”) che rac-
coglie classi di vulnerabilità specifiche, il cui scopo consiste nell’essere
sottoposta a tools di analisi statica.
Ogni programma rappresenta un “test case”, ovvero rappresenta un
singolo pezzo di codice compilabile che può essere fornito al tool di
analisi statica; un test case è implementato per individuare esattamente
un tipo di vulnerabilità, ciò nonostante è possibile che altri errori non
correlati direttamente possano essere casualmente presenti.
Per esempio, il test case (linguaggio C) di nome
“CWE476_NULL_Pointer_Dereference__char_01”
consiste esclusivamente di una singola falla di dereference di un puntato-
re nullo.
Oltre al costrutto vulnerabile, ogni test case contiene anche una o più
replicazioni della stessa funzione ma senza la vulnerabilità, così da forni-
re possibili soluzioni all’errore; solo alcuni test cases non comprendono
funzioni corrette e sono considerati “bad-only”.

In generale, i test sfruttano funzioni disponibili nativamente sul siste-


ma operativo sottostante, piuttosto che utilizzare librerie di terze parti.
Inoltre, sebbene C e C++ siano linguaggi ben distinti, i programmi ad
essi appartenenti sono raggruppati e trattati assieme, in quanto in molti
contesti C rappresenta un sottoinsieme di C++. Dove possibile, i test
C/C++ limitano l’uso di API alle sole librerie standard C, disponibili
per tutte le principali piattaforme; per garantire la copertura ad errori
comuni ed importanti, alcuni programmi sono specifici per i sistemi
operativi Windows (cioè sfruttano specifiche API Windows). I test cases
del linguaggio C sono conformi allo standard C89.
L’NSA ha utilizzato diverse fonti per selezionare i tipi di falle per i test:
1. il know-how e l’esperienza del team di Software Assurance;

49
50 la suite juliet

2. classi di vulnerabilità studiate ed analizzate nella realizzazione di


precedenti tools di analisi statica;

3. informazioni pervenute da terze parti che hanno usufruito dei tools;

4. MITRE’s CWE (Common Weakness Enumeration).

In particolare l’ultima opzione influenza il nome di ogni test case: nono-


stante ogni test case individui la CWE riportato nel nome, non è detto
che sia sempre e soltanto l’unica vulnerabilità.

7.1 struttura dei nomi dei test cases

Come descritto precedentemente, i test cases sono programmi compilabili


che consistono di una specifica tipologia di falla: il codice in ogni file con-
tiene una funzione ”cattiva” (ovvero con una vulnerabilità) ed una o più
funzioni “buone” (logicamente equivalenti alla cattiva ma implementate
per eliminare gli errori).
I test cases usano il MITRE’s CWE come base per la scelta del "nome
identificativo” e per la classificazione. Ogni codice è associato al numero
univoco CWE che meglio generalizza la falla presente; formalmente ogni
test case è univocamente descritto da 4 elementi:

1. codice numerico e breve nome della vulnerabilità CWE, separati da


un underscore;

2. un nome più autodocumentante della vulnerabilità;

3. un numero di 2 cifre che identifica la “flow variant”, ovvero l’imple-


mentazione attuata al programma in termini di dati e/o controlli di
flusso usati nel test case (approfonditi nel prossimo paragrafo);

4. estensione del file.

Quando un test case prevede l’utilizzo di API Windows, il prefisso “w32_”


è aggiunto al nome del file.

7.2 flow variants

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

utilizzati fanno parte di una classificazione standardizzata all’interno di


Juliet e sono identificati da un codice numerico di 2 cifre.
Il numero "01" indica la più semplice forma di flusso di esecuzione,
senza alcun tipo di codice supplementare aggiuntivo; inoltre non tutti
i test cases possono implementare ogni singola flow variant, perciò il
numero complessivo di test cases risulta inferiore.

7.3 progettazione dei test cases

Tutti i programmi della suite devono definire una funzione cattiva ed


almeno una funzione buona.
Ogni test case definisce esattamente una funzione cattiva nel file princi-
pale: nei casi più semplici essa contiene direttamente il codice vulnerabile,
in altri contesti è preferibile richiamare funzioni “sink” o “helper” che
forniscono l’errore desiderato.
La funzione cattiva deve:

1. essere chiamata con il suffisso “_bad”;

2. non deve accettare argomenti in input e non deve restituire alcun


valore.

La funzione buona primaria ha il compito di richiamare le funzione


buone secondarie, inoltre deve:

1. essere chiamata con il suffisso “_good”;

2. non deve accettare argomenti in input e non deve restituire alcun


valore.

Le funzioni buone secondarie contengono le implementazioni corrette


della vulnerabilità, eventualmente appoggiandosi alle funzioni sink e
helper. Ci sono 3 convenzioni sui nomi associati:

1. goodG2B, goodG2B1, goodG2B2, goodG2B3, ecc. : questi nomi sono


usati nei test cases di data flow quando una sorgente dati affidabile
è passata ad una funzione vulnerabile;

2. goodB2G, goodB2G1, goodB2G2, goodB2G3, etc. : questi nomi


sono usati nei test cases di data flow quando una sorgente dati
inaffidabile è passata ad una funzione corretta;
52 la suite juliet

3. good1, good2, good3, etc. – nomi generici per i casi esclusi rispetto
a sopra.

In C/C++ le funzioni secondarie possiedono il modificatore “static”, in


modo da rendere lo scope esclusivo nel file in cui sono scritte ed evitare
collisioni di nomi.

7.4 considerazioni sull’uso di analisi statica sulla suite


juliet

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:

1. le falle rilevate sono effettivamente presenti, involontariamente


introdotte nella generazione del test case;

2. il tool riporta un “falso positivo”, quindi non comprende corretta-


mente il codice.
8
MISRA-C 2004

MISRA C è un insieme di linee guida di sviluppo software per i linguag-


gi di programmazione C/C++, sviluppato da MISRA (Motor Industry
Software Reliability Association).
Lo scopo prefissato riguarda la garanzia di safety, portabilità e affi-
dabilità del codice nel contesto dei sistemi embedded, specificatamente
quei sistemi programmati in ISO C. MISRA è un modello di buone prati-
che di programmazione, ampiamente accettato e riconosciuto dai settori
aerospaziali, telecomunicazioni, sanità, difesa, ferroviari ecc. MISRA C
non è open-source, i documenti delle linee guida più recenti devono
essere acquistati dagli utenti, mentre le vecchie versioni sono liberamente
fruibili.
E’ importante notare che le regole MISRA-C, al contrario delle secu-
re coding OWASP, non si occupano specificatamente di Security, ma si
concentrano sulla corretta implementazione del codice in linguaggio C,
così da prevenire errori logici e/o comportamenti non definiti causati da
specifiche funzioni. La filosofia dietro il C prevede che il programmatore
conosca approfonditamente certe caratteristiche del linguaggio, come ad
esempio il debole type checking effettuato quando si esegue un cast tra
interi: tale assunzione può potenzialmente portare ad errori di program-
mazione che MISRA tenta di impedire. Le regole MISRA-C 2004 sono
distinte in:

1. required: 122 regole obbligatorie che il programmatore deve seguire


scrupolosamente a meno di evidenti ostacoli di conformità, in tali
casi è richiesta la stesura di una “deviazione formale” che attesti i
motivi per cui la regola non è stata applicata e che dimostri che il
sistema software risulti affidabile;

2. advisory: 20 regole opzionali che influenzano lo stile di programma-


zione per aumentare la leggibilità e la manutenibilità; non devono

53
54 misra-c 2004

essere ignorate, ma non è richiesta la conformità totale (né la stesura


di una deviazione formale in caso di divergenza).

La trattazione riportata nell’Appendice A esplora tutte le regole MISRA-C


2004 che possono avere un’influenza sulla prevenzione e/o rilevazione di
vulnerabilità software, in accordo con il lavoro di tesi realizzato: tutte le
regole evidentemente non concernenti tale problema sono state ignorate.
In sintesi, si può affermare che le regole MISRA-C 2004 non garantiscono
alcuna prevenzione e/o rilevazione di vulnerabilità; le poche categorie
di regole che hanno evidenziato una relazione (di cui sono riportati solo
un paio di esempi in questo capitolo, mentre il resto del lavoro è esposto
nell’Appendice B) sono:

1. statements di controllo;

2. controllo di flusso;

3. uso di funzioni nelle librerie standard.

ESEMPIO 1:

CWE242: Use of Inherently Dangerous Function


Codice “bad function”
#define DEST_SIZE 10
#include <stdio.h>
void CWE242_Use_of_Inherently_Dangerous_Function__basic_01_bad(){
char dest[DEST_SIZE];
char *result;
/* FLAW: gets is inherently dangerous and cannot be used
safely. */
/* INCIDENTAL CWE120 Buffer Overflow since gets is inherently
dangerous and is
* an unbounded copy. */
result = gets(dest);
/* Verify return value */
if (result == NULL)
{
/* error condition */
printLine("Error Condition: alter control flow to indicate
action taken");
exit(1);
}
dest[DEST_SIZE-1] = ’\0’;
misra-c 2004 55

printLine(dest);
}

2) Analisi della vulnerabilità


L’utilizzo di funzioni come gets() è rischioso a causa dei comportamenti
indefiniti o non controllati esplicitamente (in particolare gets() risulta
ormai una funzione deprecata). Tale funzione non prevede un limite
massimo di caratteri da leggere ed accettare da stdin, pertanto tutti i
dati letti vengono inseriti nel buffer, andando a generare un overflow di
sovrascrittura delle celle di memoria successive.

3)Report del tool

4) Codice corretto e conclusioni


La regola 20.9 identifica come rischiose molte funzioni della libreria
<stdio.h> (quali fopen, ftell, gets ecc), in quanto risultano funzioni dai
comportamenti indefiniti e/o non controllati. Nello sviluppo di siste-
mi embedded si ipotizza, inoltre, di non dover utilizzare funzioni di
I/O: se ciò è necessario, allora ogni funzione utilizzata deve essere op-
portunamente controllata, al fine di implementare specifici controlli di
prevenzione di vulnerabilità.

ESEMPIO 2:

CWE390: Error control without action


Codice “bad function”
#include <stdio.h>
void CWE390_Error_Without_Action__fgets_char_01_bad()
56 misra-c 2004

{
/* 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);
}

2) Analisi della vulnerabilità


Nel blocco di controllo dell’errore generato dall’istruzione fgets() non
viene eseguita alcuna azione: quando un blocco if risulta vuoto, è proba-
bile che ci sia una cattiva pratica di programmazione alla base.

3)Report del tool

4) Codice corretto e conclusioni


Il blocco di codice if deve prevedere la stampa di un messaggio di
errore e l’uscita dal programma con un codice di errore, ad esempio:
if (fgets(data, 100, stdin) == NULL){
printLine("fgets failed!");
misra-c 2004 57

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).

Negli ultimi anni il tema della security è diventato molto importante


(complice anche l’esplosione di IOT “Internet Of Things”), perciò in aprile
2016 MISRA ha rilasciato l’Amendment 1 per le MISRA-C 2012, ovvero
una raccolta di regole supplementari inerenti il tema della security; la
MISRA-C Committee ha affermato che:
"Anyone using the C language for system development, particularly

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

[1] Algirdas Avizienis, Jean-Claude Laprie, Brian Randell, and Carl


Landwehr - Basic Concepts and Taxonomy of Dependable and Secu-
re Computing - IEEE TRANSACTIONS ON DEPENDABLE AND
SECURE COMPUTING, VOL. 1, NO. 1, gennaio-marzo 2004

[2] Politecnico di Milano: dependable systems


Basic Concepts and Terminology - URL:
http://home.deib.polimi.it/bolchini/docs/ds/2018.01.basics.pdf

[3] Gary McGraw, Brian Chess - Static Analysis for Security - published
by IEEE COMPUTER SOCIETY - anno: 2007

[4] Katerina Goseva-Popstojanova, Andrei Perhinschi - On the capability


of static code analysis to detect security vulnerabilities - pgg. 18 - 33 -
anno: 2015

[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

[8] Brian Chess, Gary McGraw, published by IEEE COMPUTER SOCIE-


TY: Seven Pernicious Kingdoms: A Taxonomy of Software Security Errors,
pubblicato da Addison-Wesley

[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

[11] Andrew Banks, Chairman of the MISRA C


Committee, Nuneaton, April 22nd 2016, URL:
https://www.misra.org.uk/LinkClick.aspx?fileticket=JMG_JlEytqY%3D&tabid=59

[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

1.1 Regole MISRA-C 2004: Environment

- 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).

1.2 Regole MISRA-C 2004: Estensioni del linguaggio

- 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.

1.3 Regole MISRA-C 2004: Documentation

- Regola 3.1 (required) “Ogni affidamento a comportamenti non definiti da parte di


funzioni deve essere documentato”
Qualora una funzione non proponga un comportamento formalmente standardizzato, è
necessario documentare tale aspetto attingendo dalla documentazione del compilatore
utilizzato.

- Regola 3.2 (required) “I character sets e le rispettive codifiche devono essere


documentate”
Essenzialmente esistono due character sets: quello usato per scrivere il codice sorgente e
quello adoperato nelle operazioni di I/O durante l’esecuzione del software. Essi possono non
coincidere totalmente, ma tale scelta deve essere sempre tenuta in considerazione e
documentata.

- Regola 3.3 (advisory) “L’implementazione della divisione intera attuata dal


compilatore scelto deve essere conosciuta e documentata”
Un compilatore conforme allo standard ISO può plausibilmente compiere le seguenti due
divisioni tra interi:
• -5/3 = -1 e resto -2;
• -5/3 = -2 e resto +1;
Sebbene la seconda sia meno intuitiva, può essere effettivamente utilizzata.

- Regola 3.5 (required) “L’uso di campi bit deve essere documentata”


Il “campo bit” è una delle caratteristiche meno definite del linguaggio C, il suo possibile
utilizzo può essere:
- accedere a bit individuali in strutture più grandi (ad esempio union), ma è vietato dalla
regola 18.4;
- permettere la realizzazione di flags o altri dati (quest’ultimi rappresentati da una struttura
omogenea) per risparmiare spazio in memoria.
La seconda opzione è l’unica conforme alle regole MISRA. “Struttura omogenea” indica di
dover realizzare structs che contengano esclusivamente campi bit.
L’esempio seguente è conforme:
struct message /* Struct is for bit-fields only */
{
signed int little: 4; /* Note: use of basic types is required */
unsigned int x_set: 1;
unsigned int y_set: 1;
} message_chunk;
E’ necessario fare attenzione ai comportamenti non definiti dei campi bit, quali:
- l’allineamento dei bit nell’unità di memorizzazione (solitamente il byte) è dipendente
dall’implementazione sottostante, essa può essere sia nella parte superiore che inferiore
della cella;
- la sovrapposizione di un campo multi-bit su una o più celle (se una struttura a 6 bit ed una
a 4 bit sono definite sequenzialmente, quest’ultimo può essere gestito spostandolo alla
successiva cella, oppure può essere diviso in due metà poste su due byte consecutivi).

1.4 Regole MISRA-C 2004: Character sets

- 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.

1.5 Regole MISRA-C 2004: Identificatori

- 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.

Matteo Mauro Matricola: 5971842 !2


1.6 Regole MISRA-C 2004: Tipi

- 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.

Matteo Mauro Matricola: 5971842 !3


1.7 Regole MISRA-C 2004: Costanti

- 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”).

1.8 Regole MISRA-C 2004: Dichiarazione e definizioni

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.1 (required) “Le funzioni devono possedere il prototipo di dichiarazione,


quest’ultimo deve essere visibile sia alla definizione della funzione che alle chiamate di
funzione”
L’uso di un prototipo permette al compilatore di espletare determinati controlli sulla
definizione e sulle chiamate effettuate, ad esempio verificare la coerenza con il numero e tipo
di argomenti passati. Nel caso di funzione esterne, è consigliato fornire l’interfaccia della
funzione attraverso un header file da includere in ogni modulo desiderato; è considerata una
buona pratica di programmazione fornire le interfacce anche delle funzioni con internal
linkage, eventualmente raggruppandole tutte in punto specifico del codice sorgente (es. dopo
le “include”).

- 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”

Matteo Mauro Matricola: 5971842 !4


- Regola 8.5 (required) “All’interno di un file header non devono essere presenti
definizioni di funzioni o variabili”
I file di header sono concepiti per contenere esclusivamente le dichiarazioni di variabili,
funzioni, macro e typedefs: ciò consente di restringere la presenza di codice ai soli file
sergente “.c”. Un file di intestazione nasce con lo scopo di separare e strutturare il codice
sorgente su più moduli, rendendo disponibili le dichiarazioni di interfacce e le variabili
globali, senza però aggiungere blocchi di implementazione.

- 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.

1.9 Regole MISRA-C 2004: Inizializzazione

- Regola 9.1 (required) “Tutte le variabili locali (ovvero dichiarate internamente ad un


blocco rappresentato da due parentesi graffe, variabili non con file scope) devono
essere esplicitamente inizializzate”
All’atto dell’allocazione di spazio sullo stack, le variabili non inizializzate contengono un
valore non definito: l’intento di questa regola non è di imporre l’inizializzazione della
variabile all’atto della dichiarazione, ma di assicurarsi che venga eseguito un assegnamento
prima di un qualsiasi accesso.

Matteo Mauro Matricola: 5971842 !5


- Regola 9.2 (required) “L’inizializzazione tramite parentesi graffe deve essere
utilizzata come assegnazione iniziale (diversa da zero) per gli array e structs”
ISO C richiede liste di inizializzazione per i tipi array, structs e union tramite le parentesi
graffe. La regola si applica ricorsivamente per inizializzare strutture annidate: ciò costringe il
programmatore a definire l’ordine e la struttura dei dati inseriti. Il seguente esempio mostra
due assegnazioni lecite in ISO C, ma la prima non è conforme a questa regola:
int16_t y[3][2] = { 1, 2, 3, 4, 5, 6 }; /* not compliant */
int16_t y[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } }; /* compliant */
Lo stesso ragionamento si applica a structs e union.
Nel caso di inizializzazione a 0 o NULL, allora è convenzione poter inizializzare
esplicitamente solo il primo elemento: la regola si concentra quindi sul rendere obbligatoria
ed esplicita qualsiasi inizializzazione diversa da 0 (attraverso una forma standardizzata).

1.10 Regole MISRA-C 2004: Conversione aritmetica dei tipi

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).

Matteo Mauro Matricola: 5971842 !6


Nonostante ciò, la promozione ad intero rappresenta un’inconsistenza fondamentale del
linguaggio C laddove gli small integer types si comportino in maniera diversa rispetto ai tipi
long e int. Le regole MISRA-C tentano di prevenire errori imprevisti con tali procedure di
conversione.

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).

Rischi legati alle conversioni


- Perdita del valore: la variabile viene convertita in un tipo che non può rappresentare il
valore originario, causando un totale stravolgimento del risultato;
- Perdita del segno: conversione da un tipo signed ad uno unsigned può determinare la
perdita del segno del valore originario;
- Perdita di precisione: conversione da un tipo floating (reale) ad un tipo intero (oppure ad
un tipo reale con un range di valori rappresentabili minore) causa una perdita di precisione
del valore originario.
Si può procedere ad una conversione senza errori solo nei seguenti 2 casi:
- conversione da un tipo intero ad un tipo intero più grande con segno;
- conversione da un tipo reale ad un tipo reale più grande.

Un’importante concetto da evidenziare riguarda il legame:


- tra il tipo di variabile che conterrà il risultato;
- e il tipo attraverso cui l’operazione viene valutata.
Si consideri il seguente esempio:
uint16_t u16a = 40000; /* unsigned short / unsigned int ? */
uint16_t u16b = 30000; /* unsigned short / unsigned int ? */
uint32_t u32x; /* unsigned int / unsigned long ? */
u32x = u16a + u16b; /* u32x = 70000 o 4464 ? */
Dato che u16a e u16b sono degli small integers, ad essi verrà applicata la promozione ad
intero: se un int è rappresentato su 32 bit, la somma sarà espletata correttamente fornendo il
valore 70000, ma ciò non accadrà qualora ad un int siano associati solo 16 bit (in tal caso
avremo 70000%65536 = 4464). Ciò denota come il tipo della variabile destinataria non
influenzi il tipo attraverso cui l’espressione aritmetica viene valutata.

Matteo Mauro Matricola: 5971842 !7


Per la trattazione delle regole successive, è necessario introdurre alcuni concetti fondamentali
legati alla terminologia MISRA-C 2004:
1) Tipo sottostante (underlying type)
Il “tipo sottostante” di un'espressione rappresenta il tipo di dato che l'operazione aritmetica
restituirebbe se non fosse per gli effetti della promozione ad intero:
• quando due int sono sommati tra loro, l'espressione ritorna generalmente un tipo int;
• quando due unsigned char sono sommati tra loro, a causa della promozione ad intero
la somma restituisce effettivamente un int, ma definiamo il “tipo sottostante” del
valore ritornato come un unsigned char.
Il concetto di tipo sottostante risulta utile nel definire le prossime regole, in quanto mostrano
il comportamento che si otterrebbe senza promozione ad intero: quest'ultima risulta un
caratteristica non evitabile del linguaggio, ma le regole MISRA-C affermano che è possibile
neutralizzarne gli effetti negativi “evitando di fare affidamento sull'estensione di
rappresentazione che gli small integers subiscono a seguito di una promozione ad intero”.
E' possibile sintetizzare la precedente affermazione così: quando un programmatore somma
due int, deve assicurare che il risultato sia effettivamente rappresentabile attraverso un int;
qualora gli operandi siano invece due unsigned char, deve assicurare che il risultato sia
rappresentabile su un unsigned char, anche se la promozione ad intero permetterebbe
un'estensione del range dei valori rappresentabili.

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

1.11 Regole MISRA-C 2004: Cast implicito

- 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.”

Matteo Mauro Matricola: 5971842 !8


- Regola 10.2 (required) “Il valore di un'espressione di tipo reale non deve subire un cast
implicito ad un tipo diverso se:
• non è una conversione ad un tipo reale più grande;
• l'espressione è complessa;
• l'espressione è l'argomento di una funzione;
• l'espressione è il ritorno di una funzione.”

Le precedenti regole coinvolgono i seguenti casi:


• nessuna conversione implicita tra i tipi signed e unsigned;
• nessuna conversione implicita tra i tipi interi e floating point;
• nessuna conversione implicita da un tipo più grande ad uno più piccolo;
• nessuna conversione implicita agli argomenti delle funzioni;
• nessuna conversione implicita al valore di ritorno di una funzione;
• nessuna conversione implicita al valore restituito da un'espressione complessa.
L'ultimo punto in particolare evidenzia la necessità di garantire che ogni operazione
dell'espressione sia condotta sempre con lo stesso tipo, ad esempio:
- u32a + u16b + u16c → risulta un'espressione valida, in quanto le due somme (eseguite da
sinistra a destra) verranno eseguite sul tipo U32 (assumendo che u16b e u16c ottengano
un'estensione in seguito al processo di bilanciamento);
- u16b + u16c + u32a → non è conforme alle regole, in quanto la prima somma verrà
presumibilmente eseguita sul tipo sottostante U16 (rimanendo nell'ottica di non fare
affidamento sull'integral promotion).

1.12 Regole MISRA-C 2004: Cast esplicito

- 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;

Matteo Mauro Matricola: 5971842 !9


result_8 = (~port) >> 4; /* not compliant */
si può vedere come il risultato di ~port sia 0xffa5 su 16 bit
e 0xffffffa5 su 32 bit: in entrambi i
casi il risultato dello shift a destra sarà uguale a 0xfa, mentre il risultato corretto dovrebbe
essere 0x0a. Il codice risulta conforme e corretto nel seguente modo:
result_8 = ((uint8_t)(~port)) >> 4 ; /* compliant, applicando subito il
cast */
result_16 = ((uint16_t)(~(uint16_t)port)) >> 4 ; /* compliant */

- Regola 10.6 (required) “Il suffisso U deve essere applicato alle costanti di tipo
unsigned”

1.13 Regole MISRA-C 2004: Puntatori

I puntatori possono essere classificati come:


- puntatori a oggetti (variabili, array ecc);
- puntatori a funzioni;
- puntatori a void;
- la costante puntatore nullo (ovvero il valore 0 castato a void *).

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”

Matteo Mauro Matricola: 5971842 !10


1.14 Regole MISRA-C 2004: Espressioni

- 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”

Matteo Mauro Matricola: 5971842 !11


1.15 Regole MISRA-C 2004: Statements di controllo

- 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.4 (required) “L’espressione di controllo di un for non deve contenere


oggetti di tipo floating point”.
La condizione di uscita potrebbe non essere mai raggiunta a causa dell’aritmetica finita del
calcolatore: ciò può potenzialmente condurre a situazioni di errori come un ciclo infinito.

- 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 13.7 (required) “Espressioni booleane che restituiscono sempre lo stesso


valore non devono essere usate in espressioni di controllo”
Se un’espressione di controllo possiede un predicato sempre vero o sempre falso, allora lo
statement non ha senso di esistere e ciò può essere indice di un errore logico da parte del
programmatore.

Matteo Mauro Matricola: 5971842 !12


1.16 Regole MISRA-C 2004: Controllo del flusso

- 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.2 (required) “Qualsiasi statement non nullo deve:


- provocare un effetto nel programma;
- oppure modificare il flusso di esecuzione. “
Se un’istruzione non prevede alcuna modifica all’interno del programma, allora è probabile
che si tratti di un refuso da parte del programmatore. Ad esempio, quando si esegue
un’istruzione che restituisce un risultato e quest’ultimo viene direttamente scartato, ciò è una
violazione della regola.

- Regola 14.4 (required) “L’istruzione goto non deve essere usata”


- Regola 14.5 (required) “L’istruzione continue non deve essere usata”
- Regola 14.6 (required) “In ogni costrutto di iterazione è possibile usare al più
un’istruzione break”
La regola 14.6 rappresenta una buona pratica di programmazione per mantenere una buona
leggibilità del codice, ma è consentito usare al più un’occorrenza di break per realizzare
codice più efficiente (ad esempio permettere l’uscita diretta se una condizione non è
verificata, senza perdere tempo ad eseguire un numero indefinito di iterazioni).

- 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.

Matteo Mauro Matricola: 5971842 !13


1.17 Regole MISRA-C 2004: Funzioni

- 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.

- Regola 16.3 (required) “Ogni parametro formale all’interno di un prototipo di


funzione deve possedere un identificatore”
- Regola 16.5 (required) “Una funzione che non accetta parametri deve riportare la
parola void nella lista dei parametri formali”
Una funzione che non restituisce nessun valore deve riportare la parola void come parametro
di ritorno; in modo analogo, una funzione che non accetta nessun input deve esplicitare la
parola void: in questo modo è possibile mantenere la coerenza tra la dichiarazione di una
funzione e la successiva definizione, come mostrato nel seguente esempio,
void a();
int main(void){
a(5); //valido perché la dichiarazione di a() non esplicita void
return 0;}
}
void a(int n){ //stampa n;}

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.

Matteo Mauro Matricola: 5971842 !14


1.18 Regole MISRA-C 2004: Puntatori ed array

- Regola 11.1 (required) “Operazioni aritmetiche con i puntatori devono essere


effettuate esclusivamente su puntatori ad array”
Somme e sottrazioni di puntatori che non puntano ad elementi di un array possono portare a
comportamenti indefiniti.

- 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.4 (required) “L’indirizzamento di un array è l’unica forma di aritmetica


dei puntatori consentita”
L’indirizzamento degli elementi in un array rappresenta una forma più chiara e meno incline
ad errori rispetto alla manipolazione diretta dell’indirizzo di un puntatore. Questa regola vieta
di calcolare direttamente un indirizzo, dal momento che l’accesso esplicito agli identificatori
può portare anche ad indirizzare zone di memoria non valide.
void my_fn(uint8_t * p1, uint8_t p2[]){
uint8_t index = 0U;
uint8_t * p3;
uint8_t * p4;
*p1 = 0U;
p1 ++; /* not compliant - pointer increment */
p1 = p1 + 5; /* not compliant - pointer increment */
p1[5] = 0U; /* not compliant - p1 was not declared as an array */
p3 = &p1[5]; /* not compliant - p1 was not declared as an array */
p2[0] = 0U;
index ++;
index = index + 5U;
p2[index] = 0U; /* compliant */
p4 = &p2[5]; /* compliant */
}

- 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.

1.19 Regole MISRA-C 2004: Librerie standard

- 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;

Matteo Mauro Matricola: 5971842 !15


- le funzioni toupper() e tolower() presentano alcune implementazioni con comportamenti
indefiniti qualora si passino, rispettivamente, caratteri maiuscoli e minuscoli;
- le funzioni della libreria ctype.h presentano comportamenti indefiniti per determinati
valori;
- altri esempi sono stati ampiamente discussi nei capitoli precedenti di questo documento
(strcpy() ecc).
Riepilogando le tecniche di validazione descritte nel capitolo 4:
- controllare i valori degli argomenti prima della chiamata di funzione;
- definire procedure di controllo all’interno della funzione stessa: tale ridondanza
(considerando il punto precedente) è applicabile specialmente alle librerie scritte dal team
di sviluppo che le adopererà in futuro;
- progettare funzioni “wrapped”, ovvero versioni estese che richiamino la funzione
originaria aggiungendo i controlli di validazione necessari;
- dimostrare staticamente (ovvero a livello di compilazione) che gli input passati siano
validi.

- 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.

- Regola 20.6/7/8/9 (required) “La macro offsetof(), le funzioni setjmp e longjmp, la


gestione dei segnali in signal.h e la libreria stdio.h non devono essere usate”
A causa di comportamenti indefiniti e potenzialmente vulnerabili, tali funzionalità del
linguaggio C sono vietate: in particolare nei riguardi della libreria stdio.h, nella progettazione
di sistemi embedded non è solitamente richiesto l’uso delle funzioni di I/O.

1.20 Regole MISRA-C 2004: Fallimenti a run-time

- Regola 21.1 (required) “Per minimizzare le probabilità di fallimenti a run-time si


deve:
- usare tecniche e strumenti di analisi statica;
- usare tecniche e strumenti di analisi dinamica;
- progettare e implementare controlli di gestione di errori a run-time.”
Il controllo a run-time della presenza di errori è un problema serio, specialmente nel contesto
del linguaggio C, in quanto non è stato progettato per rilevare e/o prevenire tali problemi.
Le seguenti note forniscono alcuni consigli sulla gestione di controlli dinamici:

Matteo Mauro Matricola: 5971842 !16


- errori aritmetici: consiste negli errori inerenti alla valutazione di espressioni, come gli
underflow, overflow, divisione per zero o perdita di bit significativi in operazioni di shift;
- aritmetica dei puntatori: è necessario garantire che quando un indirizzo viene calcolato a
run-time, la cella di memoria puntata sia accessibile legittimamente, come discusso nelle
regole 11.1/1/4;
- passaggio di parametri a funzione: vedere regola 20.3;
- dereferenziare un puntatore: un puntatore ottenuto a run-time (ad esempio come risultato
di un’espressione o funzione) deve sempre essere anticipatamente controllato prima di
essere usato, verificando che sia diverso da NULL.

Matteo Mauro Matricola: 5971842 !17


APPENDICE B

CWE 121: Stack based Buffer Overflow

1° programma:
“CWE121_Stack_Based_Buffer_Overflow__char_type_overrun_memcpy_01.c"

1) Codice “bad function”


#define SRC_STR "0123456789abcdef0123456789abcde"

typedef struct _charVoid


{
char charFirst[16];
void * voidSecond;
void * voidThird;
} charVoid;

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);
}
}

2) Analisi della vulnerabilità


La stringa “SRC_STR” rappresenta la sorgente dati da copiare all’interno del campo
charFirst contenute nella struct _charVoid: per fare ciò è utilizzata la funzione memcpy() che
riceve rispettivamente:
- indirizzo del buffer di ricezione;
- indirizzo del buffer sorgente;
- numero di byte da copiare dalla sorgente verso la destinazione.
Dal momento che il numero di byte da copiare viene calcolato considerando l’intera struct
(che comprende altri campi) e non solo l’array destinatario, si ha un overflow di scrittura
(in particolare l’indirizzo del puntatore voidSecond sarà sovrascritto ed il contenuto
puntato non sarà più dereferenziabile attraverso di esso).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni
Regola MISRA-C 2004 violata # Tipo Valutazione

8.10: quando possibile, è sempre meglio usare l’internal 2 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


La chiamata a memcpy() deve essere sostituita con:

correct_length = sizeof(structCharVoid.charFirst);
memcpy(structCharVoid.charFirst, SRC_STR, correct_length);

Nessuna delle regole MISRA-C 2004 riesce direttamente o indirettamente a individuare


l’incoerenza tra la dimensione del buffer di destinazione e la quantità specificata di byte da
copiare.

5) Secure coding OWASP


Regola 195 (Memory management): “verificare che il buffer sia largo esattamente quanto
specificato".
Questa regola individua la vulnerabilità del programma 1, risulta quindi più efficace
rispetto alle regole MISRA-C 2004.


Matteo Mauro Matricola: 5971842 !2


2° programma:
“CWE121_Stack_Based_Buffer_Overflow__CWE805_char_declare_snprintf_01.c”

1) Codice “bad function”


void CWE121_Stack_Based_Buffer_Overflow__CWE806_char_declare_snprintf_01_bad()
{
char * data;
char dataBuffer[100];
data = dataBuffer;
/* FLAW: Initialize data as a large buffer that is larger than the small
buffer used in the sink */
memset(data, 'A', 100-1); /* fill with 'A's */
data[100-1] = '\0'; /* null terminate */
{
char dest[50] = "";
/* POTENTIAL FLAW: Possible buffer overflow if data is larger than dest
*/
snprintf(dest, strlen(data), "%s", data);
printLine(data);
}
}

2) Analisi della vulnerabilità


La funzione snprintf() viene utilizzata per copiare la stringa presente nel buffer dataBuffer
(largo 100 byte) nel buffer di destinazione dest (largo 50 byte). L’errore consiste nel
calcolare il numero di byte da copiare come la lunghezza della stringa sorgente, piuttosto
che la dimensione massima del buffer di destinazione; ciò provoca un buffer overflow.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 3 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


La chiamata a snprintf() deve essere sostituita con:
snprintf(dest, sizeof(dest), "%s", data);
dest[sizeof(dest)-1] = ‘\0’;

Nessuna delle regole MISRA-C 2004 riesce direttamente o indirettamente a individuare


l’incoerenza tra la dimensione del buffer di destinazione e la quantità specificata di byte da
copiare.
Matteo Mauro Matricola: 5971842 !3
5) Secure coding OWASP
Regola 195 (Memory management): “verificare che il buffer sia largo esattamente quanto
specificato".

Matteo Mauro Matricola: 5971842 !4


3° programma:
“CWE121_Stack_Based_Buffer_Overflow__CWE805_char_declare_snprintf_01.c”

1) Codice “bad function”


void CWE121_Stack_Based_Buffer_Overflow__CWE805_char_declare_snprintf_01_bad()
{
char * data;
char dataBadBuffer[50];
char dataGoodBuffer[100];
/* FLAW: Set a pointer to a "small" buffer. This buffer will be used in the
sinks as a destination buffer in various memory copying functions using a
"large" source buffer. */
data = dataBadBuffer;
data[0] = '\0'; /* null terminate */
{
char source[100];
memset(source, 'C', 100-1); /* fill with 'C's */
source[100-1] = '\0'; /* null terminate */
/* POTENTIAL FLAW: Possible buffer overflow if the size of data is less
than the length of source */
snprintf(data, 100, "%s", source);
printLine(data);
}
}
2) Analisi della vulnerabilità
La funzione snprintf() viene utilizzata per copiare la stringa presente nel buffer source
(largo 100 byte) nel buffer di destinazione dataBadBuffer (largo 50 byte). L’errore consiste
nell’impostare 100 byte come larghezza prefissata del buffer di destinazione, che però
eccede quella reale; ciò provoca un buffer overflow.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 4 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


La chiamata a snprintf() deve essere sostituita con:
snprintf(dest, sizeof(dest), "%s", data);
dest[sizeof(dest)-1] = ‘\0’;

Matteo Mauro Matricola: 5971842 !5


Nessuna delle regole MISRA-C 2004 riesce direttamente o indirettamente a individuare
l’incoerenza tra la dimensione del buffer di destinazione e la quantità specificata di byte da
copiare.

5) Secure coding OWASP


Regola 195 (Memory management): “verificare che il buffer sia largo esattamente quanto
specificato”.

Matteo Mauro Matricola: 5971842 !6


4° programma:
“CWE121_Stack_Based_Buffer_Overflow__src_char_declare_cpy_01.c”

1) Codice “bad function”


void CWE121_Stack_Based_Buffer_Overflow__src_char_declare_cpy_01_bad()
{
char * data;
char dataBuffer[100];
data = dataBuffer;
/* FLAW: Initialize data as a large buffer that is larger than the small
buffer used in the sink */
memset(data, 'A', 100-1); /* fill with 'A's */
data[100-1] = '\0'; /* null terminate */
{
char dest[50] = "";
/* POTENTIAL FLAW: Possible buffer overflow if data is larger than dest
*/
strcpy(dest, data);
printLine(data);
}
}

2) Analisi della vulnerabilità


La funzione strcpy() viene utilizzata per copiare la stringa presente nel buffer dataBuffer
(largo 100 byte) nel buffer di destinazione dest (largo 50 byte). La vulnerabilità consiste
nell’utilizzare la funzione strcpy(), la quale copia interamente il contenuto della stringa
sorgente indipendentemente dalla possibilità del buffer di destinazione di riuscire a
contenerla; ciò provoca un buffer overflow.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 3 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


La chiamata a strcpy() deve essere sostituita con:
strncpy(dest, data, sizeof(dest));
Nessuna delle regole MISRA-C 2004 riesce direttamente o indirettamente a individuare
l’incoerenza tra la dimensione del buffer di destinazione e la quantità specificata di byte da
copiare.

Matteo Mauro Matricola: 5971842 !7


5) Secure coding OWASP
Regola 195 (Memory management): “verificare che il buffer sia largo esattamente quanto
specificato”.

Matteo Mauro Matricola: 5971842 !8


CWE 401: Memory leak
5° programma
“CWE401_Memory_Leak__char_calloc_01.c”

1) Codice “bad function”


void CWE401_Memory_Leak__char_calloc_01_bad()
{
char * data;
data = NULL;
/* POTENTIAL FLAW: Allocate memory on the heap */
data = (char *)calloc(100, sizeof(char));
if (data == NULL) {exit(-1);}
/* Initialize and make use of data */
strcpy(data, "A String");
printLine(data);
/* POTENTIAL FLAW: No deallocation */
}

2) Analisi della vulnerabilità


La chiamata alla funzione calloc() consente di allocare un’area di memoria grande 100*
sizeof(char) byte; dopo averci copiato una stringa ed averla stampata, la funzione termina
senza aver deallocato esplicitamente il puntatore con una chiamata a free(), provocando un
memory leak.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

20.4: l’allocazione dinamica della memoria non dovrebbe 1 Required NON


PERTINENTE
essere usata

4) Codice corretto e conclusioni


La soluzione banale consiste nell’aggiungere come ultima istruzione della funzione la
chiamata alla funzione free(data);

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).

5) Secure coding OWASP


Regola 202 (Memory management): “liberare in modo appropriato la memoria allocata
dopo il completamento delle funzioni e prima di ogni punto di uscita”.
Questa regola individua esattamente la vulnerabilità del programma 2, risultando più
efficace rispetto alla regola 20.4 del MISRA-C 2004.

Matteo Mauro Matricola: 5971842 !10


CWE 134: Uncontrolled Format String
6° programma:
“CWE134_Uncontrolled_Format_String__char_connect_socket_fprintf_01.c”

1) Codice “bad function”


void CWE134_Uncontrolled_Format_String__char_connect_socket_fprintf_01_bad()
{
char * data;
char dataBuffer[100] = "";
data = dataBuffer;
{
/* Il codice di questo statement prevede l’apertura di una connessione
tramite socket, leggere dei dati e inserirli in dataBuffer, per i fini del
test non riporto l’intero codice qui */
}
/* POTENTIAL FLAW: Do not specify the format allowing a possible format
string vulnerability */
fprintf(stdout, data);
}

2) Analisi della vulnerabilità


Il blocco di codice non riportato, sostituito dal commento, prevede l’apertura di una socket
verso una sorgente dati esterna, con lo scopo di ricevere delle informazioni da inserire
nella variabile dataBuffer (il codice era molto verboso a causa di controlli di apertura e
chiusura della connessione, pertanto non l’ho riportato esplicitamente); infine la funzione
stampa direttamente tramite la chiamata a fprintf() il contenuto del buffer, senza verificare
la presenza di caratteri pericolosi nei dati ricevuti e introducendo una vulnerabilità del
tipo “Format string attack” (per approfondimento https://www.owasp.org/index.php/
Format_string_attack).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 5 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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.6: Ogni struttura di iterazione dovrebbe prevedere al più 1 Required NON


PERTINENTE
un’istruzione break per uscire dal ciclo (parte del codice non
riportato)

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni

Matteo Mauro Matricola: 5971842 !11


Il codice corretto deve prevedere il parsing dei dati ricevuti: nel caso di una stringa si
dovrebbe rigettarla qualora contenesse degli specificatori di formato (%x, %p, ecc).
ATTENZIONE: la soluzione proposta dalla suite Juliet prevede di specificare la chiamata
con il format %s nel seguente modo:
fprintf(stdout, “%s”, data);
tuttavia ciò non previene alcuna vulnerabilità in quanto non influenza la potenziale
struttura dannosa dei dati in input ricevuti.
Nessuna regola MISRA individuata influenza la rilevazione della vulnerabilità.

5) Secure coding OWASP


Regola 2 (Input validation): “distinguere ogni informazione tra dati sicuri e non sicuri.
Ogni dato proveniente da fonti potenzialmente pericolose (databases, servers, ecc) sono da
considerare non sicure fino al processo di validazione”.
La regola si applica al contesto analizzato, in quanto i dati ricevuti dalla socket non
vengono verificati prima dell’utilizzo.

Matteo Mauro Matricola: 5971842 !12


CWE 476: NULL Pointer Dereference
7° programma:
“CWE476_NULL_Pointer_Dereference__binary_if_01.c”

1) Codice “bad function”

/* struct used in some test cases as a custom type */


typedef struct _twoIntsStruct
{
int intOne;
int intTwo;
} twoIntsStruct;

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");
}
}
}

2) Analisi della vulnerabilità


La struct twoIntsStruct rappresenta un aggregato contenente 2 interi generici, per la quale
viene definito un puntatore (inizialmente nullo); la vulnerabilità si presenta nella verifica
della condizione dell’if: il primo controllo determina se il puntatore sia nullo o meno, in
caso affermativo accede al campo intOne e verifica che sia uguale a 5; l’operatore di bit-
wise ‘&’ prevede di eseguire tutte le istruzioni della condizione, pertanto la
dereferenziazione viene eseguita anche se il puntatore è nullo (rendendo inutile la prima
condizione).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Il codice corretto prevede l’uso dell’operatore logico && che termina l’esecuzione
anticipatamente se il risultato della condizione è False.

Matteo Mauro Matricola: 5971842 !13


if ((twoIntsStructPtr != NULL) && (twoIntsStructPtr->intOne == 5))

Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della


vulnerabilità.

5) Secure coding OWASP


Non è presente nessuna regola esplicita che evidenzi l’utilizzo scorretto di un operatore
logico piuttosto di uno di bit-wise, in quanto è evidentemente compito del programmatore
essere conscio della differenza tra le due categorie.

Matteo Mauro Matricola: 5971842 !14


CWE 78: Operative System - Command Injection
8° programma:
CWE78_OS_Command_Injection__char_connect_socket_execl_01.c

1) Codice “bad function”


#define COMMAND_INT_PATH "/bin/sh"
#define COMMAND_INT "sh"
#define COMMAND_ARG1 "-c"
#define COMMAND_ARG2 "ls "
#define COMMAND_ARG3 data

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 */

Matteo Mauro Matricola: 5971842 !15


/* POTENTIAL FLAW: Execute command without validating input possibly leading
to command injection */
execl(COMMAND_INT_PATH, COMMAND_INT_PATH, COMMAND_ARG1, COMMAND_ARG3, NULL);
}

2) Analisi delle vulnerabilità


L’istruzione execl (EXECute and Leave) permette di caricare in memoria un processo
separato, passando me argomento la cartella contente il programma, il nome
dell’eseguibile e eventuali parametri. COMMAND_ARG3 rappresenta un alias dell’array
data, che contiene parametri ricevuti in input dall’utente (letti attraverso una socket).
La vulnerabilità consiste nel non verificare i dati ricevuti, che possono portare un
attaccante a far eseguire codice pericoloso e inaspettato.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 5 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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.6: Ogni struttura di iterazione dovrebbe prevedere al più 1 Required NON


PERTINENTE
un’istruzione break per uscire dal ciclo (parte del codice non
riportato)

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Il codice corretto deve prevedere la validazione attraverso una whitelist di parametri sicuri
e certificati. I dati non corretti devono essere rigettati.

5) Secure coding OWASP


Regola 211 (General coding practices): “non passare alcun parametro, fornito dall’utente, a
funzioni caricate dinamicamente (in questo caso programmi caricati a run time)”.
Regola 2 (Input validation): “distinguere ogni informazione tra dati sicuri e non sicuri.
Ogni dato proveniente da fonti potenzialmente pericolose (databases, servers, ecc) sono da
considerare non sicure fino al processo di validazione”.
La regola si applica al contesto analizzato, in quanto i dati ricevuti dalla socket non
vengono verificati prima dell’utilizzo.

Matteo Mauro Matricola: 5971842 !16


CWE90: LDAP Injection
9° programma:
“CWE90_LDAP_Injection__w32_char_connect_socket_01.c”

1) Codice “bad function”

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);

/* Lettura dei dati dalla socket nel buffer dataBuffer */

{
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).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 6 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Il codice corretto deve prevedere la validazione attraverso una whitelist di parametri sicuri
e certificati. I dati non corretti devono essere rigettati.

5) Secure coding OWASP


Regola 211 (General coding practices): “non passare alcun parametro, fornito dall’utente, a
funzioni caricate dinamicamente (in questo caso API di accesso remoto a database)”.
Regola 2 (Input validation): “distinguere ogni informazione tra dati sicuri e non sicuri.
Ogni dato proveniente da fonti potenzialmente pericolose (databases, servers, ecc) sono da
considerare non sicure fino al processo di validazione”.
La regola si applica al contesto analizzato, in quanto i dati ricevuti dalla socket non
vengono verificati prima dell’utilizzo.

Matteo Mauro Matricola: 5971842 !18


CWE122: Heap based Buffer Overflow
10° programma:
“CWE90_LDAP_Injection__w32_char_connect_socket_01.c”

1) Codice “bad function”


#define SRC_STR "0123456789abcdef0123456789abcde"
typedef struct _charVoid
{
char charFirst[16];
void * voidSecond;
void * voidThird;
} charVoid;
void CWE122_Heap_Based_Buffer_Overflow__char_type_overrun_memcpy_01_bad()
{
{
charVoid * structCharVoid = (charVoid *)malloc(sizeof(charVoid));
if (structCharVoid == NULL) {exit(-1);}
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);
free(structCharVoid);
}
}

2) Analisi della vulnerabilità


La struct charVoid contiene 3 campi distinti: un array di char (charFirst) e 2 puntatori void
generici (voidSecond e voidThird). Dopo aver allocato un oggetto di tipo charVoid tramite
allocazione dinamica, viene copiata la stringa STR_SRC all’interno dell’array charFirst
(primo membro interno alla struct): alla funzione memcpy viene passato un numero di byte
da copiare incoerente con la dimensione del buffer di destinazione, perciò si verifica un
overflow che va a sovrascrivere il puntatore voidSecond (in questo contesto avviene nella
memoria heap).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

Matteo Mauro Matricola: 5971842 !19


Regola MISRA-C 2004 violata # Tipo Valutazione

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

20.4: l’allocazione dinamica della memoria non dovrebbe 2 Required NON


PERTINENTE
essere usata
Nota: la 20.4 è rilevata per la presenza sia di malloc() che di free()

4) Codice corretto e conclusioni


La regola 20.4 non risulta pertinente alla vulnerabilità introdotta, perché non è
l’allocazione dinamica ad essere stata usata impropriamente quanto piuttosto

5) Secure coding OWASP


Regola 202 (Memory management): “liberare in modo appropriato la memoria allocata
dopo il completamento delle funzioni e prima di ogni punto di uscita”.
Questa regola individua esattamente la vulnerabilità del programma 2, risultando più
efficace rispetto alla regola 20.4 del MISRA-C 2004.

Matteo Mauro Matricola: 5971842 !20


CWE124: Buffer Underwrite
11° programma:
“CWE124_Buffer_Underwrite__char_alloca_cpy_01.c”

1) Codice “bad function”


#include "std_testcase.h"
#include <wchar.h>
void CWE124_Buffer_Underwrite__char_alloca_cpy_01_bad()
{
char * data;
/* Alloca permette di allocare spazio direttamente nello stack, serve ai fini
del test per impedire complicazioni nell’allocare memoria nell’heap */
char * dataBuffer = (char *)ALLOCA(100*sizeof(char));
memset(dataBuffer, 'A', 100-1);
dataBuffer[100-1] = '\0';
/* FLAW: Set data pointer to before the allocated memory buffer */
data = dataBuffer - 8;
{
char source[100];
memset(source, 'C', 100-1); /* fill with 'C's */
source[100-1] = '\0'; /* null terminate */
/* POTENTIAL FLAW: Possibly copying data to memory before the
destination buffer */
strcpy(data, source);
printLine(data);
}
}

2) Analisi della vulnerabilità


Il contenuto del buffer “source” deve essere copiato nel buffer “dataBuffer” a cui si accede
tramite il puntatore “data”: quest’ultimo viene però inizializzato ad un indirizzo di
memoria arbitrariamente precedente a quello della prima cella di memoria di dataBuffer,
pertanto la successiva copia di dati avviene ad un indirizzo incoerente.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 3 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Il codice corretto dovrebbe prevedere la seguente inizializzazione
data = dataBuffer;

Matteo Mauro Matricola: 5971842 !21


Nessuna regola individuata pone limiti nella manipolazione aritmetica del puntatore, in
quanto spesso può essere una soluzione efficiente per manipolare un insieme di dati
allineati in memoria. In questo caso è scorretto decrementare l’indirizzo della cella iniziale
di un buffer per poi accedere direttamente.


Matteo Mauro Matricola: 5971842 !22


CWE126: Buffer Over-read
12° programma:
“CWE126_Buffer_Overread__char_alloca_loop_01.c”

1) Codice “bad function”


void CWE126_Buffer_Overread__char_alloca_loop_01_bad()
{
char * data;
char * dataBadBuffer = (char *)ALLOCA(50*sizeof(char));
char * dataGoodBuffer = (char *)ALLOCA(100*sizeof(char));
memset(dataBadBuffer, 'A', 50-1); /* fill with 'A's */
dataBadBuffer[50-1] = '\0'; /* null terminate */
memset(dataGoodBuffer, 'A', 100-1); /* fill with 'A's */
dataGoodBuffer[100-1] = '\0'; /* null terminate */
/* FLAW: Set data pointer to a small buffer */
data = dataBadBuffer;
{
size_t i, destLen;
char dest[100];
memset(dest, 'C', 100-1);
dest[100-1] = '\0'; /* null terminate */
destLen = strlen(dest);
/* POTENTIAL FLAW: using length of the dest where data
* could be smaller than dest causing buffer overread */
for (i = 0; i < destLen; i++)
{
dest[i] = data[i];
}
dest[100-1] = '\0';
printLine(dest);
}
}

2) Analisi della vulnerabilità


Viene eseguita una lettura dal buffer dataBadBuffer (grande 50 byte) e viene copiato nel
buffer dest (grande 100 byte). La copia viene seguita secondo la lunghezza del buffer di
destinazione, che risulta però più grande, provocando una lettura accidentale dei dati
presenti nell’area di memoria successiva.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 4 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

Matteo Mauro Matricola: 5971842 !23


4) Codice corretto e conclusioni
Quando si esegue la copia da un buffer ad un altro, la quantità di byte massima da
trasferire deve sempre essere determinata dalla grandezza del buffer di destinazione.
Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !24


CWE188: Reliance on Data Memory Layout
13° programma:
“CWE188_Reliance_on_Data_Memory_Layout__modify_local_01.c”

1) Codice “bad function”


void CWE188_Reliance_on_Data_Memory_Layout__modify_local_01_bad(){

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);

2) Analisi della vulnerabilità


La struct structCharInt consiste di due oggetti: charFirst (di tipo char) e intSecond (di tipo
int). Dopo aver creato un puntatore a charFirst, si tenta di accedere attraverso
un’operazione aritmetica all’indirizzo di intSecond, assumendo che si trovi allineato in
memoria esattamente dopo charFirst. Non solo l’operazione è sbagliata, ma non è mai
sicuro fare assunzioni sulla disposizione in memoria degli oggetti di una determinata
struttura dati.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 3 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Non è mai sicuro fare assunzioni sulla disposizione in memoria degli oggetti di una
determinata struttura dati. Nessuna regola MISRA individuata influenza la rilevazione e/
o prevenzione della vulnerabilità.


Matteo Mauro Matricola: 5971842 !25


CWE190: Integer Overflow
14° programma:
“CWE190_Integer_Overflow__char_fscanf_add_01.c”

1) Codice “bad function”


void CWE190_Integer_Overflow__char_fscanf_add_01_bad()
{
char data;
data = ' ';
/* POTENTIAL FLAW: Use a value input from the console */
fscanf (stdin, "%c", &data);
{
/* POTENTIAL FLAW: Adding 1 to data could cause an overflow */
char result = data + 1;
printHexCharLine(result);
}
}

2) Analisi della vulnerabilità


La funzione fscanf() è utilizzata per leggere (da stdin) un carattere di tipo char.
Successivamente il valore, salvato nella variabile data, viene incrementato di uno: tale
operazione può provocare un integer overflow, in quanto è necessario assicurarsi che la
somma non produca un valore troppo grande per essere rappresentato con un char.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


L’operazione dovrebbe essere eseguita solamente dopo il seguente controllo
if ( (CHAR_MAX - 1) >= data ){ … }
dove CHAR_MAX rappresenta il massimo valore rappresentabile attraverso il tipo char
(macro presente nella libreria limits.h).
La regola 6.3 rilevata evidenzia l’utilizzo di typedefs che informino il programmatore sul
numero di bit di rappresentazione associati al tipo char (ad esempio int8_t): un
programmatore attento dovrebbe quindi essere consapevole dei limiti di rappresentazione
di tale variabile, sopratutto qualora il programma venga trasferito su un’architettura
diversa. Seppur la regola 6.3 non influenzi direttamente la rilevazione di un overflow,

Matteo Mauro Matricola: 5971842 !26


pone una certa consapevolezza nei riguardi delle operazioni aritmetiche e circa i rischi di
integer overflow (in particolare la variabile result dovrebbe essere promossa ad un intero
più grande per essere sicuri che il risultato sia memorizzatile senza perdita di valore).


Matteo Mauro Matricola: 5971842 !27


CWE191: Integer Underflow
15° programma:
“CWE191_Integer_Underflow__char_fscanf_multiply_01.c”

1) Codice “bad function”


void CWE191_Integer_Underflow__char_fscanf_multiply_01_bad()
{
char data;
data = ' ';
/* POTENTIAL FLAW: Use a value input from the console */
fscanf (stdin, "%c", &data);
if(data < 0) /* ensure we won't have an overflow */
{
/* POTENTIAL FLAW: if (data * 2) < CHAR_MIN, this will underflow */
char result = data * 2;
printHexCharLine(result);
}
}

2) Analisi della vulnerabilità


Esattamente come nel test case precedente, in questo programma può verificarsi un
underflow, ovvero un’operazione che restituisce un valore che eccede il limite inferiore di
rappresentazione, in questo caso definito da CHAR_MIN.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Analogamente al 14° test case, nessuna regola MISRA individuata influenza la rilevazione
e/o prevenzione della vulnerabilità.


Matteo Mauro Matricola: 5971842 !28


CWE242: Use of Inherently Dangerous Function
16° programma:
“CWE242_Use_of_Inherently_Dangerous_Function__basic_01.c”

1) Codice “bad function”


#define DEST_SIZE 10
#include <stdio.h>
void CWE242_Use_of_Inherently_Dangerous_Function__basic_01_bad(){
char dest[DEST_SIZE];
char *result;
/* FLAW: gets is inherently dangerous and cannot be used safely. */
/* INCIDENTAL CWE120 Buffer Overflow since gets is inherently dangerous
and is
* an unbounded copy. */
result = gets(dest);
/* Verify return value */
if (result == NULL)
{
/* error condition */
printLine("Error Condition: alter control flow to indicate action
taken");
exit(1);
}
dest[DEST_SIZE-1] = '\0';
printLine(dest);
}

2) Analisi della vulnerabilità


L’utilizzo di funzioni come gets() è rischioso a causa dei comportamenti indefiniti o non
controllati esplicitamente (in particolare gets() risulta ormai una funzione deprecata). Tale
funzione non prevede un limite massimo di caratteri da leggere ed accettare da stdin,
pertanto tutti i dati letti vengono inseriti nel buffer, andando a generare un overflow di
sovrascrittura delle celle di memoria successive.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

20.9: la libreria <stdio.h> non deve essere utilizzata 1 Required PERTINENTE

Matteo Mauro Matricola: 5971842 !29


4) Codice corretto e conclusioni
La regola 20.9 identifica come rischiose molte funzioni della libreria <stdio.h> (quali fopen,
ftell, gets ecc), in quanto risultano funzioni dai comportamenti indefiniti e/o non
controllati. Nello sviluppo di sistemi embedded si ipotizza, inoltre, di non dover utilizzare
funzioni di I/O: se ciò è necessario, allora ogni funzione utilizzata deve essere
opportunamente controllata, al fine di implementare specifici controlli di prevenzione di
vulnerabilità.

Matteo Mauro Matricola: 5971842 !30


CWE252: Unchecked return value
17° programma:
“CWE252_Unchecked_Return_Value__char_fgets_01.c”

1) Codice “bad function”


#include <stdio.h>
void CWE252_Unchecked_Return_Value__char_fgets_01_bad()
{
{
/* By initializing dataBuffer, we ensure this will not be the
* CWE 690 (Unchecked Return Value To NULL Pointer) flaw for fgets() and
other variants */
char dataBuffer[100] = "";
char * data = dataBuffer;
printLine("Please enter a string: ");
/* FLAW: Do not check the return value */
fgets(data, 100, stdin);
printLine(data);
}
}

2) Analisi della vulnerabilità


Come esposto nella documentazione CWE252 (https://cwe.mitre.org/data/definitions/
252.html), controllare il valore di ritorno di una funzione permette di verificare che
l’esecuzione del programma stai proseguendo correttamente: in caso negativo, invece, un
codice di errore dovrebbe guidare la logica di esecuzione verso istruzioni adeguate a
risolvere il problema; ad esempio, se una funzione che deve revocare i privilegi di
amministratore fallisse, tale informazione dovrebbe essere verificata controllando il codice
di ritorno (senza permettere al flusso di esecuzione di proseguire).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

20.9: la libreria <stdio.h> non deve essere utilizzata 1 Required NON


PERTINENTE

Matteo Mauro Matricola: 5971842 !31


4) Codice corretto e conclusioni
Il codice corretto deve prevedere la valutazione del codice di ritorno:
if (fgets(data, 100, stdin) == NULL) {
printLine("fgets failed!");
exit(1);
}

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.

Regola 16.10 (required): se una funzione restituisce un’informazione di errore, allora


tale valore deve essere verificato.

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.

Matteo Mauro Matricola: 5971842 !32


CWE253: Incorrect checking of return value
18° programma:
“CWE253_Incorrect_Check_of_Function_Return_Value__char_fgets_01.c”

1) Codice “bad function”


#include <stdio.h>
void CWE253_Incorrect_Check_of_Function_Return_Value__char_fgets_01_bad()
{
{
/* By initializing dataBuffer, we ensure this will not be the
* CWE 690 (Unchecked Return Value To NULL Pointer) flaw for fgets() and
other variants */
char dataBuffer[100] = "";
char * data = dataBuffer;
printLine("Please enter a string: ");
/* FLAW: Incorrectly check the return value: if this function failed it
will return NULL,
* but we are checking to see if the return value is less than 0. */
if (fgets(data, 100, stdin) < 0)
{
printLine("fgets failed!");
exit(1);
}
printLine(data);
}
}

2) Analisi della vulnerabilità


La vulnerabilità consiste in un controllo incoerente del valore di ritorno della funzione
fgets(): quest’ultima restituisce NULL in caso di errore, mentre nel blocco if si controlla se il
valore restituito è minore di zero (controllo che potrebbe non avere effetto nel rilevare la
condizione di errore, in quanto il valore di NULL è dipendente dall’implementazione).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

20.9: la libreria <stdio.h> non deve essere utilizzata 1 Required NON


PERTINENTE

Matteo Mauro Matricola: 5971842 !33


4) Codice corretto e conclusioni
Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !34


CWE369: Division by zero
19° programma:
“CWE369_Divide_by_Zero__float_connect_socket_01.c”

1) Codice “bad function”


void CWE369_Divide_by_Zero__float_connect_socket_01_bad()
{
float data;
/* Initialize data */
data = 0.0F;
int recvResult;
struct sockaddr_in service;
SOCKET connectSocket = INVALID_SOCKET;
char inputBuffer[CHAR_ARRAY_SIZE];
do {
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 */
/* POTENTIAL FLAW: Use a value input from the network */
recvResult = recv(connectSocket, inputBuffer, CHAR_ARRAY_SIZE - 1, 0);
if (recvResult == SOCKET_ERROR || recvResult == 0)
{
break;
}
/* NUL-terminate string */
inputBuffer[recvResult] = '\0';
/* Convert to float */
data = (float)atof(inputBuffer);
}
while (0);
if (connectSocket != INVALID_SOCKET)
{
CLOSE_SOCKET(connectSocket);
}
/* POTENTIAL FLAW: Possibly divide by zero */
int result = (int)(100.0 / data);
printIntLine(result);
}

2) Analisi della vulnerabilità


Dopo aver ottenuto un dato da una connessione non sicura (tramite socket), esso viene
usato per eseguire un’operazione di divisione senza controllare che il valore del
denominatore fosse diverso da zero.

Matteo Mauro Matricola: 5971842 !35


3) Report del tool
Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 4 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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.6: Ogni struttura di iterazione dovrebbe prevedere al più 1 Required NON


PERTINENTE
un’istruzione break per uscire dal ciclo (parte del codice non
riportato)

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Il programma deve prevedere un controllo della variabile data prima di essere usata nella
divisione.
Ogni valore ricevuto da un fonte non sicura deve sempre essere verificato, in questo caso il
valore zero non è necessariamente un valore sbagliato (quindi non deve per forza essere
rigettato), piuttosto è l’operazione aritmetica a dover essere posta in un blocco di controllo.
Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !36


CWE377: Insecure temporary file
20° programma:
“CWE377_Insecure_Temporary_File__char_mktemp_01.c”

1) Codice “bad function”


void CWE377_Insecure_Temporary_File__char_mktemp_01_bad() {

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);
}

2) Analisi della vulnerabilità


Determinati software richiedono frequentemente l’utilizzo di file temporanei, perciò molti
linguaggi di programmazione provvedono API atti a crearne specifiche istanze; spesso tali
funzioni risultano vulnerabili a vari tipi di attacchi, perciò non dovrebbero essere usate
(per un’ampia trattazione: https://cwe.mitre.org/data/definitions/377.html).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 3 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Il linguaggio C prevede una versione più sicura di mktemp(), ovvero mkstemp().

Matteo Mauro Matricola: 5971842 !37


char filename[] = "/tmp/fileXXXXXX"; // mutable store for filename
// FIX: Use mkstemp(). This is not a perfect solution, since file
// names are too predictable, but it is a vast improvement.
int fileDesc = mkstemp(filename);
printLine(filename);
if (fileDesc != -1)
{
printLine("Temporary file was opened...now closing file");
CLOSE(fileDesc);
}

Matteo Mauro Matricola: 5971842 !38


CWE390: Error control without action
21° programma:
“CWE390_Error_Without_Action__fgets_char_01.c”

1) Codice “bad function”


#include <stdio.h>
void CWE390_Error_Without_Action__fgets_char_01_bad()
{
/* 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);
}

2) Analisi della vulnerabilità


Nel blocco di controllo dell’errore generato dall’istruzione fgets() non viene eseguita alcuna
azione: quando un blocco if risulta vuoto, è probabile che ci sia una cattiva pratica di
programmazione alla base.

3) Report della vulnerabilità


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

20.9: la libreria <stdio.h> non deve essere utilizzata 1 Required NON


PERTINENTE

4) Codice corretto e conclusioni


Il blocco di codice if deve prevedere la stampa di un messaggio di errore e l’uscita dal
programma con un codice di errore, ad esempio:
if (fgets(data, 100, stdin) == NULL){
printLine("fgets failed!");
exit(1);
}


Matteo Mauro Matricola: 5971842 !39


CWE391: Unchecked error condition
22° programma:
“CWE391_Unchecked_Error_Condition__sqrt_01.c”

1) Codice “bad function”


#include <math.h>
void CWE391_Unchecked_Error_Condition__sqrt_01_bad()
{
{
double doubleNumber;
doubleNumber = (double)sqrt((double)-1);
/* FLAW: Do not check to see if sqrt() failed */
printDoubleLine(doubleNumber);
}
}

2) Analisi della vulnerabilità


La funzione sqrt() esegue la radice quadrata dell’argomento ricevuto, se una condizione di
errore viene rilevata (ad esempio un’argomento negativo), allora la variabile globale errno
è impostata al valore EDOM. Nel codice in esame, non viene eseguito nessun controllo.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


La soluzione proposta dalla suite Juliet è di utilizzare la libreria errno.h e di controllare il
valore di errno prima di procedere nel flusso di esecuzione: tale pratica risulta però non
conforme alle regole MISRA-C 2004, piuttosto la regola violata (ma non rilevata dal tool) è
la regola 20.3:
regola 20.3 (required): “la validità degli argomenti passati alle funzioni delle librerie
standard deve essere sempre controllato”


Matteo Mauro Matricola: 5971842 !40


CWE398: Poor code quality - addition
23° programma:
“CWE398_Poor_Code_Quality__addition_01.c”

1) Codice “bad function”


void CWE398_Poor_Code_Quality__addition_01_bad()
{
{
int intOne = 1, intTwo = 1, intSum = 0;
printIntLine(intSum);
/* FLAW: the statement has no effect */
/* This generates a compiler warning, but we expect it to */
intOne + intTwo;
printIntLine(intSum);
}
}

2) Analisi della vulnerabilità


La CWE398 fa da parte della classificazione “Seven pernicious kingdom”: questo genere
di errori non introduce direttamente vulnerabilità, ma è sintomo di un codice poco
mantenibile e sviluppato con poca attenzione. Come sottolineato dagli autori del Seven
pernicious kingdom: “Poor code quality leads to unpredictable behavior. From a user's
perspective that often manifests itself as poor usability. For an adversary it provides an
opportunity to stress the system in unexpected ways.” (fonte: https://cwe.mitre.org/
data/definitions/398.html).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 3 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !41


CWE415: Double free
24° programma:
“CWE415_Double_Free__malloc_free_char_01.c”

1) Codice “bad function”


void CWE415_Double_Free__malloc_free_char_01_bad()
{
char * data;
/* Initialize data */
data = NULL;
data = (char *)malloc(100*sizeof(char));
if (data == NULL) {exit(-1);}
/* POTENTIAL FLAW: Free data in the source - the bad sink frees data as well
*/
free(data);
/* POTENTIAL FLAW: Possibly freeing memory twice */
free(data);
}

2) Analisi della vulnerabilità


Quando un programma chiama due o più volte free() nei confronti dello stesso puntatore,
la struttura dati di tracciamento della memoria dinamica può corrompersi; tale
incongruenza può portare le successive chiamate a malloc() a restituire lo stesso indirizzo
di memoria, causando danni all’integrità dei dati.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

20.4: l’allocazione dinamica della memoria non dovrebbe 1 Required NON


PERTINENTE
essere usata

4) Codice corretto e conclusioni


Il codice corretto non deve prevedere due o più chiamate allo stesso puntatore, senza che
memoria dinamica sia effettivamente riallocata tra ogni chiamata a free().
La regola 20.4 (che vieta l’uso di allocazione dinamica) non risulta sufficiente a tracciare
l’errore: tale pratica può essere accettata ed utilizzata impostando opportune regole di
deviazione, ma almeno una regola MISRA-C 2004 (e quindi il tool) avrebbe dovuto
rilevare 2 chiamate a free() consecutive.


Matteo Mauro Matricola: 5971842 !42


CWE416: Use after free
25° programma:
“CWE416_Use_After_Free__malloc_free_char_01.c”

1) Codice “bad function”


void CWE416_Use_After_Free__malloc_free_char_01_bad()
{
char * data;
/* Initialize data */
data = NULL;
data = (char *)malloc(100*sizeof(char));
if (data == NULL) {exit(-1);}
memset(data, 'A', 100-1);
data[100-1] = '\0';
/* POTENTIAL FLAW: Free data in the source - the bad sink attempts to use
data */
free(data);
/* POTENTIAL FLAW: Use of data that may have been freed */
printLine(data);
/* POTENTIAL INCIDENTAL - Possible memory leak here if data was not freed */
}

2) Analisi della vulnerabilità


Referenziare un’area di memoria heap che è stat precedentemente liberata può provocare
crash del programma, l’accesso valori incoerenti oppure esecuzione di codice malevolo.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

20.4: l’allocazione dinamica della memoria non dovrebbe 1 Required NON


PERTINENTE
essere usata

4) Codice corretto e conclusioni


Il codice corretto non deve prevedere un accesso attraverso il puntatore liberato e non
riallocato.
La regola 20.4 (che vieta l’uso di allocazione dinamica) non risulta sufficiente a tracciare
l’errore: tale pratica può essere accettata ed utilizzata impostando opportune regole di
deviazione, ma almeno una regola MISRA-C 2004 (e quindi il tool) avrebbe dovuto
rilevare un accesso non corretto al puntatore data.

Matteo Mauro Matricola: 5971842 !43
CWE426: Untrusted Search Path
26° programma:
“CWE426_Untrusted_Search_Path__char_popen_01.c”

1) Codice “bad function”


#define BAD_OS_COMMAND "ls -la"
#define POPEN popen
#define PCLOSE pclose
void CWE426_Untrusted_Search_Path__char_popen_01_bad()
{
char * data;
char dataBuffer[100] = "";
data = dataBuffer;
/* FLAW: the full path is not specified */
strcpy(data, BAD_OS_COMMAND);
{
FILE *pipe;
/* POTENTIAL FLAW: Executing the popen() function without specifying the
full path to the executable
* can allow an attacker to run their own program */
pipe = POPEN(data, "wb");
if (pipe != NULL)
{
PCLOSE(pipe);
}
}
}

2) Analisi della vulnerabilità


La funzione popen() apre un processo effettuando le operazioni seguenti:
• crea una 'pipe'
• esegue una fork()
• invoca la shell
Se il path passato come argomento alla funzione popen() non è accuratamente validato, un
attaccante potrebbe eseguire codice malevolo.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

Matteo Mauro Matricola: 5971842 !44


CWE457: Use of uninitialized variable
27° programma:
“CWE457_Use_of_Uninitialized_Variable__char_pointer_01.c”

1) Codice “bad function”


void CWE457_Use_of_Uninitialized_Variable__char_pointer_01_bad()
{
char * data;
/* POTENTIAL FLAW: Don't initialize data */
/* POTENTIAL FLAW: Use data without initializing it */
printLine(data);
}

2) Analisi della vulnerabilità


In diversi linguaggi di programmazione, le variabili allocate sullo stack non definiscono
sempre un inizializzazione di default: spesso possono contenere valori incoerenti (residuo
di altre operazioni precedenti) e l’accesso a questi dati può essere una violazione alla
security del programma.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e vulnerabilità


Ogni variabile definita deve prevedere un’inizializzazione (esplicita se quella implicita
non è garantita). 


Matteo Mauro Matricola: 5971842 !45


CWE459: Incomplete cleanup
28° programma:
“CWE459_Incomplete_Cleanup__char_01.c”

1) Codice “bad function”


void CWE459_Incomplete_Cleanup__char_01_bad()
{
{
char filename[] = "badXXXXXX";
FILE *pFile;
/* Establish that this is a temporary file and that it should be deleted
*/
int fileDesc = MKSTEMP(filename);
if (fileDesc != -1)
{
pFile = FDOPEN(fileDesc, "w");
if (pFile != NULL)
{
fprintf(pFile, "Temporary file");
fclose(pFile);
/* FLAW: We don't unlink */
}
}
}
}

2) Analisi della vulnerabilità


La funzione mkstemp() consente di creare un file temporaneo dal nome univoco e di
accedere direttamente al suo file descriptor, senza incorrere in vulnerabilità di tipo TOC/
TOU di race condition. La vulnerabilità risiede nel non richiamare unlink() per rimuovere
tale file: dal momento che un sistema potrebbe vincolare il numero massimo di file
temporanei creati, non eliminarli può condurre ad un DOS.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti
4) Codice corretto e conclusioni
Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità, in particolare nessuna regola MISRA-C 2004 prevede linee guida
sull’utilizzo sicuro di file temporanei.


Matteo Mauro Matricola: 5971842 !46


CWE464: Addition of data structure sentinel
29° programma:
“CWE464_Addition_of_Data_Structure_Sentinel__basic_01.c”

1) Codice “bad function”


void CWE464_Addition_of_Data_Structure_Sentinel__basic_01_bad()
{
char data;
data = ' ';
{
char charArraySource[2];
charArraySource[0] = (char)getc(stdin);
charArraySource[1] = '\0';
/* FLAW: If the character entered on the command line is not an int,
* a null value will be returned */
data = (char)atoi(charArraySource);
}
{
char charArraySink[4];
charArraySink[0] = 'x';
/* POTENTIAL FLAW: If data is null, the rest of the array will not be
printed */
charArraySink[1] = data;
charArraySink[2] = 'z';
charArraySink[3] = '\0';
printLine(charArraySink);
}
}
2) Analisi della vulnerabilità
A volte un carattere speciale viene usato per delineare la fine di una sequenza di dati: è il
caso di ‘\0’ per la terminazione di una stringa. Quando si permette all’utente di inserire
dati in una struttura dati, è fondamentale validare ciò che è stato inserito, al fine di
mantenere integra la collezione di dati.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


La regola violata (poiché effettivamente attinente alla validazione dell’input) ma non
rilevata dal tool è la regola 20.3.


Matteo Mauro Matricola: 5971842 !47


CWE467: Use of sizeof on pointer type
30° programma:
“CWE467_Use_of_sizeof_on_Pointer_Type__char_01.c”

1) Codice “bad function”


void CWE467_Use_of_sizeof_on_Pointer_Type__char_01_bad()
{
{
char * badChar = NULL;
/* FLAW: Using sizeof the pointer and not the data type in malloc() */
badChar = (char *)malloc(sizeof(badChar));
if (badChar == NULL) {exit(-1);}
*badChar = 'B';
printHexCharLine(*badChar);
free(badChar);
}
}

2) Analisi della vulnerabilità


Il codice sopra riportato calcola la dimensione dello spazio di memoria heap da allocare
usando sizeof sul puntatore, piuttosto che sul tipo di dato “puntato” (ovvero char).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

20.4: l’allocazione dinamica della memoria non dovrebbe 1 Required NON


PERTINENTE
essere usata

4) Codice corretto e conclusioni


Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !48


CWE468: Incorrect Pointer Scaling
31° programma:
“CWE468_Incorrect_Pointer_Scaling__char_ptr_to_int_01.c”

1) Codice “bad function”


void CWE468_Incorrect_Pointer_Scaling__char_ptr_to_int_01_bad()
{
{
int intArray[5] = { 1, 2, 3, 4, 5 };
char *charPointer = (char *)intArray; /* get a char pointer to intArray
- common idiom in file and network packet parsing */
/* get intArray[2] */
/* FLAW: sizeof() needed since pointer is a char*, not an int* */
int toPrint = (int) (*(charPointer+2));
printIntLine(toPrint);
}
}

2) Analisi della vulnerabilità


L’aritmetica dei puntatori si basa sul tipo di dato che viene puntato, in certi casi un tipo di
puntatore (ad esempio int*) può essere esplicitamente castato ad un altro tipo (come
char*): in tal caso è necessario eseguire calcoli specifici per mantenere la coerenza nella
dereferenziazione di ogni elemento, altrimenti si può andare ad accedere a celle di
memoria non corrette.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 3 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


L’istruzione di dereferenziazione deve essere modificata così:
int toPrint = (int) (*(charPointer+(2*sizeof(int))));
Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !49


CWE475: Undefined Behavior for Input to API
32° programma:
“CWE475_Undefined_Behavior_for_Input_to_API__char_01.c”

1) Codice “bad function”


void CWE475_Undefined_Behavior_for_Input_to_API__char_01_bad()
{
{
char dataBuffer[100] = "";
char * data = dataBuffer;
strcpy(data, "abcdefghijklmnopqrstuvwxyz");
/* FLAW: Copy overlapping memory regions using memcpy() for which the
result is undefined */
memcpy(data + 6, data + 4, 10*sizeof(char));
printLine(data);
}
}

2) Analisi della vulnerabilità


La funzione memcpy() è una funzione deprecata, in quanto non consente di eseguire la
copia corretta nel caso in cui i 2 buffer siano sovrapposti, assumendo un comportamento
non definito.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Per aggirare il problema si utilizza funzione memmove().
Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !50


CWE481: Assigning instead of Comparing
33° programma:
“CWE481_Assigning_Instead_of_Comparing__basic_01.c”

1) Codice “bad function”


void CWE481_Assigning_Instead_of_Comparing__basic_01_bad()
{
{
int intRand = rand();
/*FLAW:should be == and INCIDENTIAL CWE 571 Expression Is Always True */
if(intRand = 5)
{
printLine("i was 5");
}
}
}

2) Analisi della vulnerabilità


L’espressione di assegnamento può essere confusa con quella di uguaglianza, il problema
è che quest’ultima risulta essere uno statement che restituisce sempre valore vero e
permette di superare affermativamente ogni controllo.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Il codice corretto prevede di usare ‘==‘ al posto di ‘=‘. Nessuna regola MISRA individuata
influenza la rilevazione e/o prevenzione della vulnerabilità.


Matteo Mauro Matricola: 5971842 !51


CWE484: Omitted break statement in switch
34° programma:
“CWE484_Omitted_Break_Statement_in_Switch__basic_01.c”

1) Codice “bad function”


void CWE484_Omitted_Break_Statement_in_Switch__basic_01_bad()
{
int x = (rand() % 3);
/* FLAW: Missing break in first case */
switch (x)
{
case 0:
printLine("0");
case 1:
printLine("1");
break;
case 2:
printLine("2");
break;
default:
printLine("Invalid Number");
break;
}
}

2) Analisi della vulnerabilità


La mancata presenza di uno statement break in uno switch può essere causato da una
dimenticanza, che porta all’esecuzione di case non attinenti al valore di selezione.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

15.2 : ogni clausola di uno switch deve prevedere la 1 Required PERTINENTE


terminazione con un break esplicito

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


La regola 15.2 identifica correttamente la vulnerabilità introdotta.


Matteo Mauro Matricola: 5971842 !52


CWE511: Logic time bomb
35° programma:
“CWE511_Logic_Time_Bomb__counter_01.c”

1) Codice “bad function”


#define COUNT_CHECK 20000
void CWE511_Logic_Time_Bomb__counter_01_bad()
{
int count = 0;
do
{
/* FLAW: If a counter reaches a certain value, delete a file */
if (count == COUNT_CHECK)
{
unlink("important_file.txt");
}
count++;
}
while(1 == 1); /* infinite loop */
}

2) Analisi della vulnerabilità


Definizione di “logic time bomb” della CWE: il software contiene codice progettato per
eseguire operazioni illegittime, ma solo dopo una certa quantità di tempo o a seguito di
una certa condizione logica. Nel caso proposto è presente un contatore che, dopo aver
raggiunto un certo valore, consente di eseguire un’operazione potenzialmente pericolosa.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !53


CWE561: Dead code - return before code
36° programma:
“CWE561_Dead_Code__return_before_code_01.c”

1) Codice “bad function”


void bad_function()
{
return;
/* FLAW: code after the 'return' */
printLine("Hello");
}

void CWE561_Dead_Code__return_before_code_01_bad()
{
bad_function();
}

2) Analisi della vulnerabilità


La presenza di un return precedente ad una serie di istruzioni rappresenta un errore
logico, in quanto una sezione di codice che non verrà mai eseguito non ha motivo di
esistere.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


La regola 14.7 identifica correttamente la vulnerabilità.


Matteo Mauro Matricola: 5971842 !54


CWE562: Return of stack variable address
37° programma:
“CWE562_Return_of_Stack_Variable_Address__return_buf_01.c”

1) Codice “bad function”


static char *helperBad()
{
char charString[] = "helperBad string";
/* FLAW: returning stack-allocated buffer */
return charString; /* this may generate a warning -- it's on purpose */
}

void CWE562_Return_of_Stack_Variable_Address__return_buf_01_bad()
{
printLine(helperBad());
}

2) Analisi della vulnerabilità


Restituire l’indirizzo di una variabile locale rappresenta un errore, in quanto lo stack di
allocazione della funzione terminata verrà plausibilmente riutilizzato da qualche altra
funzione; perciò il valore della variabile restituita potrebbe non contenere il valore
corretto.

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 2 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !55


CWE570: Expression always false
38° programma:
“CWE570_Expression_Always_False__global_01.c”

1) Codice “bad function”


int globalFalse = 0;

void CWE570_Expression_Always_False__global_01_bad()
{
/* FLAW: This expression is always false */
if (globalFalse)
{
printLine("Never prints");
}
}

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !56


CWE571: Expression always true
39° programma:
“CWE571_Expression_Always_True__global_01.c”

1) Codice “bad function”


int globalTrue = 1;

void CWE571_Expression_Always_True__global_01_bad()
{
/* FLAW: This expression is always true */
if (globalTrue)
{
printLine("Always prints");
}
}

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

6.3: Le dichiarazioni di tipi primitivi andrebbero sostituite con 1 Advisory NON


PERTINENTE
typedefs che specificano chiaramente le dimensioni

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


Nessuna regola MISRA individuata influenza la rilevazione e/o prevenzione della
vulnerabilità.


Matteo Mauro Matricola: 5971842 !57


CWE674: Uncontrolled recursion
40° programma:
“CWE674_Uncontrolled_Recursion__infinite_recursive_call_01.c”

1) Codice “bad function”


static void helperBad()
{
/* FLAW: this function causes infinite recursion */
int x = 0;
helperBad();

void CWE674_Uncontrolled_Recursion__infinite_recursive_call_01_bad()
{
helperBad();
}

2) Analisi della vulnerabilità


La ricorsione proposta nel test case prevede delle chiamate ricorsivamente dirette alla
funzione helperBad(), che provoca un ciclo infinito (con plausibile crash del sistema a
seguito della terminazione di spazio di allocazione causata da ogni chiamata).

3) Report del tool


Regola MISRA-C 2004 violata # Tipo Valutazione

8.10: quando possibile, è sempre meglio usare l’internal 1 Required NON


PERTINENTE
linkage rispetto all’external

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

16.2: la ricorsione tra funzioni non è permessa, né diretta nè 1 Required PERTINENTE


indiretta

16.5: le funzioni senza parametri di input dovrebbe esplicitare 1 Required NON


PERTINENTE
“void” nella lista degli argomenti

4) Codice corretto e conclusioni


La regola 16.2 identifica correttamente la vulnerabilità, sebbene rappresenti una soluzione
troppo generica e adattabile a seconda dei contesti (una deviazione formale controllata
potrebbe essere necessaria a seconda del contesto di sviluppo e del software da
progettare).

Matteo Mauro Matricola: 5971842 !58


Tabella riepilogativa delle regole MISRA-C 2004 rilevate
Regola Tipo Occorrenze # Non pertinenti # Pertinenti
totali

6.3 - Types Advisory 78 78 -

8.10 - Declarations and Definitions Required 35 35 -

8.11 - Declarations and Definitions Required 35 35 -

14.6 - Control flow Required 3 3 -

14.7 - Control flow Required 1 - 1

15.2 - Control statements Required 1 - 1

16.2 - Functions Required 1 - 1

16.5 - Functions Required 36 36 -

16.10 - Functions (*) Required 1 - 1

20.3 - Standard libraries (*) Required 2 - 2

20.4 - Standard libraries Required 5 5 -

20.9 - Standard libraries Required 4 3 1

Legenda:
(*) := regola violata, ma non rilevata dal tool Understand v5.0

Regola pertinente Test case

14.7 - Control flow 36° programma - CWE561: Dead code - return before code

15.2 - Control 34° programma - CWE484: Omitted break statement in switch


statements
16.2 - Functions 40° programma - CWE674: Uncontrolled recursion

20.9 - Standard 16° programma - CWE242: Use of Inherently Dangerous Function


libraries

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.

Matteo Mauro Matricola: 5971842 !59

Vous aimerez peut-être aussi