Призрак освобожденной памяти: как работает Use-After-Free и почему его боятся даже создатели браузеров
Мы уже посмотрели, как компилятор отбивает атаки на стек и как ошибки форматирования сливают токены из памяти. Теперь шагнем на территорию, где находят самые дорогие уязвимости в ядрах ОС, браузерах и демонах - в динамическую память (Heap / Кучу).
Сегодня в Parrot Security OS на ARM64 я разобрал уязвимость класса Use-After-Free (UAF) и получил доступ администратора без единой ошибки сегментации.
Делюсь пошаговым разбором того, как аллокатор памяти сам отдает контроль над чужими объектами.
1. В чем суть бага Use-After-Free?
Когда программа выделяет блок памяти через malloc(), а затем освобождает его через free(), программист обязан занулить указатель (ptr = NULL).
Если этого не сделать, указатель становится «висячим» (Dangling Pointer). Он продолжает указывать на адрес в куче, хотя система считает этот кусок свободным. Если программа позже снова обратится по этому указателю, она прочитает то, что окажется на этом месте.
2. Эксперимент: симулируем веб-сессию
Я написал сервис, где есть две сущности:
1. Сессия пользователя UserSession: логин (24 байта) и флаг роли is_admin (4 байта).
2. Заметка пользователя global_note: обычный текстовый буфер того же размера.
Смотрим на первый скриншот:
1. Создаем сессию: выделяется блок по адресу 0xaaaaaaac16b0, статус is_admin = 0.
2. Пользователь выходит: вызывается free(), но указатель не зануляется.
3. Создаем заметку: и аллокатор glibc выделяет под нее ровно тот же самый адрес 0xaaaaaaac16b0!
Почему адреса совпали?
В glibc работает механизм кэширования Tcache (Thread Local Cache). Чтобы не тратить время на системные вызовы, аллокатор складывает недавно освобожденные чанки одинакового размера в быстрый список и при следующем malloc мгновенно отдает их повторно!
3. Ловушки ядра: почему нельзя передать нули в argv
Казалось бы, достаточно записать в новую заметку 24 символа имени и байт \x01\x00\x00\x00, чтобы выставить флаг is_admin = 1.
Но тут включились правила Linux и POSIX (второй и третий скриншоты):
- Командная строка argv считает нулевой байт \x00 концом строки.
- Python при попытке передать бинарный ноль в аргумент выбросил системную ошибку ValueError: embedded null byte.
- А когда мы передали строку без нулей, memcpy скопировал наш байт \x01, но не затер старые служебные указатели аллокатора, выдав статус is_admin = 1213399041 вместо единицы!
4. Взлом через сырой поток stdin (четвертый и пятый скриншоты)
В реальных сетевых службах данные приходят не из аргументов запуска, а из сокета или стандартного ввода stdin.
Я переписал чтение заметки на fread() из потока ввода и передал пейлоад через пайп:
python3 -c 'import sys; sys.stdout.buffer.write(b"A"*24 + b"\x01\x00\x00\x00")' | ./uaf
Все 28 байт прошли без искажений:
- 24 буквы A заполнили поле логина;
- байты \x01\x00\x00\x00 аккуратно легли на место флага роли;
- программа обратилась к старому висячему указателю global_user->is_admin, увидела значение 1 и выдала флаг:
[+++] ДОСТУП РАЗРЕШЕН! Привет, Администратор! Флаг: FLAG{UAF_HEAP_MASTER_2026}.
Никакого переполнения буфера, никаких сработавших канареек - мы просто переиспользовали штатную логику менеджера памяти.
Главные выводы по безопасности кучи
1. Правило чистого указателя: Вызвал free(ptr) - сразу пиши ptr = NULL;. Обращение к нулевому адресу приведет к мгновенному и безопасному завершению программы вместо скрытой эксплуатации.
2. Динамический анализ (ASan): Собирайте тестовые билды с флагом -fsanitize=address. AddressSanitizer блокирует использование освобожденной памяти и сразу выводит имя проблемной строки.
3. Тренд на Memory-Safe языки: Именно сложности контроля времени жизни объектов в Си/C++ подталкивают индустрию к переходу на Rust и Go, где компилятор проверяет владение памятью на этапе сборки.
Просмотры: 16