RunAs Verb not showing a UAC popup and not elevating, when invoked from a process created with a Safer token

0

I want to start a non-elevated process from an elevated one. I've accomplished this using SaferComputeTokenFromLevel. It appears to be working,since:

C:\>whoami /groups | findstr BUILTIN\Administrators
BUILTIN\Administrators     Alias    S-1-5-32-544    Group used for deny only

Deny only is there. So I am not admin.

  • 1) Why the (supodsedly) non-admin process Console Host Window shows as "Administrator:" in the title?

I even used SetTokenInformation to set it to medium integrity. It works but still showing as 'Administrator' even if it's not admin.

Then, if that non-admin process wants to start an elevated process again via ShellExecute with Verb=RunAs (with powershell -C Start-Process -Verb RunAs -FilePath CMD.EXE) the UAC popup doesn't appears, and the process is started but not elevated, not-admin.

  • 2) Why does this happens? (linked token maybe?)
  • 3) Finally, how can I launch a non-elevated process, that can use the RunAs verb to trigger an UAC and elevate succesfully?

Full Repro code (Run as admin!)

using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.InteropServices;

namespace SaferRepro
{
    class Program
    {
        static void Main(string[] args)
        {
            string appToRun = "CMD.EXE";
            string arguments = String.Empty;
            bool newWindow = true, hidden = false;
            var startupFolder = Environment.CurrentDirectory;
            int mediumIntegrity = 8192;

            using (var newToken = GetTokenFromSaferApi())
            {
                AdjustedTokenIntegrity(newToken, mediumIntegrity); // optional, launch as medium integrity process.
                var process = StartWithToken(newToken, appToRun, arguments, startupFolder, newWindow, hidden);
                GetProcessWaitHandle(process.DangerousGetHandle()).WaitOne();
            }
        }

        private static SafeTokenHandle GetTokenFromSaferApi()
        {
            IntPtr hSaferLevel;
            SafeTokenHandle hToken;

            SaferLevels level = SaferLevels.NormalUser;

            if (!NativeMethods.SaferCreateLevel(SaferScopes.User, level, 1, out hSaferLevel, IntPtr.Zero))
                throw new Win32Exception();

            if (!NativeMethods.SaferComputeTokenFromLevel(hSaferLevel, IntPtr.Zero, out hToken, SaferComputeTokenFlags.None, IntPtr.Zero))
                throw new Win32Exception();

            if (!NativeMethods.SaferCloseLevel(hSaferLevel))
                throw new Win32Exception();

            return hToken;
        }

        private static SafeProcessHandle StartWithToken(SafeTokenHandle newToken, string appToRun, string args, string startupFolder, bool newWindow, bool hidden)
        {
            var si = new STARTUPINFO();

            if (newWindow)
            {
                si.dwFlags = 0x00000001; // STARTF_USESHOWWINDOW
                si.wShowWindow = (short)(hidden ? 0 : 1);
            }

            si.cb = Marshal.SizeOf(si);

            var pi = new PROCESS_INFORMATION();
            uint dwCreationFlags = newWindow ? (uint)0x00000010 /*CREATE_NEW_CONSOLE*/: 0;

            if (!NativeMethods.CreateProcessAsUser(newToken, null, $"{appToRun} {args}",
                IntPtr.Zero, IntPtr.Zero, false, dwCreationFlags, IntPtr.Zero, startupFolder, ref si,
                out pi))
            {
                throw new Win32Exception();
            }

            NativeMethods.CloseHandle(pi.hThread);
            return new SafeProcessHandle(pi.hProcess, true);
        }

        private static bool AdjustedTokenIntegrity(SafeTokenHandle newToken, int integrityLevel)
        {
            string integritySid = "S-1-16-" + (integrityLevel.ToString(CultureInfo.InvariantCulture));
            IntPtr pIntegritySid;
            if (!NativeMethods.ConvertStringSidToSid(integritySid, out pIntegritySid))
                return false;

            TOKEN_MANDATORY_LABEL TIL = new TOKEN_MANDATORY_LABEL();
            TIL.Label.Attributes = 0x00000020 /* SE_GROUP_INTEGRITY */;
            TIL.Label.Sid = pIntegritySid;

            var pTIL = Marshal.AllocHGlobal(Marshal.SizeOf<TOKEN_MANDATORY_LABEL>());
            Marshal.StructureToPtr(TIL, pTIL, false);

            if (!NativeMethods.SetTokenInformation(newToken.DangerousGetHandle(),
               TOKEN_INFORMATION_CLASS.TokenIntegrityLevel,
               pTIL,
               (uint)(Marshal.SizeOf<TOKEN_MANDATORY_LABEL>() + NativeMethods.GetLengthSid(pIntegritySid))))
                return false;

            return true;
        }

        public static System.Threading.AutoResetEvent GetProcessWaitHandle(IntPtr processHandle) =>
            new System.Threading.AutoResetEvent(false)
            {
                SafeWaitHandle = new SafeWaitHandle(processHandle, ownsHandle: false)
            };
    }

    internal class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        internal SafeTokenHandle(IntPtr handle) : base(true)
        {
            base.SetHandle(handle);
        }

        private SafeTokenHandle() : base(true) { }

        protected override bool ReleaseHandle()
        {
            return NativeMethods.CloseHandle(base.handle);
        }
    }

    static class NativeMethods
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        internal static extern bool CloseHandle(IntPtr hObject);

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool CreateProcessAsUser(
             SafeTokenHandle hToken,
             string applicationName,
             string commandLine,
             IntPtr pProcessAttributes,
             IntPtr pThreadAttributes,
             bool bInheritHandles,
             uint dwCreationFlags,
             IntPtr pEnvironment,
             string currentDirectory,
             ref STARTUPINFO startupInfo,
             out PROCESS_INFORMATION processInformation);

        #region Safer

        [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
        public static extern bool SaferCreateLevel(
            SaferScopes dwScopeId,
            SaferLevels dwLevelId,
            int OpenFlags,
            out IntPtr pLevelHandle,
            IntPtr lpReserved);

        [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
        public static extern bool SaferCloseLevel(
            IntPtr pLevelHandle);

        [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
        public static extern bool SaferComputeTokenFromLevel(
          IntPtr levelHandle,
          IntPtr inAccessToken,
          out SafeTokenHandle outAccessToken,
          SaferComputeTokenFlags dwFlags,
          IntPtr lpReserved
        );

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool ConvertStringSidToSid(
            string StringSid,
            out IntPtr ptrSid
            );

        [DllImport("advapi32.dll")]
        public static extern int GetLengthSid(IntPtr pSid);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern Boolean SetTokenInformation(IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass,
            IntPtr TokenInformation, UInt32 TokenInformationLength);
        #endregion
    }

    [Flags]
    public enum SaferLevels : uint
    {
        Disallowed = 0,
        Untrusted = 0x1000,
        Constrained = 0x10000,
        NormalUser = 0x20000,
        FullyTrusted = 0x40000
    }
    [Flags]
    public enum SaferComputeTokenFlags : uint
    {
        None = 0x0,
        NullIfEqual = 0x1,
        CompareOnly = 0x2,
        MakeIntert = 0x4,
        WantFlags = 0x8
    }

    [Flags]
    public enum SaferScopes : uint
    {
        Machine = 1,
        User = 2
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    internal struct STARTUPINFO
    {
        public Int32 cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public Int32 dwX;
        public Int32 dwY;
        public Int32 dwXSize;
        public Int32 dwYSize;
        public Int32 dwXCountChars;
        public Int32 dwYCountChars;
        public Int32 dwFillAttribute;
        public Int32 dwFlags;
        public Int16 wShowWindow;
        public Int16 cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public int dwProcessId;
        public int dwThreadId;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct TOKEN_MANDATORY_LABEL
    {

        public SID_AND_ATTRIBUTES Label;

    }

    [StructLayout(LayoutKind.Sequential)]
    public struct SID_AND_ATTRIBUTES
    {
        public IntPtr Sid;
        public uint Attributes;
    }

    // Integrity Levels
    public enum TOKEN_INFORMATION_CLASS
    {
        TokenUser = 1, TokenGroups, TokenPrivileges, TokenOwner, TokenPrimaryGroup, TokenDefaultDacl, TokenSource, TokenType, TokenImpersonationLevel, TokenStatistics, TokenRestrictedSids, TokenSessionId, TokenGroupsAndPrivileges, TokenSessionReference, TokenSandBoxInert, TokenAuditPolicy, TokenOrigin, TokenElevationType, TokenLinkedToken, TokenElevation, TokenHasRestrictions, TokenAccessInformation, TokenVirtualizationAllowed, TokenVirtualizationEnabled, TokenIntegrityLevel, TokenUIAccess, TokenMandatoryPolicy, TokenLogonSid, MaxTokenInfoClass
    }
}
c#
winapi
uac

1 Answer

0

When your a token is elevated, it seems it can't be altered to unflag it's elevated flag status it. You can remove membership from Local Admins (actually set it as Deny Only), or set Integrity to Medium, but it will still be flagged as elevated (even without admin privileges). With this flag set, the RunAs Verb will not elevate.

Your options are:

  • Get explorer.exe token and assume it's not elevated. (its elevated if UAC is disabled or if the user .

  • If UAC is enabled, the elevated token has a link to the unelevated token that you can fetch with GetTokenInformation with TOKEN_INFORMATION_CLASS.TokenLinkedToken.

  • If UAC is disabled there is no linked token. My question doesn't really apply, since the UAC popup is disabled, will never show. FYI you can use SaferApi to create a non-admin token with the elevated flag set, but it's RunAs attempts will fail.

If you want to see how I coded this, check gsudo (a sudo for windows) that I am working on:

https://github.com/gerardog/gsudo/blob/dev/src/gsudo/Tokens/TokenManager.cs https://github.com/gerardog/gsudo/blob/dev/src/gsudo/Helpers/ProcessFactory.cs#L182

or check this other great answer

answered on Stack Overflow Mar 29, 2020 by Gerardo Grignoli

User contributions licensed under CC BY-SA 3.0