以下の記事を読んで setuid したバイナリを実行する挙動で新たに知ったことがあった
以下に引用する
Some OSes (e.g., OpenBSD) protect against this by opening /dev/null on any unused FDs in the 0-2 range when execing a setuid program. As far as I can tell, Linux does not (but maybe I'm missing something...). This behavior is permitted in POSIX.1-2001, but not before.
いくつかの OS (たとえば OpenBSD) は、setuid プログラムを実行するときに、0-2 の範囲の未使用の FD で /dev/null をオープンして、この問題を防いでいます。私の知る限り、Linuxはそうではありません(しかし、もしかしたら私は何かを見逃しているかもしれません...)。この動作はPOSIX.1-2001では許可されているが、それ以前は許可されていない。
DeepL 翻訳
OpenBSD の execve(2) の man にも 下記の説明が付いている
In the case of a new setuid or setgid executable being exe- cuted, if file descriptors 0, 1, or 2 (representing stdin, stdout, and stderr) are currently unallocated, these descriptors will be opened to point to some system file like /dev/null. The intent is to ensure these descriptors are not unallocated, since many libraries make assumptions about the use of these 3 file descriptors.
なるほどなー 。実際にどうなんだろうと Linux で試した。
実験環境
Ubuntu Jammy で実験をします
hiboma@vps:~$ uname -a Linux vps 5.15.0-60-generic #66-Ubuntu SMP Fri Jan 20 14:29:49 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux hiboma@vps:~$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 22.04 LTS Release: 22.04 Codename: jammy
実験用のコード
C のコードで sleep するだけの setuid バイナリを用意します。
#include <unistd.h> int main() { sleep(100); }
以下の手順で setuid なバイナリとします。
$ gcc setuid-sleep.c -o setuid-sleep $ sudo chown root.root setuid-sleep $ sudo chmod 4755 setuid-sleep $ ls -hal setuid-sleep -rwsr-xr-x 1 root root 16K Jun 7 10:24 setuid-sleep
0, 1, 2 のファイルディスクリプタを閉じてから 1 setuid なバイナリを実行する bash のシェルスクリプトも用意します
#!/bin/bash exec 0<&- exec 1<&- exec 2<&- exec ./setuid-sleep
実験
シェルスクリプトを実行します。これで 0, 1, 2 のデスクリプタを閉じて setuid したバイナリを exec できます。
$ ./test.sh
setuid したバイナリを実行しているプロセスの lsof をとって見ます。
$ sudo lsof -p $(pgrep setuid-sleep) COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME setuid-sl 1120951 root cwd DIR 252,2 4096 134434 /home/hiboma setuid-sl 1120951 root rtd DIR 252,2 4096 2 / setuid-sl 1120951 root txt REG 252,2 15968 135566 /home/hiboma/setuid-sleep setuid-sl 1120951 root mem REG 252,2 2216304 4337 /usr/lib/x86_64-linux-gnu/libc.so.6 setuid-sl 1120951 root mem REG 252,2 240936 194 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 setuid-sl 1120951 root 0w CHR 1,7 0t0 8 /dev/full 👈 setuid-sl 1120951 root 1r CHR 1,3 0t0 5 /dev/null 👈 setuid-sl 1120951 root 2r CHR 1,3 0t0 5 /dev/null 👈
/dev/full, /dev/null を開いていますね!
/dev/full, /dev/null を open するのはどこ?
strace をとって調べて見たところ、ld-linux-x86-64.so.2 っぽい。
openat(AT_FDCWD, "/dev/full", O_WRONLY|O_NOFOLLOW) = 0 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x9748) [0x26b38] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x903e) [0x2642e] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x24a5) [0x1f895] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x41c8) [0x215b8] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x2ec8) [0x202b8] ... 略 openat(AT_FDCWD, "/dev/null", O_RDONLY|O_NOFOLLOW) = 1 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x9748) [0x26b38] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x907e) [0x2646e] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x24a5) [0x1f895] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x41c8) [0x215b8] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x2ec8) [0x202b8] ... 略 openat(AT_FDCWD, "/dev/null", O_RDONLY|O_NOFOLLOW) = 2 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x9748) [0x26b38] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x90c2) [0x264b2] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x24a5) [0x1f895] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x41c8) [0x215b8] > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x2ec8) [0x202b8]
ソースは?
_dl_catch_errorは、GNU Cライブラリ (glibc) の内部で使用される関数です。この関数は、動的リンカーによる共有ライブラリのロードやシンボルの解決中に発生するエラーをキャッチ(捉え)する役割を果たします。
具体的には、
_dl_catch_error
は以下のような機能を提供します: 1. エラーハンドラの設定: エラーが発生した際に呼び出される関数を設定できます。これにより、バリエーションに富むエラーハンドリングを行うことができます。
エラーの発生処理: _dl_catch_error は、エラーが発生した際にエラーメッセージを生成し、設定されたエラーハンドラに渡します。これにより、詳細なエラー情報が収集され、適切なエラー処理が行われます。
エラーハンドラのリセット: エラーが捉えられた後には、元の状態に戻します。_dl_catch_errorはビルトインのエラー処理機能を提供することで、動的リンカーによる共有ライブラリのロードやシンボルの解決を安全かつ信頼性の高いものにする役割を果たします
実験の補足
ファイルディスクリプタを閉じずに setuid バイナリを実行すると、0, 1, 2 のファイルディスクリプタは /dev/pts
を指していました
~$ sudo lsof -p $(pgrep setuid-sleep) COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME setuid-sl 1125289 root cwd DIR 252,2 4096 134434 /home/hiboma setuid-sl 1125289 root rtd DIR 252,2 4096 2 / setuid-sl 1125289 root txt REG 252,2 15968 135566 /home/hiboma/setuid-sleep setuid-sl 1125289 root mem REG 252,2 2216304 4337 /usr/lib/x86_64-linux-gnu/libc.so.6 setuid-sl 1125289 root mem REG 252,2 240936 194 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 setuid-sl 1125289 root 0u CHR 136,1 0t0 4 /dev/pts/1 setuid-sl 1125289 root 1u CHR 136,1 0t0 4 /dev/pts/1 setuid-sl 1125289 root 2u CHR 136,1 0t0 4 /dev/pts/1
感想
- 細かいプロセスの挙動ではまだまだ知らないことがある
- /dev/full, /dev/null を open する実装がどこにあるのか気になる