Я всегда считал, что взлом — это магия адресов и байтов. А потом я написал десять строчек на 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 (о да, без паранойи)

  1. Всегда знайте размер буфера (и передавайте его в API).

  2. Копируйте явным количеством байтов + вручную ставьте '\0'.

  3. Санитайзеры в отладке (-fsanitize=address,undefined) — это как тесты, только для памяти.

  4. Fuzz-тесты на критичные парсеры (libFuzzer/AFL) — дешёвый способ найти краши до релиза.

  5. Статический анализ (clang-tidy, cppcheck) ловит целый зоопарк ловушек.

  6. Не плодите лишние копирования строк — часто можно работать по длине и передавать (ptr,len).

  7. Не выключайте защиты компилятора/ОС — пусть они работают на вас.

Самое интересное: где здесь «скорость хакинг»?


Скорость в безопасности — это не только «хэш посчитать быстрее». Это:

  • Быстро находить баги (санитайзеры, fuzzing),

  • Быстро локализовать причину (понятные отчёты),

  • Быстро чинить, не превращая код в поролон,

  • И быстро доставлять защитные сборки в прод.

Этот «минимальный эксплойт» научил меня простому: когда твой код падает по первому же нарушению границ, это победа, а не поражение. Он не молчит, не даёт неопределённости превращаться в уязвимость — и именно так и должна выглядеть оборона.

Куда копать дальше (этичный и прокачанный маршрут)

  1. Разобрать, что именно делают ASLR, NX, canaries, RELRO, FORTIFY на уровне двоичного формата и рантайма — без практики обхода.

  2. Превратить свои парсеры в fuzz-цели и собрать коллекцию найденных крашей (всё локально).

  3. Отточить стиль безопасных API: функции, принимающие (buf, buf_size), возвращающие статус, без сюрпризов.

Комментарии (13)