Windows内核漏洞利用教程

secM  1837天前

堆栈溢出漏洞

首先,我们将从HackSysExtremeVulnerableDriver中的vanilla栈溢出漏洞开始讲起。

当向堆栈上的缓冲区存放的数据超出其存储容量时(例如,向16字节缓冲区(这里的缓冲区可以是字符数组或类似对象)中存放20个字节时),多出来的数据将会写入附近的内存中,从而覆盖或破坏堆栈。

这里的核心思想是控制溢出过程,以便可以覆盖保存在堆栈中的返回地址,并在执行当前(易受攻击的)函数后,将返回我们的覆盖值,其中存放的是相应的shellcode

注意:在执行我们的shellcode之后,代码执行流程必须回到相应的应用程序,就这里来说,是交还给内核,否则,就会破坏应用程序。通常情况下,应用程序崩溃后,我们可以重新启动它,但是如果内核内存发生损坏的话,内核发出kernel panic,导致蓝屏死机,这是我们最不想要的。

为了解决这个问题,我们需要恢复执行路径,以便在执行shellcode之后,将执行流程返回到执行易受攻击的函数后面相应的函数。

含有堆栈溢出漏洞的代码

现在,我们已经了解了攻击思路,下面,让我们考察一下易受攻击的代码(位于StackOverflow.c文件中的函数TriggerStackOverflow)。首先,该函数会创建一个ULONG型数组,它可以容纳512个成员元素(在common.h头文件中,BufferSize被设置为512)。

1.png

存在堆栈溢出漏洞的函数

然后,内核会检查缓冲区是否驻留在用户空间中,并在非分页池中为其分配内存。

完成上述操作后,内核会将数据从用户空间的缓冲区复制到内核空间的KernelBuffer中,实际上,它本质上就是一个ULONG型数组。

1.png

溢出点

堆栈溢出

请大家注意RtlCopyMemory(它本质上就是memcpy)函数的第三个参数Size,它是用户空间缓冲区的长度,而非内核空间缓冲区的长度。这里就是发生缓冲区溢出的关键点。

漏洞验证

现在,为了验证这里是否为该漏洞的实际位置,我们将编写一个函数,让它调用函数StackOverflowIOCTLHandlerIOCTL。需要说明的是,具体IOCTL代码请参见exploit/common.h文件。

注意:我们本可以从编译后的驱动程序本身获得IOCTL代码,但是既然我们有这样的优势,为什么不用呢?

什么是IOCTL代码?

I/O控制码(IOCTL)用于用户模式应用程序和驱动程序之间的通信,或用于堆栈中驱动程序之间的内部通信。I/O控制代码可以通过IRP进行发送。”——Microsoft.com

基本上,如果驱动程序具有相应的IOCTL代码,就可以直接在驱动程序中调用内核函数。

要使用IOCTL代码,我们可以借助于DeviceIoControl函数,该函数的详细说明请参阅这里

DeviceIoControl函数的原型是:

1.png

DeviceIoControl函数的原型

我使用C++语言编写了一个函数,它可以通过DeviceIoControl来调用StackOverflowIoctlHandler,后者又会调用TriggerStackOverflow——一个含有堆栈溢出漏洞的函数。

我们知道该缓冲区是长度为512ULONG型元素,所以,我们可以再附加由me tasploit框架中的pattern_create.rb生成的100字节模式。

最后,将这个缓冲区的内容发送到HEVD,看看会发生什么情况。

注意:这个函数位于头文件StackOverflow.h中,并且由main函数来调用它。完整的代码,请访问我的代码库

1.png

用于利用堆栈溢出的POC

1.png

用于利用堆栈溢出的POC

Win7系统上编译并执行二进制文件后,我们可以在WinDbg中看到:

1.png

发生在WinDbg中的崩溃

如您所见,这里存在访问违例,EIP指向了31624130

利用me tasploitpattern_offset.rb的模式之后,我们会发现其偏移量为32。下面,我们开始利用这个漏洞。

堆栈溢出漏洞的利用方法

为了利用这个漏洞,我们只需要用HEVD中提供的TokenStealingPayloadWin7 shellcode覆盖保存的返回地址,就可以了。

注意:为了防止崩溃,可能需要对shellcode稍加修改,这一个任务留作作业。

获取Shell

让我们首先检查当前身份是否为普通用户。

1.png

普通用户

可以看出,当前身份只是普通用户。

在运行漏洞利用代码之后,转身变为ntauthority/system用户。

1.png

NT Authority/SYSTEMShell

参考资料

类型混淆漏洞

什么是类型混淆漏洞?

类型混淆是这样一种漏洞,即应用程序没有验证对象的类型(函数、数据类型等),就直接按预期类型对其进行处理,所以,当传递给程序的其实是其他类型的对象时,就会出现这种类型的安全漏洞。

含有类型混淆漏洞的代码

现在,我们已经了解了类型混淆漏洞的概念,下面,让我们开始考察含有这种类型的漏洞的代码(位于typeconfusion.c中的函数triggerTypeConfusion中)。

首先,内核会检查缓冲区是否位于用户空间中,然后,在非分页池中为其分配内存空间。完成上述操作后,内核将objectID从用户空间缓冲区赋值给内核空间缓冲区,并对对象类型执行相同的操作。

1.png

objectIDobjectType赋值

之后,内核会对对象调用TypeConfusionInitializer函数(内核模式而非用户模式)。

1.png

为对象调用TypeConfusionInitializer函数

让我们考察一下这个函数:

1.png

函数类型ConfusionobjectInitializer

这个函数首先接收对象,并调用对象中的函数指针。

让我们看看kernel_type_confusion_object的结构(本质上是一个结构体),它位于typeconfusion.h头文件中。这个头文件保存了用户空间对象和内核空间对象的定义,这使得该漏洞的利用方法要比堆栈溢出漏洞的利用方法更容易一些。

1.png

对象原型

首先,让我们看看用户模式对象中所包含的内容。实际上,这个用户模式对象是一个包含2个成员的结构体,这些成员为:

l 对象ID

l 对象类型

在内核模式对象的情况下,它也是一个包含2个成员的结构:

l 对象ID

l 第二个成员是UNION,可以保存:

1. 对象类型

  1. 回调函数(函数指针)

我们知道UNION变量一次只能存放一个成员,这里它可以是一个object Type,也可以是一个指向TypeConfusionInitializer函数调用的函数的指针。

当函数TriggerTypeConfusion函数没有验证第二个成员是objectType还是Callback时,就会触发类型混淆漏洞。

类型混淆漏洞的利用方法

为了利用这种类型的漏洞,只需传递一个结构体,并且让该结构体的第二个成员为我们想要从内核空间调用的函数的地址即可。

就这里来说,该结构体的第二个成员存放的是令牌窃取Shellcode代码的地址,以替换我们的进程的令牌,这样,当创建新进程时,将使用该令牌。

但是,需要注意的是,这里需要使用HEVD提供的shellcodeTokenStealingPayloadWin7无法正常使用,并且会导致系统崩溃)。

修改shellcode

由于函数TypeConfusionInitializer会调用Callback指针,既然它是一个函数,所以,我们需要设置函数的prologueepilogue,并将ret 8改为ret

注意:这里我将shellcode函数编译为裸函数,如果您不这样做的活,也可以直接使用提供的shellcode。之所以这么做,只是不喜欢编译器将额外的代码添加到shellcode代码中而已。

我的漏洞利用代码可以从这里下载。

获得Shell

让我们首先检查当前身份是否为普通用户。

1.png

普通用户

可以看出,当前身份只是普通用户。

运行漏洞利用代码后,我们将华丽变身为ntauthority/system用户。

1.png

利用类型混淆漏洞获取SYSTEM Shell

整数溢出

下面,我们开始介绍如何利用HacksysExtremeVulnerableDriver中的整数溢出漏洞。

什么是整数溢出漏洞?

对于不了解整数溢出的人来说,听到这个名称的时候可能会非常困惑——整数怎么会溢出呢?

实际的整数是不会溢出。CPU会将整数存储在固定长度的内存空间中(注意,这里我们不会讨论堆或类似的内容)。如果您熟悉C/C++之类的编程语言的话,您可能还记得各种数据类型都具有固定的长度。

在大多数机器和操作系统上,字符变量长度为1字节,整型变量的长度为4字节。这意味着char数据类型可以保存长度为8比特的值,取值范围从0255,如果是带符号的值,则取值范围为-128127。整型变量也是如此,在整型变量长度为4字节的机器上,可以保存0232次方-1之间的值(无符号值)。

现在,让我们考虑使用一个最大值为232次方-10xFFFFFFF的无符号整型变量。取最大值后,如果再加1,会发生什么情况呢?由于所有32位上的值均为1,因此,加1将使其变为一个长度为33位的值,但由于存储器只能容纳32位,因此这32位上的值将被设为0

在执行上面的操作时,CPU通常会将数字加载到32位寄存器(此处指x86)中,加1时会设置进位标志,而寄存器各位的值为0,因为现在所有32位上的值都为0

现在,如果进行长度检查,查看值的长度是否大于指定的值,比如10,则该检查将返回fail,但如果不存在长度限制,则比较操作将返回true

为了加深理解,让我们来考察一下具体的漏洞代码:如何利用HEVD中的整数溢出漏洞来获得Windows内核中的代码执行权限。

含有整数溢出漏洞的代码

现在已经了解了整数溢出漏洞的相关概念,接下来,我们来分析一下含有该漏洞的代码(该漏洞位于IntegerOverflow.c中的函数TriggerIntegerOverflow中)。

首先,该函数会创建一个ULONG型数组,该数组可以存放512个成员元素(在common.h头文件中,BufferSize被设置为512)。

1.png

IntegerOverflow.c中含有整数溢出漏洞的函数

然后,内核会检查缓冲区是否位于用户空间中,然后,它还会为我们打印一些信息。这些信息对于我们来说非常有帮助。

完成上述操作后,内核会检查数据的长度(以及终结符的长度,即4个字节)是否大于KernelBuffer的长度。如果是的话,则退出,并且不会将用户空间中的缓冲区内容拷贝到内核空间的相应缓冲区中。

1.png

长度检查

但是,如果情况并非如此,则继续进行,并将数据复制到内核缓冲区中。

这里要注意的另一件事是,如果它在用户区缓冲区中遇到BufferTerminator,就会停止复制,并继续执行后面的代码。因此,我们需要将BufferTerminator放在用户模式缓冲区的末尾。

1.png

将用户空间数据复制到内核空间的函数堆栈

整数溢出

IntegerOverflow.c的第100行代码的问题在于,如果我们提供的size参数为0xFFFFFFFC,然后再加上BufferTerminator的长度(这里是4个字节),则有效长度变为:0xFFFFFFFC+ 4 = 0x00000000,即大于KernelBuffer的长度,但是,由于我们通过了数据长度的检查,所以,能够将该缓冲区内容复制到内核空间。

漏洞验证

现在,为了验证这个漏洞,我们将把缓冲区内容发送到HEVD,但是将0xFFFFFFFC作为缓冲区的长度进行传递。现在,我们不用设置一个巨大的缓冲区来令内核崩溃,相反,只需发送一个小型的缓冲区并确认漏洞即可。

1.png

触发整数溢出的PoC

由于我们知道缓冲区长度为512ULONG型元素,因此,我们可以发送这么长的数据,并查看内核的反应。

注意:这里的重点是DeviceIoControl的第4个参数,而不是实际数据。

最后,将该缓冲区内容发送到HEVD,看看到底会发生什么情况。

1.png

成功触发整数溢出漏洞

正如您看到的那样,UserBufferSize的值为0xFFFFFFFC,但我们仍设法绕过了长度的有效性检查并触发了整数溢出漏洞。

我们发现,通过设置0xFFFFFFFC,我们可以绕过长度检查,接下来要做的事情,就是在UserBuffer之后放置一个模式(一个唯一无二的模式),然后放置终结符,以找到保存的返回指针。

如果您不清楚该如何操作,请阅读本文的前面部分,那里有具体的介绍。

下面,我们开始介绍如何利用这个漏洞。

整数溢出的利用方法

下面,我们会通过HEVD提供的TokenStealingPayloadWin7 shellcode来覆盖保存的返回地址,这样,我们就大功告成了。

注意:为了避免崩溃,需要对shellcode稍作修改,这项任务留作课后作业。

获得shell

下面,首先来检查一下当前的身份是否为普通用户。

1.png

普通用户

可以看出,当前身份只是普通用户。

运行我们的漏洞代码之后,我们的身份已经变为ntauthority/system

1.png

成功利用整数溢出漏洞

完整的代码,大家可以从作者的代码库中下载。

参考资料

原文地址:https://pwnrip.com/windows-kernel-exploitation-part-1-stack-buffer-overflows/

最新评论

昵称
邮箱
提交评论