![]() |
https://forum.antichat.xyz/attachmen...3260703338.png
Buffer overflow давно перестал быть историей только про учебные примеры с shellcode в стеке. Защиты стали жёстче, прямолинейная эксплуатация ломается всё чаще, а значит и сам подход к pwn давно сместился от простого переписывания адреса возврата к аккуратной работе с тем, что уже загружено в память процесса. ret2libc - как раз та техника, на которой этот переход особенно хорошо виден. Она не выглядит экзотикой, но отлично показывает, как устроена современная бинарная эксплуатация: сначала понять, какие защиты включены, потом получить утечку, вычислить базу libc и уже после этого собрать рабочую цепочку выполнения. Binary Exploitation В бинарной эксплуатации почти никогда не хватает одной уязвимости. Сам факт переполнения буфера ещё не означает, что из процесса сразу получится вытащить shell. Всё упирается в то, какие защиты включены, насколько контролируется стек, можно ли исполнять код из данных, рандомизируются ли адреса и насколько удобно привязаться к уже загруженным библиотекам. Именно поэтому ret2libc остаётся такой важной техникой. Она показывает, как эксплойт перестаёт быть тупым прыжком в shellcode и превращается в более взрослую схему: сначала получить контроль над возвратом, потом вытащить адрес из памяти процесса, вычислить базу libc и только после этого направить выполнение в system("/bin/sh") или другой полезный вызов. То есть не просто переписать RIP, а заставить процесс работать на себя в условиях, где прямое исполнение уже ограничено. NX, ASLR, Canary, PIE Перед разбором самого overflow давайте разберемся, какие ограничения задаёт бинарник и окружение. Именно они определяют, какой путь эксплуатации вообще имеет смысл. NX - это защита, которая запрещает исполнение кода в областях памяти, предназначенных для данных. Если стек неисполняемый, классический сценарий с инъекцией shellcode в буфер сразу отпадает. Придётся использовать уже существующий код в памяти процесса. ASLR - рандомизация адресного пространства. Она сдвигает адреса библиотек, стека, кучи и иногда самого бинарника. Это ломает эксплуатацию по фиксированным адресам: даже если однажды получилось найти system(), в следующем запуске этот адрес уже будет другим. Именно поэтому в ret2libc почти всегда нужен leak - утечка адреса, от которой потом считается база libc. Stack Canary - защитное значение в стеке, которое проверяется перед возвратом из функции. Если переполнение задевает canary, программа завершится до того, как управление перейдёт к атакующему. Это не делает баг неэксплуатируемым автоматически, но сильно меняет механику. PIE - позиционно-независимый исполняемый файл. Если PIE включён, рандомизируется и база самого бинарника. Это усложняет жизнь, потому что начинают плавать не только адреса libc, но и адреса внутри самого ELF: plt, got, гаджеты, полезные функции и точки возврата. В нашем сценарии интерес представляет такой набор условий: NX включён, ASLR включён, stack canary отсутствует, а PIE либо отключён, либо его влияние разбирается отдельно. Это уже достаточно реалистичная конфигурация, чтобы классический shellcode не сработал, а ret2libc стал не декоративным приёмом, а нормальным способом обойти ограничения среды. Анализ бинарника В ret2libc всё начинает ломаться не на этапе вызова Код:
system()Смысл этого этапа простой: сначала выяснить, какие защиты реально мешают, затем найти уязвимую функцию и только после этого определить точный offset (смещение) до адреса возврата. Без этого ret2libc быстро превращается в возню с неверными адресами, сломанным стеком и бесконечными сегфолтами, которые ничего не объясняют. checksec: определение защит Первое, что обычно делают с бинарником, - проверяют его защитный профиль. Здесь важен не сам факт запуска Код:
checksecТипичный старт выглядит так: Bash: Код:
checksec --fileКод:
fileКод:
checksecВ контексте ret2libc особенно важны четыре поля: ЗащитаЧто означает для эксплуатацииNXshellcode в стеке уже не выглядит нормальным путёмCanaryпростая перезапись адреса возврата может не дожить до Код:
retВ нашем сценарии самый удобный набор выглядит так: NX включён, Canary выключен, PIE выключен или предсказуем, RELRO не мешает использовать GOT для утечки. Это не делает эксплуатацию тривиальной, но задаёт правильную модель: код в стеке не исполнить, зато можно опираться на уязвимую функцию, фиксированные участки бинарника и утечку libc. Если же PIE включён, все ниже сказанное не становится бесполезным - просто растёт цена ошибки. В таком случае вместе с libc придётся отдельно разбираться и с базой самого ELF. Для нашего первого разбора ret2libc это уже лишний слой сложности, поэтому PIE мы отключим)). Disassembly: vulnerable function После checksec следующий вопрос уже более приземлённый: где именно происходит переполнение. Тут в дело идут Код:
objdumpКод:
gdbКод:
gefКод:
pwndbgКод:
radare2Базовый вариант: Bash: Код:
objdump -d ./vuln
Здесь важно не только найти опасный вызов, но и понять форму стека в этой функции. Какого размера буфер. Есть ли сохранённый Код:
rbpКод:
ripКод:
leave; retЕсли смотреть на это глазами эксплуатации, то цель этапа очень простая: убедиться, что переполнение реально доходит до адреса возврата, а не просто портит локальные переменные и завершает программу крашем без управления потоком выполнения. Определение offset через cyclic pattern После того как стало понятно, что переполнение действительно есть, остаётся самый полезный прикладной шаг - вычислить точное смещение до адреса возврата. Здесь уже не надо угадывать размер буфера на глаз. Для этого и используют cyclic pattern - специальную последовательность, по которой потом можно точно понять, какой участок входа попал в Код:
RIPВ Pwntools это обычно выглядит так: Python: Код:
fromКод:
RIPPython: Код:
cyclic_findКод:
puts@pltЗдесь можно легко ошибиться в трёх местах: ОшибкаК чему приводитперепутать разрядность Код:
cyclic_findКод:
rbpВ x86_64 особенно важно помнить, что работа идёт уже не с Код:
EIPКод:
RIPЦитата:
ret2libc становится по-настоящему интересной в тот момент, когда выясняется простая вещь: одного контроля над Код:
RIPКод:
system()Это и есть переломный момент между учебным переполнением и нормальной эксплуатацией. Пока атакующий не получил реальный адрес одной из функций libc в текущем запуске, он не знает, где находится сама библиотека. А без базы libc нельзя надёжно вычислить ни Код:
system()Код:
"/bin/sh"PLT и GOT: механика Чтобы понять, откуда вообще брать утечку, нужно быстро разобрать две ключевые таблицы в ELF: PLT и GOT. PLT (Procedure Linkage Table) - это набор stub-функций внутри бинарника, через которые вызываются внешние функции вроде Код:
putsКод:
readКод:
writeКод:
printfGOT (Global Offset Table) - таблица указателей, в которой после разрешения символов хранятся реальные адреса функций из загруженных библиотек. Для эксплуатации это важно по одной причине: если программа уже использует, например, Код:
puts()Именно поэтому ret2libc часто начинается не с вызова Код:
system()Код:
puts(puts@got)Если бинарник без PIE, работа становится удобнее: адреса Код:
pltКод:
gotputs@plt для leaking Самый классический сценарий - использовать Код:
puts@pltКод:
putsКод:
GOTЛогика здесь очень чистая:
В x86_64 такая цепочка обычно требует gadget’а вида Код:
pop rdi; retКод:
RDI
Парсинг leaked address После утечки начинается ещё одна зона, где легко сделать всё правильно на 90% и всё равно сломать exploit. Полученный адрес надо не просто увидеть, а корректно распарсить. Обычно Код:
puts()Код:
NULLКод:
u64()Типовая логика такая: Python: Код:
leakКод:
system()Есть несколько типовых ловушек: ОшибкаЧто ломаетсясчитывается не та строкавместо адреса берётся мусор из stdoutне учтён Код:
NULLНа этом этапе важно не торопиться. Нормальная ret2libc почти всегда двухшаговая: сначала аккуратный leak, потом расчёт базы, потом уже рабочая цепочка. Именно leak превращает ASLR из жёсткого барьера в задачу на вычисление. Следующая глава как раз про это - как из одного реального адреса перейти к базе libc и понять, с какой именно версией библиотеки вообще приходится работать. Цитата:
Утечка адреса сама по себе ещё ничего не завершает. Она только даёт точку привязки. Дальше задача уже более сухая, но критичная: понять, к какой именно libc относится leak, и вычислить её базовый адрес в памяти процесса. Именно в этот момент ret2libc перестаёт быть красивой идеей и превращается в рабочую математику. Логика здесь простая: если известен реальный адрес функции в памяти и известен её offset внутри конкретной версии libc, то база считается обычным вычитанием. После этого уже можно получить адреса Код:
systemКод:
exitКод:
"/bin/sh"Идентификация libc версии Это место часто недооценивают. На первый взгляд кажется, что достаточно получить реальный адрес Код:
putsКод:
libc_baseВ идеальной ситуации нужная libc уже есть под рукой. Например, challenge поставляется вместе с бинарником и файлом Код:
libc.so.6Python: Код:
elflibc database - это база сигнатур libc, где по утёкшим адресам или offsets можно подобрать версию библиотеки и восстановить расположение нужных символов. Но здесь есть важный нюанс. Один leak не всегда даёт однозначный результат. Иногда можно получить несколько кандидатов с одинаковыми младшими байтами. Тогда приходится либо искать ещё одну функцию, либо учитывать окружение, архитектуру, версию glibc и другой контекст. Иначе exploit может почти работать локально и стабильно разваливаться на remote. Если libc известна, расчёт дальше становится прямолинейным. Если libc неизвестна, утечка должна не просто существовать, а позволять достаточно надёжно определить саму библиотеку. Иначе offsets превращаются в угадывание. Расчёт offsets После того как версия libc определена, начинается самая приятная часть - обычная арифметика. Если утёк реальный адрес Код:
putsКод:
putsPython: Код:
libc_basePython: Код:
system_addrКод:
system()Код:
"/bin/sh"Есть несколько типовых мест, где всё ломается: ОшибкаЧто идёт не такневерная libc версияoffsets не совпадают, Код:
system()Ещё одна частая ловушка - слишком ранняя уверенность в результате. Если Код:
libc_baseКод:
system()Код:
"/bin/sh"На этом этапе у атакующего уже есть всё, что нужно для финальной стадии: контроль над возвратом, известный offset, утечка libc и вычисленная база. Дальше остаётся собрать ROP-цепочку так, чтобы процесс вызвал Код:
system("/bin/sh")Построение ROP-chain К этому моменту уязвимость уже не выглядит абстрактной. Есть точный offset до адреса возврата, есть утечка, есть вычисленная база libc. Значит, остаётся самое важное - собрать цепочку так, чтобы управление не просто ушло из уязвимой функции, а дошло до нужного вызова в правильном порядке и с корректными аргументами. И вот здесь ret2libc быстро перестаёт быть красивой формулой из write-up’ов. На практике эксплойт ломается не потому, что system() недоступна, а потому что стек криво выровнен, нужный аргумент не попал в регистр, gadget оказался неудобным, а цепочка развалилась раньше, чем дошла до полезного вызова. Поэтому ROP на этом этапе - уже не дополнение к ret2libc, а её рабочий каркас. ROPgadget: поиск гаджетов ROP (Return-Oriented Programming) - это техника, при которой выполнение собирается из коротких фрагментов уже существующего кода, обычно заканчивающихся инструкцией ret. В x86_64 для ret2libc почти всегда нужен хотя бы один базовый gadget: pop rdi; ret Он нужен потому, что первый аргумент функции по системному соглашению вызова передаётся через регистр RDI. Если задача - вызвать system("/bin/sh"), то адрес строки "/bin/sh" сначала должен попасть именно туда. Искать gadget’ы обычно начинают так: Bash: Код:
ROPgadget --binary ./vulnBash: Код:
ropper --file ./vuln --searchДля первого учебного сценария удобнее всего брать gadget из самого ELF, особенно если PIE отключён. Тогда его адрес не плавает между запусками, и эксплойт не усложняется лишней зависимостью от базы самого бинарника. system("/bin/sh") chain Когда база libc уже вычислена, минимальная ret2libc-цепочка выглядит довольно компактно. В x86_64 она обычно строится так:
Python: Код:
payloadНа практике exit() в конце не всегда обязателен, но часто полезен. Если цепочка по какой-то причине завершится без него, процесс может упасть грязнее, чем хотелось бы. Для локальной отладки это ещё терпимо. Для remote exploitation лишняя стабильность обычно не мешает. Есть и более короткие варианты. Иногда вместо явной цепочки до system() используют one_gadget, если условия окружения позволяют. Но это уже не такой чистый учебный сценарий, и нам для понимания ret2libc классический вызов system("/bin/sh") полезнее. Stack alignment и ret gadget Одна из самых раздражающих проблем в x86_64 - стек может быть выровнен неправильно, и цепочка, которая “почти правильная”, начнёт падать на вызове libc-функции без очевидной причины. Это особенно заметно на современных glibc, где часть инструкций внутри функций чувствительна к alignment. Stack alignment - это корректное выравнивание стека по границе, ожидаемой соглашением вызова и реализацией libc. Из-за этого в ret2libc-цепочке часто появляется ещё один лишний ret перед system(): Python: Код:
payloadИменно поэтому stack alignment - не мелкая деталь, а полноценная часть эксплуатации на x86_64. Если адреса верные, offset верный, gadget’ы на месте, а цепочка всё равно ведёт себя нестабильно, первым подозреваемым часто становится именно выравнивание стека. На этом этапе ret2libc уже почти собрана. Дальше остаётся сделать последний практический шаг: оформить всё это в нормальный exploit script на Pwntools, который сначала работает локально, а потом переносится на remote-сценарий. Цитата:
Когда все подготовительные шаги уже сделаны, эксплоит перестаёт быть набором отдельных приёмов и превращается в нормальный рабочий сценарий. Сначала бинарник анализируется, потом находится точный offset, после этого вытаскивается адрес из libc, считается база, собирается цепочка и только в самом конце всё это укладывается в один скрипт. Именно на этом этапе становится понятно, насколько хорошо была собрана вся логика до него. Pwntools здесь удобен не потому, что делает магию за атакующего, а потому что убирает лишнюю возню вокруг байтов, сокетов, упаковки адресов и переключения между локальным и удалённым режимом. Это хороший инструмент именно для того, чтобы эксплоит выглядел как последовательность шагов, а не как хаотичный набор отправок в процесс. Полный код Обычно такой скрипт строится в две стадии. Первая получает утечку и возвращает программу в безопасную точку повторного ввода. Вторая уже использует вычисленную базу libc и отправляет финальную цепочку. Типовой шаблон выглядит так: Python: Код:
fromLocal → Remote exploitation Переход от локальной эксплуатации к удалённой почти всегда оказывается менее красивым, чем хотелось бы. Именно тут всплывают несовпадения libc, разные версии glibc, иные адреса гаджетов при PIE, другая буферизация вывода, лишние строки в баннере, различия в тайминге и прочие мелочи, которые локально были незаметны. Из-за этого нормальный эксплоит лучше изначально писать так, чтобы переключение между локальным и удалённым режимом было частью конструкции, а не аварийной переделкой в последний момент. Именно поэтому в Pwntools обычно делают отдельную функцию запуска и выбирают режим через аргумент: Python: Код:
defСамая частая проблема на этом этапе - уверенность, что если эксплоит сработал локально, то дальше всё уже вопрос одной кнопки. На деле удалённая эксплуатация чаще всего ломается в трёх местах: ПроблемаЧто обычно происходитдругая libcбаза считается правильно, но offsets не совпадаютиной вывод программыутечка считывается криво или не из той строкинестабильная цепочкалокально работает, удалённо падает из-за выравнивания или тайминга Поэтому переход на удалённую цель всегда лучше проверять по частям. Сначала убедиться, что leak приходит корректно. Потом проверить, что база libc выглядит правдоподобно. И только после этого отправлять финальную цепочку. Если пытаться отлаживать всё сразу, эксплоит очень быстро превращается в шум из догадок. В pwn одна из самых полезных привычек - относиться к эксплоиту не как к финальному артефакту, а как к цепочке проверяемых шагов. Offset отдельно. Leak отдельно. База отдельно. Финальный вызов отдельно. Именно так ret2libc и перестаёт быть чем-то невероятным. Она становится обычной технической процедурой (хоть и довольно муторной), где каждая ошибка локализуется достаточно быстро, если не пытаться перепрыгнуть сразу к shell. Подведем итоги ret2libc хороша не тем, что даёт эффектный финал с shell, а тем, что очень трезво показывает, как на самом деле устроена современная бинарная эксплуатация. Одного переполнения уже недостаточно. Нужно понимать защитный профиль бинарника, уметь читать ELF, вытаскивать утечку, считать базу libc и только потом собирать цепочку так, чтобы процесс пошёл по нужному маршруту. Именно на таких техниках pwn и перестаёт быть набором трюков, а превращается в системную работу с памятью, вызовами и средой выполнения. При этом сама идея ret2libc до сих пор не выглядит музейной. Меняются защиты, усложняется окружение, появляются новые ограничения, но логика использования уже загруженного кода вместо прямой инъекции никуда не девается. И чем лучше понятна эта логика, тем проще потом разбираться и с ROP, и с libc leaks, и с более тяжёлыми сценариями эксплуатации, где всё ломается уже не в одной точке, а по цепочке. И вот здесь остаётся вопрос, который всегда отделяет просто решённый challenge от реально понятой техники: в какой момент ret2libc перестаёт быть учебным приёмом и начинает ощущаться как нормальный инженерный подход к эксплуатации памяти? |
| Время: 09:38 |