diff --git a/.github/workflows/token-right-probe.yml b/.github/workflows/token-right-probe.yml new file mode 100644 index 00000000..aa281902 --- /dev/null +++ b/.github/workflows/token-right-probe.yml @@ -0,0 +1,38 @@ +name: Native token right adjustment probe +on: + push: + branches: ['**'] + paths: + - 'scripts/TokenRightProbe*' + - 'tests/TokenRightProbe*' + - '.github/workflows/token-right-probe.yml' + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + token-right-probe: + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [windows-2022, windows-2025] + engine: [powershell, pwsh] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Native adjustment feasibility in Windows PowerShell + if: matrix.engine == 'powershell' + shell: powershell + run: ./tests/TokenRightProbe.Feasibility.ps1 -AllowDisposableAuditWrite + - name: Native adjustment feasibility in PowerShell7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: ./tests/TokenRightProbe.Feasibility.ps1 -AllowDisposableAuditWrite + - name: Retain native events and cleanup + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: token-right-probe-${{ matrix.os }}-${{ matrix.engine }} + path: ${{ runner.temp }}/wela-token-right-feasibility-*/ + if-no-files-found: error diff --git a/scripts/TokenRightProbeNative.cs b/scripts/TokenRightProbeNative.cs new file mode 100644 index 00000000..323d69c1 --- /dev/null +++ b/scripts/TokenRightProbeNative.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace Wela.TokenRightProbe { + public sealed class Privilege { public string Luid; public uint Attributes; } + public sealed class Outcome { + public string Status, Diagnostic, Luid; + public bool AdjustmentAttempted, Restored; + public uint OriginalAttributes; + public long DisableStartedFileTime, DisableReturnedFileTime, RestoreStartedFileTime, RestoreReturnedFileTime; + public Privilege[] Before, Disabled, After; + } + public static class Native { + [StructLayout(LayoutKind.Sequential)] struct Luid { public uint Low; public int High; } + [StructLayout(LayoutKind.Sequential)] struct Entry { public Luid Id; public uint Attributes; } + [StructLayout(LayoutKind.Sequential)] struct One { public uint Count; public Luid Id; public uint Attributes; } + [DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess(); + [DllImport("kernel32.dll")] static extern IntPtr GetCurrentThread(); + [DllImport("kernel32.dll", SetLastError=true)] static extern bool CloseHandle(IntPtr handle); + [DllImport("kernel32.dll")] static extern void GetSystemTimePreciseAsFileTime(out long value); + [DllImport("kernel32.dll")] static extern void SetLastError(uint error); + [DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenProcessToken(IntPtr process,uint access,out IntPtr token); + [DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenThreadToken(IntPtr thread,uint access,bool self,out IntPtr token); + [DllImport("advapi32.dll",SetLastError=true)] static extern bool GetTokenInformation(IntPtr token,int kind,IntPtr buffer,int length,out int needed); + [DllImport("advapi32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool LookupPrivilegeValue(string system,string name,out Luid value); + [DllImport("advapi32.dll",SetLastError=true)] static extern bool AdjustTokenPrivileges(IntPtr token,bool all,ref One value,uint length,IntPtr previous,IntPtr returned); + static string Hex(Luid id) { return "0x"+(((ulong)(uint)id.High<<32)|id.Low).ToString("x"); } + static long Now() { long value; GetSystemTimePreciseAsFileTime(out value); return value; } + static void PrimaryOnly() { + IntPtr thread; + if(OpenThreadToken(GetCurrentThread(),8,true,out thread)) { CloseHandle(thread); throw new InvalidOperationException("An impersonation token is not accepted."); } + int error=Marshal.GetLastWin32Error(); + if(error!=1008) throw new Win32Exception(error,"Cannot establish absence of an impersonation token."); + } + static Privilege[] Read(IntPtr token) { + int needed; bool first=GetTokenInformation(token,3,IntPtr.Zero,0,out needed); int error=Marshal.GetLastWin32Error(); + if(first||error!=122||needed<4||needed>65536) throw new InvalidOperationException("Unexpected token privilege size response."); + IntPtr buffer=Marshal.AllocHGlobal(needed); + try { + int returned; + if(!GetTokenInformation(token,3,buffer,needed,out returned))throw new Win32Exception(Marshal.GetLastWin32Error()); + int count=Marshal.ReadInt32(buffer); int size=Marshal.SizeOf(typeof(Entry)); + if(returned>needed||count<1||count>4096||4L+(long)count*size>returned)throw new InvalidOperationException("Truncated token privileges."); + var result=new List(); var seen=new HashSet(StringComparer.Ordinal); + for(int i=0;iString.CompareOrdinal(a.Luid,b.Luid));return result.ToArray(); + } finally { Marshal.FreeHGlobal(buffer); } + } + static void Change(IntPtr token,Luid id,uint attributes) { + var value=new One{Count=1,Id=id,Attributes=attributes};SetLastError(0); + bool ok=AdjustTokenPrivileges(token,false,ref value,0,IntPtr.Zero,IntPtr.Zero);int error=Marshal.GetLastWin32Error(); + if(!ok||error!=0)throw new Win32Exception(error,"The fixed privilege adjustment did not report complete success."); + } + static void Equal(Privilege[] expected,Privilege[] actual,string changed,bool enabled) { + if(expected==null||actual==null||expected.Length!=actual.Length)throw new InvalidOperationException("Privilege inventory changed."); + for(int i=0;i