ShellExecuteExW function with lpVerb = L"runas" and lpFile = L"cmd" succeeds but fails

142 Views Asked by At

I have a unit test for a class that is intended to run shell commands with elevated privileges. A simple test case is to copy a test file into C:\Program Files. The class makes a call to the C function ShellExecuteExW function with lpVerb = L"runas" and lpFile = L"cmd" set in the SHELLEXECUTEINFOAW argument. The call succeeds without any errors and the details in the UAC elevation prompt are all correct, but when I look for the test file in program files, it is not there.

I am using an Eiffel compiler that generates cryptic looking MSVC compiled C code, but this is what the code would look like if manually written in C. Note that in the actual code, %TEMP% is already expanded.

#include <windows.h>
#include <Shellapi.h>

int main() {
    SHELLEXECUTEINFOAW shExecInfo = { 0 };

    // Set the structure size
    shExecInfo.cbSize = sizeof(SHELLEXECUTEINFOAW);

    // Specify the operation to perform
    shExecInfo.lpVerb = L"runas"; // "runas" elevates the privileges

    // Specify the file or folder to be copied
    shExecInfo.lpFile = L"cmd";

    // Specify the parameters (in this case, the copy command)
    shExecInfo.lpParameters = L"/C /U copy data\\txt\\file.txt \"C:\\Program Files\" 2> %TEMP%\\copy-errors.txt";

    // Specify the working directory
    shExecInfo.lpDirectory = NULL;

    // Set the show property
    shExecInfo.nShow = SW_HIDE;

    // Execute the operation
    if (ShellExecuteExW(&shExecInfo) == FALSE) {
        // Handle error
        DWORD dwError = GetLastError();
        if (dwError == ERROR_CANCELLED) {
            // The user refused the UAC prompt
            printf("User refused the UAC prompt.\n");
        } else {
            // Other error
            printf("Error: %lu\n", dwError);
        }
        return 1;
    }

    return 0;
}

I run the unit test and I get the expected elevation prompt, and all the details of the command are correct.

UAC prompt

enter image description here

After clicking OK, the command returns without error, but the test file is not being copied into C:\Program Files as expected. If I manually open a shell with admin privileges and copy/paste the exact same command, it succeeds and the file is actually copied. So I am mystified why this is not working in the C call.

The Eiffel Unit test which calls ShellExecuteExW

test_admin_level_execution
   -- OS_COMMAND_TEST_SET.test_admin_level_execution
   note
      testing: "[
         covers/{EL_WINDOWS_ADMIN_SHELL_COMMAND}.execute,
         covers/{EL_WINDOWS_ADMIN_SHELL_COMMAND}.set_command_and_parameters
      ]"
      status: "[
         Commented out in `make_named'. Failing on Windows
      ]"
   local
      command: EL_OS_COMMAND_I; destination_path: FILE_PATH
   do
      if Executable.Is_work_bench then
         create {EL_COPY_FILE_COMMAND_IMP} command.make ("data/txt/file.txt", Directory.applications)
         command.administrator.enable
         command.execute
         assert ("successful", not command.has_error)
         destination_path := Directory.applications + "file.txt"
         if destination_path.exists then
            create {EL_DELETE_FILE_COMMAND_IMP} command.make (destination_path)
            command.administrator.enable
            command.execute
            assert ("File deleted", not destination_path.exists)
         else
            failed ("File exists")
         end
      end
   end

The test fails at the point it checks to see if the file is copied. I have also manually checked in File Explorer.

Alternative Method

In an attempt to find a workaround I am also trying an alternate function: (but the outcome is the same)

    HINSTANCE ShellExecute (
        _In_opt_ HWND    hwnd,
        _In_opt_ LPCTSTR lpOperation,
        _In_     LPCTSTR lpFile,
        _In_opt_ LPCTSTR lpParameters,
        _In_opt_ LPCTSTR lpDirectory,
        _In_     INT     nShowCmd
    );

Environment

Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01 for x64

OS: Windows 7

2

There are 2 best solutions below

2
Luke On BEST ANSWER

The problem is that apparently when cmd is launched elevated it does not honor the current directory and instead uses the system32 folder: https://stackoverflow.com/a/38034535/179634

So you need to either specify the full path in the parameter to the copy command, or change the current directory in your command line using the cd command.

0
Finnian Reilly On

As Luke pointed out, when cmd is launched elevated it does not honor the current directory and instead uses the system32 folder. The solution Luke offered was very helpful, but did not solve the problem completely as I will explain. I tried using this sequence of command parts:

Debugger View

Eiffel Debugger View

When I manually inspected the output folder, I could see file.txt, but the unit test was still failing. Why was that? Turns out the ShellExecute runs asynchronously, so I need some way to wait until the process finishes. The solution I came up with is shown in the code fragment below. (Note anything prefixed with `c_' is a C call from Eiffel)

class
   EL_WINDOWS_SHELL_COMMAND

inherit
   EL_ALLOCATED_C_OBJECT
      rename
         make_default as make
      redefine
         make
      end

feature -- Basic operations

   execute
      local
         n: INTEGER
      do
         c_set_n_show (self_ptr, show_type)
         if c_shell_execute (self_ptr) then
         -- make asynchronous process appear to be synchronous
            n := c_wait_for_single_object (process_handle)
            if c_close_handle (process_handle) then
               do_nothing
            end
            is_successful := n >= 0
         end
      end

feature {NONE} -- Implementation

   process_handle: NATURAL
      do
         Result := c_process (self_ptr)
      end

end

Class EL_WINDOWS_SHELL_COMMAND is a wrapper for the C Struct SHELLEXECUTEINFOW.

Note to self

I need to ask Stackoverflow to improve the syntax highlighting for Eiffel