
Explorando uma vulnerabilidade corrigida no JavaScriptCore
Este é um exploit para uma vulnerabilidade do WebKit que foi originalmente descoberta por Fluoroacetate durante a competição pwn2own em Vancouver. Embora eu não tenha descoberto este bug, escrevi este exploit para praticar minhas habilidades de desenvolvimento de exploits. O artigo original sobre este exploit está aqui, da Zero Day Initiative. Embora este artigo seja muito bom e tenha sido fundamental para me ajudar a entender a vulnerabilidade, ele é do ponto de vista de alguém que está verificando a vulnerabilidade. Descobri que alguns detalhes importantes estão faltando ao tentar construir este exploit do zero e espero preencher algumas das lacunas que o artigo da ZDI deixou passar e adquirir habilidades práticas sobre como construir um exploit complicado do zero.
Estes passos servem como um esboço para obter execução de código arbitrária dentro do JavaScriptCore (JSC), o motor JavaScript do WebKit
A vulnerabilidade que será explorada é um estouro de inteiro que ocorre no código produzido pelo compilador just-in-time (JIT) DFG para o WebKit. Isso ocorre especificamente na função compileNewArrayWithSpread. Esta função será chamada quando o código que usa sintaxe de espalhamento do JavaScript para criar um novo array for compilado JIT pelo DFG.

Dentro do código compilado JIT, primeiro ele calculará o tamanho do array. Ele faz isso somando o comprimento de cada argumento passado para o construtor do array. Ao calcular o tamanho para cada adição, ele verifica se há um estouro do tamanho. Depois disso, ele chamará a função compileAllocateNewArray passando o comprimento que foi calculado nesta função.

A função compileAllocateNewArray então passará o comprimento calculado anteriormente para emitAllocateButterfly.

A função emitAllocateButterfly então deslocará à esquerda o tamanho em 3 bits, o que equivale a multiplicá-lo por 8. No entanto, não há verificação de estouro e, portanto, um número como 0x20000001 pode transbordar para 0x8
Este programa em C ilustra esta vulnerabilidade:


Podemos usar esta vulnerabilidade para enganar o motor JavaScript, fazendo-o acreditar que alocamos um array com tamanho 0x20000001, mas na verdade alocamos apenas espaço suficiente para 1 JSValue (8 bytes). Isso resultará em uma primitiva de leitura e escrita fora dos limites (OOB) que pode então ser aproveitada para alcançar leitura/escrita arbitrária e, eventualmente, execução remota de código (RCE).
Identificar a vulnerabilidade
Para confirmar que temos uma leitura OOB, tentaremos desencadear esta vulnerabilidade em uma compilação com address sanitizer (ASAN) do JSC.
Para fazer isso a partir do diretório do WebKit, podemos executar os comandos:```bash Tools/Scripts/set-webkit-configuration --asan Tools/Scripts/build-jsc --jsc--only --debug
Isto construirá uma build de depuração do JSC com ASAN habilitado, permitindo-nos verificar se conseguimos ou não acionar a vulnerabilidade.
Aqui está a primeira iteração do exploit.js```javascript
function jitMe(array){
return [...array]
}
let dummy = [1.1]
for(let i = 0; i < 200; i++){
jitMe(dummy);
}
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
Ao executar isso, obtenho o seguinte erro:
Programa terminado com o sinal SIGKILL, Morto. O programa não existe mais.
Meu palpite foi que muita memória estava sendo consumida ao tentar alocar um array tão grande. Para confirmar isso, adicionei um breakpoint ao código JITed adicionando uma chamada a m_jit.breakpoint() dentro de compileNewArrayWithSpread, que adiciona uma instrução int3 ao código JITed.
Depois de adicionar o breakpoint, descobri que ele não foi atingido e então decidi testar um comprimento de 0x20001. Então percebi que o código nem estava sendo compilado, então adicionei mais iterações para ativar o compilador DFG```javascript function jitMe(array){ for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array] }
let dummy = [1.1] for(let i = 0; i < 60; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){ a[i] = 1.1 }
jitMe(a)
Testar o programa como está ainda leva ao SIGKILL, no entanto, ao testar com um comprimento menor, o breakpoint é acionado. Neste ponto, ainda me parece que o JSC está ficando sem memória ao tentar processar aquele grande array.
Para lidar com isso, decidi alocar um array `a` menor e depois usar a sintaxe de espalhamento para usá-lo várias vezes ao criar o array corrompido, resultando no seguinte exploit.js```
function jitMe(array){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}
let dummy = [1.1]
for(let i = 0; i < 100; i++){
print(i)
jitMe(dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
Utilizando este código, conseguimos atingir o breakpoint sem um SIGKILL! Como geralmente acontece, ao corrigir um problema surge outro e obtivemos um SIGABORT... Usando o comando gdb bt podemos ver que operationNewArrayWithSize foi chamada, que por sua vez chamou create.
Parece estranho que nosso código JIT estivesse chamando operationNewArrayWithSize e deve ser que o código JIT teve que seguir um slow path para o motor JavaScript por algum motivo.

Podemos ver em compileAllocateNewArrayWithSize que há de fato um bailout para operationNewArrayWithSize. Precisamos então descobrir exatamente por que estamos fazendo o bailout para o slow case.
Podemos ver que em compileNewArrayWithSpread o shouldConvertLargeSizeToArrayStorage está definido como false e que esse slow path não estará no código compilado.
Portanto, faz sentido que o slow path esteja sendo acionado em algum lugar dentro de emitAllocateJSObject