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
정성태

... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12860정성태12/1/20216277오류 유형: 767. SQL Server - 127.0.0.1로 접속하는 경우 "Access is denied"가 발생한다면?
12859정성태12/1/202112448개발 환경 구성: 608. Hyper-V 가상 머신에 Console 모드로 로그인하는 방법
12858정성태11/30/20219706개발 환경 구성: 607. 로컬의 USB 장치를 원격 머신에 제공하는 방법 - usbip-win
12857정성태11/24/20217133개발 환경 구성: 606. WSL Ubuntu 20.04에서 파이썬을 위한 uwsgi 설치 방법
12856정성태11/23/20218965.NET Framework: 1121. C# - 동일한 IP:Port로 바인딩 가능한 서버 소켓 [2]
12855정성태11/13/20216270개발 환경 구성: 605. Azure App Service - Kudu SSH 환경에서 FTP를 이용한 파일 전송
12854정성태11/13/20217893개발 환경 구성: 604. Azure - 윈도우 VM에서 FTP 여는 방법
12853정성태11/10/20216233오류 유형: 766. Azure App Service - JBoss 호스팅 생성 시 "This region has quota of 0 PremiumV3 instances for your subscription. Try selecting different region or SKU."
12851정성태11/1/20217617스크립트: 34. 파이썬 - MySQLdb 기본 예제 코드
12850정성태10/27/20218794오류 유형: 765. 우분투에서 pip install mysqlclient 실행 시 "OSError: mysql_config not found" 오류
12849정성태10/17/20217904스크립트: 33. JavaScript와 C#의 시간 변환 [1]
12848정성태10/17/20218873스크립트: 32. 파이썬 - sqlite3 기본 예제 코드 [1]
12847정성태10/14/20218710스크립트: 31. 파이썬 gunicorn - WORKER TIMEOUT 오류 발생
12846정성태10/7/20218487스크립트: 30. 파이썬 __debug__ 플래그 변수에 따른 코드 실행 제어
12845정성태10/6/20218321.NET Framework: 1120. C# - BufferBlock<T> 사용 예제 [5]파일 다운로드1
12844정성태10/3/20216313오류 유형: 764. MSI 설치 시 "... is accessible and not read-only." 오류 메시지
12843정성태10/3/20216768스크립트: 29. 파이썬 - fork 시 기존 클라이언트 소켓 및 스레드의 동작파일 다운로드1
12842정성태10/1/202125168오류 유형: 763. 파이썬 오류 - AttributeError: type object '...' has no attribute '...'
12841정성태10/1/20218591스크립트: 28. 모든 파이썬 프로세스에 올라오는 특별한 파일 - sitecustomize.py
12840정성태9/30/20218658.NET Framework: 1119. Entity Framework의 Join 사용 시 다중 칼럼에 대한 OR 조건 쿼리파일 다운로드1
12839정성태9/15/20219698.NET Framework: 1118. C# 11 - 제네릭 타입의 특성 적용파일 다운로드1
12838정성태9/13/20219342.NET Framework: 1117. C# - Task에 전달한 Action, Func 유형에 따라 달라지는 async/await 비동기 처리 [2]파일 다운로드1
12837정성태9/11/20218256VC++: 151. Golang - fmt.Errorf, errors.Is, errors.As 설명
12836정성태9/10/20217874Linux: 45. 리눅스 - 실행 중인 다른 프로그램의 출력을 확인하는 방법
12835정성태9/7/20219098.NET Framework: 1116. C# 10 - (15) CallerArgumentExpression 특성 추가 [2]파일 다운로드1
12834정성태9/7/20217534오류 유형: 762. Visual Studio 2019 Build Tools - 'C:\Program' is not recognized as an internal or external command, operable program or batch file.
... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...