上一章,我们搭建好了内核双机调试环境,实体机作为调试机,虚拟机win7Sp1x86作为被调试机。现在我们先熟悉一下HackSysExtremeVulnerableDriver这个用来练习内核漏洞利用的项目,后面简称(HEVD)下一章我将演示一些漏洞利用的技术。
本次将用到的工具如下:
- 双机内核调试环境
- HackSys Extreme Vulnerable Driver (HEVD) - 编译版本,和源码都要
- OSR Driver Loader(驱动加载器)
- DebugView (from SysInternals Suite)
- Visual Studio 2013(或任何你喜欢的版本)
安装测试 HEVD
首先,我会介绍如何安装HEVD,然后配置调试器和被调试机器显示输出调试字符串信息以及HEVD项目的符号信息,最后使用HEVD项目自带的漏洞利用程序,测试一下漏洞是否可以成功触发!
查看调试字符串
HEVD内置了很多漏洞利用时的调试字符串,我们可以使用Windbg在调试机查看,也可以使用DbgView在被调试机器中查看这些调试信息。
在安装HEVD之前,首先需要开启调试输出配置,然后在被调试机器打开DbgView,才能看到安装驱动时的调试信息。
配置调试器
建立好双机调试以后,让Windbg中断下来,输入开启打印调试字符串命令:
ed nt!Kd_Default_Mask F
然后,让被调试机再次运行起来,输入运行命令:
g
配置被调试机器
我们需要以管理员权限
运行DbgView。然后我们选择菜单:
Capture -> Capture Kernel
安装内核漏洞驱动
首先我们需要在被调试机器中下载好,预编译好的二进制文件,以及源码包。安装并测试!
我们可以在HackSysTeam的github上release页面找到最新版本的:
https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/releases
预编译压缩包中有两个版本,我们选择32位的i386,打开刚才下载的OSR Driver Loader工具,加载32位的HEVD驱动。
在OSR工具的Service Start项,选择Automatic,然后点击下面的Register Service按钮,提示成功后,再点击
Start Service按钮!
如果驱动服务安装并启动成功,我们可以看到在windbg或者被调试机器的DbgView中看到打印出的banner信息:
添加符号
预编译的HEVD包中含有调试符号PDB文件,我们可以将这个PDB文件添加到windbg中,方便我们调试时定位函数以及在源码中的位置等。
首先我们中断调试器,然后查看一下所有加载的模块:
lm
可以使用过滤命令,来找到HEVD模块:
lm m H*
我们可以看到windbg并没有使用任何符号,这个很好解决,首先开启:
!sym noisy
然后使用reload命令查看未加载的符号路径:
.reload /f
可以看到这里有两个符号路径,我们选择其中一个:
d:\mss\HEVD.pdb\38ACF1BD8B354E07B7A8C3554683ABD71\HEVD.pdb
比如我就选择第一个,然后按照这个路径创建一模一样的目录,然后把HEVD.pdb拷贝进去,
然后重新执行.reload命令,随后可以使用x命令查看关于HEVD的符号信息了:
x HEVD!*
测试漏洞利用
在下载的HEVD压缩包内有一个漏洞利用测试程序,我们可以传入不同的参数测试每种类型的漏洞,效果是打开一个system权限的cmd:
如果打开该文件时提示缺少msvcr100.dll类库,自己拷一个32位的就行过来,放在同目录下就行!
测试池溢出利用:
我们看到利用成功后弹出了一个system权限的cmd程序,并且windbg中也打印出了相关的漏洞利用调试信息:
Hi Driver,Let’s Talk!
像r3的漏洞利用一样,我们需要找到一个可以破坏程序执行的输入点,如果在r3程序就会崩溃,在内核r0,系统就会蓝屏。
为了能和驱动通讯,我们需要使用IOCTL码,也叫输入输出控制码。可以让我们从r3发送一些字符串给驱动,这也是我们漏洞利用很重要的一点:
HEVD中包含了各种类型的漏洞,每一个漏洞都可以使用IOCTL加精心构造的输入Buffer来触发。其中有一些触发后,可能会让你的系统蓝屏!
找到Device name & IOCTLs
在开始与驱动通讯之前,我们需要知道两件事:
- 驱动创建的设备对象名(如果驱动没有创建任何设备对象,那我们是不可能与驱动通讯的)
- 驱动接收的IOCTLs列表
HEVD是一个开源项目,所以我们可以直接从源码中阅读有用的信息。现实生活中的漏洞挖掘大部分情况下我们是没有源码的,有时候只能去逆向来获取相关信息。
来看一下HEVD项目源码中创建设备的相关代码
上面显示了设备对象的名称,现在我们来找一下IOCTLs, 我们将从IRPs数组中找:
https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HackSysExtremeVulnerableDriver.c#L106
连接IRP_MJ_DEVICE_CONTOL
的函数用来分发各种IOCTL给驱动,我们需要看一下这个函数:
函数中有一个switch来分发各种IOCTL给相应的处理函数,我们可以在头文件中找到这些值得定义:
https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/master/Driver/HackSysExtremeVulnerableDriver.h#L57
编写客户端程序
现在拿到了所有IOCTLs,我们可以使用我们自己编写的程序来和驱动通信,把拿到的IOCTL,写到我们自己程序的头文件中:
#pragma once
#include <windows.h>
const char kDevName[] = "\\\\.\\HackSysExtremeVulnerableDriver";
// IOCTLs
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW_GS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_NON_PAGED_POOL_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x804, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_USE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x807, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_TYPE_CONFUSION CTL_CODE(FILE_DEVICE_UNKNOWN, 0x808, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_INTEGER_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x809, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_NULL_POINTER_DEREFERENCE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80A, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80B, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80C, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_DOUBLE_FETCH CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80D, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_INSECURE_KERNEL_FILE_ACCESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80E, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_MEMORY_DISCLOSURE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80F, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_PAGED_POOL_SESSION CTL_CODE(FILE_DEVICE_UNKNOWN, 0x810, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_WRITE_NULL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x811, METHOD_NEITHER, FILE_ANY_ACCESS)
每一个IOCTL控制码是被一个标准宏创建的,定义在windows头文件winioctl.h中:
如果你添加了windows.h头文件,它会自动包含上图中这个宏的。
现在我们准备写一个简单的程序来跟驱动通信。首先,我们使用CreateFile
打开设备对象,然后我们可以用DeviceIoControl
发送IOCTl控制码。
下面是一个简单的例子,发送STACK_OVERFLOW ioctl 给驱动程序:
#include <stdio.h>
#include <windows.h>
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)
const char kDevName[] = "\\\\.\\HackSysExtremeVulnerableDriver";
HANDLE open_device(const char* device_name)
{
HANDLE device = CreateFileA(device_name,
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL
);
return device;
}
void close_device(HANDLE device)
{
CloseHandle(device);
}
BOOL send_ioctl(HANDLE device, DWORD ioctl_code)
{
//prepare input buffer:
DWORD bufSize = 0x4;
BYTE* inBuffer = (BYTE*) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize);
//fill the buffer with some content:
RtlFillMemory(inBuffer, bufSize, 'A');
DWORD size_returned = 0;
BOOL is_ok = DeviceIoControl(device,
ioctl_code,
inBuffer,
bufSize,
NULL, //outBuffer -> None
0, //outBuffer size -> 0
&size_returned,
NULL
);
//release the input bufffer:
HeapFree(GetProcessHeap(), 0, (LPVOID)inBuffer);
return is_ok;
}
int main()
{
HANDLE dev = open_device(kDevName);
if (dev == INVALID_HANDLE_VALUE) {
printf("Failed!\n");
system("pause");
return -1;
}
send_ioctl(dev, HACKSYS_EVD_IOCTL_STACK_OVERFLOW);
close_device(dev);
system("pause");
return 0;
}
尝试编译上面c程序源码,然后放到虚拟机运行,打开DbgView然后查看驱动输出的调试字符信息:
正如上图所示,驱动收到了我们发送的控制码,然后打印出了调试字符串信息。
实验:走个崩溃吧~~
尝试输入0x1000的Buffer size,直到驱动程序崩溃,因为虚拟机在调试模式下,崩溃时机器不会蓝屏,而是被WinDbg接管异常。
尝试分析一下崩溃时信息详细信息,使用以下命令打印出崩溃详情:
!analyze -v
其他有用的命令:
k - stack trace
kb - stack trace with parameters
r - registers
dd [address]- display data as DWORD starting from the address
想了解更多命令,可以使用windbg提供的帮助文件:
.hh
在我们上面写的简单程序中,用户层输入的buffer用字母 “A” -> asciii 0x41 填充.
RtlFillMemory(inBuffer, bufSize, 'A');
所以只要我们分析崩溃详情时看到了这个字母,就说明我们可以从用户层输入buffer填充内核层的内存。
附录
-
http://expdev-kiuhnm.rhcloud.com/2015/05/17/windbg/ – introduction to WinDbg (by Massimiliano Tomassoli)
-
https://github.com/mwrlabs/win_driver_plugin – An IDA Pro plugin to help when working with IOCTL codes or reversing Windows drivers (by Sam Brown)