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.
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 retAntes 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:

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.
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.
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