Return-Oriented Programming

O que é ROP

O ROP é um exploit útil para burlar a proteção NX. Sua base consiste em encadear pequenos pedaços de códigos já presentes no programa de forma que eles façam o que você deseja. Isto frequentemente envolve passar parâmetros para funções presentes dentro da libc, como a system, que se você encontrar a localização de um comando como cat flag.txt e passar como parâmetro, a system irá executar o comando e irá retornar o output. O comando mais perigoso é o /bin/sh, que se executado pela system, dá ao atacante acesso ao shell.

Funcionamento da Stack, e instrução LEAVE, RET e Gadgets

Stack

A pilha é uma região de memória, com funcionamento FIFO, responsável por armazenar diversas informações importantes para o programa. Uma dessas informações é o gerenciamento de funções.

Quando o código está sendo executado, ele segue instruções que estão em sequência, porém quando uma função é chamada, o programa deve desviar seu fluxo de execução para executar as instruções da função. Por causa disso, é necessário salvar o endereço da instrução que vem após a chamada da função, para que o programa, ao encerra-lá, possa retornar ao seu fluxo normal. E é na pilha que esse endereço é salvo, por isso sempre quando uma função é chamada, o topo da pilha é o endereço de retorno dela.

Nota: Toda função tem instruções para iniciar um segmento de pilha para elas, e encerrar esse segmento. As instruções que iniciam um segmento de pilha são conhecidas como prólogo da função, e essas instruções são: push rbp e mov rbp, rsp. Isso salva a base do segmento de pilha da função anterior (note que o endereço de retorno estará sempre após o antigo RBP), e move o RBP para o RSP para fazer ali ser a nova base.

As instruções que encerram o segmento são: mov rsp, rbp e pop rbp. Isso restaura a posição normal do topo da pilha, e em seguida retira o RBP salvo, para restaurar a antiga base do segmento.

LEAVE

A instrução LEAVE nada mais é do que as instruções que encerram o segmento da pilha para a função onde ele está sendo executado.

RET

A instrução ret nada mais é do que pop rip e jmp rip, ou seja, ela tira o endereço de retorno da pilha e pula para lá. Então imagine a seguinte sequência de códigos:

Endereço  Opcode  leave
Endereço  Opcode  ret

Antes de executar a instrução leave a pilha estará de seguinte forma:

Antes da LEAVE

Após a execução da instrução leave, o registrador rsp será movido e incrementado (instrução pop) e passará a apontar para o endereço de retorno, e após a instrução ret ser executada o rsp também será incrementado, mas o endereço estará salvo em rip, fazendo com que a pilha fique da seguinte maneira:

Depois da RET

Gadgets

Os gadgets nada mais são do que pequenos trechos de códigos que terminam com uma instrução ret, por exemplo pop rdi; ret. A importância desses gadgets é que todos eles já estão presentes dentro do código, e todos possuem um endereço, dessa forma é possível sobrescrever a pilha para que o endereço de retorno seja um gadget e as posições em sequência sejam os valores. Imagine o seguinte gadget e a pilha:

Pilha normal

Com isso nós queremos chamar uma função que recebe um parâmetro via o registrador edi. Para isso é necessário sobrescrever o valor de retorno para o endereço do gadget, no caso Endereço1, e em seguida informar os valores para cada operação do gadget, fazendo com que a pilha fique praticamente assim:

image

Nota: É possível continuar sobrescrevendo a pilha colocando mais gadgets e valores. Isso consiste o exploit ROP, encadear pequenos pedaços de códigos.

Chamadas de funções

Como visto no exemplo anterior o parâmetro foi passado via registrador, mas isso funciona somente para programas 64-bits, para programas em 32-bits é de outra forma.

32-bits

Vamos dar uma olhada no seguinte código:

Podemos ver que a main apenas chama a função vuln passando os parâmetros. Vamos olhar o disasembler da main então:

Podemos ver que todos os parâmetros são passados por meio da pilha.

Podemos ver então, que o topo da pilha quando a função foi chamada é o endereço de retorno, e os valores abaixo são os parâmetros seguindo a ordem em que serão atribuídos.

Nota: O endereço de retorno da função é o endereço da próxima instrução após a call: 0x80491f1 (main+50) ◂— add esp, 0x10.

Explorando um programa 32-bits

Como exemplo para programas em 32-bits vamos fazer um exploit no seguinte código:

Logo de cara já sabemos algumas coisas. A primeira é que os parâmetros devem ser 0xdeadc0de e 0xc0ded00d, e a segunda é que devemos escrever 40 bytes para começarmos a sobrescrever a pilha. Vamos olhar o disassembler da vuln:

Interessante notar que ao invés de criar espaço somente para o buffer (40 bytes), a função cria 12 bytes adicionais. O motivo não importa neste caso, o que importa é que para chegarmos no endereço de retorno é necessário sobrescrever 52 bytes. Usando a instrução disass flag nós encontramos o endereço da função que é 0x080491cb. Agora é só montar o código.

Nota: Neste caso nós escrevemos que o endereço de retorno da função flag como 0, mas seria possível alterar ele para ser algum gadget ou outra função.

Com isso foi possível obter o resultado:

64-bits

Em programas de 64-bits é diferente a passada de parâmetros, aqui é por meio de registradores. Para isso vamos olhar para o mesmo código em c da seção de 32-bits só que compilado para 64-bits.

Agora vamos olhar para a disassembler da função main:

Note que os parâmetros estão sendo passados para os registradores edi, esi, edx (podem mudar dependendo do SO).

Explorando um programa 64-bits

Para explorar um programa em 64-bits não basta apenas sobrescrever a pilha colocando os valores, aqui nós precisamos fazer uso dos gadgets para salvar os parâmetros nos registradores corretos e depois dar um return. Para isso vamos analisar o seguinte código em c:

Sabemos que precisamos passar dois parâmetros para a função flag, vamos procurar gadgets para os registrados edi e esi.

Encontramos dois bons 0x00000000004011fb : pop rdi ; ret e 0x00000000004011f9 : pop rsi ; pop r15 ; ret. Vamos olhar para a pilha da função vuln para encontrarmos quantos bytes é necessário para chegar no endereço de retorno.

Essa é a visão da pilha após a função criar espaço nela, para encontrarmos a quantidade necessária basta fazer EndereçoRet - RSP, que vai ser igual a 0x7fffffffdca8 - 0x7fffffffdc70 = 0x38. E usando o comando disass flag é possível encontrar o endereço da função, 0x000000000040116f. Agora é só montar o script.

Com isso foi possível obter o resultado:

Processos remotos

Quando estamos usando programas em 64-bits, e o exploit funciona normalmente localmente, mas falha remotamente, é por causa de algo chamado stack alignment. Felizmente (ou não) isso pode ser facilmente corrigido colocando um gadget de ret no retorno de um pop, seguindo o seguinte exemplo:

Ir0nstone

Pico Cetef

RazviOverflow

Atualizado