C, PHP, VB, .NET

Дневникът на Филип Петров


* Предотвратяване на препълване на буфера

Публикувано на 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';

Като заключение ще споменем един силен подход за решение на проблема. Използвайте т.нар. "тунели". Това са малки програми, написани на "сигурен" език и четящи информацията, подадена от протребителя, във виртуална среда. Те се използват за прочитане и почистване на входните данни, като препращат "сигурна" информация към програмите с по-ниско ниво на достъп до паметта.

 



Един коментар


Добави коментар

Адресът на електронната поща няма да се публикува


*