Можно ли выполнить код из стека в стандартном C? - PullRequest
17 голосов
/ 21 сентября 2010

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

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

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

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

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

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

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

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

Ответы [ 10 ]

10 голосов
/ 21 сентября 2010

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

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

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

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

9 голосов
/ 21 сентября 2010

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

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

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

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

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

4 голосов
/ 21 сентября 2010

Помимо всех других проблем, я не думаю, что кто-то еще упоминал, что код в его окончательном виде в памяти не может быть вообще перемещен.Ваш пример 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 0x75 0x09, которая идет на 9 байт вперед.Так что это можно перемещать: если мы копируем код в другое место, мы все равно хотим идти вперед на 9 байтов.Но что, если это был относительный переход или вызов кода, который не является частью функции, которую вы скопировали?Вы бы перепрыгнули в произвольное место в вашем стеке или рядом с ним.

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

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

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

1 голос
/ 21 сентября 2010

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

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

1 голос
/ 21 сентября 2010

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

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

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

Как уже отмечали другие, если ваш стек не исполняемый, то это не стартер.1007 *

1 голос
/ 21 сентября 2010

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

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

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

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

1 голос
/ 21 сентября 2010

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

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

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

0 голосов
/ 21 сентября 2010

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


{
   u8 code[256];

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

   code();
}

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

0 голосов
/ 21 сентября 2010

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

...