성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그런 부분은 클라우드 업체 쪽에 문의를 하는 것이 더 좋지 않을...
[정성태] 정적 분석과 함께, 이제는 실행 시 성능 분석까지 (비록 Azu...
[정성태] .NET Source Browser를 이용해 Roslyn 소스 ...
[정성태] Experimental C# Interceptors: AOT &...
[정성태] .NET Conf 2023 (Day 2) - Tiny, fast...
[정성태] The end of the Tye Experiment #1622...
[정성태] This is a simple app that converts ...
[정성태] Wrathmark: An Interesting Compute W...
[정성태] FFmpeg Filters Every Youtuber Needs...
[정성태] 일단, PInvokeStackImbalance 오류가 발생했다는...
글쓰기
제목
이름
암호
전자우편
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# - GetTokenInformation으로 사용자 SID(Security identifiers) 구하는 방법</h1> <p> 사용자의 <a target='tab' href='https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers'>SID</a>는 이미 BCL을 이용해 간단하게 구할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Console.WriteLine(WindowsIdentity.GetCurrent().User?.Value); // 출력 결과: S-1-5-21-1510216573-2196513108-161129836-1001 // 2.4.2.4 Well-Known SID Structures // ; <a target='tab' href='https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/81d92bba-d22b-4a8c-908a-554ab29148ab'>https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/81d92bba-d22b-4a8c-908a-554ab29148ab</a> </pre> <br /> 하지만 이 글에서는 (그냥 재미 삼아) Win32 API를 통한 방법을 설명할 텐데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > GetTokenInformation ; <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-gettokeninformation'>https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-gettokeninformation</a> </pre> <br /> C++로는 많이 알려져 있으니 이번 글에서는 C#으로 ^^ 구현해 보겠습니다.<br /> <br /> <pre style='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.Runtime.Versioning; using System.Security.Principal; [assembly: SupportedOSPlatform("windows")] internal class Program { <span style='color: blue; font-weight: bold'>[DllImport("advapi32.dll", SetLastError = true)] static extern bool GetTokenInformation(IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, IntPtr TokenInformation, uint TokenInformationLength, out uint ReturnLength);</span> <span style='color: blue; font-weight: bold'>[DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)] static extern bool ConvertSidToStringSid(IntPtr pSID, out IntPtr ptrSid);</span> private static uint ERROR_INSUFFICIENT_BUFFER = 122; static void Main(string[] args) { string sidText; <span style='color: blue; font-weight: bold'>TOKEN_USER? tokenUser = GeTokenUser(out sidText);</span> if (tokenUser == null) { return; } <span style='color: blue; font-weight: bold'>Console.WriteLine($"SID Found: {sidText}");</span> } static unsafe TOKEN_USER? GeTokenUser(out string sid) { sid = ""; IntPtr hToken = WindowsIdentity.GetCurrent().Token; uint dwBufferSize; if (!<span style='color: blue; font-weight: bold'>GetTokenInformation</span>(hToken, TOKEN_INFORMATION_CLASS.TokenUser, IntPtr.Zero, 0, out dwBufferSize)) { int win32Result = Marshal.GetLastWin32Error(); if (win32Result != ERROR_INSUFFICIENT_BUFFER) { Console.WriteLine($"GetTokenInformation failed. GetLastError returned: {win32Result}"); return null; } } IntPtr tokenInformation = <span style='color: blue; font-weight: bold'>Marshal.AllocHGlobal((int)dwBufferSize);</span> if (!<span style='color: blue; font-weight: bold'>GetTokenInformation</span>(hToken, TOKEN_INFORMATION_CLASS.TokenUser, tokenInformation, dwBufferSize, out dwBufferSize)) { Console.WriteLine($"GetTokenInformation failed. GetLastError returned: {Marshal.GetLastWin32Error()}"); return null; } try { TOKEN_USER? tokenUser = (TOKEN_USER?)Marshal.PtrToStructure(tokenInformation, typeof(TOKEN_USER)); if (tokenUser == null) { return null; } IntPtr pstr = IntPtr.Zero; if (<span style='color: blue; font-weight: bold'>ConvertSidToStringSid</span>(tokenUser.Value.User.Sid, <span style='color: blue; font-weight: bold'>out pstr</span>) == true) { sid = Marshal.PtrToStringAuto(pstr) ?? ""; <span style='color: blue; font-weight: bold'>Marshal.FreeHGlobal(pstr);</span> } return tokenUser; } finally { <span style='color: blue; font-weight: bold'>Marshal.FreeHGlobal(tokenInformation);</span> } } } [StructLayout(LayoutKind.Sequential)] public struct SID_AND_ATTRIBUTES { public IntPtr Sid; public int Attributes; } [StructLayout(LayoutKind.Sequential)] public struct TOKEN_USER { public SID_AND_ATTRIBUTES User; public SecurityIdentifier Sid; } [StructLayout(LayoutKind.Sequential)] public unsafe struct SecurityIdentifier { public byte Revision; public byte SubAuthorityCount; public <a target='tab' href='https://www.sysnet.pe.kr/2/0/13205'>fixed</a> byte IdentifierAuthority[6]; public <a target='tab' href='https://www.sysnet.pe.kr/2/0/13205'>fixed</a> int SubAuthority[1]; } internal enum TOKEN_INFORMATION_CLASS { TokenUser = 1, } </pre> <br /> 위의 코드를 보면 다음의 순서로 sid를 구하는데요,<br /> <br /> <ol> <li>GetTokenInformation을 호출해 사용자 보안 토큰의 크기를 구하고,</li> <li>그 크기만큼을 메모리에 할당한 다음,</li> <li>다시 GetTokenInformation을 호출해 보안 토큰 정보를 반환</li> <li>그 보안 토큰으로부터 SID 문자열 얻기</li> </ol> <br /> 여기서 재미있는 것은 TOKEN_USER 타입의 구조입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_user'>https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_user</a> typedef struct _TOKEN_USER { SID_AND_ATTRIBUTES User; } TOKEN_USER, *PTOKEN_USER; // <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_and_attributes'>https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_and_attributes</a> typedef struct _SID_AND_ATTRIBUTES { <span style='color: blue; font-weight: bold'>PSID Sid;</span> DWORD Attributes; } SID_AND_ATTRIBUTES, * PSID_AND_ATTRIBUTES; // <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid'>https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid</a> typedef struct _SID { BYTE Revision; BYTE SubAuthorityCount; SID_IDENTIFIER_AUTHORITY IdentifierAuthority; <span style='color: blue; font-weight: bold'>DWORD SubAuthority[1];</span> // SubAuthorityCount 수만큼 배열이 동적으로 결정됨 } SID, *PISID; // <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_identifier_authority'>https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_identifier_authority</a> typedef struct _SID_IDENTIFIER_AUTHORITY { BYTE Value[6]; } SID_IDENTIFIER_AUTHORITY, *PSID_IDENTIFIER_AUTHORITY; </pre> <br /> 보는 바와 같이 <a target='tab' href='https://devblogs.microsoft.com/oldnewthing/20230517-00/?p=108207'>TOKEN_USER는 내부에 SID를 향한 포인터를 담고 있으며</a> 다시 그 _SID의 마지막 멤버인 SubAuthority는 그 크기가 정해지지 않은, 즉, 사용자에 따라 동적으로 달라지는 배열을 포함하고 있습니다.<br /> <br /> 바로 이러한 동적 크기의 성격 때문에 GetTokenInformation API는 크기를 먼저 얻게 한 다음, 사용자 측에서 그 크기만큼 메모리를 할당하게 만들고, 그것을 다시 GetTokenInformation에 전달해 TOKEN_USER 구조체의 모든 내용을 반환받는 식으로 동작하는 것입니다.<br /> <br /> 그리고 여기서 또 한 가지 재미있는 점은, SID_AND_ATTRIBUTES 구조체의 멤버인 Sid가 포인터이긴 하지만, 그리 멀리 있지 않은, 즉 GetTokenInformation이 반환한 크기 이내에 있는 위치를 가리킨다는 점입니다. 다시 말해, GetTokenInformation이 44를 반환했다면, 이 구조체는 다음과 같은 유형으로 정의가 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // x64인 경우, GetTokenInformation이 요구한 크기가 44바이트로 가정 offset: 0x0 - TOKEN_USER의 시작, 즉 SID_AND_ATTRIBUTES의 시작, 결국 Sid 8바이트 포인터 멤버 이 포인터는 현재로부터 0x10 이후의 위치를 가리킴 offset: 0x8 - Attributes 4바이트 offset: 0xc - 4바이트 패딩 offset: 0x10 - 이하 (44 - 16) 28바이트까지 SID 구조체 내용 포함 BYTE Revision 필드 (현재는 1이지만 향후 개정판이 나온다면 변경) offset: 0x11 - BYTE SubAuthorityCount 필드 offset: 0x12 - BYTE Value[6] == SID_IDENTIFIER_AUTHORITY IdentifierAuthority offset: 0x18 - DWORD SubAuthority[SubAuthorityCount] </pre> <br /> 보는 바와 같이 포인터가 구조체 내부의 영역을 가리키는 식입니다. 따라서 만약 "Marshal.AllocHGlobal((int)dwBufferSize);"로 할당한 메모리의 주소가 0x1000이라면, Sid 포인터의 값은 0x1010입니다. (TOKEN_USER와 SID_AND_ATTRIBUTES 구조체가 향후 달라질 가능성은 거의 없긴 해도 하드 코딩하는 것은 끊임없는 주의를 요합니다. ^^)<br /> <br /> <hr style='width: 50%' /><br /> <a name='free_token'></a> <br /> 어쨌든, 이번 글의 주제에 따라 SID는 ConvertSidToStringSid API를 호출하는 시점에 안전하게 구할 수 있었지만, 혹시 기왕에 구한 tokenUser 인스턴스를 재사용하는 것이 가능할까요?<br /> <br /> 바로 전에 설명한 TOKEN_USER 인스턴스의 메모리 구조를 염두에 두고, 아래의 코드를 자세하게 다시 살펴보겠습니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > IntPtr tokenInformation = <span style='color: blue; font-weight: bold'>Marshal.AllocHGlobal((int)dwBufferSize);</span> GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenUser, tokenInformation, dwBufferSize, out dwBufferSize); try { TOKEN_USER? tokenUser = (TOKEN_USER?)<span style='color: blue; font-weight: bold'>Marshal.PtrToStructure</span>(tokenInformation, typeof(TOKEN_USER)); <span style='color: blue; font-weight: bold'>return tokenUser;</span> } finally { <span style='color: blue; font-weight: bold'>Marshal.FreeHGlobal(tokenInformation);</span> } </pre> <br /> 얼핏 보면 Marshal.PtrToStructure를 통해 값 복사를 했으므로 이후 독자적으로 tokenUser 인스턴스를 사용해도 될 것 같은데요, 하지만, 이렇게 반환한 tokenUser 인스턴스는 finally에 의해 해제되는 tokenInformation으로 인해 향후 사용 시 오류가 발생하게 됩니다. 즉, 반환한 tokenUser 인스턴스는 더 이상 유효하지 않은 값이 된 것입니다.<br /> <br /> <a name='Win32UserToken'></a> 왜냐하면, TOKEN_USER의 값 자체는 복사되었지만, PSID 포인터가 가리키는 영역이 Marshal.FreeHGlobal에 의해 해제가 되었으므로 더 이상 유효하지 않는 포인터가 됐기 때문입니다. 이로 인해, 아쉽지만 TOKEN_USER 인스턴스가 필요하다면 저런 식으로 메서드 한 개에 추상화시키는 것은 좀 무리가 있고, 차라리 그냥 클래스 수준으로 추상화하는 것이 더 좋습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Runtime.InteropServices; public class Win32UserToken : IDisposable { IntPtr _tokenInformation; TOKEN_USER? _tokenUser; string _sid = ""; public string Sid => _sid; const uint ERROR_INSUFFICIENT_BUFFER = 122; [DllImport("advapi32.dll", SetLastError = true)] static extern bool GetTokenInformation(IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, IntPtr TokenInformation, uint TokenInformationLength, out uint ReturnLength); [DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)] static extern bool ConvertSidToStringSid(IntPtr pSID, out IntPtr ptrSid); public static implicit operator TOKEN_USER(Win32UserToken token) { if (token._tokenUser == null) { throw new NullReferenceException(nameof(token._tokenUser)); } return token._tokenUser.Value; } public Win32UserToken(IntPtr hToken) { uint dwBufferSize; if (!GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenUser, IntPtr.Zero, 0, out dwBufferSize)) { int win32Result = Marshal.GetLastWin32Error(); if (win32Result != ERROR_INSUFFICIENT_BUFFER) { return; } } _tokenInformation = Marshal.AllocHGlobal((int)dwBufferSize); if (!GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenUser, _tokenInformation, dwBufferSize, out dwBufferSize)) { return; } _tokenUser = (TOKEN_USER?)Marshal.PtrToStructure(_tokenInformation, typeof(TOKEN_USER)); if (_tokenUser == null) { return; } IntPtr pstr = IntPtr.Zero; if (ConvertSidToStringSid(_tokenUser.Value.User.Sid, out pstr) == true) { _sid = Marshal.PtrToStringAuto(pstr) ?? ""; Marshal.FreeHGlobal(pstr); } } <span style='color: blue; font-weight: bold'>public void Dispose() { if (_tokenInformation != IntPtr.Zero) { Marshal.FreeHGlobal(_tokenInformation); _tokenInformation = IntPtr.Zero; } }</span> } // ...[생략]... </pre> <br /> 그래서 이런 식으로 사용하면 안전하게 TokenUser를 보호할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Runtime.Versioning; using System.Security.Principal; [assembly: SupportedOSPlatform("windows")] internal class Program { static void Main(string[] args) { IntPtr hToken = WindowsIdentity.GetCurrent().Token; <span style='color: blue; font-weight: bold'>using (Win32UserToken tokenUser = new Win32UserToken(hToken)) {</span> Console.WriteLine(tokenUser.Sid); // S-1-5-21-1510216573-2196513108-161129836-1001 // 이 범위 내에서 tokenUser._tokenUser 인스턴스를 안전하게 사용 <span style='color: blue; font-weight: bold'>}</span> } } </pre> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, 저렇게 출력한 Sid 문자열(S-1-5-21-1510216573-2196513108-161129836-1001)은 TOKEN_USER로부터 그대로 구하는 것이 가능합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > S: 접두사 1: SecurityIdentifier의 Revision 필드 값 5: SecurityIdentifier의 6바이트 IdentifierAuthority 값 21: DWORD SubAuthority[0] 1510216573: DWORD SubAuthority[0] 2196513108: DWORD SubAuthority[2] 161129836: DWORD SubAuthority[3] 1001: DWORD SubAuthority[4] </pre> <br /> 따라서 Win32UserToken 타입에 다음과 같은 방법으로 SID 문자열을 구할 수도 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public override string ToString() { if (this._tokenUser == null) { return ""; } TOKEN_USER tokenUser = this._tokenUser.Value; <span style='color: blue; font-weight: bold'>return $"S-{tokenUser.Sid.Revision}-{tokenUser.Sid.IdentifierAuthorityAsValue}-{this.SubAuthority}";</span> } public unsafe int IdentifierAuthority { get { if (this._tokenUser == null) { return 0; } TOKEN_USER tokenUser = this._tokenUser.Value; int result = 0; for (int i = 0; i < 6; i++) { byte value = tokenUser.Sid.IdentifierAuthority[i]; result |= (value << (8 * (6 - (i + 1)))); } return result; } } public unsafe string SubAuthority { get { if (this._tokenUser == null) { return ""; } TOKEN_USER tokenUser = this._tokenUser.Value; IntPtr ptr = IntPtr.Add(this._tokenInformation, 0x18); string[] texts = new string[tokenUser.Sid.SubAuthorityCount]; for (int i = 0; i < tokenUser.Sid.SubAuthorityCount; i++) { int auth = *(int *)ptr.ToPointer(); texts[i] = auth.ToString(); ptr = IntPtr.Add(ptr, 4); } return string.Join('-', texts); } } [StructLayout(LayoutKind.Sequential)] public unsafe struct SecurityIdentifier { public byte Revision; public byte SubAuthorityCount; public fixed byte IdentifierAuthority[6]; public fixed int SubAuthority[1]; public unsafe int IdentifierAuthorityAsValue { get { int result = 0; for (int i = 0; i < 6; i++) { byte value = IdentifierAuthority[i]; result |= (value << (8 * (6 - (i + 1)))); } return result; } set { int idAuthority = value & 0x3F; for (int i = 0; i < 6; i++) { long mask = 0xFF << (8 * i); long maskedValue = (idAuthority & mask); long idValue = maskedValue >> (8 * i); IdentifierAuthority[5 - i] = (byte)idValue; } } } } </pre> <br /> 보는 바와 같이 대부분의 필드 값이 SID 문자열로 직렬화되는데요, 단지 여기서 SID_AND_ATTRIBUTES의 Attributes 값은 누락돼 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_and_attributes'>Attributes</a> Specifies attributes of the SID. This value contains up to 32 one-bit flags. Its meaning depends on the definition and use of the SID. </pre> <br /> 32Bit Flags 형식으로 값을 나타낸다고 하는데, 일단 제 사용자 계정으로 테스트했을 때는 모든 값이 0이었습니다. 문서에 따르면 "아마도" 그룹 성격의 <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_groups'>TOKEN_GROUPS</a> 형식에서 사용되는 듯한데, 만약 그런 경우라면 일반 사용자 계정을 대상으로는 SID 문자열로부터 TOKEN_USER 구조체를 온전히 복원하는 것이 가능합니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2002&boardid=331301885'>첨부 파일은 이 글의 C# 예제 코드와 C++ 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1074
(왼쪽의 숫자를 입력해야 합니다.)