Pular para o conteúdo principal

The House of Force

House of Force é uma técnica de exploração de heap que permite alocar um chunk em um endereço arbitrário ao corromper o top chunk. O ataque manipula o campo size do top chunk para enganar o malloc e fazer ele acreditar que há espaço suficiente para qualquer alocação.

O top-chunk (chunk mais ao topo na memória alocada na heap) é similar a qualquer outro chunk alocado por malloc: possui um header seguido de uma seção de dados. A diferença mais importante é que ele beira o fim da memória disponível e é o único chunk que pode ser extendido ou reduzido. A implementação do malloc trata o top chunk como um caso especial.

Pré requisitos do ataque

  1. Heap overflow ou primitiva de escrita arbitrária para corromper o size do top chunk
  2. Controle sobre o tamanho de futuras alocações malloc()
  3. Leak de heap e libc

Estrutura do top chunk

O top chunk (também chamado de wilderness) é o último chunk no heap. Quando malloc() precisa de memória e não há chunks adequados nas bins, ele usa o top chunk.

Heap:
┌─────────────┐
│ chunk 1 │
├─────────────┤
│ chunk 2 │
├─────────────┤
│ chunk 3 │
├─────────────┤
│ │
│ TOP CHUNK │ ← remainder (tudo que sobra)
│ │
│ size: 0x123 │ ← metadado que indica quanto espaço resta
└─────────────┘

O modo como malloc() lida com o top chunk (código simplificado):

void *malloc(size_t size) {
int top_chunk_size = top_chunk->size

if (size <= top_chunk_size) {
void *ptr = top_chunk; // posição atual do top chunk
top_chunk += size; // avança o top chunk
top_chunk->size = top_chunk_size - size; // diminui o tamanho restante
return ptr;
}
// senão, expande heap com sbrk/mmap
}

O ataque

  1. Corromper o top_chunk->size com um valor enorme (geralmente -1 ou 0xffffffffffffffff). Isso faz o malloc() acreditar que o top chunk tem espaço "infinito".
  2. Calcular a distância até o alvo. Se queremos alocar um chunk em target_address, então a distância será distance = target_address - top_chunk_address - 0x10 (o 0x10 é para compensar o header do chunk que será alocado).
  3. Alocar um chunk com tamanho igual à distância calculado. void *p = malloc(distance). Esta alocação move o top chunk até o target_address.
  4. Fazer uma segunda alocação. Essa alocação retorna exatamente o target_address, de modo que qualquer dado inserido no campo de dados desse chunk alocado estará sobrescrevendo target_address. Escrita arbitrária!

Esse ataque é bem maluco, pois fazemos uma alocação gigantesca para fazer o malloc() levar o top chunk até onde queremos, de modo que a próxima alocação retorne o endereço desejado.

Lembre-se de que o top chunk não é em si um chunk alocado. Ele sempre estará após o último chunk alocado.

Detalhes importantes

  1. Alinhamento: malloc() sempre retorna endereços alinhados (múltiplos de 16 bytes em x64). Você precisa calcular o distance considerando isso.
# Se target não está alinhado, ajustar
target_aligned = target & ~0xf # zera últimos 4 bits
distance = target_aligned - top_chunk - 0x10
  1. Size com Flags: O campo size tem flags nos últimos 3 bits. Ao corromper, use:
evil_size = 0xffffffffffffffff  # todos os bits setados
# ou
evil_size = 0xfffffffffffffffe # preserva alinhamento
  1. Distâncias negativas: Devido ao wraparound (quando uma operação aritmética ultrapassa o valor máxima, ela volta ao início de forma circular. Se o limite é 0x0000000000000000 - 1 = 0xffffffffffffffff), distâncias "negativas" funcionam:
# Se target está ANTES do top chunk no espaço de endereços
distance = (target - top_chunk - 0x10) & 0xffffffffffffffff
# Em C, unsigned arithmetic faz wraparound automaticamente (não precisa do & 0xffffffffffffffff, que mantém apenas os 64 bits inferiores)

Mitigações

  • glibc < 2.29: Sem proteções
  • glibc ≥ 2.29: Top Chunk Integrity Check
  • glibc ≥ 2.32: Safe-linking, dificulta obter leaks necessários.

Top Chunk Integrity Check

A verificação é adicionada:

if ((unsigned long)(size) > (unsigned long)(system_mem) ||
(unsigned long)(size) < MINSIZE) {
malloc_printerr("malloc(): corrupted top size");
}

O que verifica:

  1. size não pode ser maior que memória total do sistema
  2. size não pode ser menor que tamanho mínimo de chunk

Bypass:

  • Impossível com -1 (muito grande)
  • Possível com valores "realistas" mas ainda grandes
  • Requer conhecimento preciso de system_mem

Exemplo em código C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
long *target = 0x7ffff7dd1b10; // __malloc_hook (exemplo)

// Passo 1: Alocar chunks
long *chunk_A = malloc(0x10);
long *chunk_B = malloc(0x20);

// Passo 2: Overflow para corromper top chunk
// chunk_B pode escrever além de seu tamanho
long *top_chunk_size = chunk_B + 0x20/8; // offset até top chunk size
*top_chunk_size = -1; // 0xffffffffffffffff

// Passo 3: Calcular distância
long top_chunk = (long)(chunk_B + 0x20/8 + 1); // endereço do top chunk
long distance = (long)target - top_chunk - 0x10;

// Passo 4: Mover top chunk
malloc(distance);

// Passo 5: Alocar no target
long *evil = malloc(0x10);

printf("target address: %p\n", target);
printf("evil address: %p\n", evil);
printf("Match: %s\n", (evil == target) ? "YES!" : "NO");

return 0;
}

Resumo

House of Force = Corromper top chunk size → Fazer malloc "pular" para endereço arbitrário → Escrever no endereço arbitrário

  1. top_chunk->size = -1
  2. malloc(target - top_chunk - 0x10)
  3. malloc(small_size) → retorna target
  4. Usar o endereço à La Carte!