I had best results by cloning Explorer's token as follows:

var shellWnd = WinAPI.GetShellWindow();
if (shellWnd == IntPtr.Zero)
    throw new Exception("Could not find shell window");

uint shellProcessId;
WinAPI.GetWindowThreadProcessId(shellWnd, out shellProcessId);

var hShellProcess = WinAPI.OpenProcess(0x00000400 /* QueryInformation */, false, shellProcessId);

var hShellToken = IntPtr.Zero;
if (!WinAPI.OpenProcessToken(hShellProcess, 2 /* TOKEN_DUPLICATE */, out hShellToken))
    throw new Win32Exception();

uint tokenAccess = 8 /*TOKEN_QUERY*/ | 1 /*TOKEN_ASSIGN_PRIMARY*/ | 2 /*TOKEN_DUPLICATE*/ | 0x80 /*TOKEN_ADJUST_DEFAULT*/ | 0x100 /*TOKEN_ADJUST_SESSIONID*/;
var hToken = IntPtr.Zero;
WinAPI.DuplicateTokenEx(hShellToken, tokenAccess, IntPtr.Zero, 2 /* SecurityImpersonation */, 1 /* TokenPrimary */, out hToken);

var pi = new WinAPI.PROCESS_INFORMATION();
var si = new WinAPI.STARTUPINFO();
si.cb = Marshal.SizeOf(si);
if (!WinAPI.CreateProcessWithTokenW(hToken, 0, null, cmdArgs, 0, IntPtr.Zero, null, ref si, out pi))
    throw new Win32Exception();

Alternative approach

Originally I went with drf's excellent answer, but expanded it somewhat. If the above (clone Explorer's token) is not to your liking, keep reading but see a gotcha at the very end.

When using drf's method as described, the process is started without administrative access, but it still has a high integrity level. A typical un-elevated process has a medium integrity level.

Try this: use Process Hacker to see the properties of the process started this way; you will see that PH considers the process to be elevated even though it doesn't have administrative access. Add an Integrity column and you'll see it's "High".

The fix is reasonably simple: after using SaferComputeTokenFromLevel, we need to change the token integrity level to Medium. The code to do this might look something like this (converted from MSDN sample):

// Get the Medium Integrity SID
if (!WinAPI.ConvertStringSidToSid("S-1-16-8192", out pMediumIntegritySid))
    throw new Win32Exception();

// Construct a structure describing the token integrity level
var TIL = new TOKEN_MANDATORY_LABEL();
TIL.Label.Attributes = 0x00000020 /* SE_GROUP_INTEGRITY */;
TIL.Label.Sid = pMediumIntegritySid;
pTIL = Marshal.AllocHGlobal(Marshal.SizeOf<TOKEN_MANDATORY_LABEL>());
Marshal.StructureToPtr(TIL, pTIL, false);

// Modify the token
if (!WinAPI.SetTokenInformation(hToken, 25 /* TokenIntegrityLevel */, pTIL,
                                (uint) Marshal.SizeOf<TOKEN_MANDATORY_LABEL>()
                                + WinAPI.GetLengthSid(pMediumIntegritySid)))
    throw new Win32Exception();

Alas, this still doesn't really solve the problem completely. The process won't have administrative access; it won't have a high integrity, but it will still have a token that's marked as "elevated".

Whether this is a problem for you or not I don't know, but it may have been why I ended up cloning Explorer's token in the end, as described at the start of this answer.


Here is my full source code (modified drf's answer), in all its P/Invoke glory:

var hSaferLevel = IntPtr.Zero;
var hToken = IntPtr.Zero;
var pMediumIntegritySid = IntPtr.Zero;
var pTIL = IntPtr.Zero;
var pi = new WinAPI.PROCESS_INFORMATION();
try
{
    var si = new WinAPI.STARTUPINFO();
    si.cb = Marshal.SizeOf(si);
    var processAttributes = new WinAPI.SECURITY_ATTRIBUTES();
    var threadAttributes = new WinAPI.SECURITY_ATTRIBUTES();
    var args = CommandRunner.ArgsToCommandLine(Args);

    if (!WinAPI.SaferCreateLevel(WinAPI.SaferScopes.User, WinAPI.SaferLevels.NormalUser, 1, out hSaferLevel, IntPtr.Zero))
        throw new Win32Exception();

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

    if (!WinAPI.ConvertStringSidToSid("S-1-16-8192", out pMediumIntegritySid))
        throw new Win32Exception();
    var TIL = new TOKEN_MANDATORY_LABEL();
    TIL.Label.Attributes = 0x00000020 /* SE_GROUP_INTEGRITY */;
    TIL.Label.Sid = pMediumIntegritySid;
    pTIL = Marshal.AllocHGlobal(Marshal.SizeOf<TOKEN_MANDATORY_LABEL>());
    Marshal.StructureToPtr(TIL, pTIL, false);
    if (!WinAPI.SetTokenInformation(hToken, 25 /* TokenIntegrityLevel */, pTIL, (uint) Marshal.SizeOf<TOKEN_MANDATORY_LABEL>() + WinAPI.GetLengthSid(pMediumIntegritySid)))
        throw new Win32Exception();

    if (!WinAPI.CreateProcessAsUser(hToken, null, commandLine, ref processAttributes, ref threadAttributes, true, 0, IntPtr.Zero, null, ref si, out pi))
        throw new Win32Exception();
}
finally
{
    if (hToken != IntPtr.Zero && !WinAPI.CloseHandle(hToken))
        throw new Win32Exception();
    if (pMediumIntegritySid != IntPtr.Zero && WinAPI.LocalFree(pMediumIntegritySid) != IntPtr.Zero)
        throw new Win32Exception();
    if (pTIL != IntPtr.Zero)
        Marshal.FreeHGlobal(pTIL);
    if (pi.hProcess != IntPtr.Zero && !WinAPI.CloseHandle(pi.hProcess))
        throw new Win32Exception();
    if (pi.hThread != IntPtr.Zero && !WinAPI.CloseHandle(pi.hThread))
        throw new Win32Exception();
}

And here are the P/Invoke definitions you'll need in addition to those listed in drf's answer:

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

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject);

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

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

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LocalFree(IntPtr hMem);

The Win32 Security Management functions provide the capability to create a restricted token with normal user rights; with the token, you can call CreateProcessAsUser to run the process with that token. Below is a proof of concept that runs cmd.exe as a normal user, regardless of whether the process is run in an elevated context.

// Initialize variables.  
IntPtr hSaferLevel, hToken;
STARTUPINFO si = default(STARTUPINFO);
SECURITY_ATTRIBUTES processAttributes = default(SECURITY_ATTRIBUTES);
SECURITY_ATTRIBUTES threadAttributes = default(SECURITY_ATTRIBUTES);
PROCESS_INFORMATION pi;
si.cb = Marshal.SizeOf(si);

// The process to start (for demonstration, cmd.exe)
string ProcessName = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.System),
    "cmd.exe");

// Create the restricted token info
if (!SaferCreateLevel(
     SaferScopes.User,
     SaferLevels.NormalUser, // Program will execute as a normal user
     1, // required
     out hSaferLevel,
     IntPtr.Zero))
         throw new Win32Exception(Marshal.GetLastWin32Error());

// From the level create a token
if (!SaferComputeTokenFromLevel(
     hSaferLevel,
     IntPtr.Zero,
     out hToken,
     SaferComputeTokenFlags.None,
     IntPtr.Zero))
         throw new Win32Exception(Marshal.GetLastWin32Error());

// Run the process with the restricted token
if (!CreateProcessAsUser(
     hToken,
     ProcessName,
     null, ref processAttributes, ref threadAttributes,
     true, 0, IntPtr.Zero, null,
     ref si, out pi))
         throw new Win32Exception(Marshal.GetLastWin32Error());

 // Cleanup
 if (!CloseHandle(pi.hProcess))
     throw new Win32Exception(Marshal.GetLastWin32Error());
 if (!CloseHandle(pi.hThread))
     throw new Win32Exception(Marshal.GetLastWin32Error());
 if (!SaferCloseLevel(hSaferLevel))
     throw new Win32Exception(Marshal.GetLastWin32Error());

This approach makes use the following Win32 functions:

  • SaferIdentifyLevel to indicate the identity level (limited, normal, or elevated). Setting the levelId to SAFER_LEVELID_NORMALUSER (0x20000) provides the normal user level.
  • SaferComputeTokenFromLevel creates a token for the provided level. Passing NULL to the InAccessToken parameter uses the identity of the current thread.
  • CreateProcessAsUser creates the process with the provided token. Since the session is already interactive, most of the parameters can be kept at default values. (The third parameter, lpCommandLine can be provided as a string to specify the command line.)
  • CloseHandle (Kernel32) and SaferCloseLevel to free allocated memory.

Finally, the P/Invoke code is below (copied mostly from pinvoke.net):

[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)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
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;
}


[DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
public static extern bool SaferComputeTokenFromLevel(IntPtr LevelHandle, IntPtr InAccessToken, out IntPtr OutAccessToken, int dwFlags, IntPtr lpReserved);

[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern bool CreateProcessAsUser(
    IntPtr hToken,
    string lpApplicationName,
    string lpCommandLine,
    ref SECURITY_ATTRIBUTES lpProcessAttributes,
    ref SECURITY_ATTRIBUTES lpThreadAttributes,
    bool bInheritHandles,
    uint dwCreationFlags,
    IntPtr lpEnvironment,
    string lpCurrentDirectory,
    ref STARTUPINFO lpStartupInfo,
    out PROCESS_INFORMATION lpProcessInformation); 

[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 IntPtr outAccessToken,
  SaferComputeTokenFlags dwFlags,
  IntPtr lpReserved
);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CloseHandle(IntPtr hObject);

The easiest solution would be starting the process using explorer.exe. This will start any process unelevated. You can just start explorer.exe using

    System.Diagnostics.Process.Start();

The file name will be "C:\Windows\explorer.exe" and the arguments will be the executable you want to start unelevated, surrounded by quotes.

Example:

If I wanted to start F:\folder\example.exe unelevated I would do this:

    using System.Diagnostics;

    namespace example
    {
        class exampleClass
        {
            ProcessStartInfo exampleStartInfo = new ProcessStartInfo();
            exampleStartInfo.FileName = "C:\\Windows\\explorer.exe";
            exampleStartInfo.Arguments = "\"F:\\folder\\example.exe\"";
            Process.Start(exampleStartInfo);
        }
    }

This might not work on older versions of windows, but it at least works on my laptop, so it certainly does on windows 10.


Raymond Chen addressed this in his blog:

How can I launch an unelevated process from my elevated process and vice versa?

Searching in GitHub for a C# version of this code, I found the following implementation in Microsoft's Node.js tools for Visual Studio repository: SystemUtilities.cs (see the ExecuteProcessUnElevated function).

Just in case the file disappears, here's the file's contents:

// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
using System.Runtime.InteropServices;

namespace Microsoft.NodejsTools.SharedProject
{
    /// <summary>
    /// Utility for accessing window IShell* interfaces in order to use them to launch a process unelevated
    /// </summary>
    internal class SystemUtility
    {
        /// <summary>
        /// We are elevated and should launch the process unelevated. We can't create the
        /// process directly without it becoming elevated. So to workaround this, we have
        /// explorer do the process creation (explorer is typically running unelevated).
        /// </summary>
        internal static void ExecuteProcessUnElevated(string process, string args, string currentDirectory = "")
        {
            var shellWindows = (IShellWindows)new CShellWindows();

            // Get the desktop window
            object loc = CSIDL_Desktop;
            object unused = new object();
            int hwnd;
            var serviceProvider = (IServiceProvider)shellWindows.FindWindowSW(ref loc, ref unused, SWC_DESKTOP, out hwnd, SWFO_NEEDDISPATCH);

            // Get the shell browser
            var serviceGuid = SID_STopLevelBrowser;
            var interfaceGuid = typeof(IShellBrowser).GUID;
            var shellBrowser = (IShellBrowser)serviceProvider.QueryService(ref serviceGuid, ref interfaceGuid);

            // Get the shell dispatch
            var dispatch = typeof(IDispatch).GUID;
            var folderView = (IShellFolderViewDual)shellBrowser.QueryActiveShellView().GetItemObject(SVGIO_BACKGROUND, ref dispatch);
            var shellDispatch = (IShellDispatch2)folderView.Application;

            // Use the dispatch (which is unelevated) to launch the process for us
            shellDispatch.ShellExecute(process, args, currentDirectory, string.Empty, SW_SHOWNORMAL);
        }

        /// <summary>
        /// Interop definitions
        /// </summary>
        private const int CSIDL_Desktop = 0;
        private const int SWC_DESKTOP = 8;
        private const int SWFO_NEEDDISPATCH = 1;
        private const int SW_SHOWNORMAL = 1;
        private const int SVGIO_BACKGROUND = 0;
        private readonly static Guid SID_STopLevelBrowser = new Guid("4C96BE40-915C-11CF-99D3-00AA004AE837");

        [ComImport]
        [Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39")]
        [ClassInterfaceAttribute(ClassInterfaceType.None)]
        private class CShellWindows
        {
        }

        [ComImport]
        [Guid("85CB6900-4D95-11CF-960C-0080C7F4EE85")]
        [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
        private interface IShellWindows
        {
            [return: MarshalAs(UnmanagedType.IDispatch)]
            object FindWindowSW([MarshalAs(UnmanagedType.Struct)] ref object pvarloc, [MarshalAs(UnmanagedType.Struct)] ref object pvarlocRoot, int swClass, out int pHWND, int swfwOptions);
        }

        [ComImport]
        [Guid("6d5140c1-7436-11ce-8034-00aa006009fa")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IServiceProvider
        {
            [return: MarshalAs(UnmanagedType.Interface)]
            object QueryService(ref Guid guidService, ref Guid riid);
        }

        [ComImport]
        [Guid("000214E2-0000-0000-C000-000000000046")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IShellBrowser
        {
            void VTableGap01(); // GetWindow
            void VTableGap02(); // ContextSensitiveHelp
            void VTableGap03(); // InsertMenusSB
            void VTableGap04(); // SetMenuSB
            void VTableGap05(); // RemoveMenusSB
            void VTableGap06(); // SetStatusTextSB
            void VTableGap07(); // EnableModelessSB
            void VTableGap08(); // TranslateAcceleratorSB
            void VTableGap09(); // BrowseObject
            void VTableGap10(); // GetViewStateStream
            void VTableGap11(); // GetControlWindow
            void VTableGap12(); // SendControlMsg
            IShellView QueryActiveShellView();
        }

        [ComImport]
        [Guid("000214E3-0000-0000-C000-000000000046")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IShellView
        {
            void VTableGap01(); // GetWindow
            void VTableGap02(); // ContextSensitiveHelp
            void VTableGap03(); // TranslateAcceleratorA
            void VTableGap04(); // EnableModeless
            void VTableGap05(); // UIActivate
            void VTableGap06(); // Refresh
            void VTableGap07(); // CreateViewWindow
            void VTableGap08(); // DestroyViewWindow
            void VTableGap09(); // GetCurrentInfo
            void VTableGap10(); // AddPropertySheetPages
            void VTableGap11(); // SaveViewState
            void VTableGap12(); // SelectItem

            [return: MarshalAs(UnmanagedType.Interface)]
            object GetItemObject(UInt32 aspectOfView, ref Guid riid);
        }

        [ComImport]
        [Guid("00020400-0000-0000-C000-000000000046")]
        [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
        private interface IDispatch
        {
        }

        [ComImport]
        [Guid("E7A1AF80-4D96-11CF-960C-0080C7F4EE85")]
        [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
        private interface IShellFolderViewDual
        {
            object Application { [return: MarshalAs(UnmanagedType.IDispatch)] get; }
        }

        [ComImport]
        [Guid("A4C6892C-3BA9-11D2-9DEA-00C04FB16162")]
        [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
        public interface IShellDispatch2
        {
            void ShellExecute([MarshalAs(UnmanagedType.BStr)] string File, [MarshalAs(UnmanagedType.Struct)] object vArgs, [MarshalAs(UnmanagedType.Struct)] object vDir, [MarshalAs(UnmanagedType.Struct)] object vOperation, [MarshalAs(UnmanagedType.Struct)] object vShow);
        }
    }
}