Double-Free
Conceito
O conceito por trás desse exploit é bem simples, ele consiste em realizar uma operação free()
em um ponteiro para um chunk que já foi liberado. Abaixo tem um exemplo do double-free, mas note que nas versões atuais da libc isso não funcionará.
Quando você realiza um free()
em um ponteiro para um chunk alocado, esse chunk passa a ser um chunk liberado, fazendo com que o início dos dados do usuário sejam o ponteiro para o próximo chunk na bin, e por fim o chunk é adicionado na respectiva bin. Porém, quando você realiza outro free()
nesse mesmo ponteiro (sem modificar ele) o mesmo chunk é adicionado na bin, ou seja, a bin conterá 2 chunks iguais.
Isso permite que seja possível alocar o mesmo chunk duas vezes com a malloc()
. Agora se pararmos para pensar, quando a malloc()
devolver pela primeira vez o chunk, ele será um chunk em estado alocado e liberado ao mesmo tempo, e se for possível alterar o conteúdo dele (por meio do alocado) nós estaremos escrevendo em cima dos ponteiros fd
e bk
, fazendo com que seja possível fazer a malloc()
nos retornar um ponteiro para uma área arbitrária.
Segue um exemplo:
Dessa forma, se a var for uma variável importante, nós poderemos modificar seu valor. Também é possível fazer a malloc()
retornar um endereço da .got
ou de outras constantes e ponteiros, como o _free_hook
da libc.
Importante: Acima foi demonstrado como utilizar o double-free para fazer a malloc()
retornar um endereço qualquer para nós, mas note que se for possível modificar o valor do chunk usando o mesmo ponteiro, após ele ser liberado, isso também será possível.
Proteções
Anteriormente foi citado que o exemplo dado não funcionaria, e isso se deve ao fato das bins terem proteções que foram evoluindo com as atualizações da libc.
2.25 e anteriores
Nestas versões a tcachebin ainda não havia sido implementada, então os chunks liberados iam para suas respectivas bins, como fast ou unsorted. Considerando a fastbin, é muito fácil realizar o double-free nela, pois sua verificação acontece somente com o topo da bin, ou seja, se liberarmos um outro chunk do mesmo tamanho entre o double-free o exploit não será identificado.
2.26 (teoricamente ficou assim até a 2.28)
Nesta versão a tcachebin foi implementada, ele veio com o ideal de otimizar algumas operações, e quando um chunk é liberado ele vai para ela inicialmente. É importante lembrar que a tcachebin contém apenas espaço para 7 chunks em cada bin.
Outro ponto importante é que quando ela fica cheia, os chunks começam a ir para as respectivas bins (como a fast) mas se a tcache ficar vazia, o sistema irá pegar o chunk na bin correspondente (como a fast) e irá jogar todos os outros chunks armazenados nessa bin para a tcache.
Em quesito do exploit, nessa versão não há nenhuma proteção, lançaram a bin sem usar nenhuma proteção já existente.
2.29
Foi nesta versão que uma proteção de segurança foi implementada. Essa proteção consiste em uma variável (um ponteiro para uma estrutura denominada tcache_perthread_struct
) chamada de key
, toda vez que um chunk é liberado a função tcache_put()
é chamada, e nela a key
é setada. Aí na verificação de segurança, como _int_free()
, é verificado se essa key
já está setada, se estiver, eles simplesmente percorrem a bin para verificar se já está lá.
Acima temos a estrutura tcache_entry
com a chave implementada.
E aí está a estrtura tcache_perthread_struct
, olhando o comentário podemos ver que ela meio que existem somente para otimização (e segurança).
Acima está a função tcache_entry()
, note que ali a key
é setada para ser o valor tcache
, esse valor nada mais é do que um outro ponteiro para tcache_perthread_struct
que é inicializado quando a função tcache_init()
é chamada.
Por fim, temos a parte da _int_free()
que está lidando com isso, note que a verificação do key
setado não é 100% garantida, pois o valor no key
pode ser uma coincidência ou o chunk não está na bin (nos exemplos há uma explicação de como fazer um chunk ter dois tamanhos ao mesmo tempo).
Vimos todas essas funções, mas no geral é importante de se ter em mente que o double-free sempre será detectado nas mesmas bins, para fazermos ele nessas versões é necessário abusar de algumas outras coisas. Exemplos são fazer um chunk ter dois tamanhos ao mesmo tempo, pois dessa forma a tcache vai identificar que a key
está setada mas ao percorrer a bin não vai encontrar ele, e outra forma é preencher a tcache e começar a trabalhar com as outras bins, como a fast, nesse caso só tem que ficar esperto com o fato da tcache pegar os outros elementos da bin.
2.32
Foi nessa versão que duas novas proteções foram adicionadas. A primeira é conhecida como mangling
, e ela é uma obfuscação do endereço armazenado nas áreas fd
e bk
, ou seja, quando você libera um chunk, ao invés dessas áres armazenarem o endereço correto do próximo chunk, ela na verdade armazena uma obfuscação.
Felizmente, essa obfuscação é muito fácil de se quebrar ou forjar (você só precisa de um vazamento da heap), pois ela consiste em uma operação de >> 12
com o endereço do chunk, e o resultado faz um XOR
com o endereço do próximo chunk.
Observe que a forma de se quebrar consiste em fazer a operação >> 12
com o endereço do chunk, e depois o XOR
é feito com a obfuscação. Segue um exemplo:
Tcachebin: 0x5555555592c0 -> 0x5555555592a0 -> NULL.
Mangling 1: 0x000055500000c7f9.
Mangling 2: 0x0000000555555559.
Verificando os mangling:
1: (0x5555555592c0 >> 12) ^ 0x5555555592a0 = 5550 0000 C7F9 (correto).
2: (0x5555555592a0 >> 12) ^ 0 = 5 5555 5559 (correto).
Quebrando:
1: (0x5555555592c0 >> 12) ^ 0x000055500000c7f9 = 5555 5555 92A0 (correto).
2: (0x5555555592a0 >> 12) ^ 0x0000000555555559 = 0 (correto).
A segunda proteção é a verificação de alinhamento. Existe duas implementadas:
Importante: MALLOC_ALIGNMENT para i386 vale 16, ou em binário, 10000.
A primeira verificação é para endereços, ela apenas verifica se o endereço do seu chunk termina com 0, para garantir o alinhamento. A segunda é para chunks, a única que não usa ela é a tcache.
Exemplos
Aqui estarão aluns exemplos que podem ser úteis.
Double-Free utilizando a fastbin
Importante: É muito chato realizar double-free na tcachebin após suas proteções serem adicionadas, por isso é interessante tentar nas outras. No caso acima o double-free é realizado na fastbin.
Chunk com 2 tamanhos por POISON NULL BYTE
O conceito por trás de um ataque de POISON NULL BYTE
segue a seguinte lógica:
Note que após o null byte, o tamanho do chunk passou a ser 0x100
, e antes ele era 0x111
.
Para fazer esse ataque, é importante ficar de olho em diversas falhas de segurança, vou mostrar aqui um exemplo de falha em um desafio conhecido como Zero_to_hero
do PicoCTF. Segue a função que aloca as coisas:
Está difícl de entender porque eu não renomeei as variáveis, mas simplificando, o código acima aloca um chunk com o tamanho informado, e em seguida pede para você escrever os dados. E aí está a vulnerabilidade, você pode escrever exatamente a quantidade que você digitou, e após isso o programa irá colocar um \0
no final, mas se você escreveu em todo o chunk, esse \0
vai parar no próximo, e bum, chunk envenenado.
Importante ressaltar alguns pontos sobre o código acima, não foi citado mas a função que realiza o free()
não anula os ponteiros (vulnerabilidade), e o chunk que você aloca para escrever nele inteiro deve ter o tamanho 1032 (tamanho máximo permitido pelo programa), pois a malloc()
não irá fazer alinhamento com esse tamanho (é o tamanho máximo da tcache). Outro ponto importante, é que o tamanho encontrado para o segundo chunk é 0x2..
, os dois pontos indicam que tanto faz, porque após o envenenamento o chunk será considerado como 0x200
, e a malloc()
alocará um chunk desse tamanho quando o tamanho for menor que 512 (não tão menor).
Referências
Atualizado