通过HEVD探究各种漏洞原理
背景知识
本篇 blog 主要讲述的内容是通过HEVD,来讲述对二进制漏洞的理解以及各种漏洞形成的原理,深化对各种漏洞形态以及特点的理解,有关HEVD 的优秀writeup 有很多,由于对windows 的内核驱动并不擅长,但漏洞原理都是相通的,这里我也只是写一些自己的理解。
源码 https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/tree/master/Driver/HEVD
首先在看HEVD之前,得了解windows驱动开发的一些常用的框架,了解什么是IOCTL、DriverEntry、驱动注册路径,驱动设备、注册设备各类系统接口函数 等概念。
栈溢出
有关栈溢出的这里就不多介绍了。
TriggerBufferOverflowStack(v3, IrpSp->Parameters.Create.Options);
任意地址写
起初不理解这段代码的漏洞,任意地址写,但随着不断的查阅资料,不断解惑。
我理解这段代码中可以通过传入 v1 的值来更改v2地址指向的内容。因此如果v1地址保存的内容是一个非法的地址,传递给v2地址指向的内容。这种情况是正常更改了,但是没有人调用v2地址指向的内容,那漏洞就不成立。
在这篇blog中看到了答案 https://tttang.com/archive/1344/ 。
现在可以透彻的理解任意地址写漏洞了。在具有任意地址写漏洞的程序中,需要满足以下条件:
1. 控制被任意写地址的指针:这段代码中的被任意写的指针是 *v2,如果 *v2 指针是返回地址,注意这里是指针地址,而不是指针地址指向的内容,这两者意思完全不同。
2. 可控制的指针:这段代码中控制的指针还有 *v1, 如果*v1 指针指向内容是shellcode地址。
3. 写入操作: 这里指的是*v2 = *v1
,将*v1 指针地址指向的内容(shellcode) 赋给 *v2 指针地址(返回地址)指向的内容。
4. 触发执行: 如果控制流到达了被修改的的地址时,比如这里的返回地址已经被修改到了shellcode地址。那么程序会跳转到这里来。
另外,任何利用和漏洞原理要结合汇编来理解。
1 |
|
以上就是 TriggerArbitraryWrite
函数的代码,我将一些无关的代码精简了。
另外在 https://bbs.kanxue.com/thread-270176.htm 中的漏洞利用中也可以理解
其中的 &dwZero 表示的是what 的地址,这里的地址是0 ,可以将shellcode 写到 地址为0 的内存空间中。然后 pXHalQuerySystemInformation 是where 的指针,这里应该是可以控制的程序地址,并且这个地址会有控制流执行到这里。 一言以蔽之,当别的程序执行到 pXHalQuerySystemInformation指针指向的内存地址的时候,那么程序就会跳转到 shellcode 中进行执行。
UAF
UAF漏洞,需要以下的几个步骤:
1. 申请一块内存以后释放掉它,但是没有清空该内存的指针
2. 重新申请一块同样大小的内存,此时这两个指针对指向同一块内存
3. 对第一步的指针进行操作,它将会影响到第二步申请的指针指向的内存
申请内存分配
在这个函数中,可以很明显的看到调用ExAllocatePoolWithTag( ) 函数分配内存大小为0x58 给v0 局部变量,并且后续这块分配v0 内存块做了以下的操作:
1. 将 v0+1 后的0x54 个内存都赋值为”A“
2. 将内存的最后一个字节设置为0
3. 并且将内存的前四个字节设置为 UAFObjectCallbackNonPagedPool 函数的地址。
4. 将v0内存的地址赋值给g_UseAfterFreeObjectNonPagedPool
仔细看一下g_UseAfterFreeObjectNonPagedPool,这是一个在.data 段的全局变量
释放之前给指针分配的内存
这里会调用这个 ExFreePoolWithTag() 函数来释放内存,这里释放的是这个全局变量g_UseAfterFreeObjectNonPagedPool的指针,前面在分析指针指向内存分配的时候,了解到g_UseAfterFreeObjectNonPagedPool 这个全局变量指向的内存就是分配的内存。
并且在释放之前会判断 g_UseAfterFreeObjectNonPagedPool 是否为0
这里值得注意的是 g_UseAfterFreeObjectNonPagedPool 这个全局变量在释放后,并没有被完全置空,所以 g_UseAfterFreeObjectNonPagedPool 全局变量的指针仍然指向 0x58 内存块起始地址,只是内存块内的数据已经被释放了。
所以如果有人能调用 g_UseAfterFreeObjectNonPagedPool 指针,则将造成UAF 漏洞。
调用 g_UseAfterFreeObjectNonPagedPool 指针
在 UseUaFObjectNonPagedPool() 函数中,可以看到会调用UseUaFObjectNonPagedPool 指针。
在前面分配内存,并且给内存赋值的过程中,UseUaFObjectNonPagedPool 指向的内存0x58内存中,前四个字节保存的是 UAFObjectCallbackNonPagedPool 函数的地址。
因此最终调用的是 UAFObjectCallbackNonPagedPool 这个函数。如果 0x58内存块中的数据被释放后,UAFObjectCallbackNonPagedPool 函数的地址就不复存在,所以通过调用UseUaFObjectNonPagedPool 指针来访问内存就会出现内存错误,因为内存已经被释放了。
AllocateFakeObjectNonPagedPool 函数
为了能够更好的利用这个漏洞,还存在 AllocateFakeObjectNonPagedPool 函数,这个函数就是重新分配一个0x58大小的内存。由于内存管理中在内存分配的时候,上一块已经被释放的0x58大小的内存没人用,这次分配一样大小的内存,就会覆盖上去,这样的话 g_UseAfterFreeObjectNonPagedPool 全局变量指向的内存区域就是这一次分配同样大小的内存区域。那么就可以控制程序的执行流程,调用shellcode。
总结一下: 其实这个漏洞的模式还是很理想的,在真实的UAF 漏洞环境中,是不会有 AllocateFakeObjectNonPagedPool 这样的函数。其实最好的利用方式就是直接AllocateUaFObjectNonPagedPool 分配内存的时候,就将shellcode地址放到内存的前四个字节,因为在调用g_UseAfterFreeObjectNonPagedPool 指针的时候,并没有校验指针指向的内存是否合法。
因此也存在任意地址访问的漏洞。
整数溢出
这个案例是整数溢出的案例,有关整数溢出相关原理不详细展开说:
- 1. 有符号整数可以溢出。
- 2. 无符号的整数存在回绕的问题,常用于绕过判断语句
- 3. 有符号整数在某些情况下会强转成无符号整数,就会出现溢出。比如memcpy,strncpy
函数 TriggerIntegerOverflow 中存在整数溢出漏洞
1. 定义一个局部变量分配的大小是 512* 4 字节 = 2048
2. 将userbuffer 用户传入内存地址赋值给 局部变量 v3, 其中的size 是userbuffer 的大小。
3. 这里会判断用户传入的size 的大小,首先是size +4 <=2048,说明size 的值要小于2044, 因为userbuffer 的最后四个字节是0x0BAD0B0B0,是用户终止userbuffer 拷贝到kernelbuffer 的符号;这里如果Size的值是一个很大的值,在 size +4 后,就会发生整数回绕的问题,进而小于2048,绕过这个判断。
4. 这里会将userBuffer 逐个字节的拷贝到 KernelBuffer,如果前面的Size+4 <=2048 被绕过了,那么如果userBuffer 的字节数量,超过了给KernelBuffer 分配的2048个字节内存,就会引发缓冲区溢出的问题。
这个案例的漏洞利用比较好理解,首先输入 0x820个字节覆盖kernelBuffer缓冲区,然后再传入4个字节覆盖栈底,然后覆盖 返回地址 ret 为 shellcode 的地址,这样以来,函数执行流程中会跳转到shellcode 执行。
空指针解引用
空指针引用(NULL POINT),程序在申请完内存并设置好指向内存的指针,再该内存被使用后,程序会对该内存进行释放,并且对指向该内存的指针进行清空(置空,设为NULL), 如果之后还对这个指针进行使用,很有可能会出现问题。
TriggerNullPointerDereference 漏洞函数中,按照空指针解引用漏洞的触发条件来分析:
1 . 给局部变量 v1 分配0x08 大小的内存区域, 并把内存地址赋值给指针 v1
2. 将用户输入的 userBuffer 内存地址赋值给指针v3
3. 判断 指针v3 指向内存区域的的前四个字节数据是否为 0xBAD0B0B0 ,然后进入到正常的代码流程中,给指针v1 指向的内存区域传入数据,具体操作是 前四个字节为0xBAD0B0B0,后四个字节为 NullPointerDereferenceObjectCallback 函数地址。
4. 如果 [3] 中的判断错误,那就会释放之前给指针 v1分配的 0x08字节大小的内存区域,然后将指针 v1 设置成空指针。
5. 接下来会执行v1[1],也就是v1 后四个字节保存的函数地址,这里就存在漏洞,如果用户输入的userBuffer 数据中不包含 0xBAD0B0B0,那指针v1 指向的内存区域会被释放,并且指针v1 会被指向内存地址为0(置空),没有指向任何有效内存地址,因此调用 v1[1] 函数就会出现空指针解引用的漏洞。
在漏洞利用的时候,可以设置shellcode 的内存首地址是NULL,然后在后四个字节中放入shellcode 的地址,在最后执行的时候,会执行shellcode。
但是这只是在理想的情况下,大部分的空指针解引用的问题就是程序员在开的时候,在逻辑错综复杂的代码中,没有在指针调用的时候,没有对指针是否为空进行判断。如果恰好有个地方可以释放指针指向的内存空间并且将指针置空,但在离置空指针较远的地方存在没对指针进行校验就引用指针中的数据,由于指针已经置空了,因此指向的内存区域是无效的,所以程序会产生拒绝服务的现象。
未初始化栈变量
未初始化栈变量 指的是在函数中,局部变量没有被初始化。 https://cwe.mitre.org/data/definitions/457.html
由于局部变量未初始化,因为在函数调用的时候,函数的栈空间分配时,可能会给这个局部变量分配一些意想不到的数据,因此如果当有程序调用这个局部变量的时候,就会从栈上获取这个数据,从而造成内存损坏的问题。比如拒绝服务,崩溃等危害。
在TriggerUninitializedMemoryStack函数中:
1. UninitializedMemory 变量在整个函数中被定义。
2. 传入userBuffer
3. 判断userBuffer 前四个字节是否是 0xBAD0B0B0, 如果是的话,就初始化 UninitializedMemory 局部变量。
4. 就算没有初始化UninitializedMemory局部变量,最终还是会调用 UninitializedMemory.callback() 回调函数,这里就出现了问题,如果没有对UninitializedMemory局部变量进行初始化,就直接从UninitializedMemory.callback 指针指向的内存区域中获取函数地址,并调用该函数,由于没有初始化,所以指针指向的内存地址是未知的,所以一切行为就不可控,进而产生漏洞。
这里在 https://bbs.kanxue.com/thread-270204.htm 中进行漏洞利用的时候,callBack 指针指向的内存地址是无法预测的,因此使用了栈喷射技术
DoubleFetch
CTF wiki 的说法: https://ctf-wiki.org/en/pwn/linux/kernel-mode/exploitation/race/double-fetch/
这里对Double Fetch 的讲述很明确,一个用户态线程准备数据并通过系统调用进入内核,该数据在内核最终有两次被取用,内核第一次取用数据进行安全检查(比如缓冲区大小、指针可用性等),当检查通过后内核第二次取用数据进行实际处理。而在两次取用数据之间,另一个用户态线程可创造条件竞争,对已通过检查的用户态数据进行篡改,在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升。
结合代码来看,要明确这几个信息:
1. 首先确定从用户空间输入的信息,也就是userDoubleFetch 结构体中成员变量指向用户缓冲区的内存地址。
2. 找到第一次fetch 的地方,也就是检查大小的地方,但这里检查的是userDoubleFetch->size 的大小,这个值是用户空间的值,可以在使用之前,检查之后进行修改,比如检查的时候使用0x100,在检查之后,立马在竞态线程中,修改为0x900。
3. 第二次fetch 在memcpy,也就是使用userDoubleFetch->size 值得地方,如果这里在通过检查之后,更改为了0x900,会造成缓冲区溢出的问题。
漏洞利用的话,只需要将shellcode 覆盖到返回地址就行了,kernBuffer离ebp栈底的距离是0x81C(2076个字节),那么ebp栈底的位置在2076+4,覆盖到返回地址需要 2076+4+4,也就是2084个字节。
安全修复的代码,根据修复代码修复的逻辑可以进行如下的理解:
1. 首先将 UserDoubleFetch->Size 的值赋值给局部变量 userBuffersize,因此 userBuffersize 在当前线程函数中是不会被改变的。
2. 然后判断局部变量userBuffersize的大小是否超过内核缓冲区kernelBuffer,
3. 其实这么做的话,我们知道doublefecth 是在第一次fetch 之后,通过多线程的竞态条件下更改用户缓冲区大小也就是UserDoubleFetch->Size 的值,会造成memcpy 的缓冲区溢出,但是这里由于 userBufferSize 是局部变量,因此在第一次fetch检查大小,和第二次fetch 使用之间的时间差内不会被更改,从而避免了doublefetch。
参考: https://www.freebuf.com/vuls/341170.html
https://yuvaly0.github.io/2020/09/15/hevd-writeups