Если демон запускается от имени root

Для того, чтобы процесс стал демоном, программисты используют вызов fork(), например, следующим образом:

[-]
View Code C
chdir("/");
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() для того, чтобы работать под непривилегированным пользователем, то в случае наличия уязвимости (например, переполнение буфера) атакующий может легко получить права суперпользователя (при условии, что демон изначально запускался от имени суперпользователя).

Рассмотрим простой пример:

[-]
View Code C
#include <stdio.h>
#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;
    }
}

Результат:

[-]
View Code Text
$ gcc -o test test.c && ./test
$ whoami
root

Вручную запускался только test. whoami запустился непосредственно шеллом (благодаря весёлому ioctl(pts, TIOCSTI, p++);). А могло бы быть хуже, если заменить whoami на rm -rf / --no-preserve-root.

Иногда после fork() используют setpgrp(), но это не выход: setpgrp() не закрывает управляющий терминал. Правильно будет использовать setsid():

[-]
View Code C
pid_t pid = fork();
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()). Поэтому правильным способом демонизации будет

[-]
View Code C
pid_t pid = fork();
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);
}

Идея в том, что после второго форка процесс перестаёт быть лидером группы и получить управляющий терминал уже не может.

Добавить в закладки

Связанные записи

8
Март
2009

Комментарии к статье «Почему важно использовать setsid()» (1)  »

  1. Юрец says:

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

Подписаться на RSS-ленту комментариев к статье «Почему важно использовать setsid()» Trackback URL: http://blog.sjinks.org.ua/security/511-why-setsid-is-important/trackback/

Оставить комментарий к записи «Почему важно использовать setsid()»

Вы можете использовать данные тэги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Оставляя комментарий, Вы выражаете своё согласие с Правилами комментирования.

Подписаться, не комментируя