首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第35期->技术专题
期刊号: 类型: 关键词:
Solaris cachefsd远程heap缓冲区溢出漏洞分析

作者:w3 <mailto:warning3@nsfocus.com>
主页:http://www.nsfocus.com
日期:2002-09-16

本文分析了Solaris cachefsd远程溢出的起因与利用方式,仅供感兴趣的朋友参考,
请勿用于其他目的。:-)

Sun Solaris 2.5.1、2.6、7和8 缺省自带和安装了cachefsd程序,它用来对通过
NFS mount的远程文件系统的操作请求进行缓存。在Solaris系统下,cachefsd服务
以RPC服务形式被安装,号码为100235。它只监听TCP端口。

Solaris cachefsd程序中存在一个可远程利用的堆缓冲区溢出漏洞。远程或本地攻
击者可以向cachefsd程序发送一个畸形RPC请求来获取root权限。

cachefsd的第五个RCP服务函数(CACHEFSD_FS_MOUNTED : 5):
cachefsd_fs_mounted_1_svc()需要客户端提供两个字符串参数:
* cache dir, 表示要mount的目录名
* cache name,cache名。

cachefsd_fs_mounted_1_svc()会调用subr_add_mount()来增加远程mount点,
subr_add_mount()又调用cfsd_fscache_create()函数来将这两个参数保存在一个
fscache结构中(这个结构由函数cfsd_calloc()动态分配),然而,在保存参数时程
序没有进行边界检查,而是直接调用strcpy()函数进行拷贝操作。如果攻击者提供
超长的参数,就可能发生堆溢出,动态内存边界处的内存分配管理结构就会被覆
盖,攻击者可能改变程序流程执行任意代码。


执行流程如下:

   (gdb) bt
   #0  0xff1b6b70 in strcpy () from /usr/lib/libc.so.1
   #1  0x2071c in cfsd_fscache_create ()
   #2  0x23dd8 in subr_add_mount ()
   #3  0x16d5c in cachefsd_fs_mounted_1_svc ()
   #4  0x1604c in cachefsdprog_1 ()
   #5  0xff2c97a4 in _svc_prog_dispatch () from /usr/lib/libnsl.so.1
   #6  0xff2c9584 in svc_getreq_common () from /usr/lib/libnsl.so.1
   #7  0xff2c94bc in svc_getreq_poll () from /usr/lib/libnsl.so.1
   #8  0xff2cd290 in _svc_run () from /usr/lib/libnsl.so.1
   #9  0xff2ccfe4 in svc_run () from /usr/lib/libnsl.so.1
   #10 0x15e38 in main ()

下面我们从代码中进行分析导致问题发生的原因:

<1>  cfsd_svc.c : cachefsd_fs_mounted_1_svc()

bool_t
cachefsd_fs_mounted_1_svc(struct cachefsd_fs_mounted *inp, void *outp,
    struct svc_req *reqp)
{
.......
    if (error == 0) {
        dbug_print(("info", "Mounted in %s file system %s",
            inp->mt_cachedir, inp->mt_cacheid));
         // inp->mt_cachedir : cache目录的名字
         // inp->mt_cacheid  : cache ID的名字
         // 调用subr_add_mount(), 没有做任何长度检查。

        subr_add_mount(all_object_p, inp->mt_cachedir, inp->mt_cacheid); <2>
    }

    dbug_leave("cachefsd_fs_mounted_1_svc");
    return (1);
}

   <2> cfsd_sub.c(118): subr_add_mount()

void
subr_add_mount(cfsd_all_object_t *all_object_p,
    const char *dirp,
    const char *idp)
{
.......
        // 首先要根据cachedir来检查是否已经存在这样一个cache_object
        // 程序会检查一个cache列表,这个列表中包含已经创建的cachedir
    cache_object_p = all_cachelist_find(all_object_p, dirp);
    if (cache_object_p == NULL) {
             //如果没有找到,创建一个新的cache object
        /* make the cache object */
        // cfsd_cache_create会创建一个cache_object结构
        // 它包含一些内存分配的操作。
        cache_object_p = cfsd_cache_create();
        xx = all_object_p->i_nextcacheid;
        // 设置新创建的cache_object
        xx = cache_setup(cache_object_p, dirp, xx); [注1]
        if (xx == 0) {
                // 如果返回为空(0),函数就直接返回了。
            dbug_print(("error", "invalid cache %s", dirp));
            // 这个destroy函数中包含一些free操作
            cfsd_cache_destroy(cache_object_p);
            all_unlock(all_object_p);
            dbug_leave("subr_add_mount");
            return;
        }
        // 如果xx返回不为零,将这个object增加到cache列表中。
        all_cachelist_add(all_object_p, cache_object_p);
        all_cachefstab_update(all_object_p);
    }

       .....

    /* find or create the fscache object */
    .......
    // 同上面那个很相似,不同的是这里是根据cacheid来在fscachelist查找
    //
    fscache_object_p = cache_fscachelist_find(cache_object_p, idp);
    if (fscache_object_p == NULL) {
        /* make the fscache object and add it to the list */
        xx = cache_object_p->i_nextfscacheid;
        // 根据cacheid和cachedir来创建一个fscache_object结构
        // 这里面会发生溢出!
        fscache_object_p = cfsd_fscache_create(idp, dirp, xx); <3>
        cache_fscachelist_add(cache_object_p, fscache_object_p);

.......
    fscache_setup(fscache_object_p);
.......
}

    <3> cfsd_fsc.c(58): cfsd_fscache_create()

cfsd_fscache_create(const char *name, const char *cachepath,
    int fscacheid)
{

......

    fscache_object_p = cfsd_calloc(sizeof (cfsd_fscache_object_t)); [注3]
    // 没有进行边界检查就拷贝,导致溢出
    strcpy(fscache_object_p->i_name, name);// !!!! 溢出
    // 由于这个cachepath必须是一个有效的path,这里不会导致溢出。
    strcpy(fscache_object_p->i_cachepath, cachepath);
......

下面是cfsd_fscache_object_t结构的定义,我们看到i_name是一个0x100字节大的
缓冲区,i_cachepath是一个0x400字节的缓冲区。因此,只要name长度超过0x100,
就会发生溢出,如果长度超过cfsd_fscache_object_t结构的长度,就可能覆盖掉内存
块之间的管理结构信息。

typedef struct cfsd_fscache_object {
    char    i_name[MAXNAMELEN];    //0x100    /* fscache name */
    char    i_cachepath[MAXPATHLEN];//0x400    /* cache pathname */
    int    i_fscacheid;            /* fscache identifier */

    char    i_mntpt[MAXPATHLEN];        /* mount point */
    char    i_backfs[MAXPATHLEN * 2];    /* back file system */
    char    i_backpath[MAXPATHLEN];        /* back file system path */
    char    i_backfstype[MAXNAMELEN];    /* back file system type */
    char    i_cfsopt[MAXPATHLEN * 4];    /* cachefs mount options */
    char    i_bfsopt[MAXPATHLEN * 4];    /* backfs mount options */
      .......
} cfsd_fscache_object_t;

[注3]  solaris7 x86下: cfsd_fscache_object_t的大小为 0x3654
       0x805918a <cfsd_fscache_create+82>:     pushl  $0x3654
       0x805918f <cfsd_fscache_create+87>:     call   0x805d6f0 <cfsd_calloc>

       solaris7 SPARC下: cfsd_fscache_object_t的大小为 0x365c

       由于strcpy()执行顺序是先拷贝name,然后再拷贝path,因此
        i_name    i_cachepath
       [..... ][...............][......]  -> 拷贝之前
       [XXXXXX][XXXXXXXXXXXXXXX][XXXXXX]  -> 第一次strcpy()执行后
       [XXXXXX][i_cachepath|XXX][XXXXXX]  -> 第二次strcpy()执行后
         0x100   0x400

       由于i_cachepath必须是一个有效的目录,所以能放置shellcode的区域只能是
       开头的0x100字节内,以及i_cachepath后面的区域。


现在的情况是,如果我们可以发送较长的cachename或者cachepath都可以导致溢出(理
论上)。但是,

[注 1] 在执行cache_setup(cache_object_p, dirp, xx);时,会首先判断cachdirp是
否是一个有效的目录名:
....
    if ((stat64(cachedirp, &sinfo) == -1) ||
        (!S_ISDIR(sinfo.st_mode)) ||
        (*cachedirp != '/')) {
        dbug_print(("info", "%s is not a cache directory", cachedirp));
        ret = 0;
    } else {
        strcpy(cache_object_p->i_cachedir, cachedirp);
        ret = 1;
    }

stat64()会检测目录是否存在,并且目录名不能超过0x400(1024)字节。这个检测导致
我们无法利用目录名进行溢出攻击,因为它必须真实存在而且长度小于0x400字节。一
个有效的、可以达到最大长度的目录名类似如下格式:
"/tmp///////////////....////"

如果不能通过cache_setup(),那么subr_add_mount()就直接返回了。所以我们只能在
cachename上做文章了。

让我们再来考虑一下攻击过程,要想发生溢出,我们需要满足的条件

1. 需要能够执行到cfsd_fscache_create()函数
   要执行到这个函数就要满足两个条件中的任意一个:
    . all_cachelist_find(all_object_p, dirp) 返回为真
    或
    . all_cachelist_find(all_object_p, dirp) 返回为空,但是
      cache_setup(cache_object_p, dirp, xx)返回会真。

   对于第一个条件,如果我们知道目标主机上已经存在某个cache object,就可以
   直接利用。但通常是不可能的。

   既然我们不知道远程主机上是否已经存在一个cache object,我们只能自己创建一
   个,这样all_cachelist_find(all_object_p, dirp)肯定为空,只要我们提供一个
   有效的目录名作为cachedir(dirp),cache_setup()返回就会为真。这样我们就可以
   满足第二个条件

2. cachedir是一个真实有效的目录名

3. 在执行到cfsd_fscache_create()后,如果要发生溢出,我们还需要满足cachename
   的长度超过cfsd_fscache_object_t结构的大小(0x3654或0x365c).

4. 在cfsd_fscache_create()执行完之后,程序会执行
   fscache_setup(fscache_object_p)
   它也有一个stat64()的操作:

   ......
    char buf[MAXPATHLEN * 4];
   ......
        // 这个指令会将cachepath、cachename拷贝到一个堆栈中的缓冲区内。
    sprintf(buf, "%s/%s/%s", fscache_object_p->i_cachepath,
        fscache_object_p->i_name, CACHEFS_MNT_FILE); [注4]

    /* get the modify time of the mount file */
    if (stat64(buf, &sinfo) == -1) {  // 如果文件不存在,退出
        dbug_print(("err", "could not stat %s, %d", buf, errno));
        dbug_leave("fscache_setup");
        return;
    }
  ......

   在x86下面,堆区的地址高位不为零,所以可以直接返回到堆里面去执行,执行空间
   还是相当大的。但是SPARC下,堆区的地址最高字节为零,而我们的cachename中又
   不能包含零,所以不能返回到堆区中执行,只能利用堆栈里面的内容,而上面的
   sprintf()会将部分cachename中的部分数据拷贝到堆栈中去,我们可以返回到那里
   去执行。

   注意:上面我说的是部分数据。这是因为,发生堆溢出时,首先会拷贝name,然后拷
   贝path导致的结果.

   strcpy(fscache_object_p->i_name, name);
   strcpy(fscache_object_p->i_cachepath, cachepath);

   虽然name会导致溢出,但是接下来的strcpy()会截断i_name,使得i_name 的长度
   最长等于 0x100 + strlen(cachepath).
   所以,如果cachedir = "/tmp", cachename = "AAA...AAAA" (0x4000)个'A',
   最后的buf中的内容会是:
          |<- 0x100 ->|
   [/tmp/][AAAA....AAA/tmp][/.mnt]

    所以在SPARC下,必须利用cachename中前0x100字节的缓冲区来保存shellcode.

5. 由于stat64(buf)肯定返回-1,所以程序在这里就会返回,subr_add_mount()也会
   返回。此过程中没有内存分配和释放操作,因此第一个请求不会直接触发代码执行。

   我们必须再发送一个请求,这个请求中的cachedir与第一个不同,可以是无效的
   目录名,这样subr_add_mount会调用cfsd_cache_create()为它分配一个结构,由于
   这个过程会触发动态内存的管理,就会导致一个内存重写的操作,只要安排好结构,
   我们就可以很容易的执行shellcode了。

综上所述,我们要做的事情就是:

1. 发送一个mounted请求,它包含两个参数:
   cachedir是一个有效的目录名(比如"/tmp"),
   cachename是一个超过0x3654(或0x365c)的字符串,它的前0x100字节中包含有效的
   shellcode.在0x3654 + 4(或者0x365c +4)处开始放置伪造的管理结构和内存块。

2. 再次发送一个mounted请求,它包含两个参数:

   cachedir是一个任意的无效目录名。
   cachename是任意字符串。


在测试solaris 8时,发现第一个请求总是会失败,原来solaris 8的cachefsd设置请
求模式为非阻塞式的,并且设置了每次最大的请求长度,好像是0x2000左右,如果超
长就会中断连接。但是如果要想溢出,至少也得发送0x365c自己的请求数据,所以在
Solaris 8下面似乎没有办法进行攻击。尽管漏洞是存在的。

这个非阻塞模式在Solaris 7(可能包括以前版本)上是没有的。

Solaris 2.6目前还没有测试。

解决的方法也很简单,在SPARC平台下,可以打开禁止不可执行堆栈的开关。x86平台下
则只能暂时关闭服务了。非常奇怪,Sun好像至今还没有出补丁。

参考链接:

[1]. Sun(sm) Alert Notification 44309:
     http://sunsolve.sun.com/pub-cgi/retrieve.pl?doc=fsalert/44309

[2]. [LSD] Solaris cachefsd remote buffer overflow vulnerability
     http://marc.theaimsgroup.com/?l=bugtraq&m=102066052703611&w=2

[3]. Solaris source codes
版权所有,未经许可,不得转载