Buffer Overflow, PWN, ROP-цепочки

Pwn как в старые добрые: buffer overflow, ROP-цепочки и обход ASLR/NX

Ну здарова, бродяга. Что, соскучился по старым временам, когда NOP-спрей был искусством, а не багом? Го, лови разбор — как в 2010-х, только с учётом того, что защиты сейчас на минималках, но всё ещё ломаются.

Точка входа: разведка бинаря

Первое, что делаем с любым бинарём — не гадаем, а смотрим защиты в лицо:

Смотрим на связку: Canary, NX, PIE, RELRO. Если canary включён — ищем format string или другой способ его слить, если нет — тупо перезаписываем стек. Если NX стоит — про шеллкод на стеке забудь, работаем через ROP/ret2libc. Если PIE включён — база бинаря рандомная, нужен leak адреса самого бинаря, не только libc.

Классика жанра: подавляющее большинство современных pwn-целей — это связка NX+ASLR+PIE+Canary, и именно её мы ломаем через утечку + пересчёт базы.

Шаг 1. Находим переполнение и находим канарейку

Фаззим длину буфера через pwntools cyclic:

Нашли offset до RIP — отлично, дальше вопрос: canary есть или нет.

Если canary есть — обычно между buffer и saved RBP лежит 8 байт canary, начинающийся с \x00. Юзаем это: если есть format string bug (printf(buf) без формата) — сливаем canary через %p или %llx:

Если format string нет, но есть brute-force возможность (canary меняется только при fork, не при каждом запуске процесса, как в некоторых сервисных демонах) — можно перебирать байт за байтом. Медленно, но работает на тех, кто крашится, но не убивает соединение.

Шаг 2. NX стоит — не грустим, работаем ret2libc/ROP

Раз стек не исполняемый — прыгаем не в шеллкод, а в существующий код: собираем ROP-цепочку из гаджетов бинаря + libc.

Классический ret2libc, если ASLR выключен или есть leak:

Ищем pop rdi; ret — гаджет, чтобы засунуть аргумент в регистр перед вызовом функции (x86_64 calling convention: аргументы через регистры, не стек, как в старину).

Цепочка на классику «получить шелл через system»:

Вот этот ret_gadget для выравнивания стека — не роскошь, а обязательная деталь на современных libc (Ubuntu 18.04+), где system() внутри дёргает movaps, а он падает при невыровненном стеке. Народ забывает про это и потом сидит гадает, почему всё падает с SIGSEGV прямо перед шеллом.

Обход ASLR: тактика leak-and-calculate

ASLR рандомизирует базы библиотек и (если включен PIE) базу самого бинаря при каждом старте процесса. Бороться с этим — не магия, а арифметика:

  1. Утечка любого известного адреса функции из libc (через puts, format string, GOT-таблицу).
  2. Вычитаем известный офсет этой функции в конкретной версии libc — получаем базу.
  3. От базы прибавляем офсет нужной функции (system, execve, строку /bin/sh) — готово, адрес известен несмотря на рандомизацию.

Версию libc без доступа к бинарю на сервере ищем через:

Если PIE включён и слить адрес бинаря негде — часто спасает partial overwrite: перезаписываем только младший байт return-адреса (он не рандомизируется в пределах страницы), брутфорсим 1 байт вместо всех 6 — статистически заходит за 256 попыток, а не 2^48.

Собираем ROP автоматом, а не вручную (для ленивых котов)

Раз мы уже не в 2008-м — руками цепочки собирают только на экзамене. В реальности:

pwntools.ROP сам находит гаджеты, разруливает выравнивание и порядок пуша аргументов. Плюс one_gadget — если в libc есть волшебный однострочный гаджет типа execve("/bin/sh", NULL, NULL), который сразу отдаёт шелл без сборки цепочки:

Он выведет адрес + constraints на регистры (rsp+0x30 == NULL и т.п.) — если условия сходятся на утечке, это самый короткий путь к шеллу.

Что бросается в глаза сразу (чеклист пентестера)

  • Отсутствие Full RELRO — GOT перезаписываемый, можно устроить GOT overwrite вместо classic ROP.
  • Статически слинкованный бинарь без PIE — адреса гаджетов константные, ASLR бьёт только по стеку/куче/libc, а не по коду.
  • system()/exec* символы, видимые в PLT — прямой путь к ret2libc без утечки офсетов вручную.
  • Кастомный read/gets/scanf("%s") без ограничения длины — золото, ищи offset и вперёд.

Советы:

  1. Прогони бинарь через pwninit — автоматически подтянет нужную libc и патчит interp, сэкономишь 20 минут на подготовке окружения.
  2. Проверь seccomp фильтры (seccomp-tools dump ./vuln) — если стоят, execve может быть запрещён, придётся играть в ORW (open-read-write) через sigreturn (SROP) вместо простого шелла.
  3. Если бинарь статический и без libc-символов — глянь на ret2syscall через прямые syscall-гаджеты, найденные ROPgadget --binary ./vuln | grep syscall.
  4. На куче — не забывай про UAF/double-free связку с tcache poisoning, если задача явно намекает на heap (glibc 2.31+).
  5. Если всё вышеперечисленное уже разобрано в отдельной статье или на CTF — годно, тогда следующий логичный шаг — heap exploitation (tcache, house of force) или атаки на kernel pwn, но это уже отдельный забег, не в один вечер.
Pwn как в старые добрые: buffer overflow, ROP-цепочки и обход ASLR/NX

На этом всё. Всем хорошего дня!