Сергей Попов
20.04.2026, 11:25
https://forum.antichat.xyz/attachments/4951626/img_eb642f2e5e.png
Каждый раз, когда слышу «бинарные уязвимости мертвы, всё закрыто ASLR и DEP», вспоминаю свой последний CTF - три из четырёх pwn-тасков решались через классические memory corruption примитивы. Просто с поправкой на современные реалии. Митигации усложняют эксплуатацию, но не устраняют её. На подстанции тоже написано «не влезай - убьёт», а электрики как-то работают.
Здесь разберу ключевые техники - от stack overflow до heap exploitation, от ROP-цепочек до JOP - и покажу, как каждая из них работает против ASLR, DEP и CFI. Не в вакууме, а на реальных CTF-задачах и пентестах.
Почему эксплуатация бинарных уязвимостей всё ещё актуальна
По данным CrowdStrike, memory corruption эксплойты остаются одним из сильнейших инструментов в арсенале red team. Они позволяют выполнять payload без взаимодействия с пользователем - тактика Exploitation for Client Execution (T1203) и Exploitation for Privilege Escalation (T1068) по MITRE ATT&CK.
Современные ОС навалили десятки митигаций: DEP/NX, ASLR, stack canaries, CFI, CET, ARM PAC. Но каждая из них - барьер, а не стена. На Security StackExchange хорошо сформулировали: «Все эти техники делают эксплуатацию сложнее, но при достаточном усилии и хороших багах - она далеко не совершенна». Разберёмся, почему.
Stack overflow эксплуатация: от классики к современности
Переполнение буфера на стеке - та самая уязвимость, с которой начинается путь каждого pwn-игрока. Суть проста: программа пишет в буфер больше данных, чем он вмещает, и перезаписывает соседние области памяти - включая сохранённый адрес возврата (saved return address).
В классическом сценарии без митигаций всё тривиально: перезаписываем RIP/EIP адресом шеллкода, лежащего тут же на стеке. Но сегодня такой подход блокируется тремя уровнями защиты одновременно.
Stack canaries - первый барьер
Компилятор вставляет случайное значение (канарейку) между локальными переменными и адресом возврата. Перед
ret
проверяется целостность этого значения. Канарейка померла - процесс аварийно завершается.
Обход требует отдельного примитива: утечка через format string (
%p
,
%x
в
printf
), чтение через out-of-bounds read, или брутфорс в fork-серверах (канарейка не меняется между дочерними процессами - и это подарок).
DEP/NX - запрет исполнения на стеке
Data Execution Prevention маркирует страницы памяти стека и кучи как неисполняемые через биты PTE (Page Table Entry). Даже если вы контролируете содержимое стека - выполнить его как код не получится.
Именно DEP породил целое семейство code-reuse техник: ROP, JOP, COP. Вместо инъекции собственного кода мы взаимствуем уже существующие фрагменты исполняемого кода программы. Раз нельзя принести своё - берём то, что уже лежит.
ASLR - рандомизация адресного пространства
ASLR рандомизирует базовые адреса стека, кучи, библиотек и самого бинаря при каждом запуске. Без знания адресов вы не можете ни перенаправить поток управления, ни построить ROP-цепочку.
Ключевой момент (MIT SHD Labs это подчёркивают): ASLR в Linux работает на уровне гранулярности страниц (4 КБ). Младшие 12 бит адреса остаются неизменными. Более того, ASLR применяет константный сдвиг (delta) для каждого региона - утечка одного указателя из региона раскрывает все адреса в нём. Поэтому information leak - универсальный первый шаг почти любой современной эксплуатации.
Heap exploitation техники: UAF, tcache poisoning, fastbin dup
Если stack overflow - входной билет в мир pwn, то heap exploitation - то, что отделяет новичков от исследователей уязвимостей. Куча - динамическая память, управляемая аллокатором (в glibc - ptmalloc2), и у неё собственная сложная внутренняя кухня.
Use-after-free эксплуатация
Use-after-free возникает, когда программа освобождает объект через
free()
, но сохраняет указатель на него (dangling pointer) и продолжает им пользоваться. Атакующий аллоцирует новый объект того же размера - он займёт место освобождённого - и через старый указатель модифицирует содержимое нового объекта.
По MITRE ATT&CK это часто ведёт к Process Injection (T1055) или к Exploitation for Defense Evasion (T1211).
Моя цепочка для UAF выглядит так: аллоцирую объект с виртуальной таблицей (vtable) или указателем на функцию, освобождаю его, через повторную аллокацию подменяю указатель на контролируемый адрес. При следующем вызове виртуального метода - перехватываю управление. Звучит просто, на практике нужно точно попасть в размер чанка.
Tcache poisoning - специфика glibc 2.26+
С версии glibc 2.26 появился thread-local cache (tcache) - быстрый кэш освобождённых чанков для каждого потока. Tcache работает как односвязный список с минимальными проверками безопасности, что делает его лакомой целью.
Суть tcache poisoning: перезаписываете указатель
next
(он же
fd
- forward pointer) в освобождённом чанке внутри tcache bin, и следующий
malloc()
вернёт произвольный адрес, который вы подставили. Это даёт примитив arbitrary write - запись по произвольному адресу.
В glibc 2.32+ добавили safe-linking - XOR указателя
fd
с адресом самого чанка, сдвинутым на 12 бит вправо. Но и это обходится: достаточно утечки heap-адреса, чтобы восстановить ключ и подделать указатель. Защита усложняет жизнь, но не закрывает дверь.
Fastbin dup и double free
Double free - когда один и тот же блок памяти освобождается дважды. В fastbin (для мелких аллокаций) это приводит к циклу в связном списке: чанк указывает сам на себя. Два последовательных
malloc()
вернут один и тот же адрес, что даёт контроль над метаданными чанка.
Классический fastbin dup в современных версиях glibc блокируется проверкой double-free в tcache. Обход прост: между двумя
free()
одного чанка вставляем
free()
другого чанка того же размера, разрывая проверку.
Разница между tcache poisoning и fastbin dup - не существенная. На CTF я всегда начинаю с проверки версии glibc (
strings libc.so.6 | grep "GNU C Library"
) и выбираю технику под конкретный аллокатор. Версия libc - это первое, что определяет ваш план атаки.
ROP цепочки обход защит: Return Oriented Programming (https://forum.antichat.xyz/threads/587587/)
ROP - фундаментальная техника обхода DEP. Вместо инъекции шеллкода строим цепочку из коротких фрагментов существующего кода - гаджетов - каждый из которых заканчивается инструкцией
ret
.
Механика ROP
Каждый гаджет делает одну элементарную операцию:
pop rdi; ret
загружает значение со стека в регистр RDI,
pop rsi; ret
- в RSI. Выстраивая на стеке последовательность «адрес гаджета → аргумент → адрес следующего гаджета → ...», мы по сути программируем произвольные вызовы из кусочков чужого кода.
Типичная цепочка для получения шелла на Linux x86_64 (System V AMD64 ABI): загружаем адрес строки "/bin/sh" в RDI через
pop rdi; ret
, обнуляем RSI и RDX, вызываем
execve
через syscall-гаджет или через PLT-запись.
Для поиска гаджетов я использую
ROPgadget --binary ./vuln --rop --badbytes "0a|00"
или
ropper
. В pwntools это автоматизируется через класс
ROP
:
Python:
from
pwn
import
*
elf
=
ELF
(
'./vuln'
)
rop
=
ROP
(
elf
)
rop
.
call
(
'puts'
,
[
elf
.
got
[
'puts'
]
]
)
# утекаем адрес libc
rop
.
call
(
elf
.
symbols
[
'main'
]
)
# возвращаемся в main для второго этапа
log
.
info
(
rop
.
dump
(
)
)
Этот фрагмент строит ROP-цепочку, которая утекает адрес
puts
из GOT (для обхода ASLR) и возвращается в
main
для второго этапа эксплуатации.
Ret2libc и ret2plt - частные случаи ROP (https://forum.antichat.xyz/threads/592450/)
Ret2libc - классическая вариация, где вместо произвольных гаджетов вызываем функции стандартной библиотеки:
system("/bin/sh")
или
mprotect()
для снятия NX-бита со страницы. Ret2plt работает даже при включённом ASLR с PIE, потому что PLT-таблица доступна по известному смещению от базы бинаря.
По данным Fortinet, ret2plt особенно хорошо работает на ARM-архитектуре: цепочка
puts@plt
→ утечка адреса libc → вычисление базы → вызов
system
.
JOP: Jump Oriented Programming как альтернатива ROP
ROP-цепочки зависят от инструкции
ret
. Современные митигации - вроде Intel CET (Control-flow Enforcement Technology) с Shadow Stack - защищают именно
ret
: Shadow Stack хранит теневую копию адресов возврата, и любое расхождение вызывает исключение.
JOP (Jump Oriented Programming) использует гаджеты, заканчивающиеся на
jmp reg
или
jmp [reg]
вместо
ret
. Shadow Stack обходится, потому что
jmp
с ним не взаимодействует.
Ключевое отличие JOP от ROP: в JOP нет автоматического продвижения по стеку через
ret
, поэтому нужен диспетчерский гаджет (dispatcher gadget) - фрагмент кода, который увеличивает индекс и передаёт управление следующему функциональному гаджету через косвенный переход. По сути, вы строите свой маленький интерпретатор из обломков чужого кода.
На практике JOP сложнее: нужен стабильный dispatcher и достаточное количество
jmp
-гаджетов. Но в крупных бинарях (браузеры, ядро ОС) их обычно хватает. COP (Call-Oriented Programming) - ещё одна вариация, использующая
call reg
гаджеты.
По данным CrowdStrike, в Windows код-реюз техники (ROP, COP, JOP) применяются для динамического вызова API-функций типа
VirtualProtect()
или
VirtualAlloc()
через Native API (T1106, Execution), чтобы выделить RWX-память и разместить там шеллкод.
Обход ASLR DEP: сравнение подходов
Обход ASLR и DEP - почти всегда двухэтапный процесс. Сначала ломаем ASLR (утекаем адрес), потом ломаем DEP (строим ROP/JOP). Рассмотрим техники обхода ASLR в сравнении.
Information leak - универсальный метод
Самый надёжный способ: через уязвимость чтения (format string, out-of-bounds read, partial overwrite) утекаем один указатель из нужного региона. ASLR сохраняет относительные расстояния внутри модуля - одного указателя хватает для вычисления всех остальных адресов.
Brute force - грубая сила
На 32-bit системах энтропия ASLR низкая - порядка 8-16 бит для стека и библиотек. Это от 256 до 65536 возможных позиций. При наличии fork-сервера (ASLR не меняется в дочерних процессах) брутфорс занимает секунды.
На 64-bit энтропия значительно выше, и прямой брутфорс нецелесообразен. Partial overwrite - перезапись только младших байтов адреса - снижает пространство перебора до нескольких бит.
Heap spraying - вероятностный подход
Heap spray заполняет кучу большим объёмом контролируемых данных, увеличивая вероятность того, что произвольный адрес попадёт в наш payload. Техника особенно живуча в браузерных эксплойтах в связке с JavaScript. По анализу Patsnap Eureka, heap spraying часто комбинируется с DEP-обходом.
Микроархитектурные side-channel атаки
По данным MIT SHD Labs, ASLR можно сломать аппаратно: через prefetch side channels (инструкция
prefetch
ведёт себя по-разному для mapped и unmapped страниц), через speculative probing (спекулятивное исполнение позволяет «пощупать» адресное пространство без крэша), и через egghunter-подход (сигнальные обработчики для перехвата SIGSEGV). Железо иногда предаёт свою же софтверную защиту.
Техника обхода ASLRТребуемый примитивПрименимость 64-bitНадёжностьInformation leakЧтение по произвольному адресуДаВысокаяBrute forceМногократный запускНет (слишком долго)НизкаяPartial overwriteЗапись 1-2 байтЧастичноСредняяHeap sprayКонтроль аллокацийДаСредняяPrefetch side-channelЛокальное исполнениеДаВысокая
Control Flow Integrity обход: передний край
CFI - наиболее серьёзная из современных митигаций. Она проверяет, что каждый косвенный переход (
call reg
,
jmp reg
,
ret
) ведёт в «легитимную» точку программы. Microsoft реализует это как CFG (Control Flow Guard), Clang - как forward-edge CFI, Intel CET добавляет аппаратный Shadow Stack и Indirect Branch Tracking (IBT).
Ограничения CFI на практике
На Security StackExchange хорошо сказано: «perfect CFI basically doesn't exist». И вот почему.
Грубая гранулярность. CFG в Windows проверяет, что цель перехода - начало какой-либо функции. Но не проверяет, какой именно. Любая функция с подходящей сигнатурой - валидная цель. Если в программе есть
system()
или
WinExec()
- CFG не помешает переходу к ним. Формально всё легитимно.
Модули без CFI. Если хотя бы одна загруженная DLL не скомпилирована с CFG, атакующий может перенаправить поток в неё. Как отмечают исследователи: «it can be bypassed by jumping into a module which doesn't use CFG». Одно слабое звено ломает всю цепочку.
Утечка скрытых данных. PaX RAP (kernel CFI на Linux) XOR-ит адрес возврата с ключом, хранящимся в регистре. Но зашифрованный указатель лежит на стеке между вызовами функций. Если у атакующего есть ASLR-утечка и чтение стека - он вычисляет ключ, подделывает зашифрованный адрес возврата и обходит защиту.
JOP вместо ROP. Shadow Stack от Intel CET защищает
ret
, но не защищает
jmp reg
. JOP-цепочки остаются рабочим вектором, хотя IBT (Indirect Branch Tracking) ограничивает цели
jmp
инструкциями с маркером
endbr64
.
Сравнение техник эксплуатации и митигаций
ТехникаОбходит DEPОбходит ASLRОбходит canariesОбходит CFIСложностьКлассический stack overflow + shellcodeНетНетНетНетНизкаяROP chainДаНужен leakНужен leak/обходЧастичноСредняяJOP chainДаНужен leakНужен leak/обходДа (Shadow Stack)ВысокаяRet2libc / ret2pltДаНужен leakНужен leak/обходЧастичноСредняяTcache poisoningДа (arbitrary write)Нужен heap leakНе применимоНе применимоСредняяUse-after-free + vtable overwriteДаНужен leakНе применимоЗависит от гранулярностиВысокаяHeap spray + pivotЧастичноЧастичноНе применимоНетСредняя
Практический workflow: от анализа бинаря до шелла
Вот пошаговый процесс, который я использую при решении pwn-тасков и при пентесте бинарных приложений. Потренировавшись CTF-таски уровня easy/medium, можно уверенно переходить к реальным целям.
🔓 Часть контента скрыта: Эксклюзивный контент для зарегистрированных пользователей.
Зарегистрироваться
или
Войти
Шаг 1. Разведка бинаря. Запускаем
checksec ./vuln
из pwntools - он покажет статус NX, PIE, RELRO, stack canary, Fortify. Это определяет доступные техники. NX включён, PIE выключен - ROP через сам бинарь. PIE включён - нужен leak перед построением цепочки.
Шаг 2. Поиск уязвимости. В GDB с pwndbg анализируем опасные функции:
gets
,
strcpy
,
sprintf
,
read
с недостаточной проверкой размера. В radare2 -
afl
(list functions) и
pdf @ sym.vulnerable_function
(дизассемблирование). Для хип-эксплуатации ищем паттерны allocate-free-use. Если видите
free()
без обнуления указателя - это почти наверняка ваш вектор.
Шаг 3. Получение leak. Утечка адреса libc через GOT: перезаписываем адрес возврата на
puts@plt
с аргументом
puts@got
, получаем runtime-адрес
puts
в libc, вычисляем базу libc. Для heap leak - утечка через UAF или unsorted bin (чанки в unsorted bin содержат указатели на
main_arena
в libc).
Шаг 4. Построение цепочки. На втором проходе (после
ret
обратно в
main
) используем известные адреса для финальной ROP-цепочки. Типичный финал -
execve("/bin/sh", NULL, NULL)
через syscall или
system("/bin/sh")
через libc.
Python:
from
pwn
import
*
p
=
process
(
'./vuln'
)
elf
=
ELF
(
'./vuln'
)
libc
=
ELF
(
'./libc.so.6'
)
# Этап 1: утекаем адрес libc
rop1
=
ROP
(
elf
)
rop1
.
call
(
'puts'
,
[
elf
.
got
[
'puts'
]
]
)
rop1
.
call
(
elf
.
symbols
[
'main'
]
)
p
.
sendline
(
b'A'
*
offset
+
rop1
.
chain
(
)
)
leaked
=
u64
(
p
.
recvline
(
)
.
strip
(
)
.
ljust
(
8
,
b'\x00'
)
)
libc
.
address
=
leaked
-
libc
.
symbols
[
'puts'
]
# Этап 2: получаем шелл
rop2
=
ROP
(
libc
)
rop2
.
call
(
'system'
,
[
next
(
libc
.
search
(
b'/bin/sh\x00'
)
)
]
)
p
.
sendline
(
b'A'
*
offset
+
rop2
.
chain
(
)
)
p
.
interactive
(
)
Этот двухэтапный паттерн - рабочая лошадка для большинства CTF-задач уровня medium и реальных бинарных уязвимостей с включёнными ASLR и DEP.
Шаг 5. Отладка. Если цепочка крэшит - подключаемся через
gdb.attach(p)
в pwntools или запускаем бинарь под
gdb
с
pwndbg
. Частая причина крэша на x86_64 - нарушение выравнивания стека:
system()
требует 16-байтного выравнивания RSP. Решение - добавить
ret
-гаджет перед вызовом
system
для сдвига стека на 8 байт. Вот у меня на это уходило больше времени, чем на саму эксплуатацию - пока не выработал привычку сразу добавлять
ret
перед
system
.
Что дальше: аппаратные митигации и их обход
Intel CET (Shadow Stack + IBT), ARM Pointer Authentication (PAC) и Memory Tagging Extension (MTE) - следующий рубеж обороны. Shadow Stack делает ROP значительно сложнее, PAC подписывает указатели криптографическим ключом, MTE тегирует каждое выделение памяти.
Но исследования не стоят на месте. JOP обходит Shadow Stack. PAC-ключи можно восстановить через side-channel или через signing gadgets - фрагменты кода, которые легитимно подписывают указатели (по сути, заставляем программу подписать нашу подделку за нас). MTE имеет ограниченную энтропию (4 бита тега) и обходится через brute force или speculative execution.
В терминах MITRE ATT&CK - развитие эксплойтов (T1587.004, Resource Development) и исследование уязвимостей (T1588.006, Resource Development). Понимание этих техник критично и для атакующей, и для оборонительной стороны.
Вопрос к читателям
При двухэтапной эксплуатации с leak через
puts@plt
на glibc 2.35+ с safe-linking в tcache - какой метод получения heap leak вы используете для второго этапа, если бинарь собран с Full RELRO и PIE? Через unsorted bin leak (аллокация > 0x410 байт для обхода tcache), через
IO_2_1_stdout
partial overwrite, или через другой примитив? Покажите фрагмент вашей цепочки pwntools для
DynELF
или ручного разрешения libc symbols в этом сценарии.
Каждый раз, когда слышу «бинарные уязвимости мертвы, всё закрыто ASLR и DEP», вспоминаю свой последний CTF - три из четырёх pwn-тасков решались через классические memory corruption примитивы. Просто с поправкой на современные реалии. Митигации усложняют эксплуатацию, но не устраняют её. На подстанции тоже написано «не влезай - убьёт», а электрики как-то работают.
Здесь разберу ключевые техники - от stack overflow до heap exploitation, от ROP-цепочек до JOP - и покажу, как каждая из них работает против ASLR, DEP и CFI. Не в вакууме, а на реальных CTF-задачах и пентестах.
Почему эксплуатация бинарных уязвимостей всё ещё актуальна
По данным CrowdStrike, memory corruption эксплойты остаются одним из сильнейших инструментов в арсенале red team. Они позволяют выполнять payload без взаимодействия с пользователем - тактика Exploitation for Client Execution (T1203) и Exploitation for Privilege Escalation (T1068) по MITRE ATT&CK.
Современные ОС навалили десятки митигаций: DEP/NX, ASLR, stack canaries, CFI, CET, ARM PAC. Но каждая из них - барьер, а не стена. На Security StackExchange хорошо сформулировали: «Все эти техники делают эксплуатацию сложнее, но при достаточном усилии и хороших багах - она далеко не совершенна». Разберёмся, почему.
Stack overflow эксплуатация: от классики к современности
Переполнение буфера на стеке - та самая уязвимость, с которой начинается путь каждого pwn-игрока. Суть проста: программа пишет в буфер больше данных, чем он вмещает, и перезаписывает соседние области памяти - включая сохранённый адрес возврата (saved return address).
В классическом сценарии без митигаций всё тривиально: перезаписываем RIP/EIP адресом шеллкода, лежащего тут же на стеке. Но сегодня такой подход блокируется тремя уровнями защиты одновременно.
Stack canaries - первый барьер
Компилятор вставляет случайное значение (канарейку) между локальными переменными и адресом возврата. Перед
ret
проверяется целостность этого значения. Канарейка померла - процесс аварийно завершается.
Обход требует отдельного примитива: утечка через format string (
%p
,
%x
в
printf
), чтение через out-of-bounds read, или брутфорс в fork-серверах (канарейка не меняется между дочерними процессами - и это подарок).
DEP/NX - запрет исполнения на стеке
Data Execution Prevention маркирует страницы памяти стека и кучи как неисполняемые через биты PTE (Page Table Entry). Даже если вы контролируете содержимое стека - выполнить его как код не получится.
Именно DEP породил целое семейство code-reuse техник: ROP, JOP, COP. Вместо инъекции собственного кода мы взаимствуем уже существующие фрагменты исполняемого кода программы. Раз нельзя принести своё - берём то, что уже лежит.
ASLR - рандомизация адресного пространства
ASLR рандомизирует базовые адреса стека, кучи, библиотек и самого бинаря при каждом запуске. Без знания адресов вы не можете ни перенаправить поток управления, ни построить ROP-цепочку.
Ключевой момент (MIT SHD Labs это подчёркивают): ASLR в Linux работает на уровне гранулярности страниц (4 КБ). Младшие 12 бит адреса остаются неизменными. Более того, ASLR применяет константный сдвиг (delta) для каждого региона - утечка одного указателя из региона раскрывает все адреса в нём. Поэтому information leak - универсальный первый шаг почти любой современной эксплуатации.
Heap exploitation техники: UAF, tcache poisoning, fastbin dup
Если stack overflow - входной билет в мир pwn, то heap exploitation - то, что отделяет новичков от исследователей уязвимостей. Куча - динамическая память, управляемая аллокатором (в glibc - ptmalloc2), и у неё собственная сложная внутренняя кухня.
Use-after-free эксплуатация
Use-after-free возникает, когда программа освобождает объект через
free()
, но сохраняет указатель на него (dangling pointer) и продолжает им пользоваться. Атакующий аллоцирует новый объект того же размера - он займёт место освобождённого - и через старый указатель модифицирует содержимое нового объекта.
По MITRE ATT&CK это часто ведёт к Process Injection (T1055) или к Exploitation for Defense Evasion (T1211).
Моя цепочка для UAF выглядит так: аллоцирую объект с виртуальной таблицей (vtable) или указателем на функцию, освобождаю его, через повторную аллокацию подменяю указатель на контролируемый адрес. При следующем вызове виртуального метода - перехватываю управление. Звучит просто, на практике нужно точно попасть в размер чанка.
Tcache poisoning - специфика glibc 2.26+
С версии glibc 2.26 появился thread-local cache (tcache) - быстрый кэш освобождённых чанков для каждого потока. Tcache работает как односвязный список с минимальными проверками безопасности, что делает его лакомой целью.
Суть tcache poisoning: перезаписываете указатель
next
(он же
fd
- forward pointer) в освобождённом чанке внутри tcache bin, и следующий
malloc()
вернёт произвольный адрес, который вы подставили. Это даёт примитив arbitrary write - запись по произвольному адресу.
В glibc 2.32+ добавили safe-linking - XOR указателя
fd
с адресом самого чанка, сдвинутым на 12 бит вправо. Но и это обходится: достаточно утечки heap-адреса, чтобы восстановить ключ и подделать указатель. Защита усложняет жизнь, но не закрывает дверь.
Fastbin dup и double free
Double free - когда один и тот же блок памяти освобождается дважды. В fastbin (для мелких аллокаций) это приводит к циклу в связном списке: чанк указывает сам на себя. Два последовательных
malloc()
вернут один и тот же адрес, что даёт контроль над метаданными чанка.
Классический fastbin dup в современных версиях glibc блокируется проверкой double-free в tcache. Обход прост: между двумя
free()
одного чанка вставляем
free()
другого чанка того же размера, разрывая проверку.
Разница между tcache poisoning и fastbin dup - не существенная. На CTF я всегда начинаю с проверки версии glibc (
strings libc.so.6 | grep "GNU C Library"
) и выбираю технику под конкретный аллокатор. Версия libc - это первое, что определяет ваш план атаки.
ROP цепочки обход защит: Return Oriented Programming (https://forum.antichat.xyz/threads/587587/)
ROP - фундаментальная техника обхода DEP. Вместо инъекции шеллкода строим цепочку из коротких фрагментов существующего кода - гаджетов - каждый из которых заканчивается инструкцией
ret
.
Механика ROP
Каждый гаджет делает одну элементарную операцию:
pop rdi; ret
загружает значение со стека в регистр RDI,
pop rsi; ret
- в RSI. Выстраивая на стеке последовательность «адрес гаджета → аргумент → адрес следующего гаджета → ...», мы по сути программируем произвольные вызовы из кусочков чужого кода.
Типичная цепочка для получения шелла на Linux x86_64 (System V AMD64 ABI): загружаем адрес строки "/bin/sh" в RDI через
pop rdi; ret
, обнуляем RSI и RDX, вызываем
execve
через syscall-гаджет или через PLT-запись.
Для поиска гаджетов я использую
ROPgadget --binary ./vuln --rop --badbytes "0a|00"
или
ropper
. В pwntools это автоматизируется через класс
ROP
:
Python:
from
pwn
import
*
elf
=
ELF
(
'./vuln'
)
rop
=
ROP
(
elf
)
rop
.
call
(
'puts'
,
[
elf
.
got
[
'puts'
]
]
)
# утекаем адрес libc
rop
.
call
(
elf
.
symbols
[
'main'
]
)
# возвращаемся в main для второго этапа
log
.
info
(
rop
.
dump
(
)
)
Этот фрагмент строит ROP-цепочку, которая утекает адрес
puts
из GOT (для обхода ASLR) и возвращается в
main
для второго этапа эксплуатации.
Ret2libc и ret2plt - частные случаи ROP (https://forum.antichat.xyz/threads/592450/)
Ret2libc - классическая вариация, где вместо произвольных гаджетов вызываем функции стандартной библиотеки:
system("/bin/sh")
или
mprotect()
для снятия NX-бита со страницы. Ret2plt работает даже при включённом ASLR с PIE, потому что PLT-таблица доступна по известному смещению от базы бинаря.
По данным Fortinet, ret2plt особенно хорошо работает на ARM-архитектуре: цепочка
puts@plt
→ утечка адреса libc → вычисление базы → вызов
system
.
JOP: Jump Oriented Programming как альтернатива ROP
ROP-цепочки зависят от инструкции
ret
. Современные митигации - вроде Intel CET (Control-flow Enforcement Technology) с Shadow Stack - защищают именно
ret
: Shadow Stack хранит теневую копию адресов возврата, и любое расхождение вызывает исключение.
JOP (Jump Oriented Programming) использует гаджеты, заканчивающиеся на
jmp reg
или
jmp [reg]
вместо
ret
. Shadow Stack обходится, потому что
jmp
с ним не взаимодействует.
Ключевое отличие JOP от ROP: в JOP нет автоматического продвижения по стеку через
ret
, поэтому нужен диспетчерский гаджет (dispatcher gadget) - фрагмент кода, который увеличивает индекс и передаёт управление следующему функциональному гаджету через косвенный переход. По сути, вы строите свой маленький интерпретатор из обломков чужого кода.
На практике JOP сложнее: нужен стабильный dispatcher и достаточное количество
jmp
-гаджетов. Но в крупных бинарях (браузеры, ядро ОС) их обычно хватает. COP (Call-Oriented Programming) - ещё одна вариация, использующая
call reg
гаджеты.
По данным CrowdStrike, в Windows код-реюз техники (ROP, COP, JOP) применяются для динамического вызова API-функций типа
VirtualProtect()
или
VirtualAlloc()
через Native API (T1106, Execution), чтобы выделить RWX-память и разместить там шеллкод.
Обход ASLR DEP: сравнение подходов
Обход ASLR и DEP - почти всегда двухэтапный процесс. Сначала ломаем ASLR (утекаем адрес), потом ломаем DEP (строим ROP/JOP). Рассмотрим техники обхода ASLR в сравнении.
Information leak - универсальный метод
Самый надёжный способ: через уязвимость чтения (format string, out-of-bounds read, partial overwrite) утекаем один указатель из нужного региона. ASLR сохраняет относительные расстояния внутри модуля - одного указателя хватает для вычисления всех остальных адресов.
Brute force - грубая сила
На 32-bit системах энтропия ASLR низкая - порядка 8-16 бит для стека и библиотек. Это от 256 до 65536 возможных позиций. При наличии fork-сервера (ASLR не меняется в дочерних процессах) брутфорс занимает секунды.
На 64-bit энтропия значительно выше, и прямой брутфорс нецелесообразен. Partial overwrite - перезапись только младших байтов адреса - снижает пространство перебора до нескольких бит.
Heap spraying - вероятностный подход
Heap spray заполняет кучу большим объёмом контролируемых данных, увеличивая вероятность того, что произвольный адрес попадёт в наш payload. Техника особенно живуча в браузерных эксплойтах в связке с JavaScript. По анализу Patsnap Eureka, heap spraying часто комбинируется с DEP-обходом.
Микроархитектурные side-channel атаки
По данным MIT SHD Labs, ASLR можно сломать аппаратно: через prefetch side channels (инструкция
prefetch
ведёт себя по-разному для mapped и unmapped страниц), через speculative probing (спекулятивное исполнение позволяет «пощупать» адресное пространство без крэша), и через egghunter-подход (сигнальные обработчики для перехвата SIGSEGV). Железо иногда предаёт свою же софтверную защиту.
Техника обхода ASLRТребуемый примитивПрименимость 64-bitНадёжностьInformation leakЧтение по произвольному адресуДаВысокаяBrute forceМногократный запускНет (слишком долго)НизкаяPartial overwriteЗапись 1-2 байтЧастичноСредняяHeap sprayКонтроль аллокацийДаСредняяPrefetch side-channelЛокальное исполнениеДаВысокая
Control Flow Integrity обход: передний край
CFI - наиболее серьёзная из современных митигаций. Она проверяет, что каждый косвенный переход (
call reg
,
jmp reg
,
ret
) ведёт в «легитимную» точку программы. Microsoft реализует это как CFG (Control Flow Guard), Clang - как forward-edge CFI, Intel CET добавляет аппаратный Shadow Stack и Indirect Branch Tracking (IBT).
Ограничения CFI на практике
На Security StackExchange хорошо сказано: «perfect CFI basically doesn't exist». И вот почему.
Грубая гранулярность. CFG в Windows проверяет, что цель перехода - начало какой-либо функции. Но не проверяет, какой именно. Любая функция с подходящей сигнатурой - валидная цель. Если в программе есть
system()
или
WinExec()
- CFG не помешает переходу к ним. Формально всё легитимно.
Модули без CFI. Если хотя бы одна загруженная DLL не скомпилирована с CFG, атакующий может перенаправить поток в неё. Как отмечают исследователи: «it can be bypassed by jumping into a module which doesn't use CFG». Одно слабое звено ломает всю цепочку.
Утечка скрытых данных. PaX RAP (kernel CFI на Linux) XOR-ит адрес возврата с ключом, хранящимся в регистре. Но зашифрованный указатель лежит на стеке между вызовами функций. Если у атакующего есть ASLR-утечка и чтение стека - он вычисляет ключ, подделывает зашифрованный адрес возврата и обходит защиту.
JOP вместо ROP. Shadow Stack от Intel CET защищает
ret
, но не защищает
jmp reg
. JOP-цепочки остаются рабочим вектором, хотя IBT (Indirect Branch Tracking) ограничивает цели
jmp
инструкциями с маркером
endbr64
.
Сравнение техник эксплуатации и митигаций
ТехникаОбходит DEPОбходит ASLRОбходит canariesОбходит CFIСложностьКлассический stack overflow + shellcodeНетНетНетНетНизкаяROP chainДаНужен leakНужен leak/обходЧастичноСредняяJOP chainДаНужен leakНужен leak/обходДа (Shadow Stack)ВысокаяRet2libc / ret2pltДаНужен leakНужен leak/обходЧастичноСредняяTcache poisoningДа (arbitrary write)Нужен heap leakНе применимоНе применимоСредняяUse-after-free + vtable overwriteДаНужен leakНе применимоЗависит от гранулярностиВысокаяHeap spray + pivotЧастичноЧастичноНе применимоНетСредняя
Практический workflow: от анализа бинаря до шелла
Вот пошаговый процесс, который я использую при решении pwn-тасков и при пентесте бинарных приложений. Потренировавшись CTF-таски уровня easy/medium, можно уверенно переходить к реальным целям.
🔓 Часть контента скрыта: Эксклюзивный контент для зарегистрированных пользователей.
Зарегистрироваться
или
Войти
Шаг 1. Разведка бинаря. Запускаем
checksec ./vuln
из pwntools - он покажет статус NX, PIE, RELRO, stack canary, Fortify. Это определяет доступные техники. NX включён, PIE выключен - ROP через сам бинарь. PIE включён - нужен leak перед построением цепочки.
Шаг 2. Поиск уязвимости. В GDB с pwndbg анализируем опасные функции:
gets
,
strcpy
,
sprintf
,
read
с недостаточной проверкой размера. В radare2 -
afl
(list functions) и
pdf @ sym.vulnerable_function
(дизассемблирование). Для хип-эксплуатации ищем паттерны allocate-free-use. Если видите
free()
без обнуления указателя - это почти наверняка ваш вектор.
Шаг 3. Получение leak. Утечка адреса libc через GOT: перезаписываем адрес возврата на
puts@plt
с аргументом
puts@got
, получаем runtime-адрес
puts
в libc, вычисляем базу libc. Для heap leak - утечка через UAF или unsorted bin (чанки в unsorted bin содержат указатели на
main_arena
в libc).
Шаг 4. Построение цепочки. На втором проходе (после
ret
обратно в
main
) используем известные адреса для финальной ROP-цепочки. Типичный финал -
execve("/bin/sh", NULL, NULL)
через syscall или
system("/bin/sh")
через libc.
Python:
from
pwn
import
*
p
=
process
(
'./vuln'
)
elf
=
ELF
(
'./vuln'
)
libc
=
ELF
(
'./libc.so.6'
)
# Этап 1: утекаем адрес libc
rop1
=
ROP
(
elf
)
rop1
.
call
(
'puts'
,
[
elf
.
got
[
'puts'
]
]
)
rop1
.
call
(
elf
.
symbols
[
'main'
]
)
p
.
sendline
(
b'A'
*
offset
+
rop1
.
chain
(
)
)
leaked
=
u64
(
p
.
recvline
(
)
.
strip
(
)
.
ljust
(
8
,
b'\x00'
)
)
libc
.
address
=
leaked
-
libc
.
symbols
[
'puts'
]
# Этап 2: получаем шелл
rop2
=
ROP
(
libc
)
rop2
.
call
(
'system'
,
[
next
(
libc
.
search
(
b'/bin/sh\x00'
)
)
]
)
p
.
sendline
(
b'A'
*
offset
+
rop2
.
chain
(
)
)
p
.
interactive
(
)
Этот двухэтапный паттерн - рабочая лошадка для большинства CTF-задач уровня medium и реальных бинарных уязвимостей с включёнными ASLR и DEP.
Шаг 5. Отладка. Если цепочка крэшит - подключаемся через
gdb.attach(p)
в pwntools или запускаем бинарь под
gdb
с
pwndbg
. Частая причина крэша на x86_64 - нарушение выравнивания стека:
system()
требует 16-байтного выравнивания RSP. Решение - добавить
ret
-гаджет перед вызовом
system
для сдвига стека на 8 байт. Вот у меня на это уходило больше времени, чем на саму эксплуатацию - пока не выработал привычку сразу добавлять
ret
перед
system
.
Что дальше: аппаратные митигации и их обход
Intel CET (Shadow Stack + IBT), ARM Pointer Authentication (PAC) и Memory Tagging Extension (MTE) - следующий рубеж обороны. Shadow Stack делает ROP значительно сложнее, PAC подписывает указатели криптографическим ключом, MTE тегирует каждое выделение памяти.
Но исследования не стоят на месте. JOP обходит Shadow Stack. PAC-ключи можно восстановить через side-channel или через signing gadgets - фрагменты кода, которые легитимно подписывают указатели (по сути, заставляем программу подписать нашу подделку за нас). MTE имеет ограниченную энтропию (4 бита тега) и обходится через brute force или speculative execution.
В терминах MITRE ATT&CK - развитие эксплойтов (T1587.004, Resource Development) и исследование уязвимостей (T1588.006, Resource Development). Понимание этих техник критично и для атакующей, и для оборонительной стороны.
Вопрос к читателям
При двухэтапной эксплуатации с leak через
puts@plt
на glibc 2.35+ с safe-linking в tcache - какой метод получения heap leak вы используете для второго этапа, если бинарь собран с Full RELRO и PIE? Через unsorted bin leak (аллокация > 0x410 байт для обхода tcache), через
IO_2_1_stdout
partial overwrite, или через другой примитив? Покажите фрагмент вашей цепочки pwntools для
DynELF
или ручного разрешения libc symbols в этом сценарии.