Решил немного разобраться как работают уязвимости. Как я понял, весомая их часть модифицирует стек.
#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 можно ввести экспорт двух прототипов, с доп.значениями для проверки диапазонов и без, дублирование прототипов понадобится для малого числа функций