INFORMATICA

Analisi del codice, nuova frontiera dell’intelligenza artificiale

Errori e criticità del software sotto la lente degli informatici alle prese con l’esplosione di cyberattacchi: dai malware agli spyware diffusi sugli smartphone. Ecco come i nuovi sistemi di AI e machine learning stanno rivoluzionando il settore

28 Mag 2020
Giuseppe Antonio Di Luna

Post-doc Sapienza Università di Roma

Leonardo Querzoni

Ricercatore Sapienza Università di Roma

teoria dell'argomentazione

Le tecniche di analisi del codice trovano larga applicazione nel campo della sicurezza informatica, permettendo di identificare vulnerabilità e funzionalità non desiderate anche in software in uso in ambienti critici. I nuovi traguardi raggiunti dall’Intelligenza artificiale mettono a disposizione nuovi strumenti, le cui potenzialità sono in larga parte ancora da esplorare. Vediamoli.

Cos’è l’analisi del codice

L’analisi del codice rappresenta da sempre uno degli approcci fondamentali a disposizione dell’ingegnere informatico per controllare la correttezza del software e migliorarne le prestazioni. Attraverso l’analisi del codice è possibile identificare quali parti presentano errori che causano comportamenti indesiderati, oppure quali sezioni del codice rappresentano delle criticità prestazionali. Nello specifico essa permette di rispondere a vari interrogativi, quali capire l’esatta funzionalità implementata da un software o da parte di esso; comprendere le funzionalità aggiunte in diverse versioni dello stesso software; analizzare i software al fine di trovare porzioni comuni di codice sorgente.

Come si svolge l’analisi

Tipicamente l’analisi viene svolta sul codice sorgente del software, ovvero sulla sua versione compilata. Il codice sorgente costituisce la rappresentazione iniziale del software, così come descritta dal programmatore, ed in quanto tale è tipicamente espressa attraverso un linguaggio di programmazione noto e di semplice interpretazione da parte dell’analista.

Il processo di analisi in questo caso si espleta attraverso l’ispezione del codice sorgente. Il codice compilato (o codice binario) è costituito dal prodotto del processo di compilazione che traduce il codice sorgente in un linguaggio direttamente interpretabile dall’architettura di riferimento per l’esecuzione del software (ad esempio, l’architettura x64).

Tale codice, comunemente anche chiamato codice macchina[1], è pensato e progettato per essere interpretabile ed eseguibile direttamente, nel modo più efficiente possibile, da un microprocessore. In quanto tale, la sua analisi è notevolmente più complessa!

Il linguaggio macchina non è pensato per essere interpretato da un essere umano, e quindi non presenta costrutti facilmente comprensibili. In particolare, ciascuna istruzione del linguaggio di programmazione originale può essere tradotta, durante il processo di compilazione, in molte istruzioni del linguaggio macchina di destinazione.

La combinazione di queste può cambiare notevolmente a seconda di come il processo di compilazione viene svolto, ed inoltre, ogni riferimento simbolico usato dal programmatore originale (nomi associati ai dati, commenti, documentazione del codice) viene tipicamente cancellato dal processo di compilazione, dato che tali riferimenti non sono utili, né hanno alcun significato, per il microprocessore che dovrà eseguire le istruzioni.

Infine, dato il codice sorgente di un programma, è possibile da questo ottenere codici compilati notevolmente diversi, a seconda di quale architettura viene utilizzata come riferimento per la successiva esecuzione del programma. Infatti, diverse architetture utilizzano linguaggi macchina notevolmente dissimili; l’analista che vuole interpretare il codice compilato dovrà quindi assicurarsi di conoscere lo specifico linguaggio macchina utilizzato dall’architettura per cui il software oggetto dello studio è stato compilato.

Operativamente l’analisi può svolgersi secondo due approcci principali:

  • Analisi statica
  • Analisi dinamica

Le tappe dell’analisi statica

In questo primo caso, il software oggetto di studio viene analizzato senza che il suo codice venga eseguito. L’analisi cerca di identificare specifici costrutti presenti nel codice che potrebbero portare in fase di esecuzione a comportamenti indesiderati. Uno dei vantaggi di questa analisi è quello di non necessitare di ambienti controllati (c.d. sandbox) per l’esecuzione di codice dubbio. Purtroppo, l’analisi statica non può dare risposte soddisfacenti quando il codice da analizzare interagisce con elementi esterni (ad esempio un web-service) il cui comportamento è sconosciuto. Inoltre, sono note in letteratura moltissime tecniche adatte ad offuscare il codice in modo da rendere inefficace o impraticabile l’analisi statica.

Come si esegue l’analisi dinamica

WEBINAR
Blockchain, Iot, AI per una Supply Chain intelligente e più efficiente
Blockchain
Intelligenza Artificiale

Questo secondo approccio prevede che il software venga eseguito, completamente o in alcune sue parti, per poterne osservare i diversi comportamenti ed effetti a fronte di diversi stimoli. In questo modo è possibile studiare direttamente gli effetti indesiderati ricercati. Laddove questi effetti avessero conseguenze negative sul sistema di analisi o su altri sistemi, l’analisi deve essere svolta con particolare cautela.

Nello specifico qualora il codice analizzato sia un software dannoso (ad esempio un virus, o più genericamente un malware), l’esecuzione deve avvenire in particolari ambienti controllati (le sandbox precedentemente citate).

Applicazioni nella security

Più recentemente l’analisi del codice ha trovato applicazione anche nel campo della sicurezza informatica, come metodo per identificare problematiche di sicurezza nel software.

Ad esempio, una delle maggiori minacce digitali è oggi rappresentata dal malware. Questi software diventano giorno dopo giorno più pervasivi e furtivi. Data la loro capacità di rimanere inosservati, il malware può nascondersi, vivo, all’interno di un sistema per mesi o anni (Stuxnet è un esempio celebre: rilevato nel 2010 ma probabilmente sviluppato nel 2005).

Alcuni malware, ad esempio i rootkit, sono pensati per modificare gli elementi fondamentali di un sistema operativo e rimanere non rilevabili mentre rubano dati da esso. Recentemente, i malware che attaccano i dispositivi mobili sono diventati una grave minaccia alla privacy degli utenti. I media sempre più spesso riportano notizie di spyware installato negli smartphone ed utilizzato per spiare messaggi, foto o chiamate degli utenti. A tal proposito, occorre evidenziare una recente scoperta di Google Zero; il team ha rivelato una complessa operazione che, per anni, ha compromesso la sicurezza di migliaia di dispositivi iOS (iphone and ipad) installando malware di monitoraggio tramite sofisticate tecniche di attacco.

Incredibilmente, tale operazione non era rivolta ad un singolo obiettivo di alto profilo, come di solito avviene quando vengono messe in campo tecniche di attacco particolarmente sofisticate e costose, ma colpiva indiscriminatamente qualsiasi utente visitasse determinati siti web. Dato l’uso diffuso di dispositivi mobili e le loro particolari esigenze di basso consumo energetico, individuare malware su di essi è molto difficile. Le tecniche di analisi del codice permettono di identificare tali software quando penetrano all’interno del dispositivo evitando che l’infezione vada potenzialmente a buon fine.

Firmware di sistemi embedded

Altro campo di particolare interesse è l’analisi di firmware di sistemi embedded. In questo ambito è di fondamentale importanza capire quanto un nuovo firmware si discosti dal precedente, per analizzare puntualmente le nuove funzionalità introdotte al fine di escludere l’introduzione, volontaria o meno, di criticità: bug, backdoor o semplicemente funzionalità non desiderabili (ad esempio, un server telnet utilizzato dal Vendor quale canale di manutenzione). Le tecniche di analisi del codice sono in questo contesto utilizzate ampiamente per il controllo del software fornito dai produttori, anche al fine di certificazione.

In entrambi gli esempi proposti l’analisi si applica tipicamente al codice compilato (dato che il codice sorgente spesso non è disponibile) e presenta molte delle complessità precedentemente illustrate (presenza di tecniche di offuscamento, eterogeneità delle architetture, etc.). L’altissimo livello di competenza necessario per condurre in questi casi un’analisi approfondita ne aumenta enormemente i costi, limitando di conseguenza l’applicazione di questi approcci su larga scala.

L’analisi del codice binario

Il moderno trend del Big Code[2] mira ad applicare al codice tecniche di Machine Learning (ML) per costruire modelli probabilistici del codice. Ossia, creare strumenti che permettano di risolvere problemi in maniera a volte imprecisa: vi è infatti una certa probabilità di errore, ma veloce. Questa imprecisione non è di per sé uno svantaggio. Infatti, alcuni problemi o non possono essere sempre risolti, o non possono essere definiti, esattamente.

Si pensi al problema di decidere se due codici (binari o meno) hanno lo stesso identico comportamento: in generale questo non può essere sempre deciso in maniera corretta. Uno strumento probabilistico tiene conto di questa difficoltà, ad esempio esso può fornire risposte scorrette in alcuni casi patologici. Inoltre, in molti casi pratici, siamo interessati a problemi che hanno una formulazione imprecisa: si pensi al problema di capire se due codici hanno un comportamento simile. In questo caso non è possibile, o generalmente non si vuole, definire cosa vuol dire comportamento simile; un modello probabilistico è lo strumento migliore per affrontare questo genere di problemi.

Il Big Code è possibile grazie allo sterminato patrimonio di codice open-source oggi disponibile. Esso infatti permette di collezionare facilmente quantità ragguardevoli di codici sorgenti, che possono essere poi compilati per ottenere codice binario su cui addestrare modelli di Machine Learning. E’ infatti noto che il deep learning (ossia l’utilizzo di Deep Neural Networks – DNN) per essere efficace ha bisogno di dataset di grandi dimensioni. E sono infatti proprio le tecniche che fanno parte della grande famiglia del deep learning ad essere quelle più interessanti.

La loro dimostrata capacità di modellare il linguaggio naturale, e la vicinanza tra codice e linguaggio naturale danno una forte motivazione al loro uso. Inoltre, esse offrono un’alta flessibilità permettendo la creazione di modelli supervisionati o non-supervisionati.

Addestrare le Deep Neural Networks

Una possibile soluzione è quella di addestrare le DNN per creare rappresentazioni vettoriali di funzioni binarie. Ossia, addestrarle per convertire funzioni scritte in codice binario in una rappresentazione vettoriale (un “array” numeri reali) che condensi gli aspetti sintattici e semantici del codice stesso. Tali rappresentazioni, chiamate embedding, possono essere utilizzate come input di altri algoritmi di ML, e allo stesso tempo hanno anche un valore intrinseco dovuto alla struttura che impongono nello spazio metrico (ossia, embedding vicini solitamente rappresentano funzioni simili per sintassi o semantica).

Tali embedding possono essere poi utilizzati come input per altri modelli di Machine Learning. Questo permette di creare sofisticati sistemi che permettono di risolvere svariati problemi.

Il movimento del Big Code ha già analizzato l’applicabilità delle tecniche di ML al codice sorgente. Pochi lavori hanno però applicato queste tecniche ai binari, e si sono focalizzati principalmente su singoli task. Ad esempio, svariate soluzioni sono state proposte per risolvere il problema della binary similarity, ossia stabilire se due codici compilati provengono dallo stesso codice sorgente. Queste soluzioni si possono dividere in soluzioni che utilizzano tecniche basate su embedding e soluzioni che non le usano. Le soluzioni che non usano embedding sono di solito applicabili soltanto a coppie di funzioni. Ossia, per valutare la similarità di due funzioni bisogna far partire un algoritmo che prende come input entrambe.

Questo algoritmo, nella gran parte dei casi, utilizza come misura di similarità o un mapping fuzzy del Control Flow Graph dei due binari, oppure divide le funzioni in piccole porzioni di codice chiamati strand (intuitivamente, uno strand è la catena più piccola di istruzioni che calcola il valore di una determinata variabile) e poi cerca di fare un mapping tra strand simili. Lo svantaggio di queste tecniche è che devono eseguire un algoritmo di similarità su ogni coppia di funzioni, impiegando molto tempo.

Machine learning: altre applicazioni

Altre tecniche sono quelle basate su embeddings. In queste tecniche ogni funzione viene singolarmente data come input ad un algoritmo. L’algoritmo da in output un vettore di numeri, e la similarità tra due funzioni viene calcolata con la distanza tra i due vettori corrispondenti. Il vantaggio di queste tecniche è che si possono pre-calcolare i vettori di un grande numero di funzioni, e quando una funzione nuova viene rilevata si può capire velocemente se è simile a qualcosa di già visto facendo un lookup su un database di vettori. Lo stato dell’arte sono SAFE[3], per funzioni stripped, e ASM2VEC[4] per funzioni non stripped.

Altro task applicativo in cui tecniche di Machine Learning iniziano oggi ad essere utilizzate con successo è il cosiddetto compiler provenance, ossia ricostruire la toolchain che ha prodotto un codice compilato. Questo è un problema ampiamente affrontato in letteratura, ma è stato sempre stato affrontato su sistemi Windows e Unix, mai su sistemi embedded. Tale problema è stato affrontato sia utilizzando tecniche di Machine Learning classiche, sia DNN. Le tecniche hanno mostrato una buona attitudine nel riconoscere il Compiler (GCC piuttosto che CLANG), la versione e i parametri di ottimizzazione. Non è noto come queste tecniche si comportino su firmware embedded, e se sia possibile ricostruire altri possibili parametri di compilazione.

Il futuro dell’analisi del codice

Le tecniche di apprendimento automatico stanno oggi rivoluzionando molti campi applicativi, dalla generazione automatica del testo, all’interpretazione automatica dei flussi video. Non stupisce quindi che anche il campo dell’analisi del codice stia oggi sperimentando se e come queste tecniche possano apportare miglioramenti a processi che oggi mostrano limiti nella loro applicazione su larga scala. I risultati ad oggi conseguiti, seppur iniziali, sembrano promettenti e motivano i notevoli sforzi che il mondo della ricerca sta oggi facendo in questo campo.

  1. In questo articolo, per semplicità, assumiamo che il codice macchina equivalga alla rappresentazione del software oggetto di studio in linguaggio Assembly. In realtà l’analisi del codice compilato si svolge spesso su quest’ultimo formato e non sul linguaggio macchina.
  2. Miltiadis Allamanis, Earl T. Barr, Premkumar Devanbu, and Charles Sutton. 2018. A Survey of Machine Learning for Big Code and Naturalness. ACM Comput. Surv. 51, 4, Article 81 (July 2018).
  3. L. Massarelli, G. A. Di Luna, F. Petroni, R. Baldoni, L. Querzoni: SAFE: Self-Attentive Function Embeddings for Binary Similarity. DIMVA 2019: 309-329.
  4. S. H. H. Ding, B. C. M. Fung, P. Charland: Asm2Vec: Boosting Static Representation Robustness for Binary Clone Search against Code Obfuscation and Compiler Optimization. IEEE Symposium on Security and Privacy 2019: 472-489.
DIGITAL EVENT 13 OTTOBRE
Customer experience: tutti i vantaggi offerti dai processi di Digital Identity
CRM
Intelligenza Artificiale

@RIPRODUZIONE RISERVATA

Articolo 1 di 4