这篇博文对 OpenSSH 的攻击面和安全措施进行了深入分析。
-
OpenSSH 如何实现权限分离? -
OpenSSH 权限分离 - 深入分析 -
什么是 OpenSSH 沙盒? -
OpenSSH 沙箱——深入分析 -
结论——不要扰乱默认设置! -
了解 JFrog 安全研究的最新动态 -
附录 A – OpenSSH 沙盒完整系统调用列表
OpenSSH 如何实现权限分离?
OpenSSH的权限分离机制从2002年3月就已存在,实施至今已有20多年了。
该功能旨在通过限制 SSH 服务器进程的权限并将其与用户的身份验证和会话进程分离来增强 SSH 服务器的安全性。
权限分离的目标是确保即使 OpenSSH 的其他部分以 root 权限运行,预身份验证攻击也不会危及 root 帐户。
在引入权限分离之前,OpenSSH 服务器进程必须以提升的权限运行才能访问身份验证和会话管理所需的系统资源。这种提升的权限级别使服务器进程成为攻击者的高价值目标,攻击者可以通过利用服务器进程中的任何漏洞来完全控制系统。
OpenSSH 服务器进程 ( sshd
) 中的任何远程代码执行漏洞(如果在身份验证之前发生)都可能导致立即获得远程根权限,从而让攻击者完全控制运行 OpenSSH 的机器。
权限分离机制
通过权限分离,OpenSSH 服务器进程被分成两个独立的进程:一个进程以提升的权限运行来处理系统级任务(例如网络 I/O);另一个进程以降低的权限运行来处理用户身份验证。
当用户与启用了权限分离的 OpenSSH 服务器发起 SSH 连接时,服务器会生成两个独立的进程来处理传入的连接。
第一个进程称为特权进程,以提升的权限运行,负责处理网络 I/O,例如监听传入连接、管理网络套接字和管理伪终端。
第二个进程称为非特权进程,以较低的特权运行,负责处理用户身份验证。它与特权进程隔离,对系统资源(如文件系统和网络接口)的访问权限有限。
OpenSSH 权限分离 - 深入分析
特权分离使用两个进程:具有特权的父进程监视非特权子进程的进度。
子进程没有特权。这是通过将其 uid/gid 更改为没有sshd
登录 shell 的未使用用户(通常是 )并限制其文件系统访问来实现chroot()
的/var/empty
。子进程是处理网络数据的唯一进程。
父进程判断子进程是否成功执行了身份验证。
特权进程和非特权进程之间的通信是通过管道实现的。共享内存存储无法以其他方式导出的状态,子进程必须询问特权父进程以确定身份验证是否成功。
如果子进程被破坏并且认为远程用户已经通过身份验证,则只有父进程也做出相同决定才会授予访问权限。
在身份验证期间,子进程与用户和身份验证代理进行通信以获取身份验证所需的凭据。一旦子进程获得凭据,它就会将其发送给父进程进行验证。
然后,父进程使用子进程提供的凭据对用户进行实际身份验证。如果身份验证成功,父进程将向子进程发送一条消息,表明身份验证已成功。如果身份验证失败,父进程将向子进程发送一条消息,表明身份验证失败。
父进程和子进程之间的通信是使用 Unix 域套接字完成的,这是一种进程间通信 (IPC) 机制。
父进程和子进程各自拥有自己的Unix域套接字,并且它们使用这些套接字相互通信。
通过在父进程中执行身份验证,OpenSSH 能够确保敏感的身份验证数据永远不会离开特权进程,从而提供额外的安全保障。此外,通过使用 IPC 在父进程和子进程之间进行通信,OpenSSH 能够保持两个进程之间的分离,并防止子进程干扰父进程执行的关键操作。
在预认证阶段,sshd
将chroot()
并将/var/empty
其权限更改为sshd
用户及其主要组。sshd
是一个被锁定的伪账户,未被其他守护进程使用,并且不包含有效 shell。
给出以下流程列表:
唯一标识 | PID | 局部局部性病变 | 碳 | 时间 | 终端电话 | 时间 | 命令 |
---|---|---|---|---|---|---|---|
根 | 957 | 9 | 0 | 09:14 | ? | 00:00:00 | /usr/sbin/sshd -D [listener] 0(共 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
。默认情况下,密钥设置为最严格的sandbox
设置(即使未指定密钥),这意味着预身份验证非特权进程将受到额外限制,我们将在下一节中介绍。此配置文件的默认位置是/etc/sshd_config
。
示例sshd_config
配置文件:
# Connection
Port 22
Protocol 2
UseDNS no
Compression no
# Authentication:
PubkeyAuthentication yes
PermitEmptyPasswords no
UsePAM yes
ChallengeResponseAuthentication yes
LoginGraceTime 60
UsePrivilegeSeparation sandbox # The relevant Privilege Separation config key
…
什么是 OpenSSH 沙盒?
OpenSSH 预认证沙盒是 OpenSSH 5.9 版中首次引入的安全机制,旨在防止攻击者在预认证阶段利用漏洞后完全破坏系统。它创建了一个受限环境,限制了 SSH 连接认证阶段潜在漏洞的范围。
它通过使用多种内核安全机制(例如 seccomp 过滤和命名空间隔离)启动隔离环境来运行 - 本质上将其功能限制为仅限少数预先批准的系统调用。
当用户与启用了沙盒功能的 OpenSSH 服务器建立 SSH 连接时,服务器会生成一个在受限环境(也称为沙盒)中运行的新进程。沙盒进程具有有限的权限,并且对系统资源(包括文件系统和网络接口)的访问权限受到限制。
OpenSSH 沙箱——深入分析
OpenSSH 有 7 种不同的沙盒风格,由您为其编译它的平台及其内核功能决定。
所有不同的沙盒风格都围绕系统调用限制的概念 - 这意味着沙盒进程不能使用系统的大多数服务,比如打开文件、通过网络通信等。
Linux 沙盒
OpenSSH 配置步骤(在编译步骤之前运行)通过检查内核是否配置了该SECCOMP_MODE_FILTER
选项来检查 seccomp 兼容性。
配置时的样子如下:
checking whether SECCOMP_MODE_FILTER is declared... yeschecking kernel for seccomp_filter support... yes
检查 Ubuntu 22.04 LTS sshd 守护进程二进制文件的沙盒类型,我们发现它使用了 seccomp 过滤器,因此我们将重点关注这一点:
> strings ./sshd | grep preparing%s: preparing seccomp filter sandbox
Seccomp 代表安全计算模式,自 2005 年发布的 2.6.12 版本以来一直是 Linux 内核的一个功能。它用于过滤和限制对用户空间进程可用的系统调用,从而减少暴露的内核表面,从而限制权限提升的攻击面。
这是通过仅包含应用程序正常运行所需的基本系统调用来实现的。与套接字过滤器一样,过滤器也表示为伯克利数据包过滤器 (BPF),不同之处在于操作的数据与正在进行的系统调用相关:系统调用号和系统调用参数。
我们检查该sandbox-seccomp-filter.c
文件,发现 Seccomp 过滤器附加在程序的ssh_sandbox_child()
函数中,OpenSSH 使用的 seccomp 配置文件是 -
/* Syscall filtering set for preauth. */
static const struct sock_filter preauth_insns[] = {
……………………
/* Syscalls to non-fatally deny */
#ifdef __NR_lstat
SC_DENY(__NR_lstat, EACCES),
#endif
#ifdef __NR_lstat64
SC_DENY(__NR_lstat64, EACCES),
#endif
#ifdef __NR_fstat
SC_DENY(__NR_fstat, EACCES),
#endif
……………………
/* Syscalls to permit */
#ifdef __NR_brk
SC_ALLOW(__NR_brk),
#endif
#ifdef __NR_clock_gettime
SC_ALLOW(__NR_clock_gettime),
#endif
#ifdef __NR_clock_gettime64
SC_ALLOW(__NR_clock_gettime64),
#endif
#ifdef __NR_close
SC_ALLOW(__NR_close),
#endif
#ifdef __NR_exit
SC_ALLOW(__NR_exit),
#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
/* Default 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 sandbox */
#define SECCOMP_FILTER_FAIL SECCOMP_RET_KILL
SECCOMP_RET_KILL 会导致进程立即退出而不执行系统调用。这是我们在上一篇博文中尝试触发漏洞时得到的结果,seccomp 沙盒会失败并退出进程,因为 writev() 未定义,并自动导致 SECCOMP_RET_KILL。
一些主要的被静默拒绝的系统调用[1]:
open
– 用于打开文件并获取可用于读取或写入文件的文件描述符。删除此系统调用可大大减少攻击面,因为攻击者将无法打开任意文件。
openat
– 类似于打开,但相对于给定的目录工作。
一些主要的明确允许检查参数的系统调用[2]:
mmap
– 用于将内存区域映射到调用进程的地址空间。拒绝某些参数将阻止攻击者创建危险的内存映射,并阻止某些 ROP shellcode(请参阅下一节)。
mprotect
– 用于修改一定范围的内存页面的访问权限。
一些主要的明确允许的、没有参数检查的系统调用[3]:
close``open
– 用于释放先前通过使用或openat
系统调用打开文件获取的文件描述符。
madvise
– 用于向内核提供有关内存范围的预期用途的建议。允许程序向操作系统传达其计划如何使用特定内存区域的信息,这可以帮助内核优化对该内存的管理。
mremap
– 用于改变现有内存映射的大小或位置。
munmap
– 用于删除之前使用 mmap 系统调用建立的内存映射。当程序不再需要访问内存映射时,它应该调用系统调用munmap
来释放相关内存并删除映射。
write
– 用于将数据从缓冲区写入文件描述符。
我们首先深入研究 mmap 的限制:
#ifdef __NR_mprotect
SC_ALLOW_ARG_MASK(__NR_mprotect, 2, PROT_READ|PROT_WRITE|PROT_NONE),
#endif
/* Allow if syscall argument contains only values in mask */
#define SC_ALLOW_ARG_MASK(_nr, _arg_nr, _arg_mask)
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (_nr), 0, 8),
/* load, mask and test syscall argument, low word */
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,
offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_LO_OFFSET),
BPF_STMT(BPF_ALU+BPF_AND+BPF_K, ~((_arg_mask) & 0xFFFFFFFF)),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0, 0, 4),
/* load, mask and test syscall argument, 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; all rules expect it in accumulator */
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,
offsetof(struct seccomp_data, nr))
这验证了第二个参数mmap
是下列之一:
PROT_READ|PROT_WRITE|PROT_NONE
这可以防止攻击者创建可执行内存段(PROT_EXEC
),从而使攻击者更难绕过DEP/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: starting Darwin sandbox", __func__);
if (sandbox_init(kSBXProfilePureComputation, SANDBOX_NAMED,
&errmsg) == -1)
fatal("%s: sandbox_init: %s", __func__, errmsg);
/*
* The kSBXProfilePureComputation still allows sockets, so
* we must disable these using 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 有一项称为 Seatbelt 的功能 - 它自己的沙盒内核扩展。
共有 5 个已记录的配置文件:
kSBXProfileNoInternet
– 禁止 TCP/IP 网络。
kSBXProfileNoNetwork
– 禁止所有基于套接字的网络。
kSBXProfileNoWrite
– 禁止文件系统写入。
kSBXProfileNoWriteExceptTemporary
– 文件系统写入仅限于临时文件夹 /var/tmp 和 confstr(3) 配置变量 _CS_DARWIN_USER_TEMP_DIR 指定的文件夹。
kSBXProfilePureComputation
– 禁止所有操作系统服务。
kSBXProfilePureComputation
是最严格的模式。
使用此配置文件启动应用程序时,该应用程序只能访问以下资源:
-
应用程序自身的代码和资源 -
计算所需的系统库和框架 -
共享内存 -
Unix 信号 -
网络环回接口
该应用程序无法访问文件系统、其他网络接口、用户数据、硬件外围设备或任何其他可能用于修改系统或干扰其他应用程序的资源。
我们可以看到 OpenSSH 的沙盒使用了该配置文件 - 本质上限制了所有操作系统服务,从而最大限度地减少了 OpenSSH 的攻击面。
OpenBSD 沙盒
OpenBSD 操作系统还具有其特有的沙盒风格,不能在其他平台上使用。
第一个是systrace
,它通过强制执行系统调用的访问策略来监视和控制应用程序对系统的访问,就像原始的一样seccomp
。
它使用伪设备,,/dev/systrace
允许用户进程systrace
通过ioctl
接口控制的行为。
它已被弃用,取而代之的是2015 年发布的pledge
(最初名为)。tame
它有一个名为Promises的概念,它是进程为执行其操作可以请求的权限集。
承诺是进程向系统声明它将仅使用特定的系统调用列表,从而将其系统调用访问限制在预定义的一组操作上。
进程可以使用 pledge() 系统调用来请求承诺,每个承诺都由一个字符串标识,该字符串代表该进程被允许使用的特定类别的系统调用。
承诺的一些例子包括rpath
(允许进程读取其自己的可执行文件和链接的共享库)和inet
(允许网络通信)。
pledge
无法过滤文件系统路径或互联网地址。例如,如果您启用 之类的类别inet
,您的进程将能够与任何互联网地址通信。
我们将检查该sandbox-pledge.c
文件:
void
ssh_sandbox_child(struct ssh_sandbox *box)
{
if (pledge("stdio", NULL) == -1)
fatal_f("pledge()");
}
我们可以看到 OpenSSH 使用了 promise stdio。
此承诺授予对标准输入/输出、线程和良性系统调用的访问权限。
一些明确允许的主要系统调用[4]:
close``open
– 用于释放先前通过使用或 openat 系统调用打开文件获取的文件描述符。
madvise
– 用于向内核提供有关内存范围的预期用途的建议。允许程序向操作系统传达其计划如何使用特定内存区域的信息,这可以帮助内核优化对该内存的管理。
mmap
– 用于将内存区域映射到调用进程的地址空间。
mprotect
– 用于修改一定范围的内存页面的访问权限。
munmap
-用于删除之前使用 mmap 系统调用建立的内存映射。当程序不再需要访问内存映射时,它应该调用 munmap 系统调用来释放相关内存并删除映射。
pipe
– 用于在两个相关进程之间创建进程间通信通道或“管道”。
read
– 用于从文件或输入流读取数据。
recv
– 用于从连接的套接字接收数据。
send
– 用于通过已连接的套接字发送数据。
write
– 用于将数据从缓冲区写入文件描述符。
此过滤器与 seccomp 过滤器非常相似,限制性非常强,会拒绝任何PROT_EXEC
映射或调用open()
。
结论——不要扰乱默认设置!
OpenSSH 的安全机制,即Privilege Separation
和Sandboxing
,为增强 OpenSSH 服务器的安全性提供了强大而有效的解决方案。这些机制协同工作,通过隔离和限制对关键系统资源的访问,最大限度地减少攻击面并防止特权升级攻击。
这些安全机制的一个失败点是用户配置。
OpenSSH 将默认启用所有限制(在受支持的系统上),但用户也可以选择部分启用限制机制或完全禁用它们:
-
仅使用特权分离( UsePrivilegeSeparation=yes
)而不使用沙盒。这可能允许网络攻击者通过特权升级完全破坏系统。 -
或者禁用沙盒和权限分离( UsePrivilegeSeparation=no
)。
这会导致系统不安全,一旦在预认证阶段实现代码执行,攻击者就可能完全攻陷系统。
通过在单独的非特权进程中运行 SSH 守护程序的部分内容并将其限制在沙盒环境中,OpenSSH 可以防止攻击者利用 SSH 服务器中的漏洞(如 CVE-2023-25136 Double-Free)获取系统的特权访问权限。
根据上述研究,可以肯定地说,目前,组织可以放心部署 OpenSSH,因为他们知道代码执行和权限提升攻击的风险已大大降低。但是,请务必及时了解最新版本的 OpenSSH 和所有最新的安全发现。
原文始发于微信公众号(红云谈安全):检查 OpenSSH 沙盒和权限分离 - 攻击面分析
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论