Если демон запускается от имени 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);
}
Идея в том, что после второго форка процесс перестаёт быть лидером группы и получить управляющий терминал уже не может.
Большое пасиба!