성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)</h1> <p> 아래의 글에 자세하게 설명하고 있습니다.<br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > How can I find out the last time a user logged on from C++? ; <a target='tab' href='https://devblogs.microsoft.com/oldnewthing/20230622-00/?p=108369'>https://devblogs.microsoft.com/oldnewthing/20230622-00/?p=108369</a> </pre> <br /> 간단하게는 PowerShell로 가능하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > PS C:\Users\testusr> <span style='color: blue; font-weight: bold'>echo $env:USERNAME</span> testusr PS C:\Users\testusr> <span style='color: blue; font-weight: bold'>Get-LocalUser "testusr" | Format-List</span> AccountExpires : Description : Enabled : True FullName : PasswordChangeableDate : 2021-11-19 오전 7:49:48 PasswordExpires : // 리눅스라면 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13249'>chage 명령어</a>로 제어 UserMayChangePassword : True PasswordRequired : False PasswordLastSet : 2021-11-19 오전 7:49:48 <span style='color: blue; font-weight: bold'>LastLogon : 2023-06-23 오후 6:02:07</span> // 리눅스의 경우 "who -u" 명령어로 조회 Name : testusr SID : S-1-5-....[생략]... PrincipalSource : Local ObjectClass : User </pre> <br /> 윈도우 Shell에서는 net.exe를 이용해 구할 수 있습니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C:\testusr> <span style='color: blue; font-weight: bold'>net user "testusr"</span> 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 <span style='color: blue; font-weight: bold'>Last logon 2023-06-23 오후 6:02:07</span> Logon hours allowed All Local Group Memberships *Administrators *docker-users *Performance Log Users Global Group memberships *None The command completed successfully. </pre> <br /> 코드로는 <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetinfo'>NetUserGetInfo</a> 함수를 이용해 구할 수 있습니다. (<a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetinfo#examples'>문서에 예제 코드</a>가 포함돼 있습니다.)<br /> <br /> C#으로 구해보면, 대략 다음과 같은 코드로 나옵니다. ^^<br /> <br /> <pre style='height: 400px; margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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, } </pre> <br /> C++에 맞춰진 구조체의 마샬링 코드가 좀 귀찮게 들어갔을 뿐 코드 자체는 단순합니다. 단지, 한 가지 주의할 점이 있는데요, C++의 구조체에 별도로 <a target='tab' href='https://learn.microsoft.com/en-us/cpp/preprocessor/pack'>#pragma pack</a>이 지정된 것은 아니어서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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; <span style='color: blue; font-weight: bold'>DWORD usri2_flags;</span> LPWSTR usri2_script_path; <span style='color: blue; font-weight: bold'>DWORD usri2_auth_flags;</span> 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; <span style='color: blue; font-weight: bold'>DWORD usri2_units_per_week;</span> 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; </pre> <br /> 64비트의 경우 8바이트 정렬로 인해 위의 3개 필드(usri2_flags, usri2_auth_flags, usri2_units_per_week)의 데이터 자체는 4바이트 크기이지만 패딩을 위해 각각 4바이트를 더 건너뛰어야 합니다.<br /> <br /> 자... 그래서 이제 실행을 하면 이런 출력을 보게 될 것입니다. ^^ <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2093&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> 참고로, Get과 대응해 Set에 해당하는 <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusersetinfo'>NetUserSetInfo API</a>도 있는데, 해당 문서에는 계정을 비활성화하는 예제 코드를 담고 있습니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1490
(왼쪽의 숫자를 입력해야 합니다.)