Как записывать и выполнять опкоды в секциях GNU ассемблера Linux

    В среде Linux и стандартном GNU ассемблере (GAS) разработчики часто задаются вопросом: можно ли заполнить секцию бинарными опкодами, а затем перейти к её выполнению, не используя системные вызовы вроде mmap? В этой статье мы разберём технические ограничения и практические способы работы с секциями .text и .data, включая настройку прав на исполнение.

    Можно ли писать опкоды напрямую в секцию .text?

    В GNU ассемблере секция .text по умолчанию доступна только для чтения и выполнения (RX). Запись в неё во время выполнения программы запрещена аппаратной защитой памяти. Однако на этапе компиляции вы можете разместить в ней произвольные байты, используя директиву .byte или .long, а затем вызвать их как функцию. Например:

    .section .text.globl _start_start:    mov $1, %rax    xor %rdi, %rdi    syscall

    Но динамически изменять .text после запуска программы без специальных трюков (например, mprotect) нельзя.

    Как сделать секцию .data исполняемой?

    Секция .data обычно имеет права на чтение и запись (RW), но не на выполнение. Чтобы сделать её исполняемой, нужно изменить атрибуты секции в исходном коде ассемблера. Используйте директиву .section с флагом @progbits и параметром ax (allocatable, executable):

    .section .mycode, "ax", @progbitsmy_code:    .byte 0x90, 0x90, 0xC3

    Это создаст новую секцию с правами на выполнение. Однако встраивание опкодов в .data с последующим прыжком без изменения прав защиты памяти невозможно - система запретит исполнение.

    Выполнение кода без mmap: альтернативные подходы

    Системный вызов mmap с флагом PROT_EXEC - стандартный способ создания исполняемой памяти в Linux. Если вы хотите избежать системных вызовов, рассмотрите следующие варианты:

    • Встраивание кода в секцию .text: используйте метки и директивы для размещения машинного кода на этапе сборки.
    • Самомодифицирующийся код: измените защиту памяти через mprotect (системный вызов), что всё равно требует обращения к ядру.
    • JIT-компиляция: выделите буфер с помощью mmap или sbrk и скопируйте туда опкоды.

    Полностью избежать системных вызовов для динамического исполнения произвольного кода в user-space невозможно из-за механизмов защиты ОС.

    Практический пример: заполнение секции и прыжок

    Вот простой пример на GAS, который размещает опкоды в секции .text и выполняет их:

    .section .text.globl _start_start:    # Переход на встроенный код    jmp my_code    # Возврат и завершение    mov $60, %rax    xor %rdi, %rdi    syscallmy_code:    .byte 0x48, 0xC7, 0xC0, 0x3C, 0x00, 0x00, 0x00  # mov $60, %rax    .byte 0x48, 0x31, 0xFF                         # xor %rdi, %rdi    .byte 0x0F, 0x05                               # syscall

    Этот код выполняет системный вызов exit(0). Обратите внимание: jmp my_code работает, так как секция .text уже исполняема.

    Ограничения и рекомендации

    • Для динамической записи опкодов (например, из переменной) используйте mmap с PROT_READ | PROT_WRITE | PROT_EXEC.
    • Не пытайтесь писать в .text во время выполнения - это вызовет Segmentation Fault.
    • Для экспериментов с исполняемыми секциями используйте objdump -d для проверки прав доступа.

    Часто задаваемые вопросы