检查OpenSSH沙箱和特权分离-攻击面分析
检查OpenSSH安全机制的有效性

最近的OpenSSH双自由度漏洞- CVE-2023-25136对于OpenSSH的自定义安全机制——沙盒和特权分离,人们产生了很多兴趣和困惑。到目前为止,这两种安全机制都没有被注意到,而且只有部分文档记录。这个无双重漏洞引起了受影响者和使用OpenSSH的服务器控制者的兴趣。
这篇博文深入分析了OpenSSH的攻击面和安全措施。
- OpenSSH是如何实现特权分离的?
- OpenSSH特权分离-深入分析
- 什么是OpenSSH沙盒?
- OpenSSH沙盒-深入分析
- 结论-不要乱用默认值!
- 与JFrog安全研究保持同步
- 附录A - OpenSSH沙箱完整的系统调用列表
OpenSSH是如何实现特权分离的?
OpenSSH的特权分离机制自2002年3月开始实施,至今已有20多年。
该特性旨在通过限制SSH服务器进程的特权并将其与用户的身份验证和会话进程分离来增强SSH服务器的安全性。
特权分离的目标是确保预认证攻击不会危及根帐户,即使OpenSSH的其他部分确实使用根权限运行。
在引入特权分离之前,OpenSSH服务器进程必须以提升的特权运行,才能访问身份验证和会话管理所需的系统资源。2022世界杯阿根廷预选赛赛程这种提升的特权级别使服务器进程成为攻击者的高价值目标,攻击者可能通过利用服务器进程中的任何漏洞获得对系统的完全控制。
OpenSSH服务器进程中的任何远程代码执行漏洞(sshd)如果发生在身份验证之前,可能会导致立即的远程根泄露,随后使攻击者完全控制运行OpenSSH的机器。
特权分离机制
通过特权分离,OpenSSH服务器进程被分成两个独立的进程:一个进程以更高的特权运行,以处理系统级任务(如网络I/O),另一个进程以更低的特权运行,以处理用户身份验证。
当用户启动到启用了特权分离的OpenSSH服务器的SSH连接时,服务器会生成两个单独的进程来处理传入的连接。
第一个进程称为特权进程,以提升的特权运行,负责处理网络I/O,例如监听传入的连接、管理网络套接字和管理伪终端。
第二个进程称为非特权进程,它以减少的特权运行,负责处理用户身份验证。它与特权进程隔离,对系统资源(如文件系统和网络接口)的访问受限。2022世界杯阿根廷预选赛赛程
OpenSSH特权分离-深入分析
特权分离使用两个进程:有特权的父进程监视无特权的子进程的进程。
子进程没有特权。这可以通过将其uid/gid更改为未使用的用户(通常)来实现sshd),它没有登录shell,并限制通过chroot ()来/var/empty。子进程是唯一处理网络数据的进程。
父进程确定子进程是否成功执行身份验证。
特权进程和非特权进程之间的通信是通过管道实现的。共享内存存储不能以其他方式导出的状态,子进程必须询问具有特权的父进程以确定身份验证是否成功。
如果子进程损坏并认为远程用户已通过身份验证,则只有在父进程达成相同的决定时才会授予访问权限。
在身份验证期间,子进程与用户和身份验证代理通信,以获取身份验证所需的凭据。子进程获得凭据后,将它们发送给父进程进行验证。
然后,父进程使用子进程提供的凭据对用户进行身份验证,从而执行实际的身份验证。如果身份验证成功,父进程将向子进程发送身份验证成功的消息。如果身份验证失败,父进程将向子进程发送身份验证失败的消息。
父进程和子进程之间的通信是使用Unix域套接字完成的,这是进程间通信(IPC)机制的一种形式。
父进程和子进程都有自己的Unix域套接字,它们使用这些套接字相互通信。
通过在父进程中执行身份验证,OpenSSH能够确保敏感的身份验证数据永远不会离开特权进程,这提供了额外的安全层。此外,通过使用IPC在父进程和子进程之间进行通信,OpenSSH能够保持两个进程之间的分离,并防止子进程干扰父进程执行的关键操作。
在预认证阶段,sshd将chroot ()来/var/empty并将其权限更改为sshd用户及其主组。sshd是一个伪帐户,它被锁定,不被其他守护进程使用,并且不包含有效的shell。
给定以下流程清单:
| UID | PID | PPID | C | 少许 | TTY | 时间 | 命令 |
|---|---|---|---|---|---|---|---|
| 根 | 957 | 9 | 0 | 09:14 | ? | 00:00:00 | /usr/sbin/sshd -D [listener] 10-100个创业公司 |
| 根 | 1015 | 957 | 0 | 09:14 | ? | 00:00:00 | sshd(接受): |
| sshd | 1016 | 1015 | 0 | 09:14 | ? | 00:00:00 | sshd(净): |
- 进程957是侦听新连接的sshd进程。
- 进程1015是特权监视进程。
- 进程1016是无特权进程
authenticator-handler的过程。
特权分离机制由UsePrivilegeSeparation配置的关键。默认情况下,该键被设置为最严格的沙盒设置(即使没有指定密钥),这意味着预身份验证非特权进程受到额外的限制,我们将在下一节中介绍。这个配置文件的默认位置是/etc/sshd_config。
一个示例中配置文件:
#连接端口22协议2 UseDNS no Compression no # Authentication: PubkeyAuthentication yes PermitEmptyPasswords no UsePAM yes ChallengeResponseAuthentication yes LoginGraceTime 60 useprivilegesseparation sandbox #相关特权分离配置密钥…
什么是OpenSSH沙盒?
OpenSSH预认证沙箱是OpenSSH 5.9版本中首次引入的一种安全机制,旨在防止攻击者在预认证阶段利用漏洞后完全破坏系统。它创建了一个受限制的环境,在SSH连接的身份验证阶段限制潜在漏洞的范围。
它通过使用内核安全机制(如seccomp过滤和名称空间隔离)的组合启动一个孤立的环境来运行——本质上将其功能限制为只有几个预先批准的系统调用。
当用户启动到启用了沙箱特性的OpenSSH服务器的SSH连接时,服务器会生成一个在受限环境(也称为沙箱)中运行的新进程。创建沙盒进程时,其特权有限,对系统资源(包括文件系统和网络接口)的访问也受到限制。2022世界杯阿根廷预选赛赛程
OpenSSH沙盒-深入分析
OpenSSH有7(!)种不同的沙盒风格,这些风格由编译它的平台及其内核功能决定。
所有不同的沙盒风格都围绕着系统调用限制的概念——这意味着沙盒进程不能使用大多数系统服务,比如打开文件、通过网络通信等。
Linux沙箱
OpenSSH配置步骤(在编译步骤之前运行)检查seccomp兼容性,方法是检查内核是否配置了SECCOMP_MODE_FILTER选择。
这是配置时的样子:
检查是否声明了SECCOMP_MODE_FILTER…是的
检查内核是否支持seccomp_filter…是的
检查Ubuntu 22.04 LTS sshd守护进程二进制的沙箱类型,我们看到它使用了一个seccomp过滤器,所以我们将关注它:
> strings ./sshd | grep正在准备
%s:准备第二个过滤器沙箱
Seccomp代表安全计算模式,自2005年发布的2.6.12版本以来一直是Linux内核的一个特性。它是用来过滤和限制可用的系统调用到用户进程,从而减少了暴露的内核表面,从而限制了特权升级的攻击面。
这是通过只使用应用程序正常运行所需的基本系统调用来实现的。与套接字过滤器一样,过滤器被表示为伯克利包过滤器(BPF),只不过操作的数据与正在进行的系统调用相关:系统调用号和系统调用参数。
我们检查sandbox-seccomp-filter.c文件,并找到Seccomp过滤器附件中的程序ssh_sandbox_child ()功能时,OpenSSH使用的seccomp配置文件为-
/*系统呼叫过滤设置为预认证。*/ static const struct sock_filter preauth_insns[] ={........................ /*系统调用非fatal deny */ #ifdef __NR_lstat SC_DENY(__NR_lstat64, EACCES), #endif #ifdef __NR_lstat64 SC_DENY(__NR_lstat64, EACCES), #endif #ifdef __NR_fstat SC_DENY(__NR_fstat, EACCES), #endif #ifdef __NR_clock_gettime SC_ALLOW(__NR_brk), #endif #ifdef __NR_clock_gettime), #endif #ifdef __NR_clock_gettime64 SC_ALLOW(__NR_clock_gettime64),#endif #ifdef __NR_close SC_ALLOW(__NR_close), #endif #ifdef __NR_mmap SC_ALLOW_ARG_MASK(__NR_mmap, 2, PROT_READ|PROT_WRITE|PROT_NONE), #endif #ifdef __NR_mmap2 SC_ALLOW_ARG_MASK(__NR_mmap2, 2, PROT_READ|PROT_WRITE|PROT_NONE), #endif #ifdef __NR_mprotect SC_ALLOW_ARG_MASK(__NR_mprotect, 2, PROT_READ|PROT_WRITE|PROT_NONE), #endif /*默认deny */ BPF_STMT(BPF_RET+BPF_K, SECCOMP_FILTER_FAIL),
它使用宏(SC_DENY/SC_ALLOW/SC_ALLOW_ARG_MASK)来创建用作Seccomp过滤器的BPF过滤器。
例如,我们看到lstat()被显式地拒绝以静默方式失败,close()被显式地允许,而mprotect()被允许,但必须传递一个参数掩码来拒绝不需要的参数。
我们还可以看到,非详细系统调用的默认值是SECCOMP_FILTER_FAIL:
/* Linux seccomp_filter沙箱*/ #定义SECCOMP_FILTER_FAIL SECCOMP_RET_KILL
SECCOMP_RET_KILL导致进程立即退出,而不执行系统调用。
这是我们在上一篇博客文章中尝试触发漏洞时的结果,seccomp沙箱将失败并退出进程,因为writev()没有定义,并自动导致SECCOMP_RET_KILL。
一些主要的被静默拒绝的系统调用[1]:
开放-用于打开文件并获取文件描述符,该描述符可用于对文件进行读写操作。删除此系统调用大大减少了攻击面,因为攻击者将无法打开任意文件。
openat-类似于open,但相对于给定的目录工作。
一些主要的显式允许的检查参数的系统调用[2]:
mmap-用于将内存区域映射到调用进程的地址空间。拒绝某些参数将防止攻击者创建危险的内存映射,并阻止一些ROP shell代码(参见下一节)。
mprotect-用于修改一系列内存页面的访问权限。
一些主要的显式允许的系统调用没有参数检查[3]:
关闭释放先前通过使用。命令打开文件获得的文件描述符开放或openat系统调用。
madvise-用于通知内核内存范围的预期使用情况。允许程序与操作系统通信它计划如何使用内存的特定区域,这可以帮助内核优化其对该内存的管理。
mremap-用于更改现有内存映射的大小或位置。
munmap-用于删除先前使用mmap系统调用建立的内存映射。当程序不再需要访问内存映射时,它应该调用munmap系统调用释放相关内存并删除映射。
写—用于将数据从缓冲区写入文件描述符。
我们将首先深入了解mmap的限制:
#ifdef __NR_mprotect SC_ALLOW_ARG_MASK(__NR_mprotect, 2, PROT_READ|PROT_WRITE|PROT_NONE), #endif . #
/ *允许如果系统调用参数只包含值掩码* / # define SC_ALLOW_ARG_MASK (_nr, _arg_nr _arg_mask) \ BPF_JUMP (BPF_JMP + BPF_JEQ + BPF_K, (_nr), 0, 8), \ / *负载,面具和测试系统调用参数,低字* / \ BPF_STMT (BPF_LD + BPF_W + BPF_ABS \ offsetof (struct seccomp_data, arg游戏[(_arg_nr)]) + ARG_LO_OFFSET), \ BPF_STMT (BPF_ALU + BPF_AND + BPF_K, ~ ((_arg_mask) & 0 xffffffff)), \ BPF_JUMP (BPF_JMP + BPF_JEQ + BPF_K, 0, 0, 4), \ / *负载,面具和测试系统调用参数,high word */ \ BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_HI_OFFSET), \ BPF_STMT(BPF_ALU+BPF_AND+BPF_K, \ ~(((uint32_t)((uint64_t)(_arg_mask) >> 32)) & 0xFFFFFFFF))), \ BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0,0,1), \ BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), \ /* reload syscall number;所有规则都期望累加器*/ \ BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ offsetof(struct seccomp_data, nr))
的第二个参数验证mmap是下列之一:
PROT_READ | PROT_WRITE分别| PROT_NONE
这可以防止攻击者创建可执行内存段(PROT_EXEC),这使得攻击者更难以绕过部/ NX利用缓解。
此外,大多数shellcode使用open ()系统调用打开新的文件描述符,但seccomp过滤器拒绝它,有效地减少了许多本地特权升级的可能性,因为攻击者必须找到已经打开的文件描述符才能利用特权升级。
macOS沙箱
Seccomp是Linux内核特性,这意味着它不存在于运行macOS的机器上
我们来检查一下sandbox-darwin.c文件(Darwin是macOS的核心Unix系统):
Void ssh_sandbox_child(struct ssh_sandbox *box) {char *errmsg;Struct rlimit rl_zero;debug3("%s:启动达尔文沙箱",__func__);if (sandbox_init(kSBXProfilePureComputation, SANDBOX_NAMED, &errmsg) == -1) fatal("%s: sandbox_init: %s", __func__, errmsg);/* * kSBXProfilePureComputation仍然允许套接字,所以*我们必须使用rlimit禁用这些。* / rl_zero。Rlim_cur = rl_zero。Rlim_max = 0;if (setrlimit(RLIMIT_FSIZE, &rl_zero) == -1) fatal("%s: setrlimit(RLIMIT_FSIZE, {0,0}): %s", __func__, strerror(errno));if (setrlimit(RLIMIT_NOFILE, &rl_zero) == -1) fatal("%s: setrlimit(RLIMIT_NOFILE, {0,0}): %s", __func__, strerror(errno));if (setrlimit(RLIMIT_NPROC, &rl_zero) == -1) fatal("%s: setrlimit(RLIMIT_NPROC, {0,0}): %s", __func__, strerror(errno)); }
OS X有一个叫做安全带的特性——它自己的沙箱内核扩展。
有5个文档配置文件:
kSBXProfileNoInternet—禁止TCP/IP组网。
kSBXProfileNoNetwork—禁止所有基于套接字的组网。
kSBXProfileNoWrite—禁止写文件系统。
kSBXProfileNoWriteExceptTemporary—文件系统写操作仅限于临时文件夹/var/tmp和confstr(3)配置变量_CS_DARWIN_USER_TEMP_DIR指定的文件夹。
kSBXProfilePureComputation—禁止所有操作系统服务。
kSBXProfilePureComputation是最具限制性的模式。
当应用程序使用此配置文件启动时,它仅限于访问以下资源:2022世界杯阿根廷预选赛赛程
- 应用程序自己的代码和资源2022世界杯阿根廷预选赛赛程
- 计算所需的系统库和框架
- 共享内存
- Unix信号
- 网络环回接口
应用程序无法访问文件系统、其他网络接口、用户数据、硬件外设或任何其他可能用于修改系统或干扰其他应用程序的资源。2022世界杯阿根廷预选赛赛程
我们可以看到OpenSSH的沙箱使用了这个配置文件——本质上限制了所有的操作系统服务,从而最小化了OpenSSH的攻击面。
OpenBSD沙箱
OpenBSD操作系统还具有特定于它的沙盒样式,不能在其他平台上使用。
第一个是systrace,它通过对系统调用实施访问策略来监视和控制应用程序对系统的访问,这很像一个原语seccomp。
它使用了一个伪装置,/dev/systrace,它允许用户进程控制的行为systrace通过一个ioctl接口。
它被弃用,而赞成承诺(最初命名驯服),该片于2015年上映。
它有一个名为Promises的概念,它是进程为了执行其操作可以请求的一组权限。
承诺是进程对系统做出的声明,它将只使用特定的系统调用列表,从而限制其系统调用访问预定义的一组操作。
进程可以使用pledge()系统调用请求promise,每个promise都由一个字符串标识,该字符串表示进程允许使用的特定类型的系统调用。
关于承诺的一些例子包括rpath(允许进程读取自己的可执行文件和链接的共享库)和inet(允许网络通信)。
承诺无法过滤文件系统路径或Internet地址。例如,如果启用像inet,您的进程将能够与任何互联网地址对话。
我们将检查sandbox-pledge.c文件:
void ssh_sandbox_child(struct ssh_sandbox *box) {if (pledge("stdio", NULL) == -1) fatal_f("pledge()");}
我们可以看到OpenSSH使用了promise工作室。
此承诺授予对标准输入/输出、线程和良性系统调用的访问权。
一些主要的显式允许的系统调用[4]:
关闭释放先前通过使用。命令打开文件获得的文件描述符开放或者打开系统调用。
madvise-用于通知内核内存范围的预期使用情况。允许程序与操作系统通信它计划如何使用内存的特定区域,这可以帮助内核优化其对该内存的管理。
mmap-用于将内存区域映射到调用进程的地址空间。
mprotect-用于修改一系列内存页面的访问权限。
munmap-用于删除先前使用mmap系统调用建立的内存映射。当程序不再需要访问内存映射时,它应该调用munmap系统调用来释放相关的内存并删除映射。
管-用于在两个相关进程之间创建进程间通信通道或“管道”。
读-用于从文件或输入流中读取数据。
recv-用于从已连接的套接字接收数据。
发送-用于通过已连接的套接字发送数据。
写-用于将数据从缓冲区写入文件描述符。
这个过滤器,很像第二个过滤器,是非常严格的,拒绝任何PROT_EXEC映射或调用open ()。
结论-不要乱用默认值!
OpenSSH的安全机制,即特权分离和沙盒,为增强OpenSSH服务器的安全性提供了一个健壮、有效的解决方案。这些机制协同工作,通过隔离和限制对关键系统资源的访问来最小化攻击面并防止特权升级攻击。2022世界杯阿根廷预选赛赛程
这些安全机制的一个故障点是用户配置。
OpenSSH将默认启用所有限制(在受支持的系统上),但用户也可以选择部分启用或完全禁用限制机制:
- 只使用权限分离(
UsePrivilegeSeparation = yes),没有沙盒。这可能允许网络攻击者通过特权升级完全破坏系统。 - 或者禁用沙箱和特权分离(
UsePrivilegeSeparation =没有)。
这将导致一个不安全的系统。一旦在预认证阶段完成代码执行,攻击者就可能完全危及系统。
通过在单独的无特权进程中运行SSH守护进程的部分,并将其限制在沙箱环境中,OpenSSH可以防止攻击者利用SSH服务器中的漏洞(如CVE-2023-25136 Double-Free)获得对系统的特权访问。
根据上面的研究,可以肯定地说,此时组织可以放心地部署OpenSSH,因为知道代码执行和特权升级攻击的风险已经大大降低。但是,请确保使用最新版本的OpenSSH和所有最新的安全发现。
与JFrog安全研究保持同步
安全研究团队的发现和研究对提高JFrog软件供应链平台的软件安全能力起着重要的作用。这以增强的CVE元数据和针对开发人员、DevOps和安全团队的修复建议的形式表现出来JFrog x光漏洞数据库。还有JFrog Xray使用的新的安全扫描功能。
关注JFrog安全研究团队的最新发现和技术更新研究网站,安全研究博客文章并在推特上@JFrogSecurity。
附录A - OpenSSH沙箱完整的系统调用列表
[1]Linux沙箱-无声拒绝系统调用:Lstat、lstat64、fstat、fstat64、fstat64、open、openat、newfstatat、stat、stat64、shmget、shmat、shmdt、ipc、statx
[2]Linux Sandbox -明确允许检查参数的系统调用:Mmap, mmap2, mprotect, socketcall, ioctl(仅适用于s390架构)
[3]Linux Sandbox -显式允许不检查参数的系统调用;Brk、clock_gettime、clock_gettime64、close、exit、exit_group、futext、futext_time64、geteuid、geteuid32、getpgid、getpid、getrandom、gettid、gettimeofday、getuid、getuid32、madvise、mremap、munmap、nanosleep、clock_nanosleep、clock_nanosleep_time64、clock_gettime64、newselect、ppoll、ppoll_time64、poll、pselect6、pselect6_time64、read、rt_sigprocmask、select、shutdown、sigprocmask、time、write
[4]OpenBSD沙箱-显式允许的系统调用:exit_group, close, dup, dup2, dup3, fchdir, fstat, fsync, fdatasync, ftruncate, getdents, getegid, getrandom, geteuid, getgid, getgroups, getitimer, getpgid, getpgrp, getpid, getppid, getresgid, getresuid, getrlimit, getsid, wait4, gettimeofday, getuid, lseek, madvise, brk, arch_prctl, uname, set_tid_address, clock_getres, clock_gettime, clock_nanosleep, mmap (PROT_EXEC和怪异标志不允许),mprotect (PROT_EXEC不允许),msync, munmap, nanosleep, pipe, pipe2, read, readv,pread, recv, poll, recvfrom, preadv, write, writev, pwrite, pwritev, select, send, sendto(仅当addr为null时),setitimer, shutdown, sigaction(禁止SIGSYS), sigaltstack, sigprocmask, sigreturn, sigsuspend, umask, socketpair, ioctl(FIONREAD), ioctl(FIONBIO), ioctl(FIOCLEX), ioctl(FIONCLEX), fcntl(F_GETFD), fcntl(F_SETFD), fcntl(F_GETFL), fcntl(F_SETFL)), fcntl(F_SETFL)