Как изменить параметры компиляции в уже собранном PE-файле

Вы собрали исполняемый файл, а потом поняли, что нужно переключить оптимизацию, отключить ASLR, поменять уровень оптимизации или включить отладочные символы. Пересобрать проект — вариант идеальный, но не всегда возможный. Может, нет исходников. Может, это чужой бинарник. Может, срочно нужно пропатчить один флаг и не хочется настраивать окружение. Вот тут и возникает вопрос: можно ли изменить compiler flags в уже собранном PE-файле?

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

Почему просто «поменять флаги» не получится

Compiler flags — это инструкции компилятору и линковщику на этапе сборки. Они влияют на то, как генерируется код: какие инструкции процессора используются, как располагаются секции, какие оптимизации применяются, как настраиваются таблицы адресов. Когда файл уже собран, все эти решения «запечены» в бинарнике. Вы не можете сказать компилятору «а давай переделае» — его уже нет.

Однако структура PE-файла содержит множество полей и флагов, которые либо напрямую соответствуют параметрам компиляции, либо на них влияют. Именно их мы и будем менять.

Что можно изменить в готовом PE

1. Флаги заголовка PE (Characteristics и DLL Characteristics)

Это самое очевидное и самое безопасное. В PE-заголовке есть битовые флаги, которые задают базовые свойства исполняемого файла.

Characteristics — поле в файловом заголовке (IMAGE_FILE_HEADER.Characteristics). Вот что из него обычно интересно:

  • IMAGE_FILE_DLL (0x2000) — файл является DLL. Можно снять или установить, но это меняет поведение загрузчика.
  • IMAGE_FILE_LARGE_ADDRESS_AWARE (0x0020) — приложение может работать с адресами выше 2 ГБ. Ставится флагом /LARGEADDRESSAWARE в MSVC.
  • IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP и IMAGE_FILE_NET_RUN_FROM_SWAP — управляют загрузкой с сетевых дисков.

DLL Characteristics — появляются в Optional Header для исполняемых файлов и DLL. Здесь самое интересное:

  • IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE (0x0040) — ASLR включён. Снять этот флаг — отключить ASLR.
  • IMAGE_DLLCHARACTERISTICS_NX_COMPAT (0x0100) — DEP (Data Execution Prevention) совместимость.
  • IMAGE_DLLCHARACTERISTICS_NO_SEH (0x0400) — отключение Structured Exception Handling.
  • IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE (0x8000) — поддержка Terminal Services (флаг /TSAWARE).

Все эти флаги можно менять напрямую редактированием бинарника. Они занимают ровно 2 байта в определённом смещении, и изменение одного бита — это изменение одного параметра компиляции.

2. Подсистема (Subsystem)

В Optional Header есть поле Subsystem (2 байта). Оно определяет, в какой среде выполняется программа:

  • 1 — Native (драйвер ядра)
  • 2 — Windows GUI
  • 3 — Windows CUI (консоль)
  • 5 — OS/2 CUI
  • 7 — POSIX CUI
  • 9 — Windows CE GUI
  • 10 — EFI Application
  • 11 — EFI Boot Service Driver
  • 12 — EFI ROM
  • 13 — EFI Runtime
  • 14 — XBOX
  • 16 — Windows Boot Application

Смена подсистемы с GUI на CUI или наоборот — реальна, но требует понимания, что точка входа и логика инициализации могут отличаться.

3. Точка входа (AddressOfEntryPoint)

Смещение точки входа можно изменить. Это не совсем compiler flag, но позволяет вставить свой код, который выполнится до основной программы. Техника часто используется при реверс-инжиниринге и внедрении патчей.

4. Размер стека и кучи (SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve, SizeOfHeapCommit)

Эти значения в Optional Header задаются флагами /F или /STACK и /HEAP в линковщике. Можно отредактировать вручную, если, например, стек слишком маленький и программа падает на глубокой рекурсии.

5. Базовый адрес образа (ImageBase)

Предпочтительный адрес загрузки. Меняется в поле ImageBase Optional Header. Если по этому адресу уже загружен другой модуль, система всё равно переместит образ, но при отключённом ASLR это может быть важно.

Инструменты для редактирования

Инструмент Что умеет Удобство Где искать
CFF Explorer Полноценный редактор PE-заголовков, секций, импорта, экспорта. Визуальный интерфейс. Высокий — видно структуру, можно кликнуть и отредактировать. ntcore.com/explorer
PE-bear Открытый инструмент для анализа PE. Показывает заголовки, секции, импорт. Есть редактирование. Средний — больше для анализа, но редактирование тоже работает. github.com/hasherezade/pe-bear
HxD Шестнадцатеричный редактор. Можно править любые байты напрямую. Низкий — нужно знать смещения и формат. Максимальный контроль. mh-nexus.de
LordPE Классический редактор PE. Старый, но рабочий. Средний — интерфейс местами неочевидный. В открытых репозиториях.
pefile (Python) Библиотека для парсинга и редактирования PE из скрипта. Для тех, кто умеет писать скрипты. Полная автоматизация. github.com/erocarrera/pefile

Пошаговый процесс: отключаем ASLR в готовом бинарнике

Разберём конкретный пример — отключение ASLR через снятие флага IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE. Это одна из самых частых задач.

  1. Откройте файл в CFF Explorer (или любом другом редакторе PE).
  2. Перейдите в Optional Header → раздел «DLL Characteristics».
  3. Найдите флаг «DYNAMIC_BASE» — он галочкой. Снимите его.
  4. Сохраните файл.
  5. Проверьте — откройте сохранённый файл снова и убедитесь, что флаг снят.

Всё. Теперь программа будет загружаться по фиксированному адресу (если не конфликтует с другим модулем). Это именно то, что делает флаг /DYNAMICBASE:NO в MSVC.

Если работаете через Python и библиотеку pefile:

import pefile

pe = pefile.PE("target.exe")

# Смотрим текущие DLL Characteristics
print(f"Current DLL Characteristics: {hex(pe.OPTIONAL_HEADER.DllCharacteristics)}")

# Снимаем флаг DYNAMIC_BASE (0x0040)
pe.OPTIONAL_HEADER.DllCharacteristics &= ~0x0040

# Снимаем флаг NX_COMPAT (0x0100) если нужно
pe.OPTIONAL_HEADER.DllCharacteristics &= ~0x0100

pe.write("target_no_aslr.exe")
pe.close()
print("Done. ASLR disabled.")

Что можно изменить через редактирование секций

Характеристики секций (Section Characteristics) тоже можно менять. Это ближе к тому, что задаётся через прагмы и флаги компилятора:

  • IMAGE_SCN_MEM_EXECUTE (0x20000000) — секция исполняемая. Можно добавить к секции данных, чтобы выполнять код оттуда.
  • IMAGE_SCN_MEM_READ (0x40000000) — чтение.
  • IMAGE_SCN_MEM_WRITE (0x80000000) — запись.

Например, если у вас есть секция данных, в которую нужно записать код и выполнить его, добавьте флаги EXECUTE и WRITE. Это аналог включения /NXCOMPAT:NO и изменения прав доступа на уровне секций.

Что нельзя изменить (и почему)

Важно понимать границы. Вот что вы не сможете сделать простым редактированием PE:

  • Уровень оптимизации (/O1, /O2, /Ox). Оптимизация влияет на сгенерированный машинный код. Инструкции уже вкомпилированы, перестроить их без декомпиляции и рекомпиляции невозможно.
  • Отладочная информация (/DEBUG). Можно удалить отладочные символы и директории, но добавить их обратно без пересборки — нет.
  • Frame Pointer Omission (/Oy). Это решение о том, использовать ли EBP как указатель фрейма. Оно «запечено» в каждой функции.
  • Calling convention на уровне отдельных функций. Можно поменять точку входа, но не соглашение о вызовах внутри кода.
  • Inline-функции (/Ob). Решение о инлайне принято компилятором, в бинарнике это просто последовательность инструкций.

Всё, что влияет на сгенерированный код функций, их структуру, расположение переменных в стеке — это территория декомпиляции и рекомпиляции, а не редактирования заголовков.

Смена подсистемы: консоль ↔ GUI

Реальная ситуация: у вас консольное приложение, а нужно GUI (или наоборот). Меняем поле Subsystem в Optional Header:

  1. Открываем PE в CFF Explorer.
  2. Optional Header → Subsystem. Текущее значение — 3 (CUI).
  3. Меняем на 2 (GUI).
  4. Сохраняем.

Подвоны:

  • Точка входа main() не вызывается Windows напрямую. GUI-приложения используют WinMain(). Если в коде только main(), программа упадёт.
  • CRT (C Runtime) инициализируется по-разному для консоли и GUI. Консольная версия настраивает stdin/stdout, GUI версия — нет.
  • Если в коде нет WinMain, простая смена подсистемы не поможет — нужно менять точку входа и писать обёртку.

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

Размер стека: когда маленький стек мешает

Если программа падает с Stack Overflow на относительно маленькой глубине рекурсии, можно увеличить резервирование стека:

  1. Открываем Optional Header в редакторе PE.
  2. Находим SizeOfStackReserve — общий размер стека, выделяемый виртуально.
  3. Находим SizeOfStackCommit — физически выделенная память при старте.
  4. Увеличиваем значения. Например, с 1 МБ (0x100000) до 16 МБ (0x1000000).
  5. Сохраняем.

Это эквивалент флага /STACK:reserve[,commit] в линковщике. Работает для основного потока. Для дополнительных потоков размер стека определяется при их создании.

Частые ошибки и подводные камни

  • Не сделали бэкап. Первое правило — копируйте файл перед редактированием. Один неправильный байт — и бинарник не запустится.
  • Смена флагов без понимания последствий. Отключение ASLR делает программу уязвимой для атак. Отключение DEP — тоже. Делайте это только для отладки или в контролируемой среде.
  • Несовпадение смещений в Little Endian. Если редактируете в hex-редакторе, помните: младший байт первое. Значение 0x0040 в памяти лежит как 40 00.
  • Забыли про выравнивание секций. Если добавляете данные в секцию, убедитесь, что виртуальное и физическое выравнивание соблюдены. Иначе загрузчик откажется запускать файл.
  • Смена подсистемы без проверки точки входа. Программа просто не запустится, и вы не сразу поймёте почему.
  • Редактирование защищённых файлов. Некоторые программы имеют цифровую подпись или проверку целостности. После редактирования подпись сломается, и либо ОС не запустит файл, либо сама программа обнаружит модификацию.
  • Не учли разрядность. PE32 и PE32+ имеют разный размер Optional Header. То, что работает в 32-битном, может не сработать в 64-битном из-за разного размера полей.

Что выбрать в зависимости от задачи

Нужно отключить ASLR для отладки: снять флаг DYNAMIC_BASE в DLL Characteristics. Самый простой и безопасный вариант. Используйте CFF Explorer или pefile.

Нужно включить ASLR для безопасности: установить флаг DYNAMIC_BASE. Аналогично — через DLL Characteristics.

Нужно отключить DEP: снять флаг NX_COMPAT. Но помните, что это снижает защиту от эксплойтов.

Нужно изменить размер стека: отредактировать SizeOfStackReserve и SizeOfStackCommit в Optional Header.

Нужно поменять подсистему: изменить Subsystem, но убедиться, что точка входа соответствует. Для ассемблерных программ — проще, для C/C++ — сложнее.

Нужно добавить исполняемые права секции данных: добавить IMAGE_SCN_MEM_EXECUTE в Section Characteristics. Используется для внедрения кода.

Нужно изменить базовый адрес загрузки: отредактировать ImageBase. Актуально при отладке, когда нужно избежать релокации.

Автоматизация через скрипты

Если нужно обработать несколько файлов или встроить изменение флагов в пайплайн, pefile — лучший выбор:

import pefile
import sys

def modify_pe_flags(input_path, output_path, remove_flags=0, add_flags=0):
    pe = pefile.PE(input_path)
    
    # Меняем DLL Characteristics
    current = pe.OPTIONAL_HEADER.DllCharacteristics
    new_value = (current & ~remove_flags) | add_flags
    pe.OPTIONAL_HEADER.DllCharacteristics = new_value
    
    # Меняем SizeOfStackReserve (пример: установить 8 МБ)
    # pe.OPTIONAL_HEADER.SizeOfStackReserve = 8 * 1024 * 1024
    
    pe.write(output_path)
    pe.close()
    print(f"Saved: {output_path}")
    print(f"DLL Characteristics: {hex(current)} -> {hex(new_value)}")

if __name__ == "__modify_pe_flags__":
    # Отключаем ASLR (0x0040) и DEP (0x0100)
    modify_pe_flags(sys.argv[1], sys.argv[2], remove_flags=0x0040 | 0x0100)

Этот скрипт можно вызвать для пакетной обработки или встроить в CI/CD пайплайн, если нужно гарантировать определённые флаги в артефактах.

Проверка результата

После редактирования обязательно проверьте:

  1. Файл запускается? Самое базовое. Если нет — где-то ошиблись в смещениях или нарушили структуру.
  2. Флаги установились корректно? Откройте результат в CFF Explorer и сверьте с ожиданиями.
  3. Программа работает как раньше? Запустите основные сценарии использования. Изменение флагов заголовка обычно не ломает логику, но смена подсистемы или точки входа — может.
  4. Проверьте через Process Explorer или Process Hacker — они показывают ASLR, DEP и другие флаги для запущенных процессов.

Итог

Изменить compiler flags в собранном PE-файле напрямую нельзя — компилятор уже завершил работу. Но многие параметры, которые эти флаги задают, отражены в заголовках и характеристиках PE-файла, и их можно отредактировать.

Что реально менять: флаги ASLR, DEP, TSAWARE, SEH; размеры стека и кучи; базовый адрес загрузки; подсистему; права секций.

Что не менять без декомпиляции: уровень оптимизации, отладочную информацию, calling convention, inline-решения.

Главные инструменты: CFF Explorer для ручного редактирования, pefile для автоматизации, HxD для точечной правки байтов.

Главное правило: всегда делайте бэкап и проверяйте результат. Один бит в заголовке — и программа либо не запустится, либо будет уязвима. Редактируйте осознанно.

Если ваша задача — просто пересобрать проект с другими флагами, всегда выбирайте пересборку. Редактирование готового PE — это инструмент для случаев, когда пересборка невозможна или нецелесообразна.

Оцените статью
PEFile — Безопасность и технологии простым языком