This post was written for the italian cybersecurity blog Cyberment and published here.
For this reason it is written in italian.

I Type System contro le minacce

Sebbene molti linguaggi di programmazione moderni siano progettati e sviluppati in modo da minimizzare gli errori e le vulnerabilità durante la scrittura del codice, accade comunemente che bug e debolezze vengano introdotti dal programmatore stesso.

Il Type Confusion bug insidia da sempre programmi scritti in linguaggi insicuri come il C o il C++, ma possono esserne vulnerabili anche programmi scritti in linguaggi fortemente tipati come Java o Rust. In questo articolo vedremo cosa si intende per type confusion e come può essere sfruttato da un attaccante per mettere a segno un type confusion attack.

Tipi e Casting

I tipi di dato, o più semplicemente tipi, sono un’astrazione presente in molti linguaggi di programmazione ad alto livello che permette di restringere l’insieme dei valori assegnabili ad un dato e le operazioni che su questi dati si possono effettuare.

L’utilizzo dei tipi consente di scrivere programmi più sicuri, in quanto vengono effettuati dei controlli di tipo su dati e operazioni. In un linguaggio fortemente tipato, non sarà possibile ad esempio sommare un intero ad una stringa. Questi controlli vengono effettuati dal type checker e possono avvenire:

  • a tempo di compilazione: si parla in questo caso di type checking statico; se un programma non supera il type checking statico, il compilatore restituirà un errore ed il programma non verrà compilato.

  • a tempo di esecuzione: si parla di type checking dinamico. In questo caso, il programma verrà compilato correttamente, ma a tempo di esecuzione il run-time system potrebbe sollevare un errore, con conseguente crash del programma.

Talvolta vi è la necessità di convertire il tipo di un dato in un altro tipo. Si pensi, ad esempio, al caso in cui si voglia effettuare una divisione tra due interi. In C/C++, il risultato di questa divisione viene automaticamente troncato alla parte intera. Sarà necessario un casting esplicito per preservare la parte decimale del risultato. Al contrario, se si tenta di memorizzare un valore virgola mobile in una variabile di tipo intero, il valore verrà automaticamente convertito nel rispettivo valore intero, troncato cioè alla parte precedente la virgola. Si parla in questo caso di coercion, o casting implicito.

Alcuni linguaggi, come il C++, permettono poi di distinguere il metodo di conversione del tipo in

  • static cast: vengono effettuati dei controlli a tempo di compilazione sulla validità della conversione, prendendo in considerazione i tipi statici degli oggetti coinvolti. Se questi controlli vengono correttamente superati, viene effettuato il casting.

  • dynamic cast: il compilatore genera una serie di istruzioni di controllo sui tipi dinamici degli oggetti coinvolti, che vengono aggiunti al codice del programma ed eseguiti a tempo di esecuzione, prima di effettuare la conversione di dato.

In C++ infatti, come in altri linguaggi OOP, viene mantenuta una distinzione tra tipo statico e tipo dinamico. Il primo è il tipo dell’oggetto al momento della dichiarazione. Il secondo, invece, è il tipo dell’oggetto in un determinato momento dell’esecuzione e può coincidere con il tipo statico o esserne un sottotipo.

Type Safety

Un linguaggio viene detto type-safe, o più colloquialmente sicuro, se il type-checker di cui dispone, sia questo statico o dinamico, è in grado di rilevare tutti gli errori di tipo. Molti type-checker, inoltre, non si limitano a rilevare errori di incoerenza tra tipi, ma riescono facilmente ad individuare puntatori pendenti, campi inesistenti di oggetti o strutture, errori di tipo out-of-bound e altri errori di corruzione della memoria. Per questo motivo la proprietà di type-safety di un linguaggio è fortemente collegata al concetto di memory-safety e assume importanza rilevante nella progettazione di sistemi sicuri.

Sebbene la proprietà di type-safety si associa solitamente ai linguaggi di programmazione, sarebbe più appropriato riferirsi alle primitive da essi fornite. Infatti, ogni linguaggio di programmazione ha solitamente primitive safe e primitive unsafe. Rust, ad esempio, è considerato un linguaggio sicuro, sebbene sia possibile scrivere codice C/C++ tramite la Foreign Function Interface, andando ad intaccare la safety del programma in questione.

Type Confusion Attack

Gli errori di tipo type confusion avvengono quando si alloca una risorsa, come una variabile o un oggetto, di un determinato tipo e si tenta successivamente di accedere alla stessa utilizzando un tipo incompatibile.

Bug di questo tipo possono presentarsi in molti linguaggi di programmazione, causando comportamenti indefiniti durante l’esecuzione del programma. In linguaggi non memory-safe come C e C++, la conseguenza più comune ad un errore di type confusion è un accesso out-of-bound alla memoria, che può portare ad un crash del programma. Nel campo della sicurezza informatica questo si traduce in un attacco alla disponibilità del servizio. Un bug di tipo type confusion diventa infatti una vulnerabilità quando può essere sfruttato per portare a compimento un attacco o una parte di un attacco informatico. Il DOS non è però l’unico obiettivo di un attaccante.

Vulnerabilità di tipo type confusion sono state sfruttate, infatti, per montare diversi tipi di attacchi su diversi tipi di prodotti: da Adobe Flash a Google Chrome. Molte voci CVE rientrano in questa categoria di vulnerabilità, con attacchi che possono portare anche all’esecuzione di codice remoto.

C/C++ Union

La type confusion è il vettore d’attacco principale per applicazioni C/C++ moderne. Il motivo principale per cui questa vulnerabilità è tanto presente in programmi C/C++ è la debolezza del type-system di questi linguaggi, che non offre alcuna garanzia sulla loro type-safety.

Uno dei principali costrutti che ha portato a casi di type confusion è l’union, conosciuto anche come tipo somma. In C/C++, una union è un tipo di dato contenente più campi, come una struct. A differenza di quest’ultima, però, i campi interni sono mutuamente esclusivi e riferiscono tutti la stessa area di memoria. Questo porta a type confusion nel momento in cui si tenta di accedere alla union con un tipo differente da quello attivo in quel momento.

Solitamente, gli sviluppatori C/C++ utilizzano le union all’interno di struct, associandole ad una variabile che indichi il tipo corrente. L’essenza della vulnerabilità consiste quindi nella desincronizzazione di questa variabile dal membro union.

C++ Casting

Il C++ è stato progettato come un erede ad oggetti del C. Prevede quindi il concetto di classe e di ereditarietà, proprio come Java e altri linguaggi OOP. Questa caratteristica lo rende un linguaggio molto potente, ma messa insieme alla type-unsafety di cui abbiamo parlato prima diventa causa di bug pericolosi.

Uno dei costrutti più pericolosi in questo senso è dato dal casting tra tipi dipendenti, vale a dire appartenenti ad una stessa gerarchia di classi.

Un caso d’uso in cui si potrebbe manifestare una vulnerabilità di questo tipo è dato da un programma che fa uso di una classe A e due sottoclassi B1 e B2, con un metodo virtuale ciascuno. Definendo due oggetti di tipo (statico) A, è possibile effettuare il casting statico a tipo B1 o B2, indipendentemente dal tipo dinamico dell’oggetto in questione: il compilatore controlla che vi sia una dipendenza tra tipi, considerando questa conversione legale. Nessun altro controllo verrà effettuato a run-time.

A tempo di esecuzione, quando viene invocato il metodo virtuale dell’oggetto, viene effettuato un accesso alla vtable associata alla classe dell’oggetto o, per meglio dire, al suo tipo dinamico. Questo si traduce in type confusion che può portare a comportamenti indefiniti. Si pensi, ad esempio, alla situazione in cui il tipo dinamico non coincide con quello previsto dal programmatore. Di più: si pensi a cosa accadrebbe se il metodo virtuale di una delle due classi invocasse una shell.

Altri linguaggi

Sebbene la type confusion è una categoria di errori fortemente presente e sfruttata in C e C++, questi non sono gli unici linguaggi vulnerabili.

Ad esempio, sono noti attacchi a programmi PHP che sfruttano la type confusion per ottenere un crash del programma. Questo è facilmente ottenibile, in programmi che non validano l’input, passando dati di tipo diverso da quello previsto.

Sebbene in misura minore, in passato sono stati effettuati type confusion attack a programmi scritti in linguaggi sicuri come Java e Rust. Quest’ultimo, in particolare, diventa vulnerabile nel momento in cui si fa uso della FFI per utilizzare codice C/C++. Questo accade molto spesso nello sviluppo di grossi programmi come browser o sistemi operativi, che lasciano in questo modo spazio a type confusion attack di entità importante.

Conclusioni

La type confusion è tra le vulnerabilità più comuni all’interno del CVE, presente in programmi scritti in diversi linguaggi di programmazione.

Linguaggi fortemente tipati riescono a mitigare questo tipo di vulnerabilità tramite controlli a tempo di compilazione o di esecuzione, prevenendo rispettivamente la vulnerabilità e l’attacco che la sfrutta. Ad oggi, l’impiego di questi linguaggi risulta lo strumento di prevenzione migliore, riducendo fortemente il numero di vulnerabilità possibili.

I linguaggi che più comunemente presentano errori di type confusion sono il C e il C++. Programmi scritti in questi linguaggi sono stati spesso vittima di type confusion attack che miravano al denial of service o, peggio, al controllo dell’esecuzione del programma. Se l’utilizzo di linguaggi insicuri è necessario, si dovrebbe quanto meno cercare di utilizzare metodi di conversione di tipo dinamici. I controlli di tipo a run-time effettuati dal dynamic casting in C++ permettono di neutralizzare la minaccia della type confusion, andando a valutare il tipo dinamico dell’oggetto coinvolto.

Ulteriori contromisure sono costituite da strumenti di analisi statica e fuzzing disponibili per i linguaggi più diffusi, così come estensioni per i rispettivi compilatori.