Удалить корневой UID, сохранив CAP_SYS_NICE

Я пытаюсь написать демон, который запустится как root, используя бит setuid, но затем быстро вернется к пользователю, запускающему процесс. Демон, однако, должен сохранить возможность устанавливать для новых потоков приоритет «реального времени». Код, который я использую для установки приоритета, выглядит следующим образом (запускается в потоке после его создания):

struct sched_param sched_param;
memset(&sched_param, 0, sizeof(sched_param));
sched_param.sched_priority = 90;

if(-1 == sched_setscheduler(0, SCHED_FIFO, &sched_param)) {
  // If we get here, we have an error, for example "Operation not permitted"
}

Однако часть, с которой у меня возникают проблемы, - это установка uid, сохраняя при этом возможность сделать вышеуказанный вызов sched_setscheduler.

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

if (getgid() != getegid() || getuid() != geteuid()) {
  cap_value_t cap_values[] = {CAP_SYS_NICE};
  cap_t caps;
  caps = cap_get_proc();
  cap_set_flag(caps, CAP_PERMITTED, 1, cap_values, CAP_SET);
  cap_set_proc(caps);
  prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0);
  cap_free(caps);
  setegid(getgid());
  seteuid(getuid());
}

Проблема в том, что после запуска этого кода я получаю «Операция не разрешена» при вызове sched_setscheduler, как указано в комментарии выше. Что я делаю не так?


person brooks94    schedule 01.11.2012    source источник
comment
Вместо seteuid(geteuid()); используйте явный seteuid(0); и используйте seteuid() во всем коде, кроме самого первого вызова setuid(0);.   -  person    schedule 01.11.2012
comment
@ H2CO3 H2CO3 есть несколько вещей, которые я не понимаю в вашем комментарии, во-первых, я не использую код seteuid(geteuid()) (в вашем есть лишняя буква «е»). Во-вторых, зачем мне вызывать setuid(0), я пытаюсь убрать корневое обозначение. Возможно, вы могли бы опубликовать ответ, чтобы конкретизировать свое предложение.   -  person brooks94    schedule 01.11.2012
comment
да, это была опечатка, извините. Итак, я имею в виду, что после удаления root вы можете повторно получить root на setuid(0);, чтобы не получать ошибки, связанные с недостаточным уровнем привилегий... Вы не это пытаетесь исправить? Или я что-то упускаю?   -  person    schedule 01.11.2012


Ответы (2)


Отредактировано для описания причины исходного сбоя:

В Linux есть три набора возможностей: наследуемые, разрешенные и эффективные. Наследуемый определяет, какие возможности остаются разрешенными в exec(). Разрешено определяет, какие возможности разрешены для процесса. Эффективный определяет, какие возможности действуют в данный момент.

При смене владельца или группы процесса с root на non-root действующий набор полномочий всегда сбрасывается.

По умолчанию разрешенный набор возможностей также очищается, но вызов prctl(PR_SET_KEEPCAPS, 1L) перед изменением идентификатора сообщает ядру, что разрешенный набор должен оставаться нетронутым.

После того, как процесс изменил идентификатор обратно на непривилегированного пользователя, CAP_SYS_NICE должен быть добавлен к действующему набору. (Он также должен быть установлен в разрешенном наборе, поэтому, если вы очистите свой набор возможностей, не забудьте также установить его. Если вы просто измените текущий набор возможностей, то вы знаете, что он уже установлен, потому что вы его унаследовали.)

Вот процедура, которую я рекомендую вам выполнить:

  1. Сохраните реальный идентификатор пользователя, реальный идентификатор группы и дополнительные идентификаторы групп:

     #define  _GNU_SOURCE
     #define  _BSD_SOURCE
     #include <unistd.h>
     #include <sys/types.h>
     #include <sys/capability.h>
     #include <sys/prctl.h>
     #include <grp.h>
    
     uid_t   user = getuid();
     gid_t   group = getgid();
     gid_t  *gid;
     int     gids, n;
    
     gids = getgroups(0, NULL);
     if (gids < 0) /* error */
    
     gid = malloc((gids + 1) * sizeof *gid);
     if (!gid) /* error */
    
     gids = getgroups(gids, gid);
     if (gids < 0) /* error */
    
  2. Отфильтруйте ненужные и привилегированные дополнительные группы (будьте параноиками!)

     n = 0;
     while (n < gids)
         if (gid[n] == 0 || gid[n] == group)
             gid[n] = gid[--gids];
         else
             n++;
    

    Поскольку вы не можете «очистить» идентификаторы дополнительных групп (которые просто запрашивают текущий номер), убедитесь, что список никогда не пуст. Вы всегда можете добавить реальный идентификатор группы в дополнительный список, чтобы он не был пустым.

     if (gids < 1) {
         gid[0] = group;
         gids = 1;
     }
    
  3. Переключите реальные и эффективные идентификаторы пользователей на root

     if (setresuid(0, 0, 0)) /* error */
    
  4. Установите возможность CAP_SYS_NICE в наборе CAP_PERMITTED. Я предпочитаю очистить весь набор и оставить только четыре возможности, необходимые для работы этого подхода (а позже удалить все, кроме CAP_SYS_NICE):

     cap_value_t capability[4] = { CAP_SYS_NICE, CAP_SETUID, CAP_SETGID, CAP_SETPCAP };
     cap_t       capabilities;
    
     capabilities = cap_get_proc();
     if (cap_clear(capabilities)) /* error */
     if (cap_set_flag(capabilities, CAP_EFFECTIVE, 4, capability, CAP_SET)) /* error */
     if (cap_set_flag(capabilities, CAP_PERMITTED, 4, capability, CAP_SET)) /* error */
     if (cap_set_proc(capabilities)) /* error */
    
  5. Сообщите ядру, что вы хотите сохранить возможности перехода от root к непривилегированному пользователю; по умолчанию возможности сбрасываются до нуля при переходе от root к не-root удостоверению

     if (prctl(PR_SET_KEEPCAPS, 1L)) /* error */
    
  6. Установите реальный, эффективный и сохраненный идентификатор группы на первоначально сохраненный идентификатор группы.

     if (setresgid(group, group, group)) /* error */
    
  7. Установить дополнительные идентификаторы групп

     if (setgroups(gids, gid)) /* error */
    
  8. Установите реальный, эффективный и сохраненный идентификатор пользователя на первоначально сохраненный идентификатор пользователя.

     if (setresuid(user, user, user)) /* error */
    

    На этом этапе вы фактически отказываетесь от привилегий root (без возможности получить их обратно), за исключением возможности CAP_SYS_NICE. Из-за перехода от пользователя root к пользователю без полномочий root эта возможность никогда не действует; ядро всегда будет очищать действующий набор возможностей при таком переходе.

  9. Установите возможность CAP_SYS_NICE в наборе CAP_PERMITTED и CAP_EFFECTIVE

     if (cap_clear(capabilities)) /* error */
     if (cap_set_flag(capabilities, CAP_PERMITTED, 1, capability, CAP_SET))  /* error */
     if (cap_set_flag(capabilities, CAP_EFFECTIVE, 1, capability, CAP_SET))  /* error */
     if (cap_set_flag(capabilities, CAP_PERMITTED, 3, capability + 1, CAP_CLEAR))  /* error */
     if (cap_set_flag(capabilities, CAP_EFFECTIVE, 3, capability + 1, CAP_CLEAR))  /* error */
    
     if (cap_set_proc(capabilities)) /* error */
    

    Обратите внимание, что последние две операции cap_set_flag() очищают три возможности, которые больше не нужны, так что остается только первая, CAP_SYS_NICE.

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

     if (cap_free(capabilities)) /* error */
    
  10. Скажите ядру, что вы не хотите сохранять возможности для любых дальнейших изменений от root (опять же, просто паранойя)

     if (prctl(PR_SET_KEEPCAPS, 0L)) /* error */
    

Это работает на x86-64 с использованием ядра GCC-4.6.3, libc6-2.15.0ubuntu10.3 и linux-3.5.0-18 на Xubuntu 12.04.1 LTS после установки пакета libcap-dev.

Отредактировано, чтобы добавить:

Вы можете упростить процесс, полагаясь только на то, что эффективным идентификатором пользователя является root, поскольку исполняемый файл имеет setuid root. В этом случае вам также не нужно беспокоиться о дополнительных группах, поскольку setuid root влияет только на эффективный идентификатор пользователя и ни на что другое. Возвращаясь к исходному реальному пользователю, технически вам нужен только один вызов setresuid() в конце процедуры (и setresgid(), если исполняемый файл также помечен как setgid root), чтобы установить как сохраненные, так и действующие идентификаторы пользователя (и группы). реальному пользователю.

Однако случай, когда вы восстанавливаете личность исходного пользователя, встречается редко, а случай, когда вы получаете личность именованного пользователя, встречается часто, и эта процедура здесь изначально была разработана для последнего. Вы должны использовать initgroups(), чтобы получить правильные дополнительные группы для указанного пользователя и так далее. В этом случае важно заботиться о реальных, эффективных и сохраненных идентификаторах пользователей и групп, а также о дополнительных идентификаторах групп, поскольку в противном случае процесс унаследовал бы дополнительные группы от пользователя, выполнившего процесс.

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


Отредактировано 17 марта 2013 г., чтобы показать простую тестовую программу. Это предполагает, что установлен setuid root, но при этом будут удалены все привилегии и возможности (кроме CAP_SYS_NICE, который требуется для манипуляций с планировщиком сверх обычных правил). Я сократил «лишние» операции, которые предпочитаю выполнять, в надежде, что другим будет легче читать.

#define  _GNU_SOURCE
#define  _BSD_SOURCE
#include <unistd.h>
#include <sys/types.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <grp.h>
#include <errno.h>

#include <string.h>
#include <sched.h>
#include <stdio.h>


void test_priority(const char *const name, const int policy)
{
    const pid_t         me = getpid();
    struct sched_param  param;

    param.sched_priority = sched_get_priority_max(policy);
    printf("sched_get_priority_max(%s) = %d\n", name, param.sched_priority);
    if (sched_setscheduler(me, policy, &param) == -1)
        printf("sched_setscheduler(getpid(), %s, { %d }): %s.\n", name, param.sched_priority, strerror(errno));
    else
        printf("sched_setscheduler(getpid(), %s, { %d }): Ok.\n", name, param.sched_priority);

    param.sched_priority = sched_get_priority_min(policy);
    printf("sched_get_priority_min(%s) = %d\n", name, param.sched_priority);
    if (sched_setscheduler(me, policy, &param) == -1)
        printf("sched_setscheduler(getpid(), %s, { %d }): %s.\n", name, param.sched_priority, strerror(errno));
    else
        printf("sched_setscheduler(getpid(), %s, { %d }): Ok.\n", name, param.sched_priority);

}


int main(void)
{
    uid_t       user;
    cap_value_t root_caps[2] = { CAP_SYS_NICE, CAP_SETUID };
    cap_value_t user_caps[1] = { CAP_SYS_NICE };
    cap_t       capabilities;

    /* Get real user ID. */
    user = getuid();

    /* Get full root privileges. Normally being effectively root
     * (see man 7 credentials, User and Group Identifiers, for explanation
     *  for effective versus real identity) is enough, but some security
     * modules restrict actions by processes that are only effectively root.
     * To make sure we don't hit those problems, we switch to root fully. */
    if (setresuid(0, 0, 0)) {
        fprintf(stderr, "Cannot switch to root: %s.\n", strerror(errno));
        return 1;
    }

    /* Create an empty set of capabilities. */
    capabilities = cap_init();

    /* Capabilities have three subsets:
     *      INHERITABLE:    Capabilities permitted after an execv()
     *      EFFECTIVE:      Currently effective capabilities
     *      PERMITTED:      Limiting set for the two above.
     * See man 7 capabilities for details, Thread Capability Sets.
     *
     * We need the following capabilities:
     *      CAP_SYS_NICE    For nice(2), setpriority(2),
     *                      sched_setscheduler(2), sched_setparam(2),
     *                      sched_setaffinity(2), etc.
     *      CAP_SETUID      For setuid(), setresuid()
     * in the last two subsets. We do not need to retain any capabilities
     * over an exec().
    */
    if (cap_set_flag(capabilities, CAP_PERMITTED, sizeof root_caps / sizeof root_caps[0], root_caps, CAP_SET) ||
        cap_set_flag(capabilities, CAP_EFFECTIVE, sizeof root_caps / sizeof root_caps[0], root_caps, CAP_SET)) {
        fprintf(stderr, "Cannot manipulate capability data structure as root: %s.\n", strerror(errno));
        return 1;
    }

    /* Above, we just manipulated the data structure describing the flags,
     * not the capabilities themselves. So, set those capabilities now. */
    if (cap_set_proc(capabilities)) {
        fprintf(stderr, "Cannot set capabilities as root: %s.\n", strerror(errno));
        return 1;
    }

    /* We wish to retain the capabilities across the identity change,
     * so we need to tell the kernel. */
    if (prctl(PR_SET_KEEPCAPS, 1L)) {
        fprintf(stderr, "Cannot keep capabilities after dropping privileges: %s.\n", strerror(errno));
        return 1;
    }

    /* Drop extra privileges (aside from capabilities) by switching
     * to the original real user. */
    if (setresuid(user, user, user)) {
        fprintf(stderr, "Cannot drop root privileges: %s.\n", strerror(errno));
        return 1;
    }

    /* We can still switch to a different user due to having the CAP_SETUID
     * capability. Let's clear the capability set, except for the CAP_SYS_NICE
     * in the permitted and effective sets. */
    if (cap_clear(capabilities)) {
        fprintf(stderr, "Cannot clear capability data structure: %s.\n", strerror(errno));
        return 1;
    }
    if (cap_set_flag(capabilities, CAP_PERMITTED, sizeof user_caps / sizeof user_caps[0], user_caps, CAP_SET) ||
        cap_set_flag(capabilities, CAP_EFFECTIVE, sizeof user_caps / sizeof user_caps[0], user_caps, CAP_SET)) {
        fprintf(stderr, "Cannot manipulate capability data structure as user: %s.\n", strerror(errno));
        return 1;
    }

    /* Apply modified capabilities. */
    if (cap_set_proc(capabilities)) {
        fprintf(stderr, "Cannot set capabilities as user: %s.\n", strerror(errno));
        return 1;
    }

    /*
     * Now we have just the normal user privileges,
     * plus user_caps.
    */

    test_priority("SCHED_OTHER", SCHED_OTHER);
    test_priority("SCHED_BATCH", SCHED_BATCH);
    test_priority("SCHED_IDLE", SCHED_IDLE);
    test_priority("SCHED_FIFO", SCHED_FIFO);
    test_priority("SCHED_RR", SCHED_RR);

    return 0;
}

Обратите внимание: если вы знаете, что двоичный файл запускается только на относительно новых ядрах Linux, вы можете положиться на возможности файла. Тогда ваш main() не нуждается ни в каких манипуляциях с идентификацией или возможностями — вы можете удалить все в main(), кроме функций test_priority(), — и вы просто даете своему двоичному файлу, скажем, ./testprio, приоритет CAP_SYS_NICE:

sudo setcap 'cap_sys_nice=pe' ./testprio

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

getcap ./testprio

который должен отображать

./testprio = cap_sys_nice+ep

Файловые возможности пока мало используются. В моей собственной системе только gnome-keyring-daemon имеет файловые возможности (CAP_IPC_LOCK для блокировки памяти).

person Nominal Animal    schedule 01.11.2012
comment
Спасибо, я кажется понял. Зачем нужен шаг 3? Предположительно исполняемый файл имеет setuid и принадлежит пользователю root? Кроме того, я думаю, у вас есть CAP_SET_NICE в паре мест, где вы имеете в виду CAP_SYS_NICE? - person brooks94; 02.11.2012
comment
@brooks94, спасибо; исправлено. Технически достаточно фактического идентификатора пользователя root, но полное переключение на пользователя root должно уменьшить вероятность успеха любых махинаций (на основе сигналов или, скажем, /proc/PID/ доступа), в то время как процесс имеет повышенные привилегии. Так что считайте это чисто защитной и более чем параноидальной деталью. - person Nominal Animal; 02.11.2012
comment
Также отсутствует вызов cap_set_proc(capabilities) на шаге 4? - person brooks94; 02.11.2012
comment
Кроме того, пару других ошибок, которые я обнаружил во время его фактической реализации: порядок аргументов getgroups() на шаге 1 перевернут; и я считаю, что на шаге 9 требуется вызов cap_set_flag(capabilities, CAP_PERMITTED, 1, возможность, CAP_SET), иначе я получаю операцию, не разрешенную при вызове cap_set_proc() - person brooks94; 02.11.2012
comment
@brooks94: Спасибо, что указали на это. Ошибки копирования-вставки. Я сравнил свою тестовую программу с приведенной выше, и теперь они кажутся одинаковыми. Моя тестовая программа тестирует все пять политик, устанавливая как минимальный, так и максимальный приоритеты для текущего потока (только для начального потока в процессе). В моей системе только SCHED_FIFO и SCHED_RR имеют диапазон приоритета, оба от 1 до 99 включительно. - person Nominal Animal; 03.11.2012
comment
Вызовы cap_set_flag(..., CAP_CLEAR) кажутся избыточными после cap_clear в соответствии со страницей руководства: инициализирует состояние возможностей в рабочей памяти, определяемое cap_p, так что все флаги возможностей сбрасываются. Исходный код libcap подтверждает это. - person Lekensteyn; 19.01.2013
comment
Если я удалю CAP_SETPCAP, все будет работать. В ноября 2009 г. (Linux 2.6.33) файловые возможности всегда включены. Таким образом, применимо следующее описание возможностей(7): добавить любую возможность из ограничивающего набора вызывающего потока в его наследуемый набор; удалить возможности из ограничивающего набора (через prctl(2) PR_CAPBSET_DROP); внесите изменения в флаги securebits. Здесь это кажется излишним, поскольку вы уже являетесь пользователем root. - person Lekensteyn; 19.01.2013
comment
@Lekensteyn: cap_set_flag(..., CAP_CLEAR) не требуются после cap_clear(), но они не причиняют вреда. Как я уже говорил, я предпочитаю использовать их в качестве защитной техники, так как это очень чувствительный к безопасности код. Что касается CAP_SETPCAP, то он не причиняет вреда, но может помешать работе с ядрами до 2.6.33 (а таких много в продакшене). - person Nominal Animal; 20.01.2013
comment
@NominalAnimal Из вашего описания похоже, что эти штучки CAP_CLEAR являются обязательными, но это не так и может сбить с толку читателя (например, меня), а не добавить глубокую защиту. Я также нашел в исходном коде, что cap_get_proc на самом деле cap_init + capget. Как упоминается на странице руководства (и в коде), cap_get_proc + cap_clear совпадает с cap_init. Что касается CAP_SETPCAP, можете ли вы указать в своем ответе, почему это (ненужно) добавлять? - person Lekensteyn; 20.01.2013
comment
@Lekensteyn: извинения; Я забыл об этом. Честно говоря, после еще одного дополнительного исследования я больше не уверен, когда CAP_SETPCAP понадобится в этой ситуации. Возможно, было бы намного чище полагаться только на возможности файлов (предоставляя возможности при выполнении двоичных файлов), что, на мой взгляд, является гораздо лучшим подходом. Я добавил в свой ответ простой пример тестовой программы планировщика; надеюсь, это прояснит часть путаницы, которую я непреднамеренно вызвал. - person Nominal Animal; 17.03.2013
comment
Как вы выяснили допустимые аргументы для setcap? Читать исходный код? Справочные страницы на самом деле не объясняют это. - person reinierpost; 25.06.2014
comment
@reinierpost: справочная страница возможностей man 7 объясняет каждую возможность вы можете использовать в деталях. Команда man 8 setcap говорит, что использует cap_from_text() для анализа возможностей и man 3 cap_from_text объясняет формат возможностей с примерами. проект справочных страниц Linux является хорошим справочником. - person Nominal Animal; 25.06.2014
comment
Я прочитал все три (мне пришлось читать cap_from_text онлайн, так как его нет в моей системе Ubuntu 12.04, и я не знаю, как его получить), и мне было неясно, что «предложения» в текстовом представление являются допустимыми аргументами для setcap. - person reinierpost; 25.06.2014
comment
@reinierpost: вам нужен пакет libcap-dev для написания собственного кода с учетом возможностей. в C, а также для получения этих страниц руководства cap_. Да, справочные страницы могли быть понятнее, но лучшей формулировки пока никто не придумал. Если вы (или кто-то другой) имеете в виду лучшую формулировку, внесите свой вклад< /а>. - person Nominal Animal; 25.06.2014
comment
Код testprio отсутствует cap_free(capabilities) перед вызовами test_priority(). - person EarlCrapstone; 10.07.2014
comment
@EarlCrapstone: Ну, вроде того. Его добавление было бы хорошим тоном, да, но это никак не изменило бы работу программы, и его отсутствие не является ошибкой. Это освободит любую динамически выделенную память (связанную с capabilities), вот и все. Так как программа закрывается вскоре после этого, зачем беспокоиться? (Примечание: я сам не уверен, стоит ли мне добавлять это или нет. Хороший аргумент изменит мое мнение.) - person Nominal Animal; 10.07.2014

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

Вы должны приобрести эти возможности в каждом потоке, в котором хотите их использовать, или использовать набор CAP_INHERITABLE.

Из возможностей(7):

Linux разделяет привилегии, традиционно связанные с суперпользователем, на отдельные блоки, известные как возможности, которые можно независимо включать и отключать. Возможности — это атрибут потока.

person Brian Cain    schedule 01.11.2012
comment
Значит, у меня нет выбора, мой демон должен сохранять привилегии root на протяжении всей своей жизни? - person brooks94; 01.11.2012
comment
Я так не думаю. Попробуйте изменить CAP_PERMITTED на CAP_INHERITABLE. - person Brian Cain; 01.11.2012