Как получить все (не отключенные) пользовательские SID через Windows API?

Я ищу способ получить все пользовательские SID в системе через Windows API.

Получить все SID пользователей можно с помощью wmic useraccount get sid. Есть ли способ получить эту информацию через Windows API?

Кроме того, команда wmic возвращает SID всех учетных записей, включая отключенные учетные записи — wmic useraccount get disabled,sid покажет, какие учетные записи отключены. Было бы неплохо, если бы решение могло посоветовать, как получить SID учетных записей, которые не отключены, но это не имеет решающего значения.


person Lisbeth    schedule 05.07.2019    source источник
comment
NetUserEnum -> NetUserGetInfo — это один из способов.   -  person Jonathan Potter    schedule 05.07.2019


Ответы (3)


Вы можете использовать функцию:

NET_API_STATUS NET_API_FUNCTION NetUserEnum(
  LPCWSTR servername,
  DWORD   level,
  DWORD   filter,
  LPBYTE  *bufptr,
  DWORD   prefmaxlen,
  LPDWORD entriesread,
  LPDWORD totalentries,
  PDWORD  resume_handle
);

с servername = NULL для перечисления учетных записей локальных компьютеров, затем используйте:

BOOL LookupAccountNameW(
  LPCWSTR       lpSystemName,
  LPCWSTR       lpAccountName,
  PSID          Sid,
  LPDWORD       cbSid,
  LPWSTR        ReferencedDomainName,
  LPDWORD       cchReferencedDomainName,
  PSID_NAME_USE peUse
);

для получения SID.

См. https://docs.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netuserenum и https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupaccountnamew для получения подробной информации и примеров.

В функции NetUserEnum установка параметра level=1 вернет подробную информацию об учетных записях пользователей, а параметр bufptr укажет на массив USER_INFO_1 структур.

Просмотр элемента usri1_flags структуры USER_INFO_1 с маской UF_ACCOUNTDISABLE дает статус учетной записи.

Следуя комментарию RbMm, обратите внимание, что при указании в функции NetUserEnum параметра level=3 параметр bufptr будет указывать на массив структур USER_INFO_3, содержащий пользовательские RID. Элемент usri3_user_id содержит относительный идентификатор (RID) пользователя, а член usri3_primary_group_id содержит RID первичной глобальной группы для пользователя. Используя эти значения, вам не нужно вызывать LookupAccountNameW.

Эффективность повышается с помощью предложений от RbMm в комментариях ниже.

person Frankie_C    schedule 05.07.2019
comment
не нужно использовать LookupAccountNameW в этом случае. потому что у нас уже есть пользователь RID. NetUserEnum дополнительная функция сравнения NetQueryDisplayInformation - нужно использовать ее только в том случае, если нам нужно больше информации, чем возвращает NetQueryDisplayInformation - person RbMm; 07.07.2019
comment
LookupAccountNameW вообще очень плохой дизайн. даже если нам нужен запрос только одного имени, это не очень хорошо. внутренний вызов. LsaOpenPolicy + LsaLookupNames2 + LsaClose. обратите внимание, что LsaLookupNames2 уже выделила вам память. когда вы вызываете LookupAccountNameW - вам нужно предоставить собственные буферы (и неизвестный начальный размер). LookupAccountNameW скопируйте память в свои буферы и освободите оригинал. так что даже в запросе с одним именем более простой и эффективный вызов LsaLookupNames2 напрямую. когда вы выполняете несколько запросов (скажем, дамп ACL, группы токенов) - всегда лучше вызывать LsaLookupNames2 напрямую и один раз. - person RbMm; 07.07.2019
comment
но в этом конкретном случае нам не нужен даже LsaLookupNames2, потому что у нас здесь есть дополнительный контекст - пользовательский (RID) - поэтому для получения полного SID - достаточно в начале запроса SID домена, выделить новый SID с дополнительным авторством, а затем установить пользовательский RID на этот окончательный субавтор.. конечно, если вы хотите эффективный и быстрый код - person RbMm; 07.07.2019
comment
@RbMm Ты прав. Я упоминаю ваши комментарии в ответе. - person Frankie_C; 07.07.2019
comment
и NetUserEnum vs NetQueryDisplayInformation: внутренний вызов SamConnect + SamEnumerateDomainsInSamServer + SamLookupDomainInSamServer + SamOpenDomain - это общая часть. затем NetQueryDisplayInformation звоните SamQueryDisplayInformation когда NetUserEnum - SamEnumerateUsersInDomain + SamOpenUser + SamQueryInformationUser - многое другое. и каждый звонок - это удаленный и очень тяжелый звонок - person RbMm; 07.07.2019
comment
это потому, что даже msdn write Чтобы быстро перечислить информацию об учетной записи пользователя, компьютера или глобальной группы, вызовите функцию NetQueryDisplayInformation. - если нам нужна только информация, возвращаемая NetQueryDisplayInformation, лучше используйте ее. если нам нужна дополнительная информация о пользователе или изменить информацию о пользователе - нам нужно использовать NetUserEnum. конечно эффективнее использовать прямое SamXXX api (все остальные api на его основе) (чтобы не открывать одни и те же ручки много раз), но как обычно это не документировано. - person RbMm; 07.07.2019
comment
@RbMm Достаточно обычно для MS ... В любом случае, это не очень популярная область разработки, что является одной из причин плохого знания и документации. Спасибо за очень подробное объяснение. - person Frankie_C; 07.07.2019

Есть несколько способов.

Простой — с NetQueryDisplayInformation.

Тестовый образец (Windows 10, VS 2015) =>

NET_API_STATUS NetStatus;
DWORD dwIndex = 0;
DWORD dwEntriesRequested = 0xFFFFFFFF;
DWORD dwPreferredMaximumLength = 0xFFFFFFFF;
DWORD dwReturnedEntryCount;
PVOID pNDU = NULL;
do {
    NetStatus = NetQueryDisplayInformation(NULL, 1, dwIndex, dwEntriesRequested, dwPreferredMaximumLength, &dwReturnedEntryCount, &pNDU);
    if (NetStatus != NERR_Success && NetStatus != ERROR_MORE_DATA)
        break;
    for (int i = 0; i < dwReturnedEntryCount; i++)
    {
        PNET_DISPLAY_USER NetDisplayUser = (PNET_DISPLAY_USER)(((LPBYTE)pNDU) + sizeof(NET_DISPLAY_USER) * i);              
        PSID pSID = ConvertNameToSID(NetDisplayUser->usri1_name);
        LPWSTR pszSid = NULL;
        ConvertSidToStringSid(pSID, &pszSid);
        BOOL bIsAccountDisabled = ((NetDisplayUser->usri1_flags & UF_ACCOUNTDISABLE) != 0) ? TRUE : FALSE;
        WCHAR wsBuffer[MAX_PATH];           
        wsprintf(wsBuffer, L"%4.4ld %-20.20ws SID : %ws - Disabled : %ws - Comment : %ws\n",
            NetDisplayUser->usri1_next_index,
            NetDisplayUser->usri1_name,
            pszSid,
            (bIsAccountDisabled ? L"True" : L"False"),
            NetDisplayUser->usri1_comment
        );
        LocalFree(pSID);

        OutputDebugString(wsBuffer);
        dwIndex = NetDisplayUser->usri1_next_index;                 
    }
    NetApiBufferFree(pNDU);
} while (NetStatus == ERROR_MORE_DATA);

PSID ConvertNameToSID(LPTSTR lpszName)
{
    WCHAR wszDomainName[256];
    DWORD dwSizeDomain = sizeof(wszDomainName) / sizeof(TCHAR);
    DWORD dwSizeSid = 0;
    SID_NAME_USE sidName;
    LookupAccountName(NULL, lpszName, NULL, &dwSizeSid, wszDomainName, &dwSizeDomain, &sidName);
    PSID pSid;
    pSid = (PSID)LocalAlloc(LPTR, dwSizeSid);
    LookupAccountName(NULL, lpszName, pSid, &dwSizeSid, wszDomainName, &dwSizeDomain, &sidName);
    return pSid;
}
person Castorix    schedule 05.07.2019
comment
использование ConvertNameToSID здесь очень неэффективно. у вас есть usri1_user_id - (RID) пользователя - поэтому нужно просто добавить его к SID домена. - person RbMm; 07.07.2019
comment
Нет. См. образцы MS: они делают именно так... (в том числе у Джины) - person Castorix; 07.07.2019
comment
нет, это неэффективно. очень не эффективно. у вас есть избавление от пользователя - измените его - person RbMm; 07.07.2019
comment
Нет, у вас нет SID домена для его объединения. См. старый образец Джины из SDK... - person Castorix; 07.07.2019
comment
нам нужно сначала получить SID домена - один раз, а затем использовать его - просто добавить RID пользователя к этому SID. мне не нужно искать плохие примеры - person RbMm; 07.07.2019
comment
Так что это больше кода... Всегда используйте образцы MS SDK для создания кода... (я все еще использую 16-битный код SDK 1990 года, который все еще работает в Windows 10...) - person Castorix; 07.07.2019
comment
мне не нужно использовать плохие и неэффективные образцы. опять же - нужно уметь писать эффективный код. у нас есть RID - нужно его использовать. один вызов LookupAccountName - это LsaOpenPolicy(удаленный вызов lsass) -> LsaLookupNames2(удаленный вызов lsass) -> SamLookupNamesInDomain (внутри lsass) -> LsaClose(удаленный вызов lsass). (я предполагаю, что lsass удерживает SAM открытым, чтобы не вызывать SamOpenDomain каждый раз) - person RbMm; 07.07.2019
comment
очень много примеров MS неправильных и неэффективных. и опять нужно понять - где использовать некоторые апи. если нам нужно запросить 1 имя пользователя без дополнительного контекста (как здесь) - используйте LookupAccountName ok. если нам нужно использовать 2+ имени - это уже плохо. нужно использовать LsaLookupNames2. но если у нас есть RID пользователя, как в этом случае - не нужно использовать даже LsaLookupNames2 - нужно добавить этот RID к SID домена - person RbMm; 07.07.2019
comment
Сообщите MS, что их код «неэффективен :-)» - я скажу вам, что ваш код неэффективен - person RbMm; 07.07.2019

для перечисления учетных записей пользователей в базе данных SAM (Security Account Manager) мы можем использовать или NetQueryDisplayInformation (более быстро), или NetUserEnum (если нам нужна более подробная информация о пользователе). или SAM API (самый быстрый, включите ntsam.h< /em> и связать с samlib.lib )

обратите внимание, что если у нас есть пользователь (RID), нам не нужно использовать LookupAccountName - в данном случае это очень неэффективно (многие тяжелые удаленные вызовы внутренние - LsaOpenPolicy, LsaLookupNames2, LsaClose . Внутренние LsaLookupNames2 все равно используют SAM api < a href="https://github.com/processhacker/processhacker/blob/master/phnt/include/ntsam.h#L448" rel="nofollow noreferrer">SamLookupNamesInDomain). на самом деле все, что нам нужно - сначала получить домен SID, а затем добавить к нему пользователя RID. получить SID домена мы можем с помощью LsaQueryInformationPolicy с PolicyAccountDomainInformation для SID домена учетной записи (компьютера) - всегда существует и с PolicyDnsDomainInformation или PolicyPrimaryDomainInformation для получения SID основного домена (существует только если компьютер является частью домена)

void PrintUsersInDomain(PUNICODE_STRING ServerName, PSID DomainSid)
{
    PWSTR szServerName = 0;

    if (ServerName)
    {
        if (ULONG Length = ServerName->Length)
        {
            szServerName = ServerName->Buffer;
            // if not null terminated
            if (Length + sizeof(WCHAR) < ServerName->MaximumLength || *(PWSTR)((PBYTE)szServerName + Length))
            {
                szServerName = (PWSTR)alloca(Length + sizeof(WCHAR));
                memcpy(szServerName, ServerName->Buffer, Length);
                *(PWSTR)((PBYTE)szServerName + Length) = 0;
            }
        }
    }

    UCHAR SubAuthorityCount = *GetSidSubAuthorityCount(DomainSid);
    ULONG DestinationSidLength = GetSidLengthRequired(SubAuthorityCount + 1);

    PSID UserSid = alloca(DestinationSidLength);
    CopySid(DestinationSidLength, UserSid, DomainSid);
    ++*GetSidSubAuthorityCount(UserSid);
    PULONG pRid = GetSidSubAuthority(UserSid, SubAuthorityCount);

    PVOID Buffer;
    ULONG Index = 0, ReturnedEntryCount;

    NET_API_STATUS status;

    do 
    {
        switch (status = NetQueryDisplayInformation(szServerName, 1, Index, 
            64, MAX_PREFERRED_LENGTH, &ReturnedEntryCount, &Buffer))
        {
        case NOERROR:
        case ERROR_MORE_DATA:
            if (ReturnedEntryCount)
            {
                PNET_DISPLAY_USER pndu = (PNET_DISPLAY_USER)Buffer;

                do 
                {
                    //if (!(pndu->usri1_flags & UF_ACCOUNTDISABLE))
                    {
                        *pRid = pndu->usri1_user_id;

                        PWSTR szSid;
                        if (ConvertSidToStringSidW(UserSid, &szSid))
                        {
                            DbgPrint("\t[%08x] %S %S\n", pndu->usri1_flags, pndu->usri1_name, szSid);
                            LocalFree(szSid);
                        }
                    }

                    Index = pndu->usri1_next_index;

                } while (pndu++, --ReturnedEntryCount);
            }

            NetApiBufferFree(Buffer);
        }
    } while (status == ERROR_MORE_DATA);
}

void PrintUsersInDomain_fast(PUNICODE_STRING ServerName, PSID DomainSid)
{
    SAM_HANDLE ServerHandle, DomainHandle = 0;

    //SAM_SERVER_ENUMERATE_DOMAINS|SAM_SERVER_LOOKUP_DOMAIN
    NTSTATUS status = SamConnect(ServerName, &ServerHandle, SAM_SERVER_LOOKUP_DOMAIN, 0);

    DbgPrint("SamConnect(%wZ) = %x\n", ServerName, status);

    if (0 <= status)
    {
        status = SamOpenDomain(ServerHandle, DOMAIN_READ|DOMAIN_EXECUTE, DomainSid, &DomainHandle);

        SamCloseHandle(ServerHandle);
    }

    if (0 <= status)
    {
        UCHAR SubAuthorityCount = *GetSidSubAuthorityCount(DomainSid);
        ULONG DestinationSidLength = GetSidLengthRequired(SubAuthorityCount + 1);

        PSID UserSid = alloca(DestinationSidLength);
        CopySid(DestinationSidLength, UserSid, DomainSid);
        ++*GetSidSubAuthorityCount(UserSid);
        PULONG pRid = GetSidSubAuthority(UserSid, SubAuthorityCount);

        PVOID Buffer;

        ULONG Index = 0, TotalAvailable, TotalReturned, ReturnedEntryCount;

        do 
        {
            if (0 <= (status = SamQueryDisplayInformation(DomainHandle,
                DomainDisplayUser,
                Index,
                2,
                0x10000,
                &TotalAvailable,
                &TotalReturned,
                &ReturnedEntryCount,
                &Buffer)))
            {
                if (ReturnedEntryCount)
                {
                    PSAM_DISPLAY_USER psdu = (PSAM_DISPLAY_USER)Buffer;
                    do 
                    {
                        //if (!(psdu->AccountControl & USER_ACCOUNT_DISABLED))
                        {
                            *pRid = psdu->Rid;

                            PWSTR szSid;
                            if (ConvertSidToStringSidW(UserSid, &szSid))
                            {
                                DbgPrint("\t[%08x] %wZ %S\n", psdu->AccountControl, &psdu->AccountName, szSid);
                                LocalFree(szSid);
                            }
                        }

                        Index = psdu->Index;

                    } while (psdu++, --ReturnedEntryCount);
                }
                SamFreeMemory(Buffer);
            }
        } while (status == STATUS_MORE_ENTRIES);

        SamCloseHandle(DomainHandle);
    }
}

void PrintUsers()
{
    LSA_HANDLE PolicyHandle;

    LSA_OBJECT_ATTRIBUTES ObjectAttributes = { sizeof(ObjectAttributes) };
    NTSTATUS status;
    if (0 <= (status = LsaOpenPolicy(0, &ObjectAttributes, POLICY_VIEW_LOCAL_INFORMATION, &PolicyHandle)))
    {
        union {
            PVOID buf;
            PPOLICY_DNS_DOMAIN_INFO pddi;
            PPOLICY_ACCOUNT_DOMAIN_INFO padi;
        };

        if (0 <= LsaQueryInformationPolicy(PolicyHandle, PolicyAccountDomainInformation, &buf))
        {
            DbgPrint("DomainName=<%wZ>\n", &padi->DomainName);
            if (padi->DomainSid) 
            {
                PrintUsersInDomain_fast(&padi->DomainName, padi->DomainSid);
                PrintUsersInDomain(&padi->DomainName, padi->DomainSid);
            }
            LsaFreeMemory(buf);
        }

        if (0 <= LsaQueryInformationPolicy(PolicyHandle, PolicyDnsDomainInformation, &buf))
        {
            DbgPrint("DomainName=<%wZ>\n", &pddi->Name);
            if (pddi->Sid) 
            {
                PrintUsersInDomain_fast(&pddi->Name, pddi->Sid);
                PrintUsersInDomain(&pddi->Name, pddi->Sid);
            }
            LsaFreeMemory(buf);
        }
        LsaClose(PolicyHandle); 
    }
}

typedef struct SAM_DISPLAY_USER {
    ULONG Index;
    ULONG Rid;
    ULONG AccountControl; /* User account control bits */
    UNICODE_STRING AccountName;
    UNICODE_STRING AdminComment;
    UNICODE_STRING FullName;
} *PSAM_DISPLAY_USER;
person RbMm    schedule 07.07.2019