0x1 整数溢出
0x1.1 成因分析
整数包括无符号整数和有符号整数,但是对于计算机来说,区分无符号整数和有符号整数意义不大,比如”-1”,无符号整数来说,其值为4294967295
,也就是0xFFFFFFFF
,编译器根据数据类型的不同,生成不同的代码,规定了每个数据变量的长度。在自然语义环境中,如果是4294967295加上4,最终会得到4294967299这个数字,但是,在计算机语义中,这个是在这个数据类型中是最大的,加上4就需要向高位拓展。这样就会丢弃拓展的高位。整数溢出就是这样产生的。例如,0xFFFFFFFF
加上4
之后,得到的数据就是3
。
整数溢出漏洞的作用是什么,整数溢出主要是为了绕过可能的长度检查。如下的例子,如果输入任意正整数,都不可能使得b+c 小于 a,但是如果输入的c是0xFFFFFFF0,因为整数溢出,0xFFFFFFFA+0x9 = 0x3.如此就绕过了长度检查。
HEVD的IntegerOverflow位于TriggerIntegerOverflow函数中,直接通过反汇编看,已知函数栈空间为0x820+0x04=0x824,也就是说要实现栈溢出需要0x824+0x04的大小实现。但是在代码中,对缓冲区长度进行了校验,根据自然语义下理解,缓冲区长度加上4要小于0x800,说明缓冲区不可能超过0x800,这样就不会造成栈溢出。
但是由于整数溢出,当我传入的UserBufferLength为0xFFFFFFFF,加个4,得0x3,这样自然就绕过了大小的限制。
0x1.2 漏洞利用
根据分析代码,明确使用栈溢出进行利用,使用整数溢出绕过长度检查。查看代码,要使进行缓冲区复制需要两个条件,Buffer内容不为0xBAD0B0B0,长度小于UserBufferLength/4。在使用整数溢出的是和,第二个条件得到满足,但是前面也说了实现栈溢出需要0x824+0x04的大小的缓冲区,所以,构造的payload主要构成是这样的,0x824长度用于填充缓冲,0x04是提权shellcode的地址,最后四个字节内容为0xBAD0B0B0,用于终止缓冲区复制。
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。源码是修复之后的代码。
含有漏洞代码主要位于HackSysExtremeVulnerableDriver
中的IrpDeviceIoCtlHandler
函数中,这个函数包含了HEVD所有的漏洞类型。
0x2.2 成因分析
栈溢出位于HEVD控制码为0x222003的函数处,也就是sub_44517E
,很显然,在sub_44517E
函数中,将有R3传入的缓冲区,以及该缓冲区的大小传入函数sub_4451A2
,在sub_4451A2
函数中,并没有对传入的缓冲区大小进行校验,即判断两个缓冲区大小。导致我们传入大于KernelBuffer的大小的UserBuffer,导致栈溢出。
根据IDA的解析结果来看,var_1c占了1C大小的空间,KernelBuffer占据了1C-81C共计800h大小的空间,这样子一共占用了81Ch大小的空间,加上返回地址4h的空间,一共占了820h的空间,所以我们构造栈溢出的话,只需要构造820h+4h的空间即可。
从源码来看,存在漏洞的版本,直接按照UserBuffer大小将UserBuffer复制给KernelBuffer,而修复之后的版本,是按照KernelBuffer大小将UserBuffer复制给KernelBuffer,由此修复了漏洞。
在HEVD!TriggerBufferOverflowStack
处下断,以及在Buffer复制的地方下断,首先断在HEVD!TriggerBufferOverflowStack
开头,查看栈顶寄存器为0x98075bd4,当运行到memcpy处,查看目的地址,也就是第一个参数地址为0x980753b4,两者相减,大小为0x00000820。也就是说,只需要构造一个大小为0x820+0x04的缓冲区,其中前0x820用于覆盖KernelBuffer,最后4个字节用于栈溢出,只需要将提取的shellcode地址放到最后四个字节处就可以实现漏洞利用。
0x2.3 漏洞利用
首先,需要打开驱动设备,在没有源码的情况下,在DriverEntry
函数中,创建了一个名为\\Device\\HackSysExtremeVulnerableDriver
的Device,所以在R3也应该创建\\\\.\\HackSysExtremeVulnerableDriver
的Device。
接着,如上所说,应该构造符合条件的Shellcode。具体就是构造一个大小为0x820+0x04的缓冲区,其中前0x820用于覆盖KernelBuffer,最后4个字节用于栈溢出,只需要将提取的shellcode地址放到最后四个字节处就可以实现漏洞利用。
最后将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保存。然后调整栈就可以了。
0x2.4 参考
0x3 未初始化栈变量
0x3.1 成因分析
变量创建之后,如果没有及时进行初始化赋值操作,当再次使用该变量的时候,容易产生出乎意料的运行结果。所以我们在编码过程中,一定不要忘记对变量进行初始化。
如下代码,函数指针及时进行了初始化赋值pFunc = test;
,在后续调用该指针指向的函数时(*pFunc)();
,产生了正确的结果
在例如函数指针没有进行初始化,此时pFunc为随机值,调用他将会产生出乎意料的结果。
定位到TriggerUninitializedMemoryStack
函数,在第五行,声明了函数指针v4,当传入的UserBuffer为特定的0xBAD0B0B0时,为v4赋值。然后对v4判空之后,调用v4,该漏洞存在于,如果传入的并不是0xBAD0B0B0,则不会对v4进行赋值,而v4也没有初始化,则导致程序出现出乎意料的结果,由此未初始化栈变量漏洞产生。
看一下是如何修复的,初始化UninitializedMemory变量即可。
0x3.2 漏洞利用
该漏洞是由于未初始化变量产生的,也就是变量的值是一个随机值,并不是一个默认值(NULL),所以构造零页内存是不可以的。这里用到的漏洞利用技术是栈喷射。
当传入的Buffer不为0xBAD0B0B0时,不会对v4进行赋值,此时默认的v4的值为NULL。
|
|
在查看一下kernel栈,如下。
当启用栈喷射,内核堆栈效果如下。
栈喷射实现是利用NtMapUserPhysicalPages,设置我们构造好的数据,从而填充内核堆栈。NtMapUserPhysicalPages接收的长度为1024,填充的ShellcodeAddr大小为4,所以需要开辟1024 * 4的空间。这都算是定式,记住即可。
0x4 任意地址覆盖
0x4.1 成因分析
任意地址覆盖,指的是代码没有验证地址是否有效直接使用。通过构造payload,将用来提权的Shellcode的地址覆盖到可以导致内核代码执行的区域从而实现提权。
该漏洞位于HEVD控制码为0x22200B的函数sub_444BCE
处,在sub_444BCE
函数中,将R3传来的缓冲区传入了sub_444BEE
,乍一看,很难发现这段代码有什么问题,就是单纯的将What成员复制给了Where成员,但是在内核中,没有针对地址的有效性进行验证,直接使用的话,这是非常危险的。
从源码来看,漏洞版本就是没有对地址进行检查,而修复的版本,可以看到对需要读取的地址,使用ProbeForRead和ProbeForWrite进行了检查,ProbeForRead和ProbeForWrite的作用就是检查用户模式缓冲区是否位于用户态,并验证对齐。
在学习这个漏洞的时候,我一直想不明白,就光一个内存写入怎么就触发漏洞了,后来其实才明白原理,其实内存写入这个动作并不会触发漏洞,关键是写入的这个地址(也就是代码里面的What)才是危险的,我们可以这样构造,首先What这个地方存储的是ShellCode的地址,然后在找一个地方,只要可以执行就好了,因为通过这个任意地址覆盖,将Shellcode的地址覆盖到那个可以执行的地址上,那么通过触发,就可以执行Shellcode了。而HalDispatchTable+0x4
就是这样一个地址。
如何定位HalDispatchTable+0x4
,首先查看一下nt!NtQueryIntervalProfile
这个函数的反汇编,在nt!NtQueryIntervalProfile+0x6B
调用了nt!KeQueryIntervalProfile
,跟进nt!KeQueryIntervalProfile
,显然在在0x8410e8b4
调用了nt!HalDispatchTable+0x4
这个分发表,只需要记住,shellcode往这个地方写就是了,貌似高版本的系统这个地方已经被缓解了。
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的的地址
其中如何获取ntkrnlpa.exe
在R0的基地址呢。首先EnumDeviceDrivers
获取所有的驱动模块基地址,然后根据基地址,调用GetDeviceDriverBaseNameA
获取驱动名,依次比较是否是ntkrnlpa.exe
即可。
第三步,触发漏洞,将Shellcode地址作为What参数传入,然后将HalDispatchTable+4
作为Where传入,因为任意地址覆盖,就可以将Shellcode地址覆盖到HalDispatchTable+4
地址,然后只需要调用NtQueryIntervalProfile
触发执行就可以了。NtQueryIntervalProfile
第一个参数值应该可以任意数字。
0x4.3 参考
0x5 空指针解引用
0x5.1 成因分析
释放完内存并将指针清空,但如果再次对这个指针进行引用,就会触发空指针引用漏洞,值得注意的是,要区分UAF和空指针解引用的区别,即,UAF是因为释放了内存,但是指针并没有置NULL,从而导致程序出现异常,可以通过占位的方式对该漏洞进行利用。而空指针解引用是指释放了内存,同时也置空了指针,但是仍对该指针进行引用导致异常。因为对空指针进行引用,如果提前在地址为0的地方提前写入shellcode,即可对空指针解引用进行利用。
|
|
在HEVD中,空指针解引用漏洞位于TriggerNullPointerDereference
函数中,反汇编效果所示,首先在第9行,Allocate空间,然后在19行比较传入的数据是否是BAD0B0B0
,如果是在为KernelBuffer赋值,并设置回调函数,如果不是,如33,34行所示则释放内存,并将指针置空
。无论是否为BAD0B0B0
都会调用KernelBuffer的回调函数,也就是使用了KernelBuffer。如果KernelBuffer指针没有置空,是不会有问题的,但如果KernelBuffer指针置空了,就会导致空指针解引用。
通过观察HEVD源码,发现修复之后的的逻辑是先校验了NullPointerDereference指针是否为空,然后在调用回调函数。
lim)
在这篇文章里面,也很好介绍了空指针和野指针(UAF)
0x5.2 漏洞利用
前面也具体讲了如何根据空指针解引用来进行漏洞利用,因为部分exp写的还是比较复杂,详细说说,这个漏洞利用就是通过事先开辟好零页,并将shellcode事先放到回调函数的地方。也就是(Null+4)的地址。
在观察代码逻辑,只要传入的值不是BAD0B0B0
,就会释放之前开辟的内存,并置空指针,从而导致空指针解引用。也就是说R3传入的Buffer内部只要不是BAD0B0B0
就可以了。
最后来讲一下零页内存,空指针指向的就是零页内存,在漏洞利用过程中,我们使用NtAllocateVirtualMemory
ntdll层API函数申请零页内存,具体实现如下。
我在学习这段代码的时候,有个困惑就是为什么传入的BaseAddress值为什么是(PVOID)0x00000001
,然后我看了空指针漏洞防护技术 提高篇为我解答了疑惑,当BaseAddress为0
的时候,并不能在零页内存中开辟空间,将AllocateType设置为MEM_TOP_DOWN,表示自上而下的分配内存,然后当BaseAddress设置为一个低地址,例如1
,同时指定分配内存的大小大于这个值,例如0x1000
,这样就可以申请到的内存包含了零页内存。