[05] HEVD 内核漏洞之未初始化栈变量

0x00 前言

上一篇研究了一下基础的内核整数溢出漏洞,虽然漏洞利用还有些问题,但是原理上还是比较清楚的,后续有时间继续搞一哈,时间有限,我们继续下一个内核漏洞:未初始化栈变量。

0x01 漏洞原理

顾名思义,即内核中函数栈局部变量未初始化。

分析
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
54
55
56
57
58
typedef struct _UNINITIALIZED_MEMORY_STACK
{
ULONG Value;
FunctionPointer Callback;
ULONG Buffer[58];
} UNINITIALIZED_MEMORY_STACK, *PUNINITIALIZED_MEMORY_STACK;

NTSTATUS
TriggerUninitializedMemoryStack( _In_ PVOID UserBuffer)
{
ULONG UserValue = 0;
ULONG MagicValue = 0xBAD0B0B0;
NTSTATUS Status = STATUS_SUCCESS;
#ifdef SECURE
//安全版本
UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 };
#else
//漏洞版本
UNINITIALIZED_MEMORY_STACK UninitializedMemory;
#endif
PAGED_CODE();
__try
{
ProbeForRead(UserBuffer, sizeof(UNINITIALIZED_MEMORY_STACK), (ULONG)__alignof(UCHAR));

// Get the value from user mode
UserValue = *(PULONG)UserBuffer;

DbgPrint("[+] UserValue: 0x%p\n", UserValue);
DbgPrint("[+] UninitializedMemory Address: 0x%p\n", &UninitializedMemory);

// Validate the magic value
if (UserValue == MagicValue)
{
UninitializedMemory.Value = UserValue;
UninitializedMemory.Callback = &UninitializedMemoryStackObjectCallback;//自定义内核函数 无意义
}

DbgPrint("[+] UninitializedMemory.Value: 0x%p\n", UninitializedMemory.Value);
DbgPrint("[+] UninitializedMemory.Callback: 0x%p\n", UninitializedMemory.Callback);

#ifndef SECURE
DbgPrint("[+] Triggering Uninitialized Memory in Stack\n");
#endif
// Call the callback function
if (UninitializedMemory.Callback)
{
UninitializedMemory.Callback();
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

return Status;
}

很明显可以看到,不安全的版本中,结构体变量UninitializedMemory未进行初始化。栈上的局部变量,未初始化时,则会拥有前调用函数的随机垃圾值。

如果我们传入一个正确MagicValue,它会填充变量UninitializedMemory以及回调成员。如果传递的值不正确那么就不会填充。后面看到Callback检查,但是并没有什么用。

驱动程序拖进IDA看下:

img

img

img

此时数组偏移下标为(0xB)*4

img

img

var_C变量为控制码-0x222003,那么IO控制码为0x222003+(0xB)*4 = 0x22202f

找到漏洞函数,我们看到:

img

img

img

查看代码逻辑,比较成功会命中绿色块,在这里变量被填充了适当的值,此后在红色块中当回调函数被调用时也不会出错。

img

然而如果我们比较失败了,跳过了绿色快,继续下行调用函数的时候,我们栈成员变量无初始化,虽然通过了CallBack检查,但是调用的将是一个内核栈上的垃圾值!

img

这个值是不固定的,如果你尝试重现,很可能会在Windbg中看到不同的值。在虚拟机BSOD之前,让我们快速的看一下该变量距离当前栈起始位置有多远。

img

计算方式:`0x8a15ced0 - 0x8a15c9cc = 0x504 (1284 bytes)`

让我们通过回溯流程并对BSOD进行检查。

img

可以看到,回调指针指向了一个垃圾地址,执行出错。

0x02 漏洞利用:

覆盖内核栈上的整型指针为我们shellcode的地址是最终目标。
参考j00ru的文章
其中提到了内核栈喷射(Kernel Stack-Spraying)的概念。
在Windows系统中,内核栈不同于用户栈,其为一片共用空间,使用常规的的喷射方法是有很大风险的,比如覆盖到其他关键的地址。而原作者提到的用NtMapUserPhysicalPages的方法,使我们在用户态提权的时候完成对内核栈的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NTSTATUS
NtMapUserPhysicalPages (
__in PVOID VirtualAddress,
__in ULONG_PTR NumberOfPages,
__in_ecount_opt(NumberOfPages) PULONG_PTR UserPfnArray
)
(...)
ULONG_PTR StackArray[COPY_STACK_SIZE];

//
// This local stack size definition is deliberately large as ISVs have told
// us they expect to typically do up to this amount.
//
#define COPY_STACK_SIZE 1024

有一个未文档化的函数,NtMapUserPhysicalPages,我们不关心它用于干什么,但它的一部分功能是拷贝输入的字节到内核栈上的一个本地缓冲区。最大尺寸可以拷贝1024*IntPtr::Size(32位机器上是4字节=>4096字节)。

seebug的文章中可以看到NtMapUserPhysicalPages的实现:

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
NTSTATUS __stdcall NtMapUserPhysicalPages(PVOID BaseAddress, PULONG NumberOfPages, PULONG PageFrameNumbers)

if ( (unsigned int)NumberOfPages > 0xFFFFF )

return -1073741584;

BaseAddressa = (unsigned int)BaseAddress & 0xFFFFF000;

v33 = ((_DWORD)NumberOfPages << 12) + BaseAddressa - 1;

if ( v33 <= BaseAddressa )

return -1073741584;

v4 = &P;//栈地址

v39 = 0;

v37 = &P;

if ( PageFrameNumbers )

{

if ( !NumberOfPages )

return 0;

if ( (unsigned int)NumberOfPages > 0x400 )//如果要超过1024,就要扩展池,不过这里不用

{

v4 = (char *)ExAllocatePoolWithTag(0, 4 * (_DWORD)NumberOfPages, 0x77526D4Du);

v37 = v4;

if ( !v4 )

return -1073741670;

}

v5 = MiCaptureUlongPtrArray((int)NumberOfPages, (unsigned int)PageFrameNumbers, v4);//v4 要拷贝的目标 内核栈 a2,要覆盖的EoPBuffer 长度是4*NumberOfPages

调用过程NtMapUserPhysicalPages -> MiCaptureUlongPtrArray -> memcpy。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __fastcall MiCaptureUlongPtrArray(int a1, unsigned int a2, void *a3)//4*a1为长度   a2为构造的用户缓冲区 a3为内核地址
{
size_t v3; // ecx@1
v3 = 4 * a1; //长度
if ( v3 )
{
if ( a2 & 3 )
ExRaiseDatatypeMisalignment();
if ( v3 + a2 > (unsigned int)MmUserProbeAddress || v3 + a2 < a2 )
*(_BYTE *)MmUserProbeAddress = 0;
}
memcpy(a3, (const void *)a2, v3);
return 0;
}

那么在用户态下构造好栈缓冲区,填充好payload地址。在HacksTeam的利用代码中,可以看到这个过程。
整理一下:

exp工作流将如下:(1)将shellcode放在内存任意位置,(2)使用指向shellcode的指针喷射内核栈,(3)触发未初始化变量漏洞 (4)调用执行。
测试一哈,成功提权。

![img](file:///C:/Users/xutianyao/Documents/My Knowledge/temp/0e854bca-874d-459d-80d4-01618beb013a/128/index_files/10a48a9d774a12e6468654643dfdfd7c.gif)

0x03 漏洞反思

安全版本中提到的变量初始化的好习惯可以很好的帮助我们规避这类漏洞。

1
2
3
4
#ifdef SECURE
//安全版本
UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 };
#else

0x04 链接

j00ru关于NtMapUserPhysicalPages和内核堆栈喷涂技术:https://j00ru.vexillium.org/2011/05/windows-kernel-stack-spraying-techniques/

fuzzsecurity:https://www.fuzzysecurity.com/tutorials/expDev/17.html

雨涵师傅的翻译:https://bbs.pediy.com/thread-225179.htm