Checking Security of Java Bytecode by Abstract Interpretation R. Barbuti - C. Bernardeschi - N. De Francesco Corso di Analisi e Verifica dei Programmi Carnielli Efrem Terenzoni Alessandro
Sommario Introduzione Il linguaggio JVML (ossia JVML0) La semantica operazionale di JVML Il modello per definire sicuro di JVML La semantica concreta di JVML La semantica astratta di JVML Considerazioni finali e sviluppi innerenti
Introduzione Si presenta un metodo per certificare come sicuro un sottoinsieme di comandi di Java Bytecode. Un metodo basato sulla interpretazione astratta della semantica operazionale del linguaggio.
Perché è importante che sia sicuro Java Bytecode?(1) Java Virtual Machine Bytecode Language (JVML) può essere scaricato da internet ed eseguito da un browser Java-compatibile. Lesecuzione del codice scaricato: - può richiedere laccesso ai dati privati dellutente; - può aver bisogno di informazioni in Internet che compromettono la riservatezza dei dati privati dellutente.
Perché è importante che sia sicuro Java Bytecode? (2) Quindi bisogna garantire la sicurezza dei dati privati dellutente in ogni forma di possibile utilizzo.
Perché è importante che sia sicuro Java Bytecode? (3) Alternativa possibile: lutente potrebbe fare la scelta di vietare che del codice possa accedere ai suoi dati personali, ma ciò comporterebbe la possibilità che il codice non verrebbe più eseguito correttamente.
Informazione sicura In base alla proprietà che riguarda il flusso sicuro di informazione (secure information flow property) si ha che linformazione con un dato livello di sicurezza (security level) non può essere utilizzata a un livello di sicurezza più basso.
Flusso sicuro Dato un programma in linguaggio ad alto livello nel quale ogni variabile è considerata sicura (security level) si ha un flusso sicuro (secure flow) dellinformazione se, alla terminazione del programma, il valore di ogni variabile al livello più basso non dipende dal valore della variabile con livello di sicurezza più alto.
Esempio Supponiamo che la variabile y ha un livello di sicurezza più alto della variabile x se scrivo: x:= y oppure if y=0 then x:=1 else x:=0 Si nota come alla fine dellesecuzione del codice il valore di x rivela il valore della variabile y, variabile con un livello di sicurezza più alto di x.
Falla nella sicurezza Possibile falla nella sicurezza (security leakage) che potrebbe accadere nei linguaggi ad alto livello si ha quando linformazione sicura (security level) viene rivelato il suo valore con il comportamento del programma eseguito.
Esempio while (y>0) do skip Se programma non termina per loop-infinito si capisce che la variabile y definita con alto livello di sicurezza, è maggiore di zero.
JVML Il linguaggio assembly utilizza uno stack. Larticolo considera un sottoinsieme del linguaggio. Definiremo una semantica estesa a piccoli passi del linguaggio considerando principalmente linfluenza che ha il flusso di informazione sullo stack.
Flusso di informazione Considerando il flusso di informazioni anche in un codice assembly bisogna tener conto del: - flusso di informazione esplicito - flusso di informazione implicito
Flusso di informazione esplicito Nel linguaggio ad alto livello si ha quando viene fatto un assegnamento. Esempio x:=y Il livello di sicurezza può essere definito dallespressione nella parte destra dellassegnamento. Nel codice assembly essendo il valore messo come push nello stack, una istruzione di pop non mostra la sorgente del valore.
Flusso di informazione implicito Nel linguaggio ad alto livello lambiente (scope) del flusso di informazione implicito può essere visto su comandi condizionali o comandi iterativi coincidente con lambiente del comando stesso. Esempio if y=0 the x:=1 else x:=0 Il linguaggio assembly non è strutturato. Lo stack può essere influenzato da un flusso implicito perché lo stack può essere manipolato in modi diversi attraverso i rami di una istruzione condizionale oppure può eseguire un diverso numero di operazioni push e pop in ordine differente.
Stato della macchina In JVML si deve parlare anche di stato della macchina. Il linguaggio assembly definisce una macchina astratta nel quale il programma eseguito può modificare lo stato della macchina che a sua volta può causare una modifica nel livello di sicurezza dellinformazione: ad esempio il contenuto del program counter quando il programma termina.
Il linguaggio JVML0
In JVML0 si ha un operand stack, una memoria contenente: le variabili locali, istruzioni aritmetiche semplici, salti condizionali e non condizionali
Insieme di istruzioni JVML0 op dove op {add, sub, …} Estrae due operandi dallo stack, esegue l operazione e mette il risultato nello stack pop estrae il valore in cima allo stack push k mette una costante k sullo stack load x mette il valore della variabile x nello stack store x estrae dallo stack e mette il valore nella variabile x
Insieme di istruzioni JVML0 if j estrae il valore dalla cima dallo stack e salta a j se non-zero goto j salta a j jsr j allindirizzo p, salta allindirizzo j e ritorna allindirizzo p+1 messo nelloperand stack ret x salta allindirizzo della variabile x halt istruzione di terminazione del programma
Insieme di istruzioni JVML0 Un programma è una sequenza c di istruzioni, numerate partendo dallindirizzo 1. Per ogni i {1,…,#c}, c[i] è listruzione allindirizzo i Noi assumiamo che il programma rispetti i seguenti vincoli statici, generalmente verificabili da un verificatore di codice: non si presenta nessun stack overflow o underflow, lesecuzione non salta ad un indirizzo indefinito.
Insieme di istruzioni JVML0 Data una sequenza di istruzioni c, denoteremo con Var(c) il nome delle variabili che occorrono in c.
Semantica operazionale di JVML0 La semantica operazionale descrive il significato di un linguaggio di programmazione specificando come il programma viene eseguito su una macchina astratta.
Semantica operazionale di JVML0 La semantica sarà data in termini di sistema di transizioni, dove il - cammino rappresenta la traccia di esecuzione e dove - i nodi mostrano che il programma cambia stato
Semantica operazionale di JVML0
La semantica standard del linguaggio definisce un insieme di regole …
Semantica operazionale di JVML0
Grafo del flusso di controllo (1) Dato un programma c composto di n istruzioni (#c=n), grafo di flusso di controllo di un programma è un grafo diretto (V,E) dove V={1,…,n+1} è un insieme di nodi e E sottoinsieme V V contiene gli archi (i,j) sse listruzione allindirizzo j può essere immediatamente eseguita dopo quella allindirizzo i, in più contiene larco (i,n+1) per ogni indirizzo i tale che c[i]=halt.
Grafo del flusso di controllo (2) Siano i e j nodi di grafo di flusso di controllo di un programma con n istruzioni. Diremo che il nodo j prevale su (postdominates) i, denotato da j pd i, se j i e j è in ogni cammino che parte da i. Diremo che il nodo j prevale immediatamente (immediate postdominates) i, denotato da j ipd i, se j pd i e non cè nessun nodo r tale che j pd r pd i. Useremo la notazione j=ipd(i). Per ogni nodo i n+1 tale che ipd(i) non esiste, scriveremo ipd(i)=n+1.
Esempio 3 load y 5 pop 6 store x 7 halt 4 if 6 2 push 01 push 1 j=ipd(i) 1 push 1 2 push 0 3 load y 4 if 6 5 pop 6 store x 7 halt i
Il modello per definire sicuro JVML0(1) Assumiamo un reticolo finito (L, ) dove L è un insieme di livelli di sicurezza, σ,τ … secondo un ordinamento parziale. Dati σ,τ L, σ τ indica il least upper bound di σ e di τ. σ τ significa che σ τ e σ τ.
Il modello per definire sicuro JVML0(2) Nel modello di sicurezza di JVML0 noi considereremo di annotare il programma dove ad ogni variabile verrà associato un livello di sicurezza. Un programma è una coppia (c,Λ) dove c è una sequenza di istruzioni e Λ è una partizione delle variabili in Var(c).
Il modello per definire sicuro di JVML0(3)
Il modello per definire sicuro di JVML0(4)
Il modello per definire sicuro di JVML0(5)
Il modello per definire sicuro di JVML0(6)
Esempi di σ-non secure programs
Semantica concreta Che cosè : la semantica concreta non è altro che l estensione della semantica operazionale appena vista, in modo che possa controllare violazioni di security. Come viene estesa: i valori trattati vengono arricchiti con un nuovo parametro, il livello di sicurezza. Durante la computazione di un programma il livello di sicurezza di un valore è dato dal LUB dei livelli di sicurezza dei flussi di informazione, sia esplicito che implicito, da cui il valore dipende. le istruzioni sono eseguite allinterno di un ambiente di sicurezza, che non è altro che un livello di sicurezza. In ogni momento dellesecuzione, lambiente di sicurezza rappresenta il lub dei livelli di sicurezza del flusso implicito. Lambiente di sicurezza può essere upgraded (incrementato) da un istruzioni di branching (IF e RET) e può essere downgraded alla terminazione del flusso implicito.
Semantica concreta Come trattare il flusso implicito La semantica si basa sul control flow graph per manipolare il flusso implicito. Il flusso implicito di una istruzione IF o RET allindirizzo i termina con listruzione allindirizzo ipd(i). Eseguendo la IF o la RET allinterno di un ambiente sicuro (cioè un ambiente a cui è stato assegnato un livello di sicurezza ), la semantica memorizza la coppia (ipd(i), ), e lambiente viene upgraded per considerare: per la IF il livello di sicurezza della condizione, cioè il valore al top dello stack degli operandi per la RET X il livello di sicurezza dellindirizzo memorizzato in x Quando il flusso implicito termina cioè listruzione allindirizzo ipd(i) è raggiunta, lambiente è reimpostato a, cioè al valore di sicurezza dellambiente in cui listruzione che ha generato il flusso implicito è stata eseguita. Per modellare istruzioni di branching innestati, la semantica registra la coppia (ipd(i), ) in uno stack, poiché, in presenza di istruzioni di branching innestati, il flusso implicito più interno termina prima. Lo stack per contenere tali valori è chiamato ipd stack.
Semantica concreta (k, ) è chiamato -valore e V è linsieme di questi valori. V = (V e x L) è il dominio dei valori concreti indicati come v,v,v1, … M : var V è il dominio delle memorie concrete, indicato come M,M,M1, … (cioè insieme delle funzioni che ad ogni variabile associano un valore) S =V* sono gli stack degli operandi, indicati come S,S,S1, … (sequenza finita di valori) R = (N x L)* indicati come Sono ipd stack. Le regole della semantica operazionale sono evidenziate nella slide successiva. Tali regole definiscono una relazione di transizione C x C, dove C è linsieme degli stati concreti. Introduciamo i domini della semantica concreta: I valori diventano coppie v = (k, )
Regole della semantica concreta
Semantica concreta Le regole assicurano che tutti gli elementi presenti nello stack degli operandi abbiano livello di sicurezza maggiore o uguale al livello di sicurezza dellambiente. E questo per il Lemma 1 che vedremo in seguito. Per mantenere il livello di sicurezza di un valore uguale al livello di sicurezza dellinformazione da cui dipende, la semantica modifica il livello di sicurezza di ogni valore inserito (pushed) sullo stack degli operandi, in accordo con il livello di sicurezza dellambiente. Le regole La JRS associa lindirizzo di ritorno inserito sullo stack con il livello di sicurezza dellambiente. La LOAD X assegna al valore inserito (pushed) sullo stack il LUB tra il livello di sicurezza di M(x) e lambiente. La PUSH assegna alla costante k il livello di sicurezza dellambiente (una costante non ancora usata può essere associata al minimo livello di sicurezza).
Semantica concreta Come già osservato un flusso implicito comincia allesecuzione di una IF o una RET. Le regole IF true e IF false applicate alla IF allindirizzo i: se il valore in cima (top) allo stack degli operandi (prossimo ad essere preso in considerazione) è (k, ) il ramo selezionato verrà eseguito sotto lambiente di con sicurezza. Inoltre (ipd(i), ) è inserito sul top dellipd stack ((ipd(i), ), ), dal quale sarà ripreso alla terminazione del flusso implicito. Assumiamo che non consenta linserimento di un indirizzo i sullipd stack se tale i è gia sulla cima dello stack. Infatti, se due o più istruzioni di branching innestate terminano allo stesso ipd, alla terminazione del flusso lambiente deve essere quello che era stato preso allentrata di quello più esterno. Lindirizzo i non deve essere nellipd stack Stato di partenza, con ambiente Stato finale con ambiente
Semantica concreta Le regole per la IF upgrade (incrementano) il livello di sicurezza di ogni valore che viene assegnato ad una variabile tramite una STORE nel più corto dei due rami: sia W = {x | c[j] = STORE x, e j appartiene ad un cammino del grafo di controllo che comincia da i e finisce in ipd(i), escludendo ipd(i)}. Per ogni, se M(x) = (k, ), allora upgrade M (M,i, )(x) = (k, ). Il livello di sicurezza delle variabili che non appartengono a W non è modificato dall upgrade M (M,i, ). Fare lupgrade della memoria in questo modo tiene in considerazione il fatto che una variabile può essere modificata in un ramo e non nellaltro. Anche lo stack degli operandi può essere influenzato dai flussi impliciti. Le regole infatti modificano anche il livello di sicurezza di ogni valore presente nello stack degli operandi applicando la funzione upgrade S (S, ): esso aumenta il valore di ogni variabile v in S al LUB di ed il livello di sicurezza attuale di v. La RET X, è trattata allo stesso modo, imposta il nuovo ambiente al LUB tra lambiente attuale ed il livello di sicurezza di x, e fa lupgrade della memoria in conformità con lo stack. Nellupgrade dello stack degli operandi allentrata del flusso implicito bisogna tener conto del fatto che lo stack può essere manipolato in modi diversi da rami diversi. Un ramo può eseguire un numero diverso di POP e PUSH, e con un ordine differente. Così anche elementi che non vengono usati in nessun ramo possono cambiare la loro posizione allinterno dello stack, a causa del flusso implicito. Questi elementi possono essere usati anche dopo la terminazione del flusso implicito cioè quando lambiente precedente è ripristinato; ma in questo modo dovrebbero tener traccia del fatto che sono influenzati dal flusso implicito. La nostra scelta è di fare lupgrade di tutti gli elementi dello stack degli operandi allentrata del flusso implicito.
Semantica concreta Vediamo più chiaramente tramite un esempio: quando listruzione c[6] = STORE X viene eseguita, in cima allo stack cè il valore 0 o 1, a seconda del ramo scelto dalla IF allindirizzo 4 che testa il valore della y. listruzione c[6] non appartiene allo scope del flusso implicito di valore piu alto generato dallistruzione c[4], e così è eseguita sotto lambiente trattenuto prima dellentrata nel flusso implicito che ha valore piu basso. in questo modo il flusso viene individuato come non sicuro tramite lupgrade dello stack degli operandi, allentrata del flusso implicito, che causa lassegnamento di un valore elevato a x dallistruzione c[6]. E NECESSARIO FARE LUPGRADE DELLO STACK !
Semantica concreta La regola dellIPD, unica nuova regola, aggiorna lambiente di sicurezza quando il flusso implicito termina, cioè quando listruzione che è ipd di una istruzione di branching eseguita precedentemente è raggiunta. La regola è applicata solo se il contenuto del program counter è presente sulla cima di cioè se il contenuto del program counter è i e i per qualche La regola non cambia il program counter, ma modifica solo lambiente, che è impostato a Dato un programma P = (c e una memoria M, la semantica concreta di P è il sistema di transizione C(P,M) = (C,, ) dove è il più basso livello di sicurezza, lo stato iniziale consiste nellindirizzo della prima istruzione, la memoria è quella data, lo stack degli operandi è vuoto ( ) e lipd stack anchesso vuoto( ). Nota che, se ignoriamo informazioni sulla sicurezza, la semantica concreta coincide con la semantica standard del linguaggio. Cosi può essere usata per eseguire il programma. Ora passiamo a dare alcune definizioni e risultati. Introdurremo alcune definizioni e dei teoremi che ci permettono di affermare che la tecnica che stiamo utilizzando è corretta.
Semantica concreta Tale lemma assicura, come era stato accennato prima, che in ogni stato con ambiente di sicurezza gli elementi nello stack degli operandi hanno livello di sicurezza maggiore o uguale a, mentre gli elementi nellipd stack hanno livello di sicurezza minore o uguale a e sono ordinati in maniera decrescente rispetto la sicurezza da top a bottom. Definiamo la nozione di safeness. una memoria è -safe per P se ogni variabile associata in P con un livello di sicurezza minore o uguale a, mantiene in M un livello di sicurezza minore o uguale a. uno stack degli operandi è -safe per P se contiene solo valori con livello di sicurezza minore o uguale a. Il seguente lemma esprime qualche proprietà della semantica concreta riguardante lo stack degli operandi e l ipd stack. Lemma 1. Dato P= e
Semantica concreta Definizione 5. Dato P= una memoria concreta M con D(M) = Var(c) è -safe per P se e solo se per ogni x allora Uno stack concreto S è -safe per P se e solo se Il seguente teorema afferma ladeguatezza della semantica concreta per la caratterizzazione di un flusso di informazione sicuro. Dato P=, consideriamo una memoria concreta M con D(M) = Var(c) in cui, per ogni, la variabile con livello ha -valore. Chiamiamo questa memoria – consistente. Teorema 1. Sia P= un programma e. Se ogni esecuzione terminante di P comincia da una memoria – consistente termina con uno stato <i,M,S tale che M e S sono -safe per P e, allora P è -sicuro. Più formalmente …
Semantica astratta In questa sezione presenteremo una semantica operazionale astratta in grado di controllare la security. Perché utilizziamo la semantica astratta: la semantica concreta non può essere usata come mezzo per lanalisi statica di una -security, perché mostra il flusso di informazioni di una particolare esecuzione, mentre la security fa riferimento a tutte le esecuzioni. il sistema di transizioni concrete potrebbe anche essere infinito. lo scopo dellinterpretazione astratta (o semantica astratta) è di approssimare correttamente la semantica concreta di tutte le esecuzioni in modo finito, in maniera da essere usata per costruire uno strumento utile nella pratica. Come otteniamo la semantica astratta: tale semantica è unastrazione della semantica concreta presentata nella precedente sezione: i valori concreti sono astratti considerando solo il loro livello di sicurezza e tralasciando la loro parte numerica. Tutte le altre strutture sono astratte di conseguenza.
Semantica astratta I domini astratti : V # = L (il nuovo dominio dei valori è il dominio delle security) M # = var V # (dominio delle memorie astratte) S # = (V # )* (operandi concreti ) Il dominio dellipd stack rimane invariato, gli stati astratti C #, sono analoghi a quelli concreti, ma in cui la memoria M viene sostituita con una memoria astratta M # M # e lo stack S sostituito con uno astratto S # S #. Le funzioni astratte (associano informazioni concrete a quelle astratte): la funzione astratta sui valori è V: V L, tale che V((k,s)) = (k tralasciato) la funzione astratta sulle memorie M: M M #, tale che ( M(M))(x) = V(M(x)) per ogni x D(M). la funzione astratta sugli stack, S : S S #, tale che ( S(S))[i] = V(S[i]), per ogni i {1,…,#S}. la funzione astratta sugli stati, C : C C # come segue:
Semantica astratta Le regole della semantica concreta vengono ridefinite sui nuovi domini. Le regole di IF e RET invece vengono modificate: E è linsieme degli archi del grafo di controllo del programma. Consideriamo le regole IF: nella semantica astratta entrambe le diramazioni sono esplorate per ogni condizione. Di conseguenza il sistema di transizioni astratte può contenere percorsi multipli. Le funzioni di upgrade sulla memoria e sullo stack sono le stesse viste nella semantica concreta, ma ridefinite sul dominio astratto.
Semantica astratta La relazione di transizione della semantica astratta è denotata con #. Dato un programma P ed una memoria astratta M #, il sistema di transizione astratto definito con le regole di astrazione è denotato con A(P, M # ) = (C #, #, ). Teorema 2: sia P = un programma e M # 0 $ M sia la memoria –consistente. Consideriamo lo stato finale di A(P, M # 0 ), cioè gli stati Se per ognuno di questi stati M #, S # sono mantenuti safe per P e, allora P è sicuro. Il teorema sopra è la base del nostro metodo di controllo della sicurezza: la dimostrazione della - safety di un programma P=(c, ) può essere fatta costruendo il sistema di transizione astratta che comincia da uno stato iniziale e con lambiente minimo in L, e in cui ogni variabile del programma è assegnata ad un livello di sicurezza secondo quanto specificato da Successivamente, gli stati finali sono esaminati secondo la - safety di memoria e di stack degli operandi, e secondo il valore degli ambienti. Nota che il sistema di transizione astratto è finito. Infatti, essendo livelli di sicurezza, gli ambienti e i valori astratti finiti, anche la memoria astratta è finita. Inoltre, gli ipd stack sono finiti poiché contengono al più tutti gli indirizzi. Infine gli stack degli operandi astratti sono finiti perché assumiamo stack limitati.
Related Work (sviluppi innerenti) In questa ultima parte quanto appena visto finora verrà messo brevemente in relazione con altri lavori che si prefiggono anchessi di effettuare un controllo sulla sicurezza di un linguaggio. Il tema della sicurezza dei linguaggi è molto diffuso e le proprietà del concetto di flusso di informazioni sicuro dei programmi è stato via via formulato in maniera sempre più precisa. Le soluzioni finora affrontate si possono dividere in due categorie: lapproccio basato sui tipi (type-based). lapproccio basato sulla semantica (semantic-based).
Type-based questa tecnica prevede che le informazioni sulla sicurezza di ogni variabile appartengono al corrispondente tipo ed il flusso delle informazioni di sicurezza del sistema è controllato dal significato del sistema di tipi. viene introdotto il concetto di non-inferenza, che prevede di considerare gli stati in cui il valore finale di ogni variabile non dipende dal valore iniziale delle variabili a più alti valori di sicurezza. in alcune versioni più forti la non inferenza include anche lassenza di un flusso non sicuro dovuto alla non terminazione. nelle nostre impostazioni tale proprietà è posseduta se il programma è – sicuro per ogni la non inferenza controlla che nellultimo stato del sistema di transizione astratta ogni variabile in abbia un livello di sicurezza minore o uguale a lavori recenti che seguono questo tipo di approccio considerano linguaggi funzionali e a cui danno un sistema di tipi; e altri in cui il linguaggio Java viene esteso dando ai programmi annotazioni sul flusso di informazioni.
Semantic-based E lapproccio che abbiamo visto finora. alcuni lavori definiscono un interpretazione astratta per il controllo del flusso di informazioni in linguaggio imperativo ad alto livello, tale metodo è basato sulla semantica denotazionale del linguaggio. Si parte, come abbiamo visto, col definire una semantica concreta che poi viene sviluppata ottenendo uno strumento statico per lanalisi. La differenza è che noi utilizziamo la semantica operazionale, il vantaggio nellusare l operazionale è che essa definisce una macchina astratta e così permette di tenere informazioni che non sono legate al comportamento del programma in input e output, ma riguardano gli stati della macchina durante lesecuzione altri lavori prevedono un controllo della sicurezza basato sulla semantica assiomatica un altro approccio può essere basato su relazioni di equivalenza parziale ( che però ha lo svantaggio di essere difficilmente reso automatizzabile) PROBLEMA: lutilizzo di tecniche così diversificate comporta che molti programmi che possono considerarsi sicuri per un metodo non lo possano essere per un altro.
Typed vs Semantic-based Il vantaggio dellapproccio semantico è che in generale consente di certificare la sicurezza di una larga classe di programmi. Lo svantaggio è che la semantica richiede lesecuzione del programma anche se in maniera astratta, e questo aumenta la complessità dellanalisi in termini di spazio e tempo che può essere elevata per programmi di notevoli dimensioni, che possono essere invece ben gestiti da sistemi di tipi poiché trattengono poche informazioni. Progetti futuri : estensione del metodo proposto per consentire lutilizzo di tale approccio allintero Java Bytecode.