WTSEnumerateProcessesExA_进程名截断问题分析

概述

WTSEnumerateProcessesExA用于获取指定会话中的进程信息,该API位于Wtsapi32.dllWindows 7Windows 11均受影响。使用该API获的进程名是不完整的,W版本则无问题。问题出在Unicode编码转多字节编码时,传入的Unicode字符串字节数量错误。

问题现象

开发环境:Win10 + VS2019,x86、x64均可。注意只有A版有问题,设置如下:工程属性-配置属性-高级-字符集,选择“使用多字节字符集”。

代码如下:

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
#include <stdio.h>
#include <windows.h>
#include <wtsapi32.h>

#pragma comment(lib, "Wtsapi32.lib")

int main()
{
DWORD dwError = 0;
PWTS_PROCESS_INFO ppi = NULL;
DWORD dwCount = 0;
DWORD dwLevel = 0;

dwError = WTSEnumerateProcessesExA(
WTS_CURRENT_SERVER_HANDLE,
&dwLevel,
WTS_ANY_SESSION,
(LPSTR*)&ppi,
&dwCount);

if (!dwError)
{
printf("WTSEnumerateProcessesEx error \n");
}

for (int i = 0; i < dwCount; i++)
{
printf("procname is %s\n", ppi[i].pProcessName);
}

if (ppi)
{
WTSFreeMemoryEx(WTS_TYPE_CLASS::WTSTypeProcessInfoLevel0,
ppi,
dwCount);
ppi = NULL;
}

return 0;
}

运行上述代码后,进程名显示不完整,如图所示:

截断的进程名

作为对比,使用W版的API,同时调整工程属性的字符集为Unicode,进程名会正确显示,运行结果如图:

正确的进程名

原因分析

使用IDA打开x64版本的Wtsapi32.dll并下载PDB文件,先定位到WTSEnumerateProcessesExA

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
BOOL __stdcall WTSEnumerateProcessesExA(
HANDLE hServer,
DWORD *pLevel,
DWORD SessionId,
LPSTR *ppProcessInfo,
DWORD *pCount)
{
int v5; // ebp
DWORD *v8; // rdi
WTS_TYPE_CLASS v9; // esi
DWORD *v10; // r8
PVOID v11; // rbx
PVOID pMemory; // [rsp+58h] [rbp+10h] BYREF

v5 = 0;
pMemory = 0i64;
if ( !pLevel )
goto LABEL_11;
if ( !ppProcessInfo )
goto LABEL_11;
v8 = pCount;
if ( !pCount )
goto LABEL_11;
if ( !WTSEnumerateProcessesExW(hServer, pLevel, SessionId, (LPWSTR *)&pMemory, pCount) )
return v5;
if ( *pLevel )
{
if ( *pLevel == 1 )
{
v9 = WTSTypeProcessInfoLevel1;
goto LABEL_9;
}
LABEL_11:
SetLastError(0x57u);
return v5;
}
v9 = WTSTypeProcessInfoLevel0;
LABEL_9:
v10 = pLevel;
v11 = pMemory;
v5 = ConvertProcessInfoFromUnicodeToAnsi(pMemory, ppProcessInfo, v10, v8);
if ( v11 )
WTSFreeMemoryExW(v9, v11, *v8);
return v5;
}

流程较为简单,A版的API先调用W版的API获取进程信息,再通过函数将进程信息中的Unicode转为Ansi,最后释放堆内存;

再定位到ConvertProcessInfoFromUnicodeToAnsi函数(下载PDB后才会显示函数名):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall ConvertProcessInfoFromUnicodeToAnsi(__int64 a1, PVOID *a2, _DWORD *a3, ULONG *a4)
{
// 略

v19 = StringCchLengthW(*(STRSAFE_PCNZWCH *)(v8 + 8), 0x105ui64, &pcchLength);
if ( v19 )
break;
MbSize = pcchLength + 1;
Size = (unsigned int)(pcchLength + 1);
v20 = LocalAlloc(0x40u, Size);
*((_QWORD *)v15 + 1) = v20;
if ( !v20 )
goto LABEL_16;
memset_0(v20, 0, Size);
if ( RtlUnicodeToMultiByteN(*((PCHAR *)v15 + 1), MbSize, 0i64, *(PCWCH *)(v8 + 8), pcchLength) )
{
v16 = 0;
goto LABEL_15;
}

// 略

问题出在StringCchLengthW的第三个参数pcchLength,该参数用于获取Unicode字符串的 字符数,MSDN对参数的解释如下:

StringCchLengthW

但是RtlUnicodeToMultiByteN的第四个参数,需要传递Unicode字符串的 字节数
RtlUnicodeToMultiByteN

正确做法是将参数pcchLength * 2后再传给RtlUnicodeToMultiByteN,同时也解释了为什么错误的进程名长度恰好是正确长度的一半。

验证

WTSEnumerateProcessesA的结果是正确的,用IDA看一下它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL __stdcall WTSEnumerateProcessesA(
HANDLE hServer,
DWORD Reserved,
DWORD Version,
PWTS_PROCESS_INFOA *ppProcessInfo,
DWORD *pCount)
{
// 略

v30 = StringCbLengthW(pProcessName, 0x20Aui64, (size_t *)&ppProcessInfoa);
if ( v30 )
break;
UnicodeSize = (unsigned int)ppProcessInfoa;
v15[v20].pProcessName = v18;
v30 = RtlUnicodeToMultiByteN(v18, v9, &BytesInMultiByteString, v8[v20].pProcessName, UnicodeSize);

// 略
}

WTSEnumerateProcessesA中使用了StringCbLengthW获取Unicode字符串的 字节数StringCbLengthW的参数解释如下:

StringCbLengthW

参考WTSEnumerateProcessesA的做法,在调用WTSEnumerateProcessesExA时,将StringCchLengthW替换为StringCbLengthW,应该可以得到正确的结果,代码如下:

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
// Release x64 ANSI 字符集

#include <stdio.h>
#include <windows.h>
#include <wtsapi32.h>

#pragma comment(lib, "Wtsapi32.lib")

int fun()
{
BOOL bRet = FALSE;
DWORD oldProtect = 0;

HMODULE hDll = GetModuleHandle("Wtsapi32.dll");
if (!hDll)
{
exit(-1);
}

// 为了方便直接用硬编码了
// HEADER:0000000130000000
// .text:0000000130004BF4; HRESULT __stdcall StringCchLengthW
// .text:0000000130005AD0; HRESULT __stdcall StringCbLengthW
// .text:0000000130006717 E8 D8 E4 FF FF call StringCchLengthW

PBYTE pMem = (PBYTE)hDll + 0x6717;

bRet = VirtualProtect(pMem, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
if (!bRet)
{
exit(-1);
}

// 0x5AD0 - 0x4BF4 + 0xFFFFE4D8 = 0xFFFFF3B4
*(pMem + 1) = 0xB4;
*(pMem + 2) = 0xF3;

return 0;
}




int main()
{
DWORD dwError = 0;
PWTS_PROCESS_INFO ppi = NULL;
DWORD dwCount = 0;
DWORD dwLevel = 0;


fun(); //将 StringCchLengthW 地址替换为 StringCbLengthW

dwError = WTSEnumerateProcessesExA(
WTS_CURRENT_SERVER_HANDLE,
&dwLevel,
WTS_ANY_SESSION,
(LPSTR*)&ppi,
&dwCount);

if (!dwError)
{
printf("WTSEnumerateProcessesEx error \n");
}

for (int i = 0; i < dwCount; i++)
{
printf("procname is %s\n", ppi[i].pProcessName);
}

if (ppi)
{
WTSFreeMemoryEx(WTS_TYPE_CLASS::WTSTypeProcessInfoLevel0,
ppi,
dwCount);
ppi = NULL;
}

return 0;
}

执行后效果如下,这次进程名是正确的:
fixed