LINUX.ORG.RU

Си. Почему бы не запретить запись в стек?

 


1

4

Решил немного разобраться как работают уязвимости. Как я понял, весомая их часть модифицирует стек.

#include <stdio.h>

register long unsigned rsp asm("rsp");

void print_arg(int arg) {
    ((int*)rsp)[3] = 0xBADC0DE;
    printf("arg = %x\n", arg);
}

int main(int argc, char **argv) {
    print_arg(0xF00D);
    return 0;
}

Этот код отрабатывает и не выводит ошибкок с

-fhardened -fcf-protection=full

На мой взгляд выглядит небезопасно.

Почему бы не вставлять проверки на ассемблере при записи в память, на включаемость в регион стека? Если нужно записать что то в аргумент на стеке (int), то проверку можно не вставлять. При записи по указателю, уже обязательно вставлять. Если адрес стека то ошибка. В memset проверять пересечение двух диапазонов.

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

void read_file(const char *name)
{
        char buff[999];
        FILE *f = fopen(name, "rb");
        read_block(f, buff);
}

void read_block(FILE *f, char *buff)
{
        // тут компилятор должен вывести что len(buff) == 999
        fread(buff, 1, 9999, f);
}

Что бы все идеально работало, нужно будет:

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

Последнее исправление: MOPKOBKA (всего исправлений: 2)

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

firkax ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Посмотрел в выхлоп компилятора… с -O2 оно почему-то работает, а вот без него (как и должно быть) не пишет это твое B. А выхлоп с оптимизацией очень странный, как это оно так оптимизировало?

aiqu6Ait ★★★★
()
Ответ на: комментарий от aiqu6Ait

А выхлоп с оптимизацией очень странный, как это оно так оптимизировало?

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

MOPKOBKA ★★★★★
() автор топика

Решил немного разобраться как работают уязвимости.

Молодец

Как я понял, весомая их часть модифицирует стек.

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

peregrine ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Так инструкции push/enter/leave по прежнему работают с rsp, а не с rbp. Это значит как только код скомпилированный твоим компилятором вызовет код скомпилированный другим компилятор вся защита развалится

cobold ★★★★★
()
Ответ на: комментарий от MOPKOBKA

UB Нельзя просто взять произвольный указатель и прибивать к нему произвольное n

Это разрешено только если за указателем лежит массив и результат вычисления лежит в пределах массива

maritlage
()
Ответ на: комментарий от cobold

Это сделано не для защиты мной. Аргумент так себе, .dll тоже не привяжешь к elf, это не значит что код делает из amd64 не amd64.

Вызвать dlopen можно, там просто идет смена регистров под ABI C с сохранением.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от maritlage

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

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 3)
Ответ на: комментарий от alysnix

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

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от MOPKOBKA

понятно. что такое регион стека мы пока не знаем, но «чувствуем» что он есть.

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

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

alysnix ★★★
()
Ответ на: комментарий от aiqu6Ait

Посмотрел в выхлоп компилятора… с -O2 оно почему-то работает, а вот без него (как и должно быть) не пишет это твое B

clang и с -O2 ничего не выводит, похоже тут UB где-то сидит.

anonymous
()
Ответ на: комментарий от MOPKOBKA

кстати для всех любителей UB и корректности.

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

если оптимизатор такое увидит, он с ума сойдет, и изнасилует код так, что он работать не будет.

вот для этого и язык си - чтобы такое вот писать… а не файлы ваши читать. на пытоне читай.

alysnix ★★★
()
Ответ на: комментарий от alysnix

понятно. что такое регион стека мы пока не знаем, но «чувствуем» что он есть.

Если у тебя Linux, то я могу помочь тебе узнать больше о регионе стека:

grep '\[stack\]' /proc/*/maps

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от alysnix

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

поэтому все это делается на ассемблере.

вот для этого и язык си - чтобы такое вот писать…

для такого си уже не подходит слишком высокоуровневый.

anonymous
()
Ответ на: комментарий от mx__

там вся хрень при оптимизация явно замешана на размере int, возможности перестановки местами а и b в декларации, выравнивании адреса, и присутствии ub.

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

короче код морковкина мусорный, его и рассматривать не стоит.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от mx__

Это зависит от компилятора, и его версии. Вот удобный сайт - https://godbolt.org/z/as43a9oYE

Рассказываю в чем суть, так делать нельзя, хотя это позволяет делать amd64, но С это не amd64, в С вообще не существует плоского адресного пространства. В С нельзя положить в void* указатель на функцию, потому что указатель на код, и указатель на данные это разные сущности, в amd64 можно.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от alysnix

там вся хрень при оптимизация явно замешана на размере int, возможности перестановки местами а и b в декларации, выравнивании адреса, и присутствии ub.

Бред от человека который не знает С. Но про UB верно.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от alysnix

я ему про то что если хорошо знаешь память и проц то на одном си можно такой финт сделать … и этого фича.

а какой нибудь компилятор Раста тебе просто этого не даст.

mx__ ★★★★★
()
Ответ на: комментарий от anonymous

поэтому все это делается на ассемблере.

на асме делается только загрузка и спасение стек поинтера. это две команды. все остальное пишется на си. для того он и создан.

alysnix ★★★
()
Ответ на: комментарий от MOPKOBKA

В С нельзя положить в void* указатель на функцию, потому что указатель на код, и указатель на данные это разные сущности, в amd64 можно.

Потому что «просто C» в отрыве от платформы не существует. В linux-i386/linux-amd64 С так делать можно.

firkax ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Это тут причем ? Я про то что если ты не знаешь что конкретно будет лучше это не трогать и вообще лучше не юзать си. Вдруг сам себе ногу отстрелишь и будешь потом кричать какой небезопасный Си раз мне это позволил написать.

mx__ ★★★★★
()
Ответ на: комментарий от mx__

Вот еще пример С который ну тупо идентичен ассемблеру и позволяет ВСЕ!

https://godbolt.org/z/88f346eMq

#include <stdio.h>

union data {
    int i;
    short c;
};

int f1(int* a, short* b) {
  *a = 0;
  *b = 1;
  return *a;
}

int f2(int* a, short* b) {
  *b = 1;
  return *a;
}

int main() {
    union data d;
    int *p = &d.i;
    printf("%d\n", f1(p, (short*)p));
    printf("%d\n", f2(p, (short*)p));
}
MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от firkax

Потому что «просто C» в отрыве от платформы не существует.

Конечно же существует. У ламеров не существуют, но их код в принципе перестает существовать с новой версий компилятора.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от mx__

А причем тут unsafe? Что то получится только с ним. Если ты имел виду что в С можно реализовать то, что не получится в Rust без unsafe, то да, такое есть. А с unsafe можно тоже самое.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от firkax

ну не вполне ВСЕ. полно команд проца, невыразимых в си, но для этого и существует асмовая вставка.

да и не может си вобрать в себя все асмы всех процов и прошлого и настоящего и будущего.

alysnix ★★★
()
Ответ на: комментарий от firkax

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

int get_10(void) { return 10; }
Вот например код который не зависит от платформы. Правильно говорю? Правильно.

Сложных проектов которые не переписывают под каждую платформу полно, от программ до библиотек, zlib вообще работает даже с ненормальными компиляторами.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от alysnix

на асме делается только загрузка и спасение стек поинтера. это две команды. все остальное пишется на си. для того он и создан.

Угу только с этими двумя командами хоть на паскале хоть на расте, хоть на D все также хорошо пишется никакой особой низкоуровневости у си нет.

anonymous
()

Си. Почему бы не запретить запись в стек?

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

На мой взгляд выглядит небезопасно.

Ну так и не пиши подобное, в чём проблема-то?
А вообще, сейчас существуют безопасные яп, поэтому сишку лучше оставить как есть ;)

u5er ★★
()