LINUX.ORG.RU

Нюансы запуска процессов через system/exec

 ,


6

1

Случай #1:

Как известно, при вызове exec происходит завершение всех нитей вызывающего кроме той, что вызвала system. При этом открытые дескрипторы клонируются в новый процесс (если у них не указан флаг FD/SOCK_CLOEXEC).

Представим следующую гипотетическую ситуацию:

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

Положим, что так как эта строка может содержать переменные среды, какие-то выражения подстановки и shell-related команды и синтаксис программист, написавший эту программу посчитал, что самым оптимальным образом будет запускать эту строку с помощью команды system, которая не требует списка аргументов, как семейство exec*. Плюс, так как system «под капотом» вызывает sh -c ..., то автоматически подставятся переменные, а shell-related команды и синтаксис будут работать как нужно. Так вот, глянем исходник system, например, для glibc:

<Сначала хотел показать исходник glibc, но тут слишком дофига получается кода, поэтому сжато: clone + exec + waitpid, кстати ман говорит про fork + exec + waitpid, что не совсем то же самое>

При вызове clone/fork происходит копирование открытых дескрипторов, которые наследует процесс, который запустил system. Предположим, что кто-то передал для запуска строку, содержащую запуск программы (назовем ее evil), выполняющей следующие действия:

if (fork() != 0)
    exit(0);

Что превращает ее в отвязанного от родителя демона. Однако этот fork тоже наследует все открытые хэндлеры, что приводит нас к тому, что этот демонутый процесс evil получил копию всех открытых хендлеров супервизора, однако стал от него отвязан (system вернул 0, супервизор считает, что работа выполнена).

Теперь предположим, что это супервизор с управлением по сети. То есть супервизор открывает серверный сокет и слушает команды, которые ему приходят извне. Не будем обсуждать защищенность передачи или канала связи, всё можно сломать, суть сейчас не в этом. Суть в том, что на момент запуска system супервизор владел открытым серверным сокетом (!). Да, по совести и по правилам при его создании ему нужно было выставить SOCK_CLOEXEC, но скажем честно, кто своим серверным сокетам выставляет флаг, о котором даже в мануалах на socket из 153 строк отдано 2 (!), т.е. всего 1.3%?

Таким образом, пройдя через всю цепочку форков копия серверного сокета оказывается у демонутого evil:

super      9053 alex    3u  IPv4 11458560      0t0  TCP *:34002 (LISTEN)
super      9053 alex    4u  IPv4 11464960      0t0  TCP localhost:34002->localhost:41308 (ESTABLISHED)
evil       9257 alex    3u  IPv4 11458560      0t0  TCP *:34002 (LISTEN)

Положим, что процесс супервизора по какой-то причине завершился. Ну, например, выполнив свою работу или по ошибке. И теперь начинается самая мякотка. Ведь в системе уже открыт слушающий на порту 34002 сокет! И принадлежит он программе evil! Таким образом она может прикинуться сервером, а супервизор, в свою очередь, не сможет вернуть свой сокет, поэтому система не даст ему открыть второй сокет на том же порту без REUSEPORT:

SO_REUSEPORT (since Linux 3.9)
              Permits multiple AF_INET or AF_INET6 sockets to be bound to an identical socket address.  This option must be set on each socket (including the first socket) prior to calling bind(2) on the socket.  To prevent port hijacking, all of  the  pro‐
              cesses binding to the same address must have the same effective UID.  This option can be employed with both TCP and UDP sockets.

В общем случае, когда какая-то программа-сервер запускается и получает ALREADYINUSE то надо бы сказать мол, «не не могу работать, наверное другая копия меня уже запущена» и выключиться (ну или ожидать его освобождения).

Мы же не хотим port hijacking, верно? А вот все равно профукали. Ну и теперь evil может делать что угодно. Может получать данные от клиентов, может им даже что-то отвечать,может получать список команд для запуска и запускать вместо них что угодно, сообщая наверх что всё хорошо.

Собственно, вопрос #1: почему на серверный сокет автоматически при создании не вешается FD_CLOEXEC?

Случай #2:

Мы - супервизор, запускающий программы, забудем про сетевое взаимодействие, мы сами знаем, что нам запускать. Запускают нас от root, для того, чтобы мы могли делать какую-то очень важную вещь, ну, например, читать какой-то root-owned файл:

-rw-------  1 root root        7 июн  5 23:51 rootonly

Изобразим супервизор упрощенно:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>       
#include <sys/wait.h>
#include <errno.h>       
#include <pwd.h>


static void dowork(void) {
    int pid = fork();
    int status;
    switch (pid) {
        case -1 : exit(EXIT_FAILURE);
        case 0  :
            if (setuid(1000) == -1) {
                printf("u:%u\n", errno);
            }
            if (seteuid(1000) == -1) {
                printf("e:%u\n", errno);
            }
            execl("./hijack_evil.elf", "./hijack_evil.elf", NULL);
            printf("cannot run\n");
            exit(EXIT_FAILURE);
        default :
            wait(&status);
    }
}

int main(void) {
    FILE * fileroot = fopen("/tmp/rootonly", "r");
    printf("I am %s, #%u/%u\n", getpwuid(getuid())->pw_name, geteuid(), getuid());
    
    dowork();
    
    fclose(fileroot);
    return 0;
}
gcc hijack_root.c -o hijack_root.elf

И программу, которой нельзя давать рутовые права:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>       
#include <sys/wait.h>
#include <pwd.h>

int main(void) {
    printf("I am %s, #%u/%u\n", getpwuid(getuid())->pw_name, geteuid(), getuid());
    // my stdin is 0, stdout is 1, stderr is 2, therefore...
    char buf[1024];
    read(3, buf, 1024);
    printf("%s\n", buf);
    return 0;
}
gcc hijack_evil.c -o hijack_evil.elf

Проверяем, что просто так файл прочитать нельзя:

alex@ThinkPad-L560:~$ cat /tmp/rootonly 
cat: /tmp/rootonly: Отказано в доступе
alex@ThinkPad-L560:~$ echo $USER/$UID
alex/1000

И запускаем супервизор:

$ sudo ./hijack_root.elf 
I am root, #0/0
I am alex, #1000/1000
SECRET

При этом мы не можем ставить FD_CLOEXEC на каждый хендлер, открываемый в супервизоре, потому что он может понадобиться для целей супервизора в его форках. Соответственно вопрос #2: почему внешние программы, запускаемые через exec наследуют хендлеры? Ведь очевидно, что это может быть использовано только для «угона» файлов

★★★★★

Собственно, вопрос #1: почему на серверный сокет автоматически при создании не вешается FD_CLOEXEC?

The O_CLOEXEC, O_DIRECTORY, and O_NOFOLLOW flags are not specified in POSIX.1-2001,

Т.е. ответ скорее всего: обратная совместимость.

Соответственно вопрос #2: почему внешние программы, запускаемые через exec наследуют хендлеры? Ведь очевидно, что это может быть использовано только для «угона» файлов

А разве нет программ, которые принимают номер файлового дескриптора среди своих аргументов?

xaizek ★★★★★
()

а не запускай с рутовыми правами всякую фигню.

с такими правами ты можешь снести всю систему. вот ужас-то!

Iron_Bug ★★★★★
()
Последнее исправление: Iron_Bug (всего исправлений: 1)

Всё правильно понял

  1. Так исторически сложилось.
  2. Так исторически сложилось. Это используется например, когда ты в баше запускаешь программы, они наследуют стандартный ввод/вывод.
  3. Когда запускаешь чужие программы, закрывай, сцуко, не нужное им файло. В бздях для этого есть closefrom, в линуксе /proc/self/fd
anonymous
()
Ответ на: Всё правильно понял от anonymous

Окей, стандартный ввод/вывод, пусть наследуются первые три fd (0,1,2), но остальные то зачем, особенно с учетом того, что запускаемая программа не может иметь гарантию того, что нужный ей хендлер станет в нужную позицию.

PPP328 ★★★★★
() автор топика
Ответ на: Всё правильно понял от anonymous

В бздях для этого есть closefrom, в линуксе /proc/self/fd

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

anonymous
()
Ответ на: комментарий от xaizek

А разве нет программ, которые принимают номер файлового дескриптора среди своих аргументов?

А можете привести пример программы (в смысле готовой), которая использует на старте хендлеры с индексом выше 2, зная что кто-то для нее их открыл, а она их унаследовала?

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от Iron_Bug

а не запускай с рутовыми правами всякую фигню.

А собственно в примере «фигня» запускается с правами пользователя (set[ug]id), но все равно получает доступ к рутовому файлу.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Окей, стандартный ввод/вывод, пусть наследуются первые три fd (0,1,2), но остальные то зачем

Сказал же, исторически сложилось. О тех проблемах, которые тебе сейчас кажутся очевидными, в прошлом тысячелетии тупо не задумывались. И что значит «пусть наследуются первые три»? А если я туда сокеты какие-нибудь повесил?

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

Схерали? Дескриптор при exec не меняется.

anonymous
()
Ответ на: комментарий от pon4ik

Security flaw out of the box. И в первом и во втором случае. В первом создаётся клон серверного сокета (логичнее было бы помечать хендлеры, которые надо экспортировать, а не наоборот), во втором - материнская программа может не мочь закрыть все свои хендлеры на момент запуск дитятки с правами пользователя, так что дитятка получит рута даже с выставленным setuid.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Дескриптор при exec не меняется.

Не меняется. Но материнская может сначала открыть первый файл, потом второй и в дочернюю прилетит обратный порядок.

А если я… То они заместят stdinouterr, что и ожидается.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Не меняется. Но материнская может сначала открыть первый файл, потом второй и в дочернюю прилетит обратный порядок.

Какой обратный порядок? В дочернюю прилетят те же файлы с теми же номерами.

anonymous
()
Ответ на: комментарий от anonymous

Вы про

-add-fd fd=fd,set=set[,opaque=opaque]
Add a file descriptor to an fd set. Valid options are:

fd=fd
This option defines the file descriptor of which a duplicate is added to fd set. The file descriptor cannot be stdin, stdout, or stderr.

set=set
This option defines the ID of the fd set to add the file descriptor to.

opaque=opaque
This option defines a free-form string that can be used to describe fd.

? Выглядит как какой-то лютый костыль, который нарушает связность программ

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Подржим ваша программа ожидает fd3 сервер сокет, а fd4 log file. Тогда ваша программа сможет получить это только тогда, когда материнская не будет использовать ни одной сторонней библиотеки, не будет отрывать ни одного файла, а создаст сервер сокет, а потом лог файл, и не дай боже в обратном порядке. Потому что иначе вы «унаследуете» fd3-56 - наспамила libxml при работе с файлом, fd57-71 клиенты сокета, fd72- нужный вам сервер сокет, fd79-501 - дескрипторы файлов качаемого торрента, а fd509 - нужный вам лог файл. И пометить большую часть этого мусора как cloexec вы не можете, потому что они открываются в неподконтрольном вам коде (библиотеки)

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Выглядит как какой-то лютый костыль,

Я и говорю, что жесть.

который нарушает связность программ

Нарушает связность? Ты бы всё в один процесс запихнул что-ли?

anonymous
()
Ответ на: комментарий от anonymous

Нет, как минимум разобрался бы заким хреном сторонней программе для запуска нужен дескриптор открытого файла созданный другой программой. Либо это надо слить в одну программу, либо разделить так, чтобы они общались без такой задницы, например через нормальные unix sockets

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Либо это надо слить в одну программу, либо разделить так, чтобы они общались без такой задницы, например через нормальные unix sockets

Я в qemu вообще не копенгаген, поэтому чисто теоретически

слить в одну программу Имея несколько программ и соответственно несколько процессов, можно разным процессам раздавать разные права.

они общались без такой задницы, например через нормальные unix sockets Возможно им не нужно общаться. Родитель передаёт файл открытый от рута или сокет куда-то.

Но выглядит странно, да. Больше я такого вроде не видел.

Дело такое, если дать возможность, её кто-то использует. Если её потом отнять, что-то сломается. Поэтому «исторически сложилось».

anonymous
()
Ответ на: комментарий от PPP328
main() {
   // понаоткрывали случайное количество файлов
   while (randomInt(min=0, max=100) != 0) {
     open("/foobar/" + i + ".txt")
   }

   int fd = open("/etc/hostname") // значение fd случайно

   dup2(fd, 42) // копируем fd на номер 42
   // теперь и значение fd и 42 указывают на /etc/hostname

   exec("/child")
   // у дочки по номеру 42 гарантировано /etc/hostname
   

}
anonymous
()
Ответ на: комментарий от PPP328

Алсо, с закрытием всех лишних файлов

main() {
   // понаоткрывали случайное количество файлов
   while (randomInt(min=0, max=100) != 0) {
     open("/foobar/" + i + ".txt")
   }

   int fd = open("/etc/hostname") // значение fd случайно

   dup2(fd, 3) // копируем fd на номер 3
   // теперь и значение fd и 3 указывают на /etc/hostname

#if linux
   for (char* file_name in list_files("/proc/self/fd")) {
     int fd_to_be_closed = atoi(file_name)
     if (fd_to_be_closed > 3) {
       close(fd_to_be_closed)
     }
   }
#else
   closefrom(4)
#endif

   exec("/child")
   // у дочки по номеру 3 гарантировано /etc/hostname
   

}

anonymous
()
Ответ на: Всё правильно понял от anonymous

Так исторически сложилось.

Нет. Это часть интерфейса системы, сделанная специально. Вы ещё предложите 2 вида pipe(), один для записи потомком, другой для чтения, а двунаправленный обмен через сокеты или сокетные пары вообще делать через задницу после exec. Вам предложили ручки? Вы можете сделать close после fork? FD_CLOEXEC для fcntl в POSIX черте сколько. Вы обработали файл? Ну так закройте. Дискриптор ещё нужен родителю, который запускает потомков, которым это не надо? А может надо специально? Так у вас есть все ручки! Не умеете программировать, так и не беритесь, не надо выдумывать глупости о легаси.

vodz ★★★★★
()
Ответ на: комментарий от vodz

фига маняоправдания

очевидный нормальный способ бы наоборот работать как с O_CLOEXEC по-умолчанию, а когда нужно специально выставлять O_NO_CLOEXEC

но щас у тебя будет ещё пук про хипсеров или что-то в этом роде

anonymous
()
Ответ на: комментарий от anonymous

очевидный нормальный способ

exec для солидных программ самый тяжелый и редкий и вообще самый ненужный сискол. А для shell-ов - самый распространненый. Отсюда налицо ваш типичный нигилизм «диды идиоты».

vodz ★★★★★
()
Последнее исправление: vodz (всего исправлений: 1)
Ответ на: комментарий от PPP328

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

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

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

Это не что-то выдуманное, это вполне реальная жизненная ситуация.

anarquista ★★★★★
()
Ответ на: комментарий от anarquista

Да, сейчас иметь CLOEXEC по умолчанию было бы логичным, это не возможно, потому что стандарт другой.

В разрабатываемой вами системе можете написать обёртки, которые будут подставлять CLOEXEC в вызовы open и socket.

anarquista ★★★★★
()
Ответ на: комментарий от vodz

Что мешало сделать один глобальный флаг типа sys_setcloexec(1/0)? Тогда не было бы дыр в безопасности для программ, которые кого-то запускают и вы смогли бы делать свой любимый шелл. Основная проблема в том, что даже написав свою обертку для open/socket вы все равно не перекроете использование этих сисколов в сторонних библиотеках.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от anonymous

И таким образом вы закрыли все дескрипторы своего процесса. Поздравляю. Вы только что закрыли защищенный мьютексом дескриптор сокета из потока #8 в обход самого мьютекса. А потом он задетектит что он уже мертвый и вызовет по своему workflow close. А закрывать дважды один номер дескриптора это такой тяжкий грех, что у вас может умереть вообще что угодно.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от anonymous
 The  dup2() system call performs the same task as dup(), but instead of
       using the lowest-numbered unused file  descriptor,  it  uses  the  file
       descriptor number specified in newfd.  If the file descriptor newfd was
       previously open, it is silently closed before being reused.

Вот та же проблема, что на одно моё сообщение выше. Вы НЕ знаете, кто у вас предварительно открыт под #42. Нет вообще никакого способа гарантировать, что этот дескриптор в данный момент не относится к кому-то важному. Вы просто тупо тихо его закрыли. А где-то в другой нити из-за этого поломался воркфлоу, закрылся файл лога\серверный сокет\клиентский сокет\пайп\еще какая хрень, всё есть файл\тысячи их.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от Iron_Bug

Это единственный способ запустить программу с правами пользователя если тебя самого запустили под рутом (что почти всегда используется для супервизоров).

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Тогда не было бы дыр в безопасности для программ

Нет дыр, а есть ручки, которые позволяют легитимно работать в системе с защитой: bind на порты < 1024 с вызовом потомка со сбросом suid без всяких нестрандартных capabilities, открытие конфигов на чтение, доступных опять же не для nobody, предоставления fd без права создания потомкам файлов в некоторых путях fs и т д.

vodz ★★★★★
()
Ответ на: комментарий от vodz

При этом эти сомнительные удобства того, что не надо вызывать setsockopt(.. O_NOCLOEXEC…) вызывают проблему того, что экспортируется ВООБЩЕ ВСЁ. Я выше написал, почему нельзя перед запуском чего-то или кого-то закрыть все ненужные дескрипторы. Потому что нарушается воркфлоу в других нитях и у нас многопоток.

Это как пускать в интернет по черным спискам или по белым с целью недопустить на «недетские» сайты. С CLOEXEC-по умолчанию вы не пускаете никуда, кроме адресов, которые вы 100% проверили. БЕЗ CLOEXEC по умолчанию вы пускаете везде, кроме тех 18+ сайтов, которые вы пометили. Но в сети в этот момент родилось еще миллион сайтов, и еще миллион до адреса которых у вас нет доступа. Так что это хуже для цели «не пускать ребенка на xxx».

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Это не дыры, а «ручки».

Именно. Если ручки предназначены для возможности дырявить систему защиты но легитимным способом, то и обращаться надо с ними соответственно. Как с ножом. Вы против ножей?

vodz ★★★★★
()
Ответ на: комментарий от vodz

а ты что, против легализации короткоствола

anonymous
()

При этом мы не можем ставить FD_CLOEXEC на каждый хендлер

Что мешает?

Ведь очевидно, что это может быть использовано только для «угона» файлов

Подумай на досуге, как процессы, запущенные из bash получают возможность писать/читать в тот же терминал, в котором работает shell.

И раз уж на то пошло, CLOEXEC был с нами не всегда. Никто не мешает в fork-нутом процессе закрыть все файловые дескрипторы от 0 до RLIMIT_NOFILE (кстати, так и делают обычно, ведь никто не знает, чего там, например, syslog понаоткрывает), после чего выполнить exec. Однако в этом случае возникает неловкая ситуация: когда exec возвращается с ошибкой, нужно куда-то доложить об этом. В большинстве случаев «куда-то» - это в лог (или на stderr). Если эти дескрипторы были закрыты - то ой-ой-ой, придется выкручиваться каким-то другим IPC.

kawaii_neko ★★★★
()

ITT «икзперд па бизапаснасти», не может понять, что в подавляющем большинстве случаев нужно, чтобы stdin/stdout/stderr «наследовались» дочерним процессом.

А супервизоры следует писать, задействуя кору головного мозга и отсутсвие CLOEXEC на файловых дескрипторах по умолчанию этому не помеха (можно ли закрыть все файловые дексрипторы, имея на руках syscall close?).

Также рекомендую ознакомиться с man accept4, man socket и man open, дабы убедиться, что при использовании мозга даже лишние syscall-ы на выставление CLOEXEC не нужны.

kawaii_neko ★★★★
()
Ответ на: комментарий от PPP328
sh -c "echo aaa >&4" 4> f4

По такому принципу работают многие другие скриптовые языки, а также, внимание, передача пароля через файловый дескриптр, например gpg --passphrase-fd n - что есть гораздо более здравая практика, чем передаче через переменные окружения или аргументом командной строки.

legolegs ★★★★★
()
Ответ на: комментарий от PPP328

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

Мне пофиг, потому что у меня этот способ есть. У меня одна функция main() и я не плодил в ней тредов. Когда я вызову exec всё «важное» помрёт само по себе.

А ты можешь форкнуться, оставив себе в форке только один тред.

anonymous
()
Ответ на: комментарий от kawaii_neko

При этом мы не можем ставить FD_CLOEXEC на каждый хендлер Что мешает?

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

anonymous
()
Ответ на: комментарий от vodz

Так у вас есть все ручки!

Какие нах ручки? Люди придумали компьютеры, чтобы не делать ручками. Но тут пришёл vodz и забомбил, что его дидов кто-то посмел критиковать.

anonymous
()
Ответ на: комментарий от kawaii_neko

(можно ли закрыть все файловые дексрипторы, имея на руках syscall close?).

Также относится и к ответу @anonymous - НЕЛЬЗЯ закрывать все fd перед запуском exec. Потому что согласно манам, в которые вы шлете перед выполнением exec система завершит все другие нити, а вызываемая заместится вызываемой программой. Так вот если вы закроете все fd по порядку, то:

  1. В этот момент поломаются нити которые в этот момент с ними работали - хорошо, если не по сегфолту.
  2. В момент завершения нитей системой будут вызваны все функции, помеченные как attribute((destructor)). Что? У вас там закрытие каких-то fd? Ну всё, вы сделали double-close, уже UB, которое ломает работу с другими fd.

Всё еще считаете, что можете просто так позакрывать в одном треде fd, с которыми работают все остальные?

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от anonymous

У меня одна функция main() и я не плодил в ней тредов.

Да вы великий программист, я так посмотрю, раз у вас вся программа в main и только в один поток.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Потому что согласно манам, в которые вы шлете перед выполнением exec система завершит все другие нити, а вызываемая заместится вызываемой программой

У меня для тебя хорошие новости: fork клонирует только один поток, который его позвал. Так что нет, ничего не поломается.

kawaii_neko ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.