Home Map Index Search News Archives Links About LF
[Top bar]
[Bottom bar]
эта страница доступна на следующих языках: English  Castellano  Deutsch  Francais  Nederlands  Indonesian  Russian  

[image of the authors]
автор Frédéric Raynal, Christophe Blaess, Christophe Grenier

Об авторе:

Christophe Blaess - независимый инженер по аэронавтике. Он почитатель Linux и делает большую часть своей работы на этой системе. Заведует координацией переводов man страниц, публикуемых Linux Documentation Project.

Christophe Grenier - студент 5 курса в ESIEA, где он также работает сисадмином. Страстно увлекается компьютерной безопасностью.

Frédéric Raynal использует Linux не сертифицированный никакими патентами(програмными и др.). Кроме того, вы должны посмотреть Dancer in the Dark : помимо Bjork которая великолепна, этот фильм не может оставить вас неподвижным (я не могу сказать больше не раскрыв конца, одновременно трагического и великолепного).


Содержание:

 

Как избежать дыр в безопасности при разработке приложения - Часть 1

[article illustartion]

Резюме:

Данная статья - первая из серии статей о основных типах дыр в безопасности в приложениях. Мы покажем способы, как избегать их немного изменяя свои привычки разработчика.



 

Введение

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

Для сисадмина каждодневная работа состоит в чтении списков, посвященных проблемам безопасности, и немедленном обновлении пакетов, упомянутых в этих списках. Программисту может быть полезным проверка подобных проблем безопасности, так как избежание дыр в безопасности в начале - лучший способ исправления их. Мы попытаемся определить некоторые "классические" опасные поведения программ и дадим советы, как уменьшить риск. Мы не будем говорить о проблемах сетевой безовасности, так как они часто возникают из-за ошибок в конфигурации (небезопасные скрипты cgi-bin, ...) или из-за ошибок в системе, позволяющих проводить атаки DOS-типа(Denial Of Service - отказ в обслуживании), мешая машине принимать запросы от своих клиентов. Эти проблемы - забота сисадминов и разработчиков ядра. Однако и программист приложения должен защитить код, как только программа получает доступ к внешним данным пользователя. Некоторые версии pine, acroread, netscape, access,... имеют превышенный доступ к информации или производят утечку информации при некоторых условиях. По существу, безопасность программирования - забота каждого.

Данный цикл статей показывает методы, которые могут быть использованы для повреждения Unix системы. Мы могли бы только упомянуть о них или сказать пару слов, однако мы предпочитаем полное разъяснение, чтобы дать людям осознать риск. Таким образом, при отладке программы или при разработке своей, вы сможете обойти или исправить эти ошибки. При рассмотрении каждой дыры, мы будем придерживаться одного подхода. Будем начинать с уточнения способа ее работы. Затем, мы покажем как избежать ее. Для каждого примера мы будем использовать дыры в безопасности до сих пор присутствующие в широко распространенном програмном обеспечении.

В данной - первой - статье рассказывается об основах, нужных для изучения дыр в безопасности: знакомство с привилегиями и битами Set-UID и Set-GID. Далее мы анализируем дыры основанные на функции system(), так как они просты для понимания.

Мы будем часто использовать маленькие программы на Си, чтобы показать то, о чем мы говорим. Однако подходы, описаные в данных статьях, применимы и к другим языкам программирования: perl, java, скрипты shell... Некоторые дыры в безопасности зависят от языка, однако это не всегда так, что мы и увидим при рассмотрении system().

 

Привилегии

В Unix системе пользователи не являются равноправными, то же верно и для приложений. Доступ к узлам файловой системы - и соответственно внешним устройствам машины - зависит от строгого контроля за подлинностью. Некоторым пользователям резрешено производить весьма чувствительные операции для поддержания системы в нормальном состоянии. Число, именуемое UID (User Identifier - идентификатор пользователя) позволяет производить идентификацию. Чтобы упростить процесс, имя пользователя соответствует этому числу. Эта связь произведена в файле /etc/passwd.

UID равный 0 с именем по умолчанию root, имеет полный доступ к системе. Он может создавать, изменять, удалять любой системный узел, он может также изменять физическую конфигурацию машины, монтировать разделы, активизировать сетевые интерфейсы и менять их конфигурацию (IP адрес) или делать системные вызовы такие как mlock() для управления физической памятью или sched_setscheduler() для изменения механизма очередности. В одной из следующих статей мы узнаем о возможностях Posix.1e, которые позволяют ограничивать привилегии приложения, выполняемого как root, но сейчас будем предполагать, что привилегированный пользователь может делать все что угодно на машине.

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

Чтобы пользоваться привилегиями другого пользователя, не имея возможности войти в систему под ним, нужно как минимум иметь возможность взаимодействовать с приложением, работающим с UID жертвы. Когда приложение - процесс - работает под Linux, оно имеет строго определенную индивидуальность. Во-первых программа имеет атрибут, называемый RUID (Real UID - истинный UID), который соответствует ID пользователя, запустившего его. Этим атрибутом управляет ядро и обычно он не может быть изменен. Второй атрибут завершает информацию: поле EUID (Effective UID - действующий UID) - его принимает во внимание ядро при управлении правами доступа (открытие файлов, зарезервированные системные вызовы).

Получить привилегии другого пользователя - значит производить все действия под его UID, а не под настоящим UID. Естественно, взломщик пытается получить ID root-а, однако многие другие учетные записи также представляют интерес, или потому что они дают доступ к системной информации (news, mail, lp...) или потому что позволяют читать личную информацию (почту, личные файлы и т.д.) или же они могут использоватся для скрытия нелегальной деятельности, например, атак на другие сайты.

Чтобы запустить приложение с привилегиями, где действующий UID отличен от истинного (UID пользователя, который запустил его), выполняемый файл должен иметь установленным специальный бит, называемый Set-UID. Этот бит находиться в атрибуте прав доступа файла (также как и биты исполнения, чтения, записи для пользователя, членов группы и остальных пользователей) и имеет восьмеричное значение 4000. Бит Set-UID представлен буквой s при отображении прав с помощью команды ls:

>> ls -l /bin/su
-rwsr-xr-x  1 root  root  14124 Aug 18  1999 /bin/su
>>

Команда "find / -type f -perm +4000" отображает список системных приложений, бит Set-UID у которых установлен в 1. Когда ядро выполняет программу с установленым битом Set-UID, оно использует UID владельца программы в качестве EUID процесса. С другой стороны, RUID не изменяется и соответствует пользователю, запустившему программу. Например, любой пользователь может иметь доступ к программе /bin/su, но она выполняется с правами владельца (root) с полными привелегиями в системе. Излишне говорить, что надо быть осторожным при написании программы с этим атрибутом.

Каждый процесс также имеет действующий ID группы, EGID, и истинный идентификатор RGID. Бит Set-GID (восьмеричное 2000) в правах доступа выполняемого файла, говорит ядру, что надо использовать группу владельца файла как EGID, а не GID пользователя, запустившего программу. Интересная комбинация возникает иногда при Set-GID установленном в 1, но без бита выполнения для группы. Фактически это соглашение, не имеющее ничего общего с привилегиями, относящимися к приложениям, однако указывающее, что файл может быть заблокирован функцией fcntl(fd, F_SETLK, lock). Обычно приложение не использует бит Set-GID, однако случается и такое. Например, некоторые игры используют его для сохранения лучших результатов в системный каталог.

 

Типы атак и возможные мишени

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

Первые два типа атак обычно пытаются запустить командный процессор с привилегиями владельца приложения, в то время как третий тип нацелен на получение прав записи на защищенные системные файлы. Доступ по чтению иногда является результатом слабой защиты системы (личные файлы, письма, файл паролей /etc/shadow, псевдоконфигурационные файлы ядра в /proc).

Целью атак в основном являются программы с установленым битом Set-UID (или Set-GID). Однако атаки также можно производить на любое приложение запущеное под ID отличным от ID пользователя. Большую часть таких программ представляют системные демоны. Демон - приложение, обычно запускаемое при загрузке, выполняющееся в фоне без управляющего терминала, которое делает привелигерованную работу для любого пользователя. Например демон lpd позволяет любому пользователю посылать документ на принтер, sendmail - принимает и перенаправляет электронную почту, или apmd - запрашивает у Bios статус батареи портативного компьютера. Некоторые демоны управляют связью с внешними пользователями по сети (Ftp, Http, Telnet... сервисы). Сервер, называемый inetd, управляет соединениями многих из этих сервисов.

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

 

Изменение уровня привилегий

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

  int seteuid (uid_t uid);
Процесс всегда может поменять значение своего EUID и присвоить ему значение RUID. В этом случае старый UID хранится в сохранном месте, называемом SUID (Saved UID - сохраненный UID), не путать с SID (Session ID - ID сессии), который используется для взаимодействия с управляющим терминалом. Всегда возможно вернуть назад SUID в качестве EUID. Естественно, программа с нулевым EUID (root) может менять по желанию как свой EUID так и RUID (таким образом работает /bin/su).

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

  uid_t e_uid_initial;
  uid_t r_uid;

  int
  main (int argc, char * argv [])
  {
    /* Сохраняем различные UID-ы */
    e_uid_initial = geteuid ();
    r_uid = getuid ();

    /* Ограничиваем права доступа до прав пользователя,
     * запустившего программу */
    seteuid (r_uid);
    ...
    privileged_function ();
    ...
  }

  void
  privileged_function (void)
  {
    /* Возвращаем назад начальные привилегии */
    seteuid (e_uid_initial);
    ...
    /* Часть кода, которой нужны привилегии */
    ...
    /* Назад к правам пользователя, запустившего программу */
    seteuid (r_uid);
  }

Данный метод намного безопаснее чем к сожалению общепринятый, состоящий в использовании начального EUID, а затем к временному уменьшению привелегий перед "рисковаными" операциями. Однако, данное уменьшение привилегий бесполезно при атаках на переполнение буфера. Как мы увидим в следующей статье, эти атаки пытаются заставить приложение выполнить свои инструкции, которые могут содержать системные вызовы, нужные для повышения уровня привилегий. И все-таки данный подход защищает от вызова внешних команд и большинства ситуаций перехвата.

 

Выполнение внешних команд

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

  int system (const char * command)
 

Опасность использования функции system()

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

/* system1.c */

#include <stdio.h>
#include <stdlib.h>

int
main (void)
{
  if (system ("mail $USER < system1.c") != 0)
    perror ("system");
  return (0);
}
Предположим, эта программа Set-UID root :
>> cc system1.c -o system1
>> su
Password:
[root] chown root.root system1
[root] chmod +s system1
[root] exit
>> ls -l system1
-rwsrwsr-x  1 root  root  11831  Oct 16  17:25 system1
>>

Чтобы выполнить эту программу, система запускает оболочку (/bin/sh) с опцией -c, опция сообщает ей инструкцию для запуска. Затем, оболочка проходит через иерархию каталогов в соответствии с переменной окружения PATH, чтобы найти выполняемый файл, который называется mail. Чтобы скомпрометировать программу, пользователю достаточно поменять значение этой переменной перед запуском приложения. Например:
  >> export PATH=.
  >> ./system1

ищет команду mail только в текущем каталоге. Достаточно создать выполняемый файл (например скрипт, запускающий командный процессор) и назвать его mail, и программа будет затем запущена с EUID-ом владельца основного приложения! Здесь наш скрипт запускает /bin/sh. Однако, так как он запустился с перенаправленным стандартным входом (как начальная команда mail), мы должны вернуть его на терминал. Создаем скрипт:
#! /bin/sh
# "mail" script running a shell
# getting its standard input back.
/bin/sh < /dev/tty

Вот результат:
>> export PATH="."
>> ./system1
bash# /usr/bin/whoami
  root
bash#

Конечно, первое решение состоит в указании полного пути к программе, например /bin/mail. Тогда возникает новая проблема: местоположение приложения зависит от установки системы. Если /bin/mail обычно есть на любой системе, то например где находится GhostScript? (может он в /usr/bin, /usr/share/bin, /usr/local/bin?). С другой стороны, еще один тип атаки возможен при использовании некоторых старых оболочек: использование переменной окружения IFS. Оболочка использует ее при разборе слов в командной строке. Эта переменная содержит разделители. По умолчанию это пробел, табуляция и возврат каретки. Если пользователь добавит туда слэш /, команда "/bin/mail" будет понята оболочкой как "bin mail". Выполняемый файл, который называется bin, в текущем каталоге может быть выполнен всего лишь установкой переменной PATH, как мы видели ранее, что позволит запустить эту программу с EUID приложения.

Под Linux переменная окружения IFS - уже не проблема, с тех пор как bash и pdksh закрыли ее используя символы по умолчанию при запуске. Но помня о переносимости приложения, вы должны знать, что некоторые системы могут быть менее безопасными относительно этой переменной.

Некоторые другие переменные окружения могут вызвать неожиданые проблемы. Например, приложение mail позволяет пользователю выполнить команду при написании сообщения используя управляющую последовательность "~!". Если пользователь пишет "~!command" в начале строки, команда выполняется. Программа /usr/bin/suidperl, используемая для запуска скриптов perl с битом Set-UID, вызывает /bin/mail, чтобы отправить сообщение root-у при обнаружении проблемы. Так как /usr/bin/suidperl Set-UID root, вызов /bin/mail происходит с привилегиями root и содержит имя файла, который вызвал ошибку. Тогда пользователь может создать файл, имя которого содержит возврат каретки с последующим ~!command и еще одним переводом. Если perl скрипт вызываемый suidperl аварийно завершает работу из-за низкоуровневой проблемы, относящейся к этому файлу, посылается сообщение от root, которое содержит управляющую последовательность приложения mail, и команда из имени файла выполняется с правами root.

Этой проблемы не существует, так как программа mail не допускает приема управляющих последовательностей при автоматическом выполнении (не с терминала). К сожалению, недокументированная возможность данного приложения (вероятно оставшаяся от отладки) допускает управляющие последовательности, если установлена переменная окружения interactive. Результат? Дырой в безопасности можно легко воспользоваться (и широко используется) в приложении, которое должно улучшать безопасность системы. Ответственность за это разделена. Первое, /bin/mail содержит недокументированную опцию особенно опасную, т.к. она позволяет выполнение кода, проверяя только посылаемые данные, что должно быть априори подозрительным для почтовой утилиты. Второе, даже если разработчики /usr/bin/suidperl не знали о переменной interactive, они не должны были при вызове внешней команды оставлять окружение выполнения таким же, каким оно и было, особенно делая эту программу Set-UID root.

Фактически Linux игнорирует биты Set-UID и Set-GID при выполнении скриптов (см. /usr/src/linux/fs/binfmt_script.c и /usr/src/linux/fs/exec.c). Но хитрыми действиями можно обойти это правило, как делает Perl со своими скриптами используя /usr/bin/suidperl, чтобы принять к сведению эти биты.

 

Решение

Не всегда легко найти замену функции system(). Первый вариант - использовать системные вызовы такие как execl() или execle(). Однако это будет совсем не то, так как внешняя программа будет вызываться не как подпрограмма, а будет заменять текущий процесс. Вам придется создать новый процесс при помощи fork и проанализировать аргументы командной строки. Таким образом программа:

  if (system ("/bin/lpr -Plisting stats.txt") != 0) {
    perror ("Printing");
    return (-1);
  }

превращается:
pid_t pid;
int   status;

if ((pid = fork()) < 0) {
  perror("fork");
  return (-1);
}
if (pid == 0) {
  /* дочерний процесс */
  execl ("/bin/lpr", "lpr", "-Plisting", "stats.txt", NULL);
  perror ("execl");
  exit (-1);
}
/* родительский процесс */
waitpid (pid, & status, 0);
if ((! WIFEXITED (status)) || (WEXITSTATUS (status) != 0)) {
  perror ("Printing");
  return (-1);
}

Очевидно, код стал тяжелее! В некоторых ситуациях он становиться довольно сложным, например, когда вам надо перенаправить стандартный вход приложения, как например:
system ("mail root < stat.txt");

То есть перенаправление обозначенное < делается оболочкой. Вы можете делать то же используя сложную последовательность такую как fork(), open(), dup2(), execl() и т.д. В таком случае, приемлемым решением будет использование функции system(), но с предварительной конфигурацией всего окружения.

Под Linux переменные окружения хранятся в форме указателя на таблицу символов: char ** environ. Эта таблица заканчивается NULL. Строки хранятся в форме "ИМЯ=значение".

Мы начинаем удаление окружения при помощи Gnu расширения:

    int clearenv (void);

или присваивая указателю
    extern char ** environ;

значение NULL. Далее инициализируются важные переменные окружения, используя контролируемые значения, при помощи функций:
    int setenv (const char * name, const char * value, int remove)
    int putenv(const char *string)

перед вызовом функции system(). Например:
    clearenv ();
    setenv ("PATH", "/bin:/usr/bin:/usr/local/bin", 1);
    setenv ("IFS", " \t\n", 1);
    system ("mail root < /tmp/msg.txt");

Если необходимо, вы можете сохранить содержимое некоторых полезных переменных перед очищением окружения (HOME, LANG, TERM, TZ и т.д.). Содержимое, форма представления, размер этих переменных должны быть строго проверены. Это важно, что вы очищаете все окружение перед переопределением нужных переменных. Дыра в suidperl не появилась бы, если должным образом было бы очищено окружение.

По аналогии, защита машины в сети, во-первых, предпологает запрет всякого подключения. Затем, сисадмин активизирует необходимые или полезные сервисы. Тем же методом, при программировании Set-UID приложения окружение должно быть очищено, а затем заполнено нужными переменными.

Проверка формата параметра происходит при помощи сравнения ожидаемого значения с разрешенными форматами. Если сравнение успешно, параметр принимается. Иначе он отвергается. Если вы запускаете тест, используя список неправильных значений формата, возрастает риск передачи неправильного значения, что может привести к краху системы.

Мы должны понимать, не забывая о переменной PATH, что опасность использования system() сохраняется при использовании некоторых производных функций таких как popen() или системных вызовов, например, execlp() или execvp().

 

Косвенное выполнение команд

Для повышения практичности программ, удобно оставить пользователю возможность конфигурировать поведение программного обеспечения, например, используя макросы. Для управления переменными и общими шаблонами, так как это делает оболочка, предназначена мощная функция wordexp(). Вы должны быть осторожными используя ее, так как передача строки типа $(command) позволит выполнить указанную внешнюю команду. Передача строки "$(/bin/sh)" создает Set-UID оболочку. Чтобы избежать этого, wordexp() имеет атрибут WRDE_NOCMD, который запрещает интерпретацию последовательности $( ).

При запуске внешних команд, вам надо быть осторожным и не вызывать утилиты, которые предоставляют механизм выхода в оболочку (как последовательность :!command в vi). Сложно перечислить их все, некоторые приложения очевидны в этом смысле(текстовые редакторы, файл-менеджеры...), другие сложнее определить (что мы видели с /bin/mail) или имеют опасные режимы отладки.

 

Заключение

Эта статья поясняет различные аспекты:

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

 

Страница отзывов

У каждой заметки есть страница отзывов. На этой странице вы можете оставить свой комментарий или просмотреть комментарии других читателей.
 talkback page 

Webpages maintained by the LinuxFocus Editor team
© Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL
LinuxFocus.org

Click here to report a fault or send a comment to LinuxFocus
Translation information:
fr -> -- Frédéric Raynal, Christophe Blaess, Christophe Grenier
fr -> en Georges Tarbouriech
en -> ru Kolobynin Alexey

2001-08-20, generated by lfparser version 2.17