* Предотвратяване на препълване на буфера
Публикувано на 30 ноември 2008 в раздел ОСУП.
Това е както един от трудните за реализиране проблеми, така и един от най-трудните за предотвратяване. Има няколко основни правила, които трябва да имаме предвид:
1. Не вярвайте на готовите библиотеки. Ето ви един непълен списък на вградени функции от С, които са доказано уязвими:
strcpy(), strcat(), printf(), sprintf(), vsprintf(), gets(), scanf(), fscanf(), sscanf(), vscanf(), vsscanf(), vfscanf(), realpath(), getopt(), getpass(), streadd(), strecpy(), strtrns() и др.
Ето ви и един eлементарен пример:
char buf[10]; gets(buf);
Въведете повече от 10 символа и ще видите неприятен резултат.
2. Ако директният достъп до паметта не ви е нужен, то се опитайте да стартирате проекта си чрез език от по-високо ниво (например тези, използващи виртуални машини - Java, .Net, ...).
3. Не стартирайте програмата си в незащитена среда. Използвайте jail, SELinux или други подобни "защитени среди".
4. Правете пълни проверки на всяка една информация, подадена от потребител. Става дума както за валидни (допустими) данни, така и за техният размер.
5. Винаги се грижете за поставянето на '\0' в края на низовете!
Ако все пак ни се наложи да пишем на С или подобен език, позволяващ уязвимости от тип препълване на буфер - как все пак да пишем сигурен код? Когато става дума за буфери съществуват два подхода:
- Използване на статичен буфер (например масив): при отчитане на препълване моментално се отказва достъп.
- Използване на динамичен буфер (например вектор): автоматично разширява буфера, така че да резервира допълнително пространство за данни.
Въпреки, че статичния подход изглежда по-сигурен, той има своите недостатъци. На практика всеки минимален пропуск в сигурността на кода е достатъчен за постигане на препълване на буфера.
При динамичният подход имаме сериозно предимство - не позволяваме никога, никой буфер да бъде препълнен. За нещастие обаче ние сме ограничени от максималното количество памет на системата. При този подход се появяват два други популярни пробива - претоварването на stack или heap. С други думи тук решаваме проблема с препълване на буфера, но си създаваме значително по-лесните за постигане атаки като denial of system чрез обикновен flood с голямо количество данни.
Независимо кой подход сте избрали и с кой вид уязвимост се борите, ви препоръчваме да използвате само "сигурни" функции:
- При статични буфери това са например bcopy(), fgets(), memcpy(), snprintf(), strccpy(), strcadd(), vsnprintf(), strncpy() и strncat() (те обаче също си имат свои специфични особености, например strncpy() и strncat() не терминират низовете с '\0'). Използвайте и sprintf() с особено внимание (задължително слагайте спецификатори за максимална дължина на изхода - напр. използвайте %.10s, вместо %s). Търсете и специфични за системата функции (например при BSD съществуват strlcpy() и strlcat(), които решават редица проблеми от strncpy() и strncat()).
- При динамични буфери разчитайте на вече популярни като "сигурни" библиотеки. Например за С това е библиотеката SafeStr. При C++ е въведена библиотека std::string. За съжаление всеки път когато ви се наложи да ги преобразувате към char* се получава пробив в сигурността.
В заключение ще кажем - използвайте С/С++ и подобни езици с изключително повишено внимание. Особено при С предотвратяването на buffer overflow е изключително сложна задача.
Примери за добри практики:
a) От по-горния пример би било по-добре да използваме fgets():
#define BUFSIZE 10 char buf[BUFSIZE]; fgets(buf, BUFSIZE, stdin);
b) Ако имате възможност да знаете големината на входните данни винаги правете проверка:
if(strlen(source_data) >= destination_size){ ... throw exception ... }
c) Използвайте формат при sprintf():
void main(int argc, char **argv) { char buffer[1024]; sprintf(buffer, "%.1023s", argv[0]); ... }
d) Добавяйте същата маска и при sscanf():
void main(int argc, char **argv) { char buf[1024]; sscanf(argv[0], "%1023s", &buf); }
e) Въвеждайте броячи навсякъде:
char buf[10]; int i = 0; char ch; while((ch = getchar()) != '\n') { if(ch == '\0') break; if(i==10) break; buf[i++] = ch; } buf[i] = '\0';
Като заключение ще споменем един силен подход за решение на проблема. Използвайте т.нар. "тунели". Това са малки програми, написани на "сигурен" език и четящи информацията, подадена от протребителя, във виртуална среда. Те се използват за прочитане и почистване на входните данни, като препращат "сигурна" информация към програмите с по-ниско ниво на достъп до паметта.
polzvai strncpy(), vsnprintf() i t.n. *n*() f-ii