4

In order to request UAC elevated privilege from Python, when calling an external program, you can just do

ctypes.windll.shell32.ShellExecuteW(None, "runas", my_binary_file_path, "", None, 1)

Yet, assuming your Python script is executing with admin rights, how can you call an external program without admin rigths?

Jongware
  • 22,200
  • 8
  • 54
  • 100
glezo
  • 742
  • 2
  • 9
  • 22

2 Answers2

4

One approach, which I prefer, is to run as the shell user. Start by opening the shell process and duplicating its Token. You can get the shell process PID by calling GetShellWindow and then GetWindowThreadProcessId. Usually this is Explorer.

By default, an administrator account doesn't have SeAssignPrimaryTokenPrivilege, in which case you can't call CreateProcessAsUser directly. You have to request a more privileged process to make the call on your behalf. CreateProcessWithTokenW does this for you by making a remote procedure call to the Secondary Logon service.

PyWin32 doesn't wrap GetShellWindow and CreateProcessWithTokenW, so you'll need to use ctypes to call them.

Rarely a Windows system may be running without a regular shell, or with a shell the fails to register its window via SetShellWindow[Ex]. In this case, GetShellWindow returns NULL. As a fallback for this case, you can use a somewhat questionable (but working) method to get the session user's token and call CreateProcessAsUser.

Begin by getting the PID of the session's Windows subsystem process, csrss.exe. The easiest way is to call the undocumented (but stable) function CsrGetProcessId. Enable SeDebugPrivilege to open this process with limited-query access. Then open its Token, duplicate it, and impersonate. Now you have the required SeTcbPrivilege to get the session user's Token via WTSQueryUserToken, and you also have SeAssignPrimaryTokenPrivilege to be able to call CreateProcessAsUser.

imports and ctypes definitions

import os
import contextlib

import win32con
import winerror
import win32api
import win32process
import win32security
import win32ts
import pywintypes

import ctypes
from ctypes import wintypes

ntdll = ctypes.WinDLL('ntdll')
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
advapi32 = ctypes.WinDLL('advapi32', use_last_error=True)
user32 = ctypes.WinDLL('user32', use_last_error=True)

TOKEN_ADJUST_SESSIONID = 0x0100
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
LPBYTE = ctypes.POINTER(wintypes.BYTE)

class STARTUPINFO(ctypes.Structure):
    """https://msdn.microsoft.com/en-us/library/ms686331"""
    __slots__ = ()

    _fields_ = (('cb',              wintypes.DWORD),
                ('lpReserved',      wintypes.LPWSTR),
                ('lpDesktop',       wintypes.LPWSTR),
                ('lpTitle',         wintypes.LPWSTR),
                ('dwX',             wintypes.DWORD),
                ('dwY',             wintypes.DWORD),
                ('dwXSize',         wintypes.DWORD),
                ('dwYSize',         wintypes.DWORD),
                ('dwXCountChars',   wintypes.DWORD),
                ('dwYCountChars',   wintypes.DWORD),
                ('dwFillAttribute', wintypes.DWORD),
                ('dwFlags',         wintypes.DWORD),
                ('wShowWindow',     wintypes.WORD),
                ('cbReserved2',     wintypes.WORD),
                ('lpReserved2',     LPBYTE),
                ('hStdInput',       wintypes.HANDLE),
                ('hStdOutput',      wintypes.HANDLE),
                ('hStdError',       wintypes.HANDLE))

    def __init__(self, **kwds):
        self.cb = ctypes.sizeof(self)
        super(STARTUPINFO, self).__init__(**kwds)

LPSTARTUPINFO = ctypes.POINTER(STARTUPINFO)

class PROCESS_INFORMATION(ctypes.Structure):
    """https://msdn.microsoft.com/en-us/library/ms684873"""
    __slots__ = ()

    _fields_ = (('hProcess',    wintypes.HANDLE),
                ('hThread',     wintypes.HANDLE),
                ('dwProcessId', wintypes.DWORD),
                ('dwThreadId',  wintypes.DWORD))

LPPROCESS_INFORMATION = ctypes.POINTER(PROCESS_INFORMATION)

kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)

# https://msdn.microsoft.com/en-us/library/ms682434
advapi32.CreateProcessWithTokenW.argtypes = (
    wintypes.HANDLE,       # _In_        hToken
    wintypes.DWORD,        # _In_        dwLogonFlags
    wintypes.LPCWSTR,      # _In_opt_    lpApplicationName
    wintypes.LPWSTR,       # _Inout_opt_ lpCommandLine
    wintypes.DWORD,        # _In_        dwCreationFlags
    wintypes.LPCWSTR,      # _In_opt_    lpEnvironment
    wintypes.LPCWSTR,      # _In_opt_    lpCurrentDirectory
    LPSTARTUPINFO,         # _In_        lpStartupInfo
    LPPROCESS_INFORMATION) # _Out_       lpProcessInformation

# https://msdn.microsoft.com/en-us/library/ms633512
user32.GetShellWindow.restype = wintypes.HWND

helper functions

def adjust_token_privileges(htoken, state):
    prev_state = win32security.AdjustTokenPrivileges(htoken, False, state)
    error = win32api.GetLastError()
    if error == winerror.ERROR_NOT_ALL_ASSIGNED:
        raise pywintypes.error(
                error, 'AdjustTokenPrivileges',
                win32api.FormatMessageW(error))
    return prev_state

def enable_token_privileges(htoken, *privilege_names):
    state = []
    for name in privilege_names:
        state.append((win32security.LookupPrivilegeValue(None, name),
                      win32con.SE_PRIVILEGE_ENABLED))
    return adjust_token_privileges(htoken, state)

@contextlib.contextmanager
def open_effective_token(access, open_as_self=True):
    hthread = win32api.GetCurrentThread()
    impersonated_self = False
    try:
        htoken = win32security.OpenThreadToken(
            hthread, access, open_as_self)
    except pywintypes.error as e:
        if e.winerror != winerror.ERROR_NO_TOKEN:
            raise
        win32security.ImpersonateSelf(win32security.SecurityImpersonation)
        impersonated_self = True
        htoken = win32security.OpenThreadToken(
            hthread, access, open_as_self)
    try:
        yield htoken
    finally:
        if impersonated_self:
            win32security.SetThreadToken(None, None)

@contextlib.contextmanager
def enable_privileges(*privilege_names):
    """Enable a set of privileges for the current thread."""
    prev_state = ()
    with open_effective_token(
            win32con.TOKEN_QUERY |
            win32con.TOKEN_ADJUST_PRIVILEGES) as htoken:
        prev_state = enable_token_privileges(htoken, *privilege_names)
        try:
            yield
        finally:
            if prev_state:
                adjust_token_privileges(htoken, prev_state)

def duplicate_shell_token():
    hWndShell = user32.GetShellWindow()
    if not hWndShell:
        raise pywintypes.error(
                winerror.ERROR_FILE_NOT_FOUND,
                'GetShellWindow', 'no shell window')
    tid, pid = win32process.GetWindowThreadProcessId(hWndShell)
    hProcShell = win32api.OpenProcess(
                    win32con.PROCESS_QUERY_INFORMATION, False, pid)
    hTokenShell = win32security.OpenProcessToken(
                    hProcShell, win32con.TOKEN_DUPLICATE)
    # Contrary to MSDN, CreateProcessWithTokenW also requires
    # TOKEN_ADJUST_DEFAULT and TOKEN_ADJUST_SESSIONID
    return win32security.DuplicateTokenEx(
                hTokenShell,
                win32security.SecurityImpersonation,
                win32con.TOKEN_ASSIGN_PRIMARY |
                win32con.TOKEN_DUPLICATE |
                win32con.TOKEN_QUERY |
                win32con.TOKEN_ADJUST_DEFAULT |
                TOKEN_ADJUST_SESSIONID,
                win32security.TokenPrimary, None)

@contextlib.contextmanager
def impersonate_system():
    with enable_privileges(win32security.SE_DEBUG_NAME):
        pid_csr = ntdll.CsrGetProcessId()
        hprocess_csr = win32api.OpenProcess(
            PROCESS_QUERY_LIMITED_INFORMATION, False, pid_csr)
        htoken_csr = win32security.OpenProcessToken(
            hprocess_csr, win32con.TOKEN_DUPLICATE)
    htoken = win32security.DuplicateTokenEx(
        htoken_csr, win32security.SecurityImpersonation,
        win32con.TOKEN_QUERY |
        win32con.TOKEN_IMPERSONATE |
        win32con.TOKEN_ADJUST_PRIVILEGES,
        win32security.TokenImpersonation)
    enable_token_privileges(
        htoken,
        win32security.SE_TCB_NAME,
        win32security.SE_INCREASE_QUOTA_NAME,
        win32security.SE_ASSIGNPRIMARYTOKEN_NAME)
    try:
        htoken_prev = win32security.OpenThreadToken(
            win32api.GetCurrentThread(), win32con.TOKEN_IMPERSONATE, True)
    except pywintypes.error as e:
        if e.winerror != winerror.ERROR_NO_TOKEN:
            raise
        htoken_prev = None
    win32security.SetThreadToken(None, htoken)
    try:
        yield
    finally:
        win32security.SetThreadToken(None, htoken_prev)

def startupinfo_update(si_src, si_dst):
    for name in ('lpDesktop', 'lpTitle', 'dwX', 'dwY', 'dwXSize',
                 'dwYSize', 'dwXCountChars', 'dwYCountChars',
                 'dwFillAttribute', 'dwFlags', 'wShowWindow',
                 'hStdInput', 'hStdOutput', 'hStdError'):
        try:
            setattr(si_dst, name, getattr(si_src, name))
        except AttributeError:
            pass

main functions

def runas_session_user(cmd, executable=None, creationflags=0, cwd=None,
                       startupinfo=None, return_handles=False):
    if not creationflags & win32con.DETACHED_PROCESS:
        creationflags |= win32con.CREATE_NEW_CONSOLE
    if cwd is None:
        cwd = os.getcwd()
    si = win32process.STARTUPINFO()
    if startupinfo:
        startupinfo_update(startupinfo, si)
    with impersonate_system():
        htoken_user = win32ts.WTSQueryUserToken(
            win32ts.WTS_CURRENT_SESSION)
        hProcess, hThread, dwProcessId, dwThreadId = (
            win32process.CreateProcessAsUser(
                htoken_user, executable, cmd, None, None, False,
                creationflags, None, cwd, si))
    if return_handles:
        return hProcess, hThread
    return dwProcessId, dwThreadId

def runas_shell_user(cmd, executable=None, creationflags=0, cwd=None,
                     startupinfo=None, return_handles=False):
    if not creationflags & win32con.DETACHED_PROCESS:
        creationflags |= win32con.CREATE_NEW_CONSOLE
    if cwd is None:
        cwd = os.getcwd()
    si = STARTUPINFO()
    if startupinfo:
        startupinfo_update(startupinfo, si)
    pi = PROCESS_INFORMATION()
    try:
        htoken = duplicate_shell_token()
    except pywintypes.error as e:
        if e.winerror != winerror.ERROR_FILE_NOT_FOUND:
            raise
        return runas_session_user(cmd, executable, creationflags, cwd,
                    startupinfo, return_handles)
    with enable_privileges(win32security.SE_IMPERSONATE_NAME):
        if not advapi32.CreateProcessWithTokenW(
                    int(htoken), 0, executable, cmd, creationflags, None,
                    cwd, ctypes.byref(si), ctypes.byref(pi)):
            error = ctypes.get_last_error()
            raise pywintypes.error(
                error, 'CreateProcessWithTokenW',
                win32api.FormatMessageW(error))
    hProcess = pywintypes.HANDLE(pi.hProcess)
    hThread = pywintypes.HANDLE(pi.hThread)
    if return_handles:
        return hProcess, hThread
    return pi.dwProcessId, pi.dwThreadId
Eryk Sun
  • 33,190
  • 5
  • 92
  • 111
  • I wished I could just upvote this 30 times: well explained, friendly use... Thank you so much, @eryksun !!! – glezo Jan 11 '18 at 12:49
0

You could use something like:

import os
os.system('calc.exe')

OR if you want to use ctypes call the CreateProcess API function

from ctypes.wintypes import *
from ctypes import * 

kernel32 = WinDLL('kernel32', use_last_error=True)

PVOID  = c_void_p
LPVOID = PVOID
LPTSTR = c_void_p
LPBYTE = c_char_p
LPSECURITY_ATTRIBUTES = LPVOID

# Specifies the window station, desktop, standard handles, and appearance of the main window for a process at creation time.
                                                            # https://msdn.microsoft.com/en-us/library/windows/desktop/ms686331(v=vs.85).aspx
class STARTUPINFO(Structure):                              # typedef struct _STARTUPINFO
_fields_ = [                                               # {
           ('cb',               DWORD),                    # DWORD  cb;
           ('lpReserved',       LPTSTR),                   # LPTSTR lpReserved;
           ('lpDesktop',        LPTSTR),                   # LPTSTR lpDesktop;
           ('lpTitle',          LPTSTR),                   # LPTSTR lpTitle;
           ('dwX',              DWORD),                    # DWORD  dwX;
           ('dwY',              DWORD),                    # DWORD  dwY;
           ('dwXSize',          DWORD),                    # DWORD  dwXSize;
           ('dwYSize',          DWORD),                    # DWORD  dwYSize;
           ('dwXCountChars',    DWORD),                    # DWORD  dwXCountChars;
           ('dwYCountChars',    DWORD),                    # DWORD  dwYCountChars;
           ('dwFillAttribute',  DWORD),                    # DWORD  dwFillAttribute;
           ('dwFlags',          DWORD),                    # DWORD  dwFlags;
           ('wShowWindow',       WORD),                    # WORD   wShowWindow;
           ('cbReserved2',       WORD),                    # WORD   cbReserved2;
           ('lpReserved2',     LPBYTE),                    # LPBYTE lpReserved2;
           ('hStdInput',       HANDLE),                    # HANDLE hStdInput;
           ('hStdOutput',      HANDLE),                    # HANDLE hStdOutput;
           ('hStdError',       HANDLE)                     # HANDLE hStdError;
           ]                                               # }

# Contains information about a newly created process and its primary thread. It is used with the CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW, or CreateProcessWithTokenW function.
                                                            # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684873(v=vs.85).aspx
class PROCESS_INFORMATION(Structure):                      # typedef struct _PROCESS_INFORMATION
_fields_ = [                                               # {
           ("hProcess",    HANDLE),                        # HANDLE hProcess;
           ("hThread",     HANDLE),                        # HANDLE hThread;
           ("dwProcessId",  DWORD),                        # DWORD  dwProcessId;
           ("dwThreadId",   DWORD)                         # DWORD  dwThreadId;
           ]                                               # }


# Creates a new process and its primary thread. The new process runs in the security context of the calling process.
CreateProcess = kernel32.CreateProcessW                    # https://msdn.microsoft.com/en-us/library/windows/desktop/ms682425(v=vs.85).aspx
CreateProcess.restype = BOOL                               # BOOL WINAPI CreateProcess
CreateProcess.argtypes = [                                 # (
               LPCWSTR,                                    # LPCTSTR               lpApplicationName,
               LPWSTR,                                     # LPTSTR                lpCommandLine,
               LPSECURITY_ATTRIBUTES,                      # LPSECURITY_ATTRIBUTES lpProcessAttributes,
               LPSECURITY_ATTRIBUTES,                      # LPSECURITY_ATTRIBUTES lpThreadAttributes,
               BOOL,                                       # BOOL                  bInheritHandles,
               DWORD,                                      # DWORD                 dwCreationFlags,
               LPVOID,                                     # LPVOID                lpEnvironment,
               LPCWSTR,                                    # LPCTSTR               lpCurrentDirectory,
               POINTER(STARTUPINFO),                       # LPSTARTUPINFO         lpStartupInfo,
               POINTER(PROCESS_INFORMATION)                # LPPROCESS_INFORMATION lpProcessInformation
               ]

# Process creation flags | https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863(v=vs.85).aspx
CREATE_NEW_CONSOLE           = 0x00000010 # The new process has a new console, instead of inheriting its parent's console (the default).
EXTENDED_STARTUPINFO_PRESENT = 0x00080000 # The process is created with extended startup information; the lpStartupInfo parameter specifies a STARTUPINFOEX structure.

# Where lpApplicationName is the path of the executable 

CreateProcess(
               None,                                        # _In_opt_  lpApplicationName     The lpApplicationName parameter can be NULL. In that case, the module name must be the first white space–delimited token in the lpCommandLine string.
               u"C:\\Windows\\System32\\cmd.exe",           # _Inout_opt lpCommandLine        The command line to be executed  
               None,                                        # _In_opt_  pProcessAttributes    A pointer to a SECURITY_ATTRIBUTES structure that determines whether the returned handle to the new process object can be inherited by child processes. If lpProcessAttributes is NULL, the handle cannot be inherited.
               None,                                        # _In_opt_  lpThreadAttributes    A pointer to a SECURITY_ATTRIBUTES structure that determines whether the returned handle to the new thread object can be inherited by child processes. If lpThreadAttributes is NULL, the handle cannot be inherited.
               0,                                           # _In_      bInheritHandles       If this parameter is TRUE, each inheritable handle in the calling process is inherited by the new process. If the parameter is FALSE, the handles are not inherited. Note that inherited handles have the same value and access rights as the original handles.
               (CREATE_NEW_CONSOLE | EXTENDED_STARTUPINFO_PRESENT), #_In_dwCreationFlags       The flags that control the priority class and the creation of the process # To specify these attributes when creating a process, specify EXTENDED_STARTUPINFO_PRESENT in the dwCreationFlag parameter and a STARTUPINFOEX structure in the lpStartupInfo parameter
               None,                                        # _In_opt_  lpEnvironment         A pointer to the environment block for the new process. If this parameter is NULL, the new process uses the environment of the calling process.
               None,                                        # _In_opt_  lpCurrentDirectory    The full path to the current directory for the process. If this parameter is NULL, the new process will have the same current drive and directory as the calling process
               byref(lpStartupInfo),                        # _In_      lpStartupInfo         A pointer to a STARTUPINFO or STARTUPINFOEX structure.To set extended attributes, use a STARTUPINFOEX structure and specify EXTENDED_STARTUPINFO_PRESENT in the dwCreationFlags parameter.
               byref(lpProcessInformation))                 # _Out_     lpProcessInformation  A pointer to a PROCESS_INFORMATION structure that receives identification information about the new process.
Jongware
  • 22,200
  • 8
  • 54
  • 100
  • 1
    thanks for the effort taken on the copy paste, @Highsenberg, but: 1) First answer makes no sense, since os.system target will inherit the privileges of its parent. 2) Your second 'answer' is merely a copy-paste that doesn't clarify at all how to achieve my needs. Despite all this, thank you very much. – glezo Jan 01 '18 at 22:15