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:
Antes de executar a instrução leave
a pilha estará de seguinte forma:
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:
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:
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:
Nota: É possível continuar sobrescrevendo a pilha colocando mais gadgets e valores. Isso consiste o exploit ROP
, encadear pequenos pedaços de códigos.
Importante: Existem diversas formas de se encontrar gadgets, uma delas é olhando pelo próprio código, outra é usando o ROPgadget
com o seguinte comando ROPgadget --binary progrma
. É possível combinar a instrução com o grep
de forma que filtre por um registrador específico: ROPgadget --binary progrma | grep rdi
.
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.
Importante: Em ambos, quando uma função é chamada, o topo da pilha deve ser o endereço de retorno, já que a instrução call
coloca o endereço da próxima instrução na pilha, e da jmp
para a função.
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:
Importante: O código em c
usado para esse programa é praticamente o mesmo do que o usado para 32-bits. O problema é que quando ele era compilado na minha máquina, não havia os gadgets necessários para realizar o exploit, então foi optado por usar um programa já pronto.
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:
Links úteis
Atualizado