Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 5개 있습니다.)

C# - GetTokenInformation으로 사용자 SID(Security identifiers) 구하는 방법

사용자의 SID는 이미 BCL을 이용해 간단하게 구할 수 있습니다.

Console.WriteLine(WindowsIdentity.GetCurrent().User?.Value);

// 출력 결과: S-1-5-21-1510216573-2196513108-161129836-1001
// 2.4.2.4 Well-Known SID Structures
// ; https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/81d92bba-d22b-4a8c-908a-554ab29148ab

하지만 이 글에서는 (그냥 재미 삼아) Win32 API를 통한 방법을 설명할 텐데요,

GetTokenInformation
; https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-gettokeninformation

C++로는 많이 알려져 있으니 이번 글에서는 C#으로 ^^ 구현해 보겠습니다.

using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Principal;

[assembly: SupportedOSPlatform("windows")]

internal class Program
{
    [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);

    private static uint ERROR_INSUFFICIENT_BUFFER = 122;

    static void Main(string[] args)
    {
        string sidText;
        TOKEN_USER? tokenUser = GeTokenUser(out sidText);

        if (tokenUser == null)
        {
            return;
        }

        Console.WriteLine($"SID Found: {sidText}");
    }

    static unsafe TOKEN_USER? GeTokenUser(out string sid)
    {
        sid = "";
        IntPtr hToken = WindowsIdentity.GetCurrent().Token;

        uint dwBufferSize;
        if (!GetTokenInformation(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 = Marshal.AllocHGlobal((int)dwBufferSize);
        if (!GetTokenInformation(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 (ConvertSidToStringSid(tokenUser.Value.User.Sid, out pstr) == true)
            {
                sid = Marshal.PtrToStringAuto(pstr) ?? "";
                Marshal.FreeHGlobal(pstr);
            }

            return tokenUser;
        }
        finally
        {
            Marshal.FreeHGlobal(tokenInformation);
        }
    }
}

[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 fixed byte IdentifierAuthority[6];
    public fixed int SubAuthority[1];
}

internal enum TOKEN_INFORMATION_CLASS
{
    TokenUser = 1,
}

위의 코드를 보면 다음의 순서로 sid를 구하는데요,

  1. GetTokenInformation을 호출해 사용자 보안 토큰의 크기를 구하고,
  2. 그 크기만큼을 메모리에 할당한 다음,
  3. 다시 GetTokenInformation을 호출해 보안 토큰 정보를 반환
  4. 그 보안 토큰으로부터 SID 문자열 얻기

여기서 재미있는 것은 TOKEN_USER 타입의 구조입니다.

// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_user
typedef struct _TOKEN_USER {
  SID_AND_ATTRIBUTES User;
} TOKEN_USER, *PTOKEN_USER;

// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_and_attributes
typedef struct _SID_AND_ATTRIBUTES {
    PSID Sid;
    DWORD Attributes;
} SID_AND_ATTRIBUTES, * PSID_AND_ATTRIBUTES;

// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid
typedef struct _SID {
   BYTE  Revision;
   BYTE  SubAuthorityCount;
   SID_IDENTIFIER_AUTHORITY IdentifierAuthority;
   DWORD SubAuthority[1]; // SubAuthorityCount 수만큼 배열이 동적으로 결정됨
} SID, *PISID;

// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_identifier_authority
typedef struct _SID_IDENTIFIER_AUTHORITY {
  BYTE Value[6];
} SID_IDENTIFIER_AUTHORITY, *PSID_IDENTIFIER_AUTHORITY;

보는 바와 같이 TOKEN_USER는 내부에 SID를 향한 포인터를 담고 있으며 다시 그 _SID의 마지막 멤버인 SubAuthority는 그 크기가 정해지지 않은, 즉, 사용자에 따라 동적으로 달라지는 배열을 포함하고 있습니다.

바로 이러한 동적 크기의 성격 때문에 GetTokenInformation API는 크기를 먼저 얻게 한 다음, 사용자 측에서 그 크기만큼 메모리를 할당하게 만들고, 그것을 다시 GetTokenInformation에 전달해 TOKEN_USER 구조체의 모든 내용을 반환받는 식으로 동작하는 것입니다.

그리고 여기서 또 한 가지 재미있는 점은, SID_AND_ATTRIBUTES 구조체의 멤버인 Sid가 포인터이긴 하지만, 그리 멀리 있지 않은, 즉 GetTokenInformation이 반환한 크기 이내에 있는 위치를 가리킨다는 점입니다. 다시 말해, GetTokenInformation이 44를 반환했다면, 이 구조체는 다음과 같은 유형으로 정의가 됩니다.

// 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]

보는 바와 같이 포인터가 구조체 내부의 영역을 가리키는 식입니다. 따라서 만약 "Marshal.AllocHGlobal((int)dwBufferSize);"로 할당한 메모리의 주소가 0x1000이라면, Sid 포인터의 값은 0x1010입니다. (TOKEN_USER와 SID_AND_ATTRIBUTES 구조체가 향후 달라질 가능성은 거의 없긴 해도 하드 코딩하는 것은 끊임없는 주의를 요합니다. ^^)




어쨌든, 이번 글의 주제에 따라 SID는 ConvertSidToStringSid API를 호출하는 시점에 안전하게 구할 수 있었지만, 혹시 기왕에 구한 tokenUser 인스턴스를 재사용하는 것이 가능할까요?

바로 전에 설명한 TOKEN_USER 인스턴스의 메모리 구조를 염두에 두고, 아래의 코드를 자세하게 다시 살펴보겠습니다. ^^

IntPtr tokenInformation = Marshal.AllocHGlobal((int)dwBufferSize);
GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenUser, tokenInformation, dwBufferSize, out dwBufferSize);

try
{
    TOKEN_USER? tokenUser = (TOKEN_USER?)Marshal.PtrToStructure(tokenInformation, typeof(TOKEN_USER));
    return tokenUser;
}
finally
{
    Marshal.FreeHGlobal(tokenInformation);
}

얼핏 보면 Marshal.PtrToStructure를 통해 값 복사를 했으므로 이후 독자적으로 tokenUser 인스턴스를 사용해도 될 것 같은데요, 하지만, 이렇게 반환한 tokenUser 인스턴스는 finally에 의해 해제되는 tokenInformation으로 인해 향후 사용 시 오류가 발생하게 됩니다. 즉, 반환한 tokenUser 인스턴스는 더 이상 유효하지 않은 값이 된 것입니다.

왜냐하면, TOKEN_USER의 값 자체는 복사되었지만, PSID 포인터가 가리키는 영역이 Marshal.FreeHGlobal에 의해 해제가 되었으므로 더 이상 유효하지 않는 포인터가 됐기 때문입니다. 이로 인해, 아쉽지만 TOKEN_USER 인스턴스가 필요하다면 저런 식으로 메서드 한 개에 추상화시키는 것은 좀 무리가 있고, 차라리 그냥 클래스 수준으로 추상화하는 것이 더 좋습니다.

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

    public void Dispose()
    {
        if (_tokenInformation != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(_tokenInformation);
            _tokenInformation = IntPtr.Zero;
        }
    }
}

// ...[생략]...

그래서 이런 식으로 사용하면 안전하게 TokenUser를 보호할 수 있습니다.

using System.Runtime.Versioning;
using System.Security.Principal;

[assembly: SupportedOSPlatform("windows")]

internal class Program
{
    static void Main(string[] args)
    {
        IntPtr hToken = WindowsIdentity.GetCurrent().Token;
        using (Win32UserToken tokenUser = new Win32UserToken(hToken))
        {
            Console.WriteLine(tokenUser.Sid); // S-1-5-21-1510216573-2196513108-161129836-1001

            // 이 범위 내에서 tokenUser._tokenUser 인스턴스를 안전하게 사용
        }
    }
}




참고로, 저렇게 출력한 Sid 문자열(S-1-5-21-1510216573-2196513108-161129836-1001)은 TOKEN_USER로부터 그대로 구하는 것이 가능합니다.

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]

따라서 Win32UserToken 타입에 다음과 같은 방법으로 SID 문자열을 구할 수도 있습니다.

public override string ToString()
{
    if (this._tokenUser == null)
    {
        return "";
    }

    TOKEN_USER tokenUser = this._tokenUser.Value;
    return $"S-{tokenUser.Sid.Revision}-{tokenUser.Sid.IdentifierAuthorityAsValue}-{this.SubAuthority}";
}

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

보는 바와 같이 대부분의 필드 값이 SID 문자열로 직렬화되는데요, 단지 여기서 SID_AND_ATTRIBUTES의 Attributes 값은 누락돼 있습니다.

Attributes

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.

32Bit Flags 형식으로 값을 나타낸다고 하는데, 일단 제 사용자 계정으로 테스트했을 때는 모든 값이 0이었습니다. 문서에 따르면 "아마도" 그룹 성격의 TOKEN_GROUPS 형식에서 사용되는 듯한데, 만약 그런 경우라면 일반 사용자 계정을 대상으로는 SID 문자열로부터 TOKEN_USER 구조체를 온전히 복원하는 것이 가능합니다.

(첨부 파일은 이 글의 C# 예제 코드와 C++ 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/15/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-01-09 10시37분
What are these SIDs of the form S-1-15-2-xxx?
; https://devblogs.microsoft.com/oldnewthing/20220502-00/?p=106550

What are these SIDs of the form S-1-15-3-xxx?
; https://devblogs.microsoft.com/oldnewthing/20220503-00/?p=106557

Management of SIDs in Active Directory
; https://social.technet.microsoft.com/wiki/contents/articles/20590.management-of-sids-in-active-directory.aspx

Dubious security vulnerability: Granting access to SIDs that don’t exist yet
; https://devblogs.microsoft.com/oldnewthing/20230106-00/?p=107680

SIDs are really just another a fancy way of creating unique IDs in a decentralized way
; https://devblogs.microsoft.com/oldnewthing/20230613-00/?p=108335
정성태
2023-05-18 11시12분
How do I free the pointers returned by functions like Get­Token­Information?
; https://devblogs.microsoft.com/oldnewthing/20230517-00/?p=108207

GetTokenInformation과 같은 식으로 첫 번째 호출에서 크기를 구하고, 할당한 다음 두 번째 호출에서 실제 정보를 구하는 식으로 동작하는 API들이 있는데요, 재미있는 건 그런 API가 반환한 구조체 내에는 포인터도 있다는 점입니다. 그렇다면 그 포인터도 명시적으로 해제해야 할까요?

그에 대한 답이 위의 링크에 있습니다.

(답은, 부가적인 해제 과정을 필요하진 않고, 우리가 할당해 넘긴 그 메모리만 삭제하면 됩니다.)
정성태
2024-02-15 08시26분
Windows API 중에는 본문에서 다룬 GetTokenInformation처럼 1) 버퍼 크기를 먼저 구하고, 2) 해당 버퍼 크기만큼 사용자가 할당 후, 3) 다시 그 버퍼로 API를 호출하는 유형들이 있습니다.

개인적으로, 저 1~3번의 연속 작업이 원자적 단위가 아니기 때문에 3번 단계에서 달라진 상황이 발생할 수 있고, 그렇다면 ERROR_INSUFFICIENT_BUFFER 체크를 while 루프로 처리해 반복해야 하나... 라는 생각을 한 적이 있습니다.

이에 대한 걱정을 아래의 글에서 해결해 주고 있습니다. ^^

Functions that return the size of a required buffer generally return upper bounds, not tight bounds
; https://devblogs.microsoft.com/oldnewthing/20240214-00/?p=109400
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13599정성태4/17/2024204닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/2024239닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드1
13597정성태4/15/2024306닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/2024542닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/2024636닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/2024824닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20241001닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241197C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241161닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20241071Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20241136닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20241190닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신파일 다운로드1
13587정성태3/27/20241141오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/20241279Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/20241092Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/20241045개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
13583정성태3/25/20241145Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20241220Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/20241564개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/20241132닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/20241493오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/20241622닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/20241856닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
13576정성태3/8/20241540닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/20241666닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...