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

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

Windows 운영체제에서 사용자 계정 목록을 열거하는 함수는 NetUserEnum 입니다.

Windows API로 제공하는 함수입니다.

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

윈도우즈 사용자(계정)목록 열거하기 – NetUserEnum 함수

1. Windows API dllimport 하기

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

원래 NetAPiBUfferFree 함수의 원형은 다음과 같습니다.

DWORD NET_API_FUNCTION NetApiBufferFree(LPVOID Buffer );

NET_API_FUNCTION은 netapi32.dll 에서 제공하는 함수라고 생각하세요.

DWORD는 unsigned int 형식으로 C#에서의 uint로 매핑할 수 있습니다.

LPVOID 처럼 포인터 형식은 IntPtr로 표현하면 적당합니다.

이를 다음처럼 static extern 키워드를 명시하여 선언해 줍니다.

        [DllImport("netapi32.dll")]
        public static extern uint NetApiBufferFree(IntPtr bufptr);

만약 함수 내에서 전달한 변수 주소에 값을 채워주는 인자라면 ref로 명시하면 적당합니다.

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

위의 NetUserEnum 함수는 다음처럼 선언할 수 있습니다.

        [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);

NetUserEnum 함수는 level 인자의 값에 따라 사용자 정보 수준이 달라집니다.

여기에서는 level 0을 전달하는 예로 작성하였는데 이 때 제공하는 형식은 USER_INFO_0 구조체입니다.

typedef struct _USER_INFO_0 {
LPWSTR usri0_name;
}USER_INFO_0;

이러한 형식은 [StructLayoutAttribute()]을 이용하여 정의할 수 있습니다.

        [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct USER_INFO_0
        {
            public IntPtr usri0_name;
        }

다음은 사용자 계정을 열거하기 위해 필요한 두 개의 함수와 하나의 형식을 래핑한 예입니다.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;

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

        [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;
        }
        ...중략...
    }
}

2. 사용자 계정 목록 구하는 함수 만들기

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

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

        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;
        }

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

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

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

세 번째 인자는 사용자 계정 유형으로 0이면 모든 사용자입니다.

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

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

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

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

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

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

            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);

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

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

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

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

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

                    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를 설정합니다.

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

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

            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)));
            }

3. WrapNative.cs 코드

using System;
using System.Collections.Generic;
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;
        }

        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;
        }
    }
}

4. WrapNative 클래스 사용

코드는 매우 간단합니다.

별다른 설명은 하지 않겠습니다.

using System;
using WrapNativeLib;

namespace CS_계정_목록
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("=== 사용자 계정 목록   ===");
            foreach(string username in WrapNative.GetUserNames())
            {
                Console.WriteLine(username);
            }
        }
    }
}

실행 결과 예

=== 사용자 계정 목록   ===
Administrator
BIT
DefaultAccount
Guest
WDAGUtilityAccount
홍길동