Главная / Блог / pwndbg для начинающих: отладка и pwn в CTF

14 мин.00

pwndbg для начинающих: отладка и pwn в CTF

pwndbg для начинающих: отладка и pwn в CTF

На первом CTF я убил четыре часа на pwn-задание, которое опытный игрок решает за двадцать минут. Теория buffer overflow была понятна, offset до return address нашёл — но в голом GDB. Вручную набираешь x/40gx $rsp после каждого шага, пытаешься в уме сопоставить адреса с дизассемблированным кодом, теряешь контекст, снова набираешь. Когда наставник показал pwndbg, разница была как между блокнотом и IDE: та же информация, но видна вся сразу — регистры, стек, дизассемблер, текущая инструкция. Ниже — гайд, который я хотел прочитать перед тем соревнованием: от установки до первого работающего эксплойта.

Зачем pwndbg, если есть голый GDB

GDB — мощный отладчик для pwn, но его интерфейс по умолчанию минималистичен до боли. Хочешь регистры — набирай info registers. Стек — x/20gx $rsp. Дизассемблирование — disassemble main. Три отдельных команды, три стены текста без подсветки. В pwn CTF задании, где нужно быстро итерировать — менять payload, перезапускать бинарник, проверять стек — такой workflow тупо убивает время. Подробнее — в нашем руководстве по бинарный анализ уязвимостей.

pwndbg решает проблему радикально: при каждой остановке (breakpoint, step, crash) автоматически выводит контекстный дисплей — блок регистров, блок дизассемблированного кода с подсвеченной текущей инструкцией, блок стека и backtrace. Одна остановка — и вся картина перед глазами. Плюс десятки специализированных команд: checksec для проверки защит бинарника, vmmap для карты виртуальной памяти, cyclic для генерации паттернов при поиске смещения, telescope для «умного» просмотра памяти с разыменованием указателей.

Альтернативы — GEF и PEDA. GEF — ближайший конкурент с похожим набором возможностей. PEDA устарела и практически не развивается. По README официального репозитория pwndbg загружает более 175 команд при старте, активно поддерживается (включая интеграцию с Binary Ninja для stripped-бинарников). Выбор между pwndbg и GEF — вопрос привычки, но pwndbg чаще мелькает в CTF writeups и учебных курсах по binary exploitation (pwn.college, Georgia Tech CS6265). Я выбрал pwndbg — и ни разу не пожалел.

Установка pwndbg на Linux

Три команды, меньше минуты на Ubuntu, Debian или Kali:

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

Скрипт установит Python-зависимости и добавит строку загрузки в ~/.gdbinit. После этого каждый запуск gdb автоматически подтягивает pwndbg. Проверка: набираем gdb в терминале и видим pwndbg: loaded 175 commands. Type pwndbg [filter] for a list.. Нет строки — открываем ~/.gdbinit и убеждаемся, что там есть source /path/to/pwndbg/gdbinit.py.

Типичная засада на Kali Linux — конфликт с предустановленным GEF или PEDA. Если в ~/.gdbinit уже сидит строка загрузки другого плагина, pwndbg не запустится или выдаст ошибки. Решение простое: закомментировать старые строки символом #, оставить только pwndbg. На чистой системе конфликтов не бывает.

Второй частый сценарий — работа на CTF-сервере по SSH без root. Скрипт setup.sh определяет окружение автоматически и использует pip install --user для Python-зависимостей. Права суперпользователя не нужны. На серверах некоторых университетских CTF-курсов (Georgia Tech CS6265) pwndbg предустановлен и запускается отдельной командой gdb-pwndbg.

Первый запуск: контекстный дисплей pwndbg

Загружаем бинарник: gdb ./binary_name. Ставим breakpoint на main — break main, запускаем — run. pwndbg выводит контекстный дисплей из четырёх секций. Понимание каждой — основа эффективной отладки бинарников.

REGISTERS — значения всех регистров процессора. В x86-64 это rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, rip и остальные. Регистры, изменившиеся с прошлого шага, подсвечены цветом. Для pwn ключевые: rsp (вершина стека), rbp (база текущего фрейма), rip (адрес следующей инструкции). Именно rip нужно контролировать для перехвата управления.

DISASM — дизассемблированный код вокруг текущей инструкции. Стрелка указывает на то, что выполнится следующим. Тут видны call, ret, mov, push, pop. pwndbg подсвечивает синтаксис и показывает имена функций вместо голых адресов (если бинарник не stripped). Аналогичный вывод даёт nearpc, расширенный — pdisass.

STACK — содержимое стека от текущего rsp вниз. Каждая строка — 8 байт (на x64) с автоматическим разыменованием: если значение на стеке — указатель на строку или другую область памяти, pwndbg покажет, куда оно ведёт. Вот это — ключевое преимущество перед голым GDB. Вместо гадания по шестнадцатеричным числам видна реальная структура данных.

BACKTRACE — цепочка вызовов функций, приведшая к текущей точке. Откуда вызвана текущая функция, через какие промежуточные вызовы. Полезна при отладке бинарников со сложной вложенностью.

Контекстный дисплей обновляется автоматически при каждом step, next, continue. Для ручного обновления — context. Не нужно каждый раз просить показать регистры, стек, код — всё уже на экране.

Основные pwndbg команды для отладки бинарников

checksec — первое действие с любым бинарником

Прежде чем искать уязвимости в бинарных файлах, нужно понять, какие защиты включены. checksec показывает состояние четырёх механизмов, и от результата зависит весь вектор атаки.

NX (No-Execute) — если включён, стек и куча не исполняемы. Нельзя просто положить shellcode на стек и прыгнуть туда — придётся строить ROP-цепочки.

Stack Canary — защитное значение между локальными переменными и saved return address. Переполнение затрёт канарейку — программа завершится до возврата из функции. Для обхода нужно либо утечь значение канарейки (через format string или info leak), либо найти уязвимость, которая перезаписывает return address без затирания канарейки.

PIE (Position Independent Executable) — рандомизация базового адреса бинарника. Адреса функций и гаджетов меняются при каждом запуске. Нужен info leak для определения базы.

RELRO (Relocation Read-Only) — защита GOT-таблицы. Full RELRO делает GOT read-only после загрузки, Partial RELRO оставляет записываемой — а это уже GOT overwrite.

Как читать результат: NX выключен + нет канарейки — классический ret2shellcode. NX включён + нет PIE — ret2libc или ROP по фиксированным адресам. Всё включено — нужен info leak + ROP. Ту же проверку можно выполнить из pwntools через ELF('./binary').checksec(), но в pwndbg результат доступен прямо в сессии, без переключения окон.

vmmap — карта виртуальной памяти

vmmap выводит все сегменты виртуальной памяти процесса: код программы, секции данных (.data, .bss), стек, кучу, загруженные библиотеки (libc, ld-linux). Для каждого сегмента — начальный и конечный адреса, права доступа (r--, rw-, r-x) и источник маппинга.

На практике: при построении эксплойта нужно знать, где лежит libc (для ret2libc), какие области исполняемы (для shellcode), где расположен стек (для вычисления адресов буферов). Если PIE выключен — адреса секции кода фиксированы, можно сразу искать гаджеты. Если PIE включён — адреса меняются при каждом запуске, но внутри одного запуска vmmap покажет актуальную раскладку.

Ещё полезная штука — команда aslr. Показывает, включена ли рандомизация адресного пространства в текущей сессии GDB. По умолчанию GDB отключает ASLR для воспроизводимости. Чтобы проверить эксплойт в условиях, приближённых к серверу CTF: set disable-randomization off.

telescope и анализ стека в GDB

Стандартная x/20gx $rsp покажет 20 восьмибайтовых значений от вершины стека — голые числа. telescope $rsp 20 в pwndbg делает то же, но с рекурсивным разыменованием: значение на стеке — указатель? pwndbg покажет, куда он указывает. Тот адрес тоже указатель — пойдёт дальше. В итоге видно не 0x7fffffffde08, а цепочку 0x7fffffffde08 → 0x7ffff7a2d830 (__libc_start_main+240).

Это кардинально ускоряет анализ стека: вместо ручного x/s address для каждого подозрительного значения telescope разыменовывает всё автоматически. Для кучи тоже полезен — быстро видно, куда ведут forward/backward указатели chunk'ов.

Из дополнительных команд: hexdump для hex/ASCII-дампа, search для поиска строки или байтовой последовательности по всей памяти процесса (отправили payload — выполняем search "AAAA" и видим, в каких сегментах приземлился ввод), stack для компактного отображения текущего фрейма.

Поиск смещения до return address: cyclic

Самая востребованная штука в pwndbg при решении pwn CTF заданий с переполнением буфера. Задача: определить точное количество байт между началом контролируемого буфера и saved return address на стеке.

Алгоритм по шагам:

Шаг 1. Генерируем циклический паттерн: cyclic 200. pwndbg создаёт строку из 200 байт, где каждая подстрока заданной длины уникальна. По умолчанию подстрока — 4 байта (для x86). Для x86-64 используем cyclic -n 8 200.

Шаг 2. Запускаем бинарник (run), на запрос ввода подаём сгенерированный паттерн. Можно скопировать из вывода или передать через pipe.

Шаг 3. Бинарник падает с SIGSEGV — паттерн перезаписал return address мусором, процессор пытается перейти по невалидному адресу. Смотрим вершину стека через telescope $rsp 1 — там лежит фрагмент паттерна, который занял место return address.

Шаг 4. Выполняем cyclic -l 0x6161616861616167 -n 8 (подставляем значение из стека) — pwndbg вернёт точное смещение в байтах.

Это смещение — количество байт padding'а перед адресом, который мы хотим записать в return address. Классическая ошибка новичков: забыть -n 8 для 64-битных бинарников. Если cyclic использует 4-байтовые подстроки, а бинарник 64-битный, значение из RSP может не найтись в паттерне — и смещение будет неправильным. Я на этом сам сидел полчаса, пока не дошло.

Та же функция доступна из pwntools: cyclic(200) генерирует паттерн, cyclic_find(value) находит смещение. Но в pwndbg удобнее итерировать на лету: отправили, упали, нашли — не нужно перезапускать скрипт.

Пошаговый разбор: эксплуатация переполнения буфера

Типичный сценарий pwn CTF задания: дан ELF-бинарник под Linux x86-64, исходного кода нет, задача — получить shell или прочитать флаг. Весь процесс от первого контакта с бинарником до работающего эксплойта.

Разведка. Загружаем: gdb ./vuln. Выполняем checksec. Допустим, видим: NX enabled, no canary, no PIE, Partial RELRO. Стек не исполняемый (shellcode не сработает), канарейки нет (можно перезаписывать стек свободно), PIE выключен (адреса фиксированы), GOT записываемая. Уже можно прикидывать вектор.

Статический анализ. info functions — смотрим список функций. Если бинарник не stripped, увидим имена. В CTF-тасках начального уровня часто есть функция win, flag или get_shell — целевая для перенаправления управления. Адрес берём через print &win или из дизассемблера. Stripped-бинарник — имён не будет, нужен Ghidra или трассировка от entry point.

Поиск уязвимости. Ставим break main, запускаем run, шагаем через next и step. В контекстном дисплее видны вызовы функций. Ищем опасные: gets (не ограничивает длину ввода — привет, 1990-е), scanf("%s", ...) (аналогично), read с размером больше буфера. При вызове функции pwndbg показывает аргументы — адрес буфера и его положение на стеке.

Определение смещения. Генерируем паттерн, подаём на вход, ловим crash, определяем offset через cyclic -l — процесс описан выше. Допустим, offset = 72 байта.

Построение payload. 72 байта мусора + 8 байт адреса функции win в little-endian. Проверяем в GDB: перезапускаем, подаём payload, ставим breakpoint на ret уязвимой функции. На момент ret смотрим telescope $rsp 1 — если там адрес win, смещение правильное. Если нет — пересчитываем.

Запуск. Переходим к автоматизации через pwntools.

Связка pwndbg и pwntools в pwn CTF заданиях

pwndbg — инструмент отладки. pwntools — Python-библиотека для автоматизации эксплуатации. Вместе они покрывают полный цикл: исследуем бинарник в pwndbg, находим уязвимость и смещение, пишем эксплойт на pwntools.

Стандартный шаблон эксплойта (этот паттерн встречается в каждом втором CTF writeup по binary exploitation):

from pwn import *

elf = ELF('./vuln')
p = process('./vuln')
# p = remote('ctf.example.com', 1337)

offset = 72
win_addr = elf.sym['win']
payload = b'A' * offset + p64(win_addr)

p.sendline(payload)
p.interactive()

ELF загружает бинарник и даёт доступ к адресам символов через elf.sym['name'], к секциям через elf.bss(), к построению ROP через ROP(elf). p64() упаковывает адрес в little-endian для x86-64. process() запускает локально, remote() подключается к серверу CTF. Переключение между локальной отладкой и удалённой эксплуатацией — замена одной строки.

Для отладки эксплойта с подключением к pwndbg — gdb.attach(p). Откроет терминал с pwndbg, подключённым к процессу из скрипта. Можно ставить breakpoints и наблюдать, как payload ложится в память на каждом шаге.

Критический нюанс для удалённых серверов (izzy.sh подробно разбирает это в «The Pwner's Roadmap»): несоответствие версий libc. Эксплойт работает локально, но падает на сервере, потому что смещения функций в libc различаются между версиями. На локальной машине glibc 2.39, на сервере 2.31 — ROP-цепочка, указывающая на system(), прыгнет в мусор. Утилита pwninit решает проблему: подаёте бинарник и серверную libc, она патчит бинарник для правильной версии. Для heap-тасков — glibc-all-in-one, набор скомпилированных версий glibc для локального тестирования. Без этого зоопарка heap-pwn превращается в угадайку.

Отладка stripped-бинарников и reverse engineering в GDB

В CTF заданиях повышенной сложности часто дают stripped-бинарник — имена функций и отладочные символы удалены. info functions покажет только системные символы из libc, а break main не сработает. По сути тот же приём, что описан в MITRE ATT&CK как Stripped Payloads (T1027.008) для сокрытия малвари, только здесь strip применяют к легитимному бинарнику, чтобы усложнить жизнь участникам.

pwndbg частично спасает. Команда entry ставит breakpoint на точку входа ELF. Оттуда трассируем: в x86-64 адрес main передаётся как аргумент в __libc_start_main через регистр rdi. Дойдя до соответствующего call, ставим breakpoint по адресу из rdi — и мы внутри main, пусть и без имени.

Для более глубокого анализа pwndbg поддерживает интеграцию с Binary Ninja через XML-RPC — синхронизирует имена функций, переменные стека и декомпилированный код. Если Binary Ninja нет, Ghidra делает то же для статического анализа: декомпилируем, находим структуру кода и адреса ключевых функций, ставим breakpoints по ним в pwndbg. Связка Ghidra + pwndbg — рабочий вариант для тех, кто не готов платить за Binary Ninja.

Команды nearpc и pdisass выводят дизассемблированный код с подсветкой даже для stripped-бинарников. nextcall, nextret, nextjmp позволяют быстро перемещаться по коду без имён функций — ставим breakpoint на следующий call или ret и шагаем по логике программы, ориентируясь по аргументам в регистрах и контексту стека.

Частые ошибки новичков при работе с pwndbg

pwndbg не загружается. Проверяем ~/.gdbinit — там должна быть одна строка source для pwndbg. Несколько плагинов GDB одновременно не работают. Если раньше стояли GEF или PEDA — комментируем их строки.

Breakpoint не срабатывает на PIE-бинарнике. break *0x401234 поставит breakpoint по фиксированному адресу, но при включённом PIE код загрузится по другому адресу. Решение: break main (по имени) или break *$rebase(0x1234) — pwndbg пересчитает адрес с учётом текущей базы. $rebase — специфичная для pwndbg функция, в голом GDB её нет.

Путаница с calling conventions x86 и x86-64. На 32-битных бинарниках аргументы передаются через стек. На 64-битных первые шесть аргументов идут через регистры rdi, rsi, rdx, rcx, r8, r9. При построении ROP-цепочки для вызова system("/bin/sh") на x64 нужен гаджет pop rdi; ret для загрузки адреса строки в rdi. На x86 достаточно положить аргумент на стек после адреса функции. Забыть про эту разницу — гарантированно сломать эксплойт. Я на этом горел дважды.

cyclic даёт неправильное смещение. Если бинарник 64-битный, а cyclic использует 4-байтовые подстроки (по умолчанию), значение из RSP может не найтись в паттерне. Всегда проверяем архитектуру через file ./binary перед генерацией паттерна и используем -n 8 для x64.

Бинарник завершается до breakpoint. Некоторые CTF-бинарники используют alarm() для таймаута или проверяют наличие отладчика — техника Debugger Evasion (T1622). Для обхода таймаута: handle SIGALRM ignore. Для обхода базовых anti-debug проверок на основе ptrace (T1055.008): бинарник вызывает ptrace(PTRACE_TRACEME) и по результату понимает, что его трассируют. Обход — catch syscall ptrace (catchpoint сработает дважды: при входе в syscall и при выходе), после первого срабатывания continue, на втором (выход из syscall) — set $rax=0. Грязный хак, но работает.

Не видно вывод программы. При работе в GDB stdout бинарника иногда буферизируется. Если бинарник использует printf без \n — вывод может не появиться до завершения. Решение: call (void)setvbuf(stdout, 0, 2, 0) в GDB (отключает буферизацию stdout), или использовать pwntools с process(), где stdout перенаправляется автоматически.

От первого overflow к ROP-цепочкам и heap

После базовых pwndbg команд и первых ret2win-эксплойтов следующий этап — ROP-цепочки. Когда NX включён и нет удобной win-функции, нужно собирать цепочку из «гаджетов» — коротких последовательностей инструкций, заканчивающихся ret. ROPgadget или ropper ищут гаджеты в бинарнике; pwndbg помогает верифицировать цепочку в рантайме через breakpoints и telescope. Для систематической тренировки ROP — ROP Emporium: восемь задач чисто на построение цепочек, без сложного реверса. Отличный drill.

Следом — heap exploitation. Тут уровень сложности меняется принципиально: нужно понимать внутренности аллокатора glibc (chunks, bins, tcache, fastbins). pwndbg имеет набор heap-команд для визуализации состояния кучи, но это тема для отдельного разбора. Формула на бумаге понятна, но heap corruption по-настоящему ощущается только когда сам прогоняешь сценарий на живом стенде. На HackerLab.pro есть категории pwn и reverse — готовые таски разных уровней, где можно попробовать эти техники после теории.

Мой основной тезис после полутора лет CTF-практики: pwndbg — не костыль, а мультипликатор скорости. Часто слышу от новичков «сначала освою голый GDB, а потом поставлю плагины». Это как учиться программированию в ed вместо нормального редактора — технически возможно, но время, потраченное на борьбу с инструментом, не конвертируется в понимание binary exploitation. Концепции одни и те же: стековый фрейм, регистры, виртуальная память, calling conventions. pwndbg не скрывает их — наоборот, показывает нагляднее. Кто ставит pwndbg с первого дня, быстрее видит паттерны: вот тут канарейка, вот тут контролируемый буфер, вот тут гаджет для ROP. Кто мучается с vanilla GDB — тратит когнитивный ресурс на ввод команд вместо анализа.

Второе наблюдение: разрыв между «решил первый ret2win» и «стабильно решаю heap-таски» — огромный, и застревают на нём большинство. Причина — ROP не отработан до автоматизма. Drill-площадки помогают, но дальше нужна системная подготовка с прогрессией от простого к сложному. Если хочешь пройти этот путь быстрее — на WAPT эту цепочку проходят в течение нескольких модулей с лабами на каждый вектор.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...

Читайте также

Insecure Deserialization в CTF: эксплуатация

13 мин.

4

Insecure Deserialization в CTF: эксплуатация

Разбираем PHP unserialize и Python pickle по байтам — POP chain, PHAR, jsonpickle, CVE-2020-22083. Пошаговая методология эксплуатации десериализации для CTF.

20 ОКТЯБРЬ, 2026

Декодирование кодировок CTF: алгоритм и инструменты

13 мин.

6

Декодирование кодировок CTF: алгоритм и инструменты

Пошаговый алгоритм распознавания Base64, Hex, Base32 и других кодировок в CTF-тасках. Таблица визуальных признаков, CyberChef-рецепты и Python-скрипт автоматизации.

19 ОКТЯБРЬ, 2026

XXE инъекция в CTF: от файлов до SSRF

12 мин.

3

XXE инъекция в CTF: от файлов до SSRF

Пошаговый разбор XXE-инъекций для CTF: от чтения /etc/passwd до blind OOB-эксфильтрации по FTP и SSRF на облачные метаданные. Пейлоады, обход фильтров, чеклист.

19 ОКТЯБРЬ, 2026