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

Там какая-то шизофазия. «компилятор должен вывести», но в общем случае это невозможно вывести. Вот твой пример:

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

Эту функцию нужно скомпилировать для SysV ABI. Как ты её скомпилируешь, что ты будешь проверять? Как ты отличишь (по указателю) ситуацию char login[8]; char password[8]; от char something[16];?

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

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

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

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

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

До 64 битов часто было ещё int<->pointer которые на 64 битах стали багнутыми (32 vs 64)

Бгг. Сишники наговнокодили из расчёта, что указатель влезает в int, а багнутыми стали указатели. Оригинально.

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

Там какая-то шизофазия. «компилятор должен вывести», но в общем случае это невозможно вывести.

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

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

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

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

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

Во-первых, ты предлагаешь сломать ABI. Хорошо, зафиксировали.

Во-вторых, при чём тут стек? Если уж ломать ABI и кардинально замедлять всё, то можно обеспечить полный memory safety, что в стеке, что в куче.

В-третьих, как твой «стек с размерами» будет работать с

void read_block(FILE *f, char *buff)
{
        // чему равен len(buff1), если len(buff) = 0?
        char *buff1 = buff - 9999;
        fread(buff1, 1, 9999, f);
}
// вызывать так:
//     char buff[9999];
//     read_block(f, buff + 9999); // len(buff + 9999) = 0
и
void read_block(FILE *f, char *buff)
{
        // откуда возмёшь len(buff1)?
        char *buff1 = (char *) (((long) buff) ^ 11223344);
        fread(buff1, 1, 9999, f);
}
?

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

Во-первых, ты предлагаешь сломать ABI. Хорошо, зафиксировали.

Нет.

Во-вторых, при чём тут стек? Если уж ломать ABI и кардинально замедлять всё, то можно обеспечить полный memory safety, что в стеке, что в куче.

Что бы не ломать ABI.

В-третьих, как твой «стек с размерами» будет работать с

Это стек регионов, он не привязан к значению переменной.

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

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

короче твой стек размеров - чепуха.

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

Во-первых, ты предлагаешь сломать ABI. Хорошо, зафиксировали.

Нет.

Да. По твоему «Перекомпилировать основные библиотеки, что бы не ломать ABI можно ввести экспорт двух прототипов, с доп.значениями для проверки диапазонов и без» это очевидно.

Более того, там будет проблема с тем, что нужно вызвать функцию по указателю, но неизвестно, «старая» это функция или «новая». Т.е. либо указывать ABI в атрибутах указателя на функцию (кто это будет делать, лол?), либо просто новый код не сможет работать со старым и наоборот.

Это стек регионов, он не привязан к значению переменной.

А как это защитит от ((int*)rbp)[-3] = 0xBADC0DE;, если ((int*)rbp)-3 это валидный адрес, т.е. это будет эквивалентно isAdmin = 0xBADC0DE?

Где будет храниться этот стек регионов, кто мешает атакующему и туда накакать?

Ну и это будет как -fsanitize=address, только почему-то только для стека и гораздо медленнее (в твоём описании).

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

Да.

Нет.

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

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

А как это защитит от ((int*)rbp)[-3] = 0xBADC0DE;, если ((int*)rbp)-3 это валидный адрес

Это не будет валидным адресом, rbp не приписан объект.

Где будет храниться этот стек регионов, кто мешает атакующему и туда накакать?

В начале стека. Не сможет он туда попасть, потому что стек защищен.

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

Не ну правда, почитайте например об 1С и что такое конфигурация.

Ага, ну да.

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

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

Как мне FFT написать теперь, оно будет люто тормозить или что?

FFT можно вообще на ассемблере написать - оно небольшое в смысле размера кода,можно вручную каждую инструкцию проверить.

Ошибки же по большей части обнаруживаются (или не обнаруживаются:) в коде, который никакой особой скорости исполнения не требует. И вот там-то как раз «куча проверок» не была бы лишней.

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

watchcat382
()
Ответ на: комментарий от arkhnchul
  • дополняем в линкер и загрузчик требуемую функциональность;
  • добавляем в core необходимый API «- А как именно вы, мышки, станете ежиками - это уже ваши проблемы, я >стратегией занимаюсь.»(с)

А почему,собственно, нет? Берем какое-нибудь маленькое и простое ядро. Можно даже от Линукса версии 0.01 или какая там первая более-менее живая была. Или покопаться в архивах буржуйских вузов - там иногда в качестве дипломных работ ядра бывают. Думаем как приделать к этому ядру использование сегментного механизма i386. Пишем загрузчик elf,отображающий его секции на аппаратные сегменты. Благо в формате elf вроде как всё нужное есть. Настраиваем линкер на нашей рабочей системе чтобы он делал то что нам надо. В частности придется переделать сишный стартовый код,тот что перед main отрабатывает. Это часто делается при программировании для микроконтроллеров - можно там и подсмотреть как. Ну и начинаем эксперименты. Сейчас не DOS и не начало 90х,где любая малейшая опечатка в исходнике,пытающемся запустить защищенный режим - означала глухой повис компа и перезагрузку. Сейчас есть qemu под которым всё это можно запускать и удобно смотреть отладчиком что там происходит.

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

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

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

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

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

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

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

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

если вы хотите свои защиты внедрить в си

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

си можно выбросить на помойку, как язык ненужный.

Не то чтобы совсем на помойку,но использование надо бы существенно ограничить. Но к сожалению люди весьма инерционны в своих взглядах. Ассемблер перестали широко использовать только где-то к концу 90х. Хотя к тому времени компиляторы языков высокого уровня существовали уже не один десяток лет. И я не имею в виду случаи написания чего-то типа FFT на асме. Я например в середине 90х видел работающий складской учет в магазине спорттоваров,написанный на ассемблере x86. А уж просто так понаделать в своей программе ассемблерных вставок там где не надо - это обычным делом было. В то время я работал программистом,в смысле за деньги, причем по большей части именно «низкоуровневым». И было много заказов на доработку всякого «кооперативного» софта. В смысле который купили у каких-нибудь кооператоров,без исходников конечно,а потом после замены компа или даже версии доса - оно переставало работать. Так я много чего мягко говоря странного в коде насмотрелся.

а если вы хотите свои защиты внедрить в какие-то гипотетические >защищенные языки, то она там давно есть, и реализована совершенно иначе.

Как раз я в языки не хочу,это автор данной темы хочет. Я хочу на уровне исполняемых файлов. Обидно что современный линукс не умеет использовать те возможности процессора,что умела не только OS/2 тридцатилетней давности но даже и дос-экстендеры (не любые).

Ну и просто факт: компилятор Meridian ADA,работавший под DOS с дос-экстендером,мог при включении соответствующей настройки использовать сегментный механизм защиты памяти. То есть в обычном варианте(без экстендера) у него «адские» проверки были программные, а с экстендером можно было включить аппаратные. Неприятность была одна - этот их фирменный экстендер часто конфликтовал с тем что уже было загружено в памяти. То есть требовался «чистый» dos.

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

вам этот сегмент надо делать при входе и удалять при выходе

Архитектура i386 этого не запрещает. Вопрос только в программной реализации. Причем возможность создания сегмента «на ходу» была тридцать лет назад в некоторых дос-экстендерах. В OS/2 вроде бы тоже,но её я уже не копал настолько подробно - спроса на низкоуровневое программирование в ней особо небыло.

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

Архитектура i386 этого не запрещает.

вы как хотите ставить сегменты-то? вот есть код

void ff() {
  int a[10];
  int b[10];
  int c;
  ...
}

сколько сегментов вы хотите сделать тут?

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

Есть другой подход: W^X. Процесс может либо писать в память, либо выполнять её. Таким образом в стек нагадить можно, но передать туда выполнение не получится. К примеру в iOS этот подход используется. Вроде ещё в OpenBSD, но точно не уверен.

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

сколько сегментов вы хотите сделать тут?

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

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

в случае union {…} расстановка сегментов вообще ничего не дает.

плюс количество сегментов ограничено аппаратно.

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

сколько сегментов вы хотите сделать тут?

Если я правильно помню книжку, то технически программа может иметь 16 с хвостиком тысяч сегментов размером от одного байта до 4 гигабайт. Сомневаюсь что много программ у которых надется больше сущностей,требующих отдельного сегмента. Но и это не проблема - можно что-то объединить в один сегмент если вдруг не хватит.

В вашем примере в отдельные сегменты надо помещать массивы. А просто int за своё место в памяти не вылезет. Разве что кто-то попытается обратиться к нему как к long int. Но на это даже сишный компилятор выругается. Ну и сама функция(ее исполняемый код) может быть помещена в отдельный сегмент. Тогда вызвать ее можно будет только через точку входа(«шлюз») и исключается случайная передача управления в середину,например если каким-то образом будет испорчен адрес возврата где-нибудь. Да, goto куда_попало уже будет не написать. И это хорошо. (не просто goto,а именно куда попало - то есть внутри функции можно).

Кстати, с использованием сегментного механизма одна программа на i386 может иметь доступ к виртуальному адресному пространсту в 64 терабайта. И только несовершенство внутренностей ОС приводит к тому что адресное пространство для программы ограничено четырьмя гигами - опять потому что всё пихается в один сегмент. А он действительно ограничен четырьмя гигами.

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

Стек и так в отдельном сегменте

Ахаха, нет. Никогда не замечал, что stack overflow при переполнении - это давно забытое исключение, давно забытые слова? Всегда segmentation fault. Современные компиляторы даже код такой не умеют компилировать, который на стек в отдельном сегменте ложится.

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

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

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

при глубине рекурсии в 4000, функция с 4 локальными переменными, заберет все ваши сегменты. упс.

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

Современные компиляторы даже код такой не умеют компилировать, который на >стек в отдельном сегменте ложится.

Можно спросить что именно не ложиться в случае gcc? Оно же на микроконтроллеры код компилировать может,а там код в ПЗУ,а стек в ОЗУ,и это нередко разные «банки памяти».

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

Они различные операции со стеком делают, причём, явно рассчитывая, что переменные и стек в одном адресном пространстве. По-моему, они туда даже данные напрямую копируют. Простые pop и push - не, мы так сейчас не работаем. А што ПЗУ? ПЗУ - это ОК, если адресное пространство едино.

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

В файле map компоновщика перечислены все сегменты и их размеры.
В inet много исходников в которых можно посмотреть как формировать системные таблицы, содержащие данные о сегментах.

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

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

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

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

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

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

у вас функция с локальными переменными вызывает сама себя 10 000 раз… что будет происходить в таблицей сегментов процессора?

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

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

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

Ностальжи

Когда сделал ОСРВ для М6000, СМ-1, СМ-2М, ... многозадачной, то расширил функциональность загрузчика, ...
Всё работало ok!

Так шо не «диванный теоретик».

у вас функция с локальными переменными вызывает сама себя 10 000 раз…

Сходу не отвечу.
Если тема не заглохнет, то «покапаю» (да и вы посмотрите).
Тема то, интересная.

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

короче общий вердикт.

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

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

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

  2. работа с сегментами(создание/уничтожения) - небыстрое дело.

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

про сегменты читать тут:

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

А основная проблема вот: «operating system uses segmentation in limited way.»

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

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

watchcat382
()

https://translated.turbopages.org/proxy_u/en-ru.ru.b85dba27-65c62568-cdb74cc2... Как мне найти максимальный размер стека?

https://translated.turbopages.org/proxy_u/en-ru.ru.b85dba27-65c62568-cdb74cc2... Увеличьте размер стека в Linux с помощью setrlimit

https://translated.turbopages.org/proxy_u/en-ru.ru.b85dba27-65c62568-cdb74cc2... Что такое сегмент данных c / c ++ и размер стека?

https://russianblogs.com/article/17241071674/ Глубокое понимание адресации памяти ядра LInux

https://russianblogs.com/article/27983699579/ «Встроенный Linux и IoT разработка программного обеспечения -Deep Анализ ядра C -языка» 1 1.4 Адрес памяти и адресация, выравнивание памяти ...

https://russianblogs.com/article/12851176014/ Анализ ядра Linux (3) ---- Первое понимание подсистемы управления памятью Linux

https://russianblogs.com/article/58804214432/ Управление памятью Linux: сегментированный механизм и механизм обработки памяти адресации памяти

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

Они различные операции со стеком делают, причём, явно рассчитывая, что >переменные и стек в одном адресном пространстве. По-моему, они туда даже >данные напрямую копируют.

Прямая запись в сегмент стека допустима если один из сегментных регистров установить на элемент таблицы,описывающий сегмент стека и адресоваться относительно него. И это не обязательно должен быть регистр SS, можно и другой. Относительно именно SS работают push/pop и call/ret, а mov может иметь префикс замены сегментного регистра.

ПЗУ - это ОК, если адресное пространство едино.

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

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

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

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

watchcat382
()