Главная / Блог / Pwntools для начинающих: pwn на Python без боли

11 мин.00

Pwntools для начинающих: pwn на Python без боли

Pwntools для начинающих: pwn на Python без боли

Больше сотни тасков в категории pwn за два года — picoCTF, HackTheBox, закрытые джеопарди. Ни в одном эксплойт не обошёлся без pwntools. Каждый раз, когда кто-то из команды пытается решить pwn через subprocess и struct.pack, он теряет полчаса на вещах, которые pwntools закрывает одной строкой: поиск offset через уникальный паттерн, упаковка адреса в Little-Endian, доставка payload процессу. Этот разбор — не пересказ API-документации, а сквозной путь от скачанного бинарника до флага. С акцентом на том, что другие гайды пропускают: отладка через GDB прямо из скрипта, переключение local→remote без переписывания кода и три ошибки, которые сжигают часы даже после первого десятка решённых тасков.

Окружение для pwn-задач на Python

Pwntools — Python-библиотека для написания эксплойтов, стандарт де-факто в pwn-категории CTF. Установка: python3 -m pip install pwntools. После этого в терминале появляется набор CLI-утилит (checksec, cyclic, pwn template), а в Python — модуль pwn. Если pip ругается на зависимости — на Debian/Ubuntu нужно доставить python3-dev и libssl-dev. Подробнее — в нашем статье о создание ctf заданий.

Проверка: python3 -c "from pwn import *; print('OK')" должна вывести OK без ошибок. Частая проблема у новичков — pip поставил пакет в другое Python-окружение, и импорт не находит модуль. Лечится просто: python3 -m pip install pwntools с явным указанием интерпретатора.

Ещё нужен GDB с плагином — pwndbg или GEF. По возможностям они примерно одинаковые, разница в интерфейсе. Я использую pwndbg — ставится через git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh. После установки GDB вместо голой консоли показывает цветной вывод с регистрами, стеком и дизассемблером. Авторы pwndbg сами пишут: «плагин, который делает отладку менее мучительной, с фокусом на задачах разработчиков эксплойтов». Точнее не скажешь.

Момент, который игнорируют почти все туториалы: для GDB-интеграции из pwntools-скриптов (вызов gdb.attach()) нужен терминальный мультиплексор — tmux или screen. Pwntools открывает отладчик в соседней панели терминала, без мультиплексора вызов просто выбросит ошибку. Запускайте скрипты из-под tmux — одно действие, которое экономит часы нервов.

Для дизассемблирования простых бинарников хватит objdump -d из пакета binutils. Для сложных задач — Ghidra с графом вызовов или radare2. Но для базовых ret2win связки pwndbg + pwntools хватает за глаза.

checksec и класс ELF: бинарный анализ за секунды

Первое действие после скачивания бинарника с CTF-платформы — checksec ./vuln. Утилита из пакета pwntools показывает защиты, включённые при компиляции. Результат определяет стратегию эксплуатации бинарных уязвимостей целиком.

Типичный вывод для учебного ret2win: Arch: amd64-64-little, Stack: No canary found, NX: NX enabled, PIE: No PIE (0x400000). Что означает каждая строка и как влияет на написание эксплойта pwntools:

Arch: amd64-64-little — 64-битный бинарник, байтовый порядок Little-Endian. Для упаковки адресов используем p64(), адреса восьмибайтовые.

No canary found — стековых канареек нет. Канарейка — случайное значение между локальными переменными и return address. При переполнении она затирается, программа детектирует подмену и завершается с *** stack smashing detected ***. Без канарейки перезаписываем адрес возврата свободно.

NX enabled — стек неисполняемый. Шеллкод в буфер записать можно, но передать на него управление не получится — процессор откажется выполнять код из страницы без execute-бита. Вместо инъекции кода перенаправляем выполнение на существующую функцию — суть подхода ret2win.

No PIE — бинарник грузится по фиксированному адресу 0x400000. Адреса функций не рандомизируются при каждом запуске: адрес win-функции можно подставить в payload напрямую. С включённым PIE пришлось бы сначала организовать information leak — а это уже другая весовая категория.

Тот же анализ доступен из Python. Конструктор ELF(path, checksec=True) загружает бинарник, автоматически выводит checksec в консоль и парсит таблицу символов. Адрес функции — elf.symbols['win']. Записи GOT — elf.got['puts']. PLT-стабы — elf.plt['puts']. Класс ELF умеет даже патчить и сохранять бинарники, но для решения CTF pwn-заданий хватает чтения символов. Главный бонус: если организаторы обновят бинарник, скрипт автоматически подхватит новые адреса без ручного редактирования.

Зачем эти навыки за пределами соревнований? Эксплуатация уязвимостей памяти — не абстрактное упражнение. В MITRE ATT&CK такие техники описаны как Exploitation for Client Execution (T1203, тактика Execution) и Exploitation for Privilege Escalation (T1068, тактика Privilege Escalation). Навыки автоматизации эксплойтов на Python, отточенные на CTF, напрямую переносятся в анализ реальных CVE и аудит бинарных приложений.

Поиск offset через cyclic: автоматизация вместо подбора

Центральный вопрос при buffer overflow: сколько байт мусора нужно записать, чтобы добраться ровно до return address? Ручной подбор через строку AAAA... — занятие неблагодарное. Различить 43 и 44 символа A в дампе памяти физически невозможно.

Pwntools решает задачу через cyclic() — генератор уникальных паттернов, где каждая подпоследовательность заданной длины встречается ровно один раз. Процесс:

  1. Генерируем паттерн: cyclic 200 в терминале выводит строку вида aaaabaaacaaadaaa....
  2. Загружаем бинарник в GDB с pwndbg: gdb ./vuln. Запускаем с паттерном: r <<< $(cyclic 200). Программа падает с Segmentation fault.
  3. Pwndbg автоматически покажет регистры при краше. Для 32-бит смотрим EIP. Для 64-бит смотрим значение на вершине стека командой x/gx $rsp.
  4. Находим offset: cyclic -l 0x6161616c в терминале возвращает точное число байт. В Python — cyclic_find(0x6161616c).

Почему RSP, а не RIP для x64? В 32-битной архитектуре return address попадает прямо в EIP — его и смотрим при краше. В 64-битной ситуация хитрее: при выполнении ret значение снимается со стека в RIP, но если адрес из паттерна невалиден (а он невалиден — это мусорные байты), процессор генерирует fault до записи в RIP. Значение, которое должно было стать новым RIP, остаётся на вершине стека — в RSP. На этом нюансе стабильно застревают на час-два, проверяя правильный по сути подход. Знаю, потому что сам так сидел.

Размер слова cyclic — второй источник путаницы. По умолчанию cyclic работает с 4-байтовыми уникальными блоками (для i386). Для amd64 нужны 8-байтовые: cyclic -n 8 200 для генерации, cyclic -n 8 -l <значение> для поиска. В Python это решается через context.arch = 'amd64' до вызова cyclic() — размер подхватится автоматически. Без этого cyclic_find не найдёт совпадение и молча вернёт ошибку. Неочевидно, обидно, и на это уходит ещё полчаса.

Написание эксплойта ret2win в pwntools

Ret2win — самый простой тип CTF pwn-задач и лучшая точка входа в бинарную эксплуатацию с нуля. В бинарнике две составляющие: уязвимость типа stack overflow (обычно через gets() или scanf("%s"), читающие ввод без ограничения длины) и win-функция, которая выводит флаг, но никогда не вызывается из основного кода. Задача — перезаписать return address на адрес этой функции.

Все компоненты на руках: offset (допустим, 40 байт), адрес win, архитектура amd64. Собираем:

from pwn import *

context.arch = 'amd64'
elf = ELF('./vuln')
p = process('./vuln')

offset = 40
payload = b'A' * offset + p64(elf.symbols['win'])

p.sendline(payload)
p.interactive()

Восемь строк — рабочий эксплойт. Разберём каждую.

from pwn import * импортирует всё из pwntools разом. Документация библиотеки прямо говорит: «pwntools generally follows the kitchen sink approach». В production-коде антипаттерн, в CTF — норма.

context.arch = 'amd64' задаёт архитектуру. Влияет на поведение p64(), cyclic(), asm() и модуля shellcraft. Без неё pwntools может использовать i386 по умолчанию — и тут начинаются чудеса.

ELF('./vuln') парсит бинарник. Через elf.symbols['win'] получаем адрес без ручного objdump.

process('./vuln') запускает локальный процесс и создаёт tube — универсальный канал ввода-вывода. Tube — центральная абстракция pwntools: одинаковый интерфейс для процессов, сокетов и SSH-соединений.

p64(elf.symbols['win']) упаковывает адрес в 8 байт Little-Endian. Для адреса 0x401196 результат — b'\x96\x11\x40\x00\x00\x00\x00\x00'. Именно это значение окажется в RIP после инструкции ret в уязвимой функции.

sendline(payload) отправляет данные в stdin с добавлением \n. Если win-функция вызывает system("/bin/sh") — interactive() даст shell.

GDB из pwntools-скрипта: отладка эксплойтов без боли

Тема, которую русскоязычные гайды по pwntools обходят стороной. А без неё отладка превращается в ручное копирование payload между терминалами.

Pwntools умеет подключать GDB к процессу прямо из Python-скрипта. Два подхода: gdb.attach() подключается к уже запущенному процессу, gdb.debug() запускает бинарник сразу под отладчиком.

from pwn import *

p = process('./vuln')
gdb.attach(p, '''
    break *main+42
    continue
''')

payload = b'A' * 40 + p64(0x401196)
p.sendline(payload)
p.interactive()

gdb.attach(p, gdbscript) принимает tube и строку с GDB-командами. Pwntools откроет отладчик в соседней панели tmux, установит брейкпоинт на main+42 и продолжит выполнение. В основном окне скрипт отправит payload. В GDB-панели в реальном времени видно состояние стека в момент переполнения: x/20gx $rsp покажет содержимое, где среди A-шек (0x4141414141414141) должен стоять адрес win-функции. Если адрес не на месте — offset неправильный, корректируем и перезапускаем.

Обязательное условие: скрипт работает из-под tmux. Без мультиплексора gdb.attach() не знает, куда открыть окно, и падает с ошибкой. Альтернатива — gdb.debug('./vuln', gdbscript): процесс стартует сразу под GDB с остановкой на entry point, что удобнее для пошагового прохождения пролога функции.

Один скрипт pwntools для теста и удалённого сервера

Стандартный CTF-workflow: отладка локально, эксплуатация на удалённом сервере. Разница — одна строка: process() для локального процесса, remote() для сетевого подключения. Утилита pwn template генерирует скрипт с готовым переключением, но суть механизма проще показать напрямую:

from pwn import *

context.arch = 'amd64'
elf = ELF('./vuln')

if args.REMOTE:
    p = remote('ctf.example.com', 1337)
else:
    p = process('./vuln')

payload = b'A' * 40 + p64(elf.symbols['win'])
p.sendline(payload)
p.interactive()

args.REMOTE — встроенная механика из модуля pwnlib.args. При запуске python3 exploit.py REMOTE переменная равна True. Без аргумента — False, скрипт работает локально. Один файл, два режима, ноль правок между тестом и боевым запуском.

Tube-абстракция гарантирует единый интерфейс: sendline(), recvuntil(), recvline(), interactive() работают идентично для process, remote и SSH. Если перед отправкой payload нужно дождаться приглашения — p.recvuntil(b'Enter name: ') остановит скрипт до появления нужной строки в выводе. Документация pwntools описывает pwnlib.tubes как модуль, который «предоставляет стандартный интерфейс для общения с процессами, сокетами, последовательными портами и прочим». На практике это значит: написал эксплойт для локального бинарника, поменял одну строчку — и он летит на сервер.

Три ошибки, на которых горят CTF-игроки

Забытый context.arch. Без явного context.arch = 'amd64' pwntools может использовать i386 по умолчанию. Результат: cyclic() генерирует паттерн с 4-байтовыми блоками вместо 8-байтовых, asm() собирает код для 32-битной архитектуры, cyclic_find() не находит совпадение. Симптом: всё выглядит правильным, но эксплойт молча падает. Решение: context.arch — первая строка после импорта. Всегда.

sendline() вместо send(). sendline() добавляет \n к payload. Если уязвимая функция — gets() или scanf("%s"), перевод строки нужен: эти функции читают ввод до \n. Но если используется read(fd, buf, n) с фиксированной длиной чтения, лишний байт сдвигает весь payload на одну позицию. Return address уезжает — эксплойт падает с segfault при визуально правильном содержимом. Правило: gets() / scanf() — sendline(), read() — send(). Проверяйте дизассемблирование, чтобы знать, какая функция ввода используется.

Stack alignment в x64. На amd64 инструкции SSE (movaps и подобные) в libc требуют выравнивания стека по 16 байт. Если перед вызовом system() внутри win-функции стек не выровнен — segfault, даже при правильном адресе и offset. Симптом: в GDB видно, что RIP указывает на win, но при вызове libc-функции происходит краш. Решение: вставить ret-гаджет (одна инструкция ret) перед адресом win. Гаджет выравнивает стек за счёт дополнительного pop. Найти его можно через ROPgadget --binary ./vuln | grep ': ret$' или через pwntools-класс ROP. Payload становится: b'A' * offset + p64(ret_gadget) + p64(win_addr). Проблема специфична для x64 — на x86 выравнивание по 16 байт не требуется, поэтому многие, переходящие с 32-битных тасков, о ней даже не подозревают. Я сам потерял на этом пару часов на первом x64-таске, пока не догадался посмотреть, на какой именно инструкции падает system().

За пределами ret2win: ROP chain в pwntools

Ret2win — стартовая точка. Когда в бинарнике нет готовой win-функции, нужен Return-Oriented Programming: выполнение произвольных действий через цепочку гаджетов — коротких последовательностей инструкций, заканчивающихся на ret.

Pwntools содержит класс ROP(elfs, base=None, badchars=b''), автоматизирующий поиск гаджетов и построение цепочек. Типичный сценарий — ret2libc: вызвать system("/bin/sh") из подгруженной libc. Для этого нужен адрес system в libc, адрес строки /bin/sh и ROP-гаджет для передачи аргумента через регистр RDI (на amd64 первый аргумент функции передаётся через RDI).

При переходе к удалённым таскам возникает штука, которую в сообществе называют «libc hell»: эксплойт работает локально, но падает на сервере из-за разных версий glibc — offset'ы функций не совпадают. Утилита pwninit решает это: скачивает нужный линкер, патчит бинарник под серверную libc и гарантирует совпадение окружений. Для heap-задач, где критично поведение конкретной версии аллокатора, есть glibc-all-in-one.

Для отработки ROP — ресурс ROP Emporium: восемь задач, сфокусированных исключительно на построении цепочек без сложного реверсинга. После него — Nightmare (коллекция реальных CTF-задач, организованных по техникам: ret2libc, stack pivot, format string) и pwn.college (университетский курс от создателей Shellphish — бесплатный, с видеолекциями и лабами).

Бинарная эксплуатация — один из самых честных треков в CTF. Тут нельзя обойтись перебором или удачей, как иногда бывает в web-категории. Либо ты понимаешь, что происходит на стеке, либо эксплойт не работает — промежуточного состояния нет. Pwntools убирает рутину, но не подменяет это понимание. Я видел десятки людей, бросающих pwn после второго таска — не потому что не распознали уязвимость, а потому что не смогли доставить payload. Ручная упаковка адресов, танцы с echo -ne, подбор offset строкой из A — это не эксплуатация, это трение. Pwntools снимает трение, и после первого ret2win, закрытого за десять минут вместо двух часов, мотивация двигаться дальше появляется сама. Не перескакивайте на heap и format string, пока ret2win и ret2libc не решаются уверенно. Каждый из этих классов задач закрепляет фундаментальное понимание стека, без которого всё остальное — заучивание рецептов. ROP Emporium, Nightmare, потом live-CTF — прогрессия проверена. А если помимо бинарщины нужна и веб-часть для подготовки к OSCP — на WAPT эту связку проходят в первой трети курса.

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

Поделиться

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

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

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

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

OSINT разведка для CTF: дорки, Shodan, фото

13 мин.

2

OSINT разведка для CTF: дорки, Shodan, фото

Конкретные Google Dorks, Shodan-запросы и приёмы reverse image search для OSINT-задач на CTF. Пошаговые воркфлоу с командами, готовые шаблоны дорков и цепочки pivot'ов.

2 НОЯБРЬ, 2026

Wireshark анализ PCAP: пароли и секреты в CTF

12 мин.

1

Wireshark анализ PCAP: пароли и секреты в CTF

Пошаговый алгоритм разбора PCAP в CTF: фильтры для credential hunting, Export Objects, Follow TCP Stream, tshark и ловушки авторов тасков.

1 НОЯБРЬ, 2026

SQL-инъекции в CTF: UNION, blind и sqlmap

13 мин.

2

SQL-инъекции в CTF: UNION, blind и sqlmap

Пошаговый разбор UNION-based, boolean-blind и time-based SQL-инъекций с пейлоадами, командами sqlmap и алгоритмом выбора техники на CTF

1 НОЯБРЬ, 2026