I've been looking into methods of starting an "unelevated" process from a UAC-elevated process, for use in a new installer for AutoHotkey_L. I got three different methods working (in different situations):
- fragman's function which utilizes Task Scheduler.
- Creating a restricted, medium integrity level token and passing it to CreateProcessAsUser, like in ABCza's script.
- Getting the shell to run the application on my behalf.
My test systems are:
- "VISTA": Windows Vista, with limited user account "User" and administrator "Admin".
- "EIGHT": Windows 8 x64 Release Preview, with an administrator user and UAC enabled.
1. Task Scheduler
This method did not work when the script was running from a network path with saved credentials. It worked on EIGHT after moving the script to the local drive. It did not work on VISTA, and the following was logged:
Side note: I guess the task name is "" because the task had expired and been deleted.
Changing it to use the currently logged on user rather than the user running the current process should make it work. This is actually what I want anyway; for instance, AutoHotkey.exe runs or creates AutoHotkey.ahk in %A_MyDocuments%, which would ideally be the logged on user's folder. I'm not sure how this would be done, and frankly, I'd rather not use Task Scheduler for this purpose.
2. CreateProcessAsUser
This worked on both systems, but on VISTA it ran the script as Admin (with reduced integrity level/privileges). As mentioned above, this is not suitable for my purpose.
3. Getting the shell to run an application for you
This is an ingenius method posted by blogger
BrandonLive. Because
ComObjCreate("Shell.Application")
creates an in-process object, it can't be used directly. Instead, you retrieve the
shell object of the process which hosts the desktop.
This method probably requires Explorer as the shell - support for custom shells is not essential for my usage. The program is always executed in the context of the process which hosts the desktop; i.e. the logged on user. This is exactly what I want.
/*
ShellRun by Lexikos
requires: AutoHotkey_L
license: http://creativecommons.org/publicdomain/zero/1.0/
Credit for explaining this method goes to BrandonLive:
http://brandonlive.com/2008/04/27/getting-the-shell-to-run-an-application-for-you-part-2-how/
Shell.ShellExecute(File [, Arguments, Directory, Operation, Show])
http://msdn.microsoft.com/en-us/library/windows/desktop/gg537745
*/
ShellRun(prms*)
{
shellWindows := ComObjCreate("{9BA05972-F6A8-11CF-A442-00A0C90A8F39}")
desktop := shellWindows.Item(ComObj(19, 8)) ; VT_UI4, SCW_DESKTOP
; Retrieve top-level browser object.
if ptlb := ComObjQuery(desktop
, "{4C96BE40-915C-11CF-99D3-00AA004AE837}" ; SID_STopLevelBrowser
, "{000214E2-0000-0000-C000-000000000046}") ; IID_IShellBrowser
{
; IShellBrowser.QueryActiveShellView -> IShellView
if DllCall(NumGet(NumGet(ptlb+0)+15*A_PtrSize), "ptr", ptlb, "ptr*", psv:=0) = 0
{
; Define IID_IDispatch.
VarSetCapacity(IID_IDispatch, 16)
NumPut(0x46000000000000C0, NumPut(0x20400, IID_IDispatch, "int64"), "int64")
; IShellView.GetItemObject -> IDispatch (object which implements IShellFolderViewDual)
DllCall(NumGet(NumGet(psv+0)+15*A_PtrSize), "ptr", psv
, "uint", 0, "ptr", &IID_IDispatch, "ptr*", pdisp:=0)
; Get Shell object.
shell := ComObj(9,pdisp,1).Application
; IShellDispatch2.ShellExecute
shell.ShellExecute(prms*)
ObjRelease(psv)
}
ObjRelease(ptlb)
}
}
Windows XP
As stated at the top of this post, my interest is only in reversing UAC elevation, so Windows XP is irrelevant. However, I was curious to see how it would behave on XP. As may be expected, it did not work:
- FindWindowSW's final parameter specifies the desktop window. This flag does not exist on XP, so the window is not found.
- If the current process was started via "Run As", ComObjCreate fails with a cryptic error message. If it is changed to
ComObjCreate("Shell.Application").Windows
, retrieval of the Windows property fails with "Class not registered".
For ease of cross-platform use, the function could be modified to call ComObjCreate("Shell.Application").ShellExecute(prms*)
in the event of a failure.
Focus Problems
I didn't have any problems. It's my understanding that the launched program is responsible for activating its window, and the OS is responsible for deciding when to prevent a program from "stealing" the focus.
I have two very simple solutions for this problem: a simple one that doesn't return a PID, and a more complex one that does. Both use the system shell like described above (so custom shells aren't supported), but they do it through simpler means. The simple one is to simply run the script as the argument of explorer.exe, like this:
Run explorer.exe Script.ahk
I've already tested it on my system and verified that it works. You don't even necessarily need to specify the full path of the script—it will search the working directory for it.
The second method assumes administrator status (for impersonation permission) and is more involved—it retrieves the user token from the shell (using GetShellWindow & DuplicateTokenEx), then runs the script with the token using CreateProcessWithTokenW (not CreateProcessAsUser). I've already written the function that implements this:
ShellRun(Prg, Args := "", wDir := "") {
static (StartupInfo, VarSetCapacity(StartupInfo, 6 * A_PtrSize + 44), NumPut(6 * A_PtrSize + 44, StartupInfo), VarSetCapacity(ProcessInfo, 16)), ShellToken := GetShellToken()
If A_IsAdmin {
DllCall("Advapi32\CreateProcessWithTokenW", "Ptr", ShellToken, "Int", 0, "Ptr", 0, "WStr", """" Prg """ " Args, "Int", 0, "Ptr", 0, wDir == "" ? "Ptr" : "WStr", wDir == "" ? 0 : wDir, "Str", StartupInfo, "Str", ProcessInfo)
return NumGet(ProcessInfo, A_PtrSize << 1, "UInt"), DllCall("CloseHandle", "Ptr", NumGet(ProcessInfo)), DllCall("CloseHandle", "Ptr", NumGet(ProcessInfo, A_PtrSize))
} Run "%Prg%" %Args%, %wDir%, UseErrorLevel, PID
return PID
}
GetShellToken() {
static Token
If Token
return Token
DllCall("GetWindowThreadProcessId", "Ptr", DllCall("GetShellWindow"), "UInt*", PID)
, Process := DllCall("OpenProcess", "UInt", 0x400, "Int", false, "UInt", PID, "Ptr")
, DllCall("Advapi32\OpenProcessToken", "Ptr", Process, "Int", 2, "Ptr*", ShellToken)
, DllCall("Advapi32\DuplicateTokenEx", "Ptr", ShellToken, "Int", 395, "Ptr", 0, "Int", 2, "Int", 1, "Ptr*", Token)
return Token, DllCall("CloseHandle", "Ptr", Process), DllCall("CloseHandle", "Ptr", ShellToken), OnExit(Func("CloseShellToken").Bind(Token))
}
CloseShellToken(Token) {
DllCall("CloseHandle", "Ptr", Token)
}
This implementation assumes a more recent version of AutoHotkey (to close the token when AutoHotkey exits, which may or may not be necessary, but I include it to be on the safe side.). It also falls back to Run command if the script isn't admin, since the method described above doesn't work if the script isn't admin. I've also tested this method and verified that it works on my Windows 7 Home Premium 32-bit system.
Well, there you have it. These are my two methods for starting a script (or anything else for that matter) normally from an administrator script. If you have any questions or comments, feel free to send me a message.
– SourceX