C#으로 윈도우즈 그룹 내 사용자 계정 열거하기

안녕하세요. 언제나 휴일입니다.

Windows 운영체제에서 그룹 정보를 열거하는 함수는NetLocalGroupEnum입니다.

그리고 그룹 내에 사용자 계정 정보를 얻어오는 함수는 NetLocalGroupGetMembers 함수입니다.

이 두 개의 함수는 모두 Windows API로 제공하는 함수입니다.

여기에서는 C#에서 Windows API를 dllimport하여 사용하는 방법을 이용할 거예요.

이용에 앞서 두 개의 함수에 관한 정보는 다음 글을 참고하세요.

윈도우즈 그룹 계정 열거하기 – NetLocalGroupEnum 함수

윈도우즈 그룹 내 사용자 계정 열거하기 – NetLocalGroupGetMembers함수

1. Windows API dllimport하기

C#에서 Windows API 함수나 형식을 사용하기 위해서는 [DllImport] 구문을 이용하여 선언해 주어야 합니다.

이미 앞에서 C#으로 사용자 계정 목록 열거하기와 그룹 목록 열거하기 글에서 이에 관해서 다루었습니다.

C#으로 사용자 계정 목록 열거하기

C#으로 그룹 목록 열거하기

다음은 그룹 내 계정 정보를 얻어오는 NetLocalGroupGetMembers 함수 선언문입니다.

NET_API_STATUS NET_API_FUNCTION
NetLocalGroupGetMembers (
In_opt LPCWSTR servername OPTIONAL,
In LPCWSTR localgroupname,
In DWORD level,
Outptr_result_buffer(Inexpressible(“varies”)) LPBYTE *bufptr,
In DWORD prefmaxlen,
Out LPDWORD entriesread,
Out LPDWORD totalentries,
Inout_opt PDWORD_PTR resumehandle
);

이를 dllimport 구문으로 표현하면 다음처럼 표현할 수 있습니다.

        [DllImport("NetAPI32.dll", CharSet = CharSet.Auto)]
        public static extern uint NetLocalGroupGetMembers
            (
            [MarshalAs(UnmanagedType.LPWStr)] String servername,
            [MarshalAs(UnmanagedType.LPWStr)] String localgroupname,
            uint level,
            out IntPtr bufptr,
            uint prefmaxlen,
            out uint entriesread,
            out uint totalentries,
            ref IntPtr resumehandle
            );

세 번째 인자인 level에 따라 전달하는 사용자 계정 정보 구조체가 다릅니다.

여기에서는 level2를 전달하여 사용할 거예요.

이 때는 LOCALGROUP_MEMBERS_INFO_2 구조체 정보를 수신합니다.

typedef struct _LOCALGROUP_MEMBERS_INFO_2 {

PSID lgrmi2_sid;

SID_NAME_USE lgrmi2_sidusage;

LPWSTR lgrmi2_domainandname;

} LOCALGROUP_MEMBERS_INFO_2;

이를 C#으로 래핑하면 다음처럼 포현할 수 있어요.

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct LOCALGROUP_MEMBERS_INFO_2
        {
            public IntPtr lgrmi2_sid;
            public int lgrmi2_sidusage;
            public string lgrmi2_domainandname;
        }

3. 특정 그룹에 사용자 목록 구하는 함수 만들기

앞에서 Windows API를 래핑한 WrapNative 클래스에 정적 함수로 만들기로 할게요.

먼저 함수 코드를 보여준 후에 설명하기로 할게요.

        public static List<string> GetLocalGroupMembers(string serv_name,string group_name)
        {
            List<string> members = new List<string>();
            uint rcnt, total;
            IntPtr handle = IntPtr.Zero;
            IntPtr buffer = IntPtr.Zero;
            uint val = NetLocalGroupGetMembers(
                serv_name, 
                group_name, 
                2,
                out buffer,
                0xFFFFFFF, 
                out rcnt, 
                out total, 
                ref handle);
            LOCALGROUP_MEMBERS_INFO_2 mi;
            IntPtr iter = buffer;
            for(int i = 0; i<rcnt;i++)
            {
                mi = (LOCALGROUP_MEMBERS_INFO_2)Marshal.PtrToStructure(iter, typeof(LOCALGROUP_MEMBERS_INFO_2));
                members.Add(mi.lgrmi2_domainandname);
                iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(LOCALGROUP_MEMBERS_INFO_2)));
            }
            NetApiBufferFree(buffer);
            return members;
        }

먼저 NetLocalGroupGetMembers함수를 호출합니다.

첫 번째 인자는 서버 이름으로 null을 전달하면 로컬 컴퓨터를 의미합니다.

두 번째 인자는 그룹 이름입니다.

세 번째 인자는 level로 데이터 정보 수준을 의미하며 여기에서는 2를 전달하여 얻어오는 정보는 LOCALGROUP_MEMBERS_INFO_2 구조체 형식입니다.

네 번째 인자는 사용자 정보를 수신할 버퍼로 시스템에 의해 할당합니다. (NetApiBufferFree함수로 해제합니다.)

다섯 번째 인자는 수신할 버퍼의 최대 크기(바이트)입니다.

여섯 번째 인자는 열거한 요소 수(그룹 내 사용자 개수)입니다.

일곱 번째 인자는 다시 열거할 때 수신할 수 있는 요소 수로 힌트로 사용하는 수준으로 여기에서는 큰 의미 없습니다.

마지막 인자는 다시 열거할 때 사용하는데 여기에서는 사용하지 않습니다.

이를 코드로 표현한 것이 다음입니다.

            uint rcnt, total;
            IntPtr handle = IntPtr.Zero;
            IntPtr buffer = IntPtr.Zero;
            uint val = NetLocalGroupGetMembers(
                serv_name, 
                group_name, 
                2,
                out buffer,
                0xFFFFFFF, 
                out rcnt, 
                out total, 
                ref handle);

수신한 버퍼에는 LOCALGROUP_MEMBERS_INFO_2 형식 정보가 rcnt만큼 있습니다.

이를 순차적으로 접근하여 사용자 정보를 얻어오고자 합니다.

먼저 수신한 버퍼의 시작 주소를 iter 변수에 설정한 후에 반복문을 rcnt 번 돌립니다.

            LOCALGROUP_MEMBERS_INFO_2 mi;
            IntPtr iter = buffer;
            for (int i = 0; i < rcnt; i++)
            {

현재 iter에 있는 정보를 LOCALGROUP_MEMBERS_INFO_2 형식 변수로 참조하게 합니다.

mi = (LOCALGROUP_MEMBERS_INFO_2)Marshal.PtrToStructure(iter, typeof(LOCALGROUP_MEMBERS_INFO_2));

사용자 이름을 얻어와서 반환할 리스트에 추가합니다.

members.Add(mi.lgrmi2_domainandname);

다음 사용자 정보가 있는 위치로 iter를 설정합니다.

iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(LOCALGROUP_MEMBERS_INFO_2)));

다음 코드는 수신 버퍼에 있는 사용자 정보를 반환할 리스트에 추가하는 코드입니다.

            LOCALGROUP_MEMBERS_INFO_2 mi;
            IntPtr iter = buffer;
            for (int i = 0; i < rcnt; i++)
            {
                mi = (LOCALGROUP_MEMBERS_INFO_2)Marshal.PtrToStructure(iter, typeof(LOCALGROUP_MEMBERS_INFO_2));                
                members.Add(mi.lgrmi2_domainandname);
                iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(LOCALGROUP_MEMBERS_INFO_2)));
            }

3. WrapNative.cs 코드

이전 게시글에서 래핑했던 사용자 계정 목록을 구하는 부분과 그룹 목록을 열거하는 부분도 포함한 코드입니다.

using System;
using System.Collections.Generic;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

namespace WrapNativeLib
{
    public static class WrapNative
    {
        [DllImport("netapi32.dll")]
        public static extern uint NetApiBufferFree(IntPtr buffer);

        [DllImport("netapi32.dll")]
        public static extern uint NetUserEnum(
            [MarshalAs(UnmanagedType.LPWStr)] String servername,
            uint level,
            uint filter,
            out IntPtr bufptr,
            uint prefmaxlen,
            out uint entriesread,
            out uint totalentries,
            ref IntPtr resume_handle);

        [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct USER_INFO_0
        {
            public IntPtr usri0_name;
        }
        [DllImport("netapi32.dll")]
        public static extern uint NetLocalGroupEnum(
            IntPtr servername,
            uint level,
            out IntPtr bufptr,
            uint prefmaxlen,
            out uint entriesread,
            out uint totalentries,
            ref IntPtr resumehandle
            );

        [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct LOCALGROUP_INFO_1
        {
            public IntPtr groupname;
            public IntPtr comment;
        }

        [DllImport("NetAPI32.dll", CharSet = CharSet.Auto)]
        public static extern uint NetLocalGroupGetMembers
            (
            [MarshalAs(UnmanagedType.LPWStr)] String servername,
            [MarshalAs(UnmanagedType.LPWStr)] String localgroupname,
            uint level,
            out IntPtr bufptr,
            uint prefmaxlen,
            out uint entriesread,
            out uint totalentries,
            ref IntPtr resumehandle
            );

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct LOCALGROUP_MEMBERS_INFO_2
        {
            public IntPtr lgrmi2_sid;
            public int lgrmi2_sidusage;
            public string lgrmi2_domainandname;
        }

        public static List<String> GetUserNames()
        {
            List<string> names = new List<string>();
            IntPtr buffer = IntPtr.Zero;
            uint rcnt;
            uint total;
            IntPtr handle = IntPtr.Zero;
            NetUserEnum(null, 0, 0, out buffer, 0xFFFFFFFF, out rcnt, out total, ref handle);

            IntPtr iter = buffer;
            for(uint i=0;i<rcnt; i++)
            {
                USER_INFO_0 ui = (USER_INFO_0)Marshal.PtrToStructure(iter, typeof(USER_INFO_0));
                string uname = Marshal.PtrToStringAuto(ui.usri0_name);
                names.Add(uname);
                iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(USER_INFO_0)));
            }
            return names;
        }
        public static List<String> GetGroupNames()
        {
            List<string> names = new List<string>();
            IntPtr buffer = IntPtr.Zero;
            uint rcnt;
            uint total;
            IntPtr handle = IntPtr.Zero;
            NetLocalGroupEnum(IntPtr.Zero, 1,out buffer, 0xFFFFFFFF, out rcnt, out total, ref handle);

            IntPtr iter = buffer;
            for (uint i = 0; i < rcnt; i++)
            {
                LOCALGROUP_INFO_1 gi = (LOCALGROUP_INFO_1)Marshal.PtrToStructure(iter, typeof(LOCALGROUP_INFO_1));
                string gname = Marshal.PtrToStringAuto(gi.groupname);
                names.Add(gname);
                iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(LOCALGROUP_INFO_1)));
            }
            return names;
        }
        public static List<string> GetLocalGroupMembers(string serv_name,string group_name)
        {
            List<string> members = new List<string>();
            uint rcnt, total;
            IntPtr handle = IntPtr.Zero;
            IntPtr buffer = IntPtr.Zero;
            uint val = NetLocalGroupGetMembers(
                serv_name, 
                group_name, 
                2,
                out buffer,
                0xFFFFFFF, 
                out rcnt, 
                out total, 
                ref handle);
            LOCALGROUP_MEMBERS_INFO_2 mi;
            IntPtr iter = buffer;
            for(int i = 0; i<rcnt;i++)
            {
                mi = (LOCALGROUP_MEMBERS_INFO_2)Marshal.PtrToStructure(iter, typeof(LOCALGROUP_MEMBERS_INFO_2));
                members.Add(mi.lgrmi2_domainandname);
                iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(LOCALGROUP_MEMBERS_INFO_2)));
            }
            NetApiBufferFree(buffer);
            return members;
        }
    }
}

4. WrapNative 클래스 사용

코드는 간단합니다.

먼저 그룹 목록을 출력하고 사용자에게 그룹을 선택하게 합니다.

선택한 그룹에 속하는 사용자 정보를 얻어와서 출력합니다.

using System;
using System.Collections.Generic;
using WrapNativeLib;

namespace CS_그룹_내_사용자_목록
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("=== 그룹 목록 ===");
            int num = 1;
            List<string> gnames = WrapNative.GetGroupNames();
            foreach (string name in gnames)
            {
                Console.WriteLine("{0}:{1}",num,name);
                num++;
            }
            Console.Write("번호:");
            int gnum = 1;
            if(int.TryParse(Console.ReadLine(), out gnum) == false)
            {
                Console.WriteLine("잘못 입력하였습니다. 번호를 입력하세요.");
                return;
            }
            if(gnum<0 | gnum>=gnames.Count)
            {
                Console.WriteLine("잘못 선택하였습니다.");
                return;
            }
            string gname = gnames[gnum - 1];
            Console.WriteLine("=== {0} 그룹 내 사용자 목록 ===", gname);
            foreach(string name in WrapNative.GetLocalGroupMembers(null,gname))
            {
                Console.WriteLine(name);
            }
        }
    }
}

실행 결과 예

=== 그룹 목록   ===
1:Administrators
2:Device Owners
3:Distributed COM Users
4:Event Log Readers
5:Guests
6:Hyper-V Administrators
7:IIS_IUSRS
8:Performance Log Users
9:Performance Monitor Users
10:Remote Management Users
11:System Managed Accounts Group
12:Users
13:SQLServer2005SQLBrowserUser$DESKTOP-T87J0UT
번호:12
=== Users 그룹 내 사용자 목록 ===
NT AUTHORITY\Authenticated Users
NT AUTHORITY\INTERACTIVE
DESKTOP-T87J0UT\홍길동