Home > java > Sicurezza in Java: il Verifier

Sicurezza in Java: il Verifier

8 novembre 2010
Translate in English with Google Translate

In questo articolo ci concentreremo sul Verifier per capire meglio a cosa serve e come funziona.
Come abbiamo visto in un precedente articolo, l’esecuzione di una classe Java richiede il compimento di 3 operazioni:

  1. caricamento della classe ad opera del ClassLoader
  2. controllo del codice ad opera del Verifier
  3. controllo dei permessi ad opera del SecurityManager

Il Verifier opera a stretto contatto con il ClassLoader, difatti la classe, dopo che è stata caricata dal ClassLoader, viene controllata dal Verifier per assicurarsi che sia corretta. Qualora venga riscontrato un errore, verrà rilanciata una eccezione.
Il compilatore Java non dovrebbe generare un bytecode malformato ma potrebbero verificarsi delle situazioni tali da determinare degli errori, per esempio:

  1. un baco nel compilatore: il compilatore, come tutti i software è soggetto a bachi, per quanto la Sun/Oracle effettui continui controlli. Inoltre bisogna considerare che i compilatori Java sono prodotti anche da altre enti/organizzazioni. Possiamo ricordare: JRockit precedentemente di BEA ora ORACLE, poi IBM J9 della “Big Blue” ed ancora “Apache Harmony” della “Apache Software Foundation” ed altre ancora.
  2. un tentativo di hacking: è sempre possibile modificare un file class usando un editor esadecimale in modo tale da eseguire delle operazioni pericolose oppure è possibile utilizzare un assemblatore che consenta di generare un proprio bytecode.

Funzionamento della JVM

I controlli che vengono effettuati fanno riferimento alle istruzioni del bytecode che devono essere eseguite. L’esecuzione delle istruzioni (opcode) nella JVM avviene ad opera di un Thread della JVM per il quale viene creato un Java Stack al quale vengono assegnati dei frame ossia degli spazi di memoria. Per l’esecuzione di ogni metodo di una classe è asseganto un frame nella JVM nel quale vengono memorizzate variabili e risultati intermedi di elaborazioni dette operand stack. Le istruzioni della JVM sono molto simili alle istruzioni utilizzate dal linguaggio assebly.
Ogni linea di istruzione è composta da:

  1. una istruzione di 1 byte detta opcode
  2. eventuali parametri detti: operand

Il risultato dell’elaborazione viene inserito nello operand stack oppure nella local variable.

Struttura del bytecode

Il bytecode è costituito da uno stream di bytes a 8 bit memorizzati nell’ordine big-endian ( Most Significant Bit ).
Le informazioni sono memorizzate in sequenze di bytes unsigned di 1/2/4 bytes (u, u2, u4).
La struttura del bytecode è schematizzato in questo modo:


ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

Controlli bytecode

I controlli da effettuare sono stabiliti nella JSR 202 del Java Comunity Process.
I controlli effettuati devono accertare che siano stati rispettati i vincoli di una classe che sono di tipo:

  1. statici: attengono alla corretta formattazione della classe. In particolare si riferiscono alla corretta presentazione delle istruzioni della JVM ( opcde ) ed a loro argomenti ( operand )
  2. strutturali: attengono alla corretta relazione esistente tra le istruzioni della JVM.

Il processo di verifica si divide in 4 fasi:

  1. fase 1: verificare la struttura interna del file class, alcuni esempi:
    • magic: indica il magic number delle classi Java. Il magic number viene solitamente inserito ad inizio file ed è composto da una sequenza di bit e serve per stabilire il formato del file. Nel caso di un file class questo valore in esadecimale è rappresentato da 4 bytes: CA FE BA BE. La leggenda vuole che James Gosling abbia scelto questi caratteri riferendosi alla cameriera del locale dove andava a pranzo a St Michael’s Alley: Cafe Babe!
    • minor_version, major_version: in base alla versione del compilatore Java viene riportata la versione. Java 1.5 riporta la versione 49 mentre Java 1.6 il valore è 50. In Java 1.7 il valore sarà 51.
  2. fase 2: ulteriori controll:
    • le classi final non siano state ereditate
    • tutte le classi devono avere un padre (tranne Object )
    • siano rispettate le regole x il constant-pool e le entry abbiano nomi corretti
  3. fase 3: controllo del contenuto dei metodi attraverso operazioni di controllo dette:
    • type checking: utilizzato a partire dalla versione 50, quindi da Java 1.6.Questo metodo basa sul modello conosciuto come “Proof-carrying code” ossia il codice compilato porta con se delle regole di validazione che quando vengono eseguite determinano se il codice presente è sicuro. Questo tipo di controllo permette delle migliori prestazioni in termini di CPU (riduzione del 50%) e minor uso della memoria, consentendo di verificare le classi in tempi molto brevi.
    • type inference: utilizzato fino alla versione 49, quindi fino a Java 1.5. Questo metodo si basa sulla “Data-Flow Analisis” ossia l’analisi dei tipi in base ai valori associati: la somma di 2 interi darà come risultato un intero non una stringa. Questo tipo di controllo è molto impegnativo in termini di CPU e memoria e comporta un grosso overhead in sede di verifica infatti richiede l’utilizzo di molta memoria per la rappresentazione del flusso dei dati da analizzare e risulta particolarmente lento in caso di verifica di istruzioni ricorsive.
  4. fase 4: viene effettuato un controllo in merito all’accesso ai metodi/campi:
    • Assicurarsi che i metodi/campi referenziati esistano
    • Si disponda delle si abbiano gli accessi ai metodo/campi

Limiti della JVM

In base alle caratteristiche della struttura del bytecode e alle dimensioni assegnate ai suoi componenti, si evidenziano delle limitazioni della JVM.

  • 65536 è il numero massimo di
    • metodi definiti in una classe
    • interfacce implementabili
    • entry nel constant pool
    • variabili locali nei metodi
    • caratteri utilizzabili per identificare attributi, metodi, classi e interfacce
  • 255 è il numero massimo di:
    • dimensioni di un array
    • parametri di un metodo

Nell’articolo seguente Crack the Verifier vedremo un esempio.

Categorie:java
  1. Non c'è ancora nessun commento.
  1. 18 novembre 2010 alle 7:32 PM
I commenti sono chiusi.
%d blogger cliccano Mi Piace per questo: