[PATCH v2 0/8] MR5118: server: Create processes using a limited administrator token by default.
Wine currently runs all processes with an elevated administrator token. As described in bug 40613, some applications refuse to be run with such a token. This patch set addresses the situation by creating all processes with an unelevated adminstrator token by default. When a process attempts to elevate itself through one of several mechanisms—which would show a UAC prompt on Windows—we instead silently elevate the process, as if the user had granted access through the UAC prompt. This works for almost all applications. I have found only one application which didn't get the memo, and actually asks the user to right click and run as administrator, namely PaintTool SAI. Fortunately, 063a377df4f, combined with the shell32 patch in this series, allows a Wine user to elevate that process by opening explorer.exe, right-clicking, and selecting Run as Administrator, just like on Windows. This patch series has been in Wine-Staging for about three years. This was partly to try to find and fix all the different creative ways that applications tried to elevate themselves, but mostly because I needed to find a solution for PaintTool SAI, and never quite got the time to implement run-as-administrator in shell32. -- v2: server: Create processes using a limited administrator token by default. winetest: Elevate test processes on Wine. ntdll: Always start the initial process through start.exe. ntdll: Elevate processes if requested in RtlCreateUserProcess(). kernelbase: Elevate processes if requested in CreateProcessInternal(). https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
From: Zebediah Figura <z.figura12(a)gmail.com> Like all other verbs, the actual command line template is specified in the registry. The elevation seems to be hardcoded into shell32 for this specific verb. The Foobar2000 installer requires administrator privileges, and elevates itself in this way. Based on a patch by Michael Müller. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=50727 --- dlls/shell32/shlexec.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/dlls/shell32/shlexec.c b/dlls/shell32/shlexec.c index eb9ca4a06df..f0dbb984103 100644 --- a/dlls/shell32/shlexec.c +++ b/dlls/shell32/shlexec.c @@ -293,6 +293,21 @@ static HRESULT SHELL_GetPathFromIDListForExecuteW(LPCITEMIDLIST pidl, LPWSTR psz return hr; } +static HANDLE get_admin_token(void) +{ + TOKEN_ELEVATION_TYPE type; + TOKEN_LINKED_TOKEN linked; + DWORD size; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenElevationType, &type, sizeof(type), &size) + || type == TokenElevationTypeFull) + return NULL; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenLinkedToken, &linked, sizeof(linked), &size)) + return NULL; + return linked.LinkedToken; +} + /************************************************************************* * SHELL_ExecuteW [Internal] * @@ -306,6 +321,7 @@ static UINT_PTR SHELL_ExecuteW(const WCHAR *lpCmd, WCHAR *env, BOOL shWait, UINT gcdret = 0; WCHAR curdir[MAX_PATH]; DWORD dwCreationFlags; + HANDLE token = NULL; TRACE("Execute %s from directory %s\n", debugstr_w(lpCmd), debugstr_w(psei->lpDirectory)); @@ -327,8 +343,12 @@ static UINT_PTR SHELL_ExecuteW(const WCHAR *lpCmd, WCHAR *env, BOOL shWait, dwCreationFlags = CREATE_UNICODE_ENVIRONMENT; if (!(psei->fMask & SEE_MASK_NO_CONSOLE)) dwCreationFlags |= CREATE_NEW_CONSOLE; - if (CreateProcessW(NULL, (LPWSTR)lpCmd, NULL, NULL, FALSE, dwCreationFlags, env, - NULL, &startup, &info)) + + if (psei->lpVerb && !wcsicmp(psei->lpVerb, L"runas")) + token = get_admin_token(); + + if (CreateProcessAsUserW(token, NULL, (LPWSTR)lpCmd, NULL, NULL, FALSE, + dwCreationFlags, env, NULL, &startup, &info)) { /* Give 30 seconds to the app to come up, if desired. Probably only needed when starting app immediately before making a DDE connection. */ @@ -348,6 +368,8 @@ static UINT_PTR SHELL_ExecuteW(const WCHAR *lpCmd, WCHAR *env, BOOL shWait, retval = ERROR_BAD_FORMAT; } + CloseHandle(token); + TRACE("returning %Iu\n", retval); psei_out->hInstApp = (HINSTANCE)retval; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
From: Zebediah Figura <z.figura12(a)gmail.com> This signifies that UAC is active. Foobar2000 checks this value, and won't even try to elevate itself otherwise. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=50727 --- loader/wine.inf.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loader/wine.inf.in b/loader/wine.inf.in index 35644cbd285..af40ca0260b 100644 --- a/loader/wine.inf.in +++ b/loader/wine.inf.in @@ -374,7 +374,7 @@ HKLM,%CurrentVersion%\Explorer\DriveIcons,,16 HKLM,%CurrentVersion%\Explorer\KindMap,,16 HKLM,%CurrentVersion%\Group Policy,,16 HKLM,%CurrentVersion%\Installer,"InstallerLocation",,"%11%" -HKLM,%CurrentVersion%\Policies\System,"EnableLUA",0x10003,0 +HKLM,%CurrentVersion%\Policies\System,"EnableLUA",0x10001,1 HKLM,%CurrentVersion%\PreviewHandlers,,16 HKLM,%CurrentVersion%\Run,,16 HKLM,%CurrentVersion%\Setup,"BootDir",,"%30%" -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
From: Zebediah Figura <z.figura12(a)gmail.com> Dragon Naturally Speaking 12.5 manually validates that the custom action server is elevated. One might imagine that the right approach here is to add a manifest to msiexec; however, msiexec does not always trigger a UAC prompt on Windows. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=51143 --- dlls/msi/custom.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/dlls/msi/custom.c b/dlls/msi/custom.c index d1e064f5b65..12a7c3c3676 100644 --- a/dlls/msi/custom.c +++ b/dlls/msi/custom.c @@ -573,12 +573,28 @@ UINT CDECL __wine_msi_call_dll_function(DWORD client_pid, const GUID *guid) return r; } +static HANDLE get_admin_token(void) +{ + TOKEN_ELEVATION_TYPE type; + TOKEN_LINKED_TOKEN linked; + DWORD size; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenElevationType, &type, sizeof(type), &size) + || type == TokenElevationTypeFull) + return NULL; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenLinkedToken, &linked, sizeof(linked), &size)) + return NULL; + return linked.LinkedToken; +} + static DWORD custom_start_server(MSIPACKAGE *package, DWORD arch) { WCHAR path[MAX_PATH], cmdline[MAX_PATH + 23]; PROCESS_INFORMATION pi = {0}; STARTUPINFOW si = {0}; WCHAR buffer[24]; + HANDLE token; void *cookie; HANDLE pipe; @@ -600,14 +616,18 @@ static DWORD custom_start_server(MSIPACKAGE *package, DWORD arch) lstrcatW(path, L"\\msiexec.exe"); swprintf(cmdline, ARRAY_SIZE(cmdline), L"%s -Embedding %d", path, GetCurrentProcessId()); + token = get_admin_token(); + if (is_wow64 && arch == SCS_64BIT_BINARY) { Wow64DisableWow64FsRedirection(&cookie); - CreateProcessW(path, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); + CreateProcessAsUserW(token, path, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); Wow64RevertWow64FsRedirection(cookie); } else - CreateProcessW(path, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); + CreateProcessAsUserW(token, path, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); + + if (token) CloseHandle(token); CloseHandle(pi.hThread); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
From: Zebediah Figura <z.figura12(a)gmail.com> Rufus 3.13 Portable requires administrator privileges, and uses a manifest to elevate itself. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=51000 --- dlls/kernelbase/process.c | 56 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/dlls/kernelbase/process.c b/dlls/kernelbase/process.c index 775512e7f0d..691e75b1291 100644 --- a/dlls/kernelbase/process.c +++ b/dlls/kernelbase/process.c @@ -28,6 +28,7 @@ #include "winnls.h" #include "wincontypes.h" #include "winternl.h" +#include "winuser.h" #include "kernelbase.h" #include "wine/debug.h" @@ -431,6 +432,53 @@ BOOL WINAPI DECLSPEC_HOTPATCH CloseHandle( HANDLE handle ) } +static BOOL image_needs_elevation( const WCHAR *path ) +{ + ACTIVATION_CONTEXT_RUN_LEVEL_INFORMATION run_level; + BOOL ret = FALSE; + HANDLE handle; + ACTCTXW ctx; + + ctx.cbSize = sizeof(ctx); + ctx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID; + ctx.lpSource = path; + ctx.lpResourceName = (const WCHAR *)CREATEPROCESS_MANIFEST_RESOURCE_ID; + + if (RtlCreateActivationContext( &handle, &ctx )) return FALSE; + + if (!RtlQueryInformationActivationContext( 0, handle, NULL, RunlevelInformationInActivationContext, + &run_level, sizeof(run_level), NULL )) + { + TRACE( "image requested run level %#x\n", run_level.RunLevel ); + if (run_level.RunLevel == ACTCTX_RUN_LEVEL_HIGHEST_AVAILABLE + || run_level.RunLevel == ACTCTX_RUN_LEVEL_REQUIRE_ADMIN) + ret = TRUE; + } + RtlReleaseActivationContext( handle ); + + return ret; +} + + +static HANDLE get_elevated_token(void) +{ + TOKEN_ELEVATION_TYPE type; + TOKEN_LINKED_TOKEN linked; + + if (NtQueryInformationToken( GetCurrentThreadEffectiveToken(), + TokenElevationType, &type, sizeof(type), NULL )) + return NULL; + + if (type == TokenElevationTypeFull) return NULL; + + if (NtQueryInformationToken( GetCurrentThreadEffectiveToken(), + TokenLinkedToken, &linked, sizeof(linked), NULL )) + return NULL; + + return linked.LinkedToken; +} + + /********************************************************************** * CreateProcessAsUserA (kernelbase.@) */ @@ -517,7 +565,7 @@ BOOL WINAPI DECLSPEC_HOTPATCH CreateProcessInternalW( HANDLE token, const WCHAR WCHAR *p, *tidy_cmdline = cmd_line; RTL_USER_PROCESS_PARAMETERS *params = NULL; RTL_USER_PROCESS_INFORMATION rtl_info; - HANDLE parent = 0, debug = 0; + HANDLE parent = 0, debug = 0, elevated_token = NULL; ULONG nt_flags = 0; USHORT machine = 0; NTSTATUS status; @@ -629,6 +677,9 @@ BOOL WINAPI DECLSPEC_HOTPATCH CreateProcessInternalW( HANDLE token, const WCHAR if (flags & CREATE_BREAKAWAY_FROM_JOB) nt_flags |= PROCESS_CREATE_FLAGS_BREAKAWAY; if (flags & CREATE_SUSPENDED) nt_flags |= PROCESS_CREATE_FLAGS_SUSPENDED; + if (!token && image_needs_elevation( params->ImagePathName.Buffer )) + token = elevated_token = get_elevated_token(); + status = create_nt_process( token, debug, process_attr, thread_attr, nt_flags, params, &rtl_info, parent, machine, handle_list, job_list ); switch (status) @@ -670,7 +721,8 @@ BOOL WINAPI DECLSPEC_HOTPATCH CreateProcessInternalW( HANDLE token, const WCHAR TRACE( "started process pid %04lx tid %04lx\n", info->dwProcessId, info->dwThreadId ); } - done: +done: + if (elevated_token) NtClose( elevated_token ); RtlDestroyProcessParameters( params ); if (tidy_cmdline != cmd_line) HeapFree( GetProcessHeap(), 0, tidy_cmdline ); return set_ntstatus( status ); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
From: Zebediah Figura <z.figura12(a)gmail.com> This is an ntdll port of the previous commit, although no known application depends on this behaviour from ntdll. --- dlls/ntdll/process.c | 79 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/dlls/ntdll/process.c b/dlls/ntdll/process.c index 102e0c930a9..d407b645bf0 100644 --- a/dlls/ntdll/process.c +++ b/dlls/ntdll/process.c @@ -37,6 +37,11 @@ #include "wine/exception.h" +/* we don't want to include winuser.h */ +#define CREATEPROCESS_MANIFEST_RESOURCE_ID ((ULONG_PTR)1) + +WINE_DEFAULT_DEBUG_CHANNEL(process); + /****************************************************************************** * RtlGetCurrentPeb [NTDLL.@] * @@ -89,6 +94,61 @@ NTSTATUS WINAPI RtlWow64EnableFsRedirectionEx( ULONG disable, ULONG *old_value ) } +static BOOL image_needs_elevation( const UNICODE_STRING *path ) +{ + ACTIVATION_CONTEXT_RUN_LEVEL_INFORMATION run_level; + UNICODE_STRING path0; + BOOL ret = FALSE; + HANDLE handle; + ACTCTXW ctx; + + if (RtlDuplicateUnicodeString( 1, path, &path0 )) + return FALSE; + + ctx.cbSize = sizeof(ctx); + ctx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID; + ctx.lpSource = path0.Buffer; + ctx.lpResourceName = (const WCHAR *)CREATEPROCESS_MANIFEST_RESOURCE_ID; + + if (RtlCreateActivationContext( &handle, &ctx )) + { + RtlFreeUnicodeString( &path0 ); + return FALSE; + } + + if (!RtlQueryInformationActivationContext( 0, handle, NULL, RunlevelInformationInActivationContext, + &run_level, sizeof(run_level), NULL )) + { + TRACE( "image requested run level %#x\n", run_level.RunLevel ); + if (run_level.RunLevel == ACTCTX_RUN_LEVEL_HIGHEST_AVAILABLE + || run_level.RunLevel == ACTCTX_RUN_LEVEL_REQUIRE_ADMIN) + ret = TRUE; + } + RtlReleaseActivationContext( handle ); + RtlFreeUnicodeString( &path0 ); + return ret; +} + + +static HANDLE get_elevated_token(void) +{ + TOKEN_ELEVATION_TYPE type; + TOKEN_LINKED_TOKEN linked; + + if (NtQueryInformationToken( GetCurrentThreadEffectiveToken(), + TokenElevationType, &type, sizeof(type), NULL )) + return NULL; + + if (type == TokenElevationTypeFull) return NULL; + + if (NtQueryInformationToken( GetCurrentThreadEffectiveToken(), + TokenLinkedToken, &linked, sizeof(linked), NULL )) + return NULL; + + return linked.LinkedToken; +} + + /********************************************************************** * RtlWow64GetCurrentMachine (NTDLL.@) */ @@ -462,8 +522,15 @@ NTSTATUS WINAPI RtlCreateUserProcess( UNICODE_STRING *path, ULONG attributes, PS_CREATE_INFO create_info; ULONG_PTR buffer[offsetof( PS_ATTRIBUTE_LIST, Attributes[6] ) / sizeof(ULONG_PTR)]; PS_ATTRIBUTE_LIST *attr = (PS_ATTRIBUTE_LIST *)buffer; + HANDLE elevated_token = NULL; + NTSTATUS status; UINT pos = 0; + /* It's not clear whether we should use path or ¶ms->ImagePathName here, + * but Roblox Player tries to pass an empty string for the latter. */ + if (!token && image_needs_elevation( path )) + token = elevated_token = get_elevated_token(); + RtlNormalizeProcessParams( params ); attr->Attributes[pos].Attribute = PS_ATTRIBUTE_IMAGE_NAME; @@ -510,11 +577,13 @@ NTSTATUS WINAPI RtlCreateUserProcess( UNICODE_STRING *path, ULONG attributes, InitializeObjectAttributes( &process_attr, NULL, 0, NULL, process_descr ); InitializeObjectAttributes( &thread_attr, NULL, 0, NULL, thread_descr ); - return NtCreateUserProcess( &info->Process, &info->Thread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, - &process_attr, &thread_attr, - inherit ? PROCESS_CREATE_FLAGS_INHERIT_HANDLES : 0, - THREAD_CREATE_FLAGS_CREATE_SUSPENDED, params, - &create_info, attr ); + status = NtCreateUserProcess( &info->Process, &info->Thread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, + &process_attr, &thread_attr, + inherit ? PROCESS_CREATE_FLAGS_INHERIT_HANDLES : 0, + THREAD_CREATE_FLAGS_CREATE_SUSPENDED, params, &create_info, attr ); + + if (elevated_token) NtClose( elevated_token ); + return status; } /*********************************************************************** -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
From: Zebediah Figura <z.figura12(a)gmail.com> Normally, when an application with a manifest is run, ntdll will notice and create the new process with an administrator token. However, if the process is launched directly from the loader, this code is never run. By launching all processes via start.exe, we make sure that the process is created through RtlCreateUserProcess() and thus elevated if necessary. The alternative to this patch would be to elevate the child from within ntdll.so or the server. However, either one would require parsing the manifest there, which would require copying a lot of code. This seems like a simpler approach, with no actual disadvantage. --- dlls/ntdll/unix/env.c | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/dlls/ntdll/unix/env.c b/dlls/ntdll/unix/env.c index ad9ab0dc220..427c577f2c9 100644 --- a/dlls/ntdll/unix/env.c +++ b/dlls/ntdll/unix/env.c @@ -1924,6 +1924,7 @@ static void init_peb( RTL_USER_PROCESS_PARAMETERS *params, void *module ) */ static RTL_USER_PROCESS_PARAMETERS *build_initial_params( void **module ) { + static const char *args[] = { "start.exe", "/exec" }; static const WCHAR valueW[] = {'1',0}; static const WCHAR pathW[] = {'P','A','T','H'}; RTL_USER_PROCESS_PARAMETERS *params = NULL; @@ -1952,29 +1953,8 @@ static RTL_USER_PROCESS_PARAMETERS *build_initial_params( void **module ) add_registry_environment( &env, &env_pos, &env_size ); env[env_pos++] = 0; - status = load_main_exe( NULL, main_argv[1], curdir, 0, &image, module ); - if (!status) - { - char *loader; - - if (main_image_info.ImageCharacteristics & IMAGE_FILE_DLL) status = STATUS_INVALID_IMAGE_FORMAT; - /* if we have to use a different loader, fall back to start.exe */ - if ((loader = get_alternate_wineloader( main_image_info.Machine ))) - { - free( loader ); - status = STATUS_INVALID_IMAGE_FORMAT; - } - } - - if (status) /* try launching it through start.exe */ - { - static const char *args[] = { "start.exe", "/exec" }; - free( image ); - if (*module) NtUnmapViewOfSection( GetCurrentProcess(), *module ); - load_start_exe( &image, module ); - prepend_argv( args, 2 ); - } - else rebuild_argv(); + load_start_exe( &image, module ); + prepend_argv( args, 2 ); main_wargv = build_wargv( get_dos_path( image )); cmdline = build_command_line( main_wargv ); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
From: Zebediah Figura <zfigura(a)codeweavers.com> --- programs/winetest/main.c | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/programs/winetest/main.c b/programs/winetest/main.c index a939034e8b9..dc4fc4b3581 100644 --- a/programs/winetest/main.c +++ b/programs/winetest/main.c @@ -687,6 +687,21 @@ extract_test (struct wine_test *test, const char *dir, LPSTR res_name) CloseHandle(hfile); } +static HANDLE get_admin_token(void) +{ + TOKEN_ELEVATION_TYPE type; + TOKEN_LINKED_TOKEN linked; + DWORD size; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenElevationType, &type, sizeof(type), &size) + || type == TokenElevationTypeFull) + return NULL; + + if (!GetTokenInformation(GetCurrentThreadEffectiveToken(), TokenLinkedToken, &linked, sizeof(linked), &size)) + return NULL; + return linked.LinkedToken; +} + static DWORD wait_process( HANDLE process, DWORD timeout ) { DWORD wait, diff = 0, start = GetTickCount(); @@ -722,6 +737,7 @@ run_ex (char *cmd, HANDLE out_file, const char *tempdir, DWORD ms, BOOL nocritic PROCESS_INFORMATION pi; DWORD wait, status, flags; UINT old_errmode; + HANDLE token = NULL; GetStartupInfoA (&si); si.dwFlags = STARTF_USESTDHANDLES; @@ -737,8 +753,16 @@ run_ex (char *cmd, HANDLE out_file, const char *tempdir, DWORD ms, BOOL nocritic else flags = CREATE_DEFAULT_ERROR_MODE; - if (!CreateProcessA (NULL, cmd, NULL, NULL, TRUE, flags, - NULL, tempdir, &si, &pi)) + /* Some tests cause a UAC prompt on Windows if the user is not elevated, + * so to allow them to run unattended the relevant test units need to skip + * the tests if so. + * + * However, we still want Wine to run as many tests as possible, so always + * elevate ourselves. On Wine elevation isn't interactive. */ + if (running_under_wine()) + token = get_admin_token(); + + if (!CreateProcessAsUserA(token, NULL, cmd, NULL, NULL, TRUE, flags, NULL, tempdir, &si, &pi)) { if (nocritical) SetErrorMode(old_errmode); if (pid) *pid = 0; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
From: Zebediah Figura <z.figura12(a)gmail.com> Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=40613 --- server/process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/process.c b/server/process.c index 3651696f505..e5dcff74794 100644 --- a/server/process.c +++ b/server/process.c @@ -710,7 +710,7 @@ struct process *create_process( int fd, struct process *parent, unsigned int fla if (!parent) { process->handles = alloc_handle_table( process, 0 ); - process->token = token_create_admin( TRUE, -1, TokenElevationTypeFull, default_session_id ); + process->token = token_create_admin( TRUE, -1, TokenElevationTypeLimited, default_session_id ); process->affinity = ~0; } else -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
This merge request was approved by Hans Leidekker. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/5118
participants (3)
-
Hans Leidekker (@hans) -
Zebediah Figura -
Zebediah Figura (@zfigura)