Click here to Skip to main content
15,886,002 members
Articles / Programming Languages / C++
Article

NT Remote and Local Group and User Account SID Collector Tool

Rate me:
Please Sign up or sign in to vote.
3.57/5 (4 votes)
10 Dec 2001CPOL10 min read 91.7K   2.1K   35   4
Tool to collect SIDs of Group and User Accounts from a Local or Remote NT Machine and output in an INI file and an XML file.

Introduction

Some time at the beginning of 2000, a friend of mine approached me to write a tool that is able to collect all the SIDs of all Group and User Accounts of an NT machine (local or remote).

One of his junior staff had a need to periodically collect these Group and User Account SIDs of several machines. He would later need to collate all these information into a report, using his own report generator programming tool. I offered to write a DLL with high level functions that encapsulate the LAN Manager NET functions. That'll be great, according to my friend but it'd be better if I could simply write a small application that collects the necessary information and store them into a file that the report generator program could later read and process. Sure, I replied. A simple console program that can be launched via a batch file will do. Now, what format do you need this file to be in? They were at a loss. I suggested the Windows INI file format for the following reasons:

  1. Since the information we will be collecting are basically strings (Group Names, User Account Names, SIDs), these can be stored in an INI file with no problems.
  2. Availability of relatively simple and robust INI file APIs (e.g. GetPrivateProfileString()).

We all agreed and so I wrote the CollectSID console application. It uses the LAN Manager NET APIs that probes local or remote NT machines for Group and User Account SIDs and outputs them all into an INI file (I'll cover the format of this INI file in the section "INI File Format", later in this article). One year on, and with the widespread popularity of XML, I decided to revisit this tool that I wrote and extend it to include XML file output.

How Does It Work ?

The CollectSID program source is composed of 6 files:

  1. main.h - Header file for main.cpp.
  2. main.cpp - Main running module.
  3. CollectSID.h - Header file for CollectSID.cpp.
  4. CollectSID.cpp - Module that contains functions to process the Group and User Accounts of a local or remote machine.
  5. msxmlwrp.h - Header file for msxmlwrp.cpp. We declare 3 simple classes there for XML file manipulation.
  6. msxmlwrp.cpp - Module that contain classes that encapsulate XML file creation. We also define functions to transform the contents of an INI file into XML file format.

main.cpp

  1. The only function defined here is main(). main() will perform the usual command line processing and then calls on the CollectGroups() function to start the ball rolling.
  2. CollectGroups() will later call CollectMembers() and LookupSID() to do other necessary work.
  3. If CollectGroups() completed successfully, we call on ConvertINIToXML() to convert the output INI file into an XML formatted file.

CollectSID.cpp

Three functions are defined here:

  1. CollectGroups()
  2. CollectMembers()
  3. LookupSID()

Let's take a look at CollectGroups() in more detail.

// The NetLocalGroupEnum() API retrieves
//information about each local group account
// on a target machine.
NetStatus = NetLocalGroupEnum
(
  wszMachineName,
  0,
  &Data,
  8192,
  &Index,
  &Total,
  &ResumeHandle
);

if (NetStatus != NERR_Success || Data == NULL)
{
      dwLastError = GetLastError();
      bRet = FALSE;
  goto CollectGroups_0;
}

// Write down in the output file's
// header how many groups there are.
sprintf (szOutString, "%d", Total);
WritePrivateProfileString ("HEADER",
  "GROUP_COUNT", szOutString, lpszOutputFileName);

GroupInfo = (LOCALGROUP_INFO_0 *)Data;

CollectGroups() uses NetLocalGroupEnum() to retrieve information about each local group account. After successfully calling this function, information on the Group Accounts of the target machine are stored in an array of LOCALGROUP_INFO_0 structs which is returned in the Data byte pointer. The total number of Group Accounts is returned in Total.

We then store the total number of Group Accounts in the INI file under the section HEADER and under the key name GROUP_COUNT. Full information on the format of the INI file will be discussed in the section "INI File Format", later.

We then iterate through the entire array of LOCALGROUP_INFO_0 structs and store the name of each Group Account into the target INI file.

GroupInfo = (LOCALGROUP_INFO_0 *)Data;
  for (i=0; i < Total; i++)
  {
    // Convert group name from UNICODE to ansi.
    iRetOp = WideCharToMultiByte
    (
      (UINT)CP_ACP,         // code page
      (DWORD)0,         // performance and mapping flags
      (LPCWSTR)(GroupInfo->lgrpi0_name),
          // address of wide-character string
      (int)-1,       // number of characters in string
      (LPSTR)szAnsiName,  // address of buffer for new string
      (int)(sizeof(szAnsiName)),      // size of buffer
      (LPCSTR)NULL,  // address of default for unmappable characters
      (LPBOOL)NULL   // address of flag set when default char used.
    );

    // Write down the name of each group in the
    // HEADER section indexed by the key name GROUP_n
    sprintf (szOutString, "GROUP_%d", i + 1);
    WritePrivateProfileString ("HEADER", szOutString,
        (LPCTSTR)szAnsiName, lpszOutputFileName);

    // Find out the SID of the Group.
    strcpy (szOutString, "");  // Initialise szOutString
    LookupSID ((LPCTSTR)lpszMachineName,
       (LPCTSTR)szAnsiName, (LPTSTR)szOutString);
    // Write down details of each group in its own
    // section named by the group name itself.
    WritePrivateProfileString (szAnsiName,
       "SID", szOutString, lpszOutputFileName);
    // Now lookup all members of this group
    // and record down their names and SIDs into
    // the output file.
    CollectMembers((LPCTSTR)lpszMachineName,
      (LPCTSTR)szAnsiName, (LPCTSTR)lpszOutputFileName);

    GroupInfo++;
  }

Using the name of the Group Account, we further call on the function LookupSID() to get the SID of this Group Account.

Now, once we get the SID of a Group Account, we immediately CREATE a section in the INI file for that Group and also store a SID key in that section containing the SID value.

For example, once we have determined that the local machine has got 8 Group Accounts, and that one of these groups has the name "Administrators", the INI file will contain the following information:

[HEADER]
GROUP_COUNT=8
GROUP_1=Administrators
...
...
...
[Administrators]
SID=S-1-5-32-544
...
...
...

Notice that there will be an entry in the HEADER section for "Administrators" and "Administrators" will have its own section with a SID key that contains the SID value for "Administrators".

Let's take a look at CollectMembers() in more detail.

NetStatus = NetLocalGroupGetMembers
(
  wszMachineName,
  wszGroupName,
  1,
  &Data,
  8192,
  &Index,
  &Total,
  &ResumeHandle
);

if (NetStatus != NERR_Success || Data == NULL)
{
  dwLastError = GetLastError();
  bRet = FALSE;
  goto CollectMembers_0;
}

// Write down in the output file's section
// for this group the totla nu,ber of
// members it has.
sprintf (szSID, "%d", Total);
WritePrivateProfileString (lpszGroupName,
  "MEMBER_COUNT", szSID, lpszOutputFileName);

MemberInfo = (LOCALGROUP_MEMBERS_INFO_1 *)Data;

CollectMembers() calls on the NetLocalGroupGetMembers() API to retrieve a list of the members of a particular Group. This API works similarly to NetLocalGroupEnum() by storing all retrieved information in an array. This array is an array of LOCALGROUP_MEMBERS_INFO_1 structs which is returned in the LPBYTE Data.

The total number of members in this Group is returned in Total. This value is stored in the MEMBER_COUNT key of the Group's section which has already been created in the CollectGroups() function.

We then iterate through this array of LOCALGROUP_MEMBERS_INFO_1 structs and store each member name as a key value in the Group's section. The key itself is of the form MEMBER_n where n is a unique number within that Group.

We then call LookupSID() to get the member's SID string value and store this string value as a key value in the Group's section. The key itself is the member name.

For example, if we determined that the Group "Administrators" has got 4 members and that "Domain Admins" is one such member, then the INI file will contain the following information:

[Administrators]
SID=<SID string for Administrators>
MEMBER_COUNT=4
...
...
...
MEMBER_2=Domain Admins
Domain Admins=<SID string for Domain Admnins>
...
...
...

Let's take a look at LookupSID() in more detail.

A full discussion on the subject of NT Security Identifiers is beyond the scope of this article. I will assume that the reader has some knowledge on SIDs in general and its components. Many good books on Windows NT Security would cover this in detail, e.g. "Programming Windows Security" by Keith Brown (Addison Wesley). There is also a good article by Mark Russinovich at the Systems Internals Web Site, entitled Windows NT Security, which covers SIDs.

I'll give a brief overview of an NT SID. A SID is a variable-length numeric value that consists of the following:

  1. SID revision number.
  2. 48-bit identifier authority value.
  3. A variable number of 32-bit subauthority or Relative Identifier (RID) values.

The Win32 API provides the SID struct as well as a PSID pointer but does not encourage developers to extract values from such a structure directly. Instead, APIs are provided to help us retrieve individual values from this struct. The only field value from this struct that I directly extract is the REVISION value. I have not found any API that can help me ascertain this value from a SID.

The identifier authority value identifies the agent that issued the SID. This agent is usually an NT local system or a domain. Subauthority values identify trustees relative to the issuing authority. RIDs provide a way for NT to create unique SIDs from a base SID.

pSid = (PSID)bySidBuffer;
dwSidSize = sizeof(bySidBuffer);
dwDomainNameSize = sizeof(szDomainName);

bRetOp = LookupAccountName
(
  (LPCTSTR)lpszMachineName,  // address of string for system name
  (LPCTSTR)lpszAccountName, // address of string for account name
  (PSID)pSid,              // address of security identifier
  (LPDWORD)&dwSidSize,         // address of size of security identifier
  (LPTSTR)szDomainName, // address of string for referenced domain
  (LPDWORD)&dwDomainNameSize, // address of size of domain string
  (PSID_NAME_USE)&sidType    // address of SID-type indicator
);

if (bRetOp == FALSE)
{
  dwLastError = GetLastError();
  lRet = -1;  // Unable to obtain Account SID.
  goto LookupSID_0;
}

bRetOp = IsValidSid((PSID)pSid);

if (bRetOp == FALSE)
{
  dwLastError = GetLastError();
  lRet = -2;  // SID returned is invalid.
  goto LookupSID_0;
}

LookupSID() calls on LookupAccountName() to obtain the security identifier (SID) for an account and the name of the domain on which the account was found. This API requires the name of the account and the name of the machine on which the account exists.

Before calling on LookupAccountName(), we first declare a pointer to an SID (pSid (type PSID)) and a BYTE buffer (bySidBuffer). This BYTE buffer bySidBuffer will hold full SID data after LookupAccountName() returns.

Although the name of the domain and the SID type are returned, the project that I worked on did not have use for these values and so I ignored them. It will be good if the reader modifies the source of this program to output these values too. After successfully calling LookupAccountName(), we call IsValidSid() to further validate the returned SID.

// We initialise our SID string with the current standard for SID strings.
// The "S" is the standard prefix.
// We extract the revision number
// directly from the SID struct itself.
// This revision number is the only field
// value from SID that we extract directly.
// The other field values must be enquired via APIs.
sprintf (szSID, "S-%d", (((SID*)pSid) -> Revision));

I then begin forming a string version of the SID. A string SID starts with a standard prefix of "S" and hyphens separate its various components. After the "S", the revision number is stored. We take this revision number directly from the SID struct returned to us.

// Obtain via APIs the identifier authority value.
psid_identifier_authority = GetSidIdentifierAuthority ((PSID)pSid);

// Make a copy of it.
memcpy (&sid_identifier_authority,
  psid_identifier_authority, sizeof(SID_IDENTIFIER_AUTHORITY));

// The value in IDENTIFIER AUTHORITY is an array of 6 bytes.
// However, we are only interested in the last byte of the array.
sprintf (szIdentAuthValue, "-%d", (sid_identifier_authority.Value)[5]);
strcat (szSID, szIdentAuthValue);

The identifier authority value is to be inserted next and we use the GetSidIdentifierAuthority() to obtain a pointer to the SID_IDENTIFIER_AUTHORITY structure contained inside our SID. This structure contains the 48-bit identifier authority value. This 48-bit number is divided into 6 bytes. Currently, only the last byte is of any importance because the first 5 bytes are always 0.

We next obtain the count of subauthority values associated with this SID by calling on API GetSidSubAuthorityCount(). We then perform a loop that obtains the subauthority values from the SID via the GetSidSubAuthority() API. Each retrieved subauthority value is appended into our SID string.

// Determine how many sub-authority values there are in the current SID.
puchar_SubAuthCount = (PUCHAR)GetSidSubAuthorityCount((PSID)pSid);
// Assign it to a more convenient variable.
j = (unsigned char)(*puchar_SubAuthCount);
// Now obtain all the sub-authority values from the current SID.
for (i = 0; i < (int)j; i++)
{
  DWORD dwSubAuth = 0;
  PDWORD pdwSubAuth = NULL;
  char szSubAuthValue[80];

  // Obtain the current sub-authority
  // DWORD (referenced by a pointer)
  pdwSubAuth = (PDWORD)GetSidSubAuthority
  (
    (PSID)pSid, // address of security identifier to query
    (DWORD)i   // index of subauthority to retrieve
  );

  dwSubAuth = *pdwSubAuth;

  sprintf (szSubAuthValue, "-%d", dwSubAuth);
  strcat (szSID, szSubAuthValue);
}

msxmlwrp.cpp

This source file contains definitions for 3 basic classes CDOM, CMSXML_DOMDocument and CMSXML_Element. These classes encapsulate basic functionalities to write an XML file. No XML file read processing functionalities are provided. We simply want to output an XML file.

The XML file processing is done via MSXML due to its widespread availability. For simplicity, I have used the IDispatch interface (OLE Automation) for talking to MSXML and avoided importing MSXML's type library.

The main function that is of interest in msxmlwrp.cpp is ConvertINIToXML() which takes the outputted INI file, processes it and creates an XML file equivalent for it. This function should also serve as an example on how to process the INI file.

INI File Format

The INI file that is output from this program will always have a fixed HEADER section. This section will always contain a fixed GROUP_COUNT key:

[HEADER]
GROUP_COUNT=n
GROUP_1=...
GROUP_2=...
GROUP_3=...
GROUP_n=...

The key value for GROUP_COUNT is an integer that indicates how many groups there are in this INI file. There will be a GROUP_n key name for each group defined, where n is a number from 1 through GROUP_COUNT. The corresponding values for these keys will be the name of each group.

The INI file will then contain an individual section for every group.

[{GROUP NAME}]
SID=...
MEMBER_COUNT=n
MEMBER_1={Member 1 name}
{Member 1 name}=...
{Member 2 name}=...
...
...
...
{Member n name}=...

This section will always contain a fixed SID key which will contain the SID value for the group and a fixed MEMBER_COUNT key that will contain the total number of members contained in this group.

There will be a MEMBER_n key name for each member in this group where n is a number from 1 through MEMBER_COUNT. The corresponding values for these keys will be the name of the member.

Each member will also have a key in this section (the key name is the member name itself) and the value is the SID of the member.

For example, let's say there are 3 groups defined in a local machine. The groups are "Administrators", "Backup Operators" and "Guests". The following is a sample output in the INI file:

[HEADER]
GROUP_COUNT=3
GROUP_1=Administrators
GROUP_2=Backup Operators
GROUP_3=Guests

[Administrators]
SID=...
MEMBER_COUNT=1
MEMBER_1=Administrator
Administrator=...

<Backup Operators>
SID=...
MEMBER_COUNT=0

[Guests]
SID=S-1-5-32-546
MEMBER_COUNT=1
MEMBER_1=Guest
Guest=...

Limitations

  1. Only members of the Administrators or Account Operators local group can successfully use this tool. This is because the LAN Manager NT APIs will not succeed otherwise.
  2. Because CollectSID uses the standard Windows API WritePrivateProfielString(), if you do not specify a full path to your output file, the generated INI file will be written to the Windows directory. However, the generated XML file will be output to the active directory. Beware of this fact which may raise confusion. To be safe, always specify a full path.
  3. Also because WritePrivateProfileString() is used, if an existing INI file with the same name as your target file already exists, this existing file will not first be deleted. Its contents will be appended. Be aware of this fact which may raise confusion as well due to irrelevant information being included in the INI file.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Systems Engineer NEC
Singapore Singapore
Lim Bio Liong is a Specialist at a leading Software House in Singapore.

Bio has been in software development for over 10 years. He specialises in C/C++ programming and Windows software development.

Bio has also done device-driver development and enjoys low-level programming. Bio has recently picked up C# programming and has been researching in this area.

Comments and Discussions

 
QuestionSpaces in directory names problem. Pin
Member 125748249-Jun-16 3:54
Member 125748249-Jun-16 3:54 
GeneralWrong SID Pin
3004201625-Mar-08 2:38
3004201625-Mar-08 2:38 
XML returns the wrong SID values of Members
GeneralBug in this code Pin
Gregoire13-Jul-03 20:42
Gregoire13-Jul-03 20:42 
GeneralUserlist Pin
Haresh15-May-02 1:17
Haresh15-May-02 1:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.