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)
13318정성태4/11/20233915개발 환경 구성: 673. JetBrains IDE에서 "Squash Commits..." 메뉴가 비활성화된 경우
13317정성태4/11/20234089오류 유형: 855. WSL 2 Ubuntu 20.04 - error: cannot communicate with server: Post http://localhost/v2/snaps/...
13316정성태4/10/20233409오류 유형: 854. docker-compose 시 "json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)" 오류 발생
13315정성태4/10/20233597Windows: 245. Win32 - 시간 만료를 갖는 컨텍스트 메뉴와 윈도우 메시지의 영역별 정의파일 다운로드1
13314정성태4/9/20233671개발 환경 구성: 672. DosBox를 이용한 Turbo C, Windows 3.1 설치
13313정성태4/9/20233760개발 환경 구성: 671. Hyper-V VM에 Turbo C 2.0 설치 [2]
13312정성태4/8/20233751Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)파일 다운로드1
13311정성태4/7/20234231C/C++: 163. Visual Studio 2022 - DirectShow 예제 컴파일(WAV Dest)
13310정성태4/6/20233819C/C++: 162. Visual Studio - /NODEFAULTLIB 옵션 설정 후 수동으로 추가해야 할 library
13309정성태4/5/20233998.NET Framework: 2107. .NET 6+ FileStream의 구조 변화
13308정성태4/4/20233878스크립트: 47. 파이썬의 time.time() 실숫값을 GoLang / C#에서 사용하는 방법
13307정성태4/4/20233676.NET Framework: 2106. C# - .NET Core/5+ 환경의 Windows Forms 응용 프로그램에서 HINSTANCE 구하는 방법
13306정성태4/3/20233509Windows: 243. Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
13305정성태4/1/20233833Windows: 242. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전)파일 다운로드1
13304정성태3/31/20234194VS.NET IDE: 181. Visual Studio - C/C++ 프로젝트에 application manifest 적용하는 방법
13303정성태3/30/20233478Windows: 241. 환경 변수 %PATH%에 DLL을 찾는 규칙
13302정성태3/30/20234113Windows: 240. RDP 환경에서 바뀌는 %TEMP% 디렉터리 경로
13301정성태3/29/20234247Windows: 239. C/C++ - Windows 10 Version 1607부터 지원하는 /DEPENDENTLOADFLAG 옵션파일 다운로드1
13300정성태3/28/20233908Windows: 238. Win32 - Modal UI 창에 올바른 Owner(HWND)를 설정해야 하는 이유
13299정성태3/27/20233694Windows: 237. Win32 - 모든 메시지 루프를 탈출하는 WM_QUIT 메시지
13298정성태3/27/20233639Windows: 236. Win32 - MessageBeep 소리가 안 들린다면?
13297정성태3/26/20234280Windows: 235. Win32 - Code Modal과 UI Modal
13296정성태3/25/20233640Windows: 234. IsDialogMessage와 협업하는 WM_GETDLGCODE Win32 메시지 [1]파일 다운로드1
13295정성태3/24/20233899Windows: 233. Win32 - modeless 대화창을 modal처럼 동작하게 만드는 방법파일 다운로드1
13294정성태3/22/20234080.NET Framework: 2105. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
13293정성태3/22/20234143오류 유형: 853. dumpbin - warning LNK4048: Invalid format file; ignored
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...