Я всегда считал, что взлом — это магия адресов и байтов. А потом я написал десять строчек на 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),
Быстро локализовать причину (понятные отчёты),
Быстро чинить, не превращая код в поролон,
И быстро доставлять защитные сборки в прод.
Этот «минимальный эксплойт» научил меня простому: когда твой код падает по первому же нарушению границ, это победа, а не поражение. Он не молчит, не даёт неопределённости превращаться в уязвимость — и именно так и должна выглядеть оборона.
Куда копать дальше (этичный и прокачанный маршрут)
Разобрать, что именно делают ASLR, NX, canaries, RELRO, FORTIFY на уровне двоичного формата и рантайма — без практики обхода.
Превратить свои парсеры в fuzz-цели и собрать коллекцию найденных крашей (всё локально).
Отточить стиль безопасных API: функции, принимающие (buf, buf_size), возвращающие статус, без сюрпризов.