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)

((int*)rsp)[3] = 0xBADC0DE;

Чистый C (без стандартов от NASA или Toyota) как раз и используется для вот таких конструкций, для всего остального есть Rust или современный C++ с натыканным RAII, или уж Ada, если на то пошло.

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

Стек и так в отдельном сегменте, надо просто запретить в него писать мимо объектов.

Хотя по хорошему нужно иметь два стека, стек возвратов и стек данных.

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

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

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

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

Не успел поправить: возможно return address имелся в виду. Да, проблема. Но я уверен - даже без buffer overruns на стеке я найду способ передать управление куда надо если overrun имеет место быть в принципе.

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

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

Но убрать подобное поведение на стеке будет полезно. Я не прав?

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

https://ru.wikipedia.org/wiki/Иерархия_памяти

https://translated.turbopages.org/proxy_u/en-ru.ru.02bba092-65c30cfb-f6f656f8... Структура иерархии памяти и ее характеристики

https://dzen.ru/a/Yh2t8Bo4gxeu8STQ Как устроена ЭВМ? Организации памяти и управление памятью

...

Немного о нынешних технологиях разработки.

Функции, программы, ... ничего не знают друг о друге, а как-то взаимодействовать им нужно.

К сожалению нынешние технологии разработки - НАСКАЛЬНЫЕ.

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

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

Например:

static int a, b;

int main() {
  int *pa = &a, *pb = &b + 1;
  if (pa == pb) {
    printf("A\n");
  } 
  if ((long)pa == (long)pb) {
    printf("B\n");
  } 
  return 0;
} 
./a.out 
B

Если писать код не зная как работает «виртуальная машина С», он по любому развалится с выходом новой версии компилятора, поэтому никаких фич нету, есть незнание.

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

Я описал это в посте, если непонятно

Так себе идея. И самое главное - мне кажется что вы не отдаёте отчёт что подобного уровня проверки должны делаться в железе. Или оно обходится на раз-два.

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

Может и должны, но не делаются. Почему бы их не ввести уже сейчас? Можешь ли ты придумать способ обхода? И почему идея так себе? Это интуитивная оценка?

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

Либо ты знаешь как работает процессор и как он работает с памятью либо нет.

Можешь объяснить с точки зрения процессора, почему в моем примере выполняется только второе условие? Ведь указатели ведут на один объект, их числовые значения идентичны, cmp pa, pb выдаст ZF=1.

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

Нужно отличать запись в память от записи в стек мимо объекта.

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

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

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

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

Растоманы все равно привязаны к библиотекам на Си, так что их тоже замедлим.

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

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

Почему бы их не ввести уже сейчас?

Начнём с того что как именно Вы собрались различать запись в стек как результат выгрузки auto-variables не уместившихся в регистры, и запись по указателю пришедшему извне?

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

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

Проверка на ошибку записи будет такой:

Аргумент АдресЗаписи
 
ФлагОшибки = ПроверкаПересечения(
  АдресЗаписи,
  ИсключитьДиапазон(ДиапазонСтека, ДиапазонБуфера)
)

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

Для памяти не из стека, ДиапазонБуфера будет 0...0, и следовательно любая запись в стек будет являться ошибкой.

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

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

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

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

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

У меня указатели на метаданные переменной (это много более функционально).

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

Ну восемь адресов для нашего времени это явный перебор. А вот предусмотреть с запасом под стек пару мегабайт (на фоне основной памяти, измеряемой гигабайтами и десятками гигабайт это копейки) — почему бы нет?

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

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

Как именно вы себе это представляете? Вы на disasm вообще хоть раз смотрели?

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

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

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

Добавочка к предыдущему посту.

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

Шутка

Вы уж простите «сектанта метаданных».
Впрочем те кто использует struct - СЕКТАНТЫ!

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

Было

char buff[10];
some_func(1, buff, 2);

Станет

char buff[10];

void *pStart = &buff[0]; // это делает компилятор сам
void *pEnd = &buff[10];

some_func(1, buff, pStart, pEnd, 2);

pStart, pEnd передаются не через обычный стек, а через отдельный, что бы не ломать ABI.

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

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

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

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

А как ты себе это представляешь? Обратной совместимости не будет при любых раскладах, весь софт придётся переписать заново.

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

Объясни пожалуйста почему выполняется только второе условие:

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

Указатели же равны?

Программа на С выполняется не абы как, и далеко не все позволяет, есть стандарт.

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