首页 -> 安全研究
安全研究
绿盟月刊
绿盟安全月刊->第35期->技术专题
作者: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
版权所有,未经许可,不得转载