利用HEVD学习windows kernel exploit 正式篇1 StackOverflow
本篇转自安全客 https://www.anquanke.com/post/id/218682
Window Kernel Exploit 01 - StackOverflow
栈溢出是一个最基本的漏洞利用方式,这里我们利用这个作为入门学习,了解一下在 Windows Kernel 下执行栈溢出的不同之处。
漏洞程序
找到之前准备好的HackSysExtremeVulnerableDriver.sys
,里面有一个准备好的带有栈溢出的函数,叫做StackOverflowIoctlHandler
。我们通过逆向,找到对应触发函数的IOCTL
:
记录下此时的 IOCTL Code 为 222003h。之后我们来看这个程序的内部逻辑:
1 | int __stdcall StackOverflowIoctlHandler(PIRP a1, PIO_STACK_LOCATION a2) |
这里注意一下,这类IOCTL Handle Routine
的传入参数类型是固定的,一定是第一个为PRIR
,第二个为PIO_STACK_LOCATION
,如果没有识别出参数的话,可以直接指定参数类型
此时发现,这个a2
好像识别的有一点问题,从函数名也能猜到,程序逻辑本身应该是一个读取Buffer
的逻辑,不应该和SetFile
这类文件操作相关,所以这里推测,应该是PIO_STACK_LOCATION
结构体中存在union
结构,所以此时识别的结构体出现了错误。这个时候回退到Disassembly
的界面,然后在参数的位置处右键,选择Structure Offset
,就能够修改当前结构体识别的类型。
这里我们修改成和DeviceIoControl
相关的DeviceIoControl.Type3InputBuffer
,下面的参数也修改成DeviceIoControl.InputBufferLength
,整个逻辑就变成了
1 | int __stdcall StackOverflowIoctlHandler(PIRP a1, _IO_STACK_LOCATION *a2) |
此时逻辑就清晰了很多:读取IO_STACK_LOCATION
指针指向的Buffer内容,并且将Buffer的和Buffer的长度传入到触发函数中。并且触发函数中的内容如下:
1 | int __stdcall TriggerStackOverflow(void *Address, size_t MaxCount) |
简单介绍一下内核函数ProbeForRead
:
1 | void ProbeForRead( |
函数能够检查当前的地址是否属于用户态(访问地址是否越界),并且检查当前的地址是否是按照第三个参数要求的 Alignment 进行对齐。然后就会将当前传入的Buffer
按照Buffer本身的MaxCount
拷贝到栈上,从而造成栈溢出。
利用分析
整个逻辑是分析清楚了:只要使用DeviceIoControl
从用户端这边发送请求,并且使用的是Buffer
,而且大小超过了0x81c
,就会发生栈溢出,造成返回值被劫持。
提权相关
单纯劫持返回值还不够,因为内核态并没有类似于system
这类方便的劫持函数。在内核态实现劫持,根据平台的不同,会使用的不同的劫持方式
WIN7
在Win7阶段,内核态并没有做过多的限制,所以可以在内核态执行用户态的程序。那么如果劫持了返回值,那么便是可以运行由我们自己申请的地址空间上的shellcode。一般的逻辑如下:
首先在Windows操作系统中,所有的东西都被视为对象,每一个对象都有一个安全描述符(security descriptors)(长得有点像(A;;RPWPCCDCLCRCWOWDSDSW;;;DA)
这样的)其在内存中存储的形式通常为一个token。它会描述当前进程的所有者,以及其的相关权限,包括对文件的操作等等。这里最高的权限就是NT AUTHORITY\SYSTEM
,系统权限拥有对所有文件的任意权力(相当于是su)。所以一般的提权思路就是:
- 遍历当前所有进程
- 找到当前进程中的系统进程(通常来说进程号4的进程就是系统进程啦)
- 将其的安全描述符token复制到当前进程的安全描述符中,即可完成提权
能够找到的payload如下
1 | pushad ; Save registers state |
EXP实现
内核态的通信和用户态不太一样。看过的教材中有使用C语言直接编译exe的,也有使用python/powershell调用库进行攻击的。于是这里打算介绍一下最普通的使用C语言的攻击,以及最近比较流行的使用powershell进行的攻击(这一类似乎被称之为fileless attack)
C语言
通讯准备
首先要能够实现最基本的通信,使用C(Cpp)的话,需要直接调用Windows系列的API对文件进行操作,如下:
1 |
|
提权攻击(Win7)
由于Win7上暂时没有太多的防护,所以可以直接使用拷贝token的方式进行提权。这里直接通过计算好返回值所需要的padding,然后让返回的地址跳转到我们自己申请的内存空间上来实现攻击。不过这里要考虑一件事情:以前我们都是直接弹出一个cmd结束攻击,然而提权攻击却不能只弹出一个cmd就完成攻击,这意味着类似BufferOverflow这类攻击如果将栈的内容进行了修改之后,我们需要有一个防止系统发现栈被破坏的操作。为了实现这一点,我们需要先观察一下栈中的内容:
1 | eax=00000000 ebx=9bf375f0 ecx=00000000 edx=00000000 esi=c00000bb edi=9bf37580 |
在距离返回值地址的0x18的位置上,正好有上一个函数的返回地址,所以当我们劫持了这个函数返回值的时候,在shellcode的末尾,我们可以加上一些额外的指令来实现恢复栈
1 | xor eax, eax ;伪装返回值 |
这里我们参考HEVD给出的参考答案:
1 |
|
提权攻击(Win10)
然而,如果用上述exp的话,似乎并没有那么顺利。我们调试可以看到如下结果:
1 | HackSysExtremeVulnerableDriver!TriggerStackOverflow+0xc8: |
乍一看好像是成功的,但是如果让程序继续执行的话就会爆出如下的错误:
1 | 1: kd> t |
这个错误码的意思是ATTEMPTED EXECUTE ON NOEXECUTE MEMORY
,因为从Win 8.1 开始,Windows 就有了一种新的保护措施,叫做Supervisor Mode Execution Prevention(SMEP)
。在这个保护下,不能在ring 0 的环境中执行 ring 3的代码。到了这个时候,就需要使用一些特殊的手段关闭这个特性。最常见的手段就是利用ROP攻击,修改cr4
寄存器内容。一个常见的函数就是:
1 | .text:00401000 |
利用这个ROP,让RCX赋值为CR4。不过这里注意一点,由于这里使用的,此时如果使用IDA观察的话,需要知道当前段映射的真正偏移量。这个可以通过观察如下的特征知道:
1 | .text:00401000 ; Section 1. (virtual address 00001000) |
每个段开头都会有一个virtual address
,这个值表示的是当前段会映射的地址,具体计算方式为real_address = image_base_address + virtual_address
。也就是说此时的.text
段在内存中的真正的地址为:
real_text = image_base_address + 0x1000
然后我们需要观察cr4此时的正确的值。首先我们找到储了问题时的cr4:
1 | For analysis of this file, run !analyze -v |
上网查找可知,第20bit为1表示的是SMEP打开(记得从低位往高位数,并且第一位数字是第0bit,第二位数字是第1bit),那么我们只需要将这一bit置0,即可以将这种防护关闭,此时也就是将值改成0x0406e9
。
有了ROP,那么我们就需要一个泄露内核地址的途径。这里有两种不同的方式,一个叫做:EnumDrivers
的API,另一种是利用NtQueryInformationSystem
的方式获取。前者是官方给出的API,通过调用直接获取地址,而后者是则是通过逆向分析+动态调试,验证可知当前的地址空间上存放的是ntoskrl.exe
的基地址。
前者直接就是一个API:
1 | BOOL EnumDeviceDrivers( |
并且据观察,返回的地址数组中lpImageBase
,第一个就是ntoskrl.exe
的基地址。不过使用这个方法的时候,需要用到管理员权限。
这里打算用第一种方法实现地址泄露,第二种攻击方法参考(https://www.anquanke.com/post/id/173144)[https://www.anquanke.com/post/id/173144],贴出用NtQueryInformationSystem
的exp:
1 | typedef enum _SYSTEM_INFORMATION_CLASS { |
回到正文,此时代码修改如下:
1 | VOID TokenStealingPayloadWin7() { |
不过这里由于引入了ROP,这里需要重新讨论一下栈的地址。
此时->
指向的是之后会修改成的内容。由于加入了ROP,导致原先利用的返回值会被覆盖掉,所以这里需要重新调整返回值,让esp在调用exp的地址后,加上0x1c,让其跳转到nt!IofCallDriver
的返回值,从而恢复调用栈。
Powershell版本
本质上差不多,不过这边使用的是Powershell下的编程:
1 | Add-Type -TypeDefinition @" |
攻击结果
不知道为啥,提权有时候会失败,不过失败了似乎也没有进入蓝屏的样子…
使用powershell进行攻击的结果如下