在两章中,您将学习有关进程凭证和功能的概念和实践。 除了对 Linux 中的应用开发具有实际重要性之外,本章还从本质上更深入地探讨了一个经常被忽视但极其重要的方面:安全性。
我们将这一关键领域的内容分为两大部分,每一部分都是本书的一个章节:
- 在第 7 章,*处理凭证中,*详细讨论了传统风格的 Unix 权限模型,并展示了以 root 权限运行程序但不需要 root 密码的技术。
- 在本章,进程功能,**现代方法,POSIX 功能模型中,进行了更详细的讨论。
我们将试图清楚地向读者展示,尽管了解传统机制及其工作方式很重要,但就安全性而言,这是一个典型的薄弱环节。 无论你怎么看,安全都是最重要的,尤其是在这些天;Linux 在各种设备上运行的出现-从微型物联网和嵌入式设备到移动设备、台式机、服务器和超级计算平台-使安全成为所有利益相关者的关键关注点。 因此,在开发软件时应该使用现代功能方法。
在本章中,我们将更详细地介绍*现代方法-*POSIX 功能模型。 我们将讨论它到底是什么,以及它如何提供安全性和健壮性。 读者将了解以下内容:
- 现代 POSIX 功能模型到底是什么
- 为什么它优于较旧的(传统)Unix 权限模型
- 如何在 Linux 上使用功能
- 将功能嵌入到进程或二进制可执行文件中
- 安全提示
在此过程中,我们将使用代码示例,这将允许您尝试其中的一些功能,以便您可以更好地了解它们。
考虑这个(虚构的)场景:Vidya 正在为 Alan 和他的团队开发一个 Linux 应用。 她正在开发一个组件,该组件可以捕获网络数据包并将其保存到一个文件中(供以后分析)。 该程序名为PackCap。 但是,要成功捕获网络数据包,Packcap 必须以root权限运行。 现在,Vidya 明白以用户根身份运行应用不是一个好的安全实践;不仅如此,她知道客户不会接受这样的说法:哦,它不起作用吗? 您必须以 root 身份或通过 sudo 运行它。 通过 sudo(8)运行它听起来可能是合理的,但是,当您停下来思考时,这意味着 Alan 团队的每个成员都必须获得root密码,这简直是不可接受的。
那么,她是如何解决这个问题的呢? 她突然想到了答案:使Packcap二进制可执行文件成为setuid-根文件;这样,当它启动时,进程将以root权限运行,因此不需要 root 登录/密码或 sudo。 听起来棒极了。
这就是 setuid-root 方法*,这正是解决上面简要描述的问题的传统方式。 那么,今天发生了什么变化(好吧,几年过去了)? 简而言之:对黑客的安全担忧。 现实是这样的:所有现实世界中不平凡的程序都有缺陷(Bug)--隐藏的、潜伏的、未被发现的,也许,但非常多。 现代现实世界软件项目的巨大范围和复杂性使这成为一个不幸的现实。 某些错误会导致漏洞“泄漏”到软件产品中;这正是黑客希望利用漏洞的原因。 众所周知但令人畏惧的缓冲区溢出*(BoF)攻击是基于几个频繁使用的库 API 中的软件漏洞! (我们强烈推荐阅读 David Wheeler 的书Secure Programming**How to-Creating Secure Software-有关 GitHub 存储库的进一步阅读部分。)
*At the code level, security issues are bugs; once fixed, the issue disappears. (See a link to Linux's comments on this in the Further reading section on the GitHub repository.)
那有什么意义呢? 简单地说,问题是:您交付给客户的 setuid-root 程序(PackCap)中完全有可能嵌入了不幸的、到目前为止未知的软件漏洞,黑客可能会发现并利用这些漏洞(是的,有完整的工作描述-白帽黑客或五次测试。 )
如果入侵的进程以正常权限(非超级用户)运行,则损害至少限于该用户帐户,并且不会进一步破坏。 但是,如果进程以超级用户权限运行,并且攻击成功,黑客很可能最终在系统上获得根外壳*。 系统现在被攻破了--任何事情都可能发生(机密可能被窃取,后门和 rootkit 被安装,DoS 攻击变得微不足道。)*
不过,这不仅仅是安全问题:通过限制特权,您还可以获得损害控制方面的好处;错误和崩溃将造成有限的损害-包含的情况比以前要好得多。
那么,回到我们虚构的 Packcap 示例应用,我们如何在没有 root 权限(不允许 root 登录、setuid-root*、*或 sudo(8))的情况下运行进程(看起来需要 root 权限),并让它正确执行任务?
进入 POSIX 功能模型:在此模型中,不是以 root(或其他)用户身份授予进程一揽子访问权限,而是有一种方法可以将特定功能同时嵌入到进程和/或二进制文件中。 Linux 内核很早就支持 POSIX 功能模型-2.2 版 Linux 内核(在撰写本文时,我们现在属于 4.x 内核系列)。 从实用的角度来看,从 Linux 内核版本 2.6.24(2008 年 1 月发布)开始,我们描述的特性就可以使用了。
简而言之,它是这样工作的:每个进程--实际上是每个线程--作为其操作系统元数据的一部分,都包含一个位掩码。 这些被称为能力位或能力集,因为每个位代表一个能力。** 通过仔细设置和清除位,内核(以及用户空间,如果它有能力)因此可以在每个线程的基础上设置细粒度权限(我们将在后面的第 14 章,使用 PThreadsPart I-Essentials详细介绍多线程),目前,将术语线程视为可与进程互换
More realistically, and as we shall see next, the kernel maintains several capability sets (capsets) per thread alive; each capset consists of an array of two 32-bit unsigned values.
例如,有一个名为CAP_DAC_OVERRIDE
的能力位;它通常会被清除(0)。 如果设置,则该进程将绕过内核的所有文件权限检查-任何检查:读取、写入和执行! (这称为DAC:自主访问控制。 )
现在再看几个功能位的示例会很有用(完整的列表可以在手册页上找到,此处是功能(7):https://linux.die.net/man/7/capabilities)。 下面是一些代码片段:
[...]
CAP_CHOWN
Make arbitrary changes to file UIDs and GIDs (see chown(2)).
CAP_DAC_OVERRIDE
Bypass file read, write, and execute permission checks. (DAC is an abbreviation of "discretionary access control".)
[...]
CAP_NET_ADMIN
Perform various network-related operations:
* interface configuration;
* administration of IP firewall, masquerading, and accounting;
* modify routing tables;
[...]
CAP_NET_RAW
* Use RAW and PACKET sockets;
* bind to any address for transparent proxying.
[...]
CAP_SETUID
* Make arbitrary manipulations of process UIDs (setuid(2),
setreuid(2), setresuid(2), setfsuid(2));
[...]
CAP_SYS_ADMIN
Note: this capability is overloaded; see Notes to kernel
developers, below.
* Perform a range of system administration operations
including: quotactl(2), mount(2), umount(2), swapon(2),
setdomainname(2);
* perform privileged syslog(2) operations (since Linux 2.6.37,
CAP_SYSLOG should be used to permit such operations);
* perform VM86_REQUEST_IRQ vm86(2) command;
* perform IPC_SET and IPC_RMID operations on arbitrary
System V IPC objects;
* override RLIMIT_NPROC resource limit;
* perform operations on trusted and security Extended
Attributes (see xattr(7));
* use lookup_dcookie(2);
*<< a lot more follows >>*
[...]
实际上,Capability 模型提供了细粒度的权限;这是一种将根用户(过于)巨大的权力分割成不同的可管理部分的方法。
因此,要理解我们虚构的 Packcap 示例上下文中的显著好处,请考虑以下内容:使用传统的 Unix 权限模型,发布二进制文件充其量是一个 setuid-root 二进制可执行文件;该进程将以 root 权限运行。 在最好的情况下,没有 bug,没有安全问题(或者,如果有,也没有被发现),一切都很顺利--幸运的是。 但是,我们不相信运气,对吧?“(用李查德的主人公杰克·里彻的话说,”抱最好的希望,做最坏的打算“)。 在最坏的情况下,代码中潜伏着可利用的漏洞,黑客会不知疲倦地工作,直到他们发现并利用这些漏洞。 整个系统都可能被破坏。
另一方面,使用现代 POSIX 功能模型,Packcap 二进制可执行文件根本不需要 setuid,更不用说 setuid-root 了;该进程将以正常权限运行。 这项工作仍然可以完成,因为我们为该工作(在本例中为网络数据包捕获)嵌入了功能,而绝对没有嵌入任何其他内容。 即使代码中潜伏着可利用的漏洞,黑客可能也不会有那么大的动力去发现和利用它们;原因很简单,因为即使他们确实设法获得了访问权限(例如,任意代码执行赏金),所有可以利用的都是运行进程的非特权用户的帐户。 这对黑客来说是令人泄气的(嗯,这是个笑话,但其中蕴含着根深蒂固的真相)。
Think about it: the Linux capabilities model is one way to implement a well-accepted security practice: the Principle of Least Privilege (PoLP): Each module in a product (or project) must have access only to the information and resources necessary for its legitimate work, and nothing more.
Linux 功能是一个相当复杂的话题。 出于本书的目的,我们将深入探讨系统应用开发人员从讨论中获利所需的深度。 要获取完整的详细信息,请查看此处有关功能(7)的手册页:http://man7.org/linux/man-pages/man7/capabilities.7.html以及此处有关凭证的内核文档:https://github.com/torvalds/linux/blob/master/Documentation/security/credentials.rst
能力位掩码(s)通常被称为能力集-我们将该术语缩写为capset。
要使用 POSIX 功能模型的强大功能,首先,操作系统本身必须为其提供“生命支持”;完全支持意味着以下几点:
- 每当进程或线程尝试执行某些操作时,内核都能够检查是否允许该线程执行此操作(通过检查线程的有效 Capset 中是否设置了适当的位-请参见下一节)。
- 必须提供系统调用(通常还有包装库 API),以便线程可以查询和设置其 Capset。
- Linux 内核文件系统代码必须具有这样一种功能,即可以将功能嵌入(或附加)到二进制可执行文件中(这样,当文件“运行”时,进程就会获得这些功能)。
现代的 Linux(尤其是内核版本 2.6.24 以后的版本)支持所有这三个版本,因此完全支持能力模型。
要了解更多细节,我们需要一种快速的方法来“查看”内核并检索信息;Linux 内核的proc 文件系统(通常缩写为procfs)就提供了这个特性(以及更多)。
Procfs is a pseudo-filesystem typically mounted on /proc. Exploring procfs to learn more about Linux is a great idea; do check out some links in the Further reading section on the GitHub repository.
这里,我们只关注手头的任务:为了了解细节,procfs 公开了一个名为/proc/self
的目录(它指的是当前进程的上下文,有点类似于 OOP 中的this指针);在它下面,一个名为status的伪文件显示了有关相关进程(或线程)的有趣细节。 进程的 Capset 被视为“Cap*”,因此我们只对此模式进行 grep。 在下一段代码中,我们将在一个常规的非特权进程(grep本身通过self目录)以及一个特权(根)进程(systemd/init PID 1)上执行此操作,以查看不同之处:
进程/线程上限:常规进程(如 grep):
$ grep -i cap /proc/self/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
进程/线程上限:特权(根)进程(如 systemd/init pid 1):
$ grep -i cap /proc/1/status
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
$
在表格中列举的:
| 线程能力集(Capset) | 非特权任务的典型值 | 特权任务的典型值 |
| CapInh(继承) | 0x0000000000000000
| 0x0000000000000000
|
| CapPrm(允许) | 0x0000000000000000
| 0x0000003fffffffff
|
| CapEff(有效) | 0x0000000000000000
| 0x0000003fffffffff
|
| CapBnd(有界) | 0x0000003fffffffff
| 0x0000003fffffffff
|
| CapAmb(环境) | 0x0000000000000000
| 0x0000000000000000
|
(此表描述了 x86_64 上的 Fedora 27/Ubuntu 17.10 Linux 的输出)。
一般而言,有两种类型的功能集:
- 线程功能集
- 文件功能集
在线程上限中,每个线程实际上有几种类型。
Linux 每线程功能集:
- **允许的(PRM):**线程有效功能的总体限制超集。 如果一种能力被丢弃,它将永远无法重新获得。
- 可继承(Inh):这里的继承是指通过exec吸收 capset 属性。 当一个进程执行另一个进程时,Capset 会发生什么情况? (有关执行人员的详细信息将在后面的章节中介绍。 现在,只要说如果 bash 执行 vi,那么我们就称 bash 为前身,vi 为继任者)。 后续进程是否会继承前置进程的上限? 嗯,是的,是可继承的上限,也就是。 从上一表中,我们可以看到,对于非特权进程,继承的 Capset 全为零,这意味着在 EXEC 操作中没有继承任何功能。 因此,如果一个进程想要执行另一个进程,并且该(后续)进程必须以提升的权限运行,那么它应该使用环境功能。
- **有效(EFF):**这些是内核在检查给定线程的权限时实际使用的功能。
- 环境(Amb):(从 Linux 4.3 开始)。 这些是 EXEC 操作中继承的功能。 位必须在允许的和可继承的大写中都存在(设置为 1)-只有这样它才能是“环境”的。 换句话说,如果从 PRM 或 INH 中清除了能力,则也会在 AMB 中清除该能力。 如果执行set[u|g]id程序或具有文件能力的程序(如我们将看到的),则环境集将被清除。 通常,在执行时,环境上限被添加到 PRM 并分配给(后续进程的)EFF。
- **绑定(BND):**此上限是一种限制在执行期间赋予进程的能力的方式。 它的效果是:
- 当进程执行另一个进程时,允许集是原始允许和有界的 Capset 的 AND:prm=prm和bnd。 这样,您可以限制后续进程的允许上限。
- 只有当某个功能位于边界集中时,才能将其添加到可继承的 Capset 中。
- 此外,从 Linux2.6.25 开始,功能绑定集是每个线程的属性。
除非满足以下任一条件,否则执行程序不会对 Capset 产生任何影响:
- 后继者是 setuid-root 或 setgid 程序
- 文件功能是在执行的二进制可执行文件上设置的
如何以编程方式查询和更改这些线程上限? 实际上,这正是capget(2)和capset(2)系统调用的用途。 但是,我们建议使用库级包装器 APIcap_get_proc(3)和cap_set_proc(3)。
有时,我们需要能够将功能“嵌入”到二进制可执行文件中(有关原因的讨论将在下一节中介绍)。 这显然需要内核文件系统支持。 在早期的 Linux 中,该系统是内核可配置的选项;从 Linux 内核 2.6.33 开始,文件功能总是编译到内核中,因此总是存在的。
文件上限是一个强大的安全特性--您可以说它们是旧的set[u|g]id特性的现代等价物。 要首先使用它们,操作系统必须支持它们,并且进程(或线程)需要CAP_FSETCAP
功能。 这里有一个关键点:在执行exec操作之后,(先前的)线程上限和(即将到来的)文件上限最终决定线程能力。
以下是 Linux 文件功能集:
- 允许(PRM):自动允许的功能
- 可继承(Inh)
- Effect(EFF):这是一个位:如果设置,则在 EFF 集中引发新的 PRM Capset;否则不会。
再说一次,请理解提供上述信息所依据的警告:这不是完整的细节。 要获取它们,请查看有关功能(7)的手册页:https://linux.die.net/man/7/capabilities。
以下是此手册页中的屏幕截图片段,显示了在exec操作期间用于确定功能的算法:
我们知道,与老式的仅根或 setuid-root 方法相比,功能模型的细粒度是一个主要的安全优势。 因此,回到我们虚构的 Packcap 程序:我们希望使用c功能,而不是 setuid-root。 因此,让我们假设,在仔细研究可用功能之后,我们得出结论,我们希望在我们的程序中赋予以下功能:
CAP_NET_ADMIN
CAP_NET_RAW
查看有关凭证的手册页(7)会发现,第一个凭证使进程能够执行所有必需的网络管理请求;第二个使进程能够使用“原始”套接字。
但是,开发人员究竟如何将这些必需的功能嵌入到编译后的二进制可执行文件中呢? 啊,使用getcap(8)
和setcap(8)
实用程序很容易实现这一点。 显然,您可以使用getcap(8)
查询给定文件的功能,使用setcap (8)
在给定文件上设置它们。
"If not already installed, please do install the getcap(8) and setcap(8) utilities on your system (the book's GitHub repo provides a list of madatory and optional software packages)"
警惕的读者在这里会注意到一些可疑的事情:如果您能够任意设置二进制可执行文件的功能,那么安全性在哪里? (我们只需在文件/bin/bash 上设置CAP_SYS_ADMIN
,它现在将作为根运行。)。 因此,实际情况是,只有在已经拥有CAP_FSETCAP
功能的情况下才能在文件上设置功能;在手册中:
CAP_SETFCAP (since Linux 2.6.24)
Set file capabilities.
实际上,实际上,您将因此以 root 身份通过 sudo(8)执行 setcap(8);这是因为在以 root 特权运行时,我们只获得 CAP_SETFCAP 功能。
因此,让我们来做个实验:我们构建一个简单的hello world
程序(ch8/hello_pause.c
);唯一的区别是:我们在printf
之后调用pause(2)
系统调用;pause
有进程休眠(永远):
int main(void)
{
printf("Hello, Linux System Programming, World!\n");
pause();
exit(EXIT_SUCCESS);
}
然后,我们编写另一个 C 程序来查询任何给定进程的能力;ch8/query_pcap.c
的代码:
[...]
#include <sys/capability.h>
int main(int argc, char **argv)
{
pid_t pid;
cap_t pcaps;
char *caps_text=NULL;
if (argc < 2) {
fprintf(stderr, "Usage: %s PID\n"
" PID: process to query capabilities of\n"
, argv[0]);
exit(EXIT_FAILURE);
}
pid = atoi(argv[1]);
[...]
pcaps = cap_get_pid(pid);
if (!pcaps)
FATAL("cap_get_pid failed; is process %d valid?\n", pid);
caps_text = cap_to_text(pcaps, NULL);
if (!caps_text)
FATAL("caps_to_text failed\n", argv[1]);
printf("\nProcess %6d : capabilities are: %s\n", pid, caps_text);
cap_free(caps_text);
exit (EXIT_SUCCESS);
}
这很简单:cap_get_pid(3)
API 返回功能状态,实质上是目标进程的capsets
。 唯一的麻烦是它是通过名为cap_t
的内部数据类型表示的;要读取它,我们必须将其转换为人类可读的 ASCII 文本;您猜对了,就是cap_to_text (3)
。 API 正是具有这样的功能。 我们使用它并打印结果。 (嘿,注意我们在使用之后必须如何cap_free(3)
计算变量;手册会告诉我们这一点。)
其中几个与功能相关的 API(大致是cap_*
)需要在系统上安装libcap
库。 如果尚未安装,请使用包管理器进行安装(正确的包通常称为libcap-dev[el*]
)。 显然,您必须链接到libcap
库(我们在 Makefile 中使用-lcap
来做到这一点)。
让我们试试看:
$ ./query_pcap
Usage: ./query_pcap PID
PID: process to query capabilities of
$ ./query_pcap 1
Process 1 : capabilities are: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read+ep
$
进程 PID 1,传统上是(SysV)init,,但是现在的systemd
是以root权限运行的;因此,当我们使用程序查询其 Capset 时(实际上,我们会返回有效的 Capset),我们会得到相当长的能力列表! (不出所料。)
接下来,我们在后台构建并运行hello_pause
进程;然后查询其功能:
$ make hello_pause
gcc -Wall -c -o hello_pause.o hello_pause.c
gcc -Wall -o hello_pause hello_pause.c common.o
$ ./hello_pause &
[1] 14303
Hello, Linux System Programming, World!
$ ./query_pcap 14303
Process 14303 : capabilities are: =
$
我们的hello_pause
进程当然没有特权,也没有任何嵌入其中的功能;因此,正如预期的那样,我们看到它没有没有功能。
现在是有趣的部分:首先,我们使用setcap(8)
实用程序将功能嵌入到我们的hello_pause
二进制可执行文件中:
$ setcap cap_net_admin,cap_net_raw+ep ./hello_pause
unable to set CAP_SETFCAP effective capability: Operation not permitted
$ sudo setcap cap_net_admin,cap_net_raw+ep ./hello_pause
[sudo] password for <xyz>: xxx
$
这是有道理的:正如root
(技术上,现在我们理解,有了CAP_SYS_ADMIN
能力),我们当然有CAP_SETFCAP
能力,因此成功地使用了setcap(8)
。 在语法上,我们需要指定给setcap(8)
一个功能列表,后跟一个操作列表;在此之前,我们已经指定了cap_net_admin,cap_net_raw
功能,并将添加到有效和允许的作为操作列表(使用+ep
语法)。
现在,我们重试我们的小实验:
$ ./hello_pause &
[2] 14821
Hello, Linux System Programming, World!
$ ./query_pcap 14821
Process 14821 : capabilities are: = cap_net_admin,cap_net_raw+ep
$
是!。 新的hello_pause
进程确实具有我们希望它具有的功能。
What happens if both the traditional setuid-root and the modern (file) capabilities are embedded in a binary executable? Well, in that case, when run, only the capabilities embedded into the file take effect; the process would have an EUID of 0, but would not have full root capabilities.
不过,请注意:上面的hello_pause
个程序实际上没有意识到它实际上拥有这些功能;换句话说,它在编程上没有执行任何操作来查询或设置 POSIX 功能。 然而,通过文件功能模型(和 setcap(8)实用程序),我们向其中“注入”了功能。 这种类型的二进制因此被称为能力-哑二进制*。*
在安全性方面,它仍然比笨拙的 setuid-root 要好得多,但是如果应用本身--以编程方式--在运行时使用 API 来查询和设置功能,那么它可能会变得更“智能”。 我们可以把这类 APP 想象成一种能力--智能二进制***。***
通常,在移植遗留的 setuid-root(或者更糟,只是root)类型的应用时,开发人员会剥离它的 setuid-root 位,从二进制文件中删除root所有权,然后通过在其上运行 setcap(8)命令将其转换为Capability-umb二进制文件。 这是迈向更好的安全性(或“强化”)的良好第一步。
getcap(8)
实用程序可用于查找(二进制)文件中嵌入的功能。 作为一个快速示例,让我们在 shell 程序和 ping 实用程序上运行getcap
:
$ getcap /bin/bash
$ getcap /usr/bin/ping
/usr/bin/ping = cap_net_admin,cap_net_raw+p
$
很明显,bash 没有任何文件上限--这正是我们所期望的。 而 ping 则是这样做的,这样它就可以在不需要 root 权限的情况下执行其职责。
通过 bash 脚本(类似于我们在上一章中看到的脚本)详细演示了getcap
实用程序的用法:ch8/show_caps.sh
。 运行它查看系统上安装的各种文件功能嵌入式程序(留作简单练习,供读者试用)。
与getcap(8)
在某些方面相似的是capsh(1)
实用程序-功能外壳包装器,尽管它是getcap(8)
的超集;有关详细信息,请查看其手册页。
与我们编写的query_pcap
程序类似的还有getpcaps(1)
实用程序。
因此:我们在本主题开始时编造的故事并不完全是虚构的-好吧,它确实是虚构的,但它与现实世界有着惊人的相似之处:众所周知的Wireshark(以前称为以太)网络数据包嗅探器和协议分析器应用。
在旧版本中,Wireshark 通常作为setuid-root
进程运行,以执行数据包捕获。
现代版本的 Wireshark 将数据包捕获分离到一个名为dump pcap1 的程序中。 虽然它不是作为 setuid-root 进程运行的,但它在运行时会嵌入所需的功能位,从而赋予它执行其工作所需的权限-数据包捕获。
因此,黑客现在对其执行成功攻击的潜在回报大大降低--黑客最多只能获得运行 Wireshark 和 Wireshark 组的用户的权限(EUID,EGID),而不是获得root;他不会获得 root! 我们使用*ls(1)和getcap(1)*如下所示:
$ ls -l /bin/dumpcap
-rwxr-x---. 1 root wireshark 107K Jan 19 19:45 /bin/dumpcap
$ getcap /bin/dumpcap
/bin/dumpcap = cap_net_admin,cap_net_raw+ep
$
请注意,在上面的长清单中,Other(O)访问类别没有权限;只有 root 用户和 Wireshark 成员可以执行 DumpCap(1)。 (不要而不是将其作为根执行;这样会破坏整个要点:安全性)。
仅供参考,实际的数据包捕获代码位于名为pcap—packet
Capture 的库中:
# ldd /bin/dumpcap | grep pcap
libpcap.so.1 => /lib64/libpcap.so.1 (0x00007f9723c66000)
#
仅供参考:来自 RedHat 的安全建议详细说明了 Wireshark:https://access.redhat.com/errata/RHSA-2012:0509的安全问题。 下面的一段代码证明了一个重要的观点:
... Several flaws were found in Wireshark. If Wireshark read a malformed packet off a network or opened a malicious dump file, it could crash or, possibly, execute arbitrary code as the user running Wireshark. (CVE-2011-1590, CVE-2011-4102, CVE-2012-1595) ...
突出显示的文本是关键:即使黑客管理任意代码执行的壮举,它也将以运行 Wireshark 的用户的权限执行-而不是 root!
The details on how exactly to set up Wireshark with POSIX capabilities is covered here (under the section entitled GNU/Linux distributions: https://wiki.wireshark.org/CaptureSetup/CapturePrivileges .
现在应该很清楚了:Dumpcap是一个Capability-Dumb二进制文件;Wireshark 进程(或文件)本身没有任何特权。 安全是双赢的。
我们已经了解了如何构建Capability-umb二进制文件;现在让我们了解一下如何在运行时在程序本身中添加或删除进程(线程)功能。
当然,getcap 的另一面是 setcap-我们已经在命令行上使用过该实用程序。 现在让我们来看看相关的 API。
需要理解的是:要使用进程上限,我们需要内存中的所谓“功能状态”。 要获得此功能状态,我们使用cap_get_proc(3)
API(当然,如前所述,所有这些 API 都来自libcap
库,我们将链接到该库)。 一旦我们有了工作上下文,即功能状态,我们将使用cap_set_flag(3)
API 来设置事务:
#include <sys/capability.h>
int cap_set_flag(cap_t cap_p, cap_flag_t flag, int ncap,
const cap_value_t *caps, cap_flag_value_t value);
第一个参数是我们从cap_get_proc()
接收到的能力状态;第二个参数是我们希望影响的能力集-有效、允许或继承之一。 第三个参数是我们使用这一个 API 调用操作的功能数量。 第四个参数--这是我们确定希望添加或删除的功能的位置,但是如何确定呢? 我们传递一个指向cap_value_t
元素的数组的指针。 当然,我们必须初始化数组;每个元素都有一个功能。 最后,第五个参数value
可以是两个值之一:CAP_SET
到设置能力,CAP_CLEAR
到丢弃它。
到目前为止,所有工作都是在内存上下文中进行的-能力状态变量;它并没有真正对进程(或线程)Capset 生效。 要在进程上实际设置上限,我们使用*cap_set_proc(3)*API:
int cap_set_proc(cap_t cap_p);
它的参数是我们仔细设置的功能状态变量。 现在将设置功能。
还要认识到,除非我们以root身份运行它(当然我们不会这样做--这才是真正的重点),否则我们不能仅仅提高我们的能力。 因此,在Makefile
本身内,一旦构建了程序二进制文件,我们就对二进制可执行文件本身(set_pcap
)执行sudo setcap
以增强其功能;我们将CAP_SETUID
和CAP_SYS_ADMIN
功能位赋予其允许和有效的上限。
下一个程序简要演示了进程如何添加或删除功能(当然,在它允许的上限内)。 当使用选项 1 运行时,它添加了CAP_SETUID
功能,并通过一个简单的测试函数(test_setuid()
)“证明”它。 这里有一个有趣的地方:由于二进制文件中已经嵌入了两个功能(我们在Makefile),
中执行了setcap(8)
操作,我们实际上需要删除CAP_SYS_ADMIN
功能(从其有效集合中)。
当使用选项 2 运行时,我们需要两个功能-CAP_SETUID
和CAP_SYS_ADMIN
;它可以工作,因为它们嵌入到有效和允许的上限中。
以下是ch8/set_pcap.c
***:***的相关代码
int main(int argc, char **argv)
{
int opt, ncap;
cap_t mycaps;
cap_value_t caps2set[2];
if (argc < 2)
usage(argv, EXIT_FAILURE);
opt = atoi(argv[1]);
if (opt != 1 && opt != 2)
usage(argv, EXIT_FAILURE);
/* Simple signal handling for the pause... */
[...]
//--- Set the required capabilities in the Thread Eff capset
mycaps = cap_get_proc();
if (!mycaps)
FATAL("cap_get_proc() for CAP_SETUID failed, aborting...\n");
if (opt == 1) {
ncap = 1;
caps2set[0] = CAP_SETUID;
} else if (opt == 2) {
ncap = 2;
caps2set[1] = CAP_SYS_ADMIN;
}
if (cap_set_flag(mycaps, CAP_EFFECTIVE, ncap, caps2set,
CAP_SET) == -1) {
cap_free(mycaps);
FATAL("cap_set_flag() failed, aborting...\n");
}
/* For option 1, we need to explicitly CLEAR the CAP_SYS_ADMIN capability; this is because, if we don't, it's still there as it's a file capability embedded into the binary, thus becoming part of the process Eff+Prm capsets. Once cleared, it only shows up in the Prm Not in the Eff capset! */
if (opt == 1) {
caps2set[0] = CAP_SYS_ADMIN;
if (cap_set_flag(mycaps, CAP_EFFECTIVE, 1, caps2set,
CAP_CLEAR) == -1) {
cap_free(mycaps);
FATAL("cap_set_flag(clear CAP_SYS_ADMIN) failed, aborting...\n");
}
}
/* Have the caps take effect on the process.
* Without sudo(8) or file capabilities, it fails - as expected.
* But, we have set the file caps to CAP_SETUID (in the Makefile),
* thus the process gets that capability in it's effective and
* permitted capsets (as we do a '+ep'; see below):"
* sudo setcap cap_setuid,cap_sys_admin+ep ./set_pcap
*/
if (cap_set_proc(mycaps) == -1) {
cap_free(mycaps);
FATAL("cap_set_proc(CAP_SETUID/CAP_SYS_ADMIN) failed, aborting...\n",
(opt==1?"CAP_SETUID":"CAP_SETUID,CAP_SYS_ADMIN"));
}
[...]
printf("Pausing #1 ...\n");
pause();
test_setuid();
cap_free(mycaps);
printf("Now dropping all capabilities and reverting to original self...\n");
drop_caps_be_normal();
test_setuid();
printf("Pausing #2 ...\n");
pause();
printf(".. done, exiting.\n");
exit (EXIT_SUCCESS);
}
让我们构建它:
$ make set_pcap
gcc -Wall -o set_pcap set_pcap.c common.o -lcap
sudo setcap cap_setuid,cap_sys_admin+ep ./set_pcap
$ getcap ./set_pcap
./set_pcap = cap_setuid,cap_sys_admin+ep
$
请注意,setcap(8)
已将文件功能嵌入到二进制可执行文件set_pcap
中(getcap(8)
会对其进行验证)。
试一试;我们将首先使用选项2
运行它:
$ ./set_pcap 2 &
[1] 3981
PID 3981 now has CAP_SETUID,CAP_SYS_ADMIN capability.
Pausing #1 ...
$
pause(2)
系统调用使进程进入休眠状态;这是故意这样做的,这样我们就可以尝试一下(参见下一段代码)。 顺便说一句,为了解决这个问题,程序设置了一些最小的信号处理;但是,这个主题将在后面的章节中详细讨论。 现在,只需理解暂停(以及相关的信号处理)允许我们真正地“暂停”进程、检查内容,并在完成后向其发送继续操作的信号:
$ ./query_pcap 3981
Process 3981 : capabilities are: = cap_setuid,cap_sys_admin+ep
$ grep -i cap /proc/3981/status
Name: set_pcap
CapInh: 0000000000000000
CapPrm: 0000000000200080
CapEff: 0000000000200080
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
$
在上面,我们通过我们自己的query_pcap
程序和 proc 文件系统检查该进程。 CAP_SETUID
和CAP_SYS_ADMIN
功能都存在于允许的和有效的上限中。
要继续该过程,我们向其发送信号;这是一种简单的方式-通过kill(1)
命令(详细信息见后面的第 11 章,信号-第 I 部分)。 现在有相当多的东西值得一看:
$ kill %1
*(boing!)*
test_setuid:
RUID = 1000 EUID = 1000
RUID = 1000 EUID = 0
Now dropping all capabilities and reverting to original self...
test_setuid:
RUID = 1000 EUID = 1000
!WARNING! set_pcap.c:test_setuid:55: seteuid(0) failed...
perror says: Operation not permitted
RUID = 1000 EUID = 1000
Pausing #2 ...
$
有趣的**(boing!)**只是通知我们信号处理已经发生的过程。 (忽略它。)。 我们调用test_setuid()
函数,即函数代码:
static void test_setuid(void)
{
printf("%s:\nRUID = %d EUID = %d\n", __FUNCTION__,
getuid(), geteuid());
if (seteuid(0) == -1)
WARN("seteuid(0) failed...\n");
printf("RUID = %d EUID = %d\n", getuid(), geteuid());
}
我们尝试使用seteuid(0)
行代码(有效地)成为根。 输出向我们表明,当 EUID 变为0
时,我们已经成功完成了此操作。 在此之后,我们调用drop_caps_be_normal()
函数,该函数“丢弃”所有功能*,*使用前面看到的setuid(getuid())
语义将我们还原为“我们的原始自我”;函数代码:
static void drop_caps_be_normal(void)
{
cap_t none;
/* cap_init() guarantees all caps are cleared */
if ((none = cap_init()) == NULL)
FATAL("cap_init() failed, aborting...\n");
if (cap_set_proc(none) == -1) {
cap_free(none);
FATAL("cap_set_proc('none') failed, aborting...\n");
}
cap_free(none);
/* Become your normal true self again! */
if (setuid(getuid()) < 0)
FATAL("setuid to lower privileges failed, aborting..\n");
}
程序输出确实向我们显示,EUID 现在恢复为非零(1000
的 RUID),并且seteuid(0)
如预期的那样失败(现在我们已经删除了功能和根权限)。
然后,进程再次调用pause(2)
语句(输出中的"Pausing #2 ..."
语句),以使进程保持活动状态;现在我们可以看到:
$ ./query_pcap 3981
Process 3981 : capabilities are: =
$ grep -i cap /proc/3981/status
Name: set_pcap
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
$
事实上,所有的能力都已经被放弃了。 (我们将运行带有选项1
的程序的测试用例留给读者。)
这里有一个有趣的地方:您可能会发现语句CAP_SYS_ADMIN
是新的根。 真的? 让我们测试一下:如果我们只将CAP_SYS_ADMIN
功能嵌入到二进制文件中,并修改代码,使其在选项1
下运行时不会删除它,会怎么样? 乍一看,这似乎无关紧要-我们应该仍然能够成功执行seteuid(0)
测试,因为我们实际上是以 root 身份使用此功能运行的。 但是你猜怎么着? 这不管用! 底线是:这告诉我们,虽然这句话听起来不错,但它实际上并不完全正确! 我们仍然需要CAP_SETUID
功能来执行set*id()
系统调用的任意使用。
我们让读者来编写本例的代码,并将其作为练习进行测试。
下面是剩下的一些杂乱无章但仍然有用的要点和小贴士:
当显示不同的二进制可执行文件类型时,Fedora 27(X86_64)的屏幕截图显示了漂亮的颜色*ls* -l
:
这些二进制文件到底是什么? 让我们按照上面显示的顺序列出它们:
dumpcap
:文件功能二进制可执行文件passwd
:setuid-root
二进制可执行文件ping
:文件功能二进制可执行文件write
:setgid-tty
二进制可执行文件
注意:确切的含义和颜色在不同的 Linux 发行版中当然会有所不同;显示的输出来自 Fedora27x86_64 系统。
既然我们已经了解了这两种模型的详细信息-上一章中的传统 UNIX 权限和本章中的现代 POSIX 功能,我们将对其进行鸟瞰。 现代 Linux 内核的实际情况是,遗留模型实际上是在较新的功能模型之上分层的;下表显示了这种“分层”:
| 利弊 | 模型/属性 | | 更简单, 不太安全 | 嵌入了 UID、GID 值的 UNIX 权限 进程和文件 | | | 进程凭证:{RUID,RGID,EUID,EGID} | | 更复杂, 更安全 | POSIX 功能 | | | 螺纹大写字母,文件大写字母 | | | 每线程:{继承,允许,有效,限定,环境}大写字母 二进制文件:{继承,允许,有效}大写字母 |
由于这种分层,有几点需要注意,如下所示:
- 在上层:显示为单个整数的进程 UID 和 GID 实际上是幕后的两个整数-真实有效的用户|组 ID。
- 中间层:产生四个进程凭证:{RUID,EUID,RGID,EGID}。
- 底层:它又在现代 Linux 内核上集成到 POSIX 功能模型中:
- 所有内核子系统和代码现在都使用能力模型来控制和确定对对象的访问。
- 现在,根-实际上是“新的”根-基于(重载)能力位
CAP_SYS_ADMIN
被设置。 - 一旦存在
CAP_SETUID
功能,就可以任意使用 set*id()系统调用来设置真实/有效的 ID:- 因此,您可以使 EUID=0,依此类推。
以下是有关安全的要点的快速总结:
-
显然,在我们的所有讨论中,尽可能不再使用现在已经过时的根模式;这包括(非)使用 setuid-root 程序。 相反,您应该使用功能,并且只将所需的功能分配给进程:
- 直接或以编程方式通过
libcap(3)
API(“功能智能”二进制文件),或者 - 间接通过二进制文件上的
setcap(8)
文件功能(“Capability-Dumb”二进制文件)。
- 直接或以编程方式通过
-
如果上述操作是通过 API 路由完成的,则应考虑在完成对该功能的需求后立即放弃该功能(并仅在需要时提高该功能)。
-
容器:一种“热门”的相当新的技术(本质上,容器在某种意义上是轻量级的虚拟机),它们被称为“安全的”,因为它们有助于隔离运行的代码。 然而,现实并不是那么乐观:容器部署通常很少或根本没有考虑到安全性,从而导致高度不安全的环境。 明智地使用 POSIX 功能模型可以极大地提高安全性。 下面详细介绍了一个有趣的 RHEL 博客,内容是如何要求 Docker(一种流行的容器技术产品)降低功能,从而极大地提高安全性:https://rhelblog.redhat.com/2016/10/17/secure-your-containers-with-this-one-weird-trick/。
(下面的段落仅供参考,可选;如果对更深层次的细节感兴趣,请看一看,或者跳过它。)
在 Linux 内核中,所有任务(进程和线程)元数据都保存在名为TASK_STRUT(也称为进程描述符)的数据结构中。 关于 Linux 所称的任务的安全上下文的信息保存在这个任务结构中,嵌入在另一个称为Cred(缩写为Credentials)的数据结构中。 这个结构cred包含了我们讨论过的所有内容:现代 POSIX 功能位掩码(或功能集)以及传统风格的进程特权:RUID、EUID、RGID、EGID(以及 set[u|g]id 和 fs[u|g]id 位)。
我们前面看到的procfs
方法实际上从这里查找凭证信息。 黑客显然对访问 CredD 结构感兴趣,并且能够动态地修改它:在适当的位置用零填充它会得到它们的根! 这听起来是不是很牵强呢? 请参阅 GitHub 存储库的进一步阅读部分中的*(一些)Linux 内核漏洞*。 不幸的是,这种情况发生的频率比任何人都希望的要高。
在本章中,读者已经领略了有关现代 POSIX 功能模型(在 Linux 操作系统上)的设计和实现的重要思想。 此外,我们还介绍了什么是 POSIX 功能,以及为什么它们很重要,尤其是从安全性的角度来看,这一点至关重要。 还介绍了如何将功能嵌入到运行时进程或二进制可执行文件中。
从上一章开始的讨论的全部目的是让应用开发人员了解在开发代码时出现的关键安全问题。 我们希望我们给读者们留下了一种紧迫感,当然还有以现代方式处理安全问题的知识和工具。 今天的应用不仅要工作;它们在编写时必须考虑到安全性! 否则..。*