🔍

sudo はなぜ sudo できるのか

2024/04/26に公開

概要・背景

この記事では、 sudo コマンドが root 等の任意のユーザとしてプロセスを実行できる仕組みを、実践を交えながら説明します。

UNIX 系の OS において、システムに対する重要な変更を行うときには sudo コマンドを使用します。
システムの重要な部分は root ユーザ=スーパーユーザしか読み書きができなくなっている場合があります。
sudo コマンドは、プロセスの所有者をその root ユーザに変更することで、システムの重要な部分への操作を可能にします。
また、 root 以外の一般ユーザへも切り替えが可能です。

「そういえば、こうして root ユーザに変化できる仕組み知らないな〜」と思ったので、調べた内容をここにまとめます。
なお、筆者が自由に扱えるホストがあったという理由で、 Linux のみに限って説明します。
macOS 等別の OS では異なると思われますので、ご注意ください。

なお、本記事で登場する各 C プログラムは GitHub で公開しています。

https://github.com/kino-ma/sudo-capabilities

TL;DR

  • プロセスは、 setuid 系のシステムコールを使って、自身の権限に紐づく UID を変更できる
  • CAP_SETUID ケイパビリティを持つプロセスだけが、 setuid 系のシステムコールを呼び出せる
  • root ユーザが所有するプロセスは、 CAP_SETUID を含めた全てのケイパビリティを持つ
  • /usr/bin/sudo は、 SUID bit により、誰が呼び出しても root ユーザで実行される

前提知識

  • Linux の権限管理
    • ユーザ、グループ、パーミッション
  • システムコール

環境

$ uname -a
Linux ops 5.4.0-153-generic #170-Ubuntu SMP Fri Jun 16 13:43:31 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

setuid 系システムコール

さっそく結論ですが、 Linux には seteuid というシステムコールが存在し[1]、プロセスはこのシステムコールを呼び出すことで自身の所有者を変更することができます。

このシステムコールは、 uid_t 型で変更先のユーザ ID (UID) を受け取り、プロセスの effective user ID をその UID に変更します。
effective user ID とは、プロセスがさまざざまなリソース (ファイル等) にアクセスする際の権限チェックに使われる UID です[2]
プロセスに結びついた UID は、 effective user ID 以外にも real user IDsaved user ID などがあります。
seteuid に加えて、それらの UID を一括で書き換えるシステムコールとして setuid[3]setreuid[4]setresuid[5] が提供されています。

sudo は、これらのシステムコールを使ってプロセスの UID を変更し、 root 等のユーザとしてプログラムを実行します[6]

seteuid の使用例

setuid システムコールを使って UID を変更し、 sudo 同等の機能を実現する C プログラムを以下に示します。

seteuid.c
// Default to root UID
#define UID 0

#include <unistd.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    if (seteuid(UID) != 0) {
        printf("seteuid failed\n");
        return 1;
    }

    if (argc <= 1) {
        printf("command not specified\n");
        return 1;
    }

    execvp(argv[1], &argv[1]);

    // execvp never returns unless some error occured
    printf("some error occured\n");
    return 1;
}

以下のようにコンパイル・実行できます。

gcc setuid-sudo.c
./a.out whoami

期待通りであれば、以下の出力がされます。

root

CAP_SETUID ケイパビリティ

しかし、残念ながら先ほどのプログラムは期待通り動作しません。
プログラムが正しい権限を持たないからです。

Linux では、ケイパビリティという仕組みで、プロセスがシステムに対して行える操作を制限しています。
先ほどのプログラムが何の工夫もなく root としてプログラムを実行できてしまうと、どんなユーザであってもシステムに変更を加えられることになってしまいます。
それを防ぐために、 setuid 系システムコールを発行するためには、プロセスは CAP_SETUID ケイパビリティを持っている必要があります。

このことは、 seteuid[1:1] のマニュアルに明記されています。

seteuid() sets the effective user ID of the calling process.
Unprivileged processes may only set the effective user ID to the
real user ID, the effective user ID or the saved set-user-ID.

筆者による訳:

setuid() は、呼び出し元プロセスの effective user ID を設定します。権限を持たないプロセスは、現在の real user ID, effective user ID、または saved set-user-ID にのみ設定が可能です。

つまり、起動したプロセスの各 UID は一般ユーザ(筆者の場合は kino-ma (UID=1000))であるにもかかわらず、異なる値である root (UID=0) に設定しようとしたため、失敗してしまったわけです。

実際、先ほどのプログラムを以下のように編集して確認すると、CAP_SETUID ケイパビリティが付与されていないことがわかります。

check-cap.c
#define UID 0

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

int check_cap() {
    pid_t pid = getpid();
    cap_t cap = cap_get_pid(pid);

    if (cap == NULL) {
        printf("failed to get capabilites\n");
        return 1;
    }

    cap_value_t cap_value = CAP_SETUID;
    cap_flag_value_t cap_flags_value;

    cap_get_flag(cap, cap_value, CAP_EFFECTIVE, &cap_flags_value);
    printf("EFFECTIVE %s\n", (cap_flags_value == CAP_SET) ? "YES" : "NO");

    cap_get_flag(cap, cap_value, CAP_PERMITTED, &cap_flags_value);
    printf("PERMITTED %s\n", (cap_flags_value == CAP_SET) ? "YES" : "NO");

    cap_get_flag(cap, cap_value, CAP_INHERITABLE, &cap_flags_value);
    printf("INHERITABLE %s\n", (cap_flags_value == CAP_SET) ? "YES" : "NO");

    return 0;
}

int main(int argc, char** argv) {
    if (check_cap() != 0) {
        printf("check_cap failed\n");
        return 1;
    }

    // ... 以下略 ...
}

実行(libcap-dev が必要です):

gcc cap-setuid.c -lcap
./a.out whoami

出力:

EFFECTIVE NO
PERMITTED NO
INHERITABLE NO
seteuid failed

プログラムに CAP_SETUID ケイパビリティを追加する

CAP_SETUID ケイパビリティさえあれば、期待通り sudo のような機能を実現することができます。

やり方は簡単です。
コンパイル後に下のコマンドを実行します。

sudo setcap 'cap_setuid=ep' a.out

再度試してみます。

./a.out whoami

出力:

EFFECTIVE YES
PERMITTED YES
INHERITABLE NO
root

Effective, Permitted のケイパビリティセットで CAP_SETUID が有効になり、期待通り whoami コマンドが実行できたことがわかります。

/usr/bin/sudo のケイパビリティ

では、この CAP_SETUID が設定されていることが理由で、 sudo コマンドが機能するのでしょうか。
確認してみます。

getcap $(which sudo)

何も表示されませんでした。

先ほど setuid ケイパビリティを設定した a.out では以下の通り表示されます。
反対に何も表示されない sudo では、特にケイパビリティが設定されていないと推測することができます。

$ getcap a.out
a.out = cap_setuid+ep

しかし、 seteuid のマニュアルにあった通り、 CAP_SETUID は必須なはずです。
いつ CAP_SETUID が有効になるのでしょうか。

root プロセスのケイパビリティ

capability のマニュアルを読むと、冒頭に以下の記載があります。

For the purpose of performing permission checks, traditional UNIX
implementations distinguish two categories of processes:
privileged processes (whose effective user ID is 0, referred to
as superuser or root), and unprivileged processes (whose
effective UID is nonzero). Privileged processes bypass all
kernel permission checks, while unprivileged processes are
subject to full permission checking based on the process's
credentials (usually: effective UID, effective GID, and
supplementary group list).

Starting with Linux 2.2, Linux divides the privileges
traditionally associated with superuser into distinct units,
known as capabilities, which can be independently enabled and
disabled. Capabilities are a per-thread attribute.

筆者意訳:

伝統的な UNIX 実装は、プロセスを「特権あり」「なし」に分割します。
特権ありのプロセスとは、 effectvie user ID が 0、つまり root またはスーパユーザであるようなものです。
特権ありプロセスは、カーネルによる全ての権限チェックを通過することができる一方、特権なしプロセスは effective UID/GID、追加グループリストなどをもとに権限チェックが行われます。

Linux 2.2 から、特権を個別の単位に分離するようになりました。
それらはケイパビリティと呼ばれ、個別に有効化・無効化できます。
また、ケイパビリティはスレッドごとに設定されます。

したがって、(明記はされていませんが……) root が実行したプロセスはデフォルトで全てのケイパビリティが有効になると推測できます。

root プロセスのケイパビリティを確認する

以下のプログラムで、全てのケイパビリティを確認しましょう。

参考: lcap.c (Gist)
(私の環境に合わせて改変しました[7]。)

all-caps.c
#include <unistd.h>
#include <sys/capability.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    const char *cap_name[CAP_LAST_CAP+1] = {
       "CAP_AUDIT_CONTROL",
       "CAP_AUDIT_READ",
       "CAP_AUDIT_WRITE",
       "CAP_BLOCK_SUSPEND",
       "CAP_CHOWN",
       "CAP_DAC_OVERRIDE",
       "CAP_DAC_READ_SEARCH",
       "CAP_FOWNER",
       "CAP_FSETID",
       "CAP_IPC_LOCK",
       "CAP_IPC_OWNER",
       "CAP_KILL",
       "CAP_LEASE",
       "CAP_LINUX_IMMUTABLE",
       "CAP_MAC_ADMIN",
       "CAP_MAC_OVERRIDE",
       "CAP_MKNOD",
       "CAP_NET_ADMIN",
       "CAP_NET_BIND_SERVICE",
       "CAP_NET_BROADCAST",
       "CAP_NET_RAW",
       "CAP_SETGID",
       "CAP_SETFCAP",
       "CAP_SETPCAP",
       "CAP_SETUID",
       "CAP_SYS_ADMIN",
       "CAP_SYS_BOOT",
       "CAP_SYS_CHROOT",
       "CAP_SYS_MODULE",
       "CAP_SYS_NICE",
       "CAP_SYS_PACCT",
       "CAP_SYS_PTRACE",
       "CAP_SYS_RAWIO",
       "CAP_SYS_RESOURCE",
       "CAP_SYS_TIME",
       "CAP_SYS_TTY_CONFIG",
       "CAP_SYSLOG",
       "CAP_WAKE_ALARM",
    };

    pid_t pid = getpid();
    cap_t cap = cap_get_pid(pid);

    if (cap == NULL) {
        printf("failed to get capabilites\n");
        return 1;
    }

    cap_value_t cap_value;
    cap_flag_value_t cap_flags_value;

    for (int i = 0; i < CAP_LAST_CAP + 1; i += 1) {
        cap_from_name(cap_name[i], &cap_value);

        printf("%-20s %d\t\t", cap_name[i], cap_value);
        printf("flags: \t\t");

        cap_get_flag(cap, cap_value, CAP_EFFECTIVE, &cap_flags_value);
        printf(" EFFECTIVE %-4s ", (cap_flags_value == CAP_SET) ? "YES" : "NO");
        cap_get_flag(cap, cap_value, CAP_PERMITTED, &cap_flags_value);
        printf(" PERMITTED %-4s ", (cap_flags_value == CAP_SET) ? "YES" : "NO");
        cap_get_flag(cap, cap_value, CAP_INHERITABLE, &cap_flags_value);
        printf(" INHERITABLE %-4s ", (cap_flags_value == CAP_SET) ? "YES" : "NO");
        printf("\n");
    }
}

まずは一般ユーザとして起動してみます。

gcc all-caps.c -lcap
./a.out

出力:

CAP_AUDIT_CONTROL    30		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_AUDIT_READ       37		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_AUDIT_WRITE      29		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_BLOCK_SUSPEND    36		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_CHOWN            0		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_DAC_OVERRIDE     1		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_DAC_READ_SEARCH  2		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_FOWNER           3		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_FSETID           4		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_IPC_LOCK         14		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_IPC_OWNER        15		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_KILL             5		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_LEASE            28		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_LINUX_IMMUTABLE  9		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_MAC_ADMIN        33		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_MAC_OVERRIDE     32		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_MKNOD            27		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_NET_ADMIN        12		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_NET_BIND_SERVICE 10		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_NET_BROADCAST    11		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_NET_RAW          13		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SETGID           6		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SETFCAP          31		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SETPCAP          8		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SETUID           7		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_ADMIN        21		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_BOOT         22		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_CHROOT       18		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_MODULE       16		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_NICE         23		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_PACCT        20		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_PTRACE       19		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_RAWIO        17		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_RESOURCE     24		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_TIME         25		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYS_TTY_CONFIG   26		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_SYSLOG           34		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO
CAP_WAKE_ALARM       35		flags: 		 EFFECTIVE NO    PERMITTED NO    INHERITABLE NO

特権ユーザ (root) ではなく、また setcap による権限追加も行なっていないため、すべてのケイパビリティが無効になっています。

次に、ルートユーザとして実行してみます。
ここで、 sudo コマンドを使うと結果に何か影響があるかもしれないので、念の為 root ユーザとしてログインしたシェルで実行します。

# ./a.out
CAP_AUDIT_CONTROL    30		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_AUDIT_READ       37		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_AUDIT_WRITE      29		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_BLOCK_SUSPEND    36		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_CHOWN            0		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_DAC_OVERRIDE     1		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_DAC_READ_SEARCH  2		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_FOWNER           3		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_FSETID           4		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_IPC_LOCK         14		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_IPC_OWNER        15		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_KILL             5		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_LEASE            28		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_LINUX_IMMUTABLE  9		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_MAC_ADMIN        33		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_MAC_OVERRIDE     32		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_MKNOD            27		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_NET_ADMIN        12		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_NET_BIND_SERVICE 10		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_NET_BROADCAST    11		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_NET_RAW          13		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SETGID           6		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SETFCAP          31		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SETPCAP          8		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SETUID           7		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_ADMIN        21		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_BOOT         22		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_CHROOT       18		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_MODULE       16		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_NICE         23		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_PACCT        20		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_PTRACE       19		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_RAWIO        17		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_RESOURCE     24		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_TIME         25		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_TTY_CONFIG   26		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYSLOG           34		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_WAKE_ALARM       35		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO

全てのケイパビリティが有効になっている[8] ことがわかります。

たしかに、 root ユーザとしてプロセスを起動することで、 CAP_SETUID を含めた全ての capability が有効になると確かめられました。

set-user-ID bit

/usr/bin/sudo ファイル自身に CAP_SETUID が設定されていない以上、 sudo は何らかの手段で root UID で起動していると考えることができます。

それを可能にするのが、ファイルの S_ISUID モード[9]です。

S_ISUID モード (04000) が設定された実行ファイルは、 execve で実行されたとき、プロセスの effective user ID を ファイルの所有者に変更します[10]

すなわち、実行ファイルの所有者を root にした上で set-user-ID を設定することで、プログラムを root ユーザとして起動することができます。

set-user-ID bit をつけて seteuid を実行する

それでは、 set-uesr-ID を使って seteuid.c が期待通り動作できるようにしましょう。

gcc seteuid.c
sudo chown root ./a.out
sudo chmod u+s ./a.out
./a.out whoami

出力

root

また、 all-caps.croot 権限で実行してみます。

gcc all-caps.c -lcap
sudo chown root ./a.out
sudo chmod u+s ./a.out
./a.out
CAP_AUDIT_CONTROL    30		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_AUDIT_READ       37		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_AUDIT_WRITE      29		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_BLOCK_SUSPEND    36		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_CHOWN            0		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_DAC_OVERRIDE     1		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_DAC_READ_SEARCH  2		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_FOWNER           3		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_FSETID           4		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_IPC_LOCK         14		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_IPC_OWNER        15		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_KILL             5		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_LEASE            28		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_LINUX_IMMUTABLE  9		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_MAC_ADMIN        33		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_MAC_OVERRIDE     32		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_MKNOD            27		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_NET_ADMIN        12		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_NET_BIND_SERVICE 10		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_NET_BROADCAST    11		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_NET_RAW          13		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SETGID           6		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SETFCAP          31		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SETPCAP          8		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SETUID           7		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_ADMIN        21		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_BOOT         22		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_CHROOT       18		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_MODULE       16		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_NICE         23		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_PACCT        20		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_PTRACE       19		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_RAWIO        17		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_RESOURCE     24		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_TIME         25		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYS_TTY_CONFIG   26		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_SYSLOG           34		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO
CAP_WAKE_ALARM       35		flags: 		 EFFECTIVE YES   PERMITTED YES   INHERITABLE NO

先ほど root ユーザが直接起動した時と同様、全てのケイパビリティが有効になっていることがわかります。

/usr/bin/sudo のモードを確認する

set-user-ID を使えば root ユーザとしてプログラムを実行可能なことがわかりました。

実際に sudo コマンドもこれを利用していることを確認してみます。

ls -l $(which sudo)
-rwsr-xr-x 1 root root 166056 Apr  4  2023 /usr/bin/sudo
   ^ この s が set-user-ID bit

たしかに、所有者の executable bit に当たる部分が s[11]、つまり set-user-ID が有効であることが確認できました。

したがって、一般ユーザが sudo を実行すると、 sudo プロセス自身が root ユーザとして起動され、その特権を使って他ユーザ (あるいは root 自身) への切り替えを行なっていると言えます。
また、筆者はソースを読んでいませんが、実際に切り替えを行う前に sudoers の確認やパスワード認証を行うことで、権限のチェックをしているものと思われます。

結論

sudo は、 UID を root 等別のユーザに切り替えるために setuid 系のシステムコールを使っています。

しかし、 setuid 系のシステムコールを呼び出すためには、プロせスに CAP_SETUID というケイパビリティ(権限)が必要です。

通常、プログラムのケイパビリティは setcap(8) 等で付与されますが、 sudo 実行ファイルには付与されていません。

その代わりに、 sudo 実行ファイルの所有者を root にし、加えて set-user-ID ビットを設定することで、 sudo プロセスは常に root ユーザとして起動されます。

root ユーザのプロセスは CAP_SETUID を含めた全ての特権を持つため、 setuid 系のシステムコールを使ってユーザを切り替えることができます。

まとめ

この記事では、 sudo コマンドが我々の思う sudo として機能する理由を調べてみました。

調査に当たってはできるだけ Linux マニュアル等の信頼できるソースと実際の動作を根拠にしましたが、文章の読み違い・見落としがあるかもしれません。
もし内容に誤りがあれば、または追加の情報や文脈があれば、コメントに投稿いただけると幸いです。

以上です。
読んで下さったどなたかの参考になれば嬉しいです。

脚注
  1. https://man7.org/linux/man-pages/man2/seteuid.2.html ↩︎ ↩︎

  2. https://man7.org/linux/man-pages/man7/credentials.7.html ↩︎

  3. https://man7.org/linux/man-pages/man2/setuid.2.html ↩︎

  4. https://man7.org/linux/man-pages/man2/setreuid.2.html ↩︎

  5. https://man7.org/linux/man-pages/man2/setresuid.2.html ↩︎

  6. https://github.com/sudo-project/sudo/blob/78699a8f7abc6682fa8de9100671e756ed1b5b11/src/exec.c#L201-L220 ↩︎

  7. 私の環境では CAP_LAST_CAP は 37 でした。これが異なる場合、マニュアルから cap_name を書き換えてください。 ↩︎

  8. Inheritable セットだけは無効になっています。 capabilities マニュアルの Thread capability sets 節によると、 Inheritable セットはケイパビリティを execve を超えて継承するか否かを示します。デフォルトでオフである理由は詳しく調べていませんが、 UID を切り替えてから execve する場合に権限が継承することを避けるためなどが考えられるのではないでしょうか。 ↩︎

  9. https://man7.org/linux/man-pages/man2/chmod.2.html ↩︎

  10. https://www.gnu.org/software/libc/manual/html_node/How-Change-Persona.html ↩︎

  11. ちなみに、通常の実行可能 bit (x) が無効なファイルに set-user-ID が設定されると、大文字の S になるようです。 ↩︎

Discussion