Article C0005 C# .NET
Legacy and Global Groups

When looking at security groups in legacy domains both the NetApi32.DLL and AdvApi32.DLL are commonly used. This article explains how to obtain the Security Identifiers of both local and global groups.

Now looking at available information blocks regarding the groups I came across the following matter. Regarding global groups Microsoft Windows NT 4.0 only supports, GROUP_INFO_0, GROUP_INFO_1 and GROUP_INFO_2. Within the GROUP_INFO_2 information block only the RID of the group can be found. In the GROUP_INFO_3 information block the SID is added, but as stated, this is not supported on legacy systems. To make it even worse, global groups and local groups have to be handled separately and the local group information block stops at LOCALGROUP_INFO_1, which does not contain a SID as well.

The structure of the global group layout is shown here:

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    internal struct GROUP_INFO_2
    {
        [MarshalAs(UnmanagedType.LPWStr)]
        public string grpi2_name;
        
        [MarshalAs(UnmanagedType.LPWStr)]
        public string grpi2_comment;
        
        [MarshalAs(UnmanagedType.U4)]
        public int grpi2_group_id; // RID of Group (SID not available)
        
        [MarshalAs(UnmanagedType.U4)]
        public int grpi2_attribute;
        }
All information of this structure is documented at the MSDN website:

http://msdn2.microsoft.com/en-us/library/aa370271.aspx

The structure of the local group layout is shown here:

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    internal struct LOCALGROUP_INFO_1
    {
        [MarshalAs(UnmanagedType.LPWStr)]
        public string lgrp1_name;
        
        [MarshalAs(UnmanagedType.LPWStr)]
        public string lgrp1_comment;
    }
All information of this structure is documented at the MSDN website:

http://msdn2.microsoft.com/en-us/library/aa370277.aspx

To be able to read the groups unmanaged code must called using the NetApi32.DLL, for global groups this will be:

    [DllImport("netapi32.dll", EntryPoint = "NetGroupEnum")]
    internal extern static uint NetGroupEnum([MarshalAs(UnmanagedType.LPWStr)]
        string servername,
        int level, out IntPtr bufPtr, int preMaxLen, out uint entriesRead,
        out uint totalEntries, IntPtr resumeHandle);
And for the local groups this should be:

    [DllImport("netapi32.dll", EntryPoint = "NetLocalGroupEnum")]
    internal extern static uint NetLocalGroupEnum([MarshalAs(UnmanagedType.LPWStr)]
        string servername,
        int level, out IntPtr bufPtr, int preMaxLen, out uint entriesRead,
        out uint totalEntries, IntPtr resumeHandle);
Next step is to iterate through the list of global groups use code like this:

    for (int i = 0; i < EntriesRead; i++)
    {
        Groups[i] = (GROUP_INFO_2)Marshal.PtrToStructure(iter, typeof(GROUP_INFO_2));
        iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(GROUP_INFO_2)));
        Console.WriteLine(Groups[i].grpi2_name);
    }
And the iteration of the local groups can be done using the following code:

    for (int i = 0; i < EntriesRead; i++)
    {
        Groups[i] = (LOCALGROUP_INFO_1)Marshal.PtrToStructure(iter,
            typeof(LOCALGROUP_INFO_1));
            
        iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(LOCALGROUP_INFO_1)));
        Console.WriteLine(Groups[i].lgrp1_name);
    }
Since this is a collection of groups, the memory must be release when you’re finished reading it:

    [DllImport("netapi32.dll")]
    extern static int NetApiBufferFree(IntPtr Buffer);
When calling the NetGroupEnum function keep in mind to use the right value for the structure information block, a block called GROUP_INFO_2 must have its level set to 2. The application will definitely crash otherwise.

So far so good, we can create a list of both global and local groups, but we still do not have a SID. New within the .NET 2.0 Framework is the feature to create an NTAccount object which takes both groups and users as input. To be able to use NTAccount smoothly add the System.Security.Principal namespace in your project.

Details of NTAccount can be found on the MSDN website:

http://msdn2.microsoft.com/en-us/library/system.security.principal.ntaccount(VS.80).aspx

Be careful accessing the SID since group names are not always created as expected. When creating a new account simply add the servername together with a group or user account name. In this case we need the SID of a group, so the syntax will be <servername>\<groupname>. After the group has been created the account must be translated toward a SID using the Translate function of the NTAccount object.

    try
    {
        NTAccount id = new NTAccount(edtSD.Text + "\\" + Groups[i].grpi2_name);
        SecurityIdentifier sid =
            (SecurityIdentifier)id.Translate(typeof(SecurityIdentifier));
        sidStr = sid.ToString();
    }
When you execute the code to younger operating systems some of the groups will not report a SID. This is due to the fact that we ask for groups called <servername>\<groupname>, while the builtin groups are called "BUILTIN"\<groupname>. The easy way is to catch these cases is simply ask again like I usually do:

    try
    {
        NTAccount id = new NTAccount(edtSD.Text + "\\" + Groups[i].lgrp1_name);
        SecurityIdentifier sid =
            (SecurityIdentifier)id.Translate(typeof(SecurityIdentifier));
        sidStr = sid.ToString();
    }
    catch
    {
        try
        {
            NTAccount id = new NTAccount("BUILTIN\\" + Groups[i].lgrp1_name);
            SecurityIdentifier sid = 
                (SecurityIdentifier)id.Translate(typeof(SecurityIdentifier));
            sidStr = sid.ToString();
      	}
      	catch
      	{
      	    sidStr = "?";
        }
    }
To retrieve the actual group name using the NTAccount object simply access the Value property. The shown code will look like this:

	NTAccount id = new NTAccount(edtSD.Text + "\\" + Groups[i].lgrp1_name);
	SecurityIdentifier sid =
	    (SecurityIdentifier)id.Translate(typeof(SecurityIdentifier));
	sidStr = sid.ToString();
~Edward