### `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`)条目。`FreeBSD`的`amd64`版本仍然暴露出了在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)ldt`或`i386_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->start`和`uap->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->num`,`largest_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);
```
### 参考链接
* [http://www.coresecurity.com/advisories/freebsd-kernel-amd64setldt-heap-overflow](http://www.coresecurity.com/advisories/freebsd-kernel-amd64setldt-heap-overflow)
Unavailable Comments