Linux Kernel Security Summary

概述

内核安全是系统安全体系中的核心,但系统管理者和内核开发者在刚开始追求性能和功能的时候,却往往会牺牲安全性。但随着 Linux 应用在更广泛和更复杂的系统中,对增强其安全机制的呼声也越来越强烈,也越来越重要。本文根据相关资料就内核安全的诸多方面做一些总结。

参考 《Linx 内核安全模块深入剖析》by 李志
参考 《Linux系统安全:纵深防御、安全扫描与入侵检测》by 胥峰
参考 Linux内核安全探究:漏洞利用、防御机制与调试技术 By Ba1_Ma0
参考 深入剖析Linux sudo的实现原理
参考 Linux ACL访问控制权限

历史

计算机系统应对安全的办法大致有四种:隔离、控制、混淆和监视。—— 《剖析》

自主访问控制

自主访问控制是指由用户决定制定访问策略

Unix 的自主访问控制:进程操作文件(r,w, x),root 用户的进程几乎可以做任何事情。

Linux 的自动访问控制:增加了 ACL (Access Control List),规定某一个用户或者某一个组的操作许可;而是对特权操作细化,就是 Capabilities ,将属于 root 用户的特权细化为互不相关的几十个能力。

凭证 (Credentials)

主体(Subjective)凭证和客体(Objective)凭证:进程可以是主体,也可以是客体。

在大多数情况下,主体凭证和客体凭证的值都是相同的,但在某些情况下内核代码会修改当前进程的主体凭证,以获得某种访问权限,待执行完任务后再将主体凭证改回原值。 ——《剖析》

进程 TCB 结构体中和凭证相关的成员:

1
2
3
4
5
6
7
8
9
10
struct task_struct {
...
/* Process credentials: */

const struct cred __rcu *ptracer_cred; /* Tracer's credentials at attach: */
const struct cred __rcu *real_cred; /* Objective and real subjective task credentials (COW): */
const struct cred __rcu *cred; /* Effective (overridable) subjective task credentials (COW): */
...
}

struct cred 结构体中包含 ID(gid 和 uid)、能力集密钥环(keyring)强制访问控制相关的成员。

可以读取 /proc/[pid]/status 查看进程凭证。

关于 uid、euid、suid 和 fuid

euid 和提升权限有关。

初始时,suid 为 0,当需要提升进程权限时,suideuid 做交换,此时进程拥有特权;执行完特权操作,再将两者做一次交换,进程就失去特权。

giduid 类似,但 group id 与特权无关,egid 或者别的 gid 为 0,进程不会因此获得特权。

(内核在判断是否赋予特权时,依据的是 capabilities,不是 uid)。

相关系统调用

setuidseteuid,调用这些系统调用本身也需要特权。

sudo 命令的实现原理

a. 利用 euid 进行临时权限提升

b. setuid机制

sudo本身是一个二进制可执行文件(通常位于/usr/bin/sudo),它被设置了setuid位。通过ls -l查看,可以看到:

1
-rwsr-xr-x 1 root root  sudo

这里的s表示setuid位。当普通用户运行sudo时,进程的EUID会自动变为文件拥有者的UID(即root,UID=0),从而具备root权限。

c. 配置文件解析

sudo启动后,首先以root权限运行,然后解析/etc/sudoers文件,检查调用者的权限。它使用严格的语法解析器,确保规则匹配。

d. 安全检查

在执行命令前,sudo会进行多项安全检查,例如验证用户密码、检查命令是否在允许范围内等。这些检查防止了未授权的操作。

e. 执行目标命令
sudo通过fork创建一个子进程,并在子进程中调用setuidseteuidEUID切换为目标用户ID(通常是root)。然后,它使用execvp或类似函数执行用户指定的命令。

客体标记和文件属性

struct inode 结构体中保存了文件属性,这些属性包括允许位属主和属组ACL相关强制访问控制等。

相关的系统调用:chown(需要特权) 、fchown

查看:statfstatlstat

其他客体:目录(一种特殊的文件)、管道设备socketIPCKey(密钥)

针对 IPC密钥有专门的系统调用查看和修改属性。

操作

主体凭证和客体标记都设置好后,就可以通过系统调用完成主体对客体的操作,更重要的是,内核需要在系统调用中判断相应的操作许可,进而实现访问控制。

操作:执行 和部分客体特有的操作。

允许位

普通文件使用 9 个bit 的允许位。

设置位

Unix 在 9 个允许位的基础上增加了允许位 setuidsetgidset-other-bit or sticky bit,这三个允许位与操作许无关。

setuid的机制 : 进程调用 execve 执行了允许位 setuid 为 1 的文件后,进程的 euid,还有 Linux 特有的 fsuid,被改变为所执行文件的属主 id,于是,进程可以操作一些之前不能操作的客体。比如 /usr/bin/sudo 文件。

setgid的机制 : 与 setuid 类似。

sticky bit的机制 :在 Linux 中不再使用。

系统调用

chmodfchmod 可以修改文件和目录上的允许位。

ACL(访问控制列表)

ACL 只作用于文件和目录。在普通权限中,用户对文件只有三种身份,就是属主、属组和其他;但在实际场景中,这三种身份是不够的,Linux 使用 ACL 对此进行了扩展,来实现更细粒度的访问控制。

在很多 Linux 系统上,ACL 权限默认是开启的,可以通过查看根分区文件系统信息进行确认:

1
2
3
4
5
$ sudo dumpe2fs -h /dev/sdx
...
Filesystem flags: unsigned_directory_hash
Default mount options: user_xattr acl
...

Default mount options 中可以看到是否开启 ACL 权限。

扩展属性

inode 结构体中有指向和 ACL 相关的扩展属性i_acli_default_acl 的指针。

这两个属性的类型是 struct posix_acl,它是一个变长数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct posix_acl_entry {
short e_tag;
unsigned short e_perm;
union {
kuid_t e_uid;
kgid_t e_gid;
};
};

struct posix_acl {
refcount_t a_refcount;
struct rcu_head a_rcu;
unsigned int a_count;
struct posix_acl_entry a_entries[0];
};

struct posix_acl_entry 可以记录一个用户或者一个组的操作许可,也就实现了细粒度访问控制。

系统调用

setxattrgetxattrremovexattr

权限设置指导

a. 查看 ACL 权限

1
$ sudo getfacl <file>

b. 设置 ACL 权限到特定用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ mkdir tmp
$ sudo chown root:my_group tmp/
$ sudo chmod 700 tmp/
$ sudo getfacl tmp/

$ sudo useradd test_user
$ sudo setfacl -m u:test_user:rx tmp/
$ sudo getfacl tmp/
# file: tmp
# owner: root
# group: my_group
user::rwx
user:test_test_user:r-x
group::rwx
mask::rwx
other::---

c. 设置 ACL 权限到特定用户组

1
2
3
4
5
6
7
8
9
10
11
12
$ sudo groupadd test_group
$ sudo setfacl -m g:test_group:rwx tmp
$ sudo getfacl tmp
# file: tmp
# owner: root
# group: my_group
user::rwx
user:test_test_user:r-x
group::rwx
group:test_group:rwx
mask::rwx
other::---

d. 删除 ACL 权限

1
2
3
4
5
$ sudo setfacl -x u:test_user tmp
$ sudo setfacl -x g:test_group tmp

# 删除所有权限
$ sudo serfacl -b tmp

Capabilities(能力)

特权机制:一些操作无法纳入允许位控制,如重启系统或者设置系统时间;另外一类是超越允许位控制。

如果没有设计特权机制,为了解决设置系统时间这类任务,必须要引入新的客体类型,或者引入一种新的文件类型(如 SELinux)。 —— 《剖析》

Linux 参考 IEEE 1003.1eIEEE 1003.2c 实现了自己的能力机制。

能力分类

  • 文件:如 chowndac_overridefowner 等。
  • 进程:如 killsetuidsetgidsetpcapsys_chrootsys_nice 等。
  • 网络:如 net_bind_service(绑定系统特权端口)、net_adminnet_raw 等。
  • ipc: 如 ipc_lock(不受 rlimit 锁定内存限制)
  • 系统:如 block_suspendsys_adminsys_bootsys_modulesys_timesys_resourcewake_alarm 等。
  • 设备:如 mknod
  • 审计:如 audit_writeaudit_control
  • 强制访问控制(MAC)

能力集合

struct cred 结构体中可以看到多个能力集:

1
2
3
4
5
6
7
8
9
struct cred {
...
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
...
};

AmbientInheritable 集合的意义就在于可以帮助我们在进程树或 namespace 的范围内创建一个允许任意进程使用某些 capabilities 的环境。

能力机制

Linux 内核在做特权判断时,判断的是进程凭证中的有效能力集中是否具备所要求的能力。—— 《剖析》

进程在execve 后的允许能力集有两个来源,一个是进程的可继承能力集和文件的可继承能力集的交集,另一个是文件的允许能力集和进程的限制能力集的交集。—— 《剖析》

capgetcapset 系统调用。

向后兼容

Linux 内核判断进程是否进行特权操作的唯一标准就是相关的能力是否在进程的有效能力集中。Linux 内核中没有任何依据用户 id 进行授权的逻辑。

为了做到向后兼容,内核在涉及用户id 变化的系统调用中(setuid、seteuid、setreuid、setresuid、setfsuid)增加了调整进程的能力集的逻辑。

实践

通过查看 /proc/self/status 中的 CapEff 项获取当前进程的有效能力集:

1
2
3
4
5
6
$ cat /proc/self/status| grep Cap
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000

使用 capsh 解析16进制数中每个 1 bit所对应的能力:

1
2
$ sudo capsh --decode=000001ffffffffff
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search ...

ping 命令没有使用能力,而是使用了 setuid 允许位来做特权提升。可以自行编译 ping 来使用能力

新编译的 ping 并没有设置 suid:

1
2
$ ls -l builddir/ping/ping
-rwxr-xr-x 1 mimose mimose 311960 3月 4日 12:30 builddir/ping/ping
1
2
3
$ sudo setcap 'cap_net_raw+p' builddir/ping/ping
$ sudo getcap builddir/ping/ping
builddir/ping/ping cap_net_raw=p

参考 Linux Capabilities 入门教程

强制访问控制

SELinux

SELinux 的安全机制包括:基于角色的访问控制(RBAC)类型增强(TE)多级安全(MLS)

类型增强机制下,进程和文件都有一个类型。

MLS 机制来源于 BLP 模型,BLP 模型下,进程和文件都有安全标签,标签有两项:敏感度和组别。

  • 低敏感的进程不能读高敏感度文件,高敏感度进程不能写低敏感度文件。
  • 当进程的组别包含或者等于文件的组别时,进程可以操作文件。

SMACK

Tomovo

AppArmor

参考 AppArmor使用指南:Linux应用安全防护利器

AppArmor 的机制也是 类型增强,类型也可以被称为 AppArmor 的目标是限制应用程序的权限,从而保护操作系统和应用程序免受内部和外部威胁,包括零日攻击。它通过定义每个应用程序的安全配置文件来实现此目的,这些配置文件指定了应用程序可以访问的资源和允许的操作。

即使应用程序存在漏洞,AppArmor 也能通过限制其行为,来阻止漏洞被利用。**AppArmor 的核心思想是将访问控制属性绑定到程序而不是用户。** 它也被称为 “应用装甲”。

AppArmor 基于文件路径进行访问控制。

查看安装状态:

1
2
3
4
$ sudo aa-status
63 profiles are loaded.
57 profiles are in enforce mode.
...

raspi OS上需要修改 cmdline.txt

1
.... lsm=apparmor

AppArmor 的配置文件:

  • 强制模式(enforce):在此模式下,AppArmor 将强制执行配置文件中定义的策略,并阻止任何违反规则的操作,并将违规尝试记录到日志中。
  • 抱怨模式(complain):在此模式下,AppArmor 不会阻止违反规则的操作,但会将违规尝试记录到日志中。此模式对于测试和开发新的配置文件很有用。

使用 aa-complain 命令将配置文件设置为抱怨模式,使用 aa-enforce 命令将其设置为强制模式。

1
2
$ sudo aa-complain /path/to/bin
$ sudo aa-enforce /path/to/bin

为应用程序创建一个新的配置文件:

1
$ sudo aa-genprof /usr/bin/myapp

Yama

完整性保护

审计和日志

加密

其他机制

命名空间(namespace)

针对 chroot 的安全漏洞。

a. 挂载命名空间(mount)

Linux 通过挂载命名空间可以提供比 chroot 机制更完备的隔离方案。通过挂载命令空间,进程和一棵挂载树相联系,改变了进程的挂载命名空间,进程就和另一颗挂载树相对应。

虽然 mount ns 限制了进程对文件的访问,但攻击者仍然可以利用进程间通信将本不能被进程访问的文件内容暴露给进程。所以,需要引入进程间通信命名空间。

b. 进程间通信命名空间
进程间通信的标识存储在进程间通信命名空间中,进程中通信命名空间不同,进程寻找到的进程间通信实例就不同。

c. Unix 分时命名空间(UTS)

d. 进程号命名空间
使得容器中的进程不能够看到容器外的进程(比如通过 kill 命令向容器外的进程发送信号)。

e. 网络命名空间

f. 用户命名空间

/proc/[pid]/ns 下可以看到一些进程的命名空间信息。

实际上,命名空间的开发和设计只能是量力而行和适可而止,因为在宏内核上理想的和绝对的隔离是不可能做到的。

cgroup

参考 使用 cgroupv2 限制进程资源使用
参考 Control Group v2 - Kerneldoc

一种安全攻击(Fork_bomb: 耗尽系统进程号资源):

1
2
3
4
bomb() {
bomb | bomb &
}
bomb

Unix 为应对上述安全挑战,提供了配额(quota)资源限制(rlimit)机制。但存在分散式管理缺乏统一资源管理机制的问题。

CGroups 控制组来源于 Google 开发的 Process Containers 的模块。其设计理念是将框架和具体的资源管理分离。Linux 内核子系统 cgroup 提供的用户态和内核的接口是名为cgroup的伪文件系统。

1
2
3
4
5
6
7
8
# 新建 CGroup2 类型的控制组
mkdir -p /sys/fs/cgroup/cgrp1/

# 修改 CPU 配额为 50%
echo "50000 100000" > /sys/fs/cgroup/cgrp1/cpu.max

# 将目标进程加入到该控制组,假设目标进程的 pid 为 1604
echo 1604 > /sys/fs/cgroup/cgrp1/cgroup.procs

设计原则总结如下:

  • 以进程为控制单位
  • 集中式管理
  • 功能模块化
  • 动态调配
  • 层级结构
  • 多对多结构:一个进程可以加入多个控制组,一个控制组可以包含度多个进程。

cgroup v2 是现代容器化技术(如 DockerKubernertes)的核心组件之一。它用于限制容器的资源使用,确保容器之间的资源隔离和公平分配。例如:

  • Docker 使用 cgroup v2 限制容器的 CPU内存I/O
  • Kubernetes 使用 cgroup v2 实现 Pod 的资源限制和 QoS(服务质量)策略。

seccomp

参考 Sandbox的安全机制 —— 使用 seccomp

seccomp 是内核的一个安全特性。它的原理是限制用户进程可以使用的系统调用。因此,seccomp 可以与其他安全技术一起用于构建沙箱

三种模式:

  • DISABLED: 不使用 seccomp,进程的系统调用可以不受限制。
  • STRICT:只能使用 4 个系统调用:readwriteexitsigreturn
  • FILTER:对系统调用的限制被存入 seccomp结构体的第二个成员 filter 中,由 filter 决定系统调用是否可以使用。

BPF 虚拟机运行 filter 链中定义的BPF 指令

/proc/self/status 文件中会显示当前进程的 seccomp 状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat /proc/self/status
...
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
NoNewPrivs: 0
Seccomp: 0
Seccomp_filters: 0
Speculation_Store_Bypass: thread vulnerable
SpeculationIndirectBranch: unknown
...

例如,使用 seccomp 限制 execve 系统调用的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <sys/syscall.h>
#include <seccomp.h>

int main()
{
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_load(ctx);

char *filename = "/bin/sh";
char *argv[] = {"/bin/sh", NULL};
char *envp[] = {NULL};

syscall(SYS_execve, filename, argv, envp);

return 0;
}

ASLR

ASLR 可以对栈(stack)堆(heap)内存映射(mmap)vdso 的地址做随机化处理。所谓随机化,就是内存区域的起点在一定范围内浮动,而浮动的依据是内核随机数设备产生的随机数。

随机化最好的是位置无关的可执行文件,除了 vsyscall 区,其余区域都做了地址随机化。vsyscall不能做随机化是因为它本身是 Linux 内核 ABI 的一部分。如果不考虑兼容老旧应用,可以在编译内核时把对 vsyscall 的支持去掉。

漏洞利用与防御

Linux CVE

Linux CVE 列表