Runing non admin exe from within admin code

Hello,

I am trying to start process from my app, but since I am using app as an Admin, I need to launch that exe as standard user.

Similar function that I had while working with C# was this one, which was found online

public static int RunAsDesktopUser(string fileName)
{
    if (string.IsNullOrWhiteSpace(fileName))
        throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName));

    // To start process as shell user you will need to carry out these steps:
    // 1. Enable the SeIncreaseQuotaPrivilege in your current token
    // 2. Get an HWND representing the desktop shell (GetShellWindow)
    // 3. Get the Process ID(PID) of the process associated with that window(GetWindowThreadProcessId)
    // 4. Open that process(OpenProcess)
    // 5. Get the access token from that process (OpenProcessToken)
    // 6. Make a primary token with that token(DuplicateTokenEx)
    // 7. Start the new process with that primary token(CreateProcessWithTokenW)

    var hProcessToken = IntPtr.Zero;
    // Enable SeIncreaseQuotaPrivilege in this process.  (This won't work if current process is not elevated.)
    try
    {
        var process = GetCurrentProcess();
        if (!OpenProcessToken(process, 0x0020, ref hProcessToken))
            return 0;

        var tkp = new TOKEN_PRIVILEGES
        {
            PrivilegeCount = 1,
            Privileges = new LUID_AND_ATTRIBUTES[1]
        };

        if (!LookupPrivilegeValue(null, "SeIncreaseQuotaPrivilege", ref tkp.Privileges[0].Luid))
            return 0;

        tkp.Privileges[0].Attributes = 0x00000002;

        if (!AdjustTokenPrivileges(hProcessToken, false, ref tkp, 0, IntPtr.Zero, IntPtr.Zero))
            return 0;
    }
    finally
    {
        CloseHandle(hProcessToken);
    }

    // Get an HWND representing the desktop shell.
    // CAVEATS:  This will fail if the shell is not running (crashed or terminated), or the default shell has been
    // replaced with a custom shell.  This also won't return what you probably want if Explorer has been terminated and
    // restarted elevated.
    var hwnd = GetShellWindow();
    if (hwnd == IntPtr.Zero)
        return 0 ;

    var hShellProcess = IntPtr.Zero;
    var hShellProcessToken = IntPtr.Zero;
    var hPrimaryToken = IntPtr.Zero;
    try
    {
        // Get the PID of the desktop shell process.
        uint dwPID;
        if (GetWindowThreadProcessId(hwnd, out dwPID) == 0)
            return 0;

        // Open the desktop shell process in order to query it (get the token)
        hShellProcess = OpenProcess(ProcessAccessFlags.QueryInformation, false, dwPID);
        if (hShellProcess == IntPtr.Zero)
            return 0;

        // Get the process token of the desktop shell.
        if (!OpenProcessToken(hShellProcess, 0x0002, ref hShellProcessToken))
            return 0;

        var dwTokenRights = 395U;

        // Duplicate the shell's process token to get a primary token.
        // Based on experimentation, this is the minimal set of rights required for CreateProcessWithTokenW (contrary to current documentation).
        if (!DuplicateTokenEx(hShellProcessToken, dwTokenRights, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out hPrimaryToken))
            return 0;

        // Start the target process with the new token.
        var si = new STARTUPINFO();
        var pi = new PROCESS_INFORMATION();

        if (!CreateProcessWithTokenW(hPrimaryToken, 0, fileName, "", 0, IntPtr.Zero, Path.GetDirectoryName(fileName), ref si, out pi))
            return 0;

        return pi.dwProcessId;
    }
    finally
    {
        CloseHandle(hShellProcessToken);
        CloseHandle(hPrimaryToken);
        CloseHandle(hShellProcess);
    }

}

I tried fiddleing around with winapi, even tried using github copilot and chatgpt but I just cannot find a way to run this process.

Here is my current code

extern crate winapi;

use winapi::shared::minwindef::{FALSE, DWORD, UINT};
use winapi::shared::ntdef::HANDLE;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::winuser::{GetShellWindow, GetWindowThreadProcessId};
use winapi::um::processthreadsapi::{OpenProcess, OpenProcessToken, GetCurrentProcess, PROCESS_INFORMATION, STARTUPINFOW};
use winapi::um::winbase::{CreateProcessWithTokenW, LookupPrivilegeValueA, CREATE_NEW_CONSOLE, STARTF_USESHOWWINDOW};
use winapi::um::securitybaseapi::{DuplicateTokenEx, AdjustTokenPrivileges};
use winapi::um::winnt::{SecurityImpersonation, TokenPrimary, LUID, LUID_AND_ATTRIBUTES, SE_PRIVILEGE_ENABLED, TOKEN_ADJUST_PRIVILEGES, TOKEN_ALL_ACCESS, TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE, TOKEN_PRIVILEGES, TOKEN_QUERY};
use winapi::um::handleapi::CloseHandle;
use std::mem;
use std::ptr::null_mut;
use std::ffi::CString;
use std::os::raw::c_void;
use std::default::Default;

pub unsafe fn run_as_desktop_user(executable_path: &str) -> Result<(), Box<dyn std::error::Error>> {
    // Step 1: Enable the SeIncreaseQuotaPrivilege in your current token
    let mut h_process_token: HANDLE = null_mut();
    if OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &mut h_process_token) == 0 {
        return Err("Failed to open process token".into());
    }

    let mut tkp = TOKEN_PRIVILEGES {
        PrivilegeCount: 1,
        Privileges: [LUID_AND_ATTRIBUTES {
            Luid: LUID {
                LowPart: 0,
                HighPart: 0,
            },
            Attributes: SE_PRIVILEGE_ENABLED,
        }; 1],
    };

    let se_name = CString::new("SeIncreaseQuotaPrivilege").unwrap();
    if LookupPrivilegeValueA(null_mut(), se_name.as_ptr(), &mut tkp.Privileges[0].Luid) == 0 {
        CloseHandle(h_process_token);
        return Err("Failed to lookup privilege value".into());
    }

    if AdjustTokenPrivileges(h_process_token, FALSE, &mut tkp, std::mem::size_of::<TOKEN_PRIVILEGES>() as DWORD, null_mut(), null_mut()) == 0 {
        CloseHandle(h_process_token);
        return Err("Failed to adjust token privileges".into());
    }

    // Step 2: Get an HWND representing the desktop shell
    let hwnd_shell = GetShellWindow();
    if hwnd_shell.is_null() {
        CloseHandle(h_process_token);
        return Err("Failed to get shell window handle".into());
    }

    // Step 3: Get the Process ID of the process associated with that window
    let mut dw_pid: DWORD = 0;
    GetWindowThreadProcessId(hwnd_shell, &mut dw_pid);
    if dw_pid == 0 {
        CloseHandle(h_process_token);
        return Err("Failed to get process ID".into());
    }

    // Step 4: Open that process
    let h_shell_process = OpenProcess(TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, FALSE, dw_pid);
    if h_shell_process.is_null() {
        CloseHandle(h_process_token);
        return Err("Failed to open shell process".into());
    }

    println!("Process 2 {:#?}", h_shell_process);
    // Step 5: Get the access token from that process
    let mut h_shell_process_token: HANDLE = null_mut();
    if OpenProcessToken(h_shell_process, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &mut h_shell_process_token) == 0 {
        let error = GetLastError();
        println!("OpenProcessToken failed with error code: {}", error);
        CloseHandle(h_process_token);
        CloseHandle(h_shell_process);
        return Err(format!("Failed to open process token from shell. Error: {}", error).into());
    }

    println!("Process 3");
    // Step 6: Duplicate the shell's process token
    let mut h_new_token: HANDLE = null_mut();
    if DuplicateTokenEx(h_shell_process_token, TOKEN_ALL_ACCESS, null_mut(), SecurityImpersonation, TokenPrimary, &mut h_new_token) == 0 {
        CloseHandle(h_process_token);
        CloseHandle(h_shell_process);
        CloseHandle(h_shell_process_token);
        return Err("Failed to duplicate token".into());
    }

    println!("Process 4");
    // Step 7: Start the new process with that primary token
    let app_name = CString::new(executable_path).unwrap();
    let mut si = STARTUPINFOW {
        cb: mem::size_of::<STARTUPINFOW>() as UINT,
        lpReserved: null_mut(),
        lpDesktop: null_mut(),
        lpTitle: null_mut(),
        dwFlags: 0,  // Set specific flags as needed, for example, STARTF_USESHOWWINDOW if required
        cbReserved2: 0,
        lpReserved2: null_mut(),
        hStdInput: null_mut(),
        hStdOutput: null_mut(),
        hStdError: null_mut(),
        dwX: 0,
        dwY: 0,
        dwXSize: 0,
        dwYSize: 0,
        dwXCountChars: 0,
        dwYCountChars: 0,
        dwFillAttribute: 0,
        wShowWindow: 0,  // If STARTF_USESHOWWINDOW is used, set this to the appropriate show window constant like SW_SHOW
    };
    let mut pi = PROCESS_INFORMATION {
        hProcess: null_mut(),
        hThread: null_mut(),
        dwProcessId: 0,
        dwThreadId: 0,
    };

    println!("Process 5");

    if CreateProcessWithTokenW(h_new_token, 0, null_mut(), app_name.as_ptr() as *mut _, CREATE_NEW_CONSOLE, null_mut(), null_mut(), &mut si, &mut pi) == 0 {
        CloseHandle(h_process_token);
        CloseHandle(h_shell_process);
        CloseHandle(h_shell_process_token);
        CloseHandle(h_new_token);
        return Err("Failed to create process with duplicated token".into());
    }

    println!("Process started successfully, PID: {}", pi.dwProcessId);

    // Close all the handles now that we're done with them
    CloseHandle(h_process_token);
    CloseHandle(h_shell_process);
    CloseHandle(h_shell_process_token);
    CloseHandle(h_new_token);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    Ok(())
}

Function fails at OpenProcessToken with error code of 5.
Any help would be more than welcome.
Thanks!

Which one? I think it's called twice.

Sorry, its at step 5

println!("Process 2 {:#?}", h_shell_process);
    // Step 5: Get the access token from that process
    let mut h_shell_process_token: HANDLE = null_mut();
    if OpenProcessToken(h_shell_process, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &mut h_shell_process_token) == 0 {
        let error = GetLastError();
        println!("OpenProcessToken failed with error code: {}", error);
        CloseHandle(h_process_token);
        CloseHandle(h_shell_process);
        return Err(format!("Failed to open process token from shell. Error: {}", error).into());
    }

5 is the error code for access denied, but it often shows up for invalid parameters and the like, but at a glance that doesn't seem likely for where the error is. Are you running as a desktop application, not a service or UWP app or something else potentially weird?

Here's the Raymond Chen article for launching unelevated, but it unfortunately uses a bunch of COM ugliness. The basic idea is still the same, to some extent.


As a side-note, you might want to use the windows crate instead of winapi, it's a bit more work to set up with the way it uses features and modules but it provides nicer parameters and results and is much more complete since it's generated from the API metadata. Nothing likely to be related to you immediate error, but it should make the COM stuff a bit nicer.

The windows crate’s COM support makes Raymond Chen’s code not too bad in Rust:

let shell_windows: IShellWindows = CoCreateInstance(&ShellWindows, None, CLSCTX_ALL)?;
let loc = VARIANT::from(CSIDL_DESKTOP);
let empty = VARIANT::new();
let sp: IServiceProvider = shell_windows
    .FindWindowSW(&loc, &empty, SWC_DESKTOP, &mut 0i32, SWFO_NEEDDISPATCH)?
    .cast()?;
let browser: IShellBrowser = sp.QueryService(&SID_STopLevelBrowser)?;
let sv = browser.QueryActiveShellView()?;
let folder_view: IShellFolderViewDual =
    sv.GetItemObject::<IDispatch>(SVGIO_BACKGROUND)?.cast()?;
let shell: IShellDispatch2 = folder_view.Application()?.cast()?;
let file = BSTR::from(r"C:\Windows\System32\cmd.exe");
let show_cmd = VARIANT::from(SW_SHOWNORMAL.0);
shell.ShellExecute(&file, &empty, &empty, &empty, &show_cmd)

I’m afraid I can’t tell what’s wrong with the initial code in this thread, though.

Thank you all!

As an alternative, I just found out that running exe via explorer.exe will run it as standard user, so I was using this snippet, simple one:

let mut proc = Command::new("explorer.exe")
     .arg(path_to_exe_string)
     .stdout(Stdio::null())
     .spawn()
     .expect("Failed to start process");

let _ = proc.wait().expect("Process encountered an error");

That may be a side effect of explorer.exe by default only existing as a single (unprivileged) instance per user. It is possible to disable this option and have a separate explorer.exe instance for each time it runs. This can be useful if you want to modify files for which you need admin permission without granting your own user unprivileged access to those files for security reasons (the popup explorer.exe by default shows when trying to modify a file or directory you don't have permission to will permanently grant you unprivileged access if you accept it, which isn't very secure). It would however break your code for running a process without admin permission if this is indeed a side effect of there being a single instance of explorer.exe by default.

The original...
OpenProcessToken(process, 0x0020, ref hProcessToken)

Your version...
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &mut h_process_token)

Note the original is passing 0x0020 which means just one of the two TOKEN_* values was passed. A quick grep of the SDK reveals ... #define TOKEN_ADJUST_PRIVILEGES (0x0020) ... is the correct choice.

The original...
OpenProcess(ProcessAccessFlags.QueryInformation, false, dwPID)

Your version...
OpenProcess(TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, FALSE, dw_pid)

You are requesting much more access than the original.

The original...
OpenProcessToken(hShellProcess, 0x0002, ref hShellProcessToken)

Your version...
OpenProcessToken(h_shell_process, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &mut h_shell_process_token)

The original only passes TOKEN_DUPLICATE.

I suspect a difference between the original and your version is the source of the trouble.