Подтвердить что ты не робот

Можно ли выполнить код из стека в стандартном C?

Следующий код не работает должным образом, но, надеюсь, иллюстрирует мою попытку:

long foo (int a, int b) {
  return a + b;
}

void call_foo_from_stack (void) {
  /* reserve space on the stack to store foo code */
  char code[sizeof(*foo)];

  /* have a pointer to the beginning of the code */
  long (*fooptr)(int, int) = (long (*)(int, int)) code;

  /* copy foo code to the stack */
  memcpy(code, foo, sizeof(*foo));

  /* execute foo from the stack */
  fooptr(3, 5);
}

Очевидно, что sizeof(*foo) не возвращает размер кода функции foo().

Мне известно, что выполнение стека ограничено некоторыми процессорами (или, по крайней мере, если установлен флаг ограничения). Помимо вложенных функций GCC, которые в конечном итоге могут быть сохранены в стеке, есть ли способ сделать это в стандартном C?

4b9b3361

Ответ 1

sizeof(*foo) не является размером функции foo, ее размер указателя для foo (который обычно будет такого же размера, как и любой другой указатель на вашей платформе).

sizeof Невозможно измерить размер функции. Причина в том, что sizeof является статическим оператором, а размер функции неизвестен во время компиляции.

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

Вы можете сделать что-то ужасное, используя alloca и некоторые неприятные хаки, но короткий ответ нет, я не думаю, что вы можете сделать это со стандартным C.

Следует также отметить, что стек не является исполняемым в современных безопасных операционных системах. В некоторых случаях вы можете сделать его исполняемым, но это плохая идея очень, которая заставит вашу программу широко открывать стеки, разбивая атаки и ужасные ошибки.

Ответ 2

Допустимый прецедент для такого рода вещей - это встроенная система, которая, как правило, исчерпывает память FLASH, но должна быть способна перепрограммировать себя в поле. Для этого часть кода должна запускаться с другого устройства памяти (в моем случае само FLASH-устройство не могло стирать и программировать одну страницу, позволяя читать с любой другой страницы, но есть устройства, которые могут это сделать), и в системе было достаточно ОЗУ для хранения как флеш-записи, так и нового изображения приложения.

Мы написали необходимую функцию программирования FLASH в C, но использовали директивы #pragma, чтобы она помещалась в отдельный сегмент .text из остальной части кода. В файле управления компоновщиком мы использовали компоновщик для определения глобальных символов для начала и конца этого сегмента и располагали его на базовом адресе в ОЗУ, размещая сгенерированный код в области загрузки, которая находилась во FLASH, вместе с данные инициализации для сегмента .data и чистый сегмент .rodata для чтения; базовый адрес во FLASH был вычислен и определен как глобальный символ.

Во время выполнения, когда была реализована функция обновления приложения, мы читаем изображение нового приложения в свой буфер (и выполнили все проверки работоспособности, которые должны быть выполнены, чтобы убедиться, что это действительно был образ приложения для этого устройства). Затем мы скопировали ядро ​​обновления из его неактивного местоположения во FLASH в его связанное местоположение в ОЗУ (используя глобальные символы, определенные компоновщиком), а затем вызвали его так же, как и любую другую функцию. Нам не нужно было делать что-либо особенное на сайте вызова (даже не указатель функции), потому что в отношении компоновщика все время находилось в ОЗУ. Тот факт, что при нормальной работе этот конкретный кусок ОЗУ имел совсем другую цель, не был важен для компоновщика.

Тем не менее, все механизмы, которые сделали это возможным, либо выходят за рамки стандартного, либо строго определенного поведения. Стандарту все равно, как код загружается в память до его запуска. Он просто говорит, что система может выполнять код.

Ответ 3

Помимо всех других проблем, я не думаю, что кто-то еще упоминал, что код в его окончательной форме в памяти вообще не может быть перемещен. Ваш пример foo, возможно, но учтите:

int main(int argc, char **argv) {
    if (argc == 3) {
        return 1;
    } else {
        return 0;
    }
}

Часть результата:

    if (argc == 3) {
  401149:       83 3b 03                cmpl   $0x3,(%ebx)
  40114c:       75 09                   jne    401157 <_main+0x27>
        return 1;
  40114e:       c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%ebp)
  401155:       eb 07                   jmp    40115e <_main+0x2e>
    } else {
        return 0;
  401157:       c7 45 f4 00 00 00 00    movl   $0x0,-0xc(%ebp)
  40115e:       8b 45 f4                mov    -0xc(%ebp),%eax
    }

Обратите внимание на jne 401157 <_main+0x27>. В этом случае у нас есть инструкция x86 с условным приближением (t24), которая идет на 9 байт вперед. Так что перемещаемый: если мы скопируем код в другом месте, мы по-прежнему хотим перейти на 9 байт вперед. Но что, если это относительный прыжок или вызов, чтобы код, который не является частью функции, которую вы скопировали? Вы переходите в какое-то произвольное место на вашем стеке или рядом с ним.

Не все инструкции перехода и вызова похожи на это (не на все архитектуры, и даже на все x86). Некоторые ссылаются на абсолютные адреса, загружая адрес в регистр, а затем совершая большой прыжок/вызов. Когда код готов к выполнению, так называемый "загрузчик" будет "исправлять" код, заполняя любой адрес, который конечная цель попадает в память. Копирование такого кода (в лучшем случае) приведет к коду, который перескакивает или вызывает тот же адрес, что и оригинал. Если цель не находится в коде, который вы копируете, вероятно, что вы хотите. Если цель находится в коде, который вы копируете, вы переходите к оригиналу, а не к копии.

Те же проблемы относительных и абсолютных адресов применяются к вещам, отличным от кода. Например, ссылки на разделы данных (содержащие строковые литералы, глобальные переменные и т.д.) Будут ошибочными, если они будут рассмотрены относительно и не являются частью скопированного кода.

Кроме того, указатель функции не обязательно содержит адрес первой инструкции в функции. Например, на ARM-процессоре в режиме межсетевого взаимодействия ARM/большого пальца адрес функции большого пальца на 1 больше, чем адрес его первой команды. Фактически, младший значащий бит значения не является частью адреса, это флаг, указывающий процессору переключиться в режим большого пальца как часть перехода.

Ответ 4

Если вам нужно измерить размер функции, попросите компилятор/компоновщик вывести файл карты, и вы можете рассчитать размер функции, основанный на этой информации.

Ответ 5

Ваша ОС не должна позволять вам делать это легко. Не должно быть никакой памяти как с разрешениями на запись, так и с выполнением, и особенно в стеке есть много разных защит (см. ExecShield, OpenWall patches,...). IIRC, Selinux также включает ограничения выполнения стека. Вам нужно будет найти способ сделать один или несколько из:

  • Отключить защиту стека на уровне ОС.
  • Разрешить выполнение из стека в конкретном исполняемом файле.
  • mprotect() стек.
  • Возможно, некоторые другие вещи...

Ответ 6

Есть много способов, которые пытаются сделать это, могут пойти не так, но это возможно и было сделано. Это один из способов, с которыми работали атаки переполнения буфера - пишите в маленькую вредоносную программу для того, что, вероятно, является архитектурой целевого компьютера, а также кодом и/или данными, которые могут привести к тому, что процессор завершит выполнение вредоносных кода и надежды на худшее.

Также было меньше применений этого зла, но обычно оно ограничено ОС и/или процессором. Некоторые процессоры не могут этого допускать, поскольку память кода и стека находится в разных адресных пространствах.

Одна вещь, которую вам нужно будет учитывать, если вы хотите сделать это, - это то, что код, который вы записываете в пространство стека, необходимо скомпилировать (или, если он написан как сборка или машинный код, написанный как) независимый от позиции или вам нужно будет убедиться, что он попадает на определенный адрес (и что он был написан/скомпилирован, чтобы ожидать этого).

Я не думаю, что стандарт C говорит об этом.

Ответ 7

Ваша проблема примерно похожа на динамически сгенерированный код, за исключением того, что вы хотите выполнить из стека вместо общей области памяти.

Вам потребуется собрать достаточно стека, чтобы он соответствовал вашей функции. Вы можете узнать, насколько велика функция foo(), путем компиляции и просмотра собранной сборки. Затем жестко задайте размер вашего массива code [], чтобы он поместился, по крайней мере, так много. Также убедитесь, что код [], или способ копирования foo() в код [], дает скопированной функции правильное выравнивание команд для вашей архитектуры процессора.

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

Как уже упоминалось, если ваш стек не является исполняемым, это не стартер.

Ответ 8

Как уже говорили другие, это невозможно сделать стандартным образом - то, что вы в конечном итоге, будет специфичным для платформы: CPU из-за того, что методы кодов структурированы (относительные или абсолютные ссылки), ОС, Скорее всего, вам потребуется установить защиту страницы, которая может быть выполнена из стека. Кроме того, он зависит от компилятора: нет стандартного и гарантированного способа получить размер функции.

Если у вас действительно есть хороший прецедент, например, перепрограммирование флэш-памяти, поясните RBerteig, будьте готовы к запуску скриптов компоновщика, проверьте разборку и узнайте, переписывая очень нестандартный и неуправляемый код:)

Ответ 9

В Linux вы не можете этого сделать, потому что область памяти стека НЕ ​​выполняется.
Вы можете прочитать что-то на ELF.

Ответ 10

Запасные и копии частей вашей идеи в порядке. Получение кода указателя на ваш удивительный код/​​данные стека, что сложнее. Приоритет адреса вашего стека кодовому указателю должен сделать трюк.


{
   u8 code[256];

   int (*pt2Function)() = (int (*)())&code;

   code();
}

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