概要
pingの実装を通してプロセスの権限を勉強した備忘録。
疑問
pingコマンドはset-user-IDされているので一般ユーザーで実行した場合でも実効IDがrootで実行されるはずだと思っていた。ところがpsコマンドで確認したならば以下の様な結果になった。そう、実効IDが一般ユーザーで実行されていた。なぜだろう・・?
/bin/ping
[root@localhost iputils-s20071127]# ll /bin/ping
-rwsr-xr-x. 1 root root 40760 9月 26 14:35 2013 /bin/ping
set-user-IDがセットされている。これを実行すると実効IDがrootでpingが実行されるはずだと思っていた・・
[vagrant@localhost ~]$ id
uid=500(vagrant) gid=500(vagrant) 所属グループ=500(vagrant)
[vagrant@localhost ~]$ /bin/ping 192.168.33.20
PING 192.168.33.20 (192.168.33.20) 56(84) bytes of data.
64 bytes from 192.168.33.20: icmp_seq=1 ttl=64 time=0.021 ms
64 bytes from 192.168.33.20: icmp_seq=2 ttl=64 time=0.034 ms
ping実行したまま、以下を実行
[vagrant@localhost net]$ ps -eo uid,euid,fuid,suid,command | grep [p]ing
500 500 500 500 ping 192.168.33.10
各IDを確認するとすべてvagrantユーザーの値に。pingはset-user-IDされたプログラムなのでrootの値になっているはず、しかしvagrantユーザーの値。なぜだろう?
試した環境
- Vagrant on Mac
- pingのソースコードが入っているsrpmを取得。
[root@localhost iputils-s20071127]# cat /etc/redhat-release
CentOS release 6.5 (Final)
[root@localhost iputils-s20071127]# which ping
/bin/ping
[root@localhost iputils-s20071127]# rpm -qf /bin/ping
iputils-20071127-17.el6_4.2.x86_64
[root@localhost iputils-s20071127]#
処理みてみた
pingのソースコード
int main(int argc, char **argv)
{
.
.
.
icmp_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
socket_errno = errno;
uid = getuid();
if (setuid(uid)) {
perror("ping: setuid");
exit(-1);
}
.
.
.
ソースコードを見ればわかりました。socketシステムコールを実行した後にgetuidで自プロセスの実ユーザーIDを取得して、setuidで自プロセスの実効ユーザーIDを変更しています。よってpsコマンドではsetuidされた後の値(vagrantユーザー)が見えていたわけです。
CentOS7のPing
[root@localhost iputils-s20121221]# cat /etc/redhat-release
CentOS Linux release 7.1.1503 (Core)
[root@localhost iputils-s20121221]# uname -r
3.10.0-229.el7.x86_64
[root@localhost iputils-s20121221]# which ping
/bin/ping
[root@localhost iputils-s20121221]# ll /bin/ping
-rwxr-xr-x. 1 root root 44896 6月 10 2014 /bin/ping
他にもcentos7の環境で見てみました。するとcentos7のping実行ファイルにはではケーパビリティのチェックをしていました。
limit_capabilities();
#ifdef USE_IDN
setlocale(LC_ALL, "");
#endif
enable_capability_raw();
icmp_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
socket_errno = errno;
disable_capability_raw();
socketシステムコールを実行するまえにenable_capability_raw()でCAP_NET_RAW権限だけを与えて、socketシステムコール後にdisable_capability_raw()で権限をクリアしていました。よってset-user-IDしていない実行ファイルでもsocketシステムコールを実行できているようです。しかしなぜcent6とcent7でpingの作りが違うのでしょうか??んー、なぜでしょうか。両者を見るとcent7のほうが権限を最小化できているのでセキュリティ的に望ましいといえます。
関係ありそうなカーネルの処理をみてみた
カーネル:linux-2.6.32
set-user-IDを実行ファイルから取得している箇所
sys_execve() → do_execve からの先で呼ばれている。
/*
* Fill the binprm structure from the inode.
* Check permissions, then read the first 128 (BINPRM_BUF_SIZE) bytes
*
* This may be called multiple times for binary chains (scripts for example).
*/
int prepare_binprm(struct linux_binprm *bprm)
{
umode_t mode;
struct inode * inode = bprm->file->f_path.dentry->d_inode;
int retval;
mode = inode->i_mode;
if (bprm->file->f_op == NULL)
return -EACCES;
/* clear any previous set[ug]id data from a previous binary */
bprm->cred->euid = current_euid();
bprm->cred->egid = current_egid();
if (!(bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID)) {
/* Set-uid? */
if (mode & S_ISUID) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->euid = inode->i_uid;
}
/* Set-gid? */
/*
* If setgid is set but no group execute bit then this
* is a candidate for mandatory locking, not a setgid
* executable.
*/
if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->egid = inode->i_gid;
}
}
/* fill in binprm security blob */
retval = security_bprm_set_creds(bprm);
if (retval)
return retval;
bprm->cred_prepared = 1;
memset(bprm->buf, 0, BINPRM_BUF_SIZE);
return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);
}
linux_binprm
/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
struct linux_binprm{
char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMU
struct vm_area_struct *vma;
#else
# define MAX_ARG_PAGES 32
struct page *page[MAX_ARG_PAGES];
#endif
struct mm_struct *mm;
unsigned long p; /* current top of mem */
unsigned int
cred_prepared:1,/* true if creds already prepared (multiple
* preps happen for interpreters) */
cap_effective:1;/* true if has elevated effective capabilities,
* false if not; except for init which inherits
* its parent's caps anyway */
#ifdef __alpha__
unsigned int taso:1;
#endif
unsigned int recursion_depth;
struct file * file;
struct cred *cred; /* new credentials */
int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
unsigned int per_clear; /* bits to clear in current->personality */
int argc, envc;
char * filename; /* Name of binary as seen by procps */
char * interp; /* Name of the binary really executed. Most
of the time same as filename, but could be
different for binfmt_{misc,script} */
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
};
set-user-IDの値を取得してlinux_binprmの構造体に値を入れているようです。linux_binprmはコメントにかかれているように、execveする時の各種情報を入れる構造体みたいです。その後にsecurity_bprm_set_credsで権限のチェックをしているみたいでした(ケーパビリティも含む)。この先で通常ユーザーの実効IDをset-user-IDの値で権現の上書きをしているんでしょうか???security_bprm_set_credsの先はいつか読んでみたいと思います。
他にも関係ありそうな箇所を見てみました。
task_struc構造体からひも付く各ID。
struct cred {
・
・
・
uid_t uid; /* real UID of the task */
gid_t gid; /* real GID of the task */
uid_t suid; /* saved UID of the task */
gid_t sgid; /* saved GID of the task */
uid_t euid; /* effective UID of the task */
gid_t egid; /* effective GID of the task */
uid_t fsuid; /* UID for VFS ops */
gid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management
・
・
・
}
どこかで使われていないかなーと思ってみてみました。do_execve()のさらに先でELF実行ファイルの場合にELFをメモリにロードする処理の途中で何か権限を挿入している処理がありました。
static int
create_elf_tables(struct linux_binprm *bprm, struct elfhdr *exec,
unsigned long load_addr, unsigned long interp_load_addr)
{
・
・
・
/* Create the ELF interpreter info */
elf_info = (elf_addr_t *)current->mm->saved_auxv;
/* update AT_VECTOR_SIZE_BASE if the number of NEW_AUX_ENT() changes */
#define NEW_AUX_ENT(id, val) \
do { \
elf_info[ei_index++] = id; \
elf_info[ei_index++] = val; \
} while (0)
#ifdef ARCH_DLINFO
/*
* ARCH_DLINFO must come first so PPC can do its special alignment of
* AUXV.
* update AT_VECTOR_SIZE_ARCH if the number of NEW_AUX_ENT() in
* ARCH_DLINFO changes
*/
ARCH_DLINFO;
#endif
NEW_AUX_ENT(AT_HWCAP, ELF_HWCAP);
NEW_AUX_ENT(AT_PAGESZ, ELF_EXEC_PAGESIZE);
NEW_AUX_ENT(AT_CLKTCK, CLOCKS_PER_SEC);
NEW_AUX_ENT(AT_PHDR, load_addr + exec->e_phoff);
NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr));
NEW_AUX_ENT(AT_PHNUM, exec->e_phnum);
NEW_AUX_ENT(AT_BASE, interp_load_addr);
NEW_AUX_ENT(AT_FLAGS, 0);
NEW_AUX_ENT(AT_ENTRY, exec->e_entry);
NEW_AUX_ENT(AT_UID, cred->uid);
NEW_AUX_ENT(AT_EUID, cred->euid);
NEW_AUX_ENT(AT_GID, cred->gid);
NEW_AUX_ENT(AT_EGID, cred->egid);
NEW_AUX_ENT(AT_SECURE, security_bprm_secureexec(bprm));
NEW_AUX_ENT(AT_RANDOM, (elf_addr_t)(unsigned long)u_rand_bytes);
NEW_AUX_ENT(AT_EXECFN, bprm->exec);
・
・
・
}
ELF interpreter infoにNEW_AUX_ENTマクロを使用して各IDを挿入していました。この領域はユーザースタックの環境変数へのポインタが入っている配列の直前に置かれるようです。これは一体なんなのでしょうか??実はプログラムを実行する時カーネルでexec処理するんですが最終的にカーネルではIPの値としてその実行ファイルの動的リンカのエントリポイントが挿入されます。動的リンカが実行ファイルの共有ライブラリをリンクして、実行ファイルのエントリポイントを設定する流れです。色々調べると動的リンカがこの領域を取得しているようでした。わざわざ各IDもここに挿入されているということはきっと何かに使われているのだと思います。何に使われているのかは調べていません。いつか調べられたらと思います。