Студопедия
Случайная страница | ТОМ-1 | ТОМ-2 | ТОМ-3
АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатика
ИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханика
ОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторика
СоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансы
ХимияЧерчениеЭкологияЭкономикаЭлектроника

Отказ от привилегий.

Читайте также:
  1. EV9.2 Анализ характера и последствий отказов (FMEA)
  2. В дороге в небо снова отказав
  3. В случае подачи заявки после 25 ноября организаторы оставляют за собой право увеличить стоимость семинара, либо отказать в участии!
  4. МЕТОДЫ ОТКАЗА ОТ СОЦИАЛЬНОГО СОТРУДНИЧЕСТВА
  5. Отказ от ответственности
  6. Отказ подчиниться приказам

Опасный код на Си.

 

Автор: David Chisnall.
Оригинальное название: «Writing Insecure C».
Оригинал статьи: Part 1, Part 2, Part 3 (в данной редакции все три части объединены).
Перевод на русский: n0xi0uzz.
Оригинал перевода: http://netsago.org/ru/docs/1/14/
Коррекция и форматирование: Клуб программистов «Весельчак У».
Публикуется с разрешения автора перевода.

Содержание.

 

 

Введение.

 

Использование языка программирования Си часто приводит к написанию очень опасного кода. Но это не совсем справедливое обвинение. Такие проекты, как OpenBSD, показывают, что возможно писать безопасный код на Си. Проблема Си та же, что и в ассемблере — язык открывает вам не только все возможности архитектуры, но и кое-что ещё. Он дает все возможности для написания безопасного кода, но не делает эти вещи сам.

этой статье рассмотрены стандартные примеры ошибок в коде Си и то, как их и избежать.

 

Проверка ошибок.

 

Множество современных языков программирования включают в себя некоторый механизм по обработке исключений. Вообще, исключения — это плохая идея. Они усложняют управление ходом программы, и у них есть много недостатков, от которых страдали программы с использованием GOTO перед рассветом структурного программирования. Программист должен проектировать код программы так, чтобы не пришлось обрабатывать исключения. Тем не менее, у исключений есть одно важное преимущество: вы не можете их игнорировать. В частности, код на Java часто засорен блоками try:catch, которые ничего не делают, кроме отбрасывания ошибок, но даже в этом случае механизм исключений преследует цель: это заставляет программиста помнить о том, что он небезопасно обрабатывает условия ошибок.

Си большинство функций возвращают неправильное значение, когда произошла какая-либо ошибка. Обычно это делается двумя способами. Множество функций возвращают код ошибки ноль в случае успеха. Функции, возвращающие указатели, возвращают правильное значение в случае успеха или ноль в противном случае. Эта ситуация может немного смущать, так как ноль означает и успешное выполнение одних функций, и ошибку в других. Можно возвращать нулевой указатель, его вы не сможете легко проигнорировать, так как получите ошибку сегментации, как только попытаетесь разыменовать его. Этот подход действительно опасен только в тех функциях, которые почти никогда не завершаются с ошибкой, в других ошибка может быть обнаружена во время тестирования и исправлена. Стандартный пример функции, которая почти никогда не завершается с ошибкой — malloc(), наряду со связанными с ней функциями, вроде calloc(). В спецификации Си указано, что malloc() должна вернуть NULL в случае, если памяти недостаточно, чтобы удовлетворить запрос. Linux не полностью следует этому правилу, он возвращает NULL, если в системе недостаточно виртуального адресного пространства, чтобы выполнить выделение памяти, но в случае недостатка памяти Linux по-прежнему выделяет пространство адресов, а затем возвращает ошибку, если вы пытаетесь использовать эту память. Как бы то ни было, исходя из того, что у вас есть подходящая реализация Си, необходимо проверять значения, которые возвращает malloc.

большинстве случаев вы не сможете сделать ничего разумного, если malloc завершится с ошибкой. Даже код, восстанавливающий ошибку, обычно нуждается в выделении памяти. Вы можете попробовать выделять эту память, когда программа запускается (не забудьте проверить, что вы можете получать к ней доступ). В качестве альтернативы, вы можете использовать что-нибудь вроде этого макроса:

 

Код: (c)

#define MALLOC(x,y) do { y = malloc(x); if (!y) abort(1); } while(0)

 

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

 

Начальные значения.

 

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

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

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

 

Код: (c)

int a = 42;

 

затем вы решаете, что вам необходимо условие для инициализации, поэтому вы копируете и вставляете его в выражение if:

 

Код: (c)

if ({некоторое условие})
{
int a = 42;
}

 

Эй, вы же забыли объявить переменную за пределами блока if, поэтому вы делаете это позже и даете ей значение по умолчанию:

 

Код: (c)

int a = 0;

if ({некоторое условие})
{
int a = 42;
}

 

еперь у вас есть код, который компилируется и запускается. Большинство времени (когда не встречается условие), он будет работать. Но код внутри фигурных скобок будет «холостым». Он определяет новую переменную под названием «a» и присваивает ей значение 42. Как только блок закончится, эта переменная выйдет из области видимости и останется старая «a», до сих пор со значением 0.

олее незаметный вариант может возникнуть при опечатке в инициализации, когда вы выполняете что-то, вроде этого:

 

Код: (c)

int value = value + 12;

 

огда на самом деле ожидаете выполнение этого:

 

Код: (c)

int value = othervalue + 12;

 

ак только компилятор распарсит int value, переменная станет валидной и будет находиться в области видимости. Вы можете считать её значение, добавить 12 к нему и присвоить результат ей же. К несчастью для вас, value, которую вы считываете, не определена. Теперь вам придется присвоить value неопределенное значение. Если вы считываете неосторожно, вы можете подумать, что проинициализировали её, хотя вы этого и не сделали. Компилятор с оптимизацией удалит +12, поскольку неопределенность плюс 12 есть неопределенность, и это будет эквивалентно следующему:

 

Код: (c)

int value;

 

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

 

Код: (c)

int a;

call_some_function(&a);

 

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

 

Проблемы целых.

 

сли вы используете высокоуровневые языки программирования, поддержка числовых переменных в Си может показаться мучительно примитивной. То же самое может быть, если раньше вы писали на ассемблере, так как Си также не открывает программисту условных кодов современных ЦПУ. В высокоуровневых языках числа обычно условно-точные, причем точность определена так, как вам надо. В Си есть набор целых типов. Наиболее часто используемый — это int, который должен соответствовать машинному слову. На компьютерах, на которых я учил Си, оно было длиной в 16 бит. Теперь он обычно размером в 32 бита даже на архитектурах, где машинное слово имеет длину 64 бита, так как большое количество написанного кода подразумевает, что он всегда имеет длину 32 бита. Одна из наиболее частых причин странного поведения — попытка хранить значение указателя в int. На обычных 32-битных платформах этот метод работает хорошо. На некоторых 64-битных он работает, а на некоторых — нет. Стандарт C99 определяет новый целый тип, intptr_t, который гарантированно имеет достаточный размер, чтобы хранить указатель. Если вы планируете хранить указатель в int, вы всегда должны использовать intptr_t.

казатель — один из других, слегка сбивающих с толку, моментов. Си определяет два типа указателей, один указывает на код, другой указывает на данные. Указатель на функцию не имеет гарантированно такой же размер, что и указатель, указывающий на данные. На множестве встроенных платформ часто используются 16-битные указатели для кода и 32-битные для данных. Преобразование типа в void* указателя на функцию приведет к тому, что некоторые данные будут отброшены.

ругим основным видом целого в Си является char, short и long. В C99 также определен long long. Ни один из них не имеет фиксированного размера, но все они имеют минимальные размеры (технически у них есть минимальный набор значений, который они могут хранить, не предоставляя гарантий по внутреннему формату). Short должен быть, по крайней мере, 16 бит, long как минимум 32, а lon long как минимум 64. Если вам необходимо минимальное количество точности, выберите один из них, и вы сможете получить дополнительное пространство к тому, что запросили, в зависимости от архитектуры.

не упомянул char ещё и потому, что они детально отличаются от других типов. Все другие основные целые типы данных являются знаковыми, пока не будут объявлены, как беззнаковые. Это не всегда так в случае с char. К сожалению, в случае с char, знаковая она, или нет, полностью зависит от реализации. Если вы используете char, всегда явным образом объявляйте их как знаковые или нет, так как в противном случае вы можете быть удивлены позже.

Си есть несколько довольно удивительных правил для безусловных преобразований этих типов в операциях. Часто предполагают, что точность операции зависит от того, как её используют. Предположим, вы делаете следующее:

 

Код: (c)

a = b + c;

 

ак как вы храните результат в «a», вы можете предположить, что сложение будет выполнено, каким бы типом ни была «a». По факту, оно будет выполнено с типом «b» и «c». Это имеет смысл тогда, когда вы думаете, что лучше иметь значение (b + c) в зависимости от чего-то, чем «b» и «c» сами по себе. Вы можете думать, что тип этого выражения будет типом «b». Стандарт C99 определяет набор правил для определения типа, но в основном он будет представлять собой тот тип, который больше, «b» или «c». Например, если «a» — char, а «b» — int, тип выражения будет int. Поразительно часто встречается ошибка, когда делают что-то вроде такого:

 

Код: (c)

long long a;
long b; // = чему-то
long c; // = чему-то
a = b * c;

 

азмерность «a» как минимум 64 бита, «b» и «c» как минимум 32 бита. Так как у вас есть только гарантия того, что «b» и «c» по 32 бита, вы не должны помещать нечто большее в них, даже если ваш компилятор или платформа имеет long в 64 бита. Когда вы умножаете их, вы получаете результат, подходящий для 64-битного int и вы храните его в чем-то, что подходит для 64-битного int. Звучит удобно? Так и есть, кроме того факта, что результат искажается до 32 бит прямо перед присваиванием. Вот правильный способ сделать это:

 

Код: (c)

a = (long long)b * c;

 

то расширяет «b» до 64 бит (или больше, в зависимости от реализации). Расширение типа гарантирует, что «c» имеет тип такого же размера, что и «b», так что она также расширена. Тогда умножение происходит как умножение двух long long с 32 или более первыми нулями, а результат (long long) хранится в переменной типа long long. В основном, вы можете избежать сюрпризов путем прямого преобразования типов к чему-то с достаточной точностью перед выполнением операции. Убедитесь, что отсутствие знака выполняется для всех переменных. Это очень частая причина ошибок, так как вы неожиданно теряете первый бит при преобразовании большого беззнакового значения к его знаковому эквиваленту. Более неуловимая ошибка возникает от переполнения int. Она особенно часто возникает при использовании malloc, так как стандартный шаблон написания — malloc(i * sizeof(x)). Если у взломщика есть влияние на «i», он может попытаться выполнить это переполнение. Для очень больших значений «i» это даст результат гораздо меньший, чем «i», что приведет к проблеме. Вызов malloc будет успешным, но когда вы попытаетесь использовать массив, полученный в результате, только первые несколько значений будут валидными. Взломщик может вынудить вас переписать другие данные. Простым путем избегания этого вида уязвимости может быть использование calloc() вместо malloc() (конечно, в надежде, что реализация calloc в вашей системе производит проверку границ сама, а не просто делает malloc() и memset() для количество*размер байт).

ealloc() — ещё одна проблема. Нет стандартного пути сделать это с ней, поэтому вам надо делать это самому. К счастью, OpenSSH включает в себя xrealloc(), который является версией realloc() с проверкой границ. Она включает несколько других проверок, но если вам не нужны все из них, вы можете реализовать упрощенную версию:

 

Код: (c)

void * xrealloc(void *ptr, size_t nmemb, size_t size)
{
void *new_ptr;
size_t new_size = nmemb * size;

if (SIZE_T_MAX / nmemb < size)
return NULL;

return realloc(ptr, new_size);
}

 

тот тест довольно прост. SIZE_T_MAX — это максимальное значение, которое может иметь size_t. Когда вы делите на указанное количество элементов, вы получаете максимальный размер, который может быть без переполнения. Если этот размер меньше, чем требуемое пространство, возникает переполнение, поэтому вы возвращаете NULL. realloc возвращает NULL в случае ошибки, так что вам всегда следует проверять возвращаемое значение realloc на валидность. К сожалению, это является наиболее частой причиной утечек памяти (которые, в свою очередь, являются причиной атак DDoS). Если realloc() возвращает NULL, исходный указатель по-прежнему является валидным. Часто разработчики забывают этот принцип и просто делают что-то вроде этого:

 

Код: (c)

ptr = realloc(ptr, newsize);

 

огда realloc() возвращает NULL, вы теряете вашу ссылку на предыдущее выделение памяти. FreeBSD предоставляет удобную функцию, reallocf(), которая эквивалентна следующей:

 

Код: (c)

void *reallocf(void* ptr, size_t size)
{
void *newptr = realloc(ptr, size);
if (NULL == newptr)
{
free(ptr);
}

return newptr;
}

 

сли у вас нет кода для обработки случая, когда realloc() завершается с ошибкой, вам необходимо использовать что-то вроде неё.

 

Проблемы с памятью.

 

одель организации памяти в Си делит память на два участка — куча и стек. Память в куче выделяется и освобождается вручную. Память в стеке является контекстной — она выделяется автоматически при входе в блок и освобождается, когда блок завершается. Эта методика приводит к проблемам при передаче данных назад к вызываемой функции. Для структур очевидным решением является просто вернуть структуру. Когда она скомпилирована, вызывающая функция может выделить структуру и передать указатель к вызываемой функции, которая копирует данные в свое пространство. Тем не менее, это приводит к большому количеству копий. Что ещё хуже, это не работает для всех динамически выделяемых данных. Рассмотрим что-нибудь, вроде sprintf(). Этот аналог printf() пишет в буфер вместо стандартного вывода. Проблема sprintf в том, что вызывающей функции необходимо знать размер буфера, что не всегда просто — на самом деле, это требует реализации большинства кода sprintf в вызывающей функции. По факту, почти невозможно использовать sprintf безопасно. Вам необходимо указать длину каждого отдельного элемента строки формата (вот почему sprintf была создана). Это может привести к завершению процесса (что может также вызвать проблемы безопасности, и мы вернемся к этому позднее), поэтому некоторые реализации libc включают в себя asprintf().

ункция asprintf() — это то же самое, что и sprintf, кроме того, что она выделяет свой собственный буфер с помощью malloc. Это позволяет избежать досрочного завершения процесса или переполнения. К несчастью, она приносит новую проблему, когда вызывающая функция должна освобождать указатели, которые возвращает вызываемая функция.

ольшое количество кода на Си содержит в себе эту проблему. Решение обычно заключается в том, чтобы поместить в документацию о функции строку «вызывающая функция должна освободить возвращаемый указатель». К сожалению, этот подход делает сложной возможность взглянуть на часть кода и увидеть, валидна ли она. Одно из решений заключается в том, чтобы заключать в оболочку каждый указатель, например, так:

 

Код: (c)

typedef struct _RefCountedBuffer
{
void *buffer;
int refcount;
void (free*)(struct _RefCountedBuffer*);
} *RefCountedBuffer;

 

огда вы возвращаете указатель, вы создаете одну из этих структур и устанавливаете refcount в 1. Когда вы получаете его, вы всегда вызываете функцию, которая увеличивает refcount на единицу и вызывает соответствующую функцию free(), если она достигнет нуля. Эта методика близка для программистов Objective-C, потому как OpenStep реализует схожий механизм работы. GNUstep продвинулся дальше, предоставляя макросы ASSIGN() и DESTROY(). Эти макросы делают ошибки при работе с памятью более редкими, и мы можем сделать нечто похожее на обычном Си.

режде всего, нам надо определить функции retain() и release():

 

Код: (c)

RefCountedBuffer retain(RefCountedBuffer *buf)
{
buffer->refcount++;

return buffer;
}

void release(RefCountedBuffer *buf)
{
buf->refcount--;

if (buf->refcount == 0)
buf->free(buf);
}

 

чтите, что это упрощенные версии тех функций, что вам могут действительно потребоваться. Наиболее распространенная проблема — они не являются потоко-безопасными. Операторы ++ и -- обычно компилируются в последовательность команд загрузить, добавить (или вычесть), сохранить. Если два потока, например, одновременно аккумулируют, они оба должны сначала загрузить эти значения, прежде чем сохранить их, и одна аккумуляция будет потеряна. Вы можете обойти эту проблему, используя специальный ассемблер для ЦПУ или встроенные средства в GCC для элементарных операций.

пределив эти функции, вы можете определить макросы SET и FREE следующим образом:

 

Код: (c)

#define SET(var, val) do { \
RefCountedBuffer __tmp = retain(val); \
if (NULL!= var) release(var); var = __tmp; } while(0)

 

чтите, что вы удерживаете новое значение перед освобождением старого, что может привести к проблемам в случае, если новое и старое значения равны. Соответствующий макрос FREE() довольно прост:

 

Код: (c)

#define FREE(var) do { release(var); var = NULL; } while(0)

 

тот макрос гарантирует, что каждый указатель всегда установлен в NULL после его освобождения. Даже если вы не используете подсчет ссылок, этот метод дает результат. Если вы используете оба этих макроса, у вас будет очень мало знаков равенства в коде. Что делает проще просмотр кода и поиск места, где могут быть ошибки, связанные с памятью. Подсчет ссылок — хорошее решение для данных, предназначенных только для чтения. Вы можете вернуть некоторый внутренний компонент большой структуры данных в качестве ссылки, не удаляя его из оригинальной структуры, пока его не перестанут использовать — так долго, как он находится в структуре RefCountedBuffer в обоих случаях. Тем не менее, эта модель не решает нашу первоначальную проблему функций, похожих на asprintf. Она требует вернуть строку, что часто используется только в вызывающей функции. Для этого выделение памяти в куче и заключения её в структуру подсчета ссылок будет лишним. Вам нужен способ выделить пространство в стековом фрейме вызывающей функции.

разработчиков сервера Dovecot IMAP есть превосходное решение. В дополнение к обычному контролю стека они делают отдельный стек данных и аналог asprintf, которая использует его. Вызывающая функция сначала вызывает функцию, которая выделяет фрейм в новом стеке данных. Затем она вызывает аналог asprintf(), которая выделяет пространство в стеке результатов. Этот подход работает правильно, пока вызывающая функция достает верхний фрейм стека данных. Так как контрольный стек и стек данных работают на разных скоростях, вы можете просто вернуть данные в стек данных. Работа организована таким образом, что сначала создается фрейм в стеке данных, а затем вызывается asprintf(), которая выделяет пространство в текущем фрейме стека данных и помещает результат в него. Вы используете результат, а затем выталкиваете из стека текущий фрейм.

ам ничего не мешает иметь несколько независимых областей памяти. Вы можете использовать mmap() на /dev/zero, чтобы выделить где-либо непрерывный блок памяти и использовать его по своему желанию. Одной из возможных идей может быть выделение стека данных, который работает на такой же скорости, как и контрольный стек. Используйте этот метод для всех массивов, которые вы будете по-другому выделять в стеке. В отличие от контрольного стека. Вы можете сделать его переместимым, постоянно адресуя его через глобальный указатель к началу. Например, с помощью подобного макроса:

 

Код: (c)

#define NEW_ARRAY(type, name, elements) \
__attribute__((cleanup(popDataStack))) \
type *name = dataStackCalloc(sizeof(type), elements)

 

ttribute__(cleanup) — это расширение GCC. Оно вызывает функцию popDataStack() с указателем на переменную в качестве аргумента, когда переменная выходит из области видимости. Теперь у вас есть указатель на что-то в стеке данных. Вместо использования прямого адреса, вы можете использовать макрос, который добавляет этот адрес указателю. Все это позволяет продолжать расти вашему стеку данных до тех пор, пока у вас есть достаточное количество непрерывной свободной памяти для хранения.

ак бы то ни было, вы до сих пор можете выйти за пределы массива. Вы не разрушите возвращаемый адрес, но можете переписать некоторые данные, или могут возникнуть другие проблемы. Вы можете избежать выхода за пределы стека данных, вызвав функцию mprotect() на последней странице стека, чтобы удалить все права доступа. У большинства реализаций malloc() есть режим отладки, который вставляет этот вид защитной страницы после каждого выделения памяти. Вы можете сделать это, создав свой стек данных, в котором будут чередоваться доступные и недоступные страницы, с каждым выделенным массивом, так что он завершится недоступной страницей, но эта система достаточно ресурсоемка. Никакая нормальная операционная система не выделит реальную память для защитных страниц, но вы потеряете много пространства в промежутках между началами страниц и началами массивов и будете использовать большое количество адресного пространства.

 

Буферы и строки.

 

троки в Си — вечная причина проблем. Когда Си был создан, было две концепции о том, как лучше всего реализовывать строки — известные сейчас как строки Си и строки Pascal, в соответствии с языками, которые сделали эти идеи популярными. Такие языки, как Lisp использовали третью реализацию: строки являлись связанным списком символов (Erlang до сих пор использует эту модель). Строки в стиле Lisp имеют очевидный недостаток. Каждому символу требуется один байт для хранения символа и 4 или 8 байт для хранения адреса следующего, до девяти байт уходит на хранение одного байта данных. Эта структура далека от идеальной, но делает разделение и конкатенацию строк очень простой.

олее совершенные модели представляют строки как связанный список массивов символов, позволяя легко их объединять. Все эти модели могут быть (и это было сделано) реализованы на Си, но стандартные строковые функции до сих пор работают с массивами байтов.

ольшинство «классических» строковых функций практически невозможно использовать безопасно (по этой причине линковщик OpenBSD легко выдает предупреждение, когда вы используете одну из них). Каноническим примером «плохой» функции является strcat(), которая принимает два указателя на строки Си. Функция проходит по первой строке, пока не найдет null, она записывает в нее байты из второй строки пока не дойдет до null во второй строке. Вызывающая функция должна быть уверена, что существует достаточно места в первой строке, чтобы сохранить вторую.

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

OpenBSD представлена strlcat, которая похожа на strncat, но возвращает сумму входных строк. Если результат работы функции больше третьего аргумента, имело место искажение. Эта функция находится в каждой ОС семейства BSD (включая Darwin/OS X), но её нет в glibc, так как, согласно главному разработчику glibc, является «бесполезным хламом BSD». К счастью, лицензия BSD позволяет вам копировать эту функцию из libc BSD в ваш код без каких-либо проблем.

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

дной из самых опасных вещей в C99 является модель массивов переменной длины, которая позволяет вам выделять маленькие, с динамическим размером массивы в стеке. Вы можете всегда делать это с помощью alloca(), хотя качество реализаций alloca() варьируется между разными платформами. Следующие примерно эквивалентны:

 

Код: (c)

int *a = alloca(sizeof(int) * n);

int a[n];

 

азница в поведении, если существует недостаточно пространства для роста стека до n целых значений. Первая строка будет при этом NULL, но вы сможете проверить это, что упрощает отладку — достаточно просто обратиться к началу массива. Во второй строке, если не будет хватать размера стека, он будет указывать: куда-то. А куда именно, будет зависеть от реализации. Вот почему, если вы используете массивы C99 с переменной длиной, невозможно протестировать программу на переполнение стека. В большинстве случаев это не является проблемой. Небольшие выделения памяти более безопасны для работы, но если взломщик сможет увеличить размер n, у вас может получиться массив, указывающий никуда.

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

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

 

Когда все идет не так.

 

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

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

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

азбивая компоненты программы на отдельные процессы, вы ограничиваете размеры разрушений, которые может принести один баг. Самым распространенным примером является разделение привилегий кода, находящееся во множестве серверных программ. Многие сервера должны быть запущены под root или с соответствующими правами. Им нужна эта возможность для привязки к соответствующим портам и осуществления других действий в качестве root, таких, как получение доступа к данным разных пользователей или запуск программы под другим пользователем. Хорошим методом для такого вида процессов является заключение кода, осуществляющего привилегированные операции, в отдельный процесс. Например, если вам надо написать на разные почтовые ящики пользователей, у вас может быть процесс, который запускается под root, чтобы открыть почтовый ящик, написать в него письмо и больше ничего. Он проверит аргументы и осуществит запись, но не более того. Это легко реализовать в коде и довольно просто найти возможные баги. Также можно изолировать код, содержащий важную информацию. Большей части программы не надо знать, например, ключей шифрования или паролей. Вы можете хранить такую информацию в отдельном процессе и ограничивать доступ главной программы к нему, усложнив тем самым для взломщика получение доступа к этим данным.

 

Отказ от привилегий.

 

отя разделение привилегий работает хорошо, многое может быть сделано благодаря простому отказу от привилегий. UNIX содержит семейство системных вызовов setuid(), которые позволяют процессу, запущенному в качестве root, работать как другой пользователь.

еб-сервер нуждается в запуске в качестве root, так как ему нужно быть привязанным к 80 порту и иметь доступ к файлам в директории public_html каждого пользователя. Тем не менее, как только он привязан к 80 порту, ему не нужно более работать как root, и он может отказаться от прав root. Хотя ему по-прежнему нужен способ получать доступ к директории public_html каждого пользователя. Одно решение — заставить каждого пользователя сделать его файлы доступными для группы веб-сервера. Другим может быть выполнение fork() процесса-потомка для каждого пользователя, который работает в качестве этого пользователя и имеет доступ к файлам в директории пользователя.

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

ользователь с правами root может легко избежать chroot, создав новое устройство для жесткого диска и смонтировав его внутрь chroot (или даже получая доступ к нему напрямую). Вот почему важно сбросить привилегии root сразу же после входа в chroot.

щё легче использовать chroot(), если он встроен в приложение. Команда chroot, которая запускает процесс в окружении chroot, также доступна, но этот подход имеет две проблемы. Первая — он вызывает chroot до запуска процесса, поэтому программа и все необходимые библиотеки должны быть внутри директории chroot. Вторая заключается в том, что он должен выполняться в качестве root, поэтому ему нужно что-то внутри chroot для возможности сброса привилегий. Обычным решением является поместить команду su внутрь chroot. Хотя, когда вы поместите так много кода внутрь chroot, он станет выглядеть, как и внешнее окружение.

 


Дата добавления: 2015-10-26; просмотров: 139 | Нарушение авторских прав


<== предыдущая страница | следующая страница ==>
DVD 1. Chapter 7| ВОЗРОЖДЕННАЯ ТАНТРА

mybiblioteka.su - 2015-2024 год. (0.028 сек.)