如果您可以将特权部分移动到单独的进程中,我强烈建议您这样做。父进程会构造至少一对Unix Domain socket对,一端留给自己,另一端作为子进程的标准输入或输出。
使用 Unix 域套接字对的原因是这样的一对不仅是双向的,而且还支持识别另一端的进程,并将打开的文件描述符从一个进程传递到另一个进程。
例如,如果您的主进程需要超级用户访问权限来读取文件,可能在特定目录中,或者以其他方式可识别,您可以将此类文件的打开移动到单独的帮助程序中。通过使用Unix域套接字对进行两者之间的通信,帮助程序可以使用getsockopt(ufd, SOL_SOCKET, SO_PEERCRED, &ucred, &ucred_size)获取peer credentials:进程ID、有效用户ID和有效组ID。在伪文件 /proc/PID/exe 上使用readlink()(其中PID 是作为正十进制数的进程ID),您可以获得另一端当前正在运行的可执行文件。
如果可以打开目标文件/设备,则帮助程序可以将打开的文件描述符传递回父进程。 (Linux 中的访问检查仅在打开文件描述符时进行。仅当描述符以只读方式打开或套接字读取端已关闭时才会阻止读取访问,并且仅当打开描述符时才会阻止写入访问只读或套接字写入端已关闭。)
我建议传递 int 作为数据,如果成功将描述符作为辅助消息,则传递 0,否则传递 errno 错误代码(没有辅助数据)。
但是,重要的是要考虑如何利用这些帮助程序的可能方式。限制到一个特定的目录,或者可能有一个系统范围的配置文件来指定允许的路径 glob 模式(并且不是每个人都可以写),并使用例如fnmatch() 检查是否列出了传递的路径,是很好的方法。
帮助进程可以通过setuid 或通过Linux 文件系统capabilities 获得权限。例如,只为帮助程序提供CAP_DAC_OVERRIDE 功能将使其绕过文件读取、写入和执行检查。在 Debian 衍生产品中,用于操作文件系统功能的命令行工具 setcap 位于 libcap2-bin 包中。
如果不能将特权部分移动到单独的进程中,可以使用 Linux、BSD 和 HP-UX 系统支持的接口:setresuid(),它设置 real、有效,并在单个调用中保存个用户 ID。 (有一个对应的setresgid()调用真实有效保存的组ID,但是使用那个的时候,记住补充组列表没有修改;需要显式调用setgroups()或者initgroups()来修改补充组列表。)
还有文件系统用户 ID 和文件系统组 ID,但是只要设置了有效用户和/或组 ID,C 库就会设置这些以匹配有效的。
如果进程以超级用户权限启动,则有效用户 ID 将为零。如果您首先使用getresuid(&ruid, &euid, &suid) 和getresgid(&rgid, &egid, &sgid),您可以使用setresgid(rgid, rgid, rgid) 确保仅保留真实组身份,并通过调用setresuid(ruid, ruid, 0) 暂时放弃超级用户权限。要重新获得超级用户权限,请使用setresuid(0, ruid, 0),要永久删除超级用户权限,请使用setresuid(ruid, ruid, ruid)。
这是可行的,因为允许进程在真实、有效和保存的身份之间切换。有效的是管理对资源的访问。
有一种方法可以将权限限制在进程内的专用线程中,但它很hacky和脆弱,我不推荐它。
为了将权限限制在单个线程中,您可以在 SYS_setresuid/SYS_setresuid32、SYS_setresgid/SYS_setresgid32、SYS_getresuid/SYS_getresuid32、SYS_getresgid/@987654353 周围创建自定义包装器@、SYS_setfsuid/SYS_setfsuid32 和 SYS_setfsgid/SYS_setfsgid32 系统调用。 (让包装器调用 32 位版本,如果返回 -ENOSYS,则回退到 16 位版本。)
在 Linux 中,用户和组的身份实际上是每个线程的,而不是每个进程的。使用的标准 C 库将使用例如实时 POSIX 信号和一个内部处理程序,用于向其他线程发出信号以切换身份,作为操作这些身份的库函数的一部分。
在您的进程的早期,创建一个特权线程,它将保留 root (0) 作为已保存的用户身份,但否则会将真实身份复制到有效和已保存的身份。对于主流程,将真实身份复制到有效和保存的身份。当特权线程需要做某事时,它首先将有效用户身份设置为root,然后执行该操作,然后将有效用户身份重置为真实用户身份。这样特权部分被限制在这个线程中,并且只在必要时应用于部分,因此大多数常见的信号等漏洞利用将没有机会工作,除非它们恰好发生在这样的特权部分中。
这样做的缺点是必须不使用任何可更改身份的 C 库函数(setuid()、seteuid()、setgid()、setegid()、setfsuid()、setfsgid()、setreuid() , setregid(), setresuid(), setresgid()) 必须由进程中的任何代码使用。因为在 Linux C 库中函数很弱,您可以通过将它们替换为您自己的版本来确保:自己定义这些函数,使用正确的名称(如图所示并带有两个下划线)和参数。
在所有各种方法中,我相信通过 Unix 域套接字对进行身份验证的单独过程是最明智的。
它最容易变得健壮,并且至少可以在 POSIX 和 BSD 系统之间移植。