Lucene search

K
seebugK0shlSSV:91149
HistoryMar 23, 2016 - 12:00 a.m.

FreeBSD 10.2 64位内核堆溢出漏洞(CVE-2016-1885)

2016-03-2300:00:00
k0shl
www.seebug.org
18

0.005 Low

EPSS

Percentile

75.6%

FreeBSD简介

  • FreeBSD是一种类UNIX的开源操作系统,为不同架构的计算机系统提供了不同程度的支持。FreeBSD提供先进的网络、性能、安全以及兼容性,这些特性在其他现代操作系统上仍有所缺失,即使是一些最好的商业操作系统。
  • 在网络方面,FreeBSD的性能也是相当优异的。在很重的负载之下,FreeBSD仍然可以稳定的运行,这也是很多网络服务器采用 FreeBSD 的原因之一。

漏洞描述

  • FreeBSD 10.2-RELENG之前版本内核代码中,amd_64_set_ldt()函数存在整数签名错误(在/sys/amd64/amd64/sys_machdep.c中定义),实现了i368_set_ldt系统调用amd64版本的操作系统,最终导致内核堆溢出,本地攻击者可造成系统崩溃

环境搭建及漏洞复现

其实此漏洞发生在amd64_set_ldt函数中,但罪魁祸首其实是里面的另一个函数,此漏洞是由于FreeBSD 10.2系统中的amd64_set_ldt函数在处理指针中的成员函数时,由于对函数值没有进行有效的控制,导致在函数处理时由于整数溢出,导致后续调用bzero()函数时堆被置0,引发系统异常处理,下面对此漏洞进行详细分析。

首先需要在freebsd上创建一个vsftp用于传输PoC文件,之后使用Clang编译,执行PoC即可。

执行PoC之后系统崩溃,产生了vmcore。

我们使用kgdb加载vmcore来看一下崩溃信息。

实际上在我另一篇FreeBSD的分析中提到了#8处的call trap()这段asm代码,其实到这里,已经进入了linux的内核异常处理过程,那么我们要关注的就是#9位置以及往后的代码,下面我们就从vmcore入手,结合内核源码来分析整个漏洞形成的原因。

漏洞分析 1 (来自用户@k0Sh1)

首先,我们在kgdb下通过bt来回溯一下漏洞发生的过程,和刚才直接查看vmcore稍有不同。

其中我们看到原先#9处的位置,现在调用了bzero()函数,其实这个函数是置零函数,其实,这个才是漏洞触发的真正原因!后续分析过程中我们会讲解一下bzero这个函数的函数结构,首先我们来看一下#9之前的调用情况,首先来看一下#10位置。我们来看一下amd64_set_ldt函数。

首先来到崩溃位置622行

    bzero(&((struct user_segment_descriptor *)(pldt->ldt_base))
        [uap->start], sizeof(struct user_segment_descriptor) * i);

这里调用到了一个sizeof指针i,那么这个i指针从哪里来的呢?

    largest_ld = uap->start + uap->num;
    if (largest_ld > max_ldt_segment)
      largest_ld = max_ldt_segment;
    i = largest_ld - uap->start;

可以看到,i的大小与uap有关,那么uap又是从哪里来的呢?

int
amd64_set_ldt(td, uap, descs)

函数定义部分,uap作为参数传入,那么接下来,我们就来到外层函数sysarch_ldt来仔细分析一下uap到底发生了什么,会导致内核堆溢出漏洞的发生。

首先在外层函数sysarch_ldt的定义处

int
sysarch_ldt(struct thread *td, struct sysarch_args *uap, int uap_space)

可以看到这里给uap定义了一个明确的类,struct sysarch_args,我们就通过vmcore来看一下这个结构类到底是怎么回事。

这里我们要关注几个点,第一个是op的值为1,第二个是parms的值,接下来回到源代码部分。

  if (uap_space == UIO_USERSPACE) {
    error = copyin(uap->parms, &la, sizeof(struct i386_ldt_args));
    if (error != 0)
      return (error);
    largs = &la;
  } else
    largs = (struct i386_ldt_args *)uap->parms;

进入sysarch_args函数后,在这个if语句中,largs会被struct i386_ldt_args类赋值,赋值的值是uap的parms成员,实际上通过上面图中的回溯时不能准确看到这个成员中的内容的,一会再来讲解如何查看该成员的内容,但要记住这个成员。接下来会进入一处switch语句。

  switch (uap->op) {
  case I386_GET_LDT:
    error = amd64_get_ldt(td, largs);
    break;
  case I386_SET_LDT:
    if (largs->descs != NULL && largs->num > max_ldt_segment)
      return (EINVAL);
    set_pcb_flags(td->td_pcb, PCB_FULL_IRET);
    if (largs->descs != NULL) {
      lp = malloc(largs->num * sizeof(struct
          user_segment_descriptor), M_TEMP, M_WAITOK);
      error = copyin(largs->descs, lp, largs->num *
          sizeof(struct user_segment_descriptor));
      if (error == 0)
        error = amd64_set_ldt(td, largs, lp);
      free(lp, M_TEMP);
    } else {
      error = amd64_set_ldt(td, largs, NULL);
    }
    break;
  }

语句中,对uap的成员op做了一个判断,刚才我们通过p查看uap的时候发现op的值为1,那么我们在sys_machdep.h中看一下对这两个CASE的定义,40行。

#define I386_GET_LDT	0
#define I386_SET_LDT	1

可以看到I386_SET_LDT值为1,也就是说程序会进入下面那处CASE语句中,接下来在语句中我们看到使用了largs这个结构体,之前我们用图中结构体回溯失败了,因为这是64位系统,但是vmcore给出的成员地址却是32位的,系统没法定位那个地址的内容。

但是我们注意到,在接下来amd64_set_ldt的两处调用中,都涉及到largs,作为第二个参数刚才我们已经分析过,第二个参数正是uap,那么这样,我们可以通过#10处的uap的值,来查看largs结构成员的内容。

我们可以看到三个成员都非常重要,start=1,descs=0x0(也就是NULL),num=2147483648,那么回到刚才的源代码位置。

首先descs=NULL,因此第一个if语句不会进入,那么会进入esle语句,也就是说largs会直接作为uap进入amd64_set_ldt函数中。

接下来回到amd64_set_ldt函数。

  if (descs == NULL) {
    /* Free descriptors */
    if (uap->start == 0 && uap->num == 0)
      uap->num = max_ldt_segment;
    if (uap->num == 0)
      return (EINVAL);
    if ((pldt = mdp->md_ldt) == NULL ||
        uap->start >= max_ldt_segment)
      return (0);
    largest_ld = uap->start + uap->num;

进入函数后会进入一系列判断,记住我们刚才的值,desc=NULL,因此会进入这个循环,接着start=1,那么第一个if语句不满足。num不等于0,所以第二个if语句不满足,那么largest_ld=uap->start+uap->num这就是关键了!

我们刚才num的值为2147483648,也就是0x800000000,而start值为1,也就是说,而largest_ld的定义是有符号数,因此相加后,值为一个负数!

接下来。

    if (largest_ld > max_ldt_segment)
      largest_ld = max_ldt_segment;
    i = largest_ld - uap->start;

会将这个值进行一个判断,max_ldt_segment在整个.c文件入口处已经有定义。

int max_ldt_segment = 1024;

由于largest_ld的大小为负数,条件判断肯定通过,但是接下来i为两值相减,减完之后就是uap->num的值,也就是0x80000000,一个极大值。

接下来就说到我们刚才提到的bzero(),这个函数是linux下的一个置0函数,它的定义如下

原型:extern void bzero(void *s, int n);
参数说明:s 要置零的数据的起始地址; n 要置零的数据字节个数。

这里,n就是sizeof*i,这个i的大小就是num,是一个极大的值,因此,在调用这个函数后,内存空间将有大面积置0,因此造成了内核崩溃。

漏洞分析 2(来自用户 @Pyx_

  • FreeBSD中可以得知,i386_set_ldt系统调用了英特尔i386版本,这个系统调用可以用来管理i386每个进程的局部描述符号(LDT)条目。FreeBSDamd64版本仍然暴露出了在64位版本的操作系统中这个系统调用用于运行32位应用程序
  • FreeBSD内核中的sysarch()函数中我们可以发现特定结构的系统调用,在/sys/amd64/amd64/sys_machdep.c文件中定义
int
sysarch(td, uap)
    struct thread *td;
    register struct sysarch_args *uap;
{
[...]
if (uap->op == I386_GET_LDT || uap->op == I386_SET_LDT)
    return (sysarch_ldt(td, uap, UIO_USERSPACE));
[...]
  • 从上面的代码可以看出,如果被调用的是i386_get)ldti386_set_ldt,那么调用sysarch_ldt()函数
  • 查看sysarch_ldt()函数中处理i386_set_ldt的代码
int
sysarch_ldt(struct thread *td, struct sysarch_args *uap, int uap_space)
{
struct i386_ldt_args *largs, la;
struct user_segment_descriptor *lp;
[...]
switch (uap->op) {
    [...]
    case I386_SET_LDT:
            if (largs->descs != NULL && largs->num > max_ldt_segment)
                return (EINVAL);
            set_pcb_flags(td->td_pcb, PCB_FULL_IRET);
            if (largs->descs != NULL) {
                lp = malloc(largs->num * sizeof(struct
                    user_segment_descriptor), M_TEMP, M_WAITOK);
                error = copyin(largs->descs, lp, largs->num *
                    sizeof(struct user_segment_descriptor));
                if (error == 0)
                    error = amd64_set_ldt(td, largs, lp);
                free(lp, M_TEMP);
            } else {
                error = amd64_set_ldt(td, largs, NULL);
            }
            break;
  • 从上面的代码看出,有一个指向i386_ldt_args结构体的指针,查看一下定义
struct i386_ldt_args {
    unsigned int start;
    union descriptor *descs;
    unsigned int num;
};
  • 从上面得知所有i386_ldt_args结构体的字段都是由用户控制的,用户使用i386_set_ldt()来指定这3个参数,那么查看i386_set_ldt()
  • int i386_set_ldt(int start_sel, union descriptor *descs, int num_sels);
  • 结合上面sysarch_ldt()的代码可以得知,如果我们指定NULL指针作为i386_set_ldt()的第二个参数(largs->descs),那么它最终会调用amd64_set_ldt()函数,传送largs作为第二个参数,一个NULL指针作为第三个参数
  • 接下来查看amd64_set_ldt()函数
int amd64_set_ldt(struct thread *td, struct i386_ldt_args *uap, struct user_segment_descriptor *descs);
    int
    amd64_set_ldt(td, uap, descs)
        struct thread *td;
        struct i386_ldt_args *uap;
        struct user_segment_descriptor *descs;
    {
    [...]
        int largest_ld;
    [...]
608        if (descs == NULL) {
609                /* Free descriptors */
610                if (uap->start == 0 && uap->num == 0)
611                        uap->num = max_ldt_segment;
612                if (uap->num == 0)
613                        return (EINVAL);
614                if ((pldt = mdp->md_ldt) == NULL ||
615                    uap->start >= max_ldt_segment)
616                        return (0);
617                largest_ld = uap->start + uap->num;
618                if (largest_ld > max_ldt_segment)
619                        largest_ld = max_ldt_segment;
620                i = largest_ld - uap->start;
621                mtx_lock(&dt_lock);
622                bzero(&((struct user_segment_descriptor *)(pldt->ldt_base))
623                    [uap->start], sizeof(struct user_segment_descriptor) * i);
624                mtx_unlock(&dt_lock);
625                return (0);
626        }
  • 从上面可以看出,漏洞就发生在amd64_set_ldt()这个函数中,如果第三个参数被设置成NULL时,就会执行之后的代码,完全是由用户控制的
  • 上面的两个if语句在610和612判断了uap->startuap->num,可以避免uap->num为0,接下来在614/615的if,如果mdp->ldt指针为NULL或者uap->start大于等于max_ldt_segment(1024),那么导致函数退出。如果mdp->md_ldt是一个非空的值,那么在触发这个bug之前可以通过增加一个初始入口进入LDT来完成,例如:
struct segment_descriptor desc = {0, 0, SDT_MEMRW, SEL_UPL, 1, 0, 0, 1, 0 ,0};
i386_set_ldt(LDT_AUTO_ALLOC, (union descriptor *) &desc, 1);
  • 接下来查看617-619行的漏洞代码
617                largest_ld = uap->start + uap->num;
618                if (largest_ld > max_ldt_segment)
619                        largest_ld = max_ldt_segment;
620                i = largest_ld - uap->start;
  • 从上面看出largest_ld = uap->start + uap->numlargest_ld是一个整数,代码行618-619确保largest_ld不大于max_ldt_segment(1024),但是,largest_ld是由用户控制的,可以通过设置uap->num为负数来绕过
  • 这个符号的错误最终导致在FreeBSD内核的一个堆溢出(之后调用的bzero()函数用一个极大的值作为它的len参数)
622  bzero(&((struct user_segment_descriptor *)(pldt->ldt_base))
623      [uap->start], sizeof(struct user_segment_descriptor) * i);

参考链接