- Регистрация
- 7 Авг 2025
- Сообщения
- 19
- Реакции
- 0
- Баллы
- 1
- Возраст
- 31
Самоизменяющийся код на C с использованием shellcode и ассемблера: Подробный разбор.
Самоизменяющийся код — это приём программирования, при котором исполняемая программа в процессе своей работы изменяет собственный машинный код. Несмотря на то, что современные операционные системы внедряют серьёзные защитные механизмы, такие как W^X (Write XOR Execute) и ASLR (Address Space Layout Randomization), понимание подобных техник играет важную роль в таких областях, как системное программирование, реверс-инжиниринг, разработка эксплойтов и исследование архитектуры процессоров.
В данной статье мы пошагово разберём пример на языке C с использованием inline-ассемблера, где функция программы сначала выполняет простое действие, а затем её код замещается shellcode-ом, открывающим командную оболочку /bin/sh.
Подобные техники находят применение в самых разных сценариях:
Самоизменяющийся код — инструмент для глубокого понимания модели памяти, жизненного цикла инструкций и механизмов защиты ОС. Его корректное применение ограничено исследовательскими и учебными целями: анализ низкоуровневых механизмов, разработка безопасных JIT-компонент, реверс инженеринг, моделирование атак ради проверки защит. Ниже — безопасная методика, которая помогает получить нужные знания, не переходя грань вредоносного использования и не публикуя опасных рецептов.
Работайте только в контролируемой среде.
— Виртуальная машина с отдельным снапшотом и изолированной сетью (host-only/без выхода в интернет).
— Нерутовая учётная запись, ограниченные права, отдельный тестовый пользователь.
— Отдельный тестовый образ ОС без доступа к рабочим данным.
— Юридическая чистота: любые эксперименты — лишь с собственными системами/в явной зоне согласия. Цель — исследование и повышение безопасности, а не получение несанкционированного доступа.
1) Отладка защит памяти и их поведение в реальности.
— Проверяйте, как ОС и рантайм реагируют на попытки модифицировать исполняемый участок кода.
— Изучайте взаимосвязь между таблицами страниц, правами доступа и переходом от данных к инструкциям.
2) Обучение безопасной разработке JIT-подсистем.
— Репетируйте паттерны «кодогенерации»: подготовка буфера, запись инструкций, синхронизация кэша.
— Сравнивайте подходы: RW→RX (данные→код) vs постоянный RWX (нежелателен), оценивайте риски.
3) Реверс и верификация гипотез.
— Моделируйте поведение потенциального патча кода без публикации вредоносных полезных нагрузок.
— Проверяйте гипотезы о протоколе вызовов, соглашениях о регистрах, ABI.
— Не оставлять страницы RWX дольше короткого окна технической необходимости. Это ухудшает модель угроз.
— Не тестировать на продуктивной ОС/сети, даже если «просто любопытно». Риски несоразмерны пользе.
— Не публиковать точные байтовые шаблоны «боевых» полезных нагрузок и рецепты обхода защит. Это не про обучение и не про безопасность.
— Битность и позиционно-независимый код. Смешение режимов, PIE/не-PIE и разные опции компоновки меняют адреса и относительные переходы.
— Страничные границы. Частичная модификация на стыке страниц может дать неконсистентный результат.
— Кэш инструкций/фронтенд процессора. На ряде платформ необходимо явное уведомление о том, что «код обновлён».
— Приложите дампы памяти и контрольные суммы (без вредоносных фрагментов).
— Обсудите модель угроз и практики снижения риска: краткоживущие права, минимизация поверхности атаки, строгая изоляция среды.
— Сделайте выводы о применимости подходов в безопасном JIT, профилировании и инструментарии реверса.
— Глубокое понимание того, как код превращается в данные и обратно, где хрупкие места и как их защищают.
— Навык анализа и соблюдения ABI/конвенций вызовов, внимательность к смещениям и кэшам.
— Практику построения надежных исследовательских протоколов, которые полезны в оборонительном ИБ и разработке производительных JIT-компонент.
— Умение документировать эксперименты без публикации опасных «боевых» деталей, что соответствует этике и закону.
Самоизменяющийся код — это приём программирования, при котором исполняемая программа в процессе своей работы изменяет собственный машинный код. Несмотря на то, что современные операционные системы внедряют серьёзные защитные механизмы, такие как W^X (Write XOR Execute) и ASLR (Address Space Layout Randomization), понимание подобных техник играет важную роль в таких областях, как системное программирование, реверс-инжиниринг, разработка эксплойтов и исследование архитектуры процессоров.
В данной статье мы пошагово разберём пример на языке C с использованием inline-ассемблера, где функция программы сначала выполняет простое действие, а затем её код замещается shellcode-ом, открывающим командную оболочку /bin/sh.
Что делает программа
- Первый вызов функции foo() выводит сообщение в консоль.
- С помощью системного вызова mprotect() изменяются права доступа к памяти — участок кода становится доступным для записи и исполнения.
- В память инжектируется shellcode, представленный в виде массива байтов.
- Повторный вызов foo() уже выполняет не исходный printf, а запуск оболочки через execve("/bin/sh"), что фактически даёт доступ к терминалу изнутри программы.
Ключевые аспекты реализации
- Используется архитектура x86_64.
- В изначальном состоянии функция foo() содержит простой вызов printf.
- Через mprotect() выполняется смена прав доступа для страницы памяти с RX (чтение и выполнение) на RWX (чтение, запись и выполнение).
- Затем функция полностью перезаписывается shellcode-ом, содержащим инструкции на ассемблере.
- Shellcode описывается массивом байтов, например: \x48\x31\xd2..., и помещается в тело функции с помощью memcpy.
Зачем это нужно
Подобные техники находят применение в самых разных сценариях:
- для практической демонстрации того, как работает память на низком уровне;
- в исследовании уязвимостей и написании эксплойтов;
- для анализа и тестирования механизмов защиты от исполнения кода;
- в образовательных проектах — чтобы лучше понять взаимодействие машинного кода, регистрoв, системных вызовов и прав доступа к памяти.
Что понадобится для запуска примера
- Linux-дистрибутив (например, Ubuntu).
- Компилятор GCC.
- Компиляция с отключением стандартных защит:
Самоизменяющийся код на C с использованием shellcode и ассемблера: практическое применение без риска
Самоизменяющийся код — инструмент для глубокого понимания модели памяти, жизненного цикла инструкций и механизмов защиты ОС. Его корректное применение ограничено исследовательскими и учебными целями: анализ низкоуровневых механизмов, разработка безопасных JIT-компонент, реверс инженеринг, моделирование атак ради проверки защит. Ниже — безопасная методика, которая помогает получить нужные знания, не переходя грань вредоносного использования и не публикуя опасных рецептов.
Безопасная лаборатория и правовая база
Работайте только в контролируемой среде.
— Виртуальная машина с отдельным снапшотом и изолированной сетью (host-only/без выхода в интернет).
— Нерутовая учётная запись, ограниченные права, отдельный тестовый пользователь.
— Отдельный тестовый образ ОС без доступа к рабочим данным.
— Юридическая чистота: любые эксперименты — лишь с собственными системами/в явной зоне согласия. Цель — исследование и повышение безопасности, а не получение несанкционированного доступа.
Что именно «применять» и ради чего
1) Отладка защит памяти и их поведение в реальности.
— Проверяйте, как ОС и рантайм реагируют на попытки модифицировать исполняемый участок кода.
— Изучайте взаимосвязь между таблицами страниц, правами доступа и переходом от данных к инструкциям.
2) Обучение безопасной разработке JIT-подсистем.
— Репетируйте паттерны «кодогенерации»: подготовка буфера, запись инструкций, синхронизация кэша.
— Сравнивайте подходы: RW→RX (данные→код) vs постоянный RWX (нежелателен), оценивайте риски.
3) Реверс и верификация гипотез.
— Моделируйте поведение потенциального патча кода без публикации вредоносных полезных нагрузок.
— Проверяйте гипотезы о протоколе вызовов, соглашениях о регистрах, ABI.
Методика эксперимента (безопасный, нейтральный сценарий)
Идея: заменить нежизненно важный участок функции нейтральным шаблоном (например, логикой, меняющей строку вывода или арифметический результат), не выполняя внешних программ и не эскалируя привилегии.- Выбор цели.
Возьмите небольшую функцию foo() с предсказуемым поведением (например, возвращает число или печатает строку). Подготовьте набор тестов на корректность до/после модификации (unit-тесты важнее «ручного» прогона). - Подготовка памяти.
Организуйте область для будущих инструкций так, чтобы она не нарушала политику разделения «пишется ≠ исполняется». Безопасная стратегия — раздельные этапы:
— записываете шаблон будущих инструкций в буфер данных;
— переключаете права только на время, строго для конкретной страницы;
— возвращаете консервативные права после завершения операции.
Цель: отработать саму процедуру и понять ограничения, а не удерживать страницу в небезопасном состоянии. - Замещение поведения.
Вместо опасных полезных нагрузок используйте нейтральный кодовый шаблон, меняющий логику foo() (например, иная строка, иная константа, другой путь ветвления).
— На архитектуре x86_64 в реальных системах дополнительно учитывают выравнивание, относительные смещения переходов, корректность адресации данных.
— Фиксируйте версии компилятора/опций: разные сборки меняют раскладку кода и смещения. - Синхронизация и целостность.
После модификации убедитесь, что инструкции на самом деле обновились (актуализация кэшей инструкций может требоваться на некоторых платформах).
— Снимайте контрольные суммы участка, сравнивайте дампы «до/после».
— Прогоняйте unit-тесты.
— При необходимости используйте отладчик и трассировку системных вызовов, чтобы наблюдать переходы исполнения. - Наблюдаемость и метрики.
Измеряйте накладные расходы: время переключения прав, задержки на «прогрев» кэша инструкций, влияние на производительность. Это важно для адекватной оценки JIT-подходов и решений «код-как-данные».
Что «не делать» и почему
— Не встраивать запускающие внешние оболочки/процессы полезные нагрузки. Это уводит эксперимент в сторону эксплуатационных сценариев.— Не оставлять страницы RWX дольше короткого окна технической необходимости. Это ухудшает модель угроз.
— Не тестировать на продуктивной ОС/сети, даже если «просто любопытно». Риски несоразмерны пользе.
— Не публиковать точные байтовые шаблоны «боевых» полезных нагрузок и рецепты обхода защит. Это не про обучение и не про безопасность.
Типичные ошибки и отладка
— Смещения и соглашения о вызовах. Убедитесь, что сохраняете/восстанавливаете регистры согласно ABI, не ломаете пролог/эпилог функции.— Битность и позиционно-независимый код. Смешение режимов, PIE/не-PIE и разные опции компоновки меняют адреса и относительные переходы.
— Страничные границы. Частичная модификация на стыке страниц может дать неконсистентный результат.
— Кэш инструкций/фронтенд процессора. На ряде платформ необходимо явное уведомление о том, что «код обновлён».
Как презентовать результаты на форуме/в команде
— Покажите сравнение поведения функции «до/после» на наборе тестов.— Приложите дампы памяти и контрольные суммы (без вредоносных фрагментов).
— Обсудите модель угроз и практики снижения риска: краткоживущие права, минимизация поверхности атаки, строгая изоляция среды.
— Сделайте выводы о применимости подходов в безопасном JIT, профилировании и инструментарии реверса.
Итоги и польза для практики
Что даёт такой безопасный подход:— Глубокое понимание того, как код превращается в данные и обратно, где хрупкие места и как их защищают.
— Навык анализа и соблюдения ABI/конвенций вызовов, внимательность к смещениям и кэшам.
— Практику построения надежных исследовательских протоколов, которые полезны в оборонительном ИБ и разработке производительных JIT-компонент.
— Умение документировать эксперименты без публикации опасных «боевых» деталей, что соответствует этике и закону.

