C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)
아래의 글에 자세하게 설명하고 있습니다.
How can I find out the last time a user logged on from C++?
; https://devblogs.microsoft.com/oldnewthing/20230622-00/?p=108369
간단하게는 PowerShell로 가능하고,
PS C:\Users\testusr> echo $env:USERNAME
testusr
PS C:\Users\testusr> Get-LocalUser "testusr" | Format-List
AccountExpires :
Description :
Enabled : True
FullName :
PasswordChangeableDate : 2021-11-19 오전 7:49:48
PasswordExpires : // 리눅스라면 chage 명령어로 제어
UserMayChangePassword : True
PasswordRequired : False
PasswordLastSet : 2021-11-19 오전 7:49:48
LastLogon : 2023-06-23 오후 6:02:07 // 리눅스의 경우 "who -u" 명령어로 조회
Name : testusr
SID : S-1-5-....[생략]...
PrincipalSource : Local
ObjectClass : User
윈도우 Shell에서는 net.exe를 이용해 구할 수 있습니다.
C:\testusr> net user "testusr"
User name testusr
Full Name
Comment
User's comment
Country/region code 000 (System Default)
Account active Yes
Account expires Never
Password last set 2021-11-19 오전 7:49:48
Password expires Never
Password changeable 2021-11-19 오전 7:49:48
Password required No
User may change password Yes
Workstations allowed All
Logon script
User profile
Home directory
Last logon 2023-06-23 오후 6:02:07
Logon hours allowed All
Local Group Memberships *Administrators *docker-users
*Performance Log Users
Global Group memberships *None
The command completed successfully.
코드로는
NetUserGetInfo 함수를 이용해 구할 수 있습니다. (
문서에 예제 코드가 포함돼 있습니다.)
C#으로 구해보면, 대략 다음과 같은 코드로 나옵니다. ^^
using System.Runtime.InteropServices;
using System.Text;
namespace ConsoleApp1;
internal class Program
{
[DllImport("netapi32.dll", CharSet = CharSet.Unicode)] // lmaccess.h
public extern static NERR NetUserGetInfo(string servername, string username, NET_USER_GET_TYPE level, out IntPtr bufptr);
[DllImport("netapi32.dll", CharSet = CharSet.Unicode)]
public extern static uint NetApiBufferFree(IntPtr buffer); // lmapibuf.h
static void Main(string[] args)
{
string? userName = Environment.GetEnvironmentVariable("USERNAME");
string? serverName = Environment.GetEnvironmentVariable("USERDOMAIN");
if (userName == null || serverName == null)
{
Console.WriteLine("USERNAME or USERDOMAIN is not set");
return;
}
NET_USER_GET_TYPE infoType = NET_USER_GET_TYPE.USER_INFO_2;
NERR result = NetUserGetInfo(serverName, userName, infoType, out IntPtr buffer);
if (result != NERR.NERR_Success || buffer == IntPtr.Zero)
{
return;
}
switch (infoType)
{
case NET_USER_GET_TYPE.USER_INFO_2:
var userInfo = USER_INFO_2.LoadFromPtr(buffer);
Console.WriteLine(userInfo);
break;
}
NetApiBufferFree(buffer);
}
}
public struct USER_INFO_2 // lmaccess.h
{
public string usri2_name;
public string usri2_password;
public uint usri2_password_age;
public uint usri2_priv;
public string usri2_home_dir;
public string usri2_comment;
public uint usri2_flags;
public string usri2_script_path;
public uint usri2_auth_flags;
public string usri2_full_name;
public string usri2_usr_comment;
public string usri2_parms;
public string usri2_workstations;
public uint usri2_last_logon;
public uint usri2_last_logoff;
public uint usri2_acct_expires;
public uint usri2_max_storage;
public uint usri2_units_per_week;
public byte[] usri2_logon_hours;
public uint usri2_bad_pw_count;
public uint usri2_num_logons;
public string usri2_logon_server;
public uint usri2_country_code;
public uint usri2_code_page;
public uint usri2_user_id;
public uint usri2_primary_group_id;
public string usri2_profile;
public string usri2_home_dir_drive;
public uint usri2_password_expired;
public static USER_INFO_2 LoadFromPtr(IntPtr ptr)
{
if (IntPtr.Size == 4)
{
throw new NotSupportedException();
}
USER_INFO_2 data = new USER_INFO_2();
data.usri2_name = ReadString(ref ptr);
data.usri2_password = ReadString(ref ptr);
data.usri2_password_age = ReadUInt32(ref ptr);
data.usri2_priv = ReadUInt32(ref ptr);
data.usri2_home_dir = ReadString(ref ptr);
data.usri2_comment = ReadString(ref ptr);
data.usri2_flags = (uint)ReadUInt32(ref ptr, 8); // 8 bytes alignment
data.usri2_script_path = ReadString(ref ptr);
data.usri2_auth_flags = (uint)ReadUInt32(ref ptr, 8); // 8 bytes alignment
data.usri2_full_name = ReadString(ref ptr);
data.usri2_usr_comment = ReadString(ref ptr);
data.usri2_parms = ReadString(ref ptr);
data.usri2_workstations = ReadString(ref ptr);
data.usri2_last_logon = ReadUInt32(ref ptr);
data.usri2_last_logoff = ReadUInt32(ref ptr);
data.usri2_acct_expires = ReadUInt32(ref ptr);
data.usri2_max_storage = ReadUInt32(ref ptr);
data.usri2_units_per_week = ReadUInt32(ref ptr, 8); // 8 bytes alignment
data.usri2_logon_hours = ReadBytes(ref ptr, 21);
data.usri2_bad_pw_count = ReadUInt32(ref ptr);
data.usri2_num_logons = ReadUInt32(ref ptr);
data.usri2_logon_server = ReadString(ref ptr);
data.usri2_country_code = ReadUInt32(ref ptr);
data.usri2_code_page = ReadUInt32(ref ptr);
return data;
}
public static byte[] ReadBytes(ref IntPtr ptr, int count)
{
IntPtr ptrAddr = Marshal.ReadIntPtr(ptr);
ptr = new IntPtr(ptr + IntPtr.Size);
byte[] buffer = new byte[count];
for (int i = 0; i < count; i ++)
{
buffer[i] = Marshal.ReadByte(ptrAddr, i);
}
return buffer;
}
public static string ReadString(ref IntPtr ptr)
{
IntPtr ptrAddr = Marshal.ReadIntPtr(ptr);
ptr = new IntPtr(ptr + IntPtr.Size);
return Marshal.PtrToStringUni(ptrAddr) ?? "";
}
public static uint ReadUInt32(ref IntPtr ptr, int offset = 4)
{
uint value = (uint)Marshal.ReadInt32(ptr);
ptr = new IntPtr(ptr + offset);
return value;
}
public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine($"User account name: {usri2_name}");
sb.AppendLine($"Password: {usri2_password}");
sb.AppendLine($"Password age (seconds): {usri2_password_age}");
sb.AppendLine($"Privilege level: {usri2_priv}");
sb.AppendLine($"Home directory: {usri2_home_dir}");
sb.AppendLine($"Comment: {usri2_comment}");
sb.AppendLine($"Flags (in hex): {usri2_flags:x}");
sb.AppendLine($"Script path: {usri2_script_path}");
sb.AppendLine($"Auth flags (in hex): {usri2_auth_flags:x}");
sb.AppendLine($"Full name: {usri2_full_name}");
sb.AppendLine($"User comment: {usri2_usr_comment}");
sb.AppendLine($"Parameters: {usri2_parms}");
sb.AppendLine($"Workstations: {usri2_workstations}");
sb.AppendLine($"Last logon (seconds since January 1, 1970 GMT): {usri2_last_logon}");
sb.AppendLine($"Last logoff (seconds since January 1, 1970 GMT): {usri2_last_logoff}");
sb.AppendLine($"Account expires (seconds since January 1, 1970 GMT): {(int)usri2_acct_expires}");
sb.AppendLine($"Max storage: {(int)usri2_max_storage}");
sb.AppendLine($"Units per week: {usri2_units_per_week}");
sb.Append("Logon hours:");
for (int i = 0; i < 21; i ++)
{
sb.Append($" {usri2_logon_hours[i]:x2}");
}
sb.AppendLine();
sb.AppendLine($"Bad password count: {usri2_bad_pw_count}");
sb.AppendLine($"Number of logons: {usri2_num_logons}");
sb.AppendLine($"Logon server: {usri2_logon_server}");
sb.AppendLine($"Country code: {usri2_country_code}");
sb.AppendLine($"Code page: {usri2_code_page}");
return sb.ToString();
}
}
public enum NET_USER_GET_TYPE : uint // lmaccess.h
{
USER_INFO_2 = 2,
}
public enum NERR // lmerr.h
{
NERR_Success = 0,
ERROR_ACCESS_DENIED = 5,
ERROR_BAD_NETPATH = 53,
ERROR_INVALID_LEVEL = 124,
NERR_UserNotFound = 2221,
NERR_InvalidComputer = 2351,
}
C++에 맞춰진 구조체의 마샬링 코드가 좀 귀찮게 들어갔을 뿐 코드 자체는 단순합니다. 단지, 한 가지 주의할 점이 있는데요, C++의 구조체에 별도로
#pragma pack이 지정된 것은 아니어서,
typedef struct _USER_INFO_2 {
LPWSTR usri2_name;
LPWSTR usri2_password;
DWORD usri2_password_age;
DWORD usri2_priv;
LPWSTR usri2_home_dir;
LPWSTR usri2_comment;
DWORD usri2_flags;
LPWSTR usri2_script_path;
DWORD usri2_auth_flags;
LPWSTR usri2_full_name;
LPWSTR usri2_usr_comment;
LPWSTR usri2_parms;
LPWSTR usri2_workstations;
DWORD usri2_last_logon;
DWORD usri2_last_logoff;
DWORD usri2_acct_expires;
DWORD usri2_max_storage;
DWORD usri2_units_per_week;
PBYTE usri2_logon_hours;
DWORD usri2_bad_pw_count;
DWORD usri2_num_logons;
LPWSTR usri2_logon_server;
DWORD usri2_country_code;
DWORD usri2_code_page;
}USER_INFO_2, *PUSER_INFO_2, *LPUSER_INFO_2;
64비트의 경우 8바이트 정렬로 인해 위의 3개 필드(usri2_flags, usri2_auth_flags, usri2_units_per_week)의 데이터 자체는 4바이트 크기이지만 패딩을 위해 각각 4바이트를 더 건너뛰어야 합니다.
자... 그래서 이제 실행을 하면 이런 출력을 보게 될 것입니다. ^^
User account name: testusr
Password:
Password age (seconds): 50253110
Privilege level: 2
Home directory:
Comment:
Flags (in hex): 10221
Script path:
Auth flags (in hex): 0
Full name:
User comment:
Parameters:
Workstations:
Last logon (seconds since January 1, 1970 GMT): 1687523407
Last logoff (seconds since January 1, 1970 GMT): 0
Account expires (seconds since January 1, 1970 GMT): -1
Max storage: -1
Units per week: 168
Logon hours: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
Bad password count: 0
Number of logons: 1282
Logon server: \\*
Country code: 0
Code page: 0
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
참고로, Get과 대응해 Set에 해당하는
NetUserSetInfo API도 있는데, 해당 문서에는 계정을 비활성화하는 예제 코드를 담고 있습니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]