Perché alcune API Java ignorano i controlli standard di SecurityManager?

15

In Java, normalmente i controlli delle autorizzazioni sono gestiti da SecurityManager. Per impedire a codice non affidabile di richiamare codice privilegiato e sfruttare qualche bug nel codice privilegiato, SecurityManager controlla l'intero stack di chiamate; se qualcuno dei chiamanti nella traccia dello stack non ha privilegi, per impostazione predefinita la richiesta viene negata. Almeno, è come funzionano gli assegni standard di SecurityManager .

Tuttavia, alcune API Java speciali seguono regole diverse. Ignorano i controlli standard di SecurityManager e sostituiscono un controllo più debole. In particolare, controllano solo il chiamante immediato, non l'intero stack di chiamate. Per ulteriori dettagli, consulta la Linea guida 9-8 delle Linee guida per la codifica sicura di Java . includi, ad esempio, Class.forName() , Class.getMethod() e altro.)

Perché? Perché queste API speciali ignorano i controlli standard e sostituiscono un controllo più debole? E perché questo è sicuro? In altre parole, perché è sufficiente che controllino solo il chiamante immediato? Questo non reintroduce tutti i rischi che i controlli standard di SecurityManager sono stati progettati per difendersi da?

L'ho saputo per la prima volta leggendo un'analisi del recente exploit Java zero-day (CVE-2012-4681). Questa analisi decostruisce come funziona l'exploit. Tra le altre cose, l'attacco consiste nel trarre vantaggio dal controllo più debole effettuato da queste API speciali. In particolare, il codice Java dannoso riesce a ottenere un riferimento a una classe di sistema attendibile (attraverso un bug separato), quindi ingannevole quella classe di sistema attendibile a richiamare una di queste API speciali. Il controllo delle autorizzazioni risultante esamina solo il chiamante immediato, rileva che il chiamante immediato è affidabile e consente l'operazione, anche se l'operazione è stata originariamente avviata da codice non attendibile. Quindi i controlli più deboli non fermano l'attacco, ma per quanto posso vedere, questo attacco sarebbe stato prevenuto usando i controlli standard di SecurityManager (dal momento che il chiamante non è attendibile). In altre parole, questo recente attacco sembra un esempio del perché il controllo più debole è rischioso.

Tuttavia, so che i designer Java sono persone intelligenti. Ho il sospetto che i progettisti Java debbano aver considerato questi problemi e avere qualche buona ragione per aggirare i controlli standard e sostituire i controlli più deboli per queste API speciali - o, almeno, hanno pensato che avessero una buona ragione per cui questo era sicuro. Quindi, forse mi manca qualcosa.

Qualcuno può far luce su questo? I progettisti Java hanno fallito con queste API speciali, o c'erano validi motivi per sostituire i controlli più deboli?

Modifica 9/1: non sto chiedendo come funziona l'exploit; Penso di capire come funziona l'exploit. Non sto nemmeno chiedendo perché, in questo particolare esempio, il codice attendibile che ha invocato queste API speciali era buggato. Piuttosto, sto chiedendo perché le API speciali - come Class.forName() , Class.getMethod() , e così via - siano specificate e implementate per usare il controllo dei permessi non standard più debole (guarda solo al chiamante immediato) invece del controllo permesso standard di SecurityManager (guarda l'intero stack di chiamate). Questa decisione progettuale (l'utilizzo di controlli delle autorizzazioni più deboli per tali API speciali) ha consentito la recente vulnerabilità, quindi sarebbe facile criticare la decisione di progettazione. Tuttavia, immagino che ci potrebbero essere state alcune buone ragioni per fare le cose in questo modo, e mi chiedo cosa potrebbero essere quelle.

    
posta D.W. 01.09.2012 - 07:14
fonte

2 risposte

8

(Questo è solo un commento generale sul "perché", non sull'attacco specifico a cui stai alludendo.)

Sfortunatamente, i designer Java hanno scoperto che era altamente possibile dipingersi in un angolo, strutturalmente parlando. Ad esempio, ci sono classi in java.io e in java.net , che sono entrambi coinvolti nel fare I / O. Supponiamo che una data JVM abbia un codice nativo speciale di interazione con il sistema operativo in java.io.FileDescriptor , che gli consenta di effettuare chiamate di sistema send() e receive() (non è il caso della JVM Sun / Oracle, ma potrebbe accadere in un'altra JVM, e in effetti lo fa almeno in quello che ho scritto una volta). Per applicare la semantica della sandbox, questi metodi sono non public , naturalmente.

L'implementazione di java.net.Socket vorrebbe, abbastanza naturalmente, usare questi metodi. Ma, secondo le convenzioni di denominazione , Socket è una classe che si trova nel pacchetto java.net , non in java.io . Come può accedere ai metodi non pubblici di java.io.FileDescriptor , considerando che deve farlo anche se invocato da codice non attendibile (il codice non attendibile può aprire socket, anche se non in tutte le destinazioni)? Ci sono principalmente due modi:

  • Aggiungi alcuni metodi "bridge" native in java.net.Socket , che inoltra le chiamate ai metodi in java.io.FileDescriptor . Sbeker di codici nativi su pacchetti e visibilità; il codice nativo può fare tutto.

  • Permetti al codice da java.net di fare qualche riflessione per accedere a metodi non pubblici in altri pacchetti. Codice che può fare ciò che può fare su tutto, perché potrebbe modificare le strutture di dati usate da SecurityManager stesso (hey, ricordo che me lo hai indicato tu stesso, forse dieci anni fa, in una discussione Usenet in cui entrambi stavamo usando i nostri veri nomi). Quindi, la riflessione illimitata non deve essere concessa a tutti, ma, nella situazione che descrivo qui, deve essere concessa al codice in un pacchetto di sistema specifico ( java.net ) anche se il codice che è chiamato da un'applet non affidabile.

Il secondo metodo richiede l'indebolimento del modello di sicurezza. In realtà, è abbastanza generico: se l'applet non affidabile deve fare qualcosa di utile a un certo punto, deve essere in grado di accedere al sistema (se solo per visualizzare il risultato di calcoli o inviarlo al server ), che ha bisogno di una specie di cancello. Il codice nativo è un tale gate. Il modello di indebolimento della sicurezza è un altro tipo di gate, che ha l'ulteriore vantaggio di rimanere nel mondo "puro Java" (posso capire che nel team di manutenzione di JVM, il codice nativo è probabilmente disapprovato perché rende le cose più costose). Il lato negativo è che indebolendo il modello di sicurezza, la superficie di attacco è notevolmente ingrandita: ora, tutto il codice Java nei pacchetti con privilegi è diventato critico. Per riutilizzare l'analogia di @Hendrik, si tratta di dare una radice setuid bit a una porzione sostanziale di codice (dai pacchetti java.* ).

In particolare, il modo in cui Java ha indebolito il modello di sicurezza consiste nel designare alcune API speciali - come Class.findClass() , Class.newInstance() , Class.getMethod() e altre API relative alla riflessione - come se si usassero controlli delle autorizzazioni più deboli. Nell'esempio precedente, questo consente al codice di sistema attendibile in java.net.Socket di utilizzare queste API speciali per ottenere un riferimento a un metodo non pubblico in java.io.FileDescriptor e richiamarlo in modo riflessivo.

  • Ad esempio, il codice java.net.Socket può utilizzare Class.getMethod() per ottenere un riferimento al metodo non pubblico in java.io.FileDescriptor (ciò è consentito poiché il chiamante immediato di Class.getMethod() è java.net.Socket() , che è attendibile codice) e quindi richiamarlo.

    Si noti che questa strategia di implementazione si basa su Class.getMethod() per utilizzare i controlli delle autorizzazioni più deboli. Semplicemente non funzionerebbe, se Class.getMethod() usasse i controlli di autorizzazione standard. Se il codice non affidabile richiama java.net.Socket , e quindi il codice java.net.Socket chiama Class.getMethod() , i controlli di autorizzazione standard rifiuterebbero questa chiamata perché c'è un codice non affidabile da qualche parte sullo stack delle chiamate e la roba java.net.Socket non funzionerà correttamente (in scenari non di attacco). Al contrario, i controlli delle autorizzazioni più deboli utilizzati nel modello di sicurezza indebolito lo consentono.

Quindi, i controlli delle autorizzazioni più deboli aiutano i progettisti Java a tirarsi fuori dall'angolo in cui si sono dipinti.

Presumibilmente, con una struttura "perfetta" del codice di sistema e delle convenzioni di denominazione, basterebbe una manciata di metodi nativi, ma la presenza di specifiche chiamate is-immediate-call-da-a-trusted-package nella JVM il codice della libreria di sistema mostra che la struttura di detto codice non è perfetta.

    
risposta data 01.09.2012 - 14:59
fonte
3

Sommario

Alcuni metodi fidati richiedono più autorizzazioni internamente per adempiere ai loro compiti. Ma se questi metodi hanno dei bug, possono consentire a un utente malintenzionato non autorizzato di eseguire azioni indesiderate a un livello di privilegio aumentato.

Sfondo

Abbiamo una situazione molto simile a livello di sistema operativo. Su Unix ci sono i flag setuid / setgid e il comando sudo. Consentono a un utente non privilegiato di eseguire attività che richiedono un livello superiore di privilegi internamente.

Ad esempio, un utente normale non può modificare /etc/shadow . Ma vogliamo che gli utenti siano in grado di cambiare la loro password. Pertanto il comando passwd è contrassegnato come attendibile (setuid root) e gli è consentito modificare questo file. Ovviamente deve eseguire i propri controlli di sicurezza.

La stessa situazione si applica alla sandbox Java. Ad esempio, il codice non privilegiato non è autorizzato a invocare dinamicamente i metodi. Ma parti del sistema devono farlo internamente.

Proprio come il comando passwd consente agli utenti normali di modificare solo la propria password, il MethodFinder.findMethod() dovrebbe consentire solo al codice attendibile di chiamare metodi arbitrari.

Finora tutto va bene.

exploit di caricamento della classe

ClassFinder.findClass() è un metodo affidabile. Carica classi aggiuntive con i privilegi del codice chiamante. Proprio come passwd ti consente di cambiare la tua password.

Ma in caso di errore, ha cercato di recuperare caricando la classe con il permesso completo. Se pensiamo a passwd , un errore simile causerebbe passwd per cambiare la password di root invece che l'utente corrente.

I sistemi operativi permetteranno a passwd di cambiare la password di root, proprio come Class.forName() consente ClassFinder.findClass() di caricare le classi nel contesto di sicurezza sbagliato.

Metodo invocazione exploit

Questo è un po 'più complesso. Il metodo fidato è Method.invoke() qui.

Ma c'era un secondo metodo fidato coinvolto chiamato MethodFinder.findMethod() . Per stare con l'analogia del sistema operativo, pensala come uno script di shell che viene eseguito con i privilegi di root tramite sudo.

Questo metodo / programma non convalida i suoi parametri, li ha appena passati a passwd . Ora passwd è invocato in un contesto attendibile. Pertanto cambierà felicemente la password di chiunque.

    
risposta data 01.09.2012 - 10:25
fonte

Leggi altre domande sui tag