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)
13272정성태2/27/20234218오류 유형: 850. SSMS - mdf 파일을 Attach 시킬 때 Operating system error 5: "5(Access is denied.)" 에러
13271정성태2/25/20234150오류 유형: 849. Sql Server Configuration Manager가 시작 메뉴에 없는 경우
13270정성태2/24/20233764.NET Framework: 2098. dotnet build에 /p 옵션을 적용 시 유의점
13269정성태2/23/20234303스크립트: 46. 파이썬 - uvicorn의 콘솔 출력을 UDP로 전송
13268정성태2/22/20234849개발 환경 구성: 667. WSL 2 내부에서 열고 있는 UDP 서버를 호스트 측에서 접속하는 방법
13267정성태2/21/20234775.NET Framework: 2097. C# - 비동기 소켓 사용 시 메모리 해제가 finalizer 단계에서 발생하는 사례파일 다운로드1
13266정성태2/20/20234382오류 유형: 848. .NET Core/5+ - Process terminated. Couldn't find a valid ICU package installed on the system
13265정성태2/18/20234304.NET Framework: 2096. .NET Core/5+ - PublishSingleFile 유형에 대한 runtimeconfig.json 설정
13264정성태2/17/20235803스크립트: 45. 파이썬 - uvicorn 사용자 정의 Logger 작성
13263정성태2/16/20233947개발 환경 구성: 666. 최신 버전의 ilasm.exe/ildasm.exe 사용하는 방법
13262정성태2/15/20235009디버깅 기술: 191. dnSpy를 이용한 (소스 코드가 없는) 닷넷 응용 프로그램 디버깅 방법 [1]
13261정성태2/15/20234303Windows: 224. Visual Studio - 영문 폰트가 Fullwidth Latin Character로 바뀌는 문제
13260정성태2/14/20234089오류 유형: 847. ilasm.exe 컴파일 오류 - error : syntax error at token '-' in ... -inf
13259정성태2/14/20234222.NET Framework: 2095. C# - .NET5부터 도입된 CollectionsMarshal
13258정성태2/13/20234119오류 유형: 846. .NET Framework 4.8 Developer Pack 설치 실패 - 0x81f40001
13257정성태2/13/20234205.NET Framework: 2094. C# - Job에 Process 포함하는 방법 [1]파일 다운로드1
13256정성태2/10/20235057개발 환경 구성: 665. WSL 2의 네트워크 통신 방법 - 두 번째 이야기
13255정성태2/10/20234353오류 유형: 845. gihub - windows2022 이미지에서 .NET Framework 4.5.2 미만의 프로젝트에 대한 빌드 오류
13254정성태2/10/20234265Windows: 223. (WMI 쿼리를 위한) PowerShell 문자열 escape 처리
13253정성태2/9/20235044Windows: 222. C# - 다른 윈도우 프로그램이 실행되었음을 인식하는 방법파일 다운로드1
13252정성태2/9/20233856오류 유형: 844. ssh로 명령어 수행 시 멈춤 현상
13251정성태2/8/20234318스크립트: 44. 파이썬의 3가지 스레드 ID
13250정성태2/8/20236119오류 유형: 843. System.InvalidOperationException - Unable to configure HTTPS endpoint
13249정성태2/7/20234934오류 유형: 842. 리눅스 - You must wait longer to change your password
13248정성태2/7/20234050오류 유형: 841. 리눅스 - [사용자 계정] is not in the sudoers file. This incident will be reported.
13247정성태2/7/20234962VS.NET IDE: 180. Visual Studio - 닷넷 소스 코드 디버깅 중 "Decompile source code"가 동작하는 않는 문제
1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...