作者:zhanglei

《Linux内核安全子系统简介(上)》

资源隔离

资源隔离是一个历史悠久又异常有效的安全手段。

从操作系统的角度来看,它对各个进程的管理实际上就是一个隔离。每个进程都拥有从0开始的连续一大片地址空间可以使用,但实际上在物理地址上,各个进程却被分割开来。

在Linux系统下,早期比较出名的资源隔离手段是chroot。Linux用户可以创建一个虚拟的根文件系统,在其中部署软件,再通过chroot命令运行部署在其中的软件,这些软件运行的结果只会影响到虚拟文件系统,这样就可以避免对真实的根文件系统造成影响。

在当下,Linux操作系统中已经提供了namespace(命名空间),它包括cgroup、IPC、network、mount、pid、user与UTS等多个子系统的隔离支持。例如,通过mount namespace即可创建虚拟的文件系统,而通过pid namespace则可以创建虚拟的进程号系统。使用namespace可以隔离进程的运行环境,使得进程互不影响,也不会影响到系统,这样即使一个进程出现了安全问题,也不会影响到系统与其它进程。

在Linux操作系统中,程序可以调用cloneunsharesetns等系统调用调整当前进程的namespace,系统管理员也可以通过firejail或者unshare命令设置一个进程的namespace。例如,使用firejail运行firefox可以这样:

firejail firefox

对于运行firefox究竟应该如何进行限制,则可以通过/etc/firejail/firefox.profile来限制,如下:

可以看到,大部分限制都是针对路径进行限制的,即firejailfirefox的运行环境创建了一个mount namespace,这样就可以将firefox与实际的文件系统进行隔离了。

当然,也可以通过firejail的命令行参数设置命名空间隔离,例如设定DNS等。

配额限制

配额限制可以设定一个进程能使用的计算资源,这些计算资源包括处理器、内存、设备、网络等物理资源,也可能包括进程转储(coredump)、栈大小等更抽象的资源。

配额限制分为多个场景:

  • 如果希望限制当前shell下的进程资源,可以使用ulimit命令
  • 如果希望限制某个登录会话的进程资源,可以使用pam_limits.so,它使用的配置文件一般是/etc/security/limits.conf,它可以限制的资源与ulimit类似
  • 如果希望限制某个守护进程(即systemd服务)的资源,可以使用systemd.resource-control,它是基于cgroups对守护进程进行资源限制的
  • 对于任意进程,可以使用cgroups对其进行资源限制,一般的步骤是:
    • /sys/fs/cgroup具体资源目录下创建目录,例如假设想限制某个进程的资源,则可以sudo mkdir /sys/fs/cgroup/memory/mygroup
    • 为了操作方便,设置目录的属主:sudo -R chown raphael:raphael /sys/fs/cgroup/memory/mygroup
    • 设置进程的最大内存为64M:echo 64m > /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes
    • 将某个进程($mypid)加入此cgroup组中:echo $mypid > /sys/fs/cgroup/memory/mygroup/tasks
    • 如果希望一启动进程就将其加入某个组,则可以使用cgexec命令

对于当前的云计算来说,资源隔离与配额限制是最为重要的两个功能,前者可以使得租户可以互不影响,后者则可以为每个租户配置不同的资源,另外,也可以保证某个进程不至于占用任意多的资源,从而可以防止拒绝服务的攻击。

系统沙箱

沙箱这个词非常模糊,看起来它更像上面说到的资源隔离,就是把各个进程隔离在一个单独的箱子里运行。但实际上在Linux中,系统沙箱指的是对系统调用进行过滤的方法,对应的功能是seccomp。

seccomp是Linux提供的一个系统调用,通过seccomp,程序可以限制当前进程在调用系统调用时,可以根据系统调用号、系统调用参数来决定系统调用的返回值。除此之外,也可以使用prctl(PR_SET_SECCOMP...)来提供类似的功能

例如程序可以设置当前进程不能调用socket(即返回错误),或者调用socket创建IPv6套接字的时候杀死当前线程,或者调用socket创建IPv4套接字或者Unix域套接字的时候产生一个SIGSYS信号。

这些复杂的逻辑实际上是通过内核提供的cBPF虚拟机实现的。要设置这些逻辑,开发人员需要学习cBPF字节码,并使用字节码编程,将字节码数组通过bpf系统调用传递到内核中去。

下面是内核自带的使用prctl进行系统调用控制的一个例子。在这个例子里,如果当前进程在x_arch架构下调用系统调用号为x_nr的系统调用时,内核会直接返回x_err的错误:

可以看到对应的BPF字节码一共有六条(注意所有的跳转都是相对跳转),它们的作用依次是:

  1. 读取当前系统的架构至寄存器
  2. 寄存器的值与x_arch,若不等则跳到最后一条指令(3),不然继续执行下一条指令(0)
  3. 读取当前的系统调用号至寄存器
  4. 比较寄存器的值与x_nr,若不等则跳到最后一条指令(1),不然继续执行下一条指令(0)
  5. 返回(BPF_RET)错误号x_err
  6. 返回(BPF_RET)允许继续进行系统调用(SECCOMP_RET_ALLOW

cBPF源自于BSD系统,它是基于寄存器的语言,有累加寄存器与索引寄存器两个32位的寄存器,一共约三十条32位的指令,每条指令包含16位的操作码(opcode)、8位的为真时跳转地址,以及8位的为假时跳转地址,还可以通过JIT进行加速。不过为了保证安全性,cBPF不支持循环。在内部,cBPF会被修改为eBPF进行处理。

使用seccomp最多的就是浏览器、容器等软件。seccomp比较大的问题包括:

  • 需要学习和使用内核的cBPF语言,在用户态没有特别好的工具链可用,内核提供了一个libbpf,但是用起来还是太麻烦,而基于llvm的bcc等工具则需要携带整套编译器,对于生产环境来说非常不便
  • 只能由当前进程调用seccomp限制自身的系统调用,系统管理员无法限制进程
  • 与用户态交互不方便,有时候可能需要读取用户态管理程序设置的数据判定是否可以进行系统调用

可信计算

可信计算是一套围绕可信芯片为核心的可信链管理体系。其原理是系统出厂时对系统的关键部件进行扫描,提取其唯一性的标识(当部件的内容改变时此唯一性标识必须也相应变化),并将其在可信芯片中存储下来。当系统加电时,会经历下面几个步骤:

  • 可信芯片首先对包括处理器、内存、总线在内的硬件部件进行扫描,确认其标识没有变化之后,再将系统移交给处理器
  • 处理器验证bootloader的完整性(通过计算bootloader的散列值,并将计算出来的散列值与存储在可信芯片内的散列值对比),确认其完整性没有变化之后将系统移交给bootloader
  • bootloader验证内核的完整性(同样是散列值比较),确认其完整性没有变化之后将其移交给内核
  • 内核验证外部内核模块与init相关程序的完整性,若完整性没有变化之后加载内核模块,并启动init程序,之后的完整性校验则主要被转交给了用户程序主导

除了上述流程外,可信计算还有几个安全约束。首先是上述存储在可信芯片里的唯一性标识是不可更改的,因此对可信系统的更新是一个棘手的问题。其次是这些唯一性标识也是不可读的,因此如果需要验证唯一性,需要将待校验数据输入可信芯片,由可信芯片在内部进行散列计算和比对。因此在对大数据量进行完整性校验时,如果带宽不足,则可能严重影响系统的性能。另外,在实施上,实现完全的可信需要从固件开始做起,因此对整个系统的改造量是比较大的。

除了提供完整性校验之外,可信芯片还提供了多个加密算法的实现。

可信计算在国外的标准是TPM(trusted platform module),Linux内核早已支持了它,对应内核里的源码在drivers/char/tpm目录下。国内对应的标准是TCM与TPCM,对应的加密算法包括SM2( 椭圆曲线公钥密码算法 )、SM3(散列函数)、SM4(分组密码算法)与SM9(标识密码算法)等。

在内核里,除了对TPM的支持外,还有对TPM的应用。对应模块是IMA与EVM,它们是结合文件扩展属性对文件完整性进行校验的,不过其实际应用很少见。

安全审计

在Linux操作系统内核中,审计系统起到的作用是按照设计规则,对整个系统中发生的感兴趣的事件进行记录,并将记录保存下来成为审计日志。一般的日志与审计有相似之处,也有不同之处。相似之处在于它们都可以记录下系统中发生的事件,不同之处在于日志一般是基于自觉的行为,应用程序和系统中的守护进程在执行自己任务的过程中会主动自觉的生成日志,并将其发送给日志守护进程,由日志守护进程将其记录在日志文件中。而审计则是整个系统内部强制性的事件检查。

Linux操作系统内核的审计系统由内核态与用户态组成。内核态的审计模块内嵌于内核之中,属于不可卸载,可以配置的内核功能模块。它的主要功能是根据用户态传入的审计规则,对内核中发生的相应事件进行检查与审计,并将审计信息传给用户态守护进程(auditd),由用户态守护进程将其记录下来。审计系统的用户态程序较多,有负责记录审计日志的auditd,有负责提供搜索审计结果的ausearch,有配置审计规则的auditctl,有生成审计报告的aureport,还有可以进行进一步分发的audispd等。

审计系统的内核态与用户态之间的接口使用的是netlink socket,这种通讯方式可以以异步的方式进行回调通信,因此使用较为灵活。auditd守护进程通过netlink套接字持续获取内核的审计消息,以进行记录和分发。

内核生成审计信息的途径主要包括系统调用与主动调用两种方式,其中主动调用又可以分为内核模块发起的主动审计信息发生与用户态进程发起的主动审计信息发生。在系统调用的入口和出口,内核会调用kernel/auditsc.c中的__audit_syscall_entry__audit_syscall_exit函数,分别用来分配与初始化系统调用的审计相关信息(架构、系统调用号、系统调用参数等),以及用来销毁系统调用的审计相关信息的。

这些函数最终会调用内核的audit_log_开头的一系列函数,将审计日志通过netlink套接字发送到auditd进行处理。

其它安全子系统

Linux下的数据透明加密主要可以分为两类:

  • 基于块设备映射技术的透明加密,即基于dev-mapper的dm-crypt等
  • 基于文件系统的透明加密,如eCryptFS,以及各种加解密的FUSE文件系统

其中Linux下使用最广的透明加密解决方案LUKS就是依赖于dm-crypt的。

Linux下的网络防火墙历经变革,其原理基本都是基于数据包过滤的思想。Linux内核中对应的内核模块为netfilter/nftables,用户态对应的工具主要是iptables/nft,不过由于eBPF最近几年的快速发展与应用,所以实际上防火墙已经有相当部分功能可以通过bpf来控制了。

问题与方向

当前Linux内核安全系统设计的问题不少,包括:

  • 使用C/C++开发内核代码容易导致安全隐患,主要是内存管理的问题,如溢出、泄漏、越界访问等。虽然有人已经在尝试使用rust开发内核模块,但是仍然有很多的问题需要解决
  • 自主访问控制无法解决猪队友的问题,强制访问控制无法解决可扩展性的问题
  • 用户验证缺乏对新型验证硬件与技术的支持,以及对图形用户界面的规范
  • 可信计算难以避免管理僵硬与性能低下的问题

上述问题有的按照现有的方向其实很难解决,因此我们也许需要新的方法来处理。

比如:

  • 对系统与应用进行分离。这一点已经在iOS与Android中取得了成功
  • 形式化验证。对于小型化系统,例如几千行或者甚至万行级别的系统,这也许是可行的
  • 新的访问控制模型。例如基于对象的能力模型,即oCap
  • 结合证书,而不仅是可信计算的系统闭环管理
  • 新的安全开发语言,例如类似rust的语言,而现在看来lisp/haskell难当重任

发表评论