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

Пропустить стекирование стека в C

Я хотел бы спровоцировать переполнение стека в функции C для проверки мер безопасности в моей системе. Я мог бы сделать это, используя встроенный ассемблер. Но C будет более портативным. Однако я не могу придумать способ спровоцировать переполнение стека с помощью C, поскольку память стека безопасно обрабатывается языком в этом отношении.

Итак, есть ли способ спровоцировать переполнение стека с помощью C (без использования встроенного ассемблера)?

Как указано в комментариях: "Недостаток стека" означает, что указатель стека указывает на адрес ниже начала стека ( "ниже" для архитектур, где стек растет от низкого до высокого).

4b9b3361

Ответ 1

Есть веская причина, по которой трудно спровоцировать стеки в C. Причина в том, что стандарты, совместимые с C, не имеют стека.

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

Ответ 2

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

Следовательно, невозможно создать "стековый поток" в чистом C. Вы не можете выскочить из стека в C и не можете установить указатель стека из C. Концепция стека - это что-то еще на более низком уровне чем язык С. Чтобы получить прямой доступ и управлять указателем стека, вы должны написать ассемблер.


Что вы можете сделать в C, это намеренно выписывать границы стека. Предположим, мы знаем, что стек начинается с 0x1000 и растет вверх. Тогда мы можем это сделать:

volatile uint8_t* const STACK_BEGIN = (volatile uint8_t*)0x1000;

for(volatile uint8_t* p = STACK_BEGIN; p<STACK_BEGIN+n; p++)
{
  *p = garbage; // write outside the stack area, at whatever memory comes next
}

Зачем вам нужно тестировать это в чистой программе на C, которая не использует ассемблер, я понятия не имею.


ИЗМЕНИТЬ

Если кто-то неправильно понял, что приведенный выше код вызывает поведение undefined, это то, что на самом деле говорит стандарт C, нормативный текст C11 6.5.3.2/4 (акцент мой):

Унарный * оператор обозначает косвенность. Если операнд указывает на функцию, результат обозначение функции; если он указывает на объект, результатом является значение l, обозначающее объект. Если операнд имеет тип '' указатель на тип, результат имеет тип ''. Если недопустимое значение было присвоено указателю, поведение унарного * оператора undefined 102)

Вопрос заключается в том, какое определение "недопустимое значение", поскольку это не формальный термин, определенный стандартом. Нотная заметка 102 (информативная, а не нормативная) содержит несколько примеров:

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

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

И последний случай "недопустимого значения" будет адресом, который не поддерживается конкретной системой. Это, очевидно, не то, что упоминается в стандарте C, поскольку схемы памяти конкретных систем не покрываются стандартом C.

Ответ 3

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

В 1980-х годах существовали реализации C, которые выполняли C путем интерпретации, а не путем компиляции. Действительно, некоторые из них использовали динамические векторы вместо стека, предоставляемые архитектурой.

память стека безопасно обрабатывается языком

Память стека не обрабатывается языком, а реализуется. Можно запустить C-код и вообще не использовать стек.

Ни в ISO 9899, ​​ни K & R не указывает ничего о существовании стека на языке.

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

Ответ 4

Что касается уже существующих ответов: я не думаю, что говорить о стиле undefined в контексте методов смягчения эксплуатации является подходящим.

Очевидно, что если реализация обеспечивает смягчение стека, то предоставляется стек. На практике void foo(void) { char crap[100]; ... } будет иметь массив в стеке.

Примечание, вызванное комментариями к этому ответу: поведение undefined - это вещь и в принципе, любой выполняющий его код может в конечном итоге скомпилироваться абсолютно к чему-либо, включая что-то, не похожее на исходный код в малейший. Тем не менее, предмет использования методов смягчения последствий тесно связан с целевой средой и что происходит на практике. На практике приведенный ниже код должен "работать" просто отлично. Когда вы работаете с такими вещами, вам всегда нужно проверить сгенерированную сборку.

Это подводит меня к тому, что на практике даст недоиспользование (волатильность добавлена, чтобы предотвратить компилятор от ее оптимизации):

static void
underflow(void)
{
    volatile char crap[8];
    int i;

    for (i = 0; i != -256; i--)
        crap[i] = 'A';
}

int
main(void)
{
    underflow();
}

Valgrind прекрасно сообщает о проблеме.

Ответ 5

Это предположение:

C будет более переносимым

неверно. C ничего не говорит о стеке и о том, как он используется при реализации. На вашей типичной платформе x86 следующий код (ужасно недействительный) будет обращаться к стеку за пределами допустимого фрейма стека (пока он не остановится ОС), но на самом деле он не "поп", от него:

#include <stdarg.h>
#include <stdio.h>

int underflow(int dummy, ...)
{
    va_list ap;
    va_start(ap, dummy);
    int sum = 0;
    for(;;)
    {
        int x = va_arg(ap, int);
        fprintf(stderr, "%d\n", x);
        sum += x;
    }
    return sum;
}

int main(void)
{
    return underflow(42);
}

Итак, в зависимости от того, что именно вы имеете в виду под "стеком", этот код делает то, что вы хотите на какой-то платформе. Но, как с точки зрения C, это просто демонстрирует поведение undefined, я бы не предложил его использовать. Он не "переносится" вообще.

Ответ 6

По определению стекирование стека является типом поведения undefined, и поэтому любой код, который запускает такое условие, должен быть UB. Следовательно, вы не можете надежно вызывать переполнение стека.

Тем не менее, следующее злоупотребление массивами переменной длины (VLA) приведет к переполнению управляемых стеков во многих средах (протестировано с x86, x86-64, ARM и AArch64 с Clang и GCC), фактически устанавливая указатель стека на над его начальным значением:

#include <stdint.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
    uintptr_t size = -((argc+1) * 0x10000);
    char oops[size];
    strcpy(oops, argv[0]);
    printf("oops: %s\n", oops);
}

Здесь выделяется VLA с "отрицательным" (очень большим) размером, который будет обертывать указатель стека и выводит указатель стека вверх. argc и argv используются для предотвращения оптимизации извлечения массива. Предполагая, что стек растет (по умолчанию для перечисленных архитектур), это будет стеком стека.

strcpy будет либо инициировать запись на недопустимый адрес при вызове, либо когда строка будет записана, если strcpy встроена. Окончательный printf не должен быть доступен.


Конечно, все это предполагает компилятор, который не просто делает VLA своего рода временным распределением кучи, - что компилятор совершенно свободен. Вы должны проверить сгенерированную сборку, чтобы убедиться, что приведенный выше код делает то, что вы на самом деле ожидаете от него. Например, в ARM (gcc -O):

8428:   e92d4800    push    {fp, lr}
842c:   e28db004    add fp, sp, #4, 0
8430:   e1e00000    mvn r0, r0 ; -argc
8434:   e1a0300d    mov r3, sp
8438:   e0433800    sub r3, r3, r0, lsl #16 ; r3 = sp - (-argc) * 0x10000
843c:   e1a0d003    mov sp, r3 ; sp = r3
8440:   e1a0000d    mov r0, sp
8444:   e5911004    ldr r1, [r1]
8448:   ebffffc6    bl  8368 <[email protected]> ; strcpy(sp, argv[0])

Ответ 7

Возможно ли это сделать в стандартном соответствии C? Нет

Можно ли сделать это, по крайней мере, на одном практическом компиляторе C, не прибегая к встроенному ассемблеру? Да

void * foo(char * a) {
   return __builtin_return_address(0);
}

void * bar(void) {
   char a[100000];
   return foo(a);
}

typedef void (*baz)(void);

int main() {
    void * a = bar();
    ((baz)a)();
}

Построить, что на gcc с "-O2 -fomit-frame-pointer -fno-inline"

https://godbolt.org/g/GSErDA

В основном бар выделяет кучу места в стеке (благодаря большому массиву), вызывает foo, освобождает это пространство и возвращает. Foo читает обратный адрес (используя расширение gcc) и возвращает его.

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

Нам нужно -fno-inline, чтобы остановить оптимизатор, вложив в него материал и сломав тщательно продуманную структуру. Нам также нужен компилятор для освобождения пространства в стеке путем вычисления, а не с помощью указателя фрейма, -fomit-frame-pointer по умолчанию используется для большинства gcc-сборок в настоящее время, но это не помешает явно указывать его.

Ответ 8

Существует способ, но он очень сложный. Единственный способ, о котором я могу думать, - определить указатель на нижний элемент, а затем уменьшить его значение адреса. То есть * (PTR) -. Мои скобки могут быть выключены, но вы хотите уменьшить значение указателя, затем разыщите указатель.

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

Ответ 9

Таким образом, в C есть более старые библиотечные функции, которые не защищены. Примером этого является strcpy. Он копирует одну строку в другую, пока не достигнет нулевого терминатора. Одна забавная вещь - передать программу, которая использует эту строку с удаленным терминатором. Он будет работать, пока он не достигнет нулевого терминатора. Или напечатайте строчку для себя. Так что, к тому, о чем я говорил раньше, C поддерживает указатели на что угодно. Вы можете сделать указатель на элемент в стеке на последнем элементе. Затем вы можете использовать итератор указателя, встроенный в C, чтобы уменьшить значение адреса, изменить значение адреса до местоположения, предшествующего последнему элементу в стеке. Затем передайте этот элемент в поп. Теперь, если вы делаете это для стека процессов операционной системы, который будет сильно зависеть от реализации компилятора и операционной системы. В большинстве случаев указатель функции на основной и декремент должен работать, чтобы переполнить стек. Я не пробовал это в C. Я только сделал это на языке Ассамблеи, поэтому нужно проявлять большую осторожность при работе. Большинство операционных систем хорошо справились с этим, так как это был длительный вектор атаки.

Ответ 10

Вы имеете в виду переполнение стека? Поместить в стек больше вещей, чем стек может разместиться? Если это так, рекурсия - это самый простой способ выполнить это.

void foo();
   {foo();};

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