10

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問題は、このコードを実行した後、上記のコメントで示唆されているように、呼び出し時に「操作が許可されていません」というメッセージが表示されることです。私は何を間違っていますか?

4

2 に答える 2

27

元の失敗の理由を説明するために編集されました。

Linux には、継承可能、許可、有効という 3 つの機能セットがあります。Inhertable は、どの機能がexec(). Permitted は、プロセスに許可される機能を定義します。有効は、現在有効な機能を定義します。

プロセスの所有者またはグループをルートから非ルートに変更すると、有効な機能セットは常にクリアされます。

デフォルトでは、許可された機能セットもクリアされますがprctl(PR_SET_KEEPCAPS, 1L)、ID を変更する前に呼び出すと、許可されたセットをそのまま保持するようカーネルに指示されます。

プロセスが ID を特権のないユーザーに戻した後CAP_SYS_NICE、有効なセットに追加する必要があります。(許可されたセットにも設定する必要があるため、機能セットをクリアする場合は、それも設定することを忘れないでください。現在の機能セットを変更するだけの場合は、それを継承しているため、既に設定されていることがわかります。)

従うべきことをお勧めする手順は次のとおりです。

  1. 実ユーザー ID、実グループ ID、および補助グループ ID を保存します。

     #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++;
    

    補助グループ ID (現在の番号を要求するだけ) を「クリア」できないため、リストが空にならないようにしてください。実際のグループ ID を補助リストにいつでも追加して、空にしないようにすることができます。

     if (gids < 1) {
         gid[0] = group;
         gids = 1;
     }
    
  3. 実際の有効なユーザー ID を root に切り替えます

     if (setresuid(0, 0, 0)) /* error */
    
  4. セットにCAP_SYS_NICE機能をCAP_PERMITTED設定します。セット全体をクリアし、このアプローチが機能するために必要な 4 つの機能のみを保持することを好みます (後で、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 から非特権ユーザーに変更しても機能を保持したいことをカーネルに伝えます。デフォルトでは、ルート ID から非ルート ID に変更すると、機能はゼロにクリアされます

     if (prctl(PR_SET_KEEPCAPS, 1L)) /* error */
    
  6. 実際の有効な保存済みグループ ID を、最初に保存されたグループ ID に設定します。

     if (setresgid(group, group, group)) /* error */
    
  7. 補助グループ ID を設定する

     if (setgroups(gids, gid)) /* error */
    
  8. 実際の有効な保存済みユーザー ID を、最初に保存されたユーザー ID に設定します。

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

    この時点で、機能を除いて、root 権限を効果的に削除します (これ以上元に戻すことはできません) CAP_SYS_NICE。root ユーザーから非 root ユーザーへの移行により、この機能は有効になりません。カーネルは、このような遷移で有効な機能セットを常にクリアします。

  9. andセットでCAP_SYS_NICE機能を設定しますCAP_PERMITTEDCAP_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 */
    

    後の 2 つのcap_set_flag()操作は、不要になった 3 つの機能をクリアするため、最初の機能だけがCAP_SYS_NICE残ることに注意してください。

    この時点で、機能の記述子は不要になったため、解放することをお勧めします。

     if (cap_free(capabilities)) /* error */
    
  10. カーネルに、ルートからのそれ以上の変更に対して機能を保持したくないことを伝えます (繰り返しますが、パラノイアです)。

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

libcap-devこれは、パッケージをインストールした後、Xubuntu 12.04.1 LTS で GCC-4.6.3、libc6-2.15.0ubuntu10.3、および linux-3.5.0-18 カーネルを使用する x86-64 で動作します。

追加するために編集:

実行可能ファイルは setuid root であるため、実効ユーザー ID が root であることにのみ依存することで、プロセスを簡素化できます。その場合、setuid root は有効なユーザー ID にのみ影響し、それ以外には影響しないため、補助グループについても心配する必要はありません。元の実際のユーザーに戻ると、技術的setresuid()には手順の最後に 1 回の呼び出しのみが必要であり (setresgid()実行可能ファイルもたまたま setgid root とマークされている場合)、保存されたユーザー ID と実効ユーザー ID (およびグループ ID) の両方を実際のユーザー。

ただし、元のユーザーの ID を取り戻すケースはまれであり、指定されたユーザーの ID を取得するケースは一般的であり、ここでの手順はもともと後者用に設計されています。initgroups()名前付きユーザーの正しい補助グループを取得するために使用します。その場合、実際の、有効な、保存されたユーザー ID とグループ ID、および補助グループ ID を慎重に処理することが重要です。そうしないと、プロセスは、プロセスを実行したユーザーから補助グループを継承してしまうからです。

ここでの手順は偏執的ですが、セキュリティに敏感な問題を扱っている場合、偏執的であることは悪いことではありません。実際のユーザーに戻す場合は、単純化できます。


簡単なテスト プログラムを示すために 2013 年 3 月 17 日に編集されました。これは、setuid ルートでインストールされていることを前提としていますが、すべての権限と機能を削除します (通常のルールを超えるスケジューラ操作に必要な 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()ID や機能を操作する必要はありません。関数main()以外のすべてを削除できtest_priority()ます。バイナリ、たとえば./testprioCAP_SYS_NICE 優先度を指定するだけです。

sudo setcap 'cap_sys_nice=pe' ./testprio

getcap実行して、バイナリの実行時に付与される優先度を確認できます。

getcap ./testprio

どちらが表示されますか

./testprio = cap_sys_nice+ep

これまでのところ、ファイル機能はほとんど使用されていないようです。私のシステムでgnome-keyring-daemonは、ファイル機能 (メモリをロックするための CAP_IPC_LOCK) を持つ唯一のシステムです。

于 2012-11-01T21:51:16.753 に答える
1

アプリケーションのメインスレッドで、起動の近くで実行されるコードがいくつかあります。

これらの機能は、使用する各スレッドで取得するか、セットを使用する必要がありますCAP_INHERITABLE

機能(7)から:

Linuxは、従来スーパーユーザーに関連付けられていた特権を、機能と呼ばれる個別の単位に分割します。これらの単位は、個別に有効または無効にできます。 機能はスレッドごとの属性です。

于 2012-11-01T19:17:33.560 に答える