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)
Ответ на: комментарий от u5er

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

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

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

У С есть своя логика выполнения и ограничения.

Пример 1 https://godbolt.org/z/as43a9oYE

Пример 2 https://godbolt.org/z/88f346eMq

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

может просто твои паскали русты и ди в данном случае предоставляют примерно те же возможности, что и си? в ДАННОМ СЛУЧАЕ. но не во всех. но по совокупности качеств, необходимых для задач системного уровня, данные языки проигрывают сишечке. но в задачах иного рода они запросто могут у сишечки и выиграть.

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

А что значит не записаны верно? Пример должен показывать правду людям, которые думают что указатель это просто dq, а сравнение указателей это просто cmp rax, rbx.

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

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

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

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

Я сравниваю два указателя, а не значения переменных (намеренно). Суть в том что значения указателей в численном представлении равны, а указатели между собой не равны, потому что в С указатели это не просто числа.

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

ну вот напиши драйвер на gc-шном D. и вставь его в микроконтроллер.

Какие еще драйверы в микроконтроллере, там же и так все доступно os нет, или в лучшем случае что-то близкое к dos. Там можно даже на питоне писать если влезешь по памяти. Если же смотреть просто на драйверы, то D может и без gc https://dlang.org/spec/betterc.html ну и по остальной низкоуровневости он с си идентичен.

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

необходимых для задач системного уровня

Опять мантры про некие «задачи системного уровня». Что это за задачи такие? Драйвер ты хочешь написать? Модуль ядра? Что угодно в кернел-спейс? Товарищ, если за тридцать лет в линуксе не появилась объемлющая стандартная библиотека инкапсулирующая на самом деле низкоуровневые операции, не выразимые иначе как на ассемблере, вылизаная, оптимизированная и протестирована; если тебе в 2024м году для «системных задач» по прежнему охота костылить ассемблерные вставки - выкинь такую систему на помойку и забудь, это чудовищно уродский дизайн системы, и исправить его не поможет ни си, ни раст ни господь бог

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

вы о чем товарищ? 80-90 процентов работающих ныне вычислительных устройств это микроконтроллеры которым противопоказаны ваши мусоросборочные навороты и виртуальные машины. а вы все мыслите линуксами и кернелспейсом.

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

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

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

в нормальных языках есть ссылки

и чем по вашему ссылка радикально отличается от указателя? для начала, ссылки трактутся по-разному в разных языках, от «почти указателя», до совсем не указателя.

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

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

Какие виртуальные машины, ты о чем? Впрочем, после последней нашей беседы Вопрос по Rust (комментарий) я глубоко уверен, что тебе совершенно плевать на контекст разговора, ты общаешься с шумами в собственной голове

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

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

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

Это конвертация указателя в указатель, только в указатель на другой тип. А я перевожу указатель в адрес, который будет записан в long int, и сравниваю уже так.

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

Я не знаю как это правильно называется, но разница между кастом в (T*) и (T) думаю понятна.

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

К предыдущему посту.

В Вашем варианте как раз конвертация необходима.

Я не знаю как это правильно называется, но разница между кастом в (T*) и (T) думаю понятна.

Понятна.

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

Если это сделать, то это будет не C. А так можно использовать разные чекалки которые позволяют в специальных вариантах сборок ловить такие моменты. У меня например ловятся многие ошибки типа переполнения uint или записи значения за пределами enum, но такое не ловится.

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

Если это сделать, то это будет не C

Разве это будет нарушать стандарт С?

Уже есть проверки на перезапись кода возврата на стеке, я просто хочу предотвратить еще и перезапись значений на стеке.

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

Идея не в том что бы запретить писать в весь стек. Идея в том что бы:

void func() {
  int arr[10]; // значение на стеке
  int arr2[1];
  
  memset(arr, 0, sizeof(arr));
  memset(arr, 0, sizeof(arr) + sizeof(int));
}
Во втором memset выбило ошибку. Проверяется не просто принадлежность адреса по которому пишем к стеку, но и к переменной которая была передана на запись.

То есть если в memset передали адрес arr, то в него писать можно, хоть он и на стеке, а на остальное нельзя.

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

Пример можно поинтереснее было сделать:

#include <stdio.h>

void f(int* a, short* b) {
  *a = 0;
  *b = 1;
  printf("%d,%d\n", (int)*a, (int)*b); // Выводит 0, 1
}

int main() {
    int i = -1;
    f(&i, (short*)&i);
}
Так что С подходит для квантовых компьютеров %)

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

ну все…тему можно закрывать

Да ладно, все драйверы там тупо библиотеки, или вообще просто описание адресов куда замаплены внешние устройства, никаких особых приседаний как в обычных OS для ядра нет, разве только в обработчиках прерываний нужно соблюдать ограничения. Но мы то говорили про какой-то особый статус си, его отличающую от других низкоуровневость, так вот этого нет, полно языков которые могут даже сейчас заменить технически си в любом месте. Вот для примера очень высокоуровневый язык OCaml и вот проект OCaPIC который позволяет писать для 8 разрядного микро контролера с единицами килобайт ОЗУ на OCaml. На выходе выдает ассемблер и чистейший целевой бинарный код.

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

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

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

Стандарт не запрещает писать куда угодно

Это UB, так что я просто предоставляю особый behaviour.

Также такие проверки приведут к падению производительности.

Да.

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

Этот код отрабатывает и не выводит ошибкок с -fhardened -fcf-protection=full

А вот при исполнении второго приведённого примера срабатывает защита.

#include <stdio.h>

void read_block(FILE *f, char *buff)
{
        fread(buff, 1, 16, f);
}

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

int main(int argc, char **argv) {

    read_file("test.txt");

    return 0;
}
$ cat <<EOF > test.txt
01234567890123456789
EOF

Со стандартными флагами из Debian -fstack-protector-strong -D_FORTIFY_SOURCE=2:

$ gcc --version
gcc (Debian 13.2.0-9) 13.2.0
$ gcc -Wall -g -fstack-protector-strong -D_FORTIFY_SOURCE=2 -o test test.c
$ ./test
*** stack smashing detected ***: terminated
Aborted
gag ★★★★★
()
Ответ на: комментарий от MOPKOBKA

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

чтобы сделать язык безопасным, надо

  1. строгая типизация
  2. запрет адресной арифметики
  3. неприводимость числа к указателю
  4. тегированные вариантные записи (юнионы) с рантайм проверками
  5. рантайм проверки индекса в массиве, если нельзя проверить статически.

вроде ничего не забыл. и всему этому лет 45-50 минимум. можешь проверить, все эти модные «защищенные языки» реализуют как раз все эти прынцыпы.

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

1.строгая типизация

Использую.
Да и код более readable.

2. запрет адресной арифметики

Бывает и использую потому, что аргументы всегда проходят валидацию.

3. неприводимость числа к указателю

Не возникала необходимость.

4. тегированные вариантные записи (юнионы) с рантайм проверками
5. рантайм проверки индекса в массиве, если нельзя проверить статически.

Имеется.

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