0%

Linux Set-UID 初探

Set-UID 是两种常见特权程序之一,是 SEED Labs 软件安全部分的典型特权程序目标

不同类型的特权程序

特权程序有两种常见的存在方式:守护进程和Set-UID程序。

  • 守护进程是在后台运行的计算机程序。为了成为特权程序,需要以特权用户身份来运行。用户向守护进程发起请求,而守护进程拥有特权权限,完成敏感操作。在 Windows 中守护进程被称为服务。
  • Set-UID 机制采用一个比特位来标记程序,在运行时加一区别对待。在 UNIX 操作系统中被广泛使用。

Set-UID 的工作原理

Set-UID 方式下,特权操作直接由普通用户来完成。当普通用户运行 Set-UID 程序时,进程会获得特权权限,
通过对进程行为的严格限制,使普通用户只能执行程序中的指定操作,从而实现了细粒度的权限控制。
当 Set-UID 用于用户组时,也被称为 Set-gid 机制。

Set-UID 程序和其他 UNIX 程序的唯一区别是具有 Set-UID 比特位,用以帮助操作系统区别对待。
在 UNIX 系统中,一个进程有三个用户 ID:真实用户 ID、有效用户 ID 和保留用户 ID。

  • 真实用户 ID:表明进程真正的拥有者,即运行该进程的用户。
  • 有效用户 ID:在访问控制中使用的 ID,代表了进程拥有什么样的权限,对非 Set-UID 程序而言,与真实 ID 一致,1对于 Set-UID 程序而言,则为程序的所有者 ID,从而拥有了所有者的特权
  • 保留用户 ID:

实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ls -l /usr/bin/id
# -rwxr-xr-x 1 root root ... ... ... ... /usr/bin/id

cp /usr/bin/id ./myid
ls -l myid
# -rwxr-xr-x 1 <your_username> <your_username> ... ... ... ... /usr/bin/id

./myid
# uid=<your_uid>(<your_username>) gid=<your_gid>(<your_groupname>) groups=...

#非 Set-UID 程序,切换所有权,有效用户仍为执行用户
sudo chown root myid
ls -l myid
# -rwxr-xr-x 1 root <your_username> ... ... ... ... /usr/bin/id

./myid
# uid=<your_uid>(<your_username>) gid=<your_gid>(<your_groupname>) groups=...

#修改为 Set-UID 程序后,有效用户 ID (eid) 为所有者 ID
sudo chmod 4755 myid
ls -l myid
# -rwsr-xr-x 1 root <your_username> ... ... ... ... /usr/bin/id

./myid
# uid=<your_uid>(<your_username>) gid=<your_gid>(<your_groupname>) euid=0(root) groups=...

注:执行 chown 后会自动清空 Set-UID 比特位,需要重新 chmod 添加

Set-UID 机制安全性

从原理上来说,Set-UID 机制是安全的,即便允许普通用户提升权限,普通用户也只能执行 Set-UID
程序中定义好的操作。这种安全性的前提是用户只能执行特权程序已有代码而非其他代码的前提下。
如果程序中存在错误,导致用户可以执行本不应在特权程序中执行的操作,安全性保障就失效了。

Set-UID 的攻击面

对于一个特权程序来说,攻击面存在于程序获得输入的地方。如果没有恰当的校验这些输入,它们就可能会影响程序的行为。
对于 Set-UID 程序,主要攻击面有:

  1. 用户输入:一般由程序明确地要求用户提供,如果程序没有很好地检查这些输入,将很容易受到攻击。
    例如,输入的数据可能被复制到缓冲区,而缓冲区有可能溢出从而运行恶意代码。
  2. 能够被用户左右的系统输入:系统提供的输入是否安全取决于它们是否会被不可信的用户所控制。
    例如,一个特权程序需要修改 /tmp 目录下的 xyz 文件,并且程序已经确定了文件名。系统根据文件
    名提供目标文件。在这种情况下,文件位于全局可修改的 /tmp 文件夹中,因此真正的目标文件可能被
    用户控制。
  3. 环境变量:程序运行时,行为可能被在程序内部不可见的输入影响。查看源代码时可能从来看不到这些输入。
    环境变量是其中一个例子,是构成程序运行环境的一系列环境参数,可以影响进程行为。例如,Set-UID
    特权程序简单地使用system('ls')而不是指令的完整路径,程序就会有安全隐患。system()
    首先运行/bin/sh,因为没有提供ls的完整路径,/bin/sh将从环境变量 PATH 中寻找ls的位置,
    用户可以通过操控 PATH 变量,使得/bin/sh执行用户提供的名为ls的恶意软件。
  4. 权限泄露:在一些程序中,特权程序会在完成特权操作后抛弃特权,这样进程就可以不再受约束。权限泄露发生在一个进程从特权进程转变为非特权进程时,其实质是进程在特权状态时获得过一些特权能力,但当特权被降级,程序没有清除这些权限,使得虽然有效用户 ID 变成了非特权的,但是进程仍具有特权。

经验教训

数据与代码分离:数据与代码应该清晰地分离开

如果输入是作为数据来使用的,它应该严格地被当作数据,其中的任何内容都不应该作为代码(比如命令的名称)。system()函数不支持数据和代码的分离,因此攻击者可以把新的指令或特殊字符(另一种形式的代码)嵌入输入中,导致攻击者选择的代码被执行。execve()函数明确地要求开发者将他们的输入分成代码(第一个参数)和数据(第二个和第三个参数),因此攻击者的数据输入无法变成代码。

使用该原则也是有代价的,即牺牲了便利性。

最小特权原则:系统的每个程序和特权用户应该以任务所需的最小权限进行操作

Set-UID 程序大多只需要一小部分 root 权限,但它们被给予了 root 的所有权限。这使得 Set-UID 程序被攻陷后造成的危害极为严重。Set-UID 的设计违背了最小特权原则。大多数操作系统没有为特权提供足够的粒度,POSIX 将 root 权限分成一系列小的权限,可以按需分配给程序对应的权限。

最小特权原则还意味着,如果一个特权程序在执行的某个阶段不需要一些特权,这些特权应该被关闭。对于不再需要的权限应该永久性关闭,对于还需要的权限应暂时关闭,用时再打开。这样即使代码中有错误,也可以把风险降到最低。对于 Set-UID 程序,seteuid()设置有效用户 ID 来暂时关闭和开启特权,setuid() 会把真实 ID、有效 ID、保留 ID 一起设置,使进程变成一个非 Set-UID 进程,从而不可逆地失去特权。