Безопасное общение ребенка с родителем в python

Моя программа на Python требует повышенных привилегий и поэтому запускается с правами root (с setuid-binary-wrapper).

Чтобы свести поверхность атаки (и влияние ошибок кода) к минимуму, я решил разделить свой код на две части: одна часть должна выполняться как root, а другая — с разрешениями regular user. Проблема в том, что код взаимозависим и поэтому нуждается в secure two-way communication.

Я не знаю, правильный ли это подход (другие идеи приветствуются), но я решил пойти с two processes - один родительский процесс с повышенными привилегиями и один дочерний процесс с привилегиями обычного пользователя.

Идея:

  • Родительский процесс запускается пользователем root и сохраняет свои привилегии
  • Родительский процесс порождает дочерний процесс, который переходит к обычному пользователю (дочерний процесс не может восстановить привилегии root)
  • Дочерний процесс делает большую часть работы, но если ему нужно что-то выполнить с root-правами, он говорит родительскому процессу сделать это за него.

Вопросы:

  • Достаточно ли для этого subprocess(.Popen)? Может ли multiprocessing подойти лучше?

  • Как дочерний процесс и родительский процесс могут взаимодействовать интерактивным и безопасным способом (безопасен ли subprocess.PIPE)?

  • Знаете ли вы какие-нибудь простые примеры кода для этого сценария?

-------------------------------------------------------------------------

Основываясь на предложениях Гила Гамильтона, я придумал следующий код

Остались некоторые вопросы:

  • Это безопасно? Нужно ли мне удалять дополнительные элементы, такие как файловые дескрипторы, или достаточно os.setuid(<unprivileged UID>)?
  • В общем, если процесс отбрасывается для определенного пользователя таким образом, может ли этот пользователь вмешиваться в память отброшенных процессов?

privileged.py:

#!/bin/python
from multiprocessing import Process, Pipe
from unprivileged import Unprivileged

if __name__ == '__main__':
  privilegedProcessPipeEnd, unprivilegedProcessPipeEnd = Pipe()
  unprivilegedProcess = Process(target=Unprivileged(unprivilegedProcessPipeEnd).operate)
  unprivilegedProcess.start()

  print(privilegedProcessPipeEnd.recv())
  privilegedProcessPipeEnd.send("ok")
  print(privilegedProcessPipeEnd.recv())
  privilegedProcessPipeEnd.send("nok")

  privilegedProcessPipeEnd.close()
  unprivilegedProcessPipeEnd.close()
  unprivilegedProcess.join()

unprivileged.py:

import os

class Unprivileged:

  def __init__(self, unprivilegedProcessPipeEnd):
    self._unprivilegedProcessPipeEnd = unprivilegedProcessPipeEnd

  def operate(self):
    invokerUid = os.getuid()

    if invokerUid == 0:
      # started by root; TODO: drop to predefined standard user
      # os.setuid(standardUid)
      pass
    else:
      # started by a regular user through a setuid-binary 
      os.setuid(invokerUid) # TODO: drop to predefined standard user (save invokerUid for future stuff)

    # os.setuid(0) # not permitted anymore, cannot become root again

    print("os.getuid(): " + str(os.getuid()))

    self._unprivilegedProcessPipeEnd.send("invoke privilegedFunction1")
    print(self._unprivilegedProcessPipeEnd.recv())
    self._unprivilegedProcessPipeEnd.send("invoke privilegedFunction2")
    print(self._unprivilegedProcessPipeEnd.recv())

    return

main.c (программа setuid-обертки):

#include <unistd.h>
#define SCRIPT_PATH "/home/u1/project/src/privileged.py"

int
main(int argc,
     char **argv) {
  return execv(SCRIPT_PATH, argv);
}

/* compile and run like this:
$ gcc -std=c99 main.c -o main
# chown root:root main
# chmod 6771 main
$ chmod +x /home/u1/project/src/privileged.py
$ ./main
*/

person MCH    schedule 28.03.2016    source источник
comment
Еще одна вещь, которую я бы сделал просто для чистоты, это (после разветвления): закрыть unprivilegedProcessPipeEnd в родительском (привилегированном процессе) и закрыть privilegedProcessPipeEnd в дочернем, чтобы каждая сторона имела только свой собственный процесс. конец трубы открыт.   -  person Gil Hamilton    schedule 29.03.2016
comment
Единственными другими файловыми дескрипторами, которыми вы должны делиться, являются stdin/stdout/stderr, что не должно повредить чему-либо. Худшее, что может случиться, это то, что родитель и ребенок будут конкурировать за входные данные. Вы можете повторно открыть все три, используя /dev/null, если вам это не нравится.   -  person Gil Hamilton    schedule 29.03.2016
comment
да. Например, если вы используете setuid для установки идентификатора пользователя MCH, то пользователь MCH сможет использовать gdb или strace для непривилегированного потомка. Следовательно, они могут делать с памятью процесса практически все, что захотят. Вы всегда можете установить выделенный идентификатор пользователя-администратора (например, bin и daemon в стандартной установке Linux) и setuid для этого пользователя.   -  person Gil Hamilton    schedule 29.03.2016


Ответы (1)


Это можно сделать с помощью Popen, но это немного неуклюже, IMO, потому что у вас нет контроля над переходом процесса с помощью Popen. Если вы полагаетесь на UID для ослабления привилегий, вам нужно fork, а затем в дочернем элементе настроить свой UID, прежде чем вызывать другой дочерний код.

(Нет реальной причины, по которой вы не могли бы поместить свой дочерний код в отдельную программу, которую вы вызываете с помощью Popen, и настроить ее UID в качестве первого шага, просто мне кажется странным способ ее структурировать.)

Я бы порекомендовал вам взглянуть на использование модуля multiprocessing. Этот модуль позволяет легко создать новый процесс (он будет обрабатывать вилку для вас). Затем вы можете легко добавить код, который настраивает UID (см. ниже), а затем вы можете просто запустить дочерний код в той же «базе кода». То есть вам не обязательно нужно вызывать отдельную программу.

Модуль multiprocessing также предоставляет свой собственный объект Pipe, а также объект Queue, оба из которых являются механизмами межпроцессного взаимодействия. Оба безопасны — в том смысле, что ни один внешний пользователь не может подключиться к ним (без привилегий суперпользователя). Но, конечно, если ваш непривилегированный дочерний процесс скомпрометирован, он может отправить все, что захочет, родителю, поэтому вашему привилегированному родителю все равно нужно будет проверять / проверять его ввод.

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

Что касается настройки UID, это просто один вызов os.setuid в дочернем элементе перед вызовом кода, который вы хотите запустить как непривилегированный пользователь. Прочтите страницы руководства setuid(2) и credentials(7) для получения дополнительной информации.

person Gil Hamilton    schedule 28.03.2016
comment
Спасибо. Я сделаю, как вы предложили, обновлю вопрос полученным кодом и приму ваш ответ, когда он сработает :) - person MCH; 29.03.2016