概述
WTSEnumerateProcessesExA
用于获取指定会话中的进程信息,该API位于Wtsapi32.dll
,Windows 7
至Windows 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; DWORD *v8; WTS_TYPE_CLASS v9; DWORD *v10; PVOID v11; PVOID pMemory;
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对参数的解释如下:
但是RtlUnicodeToMultiByteN
的第四个参数,需要传递Unicode
字符串的 字节数
正确做法是将参数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
的参数解释如下:
参考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
|
#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); }
PBYTE pMem = (PBYTE)hDll + 0x6717;
bRet = VirtualProtect(pMem, 5, PAGE_EXECUTE_READWRITE, &oldProtect); if (!bRet) { exit(-1); }
*(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();
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; }
|
执行后效果如下,这次进程名是正确的: