mirror of
https://github.com/praetorian-inc/wasmforge
synced 2026-06-22 22:52:31 +00:00
722 lines
37 KiB
C#
722 lines
37 KiB
C#
// WfToken.cs — Token groups and privileges via direct Win32 wf_call pattern.
|
|
// Replaces the deleted token_groups_get / token_privs_get Go host bridges.
|
|
// Follows the WfNetapi.cs canonical pattern: mod_load → mod_resolve → mod_invoke
|
|
// + RtlMoveMemory for host→WASM buffer copies.
|
|
//
|
|
// Struct layouts (x64):
|
|
// TOKEN_GROUPS:
|
|
// DWORD GroupCount (offset 0)
|
|
// [4-byte pad for 8-byte array alignment]
|
|
// SID_AND_ATTRIBUTES[] each 16 bytes: ptr Sid (8) + DWORD Attributes (4) + 4 pad
|
|
//
|
|
// TOKEN_PRIVILEGES:
|
|
// DWORD PrivilegeCount (offset 0)
|
|
// LUID_AND_ATTRIBUTES[] each 12 bytes: DWORD LowPart + DWORD HighPart + DWORD Attributes
|
|
// (TOKEN_PRIVILEGES is packed — no padding between count and array)
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace WasmForge.Helpers
|
|
{
|
|
public static unsafe class WfToken
|
|
{
|
|
// ── Bridge primitives (same declarations as WfNetapi.cs) ────────────────
|
|
[DllImport("env", EntryPoint = "mod_load")]
|
|
private static extern uint mod_load(uint namePtr);
|
|
|
|
[DllImport("env", EntryPoint = "mod_resolve")]
|
|
private static extern uint mod_resolve(uint libHandle, uint namePtr);
|
|
|
|
[DllImport("env", EntryPoint = "mod_invoke")]
|
|
private static extern ulong mod_invoke(
|
|
ulong procHandle, uint nargs,
|
|
ulong a0, ulong a1, ulong a2, ulong a3,
|
|
ulong a4, ulong a5, ulong a6, ulong a7,
|
|
ulong a8, ulong a9, ulong a10, ulong a11,
|
|
ulong a12, ulong a13, ulong a14,
|
|
ulong ret1Ptr, ulong errPtr);
|
|
|
|
// ── Cached proc handles ─────────────────────────────────────────────────
|
|
private static uint _kernel32;
|
|
private static uint _advapi32;
|
|
private static uint _ntdll;
|
|
private static uint _hGetCurrentProcess;
|
|
private static uint _hOpenProcessToken;
|
|
private static uint _hGetTokenInformation;
|
|
private static uint _hConvertSidToStringSidW;
|
|
private static uint _hLookupAccountSidW;
|
|
private static uint _hLookupPrivilegeNameW;
|
|
private static uint _hCloseHandle;
|
|
private static uint _hLocalFree;
|
|
private static uint _hVirtualAlloc;
|
|
private static uint _hVirtualFree;
|
|
private static uint _hRtlMoveMemory;
|
|
|
|
// ── Resolve + Invoke helpers (identical to WfNetapi.cs pattern) ─────────
|
|
private static uint Resolve(string dll, ref uint cachedLib, string fn, ref uint cachedProc)
|
|
{
|
|
if (cachedProc != 0) return cachedProc;
|
|
if (cachedLib == 0)
|
|
{
|
|
byte[] db = Encoding.ASCII.GetBytes(dll + "\0");
|
|
fixed (byte* dp = db) cachedLib = mod_load((uint)(IntPtr)dp);
|
|
if (cachedLib == 0) return 0;
|
|
}
|
|
byte[] fb = Encoding.ASCII.GetBytes(fn + "\0");
|
|
fixed (byte* fp = fb) cachedProc = mod_resolve(cachedLib, (uint)(IntPtr)fp);
|
|
return cachedProc;
|
|
}
|
|
|
|
private static ulong Invoke(uint proc, uint nargs,
|
|
ulong a0 = 0, ulong a1 = 0, ulong a2 = 0, ulong a3 = 0,
|
|
ulong a4 = 0, ulong a5 = 0, ulong a6 = 0, ulong a7 = 0,
|
|
ulong a8 = 0, ulong a9 = 0)
|
|
{
|
|
ulong ret1 = 0, err = 0;
|
|
return mod_invoke((ulong)proc, nargs,
|
|
a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, 0, 0, 0, 0, 0,
|
|
(ulong)(uint)(IntPtr)(&ret1),
|
|
(ulong)(uint)(IntPtr)(&err));
|
|
}
|
|
|
|
// Copy len bytes from a host address into a WASM-side buffer.
|
|
private static bool CopyHostToWasm(ulong hostAddr, uint wasmPtr, uint len)
|
|
{
|
|
if (hostAddr == 0 || wasmPtr == 0 || len == 0) return false;
|
|
uint pCopy = Resolve("ntdll.dll", ref _ntdll, "RtlMoveMemory", ref _hRtlMoveMemory);
|
|
if (pCopy == 0) return false;
|
|
Invoke(pCopy, 3, (ulong)wasmPtr, hostAddr, (ulong)len);
|
|
return true;
|
|
}
|
|
|
|
// Read a little-endian 8-byte value from a WASM buffer at byte offset off.
|
|
private static ulong Read8(byte* p, int off) =>
|
|
((ulong)p[off+0]) | ((ulong)p[off+1] << 8) |
|
|
((ulong)p[off+2] << 16) | ((ulong)p[off+3] << 24) |
|
|
((ulong)p[off+4] << 32) | ((ulong)p[off+5] << 40) |
|
|
((ulong)p[off+6] << 48) | ((ulong)p[off+7] << 56);
|
|
|
|
// Read a little-endian 4-byte value from a WASM buffer at byte offset off.
|
|
private static uint Read4(byte* p, int off) =>
|
|
(uint)(p[off+0] | (p[off+1] << 8) | (p[off+2] << 16) | (p[off+3] << 24));
|
|
|
|
// Read a NUL-terminated UTF-16LE wide string from a host pointer into a managed string.
|
|
private static string ReadWStringFromHost(ulong hostAddr, int maxChars)
|
|
{
|
|
if (hostAddr == 0 || maxChars <= 0) return "";
|
|
byte[] buf = new byte[maxChars * 2];
|
|
fixed (byte* bp = buf)
|
|
{
|
|
if (!CopyHostToWasm(hostAddr, (uint)(IntPtr)bp, (uint)buf.Length)) return "";
|
|
}
|
|
int charLen = 0;
|
|
for (int i = 0; i < maxChars; i++)
|
|
{
|
|
if (buf[2*i] == 0 && buf[2*i+1] == 0) break;
|
|
charLen++;
|
|
}
|
|
if (charLen == 0) return "";
|
|
char[] chars = new char[charLen];
|
|
for (int i = 0; i < charLen; i++)
|
|
chars[i] = (char)(buf[2*i] | (buf[2*i+1] << 8));
|
|
return new string(chars);
|
|
}
|
|
|
|
// Parse a SID structure from raw bytes into SDDL string form (S-1-X-Y-...).
|
|
// BYTE Revision (offset 0)
|
|
// BYTE SubAuthorityCount (offset 1) — typically 0..15
|
|
// BYTE IdentifierAuthority[6] (offset 2, big-endian 48-bit)
|
|
// DWORD SubAuthority[SubAuthorityCount] (offset 8, little-endian)
|
|
// Returns empty on malformed input.
|
|
private static string ParseSidBytesToString(byte[] buf, int sidOff, int bufLen)
|
|
{
|
|
if (buf == null || sidOff < 0 || sidOff + 8 > bufLen) return "";
|
|
byte rev = buf[sidOff + 0];
|
|
byte subCnt = buf[sidOff + 1];
|
|
if (rev != 1 || subCnt > 15) return "";
|
|
int needed = 8 + subCnt * 4;
|
|
if (sidOff + needed > bufLen) return "";
|
|
|
|
// IdentifierAuthority is 48-bit big-endian.
|
|
ulong idAuth = 0;
|
|
for (int j = 0; j < 6; j++)
|
|
idAuth = (idAuth << 8) | buf[sidOff + 2 + j];
|
|
|
|
var sb = new System.Text.StringBuilder("S-1-");
|
|
sb.Append(idAuth.ToString());
|
|
for (int k = 0; k < subCnt; k++)
|
|
{
|
|
int subOff = sidOff + 8 + k * 4;
|
|
uint sub = (uint)(buf[subOff + 0]
|
|
| (buf[subOff + 1] << 8)
|
|
| (buf[subOff + 2] << 16)
|
|
| (buf[subOff + 3] << 24));
|
|
sb.Append('-').Append(sub.ToString());
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
// Allocate a zeroed block of host memory (MEM_COMMIT|MEM_RESERVE = 0x3000, PAGE_READWRITE = 4).
|
|
// Returns the host address, or 0 on failure.
|
|
// Note: callers that pass the returned address back through Invoke() as an arg
|
|
// can hit the wasmforge WASM pointer-translation collision when the OS allocates
|
|
// a low 32-bit address. The TokenGroups path was migrated to WfHost.HostAlloc
|
|
// (handle-based, guaranteed high address) to avoid this. TokenPrivileges still
|
|
// uses this path and works because GetTokenInformation tolerates the collision
|
|
// for that struct shape (no nested pointer chains to chase post-read).
|
|
private static ulong VirtualAllocHost(uint size)
|
|
{
|
|
uint pAlloc = Resolve("kernel32.dll", ref _kernel32, "VirtualAlloc", ref _hVirtualAlloc);
|
|
if (pAlloc == 0) return 0;
|
|
return Invoke(pAlloc, 4, 0u, (ulong)size, 0x3000u, 4u);
|
|
}
|
|
|
|
// Release host memory allocated by VirtualAllocHost (MEM_RELEASE = 0x8000).
|
|
private static void VirtualFreeHost(ulong addr)
|
|
{
|
|
if (addr == 0) return;
|
|
uint pFree = Resolve("kernel32.dll", ref _kernel32, "VirtualFree", ref _hVirtualFree);
|
|
if (pFree != 0) Invoke(pFree, 3, addr, 0u, 0x8000u);
|
|
}
|
|
|
|
// ── Public API ───────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Returns the SID string and account name for each group in the current process token.
|
|
/// Equivalent to WindowsIdentity.GetCurrent().Groups.
|
|
/// </summary>
|
|
public static List<(string Sid, string Name)> GetGroups()
|
|
{
|
|
var result = new List<(string, string)>();
|
|
try
|
|
{
|
|
// Resolve all needed procs.
|
|
uint pGetCurrentProcess = Resolve("kernel32.dll", ref _kernel32, "GetCurrentProcess", ref _hGetCurrentProcess);
|
|
uint pOpenProcessToken = Resolve("advapi32.dll", ref _advapi32, "OpenProcessToken", ref _hOpenProcessToken);
|
|
uint pGetTokenInfo = Resolve("advapi32.dll", ref _advapi32, "GetTokenInformation", ref _hGetTokenInformation);
|
|
uint pConvSid = Resolve("advapi32.dll", ref _advapi32, "ConvertSidToStringSidW", ref _hConvertSidToStringSidW);
|
|
uint pLookupAcct = Resolve("advapi32.dll", ref _advapi32, "LookupAccountSidW", ref _hLookupAccountSidW);
|
|
uint pCloseHandle = Resolve("kernel32.dll", ref _kernel32, "CloseHandle", ref _hCloseHandle);
|
|
uint pLocalFree = Resolve("kernel32.dll", ref _kernel32, "LocalFree", ref _hLocalFree);
|
|
if (pGetCurrentProcess == 0 || pOpenProcessToken == 0 || pGetTokenInfo == 0) return result;
|
|
|
|
// Get pseudo-handle for current process (returns -1 = 0xFFFFFFFF as uint).
|
|
ulong hProcess = Invoke(pGetCurrentProcess, 0);
|
|
|
|
// OpenProcessToken(hProcess, TOKEN_QUERY=8, &hToken)
|
|
ulong hToken = 0;
|
|
ulong tokenOk = Invoke(pOpenProcessToken, 3,
|
|
hProcess,
|
|
8u, // TOKEN_QUERY
|
|
(ulong)(uint)(IntPtr)(&hToken));
|
|
if (tokenOk == 0 || hToken == 0) return result;
|
|
|
|
try
|
|
{
|
|
// First call: get required buffer size.
|
|
uint returnedSize = 0;
|
|
// GetTokenInformation(hToken, TokenGroups=2, NULL, 0, &returnedSize)
|
|
Invoke(pGetTokenInfo, 5,
|
|
hToken,
|
|
2u, // TokenGroups
|
|
0u, 0u,
|
|
(ulong)(uint)(IntPtr)(&returnedSize));
|
|
|
|
if (returnedSize == 0) return result;
|
|
if (returnedSize > 65536) returnedSize = 65536;
|
|
|
|
// Allocate host buffer via WfHost.HostAlloc — returns a handle
|
|
// backed by a host-side allocation whose real address is
|
|
// guaranteed > wasmMemSize so wf_call's WASM pointer translation
|
|
// skips it. This was the root cause of the previous failure:
|
|
// VirtualAllocHost returned a low 32-bit address that COLLIDED
|
|
// with wf_call's translation threshold, causing GetTokenInformation
|
|
// to write to WASM memory instead of the host buffer.
|
|
int hostHandle = 0;
|
|
ulong hostBuf = 0;
|
|
try
|
|
{
|
|
hostHandle = WfHost.HostAlloc((int)returnedSize);
|
|
hostBuf = WfHost.GetHostAddress(hostHandle);
|
|
}
|
|
catch { return result; }
|
|
if (hostHandle == 0 || hostBuf == 0) {
|
|
if (hostHandle != 0) WfHost.HostFree(hostHandle);
|
|
return result;
|
|
}
|
|
try
|
|
{
|
|
uint actualSize = 0;
|
|
ulong ok = Invoke(pGetTokenInfo, 5,
|
|
hToken,
|
|
2u,
|
|
hostBuf,
|
|
(ulong)returnedSize,
|
|
(ulong)(uint)(IntPtr)(&actualSize));
|
|
if (ok == 0) return result;
|
|
|
|
// Read GroupCount (DWORD at offset 0).
|
|
uint groupCount = 0;
|
|
{
|
|
byte[] tmp = new byte[4];
|
|
fixed (byte* tp = tmp)
|
|
{
|
|
CopyHostToWasm(hostBuf, (uint)(IntPtr)tp, 4);
|
|
groupCount = (uint)(tmp[0] | (tmp[1] << 8) | (tmp[2] << 16) | (tmp[3] << 24));
|
|
}
|
|
}
|
|
if (groupCount == 0 || groupCount > 4096) return result;
|
|
|
|
// Canonical x64 TOKEN_GROUPS layout (verified via on-host buffer dump):
|
|
// [000]: DWORD GroupCount (4 bytes)
|
|
// [004]: padding (4 bytes — alignment to 8 for next PSID)
|
|
// [008]: SID_AND_ATTRIBUTES[0]
|
|
// PSID Sid (8 bytes, absolute host pointer into same buffer)
|
|
// DWORD Attributes (4 bytes)
|
|
// padding (4 bytes to align next entry)
|
|
// [018]: SID_AND_ATTRIBUTES[1]
|
|
// ...
|
|
// [0D8]: SID structure for Group[0] (Revision + SubAuthCount + ...)
|
|
//
|
|
// Earlier failures observed an 8-byte stride with truncated 4-byte PSIDs
|
|
// — that was a symptom of allocating the buffer at a low 32-bit address
|
|
// which collided with wf_call's WASM pointer translation, causing the
|
|
// OS to write the data into WASM memory rather than the host buffer.
|
|
// We were then reading garbage/stale memory from the actual host buffer.
|
|
// The WfHost.HostAlloc path returns a high-address buffer that wf_call
|
|
// skips, so the OS writes the real canonical x64 layout.
|
|
const int SID_AND_ATTR_SIZE = 16;
|
|
uint arrayBytes = groupCount * SID_AND_ATTR_SIZE;
|
|
byte[] entryBuf = new byte[arrayBytes];
|
|
fixed (byte* ep = entryBuf)
|
|
{
|
|
if (!CopyHostToWasm(hostBuf + 8, (uint)(IntPtr)ep, arrayBytes))
|
|
return result;
|
|
|
|
// Read the whole buffer once via HostRead — bypasses the
|
|
// CopyHostToWasm path entirely (handle-based, no WASM ptr
|
|
// translation issues).
|
|
byte[] fullBuf = WfHost.HostRead(hostHandle, 0, (int)actualSize);
|
|
|
|
for (uint i = 0; i < groupCount; i++)
|
|
{
|
|
int off = (int)(i * SID_AND_ATTR_SIZE);
|
|
ulong psid = Read8(ep, off);
|
|
if (psid < hostBuf) continue;
|
|
ulong sidOffsetL = psid - hostBuf;
|
|
if (sidOffsetL == 0 || sidOffsetL + 8 > actualSize) continue;
|
|
int sidOffset = (int)sidOffsetL;
|
|
string sidStr = ParseSidBytesToString(fullBuf, (int)sidOffset, (int)actualSize);
|
|
if (!string.IsNullOrEmpty(sidStr))
|
|
{
|
|
// Skip WfSec.SidToAccountName here: the underlying
|
|
// ConvertStringSidToSidW P/Invoke gets trim-stripped
|
|
// when reached through this call chain, causing a
|
|
// WASM unreachable trap (not a catchable exception).
|
|
// Name resolution happens lazily downstream via the
|
|
// group-name cache + WindowsIdentity.GetCurrent path
|
|
// already wired by the patcher. Empty name here matches
|
|
// native Seatbelt output for SIDs that don't resolve.
|
|
result.Add((sidStr, ""));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
WfHost.HostFree(hostHandle);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (pCloseHandle != 0 && hToken != 0)
|
|
Invoke(pCloseHandle, 1, hToken);
|
|
}
|
|
}
|
|
catch { /* return whatever we have */ }
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drop-in replacement enumerable for WindowsIdentity.GetCurrent().Groups.
|
|
/// Yields SecurityIdentifier objects so existing cast patterns still work.
|
|
/// Per-SID name resolution is performed eagerly via WfSec.SidToAccountName.
|
|
/// Companion: GetGroupNameForSid() returns the cached name.
|
|
/// </summary>
|
|
private static readonly System.Collections.Generic.Dictionary<string, string> _groupNameCache
|
|
= new System.Collections.Generic.Dictionary<string, string>(System.StringComparer.Ordinal);
|
|
|
|
// GetGroupsAsSids returns SDDL SID strings directly (NOT SecurityIdentifier
|
|
// objects). NativeAOT-WASI throws PlatformNotSupportedException for
|
|
// `new SecurityIdentifier(sddl)` — "Windows Principal functionality is not
|
|
// supported on this platform" — so we can't construct one.
|
|
// The patcher rewrites Seatbelt's `(SecurityIdentifier)group` cast to a
|
|
// no-op so the existing $"{...}" format string just embeds the SDDL.
|
|
public static System.Collections.Generic.IEnumerable<string> GetGroupsAsSids()
|
|
{
|
|
var groups = GetGroups();
|
|
var refs = new System.Collections.Generic.List<string>();
|
|
foreach (var g in groups)
|
|
{
|
|
if (string.IsNullOrEmpty(g.Sid)) continue;
|
|
_groupNameCache[g.Sid] = g.Name ?? string.Empty;
|
|
refs.Add(g.Sid);
|
|
}
|
|
return refs;
|
|
}
|
|
|
|
public static string GetGroupNameForSid(string sddlSid)
|
|
{
|
|
if (string.IsNullOrEmpty(sddlSid)) return string.Empty;
|
|
string name;
|
|
if (_groupNameCache.TryGetValue(sddlSid, out name) && !string.IsNullOrEmpty(name))
|
|
return name;
|
|
return ResolveWellKnownSid(sddlSid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves well-known SIDs to their canonical NTAccount-form names.
|
|
/// Used by LsaWrapper.ResolveAccountName and WfToken.GetGroupNameForSid
|
|
/// as a fallback when sid.Translate(typeof(NTAccount)) throws PNS on
|
|
/// NativeAOT-WASI. Covers Seatbelt's UserRightAssignments output set
|
|
/// (Local System, Administrators, Authenticated Users, etc.) so
|
|
/// "NT AUTHORITY\\SYSTEM"-style names appear instead of raw SDDL.
|
|
/// Returns empty string for unknown SIDs — caller falls back to SDDL.
|
|
/// </summary>
|
|
public static string ResolveWellKnownSid(string sddlSid)
|
|
{
|
|
if (string.IsNullOrEmpty(sddlSid)) return string.Empty;
|
|
switch (sddlSid)
|
|
{
|
|
// Well-known universal SIDs
|
|
case "S-1-0-0": return "NULL SID";
|
|
case "S-1-1-0": return "Everyone";
|
|
case "S-1-2-0": return "LOCAL";
|
|
case "S-1-2-1": return "CONSOLE LOGON";
|
|
case "S-1-3-0": return "CREATOR OWNER";
|
|
case "S-1-3-1": return "CREATOR GROUP";
|
|
case "S-1-3-2": return "CREATOR OWNER SERVER";
|
|
case "S-1-3-3": return "CREATOR GROUP SERVER";
|
|
case "S-1-3-4": return "OWNER RIGHTS";
|
|
// NT Authority (S-1-5-*)
|
|
case "S-1-5-1": return "NT AUTHORITY\\DIALUP";
|
|
case "S-1-5-2": return "NT AUTHORITY\\NETWORK";
|
|
case "S-1-5-3": return "NT AUTHORITY\\BATCH";
|
|
case "S-1-5-4": return "NT AUTHORITY\\INTERACTIVE";
|
|
case "S-1-5-6": return "NT AUTHORITY\\SERVICE";
|
|
case "S-1-5-7": return "NT AUTHORITY\\ANONYMOUS LOGON";
|
|
case "S-1-5-8": return "NT AUTHORITY\\PROXY";
|
|
case "S-1-5-9": return "NT AUTHORITY\\ENTERPRISE DOMAIN CONTROLLERS";
|
|
case "S-1-5-10": return "NT AUTHORITY\\SELF";
|
|
case "S-1-5-11": return "NT AUTHORITY\\Authenticated Users";
|
|
case "S-1-5-12": return "NT AUTHORITY\\RESTRICTED";
|
|
case "S-1-5-13": return "NT AUTHORITY\\TERMINAL SERVER USER";
|
|
case "S-1-5-14": return "NT AUTHORITY\\REMOTE INTERACTIVE LOGON";
|
|
case "S-1-5-15": return "NT AUTHORITY\\This Organization";
|
|
case "S-1-5-17": return "NT AUTHORITY\\IUSR";
|
|
case "S-1-5-18": return "NT AUTHORITY\\SYSTEM";
|
|
case "S-1-5-19": return "NT AUTHORITY\\LOCAL SERVICE";
|
|
case "S-1-5-20": return "NT AUTHORITY\\NETWORK SERVICE";
|
|
case "S-1-5-33": return "NT AUTHORITY\\WRITE RESTRICTED";
|
|
case "S-1-5-64-10": return "NT AUTHORITY\\NTLM Authentication";
|
|
case "S-1-5-64-14": return "NT AUTHORITY\\SChannel Authentication";
|
|
case "S-1-5-64-21": return "NT AUTHORITY\\Digest Authentication";
|
|
case "S-1-5-113": return "NT AUTHORITY\\Local account";
|
|
case "S-1-5-114": return "NT AUTHORITY\\Local account and member of Administrators group";
|
|
// BUILTIN aliases (S-1-5-32-*)
|
|
case "S-1-5-32-544": return "BUILTIN\\Administrators";
|
|
case "S-1-5-32-545": return "BUILTIN\\Users";
|
|
case "S-1-5-32-546": return "BUILTIN\\Guests";
|
|
case "S-1-5-32-547": return "BUILTIN\\Power Users";
|
|
case "S-1-5-32-548": return "BUILTIN\\Account Operators";
|
|
case "S-1-5-32-549": return "BUILTIN\\Server Operators";
|
|
case "S-1-5-32-550": return "BUILTIN\\Print Operators";
|
|
case "S-1-5-32-551": return "BUILTIN\\Backup Operators";
|
|
case "S-1-5-32-552": return "BUILTIN\\Replicator";
|
|
case "S-1-5-32-554": return "BUILTIN\\Pre-Windows 2000 Compatible Access";
|
|
case "S-1-5-32-555": return "BUILTIN\\Remote Desktop Users";
|
|
case "S-1-5-32-556": return "BUILTIN\\Network Configuration Operators";
|
|
case "S-1-5-32-558": return "BUILTIN\\Performance Monitor Users";
|
|
case "S-1-5-32-559": return "BUILTIN\\Performance Log Users";
|
|
case "S-1-5-32-560": return "BUILTIN\\Windows Authorization Access Group";
|
|
case "S-1-5-32-561": return "BUILTIN\\Terminal Server License Servers";
|
|
case "S-1-5-32-562": return "BUILTIN\\Distributed COM Users";
|
|
case "S-1-5-32-568": return "BUILTIN\\IIS_IUSRS";
|
|
case "S-1-5-32-569": return "BUILTIN\\Cryptographic Operators";
|
|
case "S-1-5-32-573": return "BUILTIN\\Event Log Readers";
|
|
case "S-1-5-32-574": return "BUILTIN\\Certificate Service DCOM Access";
|
|
case "S-1-5-32-575": return "BUILTIN\\RDS Remote Access Servers";
|
|
case "S-1-5-32-576": return "BUILTIN\\RDS Endpoint Servers";
|
|
case "S-1-5-32-577": return "BUILTIN\\RDS Management Servers";
|
|
case "S-1-5-32-578": return "BUILTIN\\Hyper-V Administrators";
|
|
case "S-1-5-32-579": return "BUILTIN\\Access Control Assistance Operators";
|
|
case "S-1-5-32-580": return "BUILTIN\\Remote Management Users";
|
|
// Mandatory integrity labels
|
|
case "S-1-16-0": return "Mandatory Label\\Untrusted Mandatory Level";
|
|
case "S-1-16-4096": return "Mandatory Label\\Low Mandatory Level";
|
|
case "S-1-16-8192": return "Mandatory Label\\Medium Mandatory Level";
|
|
case "S-1-16-8448": return "Mandatory Label\\Medium Plus Mandatory Level";
|
|
case "S-1-16-12288": return "Mandatory Label\\High Mandatory Level";
|
|
case "S-1-16-16384": return "Mandatory Label\\System Mandatory Level";
|
|
case "S-1-16-20480": return "Mandatory Label\\Protected Process Mandatory Level";
|
|
}
|
|
// Logon session SIDs (S-1-5-5-X-Y): canonical form is "NT AUTHORITY\\LogonSessionId_X_Y"
|
|
if (sddlSid.StartsWith("S-1-5-5-", System.StringComparison.Ordinal))
|
|
{
|
|
var parts = sddlSid.Substring("S-1-5-5-".Length).Split('-');
|
|
if (parts.Length == 2)
|
|
return "NT AUTHORITY\\LogonSessionId_" + parts[0] + "_" + parts[1];
|
|
}
|
|
return string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the privilege name and attributes for each privilege in the current process token.
|
|
/// Equivalent to WindowsIdentity.GetCurrent().Privileges.
|
|
/// </summary>
|
|
public static List<(string Name, uint Attributes)> GetPrivileges()
|
|
{
|
|
var result = new List<(string, uint)>();
|
|
try
|
|
{
|
|
uint pGetCurrentProcess = Resolve("kernel32.dll", ref _kernel32, "GetCurrentProcess", ref _hGetCurrentProcess);
|
|
uint pOpenProcessToken = Resolve("advapi32.dll", ref _advapi32, "OpenProcessToken", ref _hOpenProcessToken);
|
|
uint pGetTokenInfo = Resolve("advapi32.dll", ref _advapi32, "GetTokenInformation", ref _hGetTokenInformation);
|
|
uint pLookupPrivilegeName = Resolve("advapi32.dll", ref _advapi32, "LookupPrivilegeNameW", ref _hLookupPrivilegeNameW);
|
|
uint pCloseHandle = Resolve("kernel32.dll", ref _kernel32, "CloseHandle", ref _hCloseHandle);
|
|
if (pGetCurrentProcess == 0 || pOpenProcessToken == 0 || pGetTokenInfo == 0) return result;
|
|
|
|
ulong hProcess = Invoke(pGetCurrentProcess, 0);
|
|
|
|
ulong hToken = 0;
|
|
ulong tokenOk = Invoke(pOpenProcessToken, 3,
|
|
hProcess,
|
|
8u, // TOKEN_QUERY
|
|
(ulong)(uint)(IntPtr)(&hToken));
|
|
if (tokenOk == 0 || hToken == 0) return result;
|
|
|
|
try
|
|
{
|
|
uint returnedSize = 0;
|
|
// GetTokenInformation(hToken, TokenPrivileges=3, NULL, 0, &returnedSize)
|
|
Invoke(pGetTokenInfo, 5,
|
|
hToken,
|
|
3u, // TokenPrivileges
|
|
0u, 0u,
|
|
(ulong)(uint)(IntPtr)(&returnedSize));
|
|
|
|
if (returnedSize == 0) return result;
|
|
if (returnedSize > 65536) returnedSize = 65536;
|
|
|
|
ulong hostBuf = VirtualAllocHost(returnedSize);
|
|
if (hostBuf == 0) return result;
|
|
try
|
|
{
|
|
uint actualSize = 0;
|
|
ulong ok = Invoke(pGetTokenInfo, 5,
|
|
hToken,
|
|
3u,
|
|
hostBuf,
|
|
(ulong)returnedSize,
|
|
(ulong)(uint)(IntPtr)(&actualSize));
|
|
if (ok == 0) return result;
|
|
|
|
// TOKEN_PRIVILEGES layout (packed, no padding):
|
|
// DWORD PrivilegeCount (offset 0)
|
|
// LUID_AND_ATTRIBUTES[] (offset 4), each entry 12 bytes:
|
|
// DWORD LowPart (offset 0)
|
|
// DWORD HighPart (offset 4)
|
|
// DWORD Attributes (offset 8)
|
|
uint privCount = 0;
|
|
{
|
|
byte[] tmp = new byte[4];
|
|
fixed (byte* tp = tmp)
|
|
{
|
|
CopyHostToWasm(hostBuf, (uint)(IntPtr)tp, 4);
|
|
privCount = (uint)(tmp[0] | (tmp[1] << 8) | (tmp[2] << 16) | (tmp[3] << 24));
|
|
}
|
|
}
|
|
if (privCount == 0 || privCount > 4096) return result;
|
|
|
|
const int LUID_AND_ATTR_SIZE = 12;
|
|
uint arrayBytes = privCount * LUID_AND_ATTR_SIZE;
|
|
byte[] entryBuf = new byte[arrayBytes];
|
|
fixed (byte* ep = entryBuf)
|
|
{
|
|
// Array starts at offset 4 (immediately after DWORD PrivilegeCount — packed).
|
|
if (!CopyHostToWasm(hostBuf + 4, (uint)(IntPtr)ep, arrayBytes))
|
|
return result;
|
|
|
|
for (uint i = 0; i < privCount; i++)
|
|
{
|
|
int off = (int)(i * LUID_AND_ATTR_SIZE);
|
|
uint luidLow = Read4(ep, off + 0);
|
|
uint luidHigh = Read4(ep, off + 4);
|
|
uint attrs = Read4(ep, off + 8);
|
|
|
|
string privName = "";
|
|
if (pLookupPrivilegeName != 0)
|
|
{
|
|
// LookupPrivilegeNameW needs an in-WASM LUID, and an in-WASM name buffer.
|
|
// The LUID must be in a fixed local — pass its WASM address.
|
|
ulong luidVal = (ulong)luidLow | ((ulong)luidHigh << 32);
|
|
byte[] privNameBuf = new byte[128]; // up to 64 UTF-16 chars
|
|
uint nameCch = 64;
|
|
fixed (byte* pp = privNameBuf)
|
|
{
|
|
Invoke(pLookupPrivilegeName, 4,
|
|
0u, // NULL = local system
|
|
(ulong)(uint)(IntPtr)(&luidVal),
|
|
(ulong)(uint)(IntPtr)pp,
|
|
(ulong)(uint)(IntPtr)(&nameCch));
|
|
privName = DecodeUtf16LE(pp, (int)nameCch);
|
|
}
|
|
}
|
|
|
|
result.Add((privName, attrs));
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
VirtualFreeHost(hostBuf);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (pCloseHandle != 0 && hToken != 0)
|
|
Invoke(pCloseHandle, 1, hToken);
|
|
}
|
|
}
|
|
catch { /* return whatever we have */ }
|
|
return result;
|
|
}
|
|
|
|
// Decode a UTF-16LE byte array (already in WASM linear memory) into a managed string.
|
|
private static string DecodeUtf16LE(byte* p, int charCount)
|
|
{
|
|
if (charCount <= 0) return "";
|
|
char[] chars = new char[charCount];
|
|
for (int i = 0; i < charCount; i++)
|
|
chars[i] = (char)(p[2*i] | (p[2*i+1] << 8));
|
|
return new string(chars);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the current process token handle as an IntPtr, opened with TOKEN_QUERY.
|
|
/// Replaces WindowsIdentity.GetCurrent().Token which throws on NativeAOT-WASI.
|
|
/// Callers are responsible for CloseHandle; in Seatbelt TokenPrivilegesCommand
|
|
/// the handle is passed to GetTokenInformation via P/Invoke and never explicitly closed.
|
|
/// </summary>
|
|
public static System.IntPtr GetCurrentTokenHandle()
|
|
{
|
|
try
|
|
{
|
|
uint pGetCurrentProcess = Resolve("kernel32.dll", ref _kernel32, "GetCurrentProcess", ref _hGetCurrentProcess);
|
|
uint pOpenProcessToken = Resolve("advapi32.dll", ref _advapi32, "OpenProcessToken", ref _hOpenProcessToken);
|
|
if (pGetCurrentProcess == 0 || pOpenProcessToken == 0) return System.IntPtr.Zero;
|
|
|
|
ulong hProcess = Invoke(pGetCurrentProcess, 0);
|
|
// TOKEN_QUERY = 0x0008
|
|
ulong hTokenOut = 0;
|
|
unsafe
|
|
{
|
|
ulong rc = Invoke(pOpenProcessToken, 3, hProcess, 0x0008u,
|
|
(ulong)(uint)(System.IntPtr)(&hTokenOut));
|
|
if (rc == 0) return System.IntPtr.Zero;
|
|
}
|
|
// hTokenOut holds the host-side handle value written by OpenProcessToken.
|
|
// Return it as IntPtr for use with GetTokenInformation P/Invoke.
|
|
return (System.IntPtr)(long)hTokenOut;
|
|
}
|
|
catch
|
|
{
|
|
return System.IntPtr.Zero;
|
|
}
|
|
}
|
|
|
|
// ── IsHighIntegrity ─────────────────────────────────────────────
|
|
//
|
|
// Reads TokenIntegrityLevel (class 25) from the current process token
|
|
// and compares the RID of the integrity-level SID against
|
|
// SECURITY_MANDATORY_HIGH_RID (0x3000). Uses the same wf_call /
|
|
// mod_invoke pattern as the rest of WfToken so no new host APIs are
|
|
// needed — advapi32!GetTokenInformation + GetSidSubAuthority + a
|
|
// RtlMoveMemory copy to land the RID in WASM linear memory.
|
|
public static bool IsHighIntegrity()
|
|
{
|
|
try
|
|
{
|
|
System.IntPtr hToken = GetCurrentTokenHandle();
|
|
if (hToken == System.IntPtr.Zero) return false;
|
|
|
|
uint pGetTokenInfo = Resolve("advapi32.dll", ref _advapi32, "GetTokenInformation", ref _hGetTokenInformation);
|
|
if (pGetTokenInfo == 0) return false;
|
|
|
|
const uint TokenIntegrityLevel = 25;
|
|
byte[] buf = new byte[64];
|
|
uint returnedSize = 0;
|
|
ulong rc;
|
|
fixed (byte* bPtr = buf)
|
|
{
|
|
rc = Invoke(pGetTokenInfo, 5,
|
|
(ulong)(uint)(long)hToken,
|
|
(ulong)TokenIntegrityLevel,
|
|
(ulong)(uint)(System.IntPtr)bPtr,
|
|
(ulong)(uint)buf.Length,
|
|
(ulong)(uint)(System.IntPtr)(&returnedSize));
|
|
}
|
|
if (rc == 0 || returnedSize < 8) return false;
|
|
|
|
// TOKEN_MANDATORY_LABEL starts with PSID (8 bytes on x64).
|
|
ulong pSid;
|
|
fixed (byte* bPtr = buf)
|
|
{
|
|
pSid = *(ulong*)bPtr;
|
|
}
|
|
if (pSid == 0) return false;
|
|
|
|
uint pGetSidSubAuth = Resolve("advapi32.dll", ref _advapi32, "GetSidSubAuthority", ref _hGetSidSubAuthority);
|
|
if (pGetSidSubAuth == 0) return false;
|
|
// SubAuthority index 0 because mandatory-integrity SIDs only
|
|
// have one sub-authority (the RID).
|
|
ulong pRid = Invoke(pGetSidSubAuth, 2, pSid, 0UL);
|
|
if (pRid == 0) return false;
|
|
|
|
// pRid is a host pointer; can't deref in WASM linear memory.
|
|
// Copy 4 bytes back via RtlMoveMemory.
|
|
uint rid = 0;
|
|
uint pMove = Resolve("ntdll.dll", ref _ntdll, "RtlMoveMemory", ref _hRtlMoveMemory);
|
|
if (pMove == 0) return false;
|
|
Invoke(pMove, 3,
|
|
(ulong)(uint)(System.IntPtr)(&rid),
|
|
pRid,
|
|
4UL);
|
|
|
|
// SECURITY_MANDATORY_HIGH_RID = 0x3000, SYSTEM_RID = 0x4000.
|
|
return rid >= 0x3000;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
private static uint _hGetSidSubAuthority;
|
|
}
|
|
|
|
// ── WfGroupIdentityReference ────────────────────────────────────────────
|
|
//
|
|
// Lightweight IdentityReference subclass that wraps a SecurityIdentifier
|
|
// AND carries a pre-resolved DOMAIN\User name (from SidToAccountName at
|
|
// construction time). The Translate(typeof(NTAccount)) override returns
|
|
// a synthetic NTAccount with the stored name, avoiding NativeAOT-WASI
|
|
// throws from the real SecurityIdentifier.Translate path.
|
|
//
|
|
// Used by WfToken.GetGroupsAsIdentityReferences to provide a drop-in
|
|
// replacement for WindowsIdentity.GetCurrent().Groups (an
|
|
// IdentityReferenceCollection — also IEnumerable<IdentityReference>).
|
|
}
|