355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Арнольд Роббинс » Linux программирование в примерах » Текст книги (страница 35)
Linux программирование в примерах
  • Текст добавлен: 6 мая 2017, 11:00

Текст книги "Linux программирование в примерах"


Автор книги: Арнольд Роббинс



сообщить о нарушении

Текущая страница: 35 (всего у книги 55 страниц)

11.8. Пересечение минного поля безопасности: setuid root

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

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

В частности, стоит специально изучить проблемы безопасности Linux/Unix и потратить время на обучение написанию программ setuid root. Если вы сразу нырнете в эту проблему, прочитав лишь эту книгу и ничего более, можно быть уверенным, что ваша система будет взломана, легко и сразу. Маловероятно, что вы или ваши клиенты будут довольны.

Вот несколько руководящих принципов:

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

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

• При изменении или сбрасывании привилегий используйте setresuid(), если она у вас есть. В противном случае используйте setreuid(), поскольку у этих функций самая чистая семантика. Используйте setuid(), лишь когда вы хотите сделать постоянное изменение.

• Переходите от root к обычному пользователю в соответствующем порядке: сначала установите набор групп и значения GID, затем значения UID. Будьте особенно осторожны с fork() и exec(); действительные и эффективные UID при их вызове не изменяются, если вы не измените их явным образом.

• Рассмотрите использование прав доступа setgid и особой группы для вашего приложения. Если это будет работать, это убережет вас от большой головной боли.

• Рассмотрите отказ от наследуемого окружения. Если вам нужно сохранить некоторые переменные окружения, сохраните как можно меньше. Убедитесь в предоставлении подходящих значений для переменных окружения PATH и IFS.

• Избегайте execlp() и execvp(), которые зависят от значения переменной окружения PATH (хотя это менее проблематично, если вы сами восстанавливаете PATH).

Это просто некоторые из множества тактик по пересечению опасной зоны, известной своими подвохами, минами-ловушками и фугасами. Ссылки на другие источники информации см. в следующем разделе.

11.9. Рекомендуемая литература

Безопасность Unix (а следовательно, и GNU/Linux) является темой, требующей знаний и опыта для того, чтобы справиться с ней должным образом. В Эпоху Интернета она стала лишь труднее, не проще.

1. Practical UNIX & Internet Security, 3rd edition, by Simson Garfinkel, Gene Spafford, and Alan Schwartz, O'Reilly & Associates, Sebastopol, CA, USA, 2003. ISBN: 0-596-00323-4.

Это стандартная книга по безопасности Unix.

2. Building Secure Software. How to Avoid Security Problems the Right Way, by John Viega and Gary McGraw. Addison-Wesley, Reading, Massachusetts, USA, 2001. ISBN: 0-201-72152-X.

Это хорошая книга по написанию безопасного программного обеспечения, она включает проблемы setuid. Предполагается, что вы знакомы с основными API Linux/Unix; к моменту прочтения данной книги вы должны быть готовы к ее прочтению.

3. "Setuid Demystified," by Hao Chen, David Wagner, and Drew Dean. Proceedings of the 11th USENIX Security Symposium, August 5–9, 2002 http://www.cs.berkeley.edu/~daw/papers/setuid-usenix02.pdf.

Гарфинкель, Спаффорд и Шварц (Garfinkel, Spafford, Schwartz) рекомендуют прочтение этого материала «до того, как вы даже подумаете о написании кода, который пытается сохранять и восстанавливать привилегии». Мы всецело согласны с ними.

11.10. Резюме

• Использование значений ID пользователя и группы (UID и GID) для идентификации файлов и процессов – вот что превращает Linux и Unix в многопользовательские системы. Процессы имеют значения как действительных, так и эффективных UID и GID, а также набор дополнительных групп. Обычно именно эффективный UID определяет, как один процесс может повлиять на другой, и эффективные UID, GID и набор групп проверяются на соответствие с правами доступа к файлу. Пользователи с эффективным UID, равным нулю, известные как root или суперпользователи, могут делать все, что захотят; система не использует для такого пользователя проверку прав доступа.

• Концепции сохраненных set-user ID и set-group ID пришли из System V и были приняты POSIX с полной поддержкой в GNU/Linux. Наличие этих отдельных значений ID дает возможность легко и безошибочно переключать при необходимости действительные и эффективные UID (и GID).

• Программы setuid и setgid создают процессы, в которых действительные и эффективные ID различаются. Программы как таковые помечаются дополнительными битами прав доступа к файлу. Биты setuid и setgid должны быть добавлены к файлу после его создания.

• getuid() и geteuid() получают значения действительного и эффективного UID соответственно, a getgid() и getegid() получают значения действительного и эффективного GID соответственно, getgroups() получает набор дополнительных групп, а в среде POSIX может запросить у системы, сколько членов содержит набор групп.

• Функция access() осуществляет проверку прав доступа к файлу для действительного пользователя, давая возможность программе setuid проверить полномочия реального пользователя. Обратите внимание, что часто проверка возвращаемых stat() сведений может не представить полной картины при условии, что файл может находиться на не родной или сетевой файловой системе.

• Функция GLIBC euidaccess() сходна с access(), но осуществляет проверку на основе значений эффективных UID и GID.

• «Липкий» бит и бит setgid при использовании с каталогами привносят дополнительную семантику. Когда для каталога установлен бит setgid, новые файлы в этом каталоге наследуют группу этого каталога. Новые каталоги делают то же самое, они также автоматически наследуют установку бита setgid. Без установленного бита setgid новые файлы и каталоги получают эффективный GID создающего их процесса. «Липкий» бит, установленный для каталогов, в которые в других отношениях разрешена запись, ограничивает право на удаление файла владельцем файла, владельцем каталога и root.

• Набор групп изменяется с помощью setgroups(). Эта функция не стандартизована POSIX, но существует на всех современных системах Unix. Ее может использовать лишь root.

• Изменение UID и GID довольно сложно. Семантика различных системных вызовов с течением времени изменилась. Новые приложения, которые будут изменять лишь свои эффективные UID/GID, должны использовать seteuid() и setegid(). Приложения, не действующие от имени root, могут также устанавливать свои эффективные ID с помощью setuid() и setgid(). Вызовы setreuid() и setregid() от BSD были предназначены для обмена значениями UID и GID; их использование в новых программах не рекомендуется.

• Приложения, действующие как root, могут перманентно заменить значения действительного, эффективного и сохраненного ID с помощью setuid() и setgid(). Одним из таких примеров является login, которая должна превратиться из программы, выполняющейся как root в не непривилегированную зарегистрированную оболочку, выполняющуюся от имени обычного пользователя.

• Функции Linux setresuid() и setresgid() следует использовать всегда, когда они доступны, поскольку они обеспечивают самое чистое и наиболее надежное поведение

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

Упражнения

1. Напишите простую версию команды id. Ее назначением является отображение в стандартный вывод ID пользователя и группы с указанием имен групп. Когда эффективный и действительный ID различаются, выводятся оба. Например:

$ id

uid=2076(arnold) gid=42(devel) groups=19(floppy), 42(devel), 2076(arnold)

Ее использование:

id [ пользователь ]

id -G [ -nr ] [ пользователь ]

id -g [ -nr ] [ пользователь ]

id -u [ -nr ] [ пользователь ]

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

 -G  Выводит все значения групп в виде чисел, без имен.

 -n  Выводит лишь имена, без числовых значений. Применяется с значениями пользователя и группы.

 -g  Выводит лишь эффективный GID.

 -u  Выводит лишь эффективный UID.

2. Напишите простую программу с именем sume и установите setuid на себя. Она должна запрашивать пароль (см. getpass(3)), который в целях данного примера может быть жестко вшит в исходный код программы. Если лицо, запустившее программу, вводит пароль правильно, sume должна выполнить exec оболочки. Попросите другого пользователя помочь вам ее протестировать.

3. Как вы относитесь к тому, чтобы сделать sume доступной для ваших друзей? Для ваших приятелей студентов или сотрудников? Для каждого пользователя на вашей системе?

Глава 12 Общие библиотечные интерфейсы – часть 2

В главе 6, «Общие библиотечные интерфейсы – часть 1», был представлен первый набор API библиотеки общего пользования. В некотором смысле, эти API поддерживают работу с фундаментальными объектами, которыми управляют системы Linux и Unix: время дня, пользователи и группы для файлов, сортировка и поиск.

Данная глава более эклектична; функции API, рассмотренные здесь, не особо связаны друг с другом. Однако, все они полезны в повседневном программировании под Linux/Unix. Наше представление движется от простых, более общих функций API к более сложным и более специализированным.

12.1. Операторы проверки: assert()

Оператор проверки (assertion) является утверждением, которое вы делаете о состоянии своей программы в определенный момент времени ее исполнения. Использование операторов проверок для программирования было первоначально разработано Хоаром (C.A.R. Hoare)[121]121
  Однако, в своей лекции в честь присуждения премии Тьюринга Ассоциации по вычислительной технике в 1981 г. д-р Хоар утверждает, что эту идею выдвинул сам Алан Тьюринг – Примеч. автора.


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

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

/* lsearch – возвратить индекс с данным значением в массиве или -1,

   если не найдено */

int lsearch(int *array, size_t size, int value) {

 size_t i;

 /* предусловие: array != NULL */

 /* предусловие: size > 0 */

 for (i = 0; i < size; i++)

  if (array[i] == value)

   return i;

 /* постусловие: i == size */

 return -1;

}

Этот пример определяет условия, используя комментарии. Но не было бы лучше проверить условия с использованием кода? Это является задачей макроса assert():

#include /* ISO С */

void assert(/* скалярное выражение */);

Когда скалярное выражение ложно, макрос assert() выводит диагностическое сообщение и завершает программу (с помощью функции abort(); см. раздел 12.4 «Совершение самоубийства: abort()»). ch12-assert.c снова предоставляет функцию lsearch(), на этот раз с оператором проверки и функцией main():

1  /* ch12-assert.с – демонстрация операторов проверки */

2

3  #include

4  #include

5

6  /* lsearch – возвращает индекс с данным значением в массиве или -1, если не найдено */

7

8  int lsearch(int *array, size_t size, int value)

9  {

10  size_t i;

11

12  assert(array != NULL);

13  assert(size > 0);

14  for (i = 0; i < size; i++)

15   if (array[i] == value)

16    return i;

17

18  assert(i == size);

19

20  return -1;

21 }

22

23 /* main – проверить наши условия */

24

25 int main(void)

26 {

27 #define NELEMS 4

28  static int array[NELEMS] = { 1, 17, 42, 91 };

29  int index;

30

31  index = lsearch(array, NELEMS, 21);

32  assert(index == -1);

33

34  index = lsearch(array, NELEMS, 17);

35  assert(index == 1);

36

37  index = lsearch(NULL, NELEMS, 10); /* won't return */

38

39  printf("index = %dn", index);

40

41  return 0;

42 }

После компиляции и запуска оператор проверки в строке 12 «выстреливает»:

$ ch12-assert /* Запуск программы */

ch12-assert: ch12-assert.c:12: lsearch: Assertion 'array != ((void *)0)' failed.

Aborted (core dumped)

Сообщение от assert() варьирует от системы к системе. Для GLIBC на GNU/Linux сообщение включает имя программы, имя файла с исходным кодом и номер строки, имя функции, а затем текст завершившегося неудачей условия. (В этом случае именованная константа NULL проявляется в виде своего макрорасширения '((void*)0)'.)

Сообщение 'Aborted (core dumped)' означает, что ch12-assert создала файл core; т.е. снимок адресного пространства процесса непосредственно перед его завершением.[122]122
  Как упоминалось в разделе 10.2 «Действия сигналов», некоторые дистрибутивы GNU/Linux запрещают создание файлов core. Чтобы снова разрешить их, поместите в свой файл ~/.profile строку 'ulimit -S -с unlimited' – Примеч. автора.


[Закрыть]
Этот файл может быть использован впоследствии с отладчиком; см. раздел 15.3 «Основы GDB». Создание файла core является намеренным побочным результатом assert(); предполагается, что произошла решительная ошибка, и вы хотите исследовать процесс с помощью отладчика для ее определения.

Вы можете отменить оператор проверки, компилируя свою программу с помощью опции командной строки '-DNDEBUG'. Когда этот макрос определен до включения , макрос assert() расширяется в код, который ничего не делает. Например:

$ gcc -DNDEBUG=1 ch12-assert.c -о ch12-assert /* Компиляция с -DNDEBUG */

$ ch12-assert /* Запуск */

Segmentation fault (core dumped) /* Что случилось? */

Здесь мы получили настоящий дамп ядра! Мы знаем, что операторы проверки были запрещены; сообщения «failed assertion» нет. Что же случилось? Рассмотрите строку 15 lsearch() при вызове из строки 37 main(). В этом случае переменная array равна NULL. Доступ к памяти через указатель NULL является ошибкой. (Технически различные стандарты оставляют «неопределенным» то, что происходит при разыменовывании указателя NULL. Наиболее современные системы делают то же, что и GNU/Linux; они завершают процесс, посылая ему сигнал SIGSEGV; это, в свою очередь, создает дамп ядра. Этот процесс описан в главе 10 «Сигналы».

Этот случай поднимает важный момент относительно операторов проверки. Часто программисты ошибочно используют операторы проверки вместо проверки ошибок времени исполнения. В нашем случае тест 'array != NULL' должен был быть проверкой времени исполнения:

if (array == NULL) return -1;

Тест 'size > 0' (строка 13) менее проблематичен; если size равен 0 или меньше 0, цикл никогда не исполнится, и lsearch() (правильно) возвратит -1. (По правде, этот оператор проверки не нужен, поскольку код правильно обрабатывает случай 'size <= 0'.)

Логика, стоящая за отменой оператора проверки, заключается в том, что дополнительные проверки могут снизить производительность программы и поэтому должны быть запрещены в заключительной версии программы. Хоар[123]123
  Hints On Programming Language Design, C.A.R. Hoare Stanford University Computer Science Technical Report CS-73-403 (ftp://reports.stanford.edu/pub/cstr/reports/cs/tr/73/403/CS-TR-73-403.pdf). December, 1973 – Примеч. автора.


[Закрыть]
, однако, сделал такое замечание:

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

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

Наконец, отметим следующее из раздела «Ошибки» справочной страницы GNU/Linux assert(3):

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

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

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

Справочная страница предостерегает нас от использования при вызовах assert() выражений с побочными эффектами:

assert(*p++ == 'n');

Здесь побочным эффектом является увеличение указателя p как часть теста. Когда определен NDEBUG, аргумент выражения исчезает из исходного кода; он никогда не исполняется. Это может привести к неожиданной неудаче. Однако, как только при подготовке к отладке запрет на операторы проверки отменяется, все начинает снова работать! Такие проблемы трудно отследить.

12.2. Низкоуровневая память: функции memXXX()

Несколько функций предоставляют возможность для работы с произвольными блоками памяти низкоуровневые службы. Все их имена начинаются с префикса 'mem':

#include /* ISO C */

void *memset(void *buf, int val, size_t count);

void *memcpy(void *dest, const void *src, size_t count);

void *memmove(void *dest, const void *src, size_t count);

void *memccpy(void *dest, const void *src, int val, size_t count);

int memcmp(const void *buf1, const void *buf2, size_t count);

void *memchr(const void *buf, int val, size_t count);

12.2.1. Заполнение памяти: memset()

Функция memset() копирует значение val (интерпретируемое как unsigned char) в первые count байтов буфера buf. Она особенно полезна для обнуления блоков динамической памяти:

void *p = malloc(count);

if (p != NULL)

 memset(p, 0, count);

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

12.2.2. Копирование памяти: memcpy(), memmove() и memccpy()

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

void *memcpy(void *dest, const void *src, size_t count)

Это простейшая функция. Она копирует count байтов из src в dest. Она не обрабатывает перекрывающиеся области памяти. Функция возвращает dest.

void *memmove(void *dest, const void *src, size_t count)

Подобно memcpy(), она также копирует count байтов из src в dest. Однако, она обрабатывает перекрывающиеся области памяти. Функция возвращает dest.

void *memccpy(void *dest, const void *src, int val, size_t count)

Эта копирует байты из src в dest, останавливаясь либо после копирования val в dest, либо после копирования count байтов. Если она находит val, то возвращает указатель на положение в dest сразу за val. В противном случае возвращается NULL.

Теперь, в чем проблема с перекрывающейся памятью? Рассмотрим рис. 12.1.

Рис. 12.1. Перекрывающиеся копии

Целью является скопировать четыре экземпляра struct xyz от data[0] до data[3] в участок от data[3] до data[6]. Здесь проблемой является data[3], побайтовое копирование с перемещением в памяти из data[0] затрет data[3] до того, как он будет безопасно скопирован в data[6]! (Может возникнуть также сценарий, когда копирование в памяти в обратном направлении разрушит перекрывающиеся данные.)

Функция memcpy() была первоначальной функцией в System V API для копирования блоков памяти; ее поведение для перекрывающихся блоков памяти не была подробно определена тем или иным способом. Для стандарта С 1989 г. комитет почувствовал, что это отсутствие определенности является проблемой, поэтому они придумали memmove(). Для обратной совместимости memcpy() была оставлена, причем поведение для перекрывающейся памяти было специально отмечено как неопределенное, а в качестве процедуры, корректно разрешающей проблемные случаи, была предложена memmove().

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

Как для memcpy(), так и для memmove() (как и для strcpy()) буфер назначения является первым аргументом, а источник – вторым. Чтобы запомнить это, обратите внимание на порядок, который тот же самый, как в операторе присваивания:

dest = src;

(Справочные страницы во многих системах не помогают, предлагая прототип в виде 'void *memcpy(void *buf1, void *buf2, size_t n)' и полагаясь на то, что текст объяснит, что есть что. К счастью, справочная страница GNU/Linux использует более осмысленные имена.)


    Ваша оценка произведения:

Популярные книги за неделю