Регістри x86_64
Загального призначення
| Регістр | Призначення |
|---|---|
rax |
Повернення значення з функції |
rdi |
1-й аргумент функції |
rsi |
2-й аргумент функції |
rdx |
3-й аргумент функції |
rcx |
4-й аргумент функції |
Ключові регістри стеку
| Регістр | Призначення |
|---|---|
rip |
Instruction Pointer — адреса наступної інструкції що виконається |
rsp |
Stack Pointer — вершина стеку (поточна позиція) |
rbp |
Base Pointer — основа поточного фрейму |
Що читати в першу чергу при крашу
|
1 2 3 4 |
rip = 0x41414141 → CPU спробував стрибнути на адресу AAAA — overflow! rip = 0x0 → return address затертий нулем rbp = 0x4646... → base pointer затертий (FFFF...) — overflow дістався сюди |
ASCII → Hex шпаргалка
| Літера | Hex |
|---|---|
| A | 0x41 |
| B | 0x42 |
| C | 0x43 |
| D | 0x44 |
| E | 0x45 |
| F | 0x46 |
Якщо бачиш 0x4141414141414141 — це AAAAAAAA в пам’яті.
GDB — основні команди
Запуск
|
1 2 3 |
gdb ./program # запуск з бінарником gdb -q ./program # тихий режим (без банера) |
Управління виконанням
|
1 2 3 4 5 6 7 |
run # запустити програму run arg1 arg2 # з аргументами run < file.txt # перенаправити stdin з файлу run < <(python3 -c "import sys; sys.stdout.buffer.write(b'A'*20)") # payload kill # зупинити програму quit / q # вийти з GDB |
Breakpoints
|
1 2 3 4 5 6 |
break main # зупинитись на початку main break *0x401196 # зупинитись за адресою break 42 # зупинитись на рядку 42 info breakpoints # список breakpoints delete 1 # видалити breakpoint №1 |
Крокування
|
1 2 3 4 5 |
next / n # наступний рядок (не заходить в функції) step / s # наступний рядок (заходить в функції) continue / c # продовжити до наступного breakpoint finish # виконати до кінця поточної функції |
Регістри
|
1 2 3 4 5 |
info registers # всі регістри info registers rbp rip rsp # конкретні регістри p $rip # значення rip p $rsp # значення rsp |
Пам’ять — команда x (examine)
|
1 2 3 4 5 6 |
x/8gx $rsp # 8 значень по 8 байт (64-bit) від rsp x/32gx $rsp-64 # широкий вид стеку x/16xb $rsp # 16 байт від rsp x/s 0x401234 # рядок за адресою x/i $rip # інструкція за rip |
Формат команди x:
|
1 2 3 4 5 |
x / [кількість] [формат] [розмір] [адреса] Формати: x=hex, d=decimal, s=string, i=instruction Розміри: b=1байт, h=2байти, w=4байти, g=8байт |
Функції і символи
|
1 2 3 4 5 6 |
p secret # адреса функції secret p &variable # адреса змінної info functions # всі функції disassemble main # дизасемблювати main disassemble secret # дизасемблювати secret |
Backtrace (стек викликів)
|
1 2 3 4 5 |
bt # показати стек викликів bt full # з локальними змінними frame 0 # поточний фрейм frame 1 # фрейм вище |
Розкладка стеку x86_64
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Вищі адреси ┌─────────────────────┐ │ аргументи main │ │ return address │ ← rip після ret │ saved rbp │ ← старий rbp │ local var 2 │ │ local var 1 │ │ buf[7] │ │ buf[6] │ │ ... │ │ buf[0] │ ← rsp вказує сюди └─────────────────────┘ Нижчі адреси |
При overflow заповнюємо знизу вгору:
|
1 2 3 4 |
buf → A×N saved rbp → B×8 ret addr → наша адреса |
Little-endian — як записувати адреси
x86 зберігає байти у зворотньому порядку!
|
1 2 3 |
Адреса: 0x401296 В пам'яті: \x96\x12\x40\x00\x00\x00\x00\x00 |
Тобто молодший байт іде першим.
Python для генерації payload:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
import sys import struct addr = 0x401296 # Варіант 1 — вручну payload = b'A'*8 + b'B'*8 + b'\x96\x12\x40\x00\x00\x00\x00\x00' # Варіант 2 — struct.pack (автоматично little-endian) payload = b'A'*8 + b'B'*8 + struct.pack('<Q', addr) # Q = uint64 sys.stdout.buffer.write(payload) |
Захисти і як їх бачити
Stack Canary
|
1 2 3 4 |
# Перевірити чи є canary в бінарнику checksec --file=./program # потрібен pwntools: pip install pwntools objdump -d ./program | grep "canary" |
Якщо є — overflow викликає *** stack smashing detected *** до ret.
ASLR
|
1 2 3 4 5 6 7 |
# Поточний стан cat /proc/sys/kernel/randomize_va_space # 0 = вимкнено, 1 = частково, 2 = повністю # Перевірити — запусти двічі і порівняй адреси cat /proc/self/maps | head -5 |
PIE (Position Independent Executable)
|
1 2 3 4 |
file ./program # "pie executable" = PIE увімкнено # "executable" = PIE вимкнено (адреси фіксовані) |
Компіляція — прапорці захисту
|
1 2 3 4 5 6 7 8 9 |
# Всі захисти ВИМКНЕНІ (для навчання) gcc -fno-stack-protector -z execstack -no-pie -g -o program source.c # Всі захисти УВІМКНЕНІ (продакшн) gcc -o program source.c # Тільки без canary gcc -fno-stack-protector -o program source.c |
| Прапорець | Що вимикає |
|---|---|
-fno-stack-protector |
Stack canary |
-z execstack |
NX (стек стає виконуваним) |
-no-pie |
PIE (адреси стають фіксованими) |
-g |
Debug символи (для GDB) |
Небезпечні функції C
| Функція | Проблема | Замінити на |
|---|---|---|
gets(buf) |
не перевіряє розмір взагалі | fgets(buf, sizeof(buf), stdin) |
strcpy(dst, src) |
копіює до \0 без перевірки |
strncpy(dst, src, sizeof(dst)-1) |
strcat(dst, src) |
не перевіряє розмір | strncat(dst, src, sizeof(dst)-strlen(dst)-1) |
sprintf(buf, fmt, ...) |
може переповнити buf | snprintf(buf, sizeof(buf), fmt, ...) |
scanf("%s", buf) |
не перевіряє розмір | scanf("%7s", buf) (з обмеженням) |
Швидкий workflow при крашу
|
1 2 3 4 5 6 7 8 |
1. gdb ./program 2. run [args або payload] 3. info registers rbp rip rsp ← що в регістрах? 4. x/32gx $rsp-64 ← де наш паттерн на стеку? 5. p function_name ← адреса цільової функції 6. Рахуємо offset до ret addr 7. Формуємо payload: padding + адреса |