MokaByte
Numero 21 - Luglio 1998
|
|||
|
III parte |
||
Lorenzo Bettini |
|
||
Sicurezza nel jdk 1.1
Ogni computer connesso alla
rete potenzialmente può essere attaccato dall’esterno. Questo è
ancora più vero nel caso delle applet, in cui del software viene
scaricato dalla rete. Del resto il codice binario delle applet viene scaricato
automaticamente quando si accede tramite browser ad una pagina web che
le contiene, quindi non si ha nemmeno il tempo di decidere (a meno che
non si imposti nelle opzioni del browser di non scaricare le applet Java).
Questi pericoli non si limitano alle
applet, ma si riscontrano anche se si scrivono applicazioni distribuite che
fanno uso di codice mobile cioè scaricano a run time dalla rete
del codice di cui non vi è traccia sul computer locale. Nei due precedenti
articoli (che vi consiglio di tenere alla mano) abbiamo visto come implementare
agenti mobili in Java. Effettivamente quando un agente viene mandato in esecuzione
sul server, può potenzialmente eseguire qualsiasi operazione (si ricordi
che per le applicazioni non vengono applicate le restrizioni delle applet),
anche cancellare file sul file system locale o eseguire system call.
Essendo Java un linguaggio
molto adatto alle applicazioni distribuite, e permettendo il loading dinamico
(cioè a run-time) delle classi, deve mettere a disposizione un meccanismo
per gestire la sicurezza e proteggersi dal codice remoto.
In questo articolo vedremo
come aggiungere dei meccanismi di sicurezza sull'AgentServer in
modo da evitare che un agente ricevuto dalla rete danneggi il sistema locale.
Il modello SandBox
Il meccanismo fondamentale
è costituito dal modello sandbox, un ambiente in cui il codice
può compiere solo un numero limitato di operazioni che accedono
a risorse di sistema come file e connessioni in rete. Secondo questo modello
le applicazioni (codice locale, cioè trusted) non sono soggette
a restrizioni, mentre le applet (codice scaricato dalla rete, quindi untrusted),
essendo eseguite all’interno della sandbox, possono accedere solo ad un
numero limitato di risorse.
Questo modello si basa fondamentalmente
sull’idea che "prevenire è meglio che curare": di solito una volta
che un virus si è diffuso nel sistema è difficile poterlo
fermare e comunque il sistema non è più da ritenersi sicuro
(anche per un sistema Unix, una volta penetrato da un hacker, sarebbe necessaria
una nuova installazione). In questo modo tramite la sandbox si proibisce,
fin dall’inizio, di compiere alcune azioni potenzialmente pericolose a
codice non fidato.
La sicurezza riguarda diversi
aspetti dell’architettura di Java: le varie parti dell’architettura daranno
il proprio contributo garantendo che alcuni programmi non riescano a compiere
azioni dannose. Queste parti sono:
Il Security Manager
Si è visto che tramite
il linguaggio ed il class verifier è possibile "scartare" codice
strutturalmente non corretto e potenzialmente pericoloso; col class loader
inoltre è possibile creare name space distinti, evitando
così che gli oggetti, appartenenti a name space diversi possano
interferire l’uno con l’altro. Tuttavia il sistema è ancora aperto
ad azioni legali ma potenzialmente dannose, come l’accesso al file system
o alle system call. Questo non riguarda le applet, che possono compiere
solo un numero limitato di azioni, e fra queste non ci sono quelle appena
citate, ma le applicazioni sulle quali non è attiva nessuna restrizione.
Il Security Manager permette
di definire, e quindi di personalizzare, i limiti (nel senso di confini)
della sand box. In questo modo si può definire le azioni che possono
essere effettuate da certe classi o thread, e impedire che ne vengano compiute
altre.
Per creare un Security Manager personalizzato
si deve derivare dalla classe SecurityManager, una classe astratta appartenente
al package java.lang. Un Security Manager è quindi programmato
direttamente in Java. Una volta installato dall’applicazione corrente, tale
Security Manager effettuerà un monitoraggio continuo sulle azioni svolte
(o meglio che sarebbero svolte) dai vari thread dell’applicazione: le API Java
chiedono al Security Manager attivo il permesso di compiere certe azioni, potenzialmente
pericolose, prima di eseguirle effettivamente. Per ogni azione di questo tipo
esiste un metodo nella classe SecurityManager della forma checkXXX,
dove XXX indica l’azione in questione, che viene richiamato prima di
compiere tale azione. Ad esempio il metodo checkRead viene chiamato dalle
API di Java prima di eseguire un’azione di lettura su un file, mentre checkWrite
prima di eseguire un’azione di scrittura su un file.
L’implementazione di questi
metodi stabilisce la politica di sicurezza che verrà applicata all’applicazione
corrente; quindi i metodi checkXXX stabiliscono se il thread corrente
può compiere l’azione descritta da XXX. Si tenga comunque
presente che un Security Manager non riesce ad impedire l’allocazione di
memoria e lo spawning di thread; questo vuol dire che non riesce a controllare
i cosiddetti attacchi denial of service in cui si impedisce l’utilizzo
del computer sommergendolo di richieste (esecuzione di processi e allocazione
di tutta la memoria). In questi casi si dovrebbero utilizzare tecniche
e controlli costruiti ad hoc.
Alla partenza un’applicazione
non ha nessun Security Manager installato, ed è quindi aperta a
qualsiasi tipo di azione, in quanto non viene applicata nessuna restrizione
sulle azioni. Come già detto questo non è il caso delle applet:
il browser installa automaticamente un Security Manager, che proibisce
diverse azioni alle applet. Una volta installato il Security Manager rimarrà
attivo per tutta la durata dell’applicazione e ovviamente, per motivi di
sicurezza, non sarà possibile installare, durante il corso dell’applicazione
un altro Security Manager (pena una SecurityException). Questo è
consistente col fatto che un’applet non può installare un proprio
Security Manager (altrimenti la sicurezza non sarebbe più garantita,
in quanto l’applet potrebbe concedere a se stessa qualsiasi tipo di azione).
Per installare un Security Manager basterà eseguire le seguenti
istruzioni:
try {Tipicamente un metodo checkXXX ritorna semplicemente se l’azione viene permessa, mentre lancia una SecurityException in caso contrario; ad esempio un’implementazione di un metodo checkRead potrebbe essere la seguente:
System.setSecurityManager( new mySecurityManager(...) ) ;
} catch ( SecurityException se ) {
System.err.println( "Sec.Man. già installato!" ) ;
}
public void checkRead( String filename ) {
if ( azione non permessa )
throw new SecurityException( "Lettura non permessa" ) ;
}
Quindi quando viene invocato
un metodo che utilizza un’API di Java, viene controllato se è installato
un SecurityManager, ed in caso positivo viene richiamato il metodo check
opportuno; ad esempio un’idea di quello che può compiere un’API,
prima di uscire dall’applicazione corrente è:
...
SecurityManager secMan = System.getSecurityManager() ;
if ( secMan != null ) {
secMan.checkExit( status ) ;
}
esegue le azioni per terminare l’applicazione corrente
Nella seguente tabella
sono illustrati i vari metodi presenti nella classe SecurityManager
relativi alle varie azioni che si possono compiere su determinate risorse
di sistema (si rimanda alla documentazione in linea per una descrizione
completa dei vari metodi):
sockets checkAccept(String host, int port) checkConnect(String host, int port) checkConnect(String host, int port, Object executionContext) checkListen(int port) threads checkAccess(Thread thread) checkAccess(ThreadGroup threadgroup) class loader checkCreateClassLoader() file system checkDelete(String filename) checkLink(String library) checkRead(FileDescriptor filedescriptor) checkRead(String filename) checkRead(String filename, Object executionContext) checkWrite(FileDescriptor filedescriptor) checkWrite(String filename) system commands checkExec(String command) interpreter checkExit(int status) package checkPackageAccess(String packageName) checkPackageDefinition(String packageName) properties checkPropertiesAccess() checkPropertyAccess(String key) checkPropertyAccess(String key, String def) networking checkSetFactory() windows checkTopLevelWindow(Object window) |
Ad esempio se si volesse
proibire ad una certa classe (e alle sue derivate) la possibilità
di accedere in scrittura al file system basterà implementare
public void checkWrite(FileDescriptor fd) {Oppure se lo si vuole proibire alle classi caricate da un class loader personalizzato (ad esempio che sono state scaricate dalla rete):
if (Thread.currentThread() instance of NomeClasse )
throw new SecurityException();
}
public void checkWrite(FileDescriptor fd) {La sicurezza nel package Agent: la classe AgentSecurityManager
if (Thread.currentThread().getClass().getClassLoader()
instanceof NetworkClassLoader)
throw new SecurityException();
}
public void checkWrite(String file) {Dove indicativamente il metodo notTrustedProcess() è così implementato:
if ( notTrustedProcess() )
throw new SecurityException();
}
protected boolean notTrustedProcess() {Quindi per capire se si tratta di un processo (agente) ricevuto dalla rete, basterà controllare se è stato caricato con un class loader diverso da quello primordiale. Si è detto indicativamente, perchè in realta sono altri i controlli da effettuare.
if ( Thread.currentThread().getClass().getClassLoader() != null )
return true ;
return false ;
}
Effettivamente è stato necessario modificare alcune classi del package (questa volta non si tratta di una svista, è che inizialmente non pensavo di aggiungere un Security Manager al package). Infatti così come è adesso il pacchetto il controllo sopra non avrebbe funzionato correttamente: il processo (il thread) che esegue quel metodo è di classe AgentHandler, la quale classe è caricata col class loader primordiale. Allora si dovrà modificare la classe DefaultAgentLoader in modo che estenda la classe thread (in modo che quando si richiama start dalla classe AgentHandler effettivamente si esegua lo spawn di un nuovo thread).
public interface AgentLoader {Ovviamente si dovrà anche cambiare il metodo notTrustedProcess in modo che tratti in modo particolare questa classe, che è sì caricata con un class loader personalizzato, ma che comunque deve poter effettuare alcune operazioni. In effetti, altrimenti, si sarebbero ottenuti errori nel recupero dei byte della classe dell'agente: non proprio una SecurityException, ma un errore nel formato della classe in lettura. Non ho compreso molto bene questo errore, che comunque si presenta solo se è attivo un Security Manager, quindi, probabilmente, questa lettura provoca un errore di sicurezza, con conseguente fallimento del metodo.
public void start() ;
public void run() ;
public void setServer( AgentServer server ) ;
public void setInputStream( InputStream istream ) ;
public void setOutputStream( OutputStream ostream ) ;
}
...
public class DefaultAgentLoader extends Thread implements AgentLoader {
Del resto quando viene mandato in esecuzione un agente, è il thread di classe AgentLoader che esegue le operazioni, quindi ancora una volta non si avrebbe la possibilità di scoprire se è in esecuzione del codice dell'agente oppure sono in esecuzione le azione dell'AgentLoader. Anche in questo caso la soluzione è quella di far partire l'agente (richiamando il metodo onArrival) da un thread diverso (AgentExecutor); ecco quindi come viene modificata la classe DefaultAgentLoader (dalla quale tra l'altro deriva anche AgentLoaderEx, che quindi non necessiterà di modifiche):
public class DefaultAgentLoader extends Thread implements AgentLoader {A questo punto il metodo all'interno del Security Manager sarà:
...protected void startAgent( AgentPacket pack )
throws ClassNotFoundException, IOException {
ByteArrayInputStream byteIStream = new ByteArrayInputStream( pack.AgentBytes ) ;
ObjectInputStream objIStream = new ObjectInputStream( byteIStream ) ;
// questo provocherà il caricamento della classe dell'agente
Agent agent = (Agent)objIStream.readObject() ;
(new AgentExecutor( agent )).start() ;
}
}class AgentExecutor extends Thread {
Agent agent ;public AgentExecutor( Agent agent ) {
this.agent = agent ;
}public void run() {
System.out.println( "Esecuzione agente : " + agent.AgentName() ) ;
try {
agent.onArrival() ;
} catch ( AgentException ae ) {
System.err.println( "Eccezione AgentException non gestita" ) ;
ae.printStackTrace() ;
}
}
}
protected boolean notTrustedProcess() {Il controllo thread.getClass().getClassLoader() != null ) servirà solo per intercettare eventuali nuovi thread mandati in esecuzione dall'agente.
Thread thread = Thread.currentThread() ;
if ( thread instanceof Agent.AgentLoader )
return false ;
if ( thread instanceof Agent.AgentExecutor ||
thread.getClass().getClassLoader() != null )
return true ;
return false ;
}
Sono state introdotte nel pacchetto la classe AgentServerSecure e la classe AgentServerExSecure che installano il Security Manager; si è scelto di derivare queste classi dalle precedenti e ridefinire il metodo main, ma si sarebbe potuto ottenere lo stesso risultato modificando le classi precedenti in modo da accettare un paramentro a linea di comando per settare o meno il Security Manager.
public class AgentServerExSecure extends AgentServerEx {
...
protected void setSecurityManager() {
try {
System.setSecurityManager( new AgentSecurityManager() ) ;
} catch ( SecurityException se ) {
System.err.println( "Sec.Man. già installato!" ) ;
}
}public static void main( String args[] ) throws IOException {
if ( args.length > 3 )
throw new IOException(
"Syntax : AgentServerExSecure {<port> <messagesOn(1)> <loaderName>}" ) ;
int port = AgentServer.defaultPort ;
if ( args.length > 0 )
port = Integer.parseInt( args[ 0 ] ) ;System.out.println( "Starting AgentServer on port " + port ) ;
AgentServerExSecure agentServer ;
if ( args.length > 2 )
agentServer = new AgentServerExSecure( port, args[2] ) ;
else
agentServer = new AgentServerExSecure( port ) ;if ( args.length > 1 && args[1] == "1" )
agentServer.setMessages( true ) ;agentServer.setSecurityManager() ;
System.out.println( "Security Manager installato" ) ;agentServer.start() ;
}
}
Un esempio
Ecco come sempre un esempio di agente
per testare il Security Manager (per eseguire correttamente l'esempio si seguano
le istruzioni per il settaggio della variabile CLASSPATH):
public class TestAgentExSecure extends AgentEx {Come vedete quando l'agente arriverà sul server cercherà di terminarlo con una banalissima System.exit(1); se provate a spedirlo su un semplice AgentServerEx noterete che l'agente riuscirà tranquillamente nel suo intento! Lo stesso succederebbe se l'agente provasse a scrivere sul disco o cancellare file! (ho preferito non effettuare questa dimostrazione :-); se volete potete provare, ma fatelo con un file in lettura, per essere sicuri di non modificare o cancellare per sbaglio un file importante). Provate adesso invece a spedirlo su un AgentServerExSecure, e l'agente verrà scoperto e si "beccherà" una bella SecurityException: la giustizia ha trionfato :-)
protected String host ;
protected int port ;public TestAgentExSecure( String s, int p ) {
super( "MALIGNO" ) ;
host = s ;
port = p ;
}protected void execute() throws AgentException {
setMessages( true ) ; // per debug
System.out.println( "Salve io l'agente " + AgentName() ) ;
System.out.println( "e cercherò di STENDERE il server =:->" ) ;
System.out.println( "eh eh eh... =:->" ) ;
System.out.println( "Adesso migro su " + host + ":" + port ) ;
migrate( host, port ) ;
System.out.println( "Agente migrato" ) ;
}public void onArrival() throws AgentException {
System.out.println( "Salve, sono l'agente " + AgentName() +
" appena arrivato..." ) ;
System.out.println( "E adesso STENDERO' il server =:->" ) ;
System.out.println( "eh eh eh..." ) ;
try {
System.exit(1) ;
} catch ( SecurityException se ) {
System.out.println( "Ach... mi hanno beccato ! =:-(" ) ;
}
}public static void main( String args[] ) {
...
}
}
public class ClassBytesLoader {Conclusioni
// può essere chiamata anche da altre classi
public static byte[] loadClassBytes( String className )
throws IOException {
int size ;
byte[] classBytes ;
InputStream is ;
String fileSeparator = System.getProperty( "file.separator" ) ;
className = className.replace('.', fileSeparator.charAt(0));
className = className + ".class";String classpath = System.getProperty( "java.class.path" ) ;
StringTokenizer st = new StringTokenizer(
classpath, System.getProperty("path.separator") ) ;
File classFile = null ;
File dir = null ;
while( st.hasMoreTokens() ) {
dir = new File( st.nextToken() ) ;
System.out.println( "directory : " + dir ) ;
classFile = new File( dir, className ) ;
if ( classFile.exists() )
break ;
}FileInputStream fi = new FileInputStream( classFile ) ;
classBytes = new byte[ fi.available() ] ;
fi.read( classBytes ) ;return classBytes;
}
}
Sorgenti
ag3src.zip
Bibliografia
[1] Lorenzo Bettini, La programmazione
distribuita in Java, MokaByte Novembre 1997
[2] Lorenzo Bettini, Agenti mobili
in Java (I parte), MokaByte Maggio 1998
[3] Lorenzo Bettini, Agenti mobili
in Java (II parte), MokaByte Giugno 1998
[4] Lorenzo Bettini, Il class
loader, MokaByte Marzo 1998
[5] B.Venners, Security
and the class verifier, JavaWorld ( http://www.javaworld.com
) Ottobre 1997.
[6] Providing Your Own
Security Manager, dal Tutorial di Java ( http://java.sun.com/docs/books/tutorial/
)
[7] Donato Cappetta, Lorenzo
Bettini, Un NetworkClassLoader in Java, MokaByte Aprile 1998
[8] Lorenzo Bettini,
Progetto
e realizzazione di un linguaggio di programmazione per codice mobile,
tesi di Laurea in Scienze dell'Informazione, Aprile 1998, http://rap.dsi.unifi.it/xklaim
.
MokaByte Web 1998 - www.mokabyte.it MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it |