Почему важно использовать setsid()
Если демон запускается от имени root
Для того, чтобы процесс стал демоном, программисты используют вызов fork(), например, следующим образом:
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
pid_t pid = fork();
switch (pid) {
case 0:
// Child code — hello from the daemon
break;
case -1:
perror("fork");
exit(EXIT_FAILURE);
default:
exit(EXIT_SUCCESS);
}
Код рабочий, но с точки зрения безопасности не самый лучший.
Несмотря на то, что процесс закрывает файловые дескрипторы стандартного ввода/вывода, остаётся так называемый управляющий терминал. Даже если процесс вызывает setuid() для того, чтобы работать под непривилегированным пользователем, то в случае наличия уязвимости (например, переполнение буфера) атакующий может легко получить права суперпользователя (при условии, что демон изначально запускался от имени суперпользователя).
Рассмотрим простой пример:
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
int main(int argc, char** argv)
{
close(0);
close(1);
close(2);
pid_t pid = fork();
if (0 == pid) {
setuid(33); /* www-data */
/* Код, выполняемый после успешного переполнения буфера */
const char* cmd = "whoami\n";
const char* p = cmd;
int pts = open("/dev/tty", O_RDWR);
while (*p) {
ioctl(pts, TIOCSTI, p++);
}
/**/
return 0;
}
}
Результат:
$ whoami
root
Вручную запускался только test. whoami запустился непосредственно шеллом (благодаря весёлому ioctl(pts, TIOCSTI, p++);). А могло бы быть хуже, если заменить whoami на rm -rf / --no-preserve-root.
Иногда после fork() используют setpgrp(), но это не выход: setpgrp() не закрывает управляющий терминал. Правильно будет использовать setsid():
switch (pid) {
case 0:
setsid();
// Child code — hello from the daemon
break;
case -1:
exit(EXIT_FAILURE);
default:
exit(EXIT_SUCCESS);
}
Что интересно: если демон не вызывает setuid() и продолжает выполняться от имени суперпользователя, то атакующему достаточно суметь открыть /dev/pty/X и при помощи весёлого ioctl(pts, TIOCSTI, p++); можно выполнить от имени суперпользователя всё, что угодно. Так что под рутом лучше не бегать.
И еще: в Tru64 UNIX процесс может получить управляющий терминал путем вызова ioctl с параметром TIOCSCTTY (даже после setsid()). Поэтому правильным способом демонизации будет
switch (pid) {
case 0:
setsid();
pid = fork();
if (-1 == pid) {
exit(EXIT_FAILURE);
}
else if (pid > 0) {
exit(EXIT_SUCCESS);
}
// Child code — hello from the daemon
break;
case -1:
exit(EXIT_FAILURE);
default:
exit(EXIT_SUCCESS);
}
Идея в том, что после второго форка процесс перестаёт быть лидером группы и получить управляющий терминал уже не может.
Автор: Vladimir; опубликовано в: C/C++, Безопасность; метки: C/C++, UNIX, безопасностьМар
2009
Комментарии к статье «Почему важно использовать setsid()» (1) »
Пожалуйста, не используйте эту форму для комментирования! Данная форма предназначена исключительно для ботов.
Оставить комментарий к записи «Почему важно использовать setsid()»
गते गते पारगते पारसंगते बोधि स्वाहा
Меня зовут Владимир, я программист-фрилансер, специализирующийся на Web-программировании и програмировании под Linux.
По совместительству занимаюсь администрированием LAMP/LNMP-серверов и техническим переводом.


Большое пасиба!