This code has been taken from https://learn.microsoft.com/en-us/windows/win32/api/lmwksta/nf-lmwksta-netwkstauserenum
I just modified dwPrefMaxLen to 100 to simulate the case where it returns just one user. In real case I put 4096 in my application, I didn't put MAX_PREFERRED_LENGTH, because I don't want risk to allocate too much memory/ block the application because the call can retrieve too many users.
In this case as I understand from documentation, the call should just return ERROR_MORE_DATA and we can recall it with resumeHandle to get the remaining users.
But this is not working, the call seems to completely ignore the resumeHandle and retrieve always the same list with the result to stay stuck in the loop forever.
What Am I doing wrong? is it a windows bug?
#ifndef UNICODE
#define UNICODE
#endif
#pragma comment(lib, "netapi32.lib")
#include <stdio.h>
#include <assert.h>
#include <windows.h>
#include <lm.h>
int wmain(int argc, wchar_t *argv[])
{
LPWKSTA_USER_INFO_0 pBuf = NULL;
LPWKSTA_USER_INFO_0 pTmpBuf;
DWORD dwLevel = 0;
DWORD dwPrefMaxLen = 100;
DWORD dwEntriesRead = 0;
DWORD dwTotalEntries = 0;
DWORD dwResumeHandle = 0;
DWORD i;
DWORD dwTotalCount = 0;
NET_API_STATUS nStatus;
LPWSTR pszServerName = NULL;
if (argc > 2)
{
fwprintf(stderr, L"Usage: %s [\\\\ServerName]\n", argv[0]);
exit(1);
}
// The server is not the default local computer.
//
if (argc == 2)
pszServerName = argv[1];
fwprintf(stderr, L"\nUsers currently logged on %s:\n", pszServerName);
//
// Call the NetWkstaUserEnum function, specifying level 0.
//
do // begin do
{
nStatus = NetWkstaUserEnum( pszServerName,
dwLevel,
(LPBYTE*)&pBuf,
dwPrefMaxLen,
&dwEntriesRead,
&dwTotalEntries,
&dwResumeHandle);
//
// If the call succeeds,
//
if ((nStatus == NERR_Success) || (nStatus == ERROR_MORE_DATA))
{
if ((pTmpBuf = pBuf) != NULL)
{
//
// Loop through the entries.
//
for (i = 0; (i < dwEntriesRead); i++)
{
assert(pTmpBuf != NULL);
if (pTmpBuf == NULL)
{
//
// Only members of the Administrators local group
// can successfully execute NetWkstaUserEnum
// locally and on a remote server.
//
fprintf(stderr, "An access violation has occurred\n");
break;
}
//
// Print the user logged on to the workstation.
//
wprintf(L"\t-- %s\n", pTmpBuf->wkui0_username);
pTmpBuf++;
dwTotalCount++;
}
}
}
//
// Otherwise, indicate a system error.
//
else
fprintf(stderr, "A system error has occurred: %d\n", nStatus);
//
// Free the allocated memory.
//
if (pBuf != NULL)
{
NetApiBufferFree(pBuf);
pBuf = NULL;
}
}
//
// Continue to call NetWkstaUserEnum while
// there are more entries.
//
while (nStatus == ERROR_MORE_DATA); // end do
//
// Check again for allocated memory.
//
if (pBuf != NULL)
NetApiBufferFree(pBuf);
//
// Print the final count of workstation users.
//
fprintf(stderr, "\nTotal of %d entries enumerated\n", dwTotalCount);
return 0;
}
when we call NetWkstaUserEnum it do request over \\servername\Pipe\wkssvc
pipe to LanmanWorkstation service. from server side request handled in wkssvc.dll in function NetrWkstaUserEnum
this api call LsaCallAuthenticationPackage
with MsV1_0EnumerateUsers
on MSV1_0_PACKAGE_NAME
and then MsV1_0GetUserInfo
for every LogonId.
most job is done inside WsEnumUserInfo
function. despite this yet, 2003 src code, i look this under latest win 11 and code was not changes since.
if we need this from local machine, we can direct call MsV1_0EnumerateUsers
and MsV1_0GetUserInfo
. but we need have TCB privileges for this. but if we run as admin, we can got it. code can look like:
NTSTATUS status, ProtocolStatus;
HANDLE LsaHandle;
ImpersonateToken(&tp_Tcb);// not listed here
status = LsaConnectUntrusted(&LsaHandle);
RevertToSelf();
if (0 <= status)
{
ULONG AuthenticationPackage;
LSA_STRING PackageName;
RtlInitString(&PackageName, MSV1_0_PACKAGE_NAME);
if (0 <= (status = LsaLookupAuthenticationPackage(LsaHandle, &PackageName, &AuthenticationPackage)))
{
MSV1_0_ENUMUSERS_REQUEST request = { MsV1_0EnumerateUsers };
PMSV1_0_ENUMUSERS_RESPONSE response = 0;
ULONG len;
if (0 <= (status = LsaCallAuthenticationPackage(LsaHandle, AuthenticationPackage, &request, sizeof(request),
(void**)&response, &len, &ProtocolStatus)))
{
if (0 > ProtocolStatus)
{
status = ProtocolStatus;
}
else if (ULONG NumberOfLoggedOnUsers = response->NumberOfLoggedOnUsers)
{
PLUID LogonIds = response->LogonIds;
PULONG EnumHandles = response->EnumHandles; // InterlockedIncrement(&NlpEnumerationHandle);
MSV1_0_GETUSERINFO_REQUEST getuser = { MsV1_0GetUserInfo };
do
{
ULONG EnumHandle = *EnumHandles++;
getuser.LogonId = *LogonIds++;
PMSV1_0_GETUSERINFO_RESPONSE userinfo = 0;
if (0 <= (status = LsaCallAuthenticationPackage(LsaHandle,
AuthenticationPackage, &getuser, sizeof(getuser),
(void**)&userinfo, &len, &ProtocolStatus)))
{
if (0 > ProtocolStatus)
{
status = ProtocolStatus;
}
else
{
WCHAR wz[SECURITY_MAX_SID_STRING_CHARACTERS];
UNICODE_STRING us = { 0, sizeof(wz), wz };
RtlConvertSidToUnicodeString(&us, userinfo->UserSid, FALSE);
DbgPrint("%x:{%08x-%08x}: %x %wZ\\%wZ %wZ \"%wZ\"\n",
EnumHandle, // some uniqie number
getuser.LogonId.HighPart, getuser.LogonId.LowPart,
userinfo->LogonType,
&userinfo->LogonDomainName,
&userinfo->UserName,
&us,
&userinfo->LogonServer);
}
LsaFreeReturnBuffer(userinfo);
}
} while (--NumberOfLoggedOnUsers);
}
LsaFreeReturnBuffer(response);
}
}
LsaDeregisterLogonProcess(LsaHandle);
}
structures defined in ntifs.h from wdk. if you have problems to include it, we can simply copy paste it from there
//
// MsV1_0EnumerateUsers submit buffer and response
//
typedef struct _MSV1_0_ENUMUSERS_REQUEST {
MSV1_0_PROTOCOL_MESSAGE_TYPE MessageType;
} MSV1_0_ENUMUSERS_REQUEST, *PMSV1_0_ENUMUSERS_REQUEST;
typedef struct _MSV1_0_ENUMUSERS_RESPONSE {
MSV1_0_PROTOCOL_MESSAGE_TYPE MessageType;
ULONG NumberOfLoggedOnUsers;
PLUID LogonIds;
PULONG EnumHandles;
} MSV1_0_ENUMUSERS_RESPONSE, *PMSV1_0_ENUMUSERS_RESPONSE;
//
// MsV1_0GetUserInfo submit buffer and response
//
typedef struct _MSV1_0_GETUSERINFO_REQUEST {
MSV1_0_PROTOCOL_MESSAGE_TYPE MessageType;
LUID LogonId;
} MSV1_0_GETUSERINFO_REQUEST, *PMSV1_0_GETUSERINFO_REQUEST;
typedef struct _MSV1_0_GETUSERINFO_RESPONSE {
MSV1_0_PROTOCOL_MESSAGE_TYPE MessageType;
PSID UserSid;
UNICODE_STRING UserName;
UNICODE_STRING LogonDomainName;
UNICODE_STRING LogonServer;
SECURITY_LOGON_TYPE LogonType;
} MSV1_0_GETUSERINFO_RESPONSE, *PMSV1_0_GETUSERINFO_RESPONSE;
internally msv1_0 have linked list ( with NlpActiveLogonListAnchor
head) of private ACTIVE_LOGON
structures ( used to keep track of all private information related to a particular LogonId.). this struct have
ULONG EnumHandle; // The enumeration handle of this logon session
member, assigned in next way:
//
// Get the next enumeration handle for this session.
//
pActiveLogonEntry->EnumHandle = (ULONG)InterlockedIncrement((PLONG)&NlpEnumerationHandle);
msv1_0 enumerate list from last to first entry. by Flink
links. as results
MSV1_0_ENUMUSERS_RESPONSE::EnumHandles
go in descending order
//
// Loop through the Active Logon Table copying the EnumHandle of
// each session.
//
pEnumResponse->EnumHandles = (PULONG)(ClientBufferDesc.UserBuffer +
(pWhere - ClientBufferDesc.MsvBuffer));
for ( pScan = NlpActiveLogonListAnchor.Flink;
pScan != &NlpActiveLogonListAnchor;
pScan = pScan->Flink )
but WsEnumUserInfo assume that it go in ascending order
if (StartEnumeration <= EnumUsersResponse->EnumHandles[i]) {
//
// Get the enumeration starting point.
//
if (ARGUMENT_PRESENT(ResumeHandle)) {
StartEnumeration = *ResumeHandle;
}
if (status == ERROR_MORE_DATA && ARGUMENT_PRESENT(ResumeHandle)) {
*ResumeHandle = EnumUsersResponse->EnumHandles[i - 1];
}
let we have m = NumberOfLoggedOnUsers
with next EnumHandles
, go in ascending order
N(0) < .. < N(i-1) < N(i) < .. < N(m-1)
MsV1_0GetUserInfo
return it in in descending order
N(m-1), .. , N(i), N(i-1), .., N(0)
let only N(m-1), .. , N(i)
fit to prefmaxlen
so NetWkstaUserEnum
return (m - i)
entries and ResumeHandle = i - 1
(loop is breaked on copy N(i-1)
entry with WsPackageUserInfo
return ERROR_MORE_DATA
)
so on next call to NetWkstaUserEnum
with ResumeHandle == i - 1
will be enumerated only entries with N >= N(i-1)
so again N(m-1), .. , N(i)
and may be N(i-1)
if you increase buffer
also you can do next simply demo test - call NetWkstaUserEnum
several times, with different buffer sizes, but not big enough to accommodate all users, and with dwResumeHandle = 0
as input parameter - the bigger the buffer size - the bigger the dwEntriesRead
will be when returning and the less the dwResumeHandle
value will be
void eu()
{
ULONG dwEntriesRead, dwTotalEntries, prefmaxlen = 0x10, dwResumeHandle;
NET_API_STATUS nStatus;
do
{
PWKSTA_USER_INFO_0 pBuf;
switch (nStatus = NetWkstaUserEnum(0, 0, (PBYTE*)&pBuf,
prefmaxlen, &dwEntriesRead,
&dwTotalEntries, &(dwResumeHandle = 0)))
{
case ERROR_MORE_DATA:
case NERR_Success:
DbgPrint("(%x)=%u dwEntriesRead=%u dwResumeHandle=%u\n",
prefmaxlen, nStatus, dwEntriesRead, dwResumeHandle);
if (dwEntriesRead)
{
pBuf += dwEntriesRead;
do
{
DbgPrint("\t-- %ws\n", (--pBuf)->wkui0_username);
} while (--dwEntriesRead);
}
prefmaxlen += 0x10;
NetApiBufferFree(pBuf);
break;
}
} while (ERROR_MORE_DATA == nStatus);
}
and output
(10)=234 dwEntriesRead=0 dwResumeHandle=5
(20)=234 dwEntriesRead=0 dwResumeHandle=5
(30)=234 dwEntriesRead=1 dwResumeHandle=2
-- Administrator
(40)=234 dwEntriesRead=2 dwResumeHandle=1
-- User
-- Administrator
(50)=0 dwEntriesRead=3 dwResumeHandle=0
-- User
-- User
-- Administrator
and if direct run first code snipet, result will be
5:{00000000-00483cba}: 2 DESKTOP-*******\Administrator S-1-5-21-... "DESKTOP-*******"
2:{00000000-000341ef}: 2 DESKTOP-*******\User S-1-5-21-... "DESKTOP-*******"
1:{00000000-00034188}: 2 DESKTOP-*******\User S-1-5-21-... "DESKTOP-*******"