Проблема
DEP (Data Execution Prevention) помечает страницы стека и кучи как неисполняемые — это должно было закрыть классический stack buffer overflow, где atacker кладёт shellcode прямо в переполняемый буфер и передаёт на него управление. DEP действительно закрывает именно эту технику.
Но DEP не проверяет, ЧТО исполняется — только ГДЕ. Если можно передать управление на уже загруженный, исполняемый код, DEP не мешает. Отсюда ROP (Return-Oriented Programming): вместо инъекции нового кода атакующий склеивает цепочку из коротких, уже существующих в памяти инструкций, каждая из которых заканчивается на ret («гаджет»), — и вызывает их последовательно через стек. Новый исполняемый код не появляется вообще, поэтому DEP формально не нарушается.
Похожая логика с ASLR (Address Space Layout Randomization): рандомизация базовых адресов модулей должна не дать атакующему знать, где находится нужный ему код. Но ASLR применяется к модулю целиком — если хотя бы один загруженный в процесс модуль скомпилирован без /DYNAMICBASE (или отключил ASLR явно), его адрес предсказуем при каждом запуске, и всех гаджетов для ROP-цепочки из этого одного модуля обычно достаточно.
Egghunter: когда мало места, а не мало прав
Отдельный класс проблем — ограниченный размер controlled-буфера. Например, при SEH-based переполнении атакующему может быть доступно всего несколько десятков байт под непосредственный payload, тогда как полный shellcode занимает сотни. Egghunter решает это разделением: в ограниченное место кладётся крошечный стаб (обычно 30-40 байт), который при исполнении сам ищет в адресном пространстве процесса заранее известную метку («egg», обычно 4-8 байт), а настоящий payload с той же меткой кладётся туда, где места достаточно — в другом месте буфера или кучи.
Технически egghunter обязан проверять доступность каждой страницы памяти перед чтением (через безопасные syscall вроде NtDisplayString или NtAccessCheckAndAuditAlarm, которые возвращают код ошибки вместо падения на невалидном адресе) — иначе он сам обвалит процесс раньше, чем найдёт payload. Отсюда практический вывод для защитника: маленький доступный буфер не гарантирует, что эксплуатация невозможна — он просто требует другой техники, а не останавливает атаку.
Что проверить руками
- DEP/NX: включён ли на бинаре и на всех загружаемых DLL, а не только на основном exe.
- ASLR: у КАЖДОГО модуля в процессе, а не только у главного бинаря — один нерандомизированный модуль обесценивает ASLR для всего процесса.
- CFG (Control Flow Guard) / CET: ограничивают допустимые цели косвенных вызовов, что резко сужает пригодные для ROP гаджеты, даже если ASLR обойдён.
- Stack cookies (
/GS): защищают от классического overwrite return address, но не от SEH-based или heap-based техник. - Комбинация защит на конкретном модуле — не факт «DEP=on», а полный список одновременно включённых mitigations.
Безопасный пример кода
Read-only аудит PE-файла на включённые mitigations через pefile: проверяет DEP (IMAGE_DLLCHARACTERISTICS_NX_COMPAT), ASLR (IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) и CFG (IMAGE_DLLCHARACTERISTICS_GUARD_CF) в заголовке. Скрипт ничего не исполняет и не эксплуатирует — только читает метаданные заголовка.
import pefile
import sys
FLAGS = {
0x0040: "DYNAMIC_BASE (ASLR)",
0x0100: "NX_COMPAT (DEP)",
0x4000: "GUARD_CF (Control Flow Guard)",
0x0400: "NO_SEH",
0x8000: "TERMINAL_SERVER_AWARE",
}
def audit(path: str) -> None:
pe = pefile.PE(path, fast_load=True)
characteristics = pe.OPTIONAL_HEADER.DllCharacteristics
print(f"{path}:")
for bit, name in FLAGS.items():
status = "ON" if characteristics & bit else "OFF"
print(f" {name}: {status}")
if not (characteristics & 0x0040) or not (characteristics & 0x0100):
print(" WARNING: DEP or ASLR disabled - review before shipping")
if __name__ == "__main__":
audit(sys.argv[1])
Как это должно попасть в отчёт
- Signals: модуль без DYNAMIC_BASE/NX_COMPAT среди зависимостей процесса, отсутствие CFG на бинарях, обрабатывающих недоверенный ввод.
- Controls: пересборка со всеми актуальными compiler-флагами, CFG/CET на новых сборках, инвентаризация сторонних DLL на предмет устаревших mitigations.
- Retest: те же флаги PE-заголовка после пересборки, плюс подтверждение, что рантайм-поведение (crash triage) не изменилось непредвиденно.