0

I'm attempting to create a utility tool via Python 3.x for the Windows 10 command-line. Since it will better format general command-line commands into more user-friendly menus, I want it to require elevated permissions through UAC when it runs.

I'm using the ctypes method described here, and it does indeed have Python's executable request UAC elevation.

However, since a lot of the things I'll be writing menus and the like for will require (or be heavily limited without) these elevated permissions, I want the script to exit (preferably through sys.exit) if it doesn't find any.

In the ctypes method I mentioned, it should run as follows;

  1. It defines a function is_admin(), which gets the value of ctypes.windll.shell32.IsUserAnAdmin(), and if it's 0, returns false instead.

  2. is_admin() is called in a conditional, and if it gets false, it attempts to execute the command-line command to re-run the script as an executable using ShellExecuteW and some variables from sys;

    ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv[0]), None, 1)
    

In my code, I have the above conditional with the addition of a variable elevReq that I set to true;

if is_admin():
    success("Already running as administrator!") # "success" and "warn" are defined earlier
    elevReq = True
else:
    warn("Requesting administrative permissions...", False)
    ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv[0]), None, 1)
    elevReq = True

I follow it up with another conditional that checks to see if elevReq is true and is_admin() is false, to see if the user selected "no" on UAC's pop-up -- if it is, it should throw an error about the lack of elevated privileges, and then quit;

if elevReq and is_admin() == False:
    error("[FATAL] Elevation was not given! Stopping...", True)
    sys.exit(1)

The problem I'm having is that the given method doesn't seem to actually be elevating permissions of Python. UAC does pop up, but when any option is selected, it doesn't seem to matter, as the above condition fires anyway. Manually running the script in an elevated command prompt from the start doesn't have this issue.

Is this an issue with the script not re-loading when it should? If not, why is it exiting anyway?

martineau
  • 119,623
  • 25
  • 170
  • 301
EarthToAccess
  • 71
  • 1
  • 10

2 Answers2

0
import ctypes
import sys
import platform

def admin() -> "Admin Bool":
    """Requests UAC Admin on Windows with a prompt"""
    if platform.system() == "Windows":
        ctypes.windll.shell32.ShellExecuteW(
            None,
            'runas',
            sys.executable,
            ' '.join(sys.argv),
            None,
            None
        )
        
        try:
            return ctypes.windll.shell32.IsUserAnAdmin()
        
        except:
            return False
        
    else:
        raise OSError("admin() only works for windows.")

This will request admin with an admin prompt. If True is returned, then admin has been granted by the user. Else, False will be returned.

  • This is the same method I mentioned before, minor exception that, this time, it's all in the same function. The problem isn't that UAC isn't popping up, it's that even after it does, permissions still don't seem to be given. Even with the code formatted like this, it's still returning False and running the code for if it does (the final code block). – EarthToAccess Dec 14 '21 at 21:28
0

The ShellExecute API call will spawn a new process, it won't elevate permissions for the current process running.

Let's analyze this code snippet:

if is_admin():
    main()
else:
    ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)

When you first run this Python script without privileges, this initial process will jump to the last line of the code because is_admin returns False. Once there, the UAC prompt is displayed.

  • If the UAC prompt is accepted, then a completely new process (with different process ID) is created and the code is executed again (from the beginning), but this time with admin privileges. Now is_admin should return True and main should be called.
  • If the UAC prompt is rejected, no new process is created.

Regardless of the UAC response, the initial process will get the return code back, but its privileges will remain unaltered.

If you want to try this yourself, add an input() at the end of the file and you should be able to see two different windows after accepting the UAC prompt.

To avoid having your code being executed twice be sure to keep everything inside the main function. If you want to take an action based on the return code, this only makes sense for failures (code <= 32). If the return code is successfull (> 32), then the process should end gracefully and let the new spawned process do its job.

Martín De la Fuente
  • 6,155
  • 4
  • 27
  • 28
  • that's the issue, then; it doesn't seem to be doing anything with that new process. i can see it being made in Task Manager, but it doesn't appear to be running any new windows or command prompt/PowerShell instances. i've tried running it both in a standalone command prompt window, and in the built-in PowerShell prompt that Visual Studio Code provides. – EarthToAccess Dec 16 '21 at 05:29
  • so, i just tried to modify the `lpFile` and `lpParameters` arguments to run `cmd /k – EarthToAccess Dec 16 '21 at 05:37
  • Try using `ShellExecuteEx` with `nShow=SW_SHOW`, maybe? That's what I used and I can see the prompt of my new process. – DaLynX Dec 20 '21 at 11:25
  • i can't seem to find any documentation on ShellExecuteEx to implement it -- all i'm finding on Microsoft's side is ShellExecuteA, W, or ExA and W (the final two being vastly different) -- regardless, both A and W have the `nCmdShow` parameter anyway so i gave it a try, but still have no such luck on it actually restarting the script in a new process. – EarthToAccess Dec 20 '21 at 16:49
  • `nShowCmd` is the sixth parameter of the call. In my example, 1 means `SW_NORMAL`. You can check all the possible values [here](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow). I don't think that's the cause of your problem though. – Martín De la Fuente Dec 20 '21 at 17:03
  • If you run JUST the code snippet (remove all your own code) and call `input` in the `main` function, does it open the new window for you after accepting the UAC prompt? – Martín De la Fuente Dec 20 '21 at 17:05
  • it seems to appear for a very split second, but doesn't show any past that, and definitely doesn't stay put when `input` is called. might this be an issue specifically with ctypes' use of ShellExecuteW, somehow? i feel this issue is unique based on the answers so far – EarthToAccess Dec 20 '21 at 18:04
  • Do you have any other code inside the `main` function besides calling `input()`? What you describe seems like the code is raising an exception – Martín De la Fuente Dec 20 '21 at 18:47
  • nope, i made an entirely new script file, imported sys and ctypes, and made a completely different main function that holds only print and the input function. – EarthToAccess Dec 21 '21 at 00:11
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/240311/discussion-between-earthtoaccess-and-martin-de-la-fuente). – EarthToAccess Dec 21 '21 at 13:09