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를 구하는데요,
- GetTokenInformation을 호출해 사용자 보안 토큰의 크기를 구하고,
- 그 크기만큼을 메모리에 할당한 다음,
- 다시 GetTokenInformation을 호출해 보안 토큰 정보를 반환
- 그 보안 토큰으로부터 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++ 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]