本文共 5736 字,大约阅读时间需要 19 分钟。
原文:
译者:
下面的代码段属于某个特权程序(即 Set-UID 程序),它使用 Root 权限运行。
1: if (!access("/tmp/X", W_OK)) { 2: /* the real user ID has access right */ 3: f = open("/tmp/X", O_WRITE); 4: write_to_file(f); 5: } 6: else { 7: /* the real user ID does not have access right */ 8: fprintf(stderr, "Permission denied\n"); 9: }
access
系统该调用检查了真实 UID 或者 GID 是否拥有访问文件的权限,有的话返回 0。在代表真实 UID (而不是有效 UID)访问文件之前,该系统调用通常由 Set-UID 程序使用。open
系统调用也执行访问控制,但是仅仅检查有效 UID 或 GID 是否拥有访问文件的权限。/tmp/X
。在这么做之前,它要确保,文件确实由真实 UID 写入。如果没有这种检查,程序可以写入这个文件,无论真实 UID 可不可以写入它,因为程序使用 Root 权限运行(即open
所检查的有效 UID 是 Root)。/etc/passwd
嘛?/tmp
目录的权限为rwxrwxrwx
,这允许任何用户在里面创建文件或链接。/tmp/X
不需要是真实文件,他可以是符号链接。/tmp/X
在第一行之前打印/etc/passwd
,access
调用就会发现,真实 UID 没有权限来修改/etc/passwd
。因此,执行流会来到else
分支。在第一行之前,/tmp/X
必须是一个能被真实 UID 写入的文件。/tmp/X
会打开,攻击者不能获得任何东西。/tmp/X
并且使用相同名称创建符号链接。并使其指向/etc/passwd
。open
来打开/etc/passwd
。open
系统调用只检查有效 UID 或 GID 是否可以访问文件。由于这是个 Set-UID Root 程序,有效 UID 是 Root,它可以读写/etc/passwd
。/etc/passwd
。如果写入文件的内容也可以由用户控制,攻击者就可以修改密码文件,并且最终获得 Root 权限。如果内容不能由用户控制,攻击者可以破坏密码文件,组织其他用户登入系统。/tmp/X
在访问和打开调用中,表现为两个文件。access(/tmp/X, W_OK)
之前,/tmp/X
就是/tmp/X
。access(/tmp/X, W_OK)
之后,将/tmp/X
修改为/etc/passwd
。access
后进行上下文切换,之后执行其它进程。另一个例子(Set-UID 程序):
file = "/tmp/X"; fileExist = check_file_existence(file); if (fileExist == FALSE){ // The file does not exist, create it. f = open(file, O_CREAT); }
open
系统调用来创建文件。open(file, O_CREAT)
在文件不存在时创建文件,如果文件存在,它只会打开文件。/etc/passwd
。open(file, O_CREAT | O_EXCL)
可以在一条原子指令中检查和打开文件。如果文件已经存在,它就会返回错误,否则它会创建文件。mkstemp
函数会按照模板生成一个唯一的临时文件名称。这个函数使用O_EXCL
来使用open
。来防止竞态条件问题。open
创建另一个选项,来一起执行access
和open
。虽然这种选项不存在于 POSIX 标准中,但是它很容易实现。也就是,我们可以定义一个选项叫做O_REAL_USER_ID
。当我们使用open
调用open(file, O WRITE | O REAL USER ID)
,我们让open
检查有效和真实 UID,并仅当两个 UID 都有权限打开文件时,才打开文件。实际上,让 POSIX 标准委员会接收这个新的选项并不是很容易。检查-使用-再检查方式
lstat(file, &result)
可以获取文件状态。如果文件是个符号链接,它返回链接的状态(不是链接指向的文件)。在 TOCTOW 之前,我们可以使用它来检查文件状态。接着在间隔之后,执行另一个检查。如果结果不同,我们就检测到了竞态条件。让我们看看下面的解决方案:
struct stat statBefore, statAfter;1: lstat("/tmp/X", &statBefore);2: if (!access("/tmp/X", O_RDWR)) { /* the real UID has access right */ 3: f = open("/tmp/X", O_RDWR); 4: lstat("/tmp/X", &statAfter);5: if (statAfter.st_ino == statBefore.st_ino) 6: { /* the I-node is still the same */ 7: Write_to_file(f) 8: } 9: else perror("Race Condition Attacks!"); 10: } 11: else fprintf(stderr, "Permission denied\n");
但是,上面的解决方案不能工作(open
和第二个`lstat之间存在竞态条件漏洞)。为了利用这个漏洞,攻击者需要执行另个静态条件攻击,第一个在第二行和第三行之间,另一个在第三行和第四行之间。虽然赢得两次竞争的可能性低于前面的情况,但还是可能的。
为了修复漏洞,我们打算在文件描述符f
上使用lstat
,而不是在文件名称上。虽然lstat
不能这样做,但是fstat
可以。
#include#include #include #include int main() { struct stat statBefore, statAfter;1: lstat("/tmp/X", &statBefore); 2: if (!access("/tmp/X", O_RDWR)) { /* the real UID has access right */ 3: int f = open("/tmp/X", O_RDWR); 4: fstat(f, &statAfter); 5: if (statAfter.st_ino == statBefore.st_ino) 6: { /* the I-node is still the same */ 7: write_to_file(f); 8: } 9: else perror("Race Condition Attacks!");10: } 11: else fprintf(stderr, "Permission denied\n"); 12: }
问题:lstat
和fstat
之间有没有竞态条件?如果在第一行使用符号链接(例如到/etc/shadow
)。之后在第二行之前,快速切换到/tmp/X
,之后在第三行之前再次快速切换会符号链接呢?
答案:这个攻击是不可行的。函数调用lstat("/tmp/X",...)
返回链接的状态,如果/tmp/X
是个符号链接,而不是链接所指向文件的状态。换句话说,当/tnp/X
指向了/etc/shadow
,由lstat(/tmp/X,...)
返回的 inode 就是/tmp/X
的 inode,但是由fstat(f, ...)
返回的 unode 是文件的 inode(这里是/etc/shadow
的 inode)。即使/tmp/X
指向了/etc/shadow
,这两个 inode 是不同的。
要注意:所有这类调用都有两个版本,一个用于文件名,另一个用于文件描述符(思考:如果access
也可以用于文件描述符,解法会简单很多)。
检查-使用-重复方式:在几个迭代内重复访问和打开。在下面的示例中,攻击者需要赢得五个竞态条件(1~2,2~3,3~4,4~5,5~6):
1: if (access("tmp/X", O_RDWR)) goto error handling 2: else f1 = open("/tmp/X", O_RDWR); 3: if (access("tmp/X", O_RDWR)) goto error handling 4: else f2 = open("/tmp/X", O_RDWR); 5: if (access("tmp/X", O_RDWR)) goto error handling 6: else f3 = open("/tmp/X", O_RDWR); // Check whether f1, f2, and f3 has the same i-node (using fstat)
基于最小权限原则:
access
和open
的程序中,我们知道open
比我们想要的更加强大(它只检查有效 UID),这就是我们需要使用access
来确保我们没有滥用权限的原因。我们从竞态条件攻击中得到的启示,就是这种检查不是始终可靠。在 Unix 中,我们可以使用seteuid
或者setuid
系统调用,来开启、禁用或删除权限。
/* disable the root privilege */ #include#include uid_t real_uid = getuid(); // get real user id uid_t effective_uid = geteuid(); // get effective user id1: seteuid (real_uid);2: f = open("/tmp/X", O_WRITE); 3: if (f != -1) 4: write_to_file(f); 5: else 6: fprintf(stderr, "Permission denied\n"); /* if needed, enable the root privilege */ 7: seteuid (effective_uid);
转载地址:http://ercml.baihongyu.com/