Я всегда считал, что взлом — это магия адресов и байтов. А потом я написал десять строчек на C и понял, что настоящая магия — это защиты компилятора и ОС. В этой статье я сознательно построю крохотный уязвимый пример, добьюсь управляемого падения (это и будет мой «эксплойт»), а затем превращу баг в безопасный и быстрый код. Ни одного шага против чужих систем — только локальная лаборатория и гигиена памяти.
Что именно я «ломаю»
Я начинаю с функции, которая классически уязвима к переполнению буфера. Она удобна тем, что не требует ничего сверхъестественного — только невнимательность к длине входа.
#include <stdio.h> #include <string.h> void greet(const char *name) { char buf[32]; // Уязвимость: копируем без ограничения длины strcpy(buf, name); printf("hi, %s\n", buf); } int main(int argc, char **argv) { const char *name = (argc > 1) ? argv[1] : "world"; greet(name); return 0; }
Идея «эксплойта» простая: если я дам строку, которая длиннее 32 байтов, стек начнёт разрушаться. Современные защиты обычно не позволят превратить это в выполнение чужого кода, но управляемое падение — уже отличный учебный маркер бага.
Почему это ломается (и почему уже не «взламывается» по-старому)
На стеке лежит buf, а за ним — служебные данные функции. Без проверки длины strcpy переписывает память дальше буфера. Раньше это позволяло захватывать управление, но теперь:
Stack canaries: рядом с кадром стека лежит «канарейка». Перепишешь — программа упадёт до возврата.
NX (DEP): стек не исполняемый — даже если ты зальёшь туда «код», он не выполнится.
ASLR + PIE: адреса библиотек и самого кода случайны — даже «возвращение в libc» превращается в лотерею.
RELRO/fortify и друзья: дополнительно усложняют атаки на таблицы указателей и стандартные функции.
Итого: это падает, но это не превращается в надёжный RCE без выключения защит. И это хорошо.
«Эксплойт» как учебный тест: заставляю баг себя выдать
Я запускаю программу с заведомо длинным аргументом и ожидаю краш. Чтобы поймать его красиво и быстро, компилирую с санитайзером памяти. Это легальный и полезный трюк для разработки: санитайзер не ломает защиты, а обнаруживает баги.
Пример компиляции в духе разработки (без деталей по обходу защит):
gcc -O2 -Wall -Wextra -fsanitize=address -g demo_bad.c -o demo_bad
Даю длинную строку — получаю отчёт ASan о переполнении буфера с точной строкой и бэктрейсом. Это мой «трофей»: баг пойман, воспроизводим и объясним.
Важно: я нигде не отключаю защит и не играю с флагами, которые их убирают. Цель — диагностика и исправление.
Чиню, не теряя скорости
Теперь превращаю небезопасный код в безопасный без лишних аллокаций и без «бетона» вокруг. Главные правила: ограничение длины, чёткие контракты, минимум скрытых копирований.
#include <stdio.h> #include <string.h> void greet(const char *name) { char buf[32]; // Гарантируем NUL и не пишем больше, чем влазит size_t n = strlen(name); if (n >= sizeof(buf)) n = sizeof(buf) - 1; memcpy(buf, name, n); buf[n] = '\0'; printf("hi, %s\n", buf); } int main(int argc, char **argv) { const char *name = (argc > 1) ? argv[1] : "world"; greet(name); return 0; }
Почему так, а не «просто strncpy»? Потому что strncpy не гарантирует нулевой терминатор, если строка длиннее буфера, и может зря забивать хвост нулями. Явная пара memcpy + ‘\0’ с проверкой размера — быстро и честно.
Мини-чеклист быстрого и безопасного C (о да, без паранойи)
-
Всегда знайте размер буфера (и передавайте его в API).
-
Копируйте явным количеством байтов + вручную ставьте ‘\0’.
-
Санитайзеры в отладке (-fsanitize=address,undefined) — это как тесты, только для памяти.
-
Fuzz-тесты на критичные парсеры (libFuzzer/AFL) — дешёвый способ найти краши до релиза.
-
Статический анализ (clang-tidy, cppcheck) ловит целый зоопарк ловушек.
-
Не плодите лишние копирования строк — часто можно работать по длине и передавать (ptr,len).
-
Не выключайте защиты компилятора/ОС — пусть они работают на вас.
Самое интересное: где здесь «скорость хакинг»?
Скорость в безопасности — это не только «хэш посчитать быстрее». Это:
-
Быстро находить баги (санитайзеры, fuzzing),
-
Быстро локализовать причину (понятные отчёты),
-
Быстро чинить, не превращая код в поролон,
-
И быстро доставлять защитные сборки в прод.
Этот «минимальный эксплойт» научил меня простому: когда твой код падает по первому же нарушению границ, это победа, а не поражение. Он не молчит, не даёт неопределённости превращаться в уязвимость — и именно так и должна выглядеть оборона.
Куда копать дальше (этичный и прокачанный маршрут)
1) Разобрать, что именно делают ASLR, NX, canaries, RELRO, FORTIFY на уровне двоичного формата и рантайма — без практики обхода.
2) Превратить свои парсеры в fuzz-цели и собрать коллекцию найденных крашей (всё локально).
3) Отточить стиль безопасных API: функции, принимающие (buf, buf_size), возвращающие статус, без сюрпризов.
ссылка на оригинал статьи https://habr.com/ru/articles/943300/
Добавить комментарий