In a test framework process A has to start process B under different user credentials (say, _limited_user) using CreateProcessWithLogonW API. lpStartupInfo->lpDesktop is NULL, so process B is supposed to run in the same desktop and window station as process A.
Everything works fine when process A is started manually (as _glagolig). But when process A is started by the test framework service (running under designated test framework’s user account _test_framework) that does not work. CreateProcessWithLogonW returns success but process B is unable to do any work. It terminates right away apparently because its conhost.exe fails to initialize user32.dll and returns 0xC0000142 (I got that from SysInternals’ procmon.exe logs). So it looks like the problem is with desktop/window station access.
I would like to understand the root cause. It is not clear what makes test framework service’s desktop/window station objects different from those of a user that logged in manually.
Also I would like to find a workaround while keeping the overall scheme the same (test framework’s service under account _test_framework has to start process B under _limited_user).
Addendum: according to the documentation, it should be possible to use CreateProcessAsUser without going through these steps, provided you don't want the new process to interact with the user. I haven't tested this yet, but assuming it is true, that would be a much simpler solution for many scenarios.
It turns out that Microsoft has already provided sample code to manipulate window station and desktop access rights, under the title Starting an Interactive Client Process in C++. As of Windows Vista, starting a subprocess in the default window station is no longer enough to allow the subprocess to interact with the user, but it does allow the subprocess to run with alternate user credentials.
I should note that Microsoft's code uses LogonUser and CreateProcessAsUser rather than CreateProcessWithLogonW. This does mean that the service will need SE_INCREASE_QUOTA_NAME privilege and possibly SE_ASSIGNPRIMARYTOKEN_NAME. It may be preferable to substitute CreateProcessWithTokenW which only requires SE_IMPERSONATE_NAME. I don't recommend the use of CreateProcessWithLogonW in this context because it doesn't allow you to access the logon SID prior to launching the subprocess.
I wrote a minimal service to demonstrate using Microsoft's sample code:
/*******************************************************************/
#define _WIN32_WINNT 0x0501
#include <windows.h>
/*******************************************************************/
// See http://msdn.microsoft.com/en-us/library/windows/desktop/aa379608%28v=vs.85%29.aspx
// "Starting an Interactive Client Process in C++"
BOOL AddAceToWindowStation(HWINSTA hwinsta, PSID psid);
BOOL AddAceToDesktop(HDESK hdesk, PSID psid);
BOOL GetLogonSID (HANDLE hToken, PSID *ppsid);
VOID FreeLogonSID (PSID *ppsid);
BOOL StartInteractiveClientProcess (
LPTSTR lpszUsername, // client to log on
LPTSTR lpszDomain, // domain of client's account
LPTSTR lpszPassword, // client's password
LPTSTR lpCommandLine // command line to execute
);
/*******************************************************************/
const wchar_t displayname[] = L"Demo service for CreateProcessWithLogonW";
const wchar_t servicename[] = L"demosvc-createprocesswithlogonw";
DWORD dwWin32ExitCode = 0, dwServiceSpecificExitCode = 0;
/*******************************************************************/
#define EXCEPTION_USER 0xE0000000
#define FACILITY_USER_DEMOSVC 0x0001
#define EXCEPTION_USER_LINENUMBER (EXCEPTION_USER | (FACILITY_USER_DEMOSVC << 16))
HANDLE eventloghandle;
/*******************************************************************/
wchar_t subprocess_username[] = L"harry-test1";
wchar_t subprocess_domain[] = L"scms";
wchar_t subprocess_password[] = L"xyzzy916";
wchar_t subprocess_command[] = L"cmd.exe /c dir";
void demo(void)
{
if (!StartInteractiveClientProcess(subprocess_username, subprocess_domain, subprocess_password, subprocess_command))
{
const wchar_t * strings[] = {L"Creating subprocess failed."};
DWORD err = GetLastError();
ReportEventW(eventloghandle,
EVENTLOG_ERROR_TYPE,
0,
2,
NULL,
_countof(strings),
sizeof(err),
strings,
&err);
return;
}
{
const wchar_t * strings[] = {L"Creating subprocess succeeded!"};
ReportEventW(eventloghandle,
EVENTLOG_INFORMATION_TYPE,
0,
1,
NULL,
_countof(strings),
0,
strings,
NULL);
}
return;
}
/*******************************************************************/
CRITICAL_SECTION service_section;
SERVICE_STATUS service_status; // Protected by service_section
SERVICE_STATUS_HANDLE service_handle = 0; // Constant once set, so can be used from any thread
static DWORD WINAPI ServiceHandlerEx(DWORD control, DWORD eventtype, LPVOID lpEventData, LPVOID lpContext)
{
if (control == SERVICE_CONTROL_INTERROGATE)
{
EnterCriticalSection(&service_section);
if (service_status.dwCurrentState != SERVICE_STOPPED)
{
SetServiceStatus(service_handle, &service_status);
}
LeaveCriticalSection(&service_section);
return NO_ERROR;
}
return ERROR_CALL_NOT_IMPLEMENTED;
}
static VOID WINAPI ServiceMain(DWORD argc, LPTSTR * argv)
{
SERVICE_STATUS status;
EnterCriticalSection(&service_section);
service_handle = RegisterServiceCtrlHandlerEx(argv[0], ServiceHandlerEx, NULL);
if (!service_handle) RaiseException(EXCEPTION_USER_LINENUMBER | __LINE__, EXCEPTION_NONCONTINUABLE, 0, NULL);
service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
service_status.dwCurrentState = SERVICE_RUNNING;
service_status.dwControlsAccepted = 0;
service_status.dwWin32ExitCode = STILL_ACTIVE;
service_status.dwServiceSpecificExitCode = 0;
service_status.dwCheckPoint = 0;
service_status.dwWaitHint = 500;
SetServiceStatus(service_handle, &service_status);
LeaveCriticalSection(&service_section);
/************** service main function **************/
{
const wchar_t * strings[] = {L"Service started!"};
ReportEventW(eventloghandle,
EVENTLOG_INFORMATION_TYPE,
0,
2,
NULL,
_countof(strings),
0,
strings,
NULL);
}
demo();
/************** service shutdown **************/
EnterCriticalSection(&service_section);
status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
status.dwCurrentState = service_status.dwCurrentState = SERVICE_STOPPED;
status.dwControlsAccepted = 0;
status.dwCheckPoint = 0;
status.dwWaitHint = 500;
status.dwWin32ExitCode = dwWin32ExitCode;
status.dwServiceSpecificExitCode = dwServiceSpecificExitCode;
LeaveCriticalSection(&service_section);
SetServiceStatus(service_handle, &status); /* NB: SetServiceStatus does not return here if successful,
so any code after this point will not normally run. */
return;
}
int wmain(int argc, wchar_t * argv[])
{
const static SERVICE_TABLE_ENTRY servicetable[2] = {
{(wchar_t *)servicename, ServiceMain},
{NULL, NULL}
};
InitializeCriticalSection(&service_section);
eventloghandle = RegisterEventSource(NULL, displayname);
if (!eventloghandle) return GetLastError();
{
const wchar_t * strings[] = {L"Executable started!"};
ReportEventW(eventloghandle,
EVENTLOG_INFORMATION_TYPE,
0,
2,
NULL,
_countof(strings),
0,
strings,
NULL);
}
if (StartServiceCtrlDispatcher(servicetable)) return 0;
return GetLastError();
}
This must be linked with Microsoft's sample code. You can then install the service using the sc command:
sc create demosvc-createprocesswithlogonw binPath= c:\path\demosvc.exe DisplayName= "Demo service for CreateProcessWithLogonW"
I ended up with the following workaround. I configured a different service running as _limited_user and started on demand. Then the test framework can start and stop the limited user service. And the limited user service can run the processes required for my tests.
The workaround works. Hence my processes do not require interactive desktops (even though they load user32.dll). Apparently user32.dll can be loaded in non-interactive context. But there is some unknown subtlety that does not allow the process to run when started directly from test framework service using CreateProcessWithLogonW.
Related
My friend asked me to write a simple program that captures a fingerprint from the built-in reader on a computer and prints out some kind of identifier. I can choose operating system myself, like a laptop with Windows or Linux, or an Android phone.
I thought that would be simple, surely there are many API:s for this, and I noticed that Microsoft themselves actually provides an API for it. Since I can log into my win10 laptop with the fingerprint reader, I know that the reader works.
For some reason the example that Microsoft themselves provide in their documention does not work for me. https://learn.microsoft.com/en-us/windows/desktop/api/Winbio/nf-winbio-winbiocapturesample
I suppose that the people writing those pages forgot to mention some important aspect or step, perhaps there is a way to add permission somewhere in Visual Studio.
After rewriting and trying many things, at least I get ONE step further in the process, but it still fails.
Here is the current version
#include "pch.h"
#include <iostream>
#include "Windows.h"
#include "Stdio.h"
#include "Conio.h"
#include "Winbio.h"
HRESULT CaptureSample();
void capture(WINBIO_SESSION_HANDLE sessionHandle, int flag);
int main()
{
std::cout << "Hello World!\n";
HRESULT x = CaptureSample();
}
HRESULT CaptureSample()
{
HRESULT hr = S_OK;
WINBIO_SESSION_HANDLE sessionHandle = NULL;
WINBIO_REJECT_DETAIL rejectDetail = 0;
// Connect to the system pool.
hr = WinBioOpenSession(
WINBIO_TYPE_FINGERPRINT, // Service provider
WINBIO_POOL_SYSTEM, // Pool type
WINBIO_FLAG_DEFAULT, // Access: Capture raw data
NULL, // Array of biometric unit IDs
0, // Count of biometric unit IDs
WINBIO_DB_DEFAULT, // Default database
&sessionHandle // [out] Session handle
);
if (FAILED(hr))
{
wprintf_s(L"WinBioOpenSession failed. hr = 0x%x\n", hr);
goto e_Exit;
}
wprintf_s(L"Start my fingerprint capturing...\n");
capture(sessionHandle, WINBIO_DATA_FLAG_INTEGRITY);
capture(sessionHandle, WINBIO_DATA_FLAG_PRIVACY);
capture(sessionHandle, WINBIO_DATA_FLAG_SIGNED);
capture(sessionHandle, WINBIO_DATA_FLAG_OPTION_MASK_PRESENT);
capture(sessionHandle, WINBIO_DATA_FLAG_RAW);
capture(sessionHandle, WINBIO_DATA_FLAG_INTERMEDIATE);
capture(sessionHandle, WINBIO_DATA_FLAG_PROCESSED);
hr = WinBioEnrollCapture(sessionHandle, &rejectDetail);
wprintf_s(L"WinBioEnrollCapture hr=%x rejection = %d\n", hr, rejectDetail);
if (sessionHandle != NULL)
{
WinBioCloseSession(sessionHandle);
sessionHandle = NULL;
}
e_Exit:
wprintf_s(L"\n Press any key to exit...");
_getch();
return hr;
}
void capture(WINBIO_SESSION_HANDLE sessionHandle, int flag) {
WINBIO_UNIT_ID unitId = 0;
WINBIO_REJECT_DETAIL rejectDetail = 0;
PWINBIO_BIR sample = NULL;
SIZE_T sampleSize = 0;
wprintf_s(L"\n Calling WinBioCaptureSample. Flag = %d.\n", flag);
HRESULT hr = WinBioCaptureSample(
sessionHandle,
WINBIO_PURPOSE_IDENTIFY,
flag,
&unitId,
&sample,
&sampleSize,
&rejectDetail
);
if (FAILED(hr))
{
if (hr == WINBIO_E_BAD_CAPTURE)
{
wprintf_s(L"\n Bad capture; reason: %d\n", rejectDetail);
}
else if (hr == E_ACCESSDENIED)
{
wprintf_s(L"\n WinBioCaptureSample failed, access denied.");
}
else
{
wprintf_s(L"\n WinBioCaptureSample failed. hr = 0x%x\n", hr);
}
goto e_Exit;
}
wprintf_s(L"\n Swipe processed - Unit ID: %d\n", unitId);
wprintf_s(L"\n Captured %d bytes.\n", sampleSize);
e_Exit:
if (sample != NULL)
{
WinBioFree(sample);
sample = NULL;
}
}
and here is the result of running it:
Hello World!
Start my fingerprint capturing...
Calling WinBioCaptureSample. Flag = 1.
WinBioCaptureSample failed, access denied.
Calling WinBioCaptureSample. Flag = 2.
WinBioCaptureSample failed, access denied.
Calling WinBioCaptureSample. Flag = 4.
WinBioCaptureSample failed, access denied.
Calling WinBioCaptureSample. Flag = 8.
WinBioCaptureSample failed, access denied.
Calling WinBioCaptureSample. Flag = 32.
WinBioCaptureSample failed, access denied.
Calling WinBioCaptureSample. Flag = 64.
WinBioCaptureSample failed, access denied.
Calling WinBioCaptureSample. Flag = 128.
WinBioCaptureSample failed, access denied.WinBioEnrollCapture hr=8009802c rejection = 0
Press any key to exit...
What am I missing?
I think you should try using another API, though your objective is not clear here. You can check the CloudABIS™ which is superscalar, biometrics-as-a-service (BaaS) matching system for fast deployment at lower costs. The API is provided by M2SYS TEchnology and they have an vast array of hardware also.
I want to write a program that needs sometimes to start processes of another applications (mainly Sumatra PDF) on Windows 10, version 1803 (April 2018 Update).
These applications should be started on a specific monitor. I also want to be able to close the processes when needed.
The preferred languages are C# and Java, but any help is appreciated.
EDIT
I've tried to use the ShellExecuteExW function suggested by IInspectable in C++ code directly, but it doesn't work, as applications appear on the main monitor. I must have certainly made a mistake as I am absolutely new to WinAPI and know very little C++.
#include <Windows.h>
HMONITOR monitors[2]; // As it's only a test and I have currently only two monitors.
int monitorsCount = 0;
BOOL CALLBACK Monitorenumproc(HMONITOR hMonitor, HDC hdc, LPRECT lprect, LPARAM lparam)
{
monitors[monitorsCount] = hMonitor;
monitorsCount++;
return TRUE;
}
int main()
{
EnumDisplayMonitors(NULL, NULL, Monitorenumproc, 0);
_SHELLEXECUTEINFOW info;
ZeroMemory(&info, sizeof(info));
info.cbSize = sizeof(info);
info.fMask = SEE_MASK_HMONITOR;
//info.lpVerb = L"open";
info.lpFile = L"C:\\Windows\\System32\\cmd.exe";
info.nShow = SW_SHOW;
info.hMonitor = monitors[1]; // Trying to start on the second monitor.
ShellExecuteExW(&info);
return 0;
}
As suggested by others, this is intended behavior of Windows and for the good reasons.
Also, you can not rely on default window placement atleast for SumatraPDF, since it surely does not use CW_USEDEFAULT, and instead stores these values in :
%USERPROFILE%\AppData\Roaming\SumatraPDF\SumatraPDF-settings.txt
There are multiple options though:
Use third party tools that monitor top level windows and based on pre-configured rules moves them to specified display. E.g. DisplayFusion, etc.
Use ligher weight solutions like AutoHotkey/AutoIt.
Try and do this in code itself. Following is a working solution. I smoke tested on my box.
Disclaimer: I have not written the entire code, for saving time I pulled it up from couple of sources, tweaked it, glued it together, and tested with SumantraPDF. Do also note, that this code is not of highest standards, but solves your problem, and will act as a can-be-done example.
C++ code: (scroll down for C# code)
#include <Windows.h>
#include <vector>
// 0 based index for preferred monitor
static const int PREFERRED_MONITOR = 1;
struct ProcessWindowsInfo
{
DWORD ProcessID;
std::vector<HWND> Windows;
ProcessWindowsInfo(DWORD const AProcessID)
: ProcessID(AProcessID)
{
}
};
struct MonitorInfo
{
HMONITOR hMonitor;
RECT rect;
};
BOOL WINAPI EnumProcessWindowsProc(HWND hwnd, LPARAM lParam)
{
ProcessWindowsInfo *info = reinterpret_cast<ProcessWindowsInfo*>(lParam);
DWORD WindowProcessID;
GetWindowThreadProcessId(hwnd, &WindowProcessID);
if (WindowProcessID == info->ProcessID)
{
if (GetWindow(hwnd, GW_OWNER) == (HWND)0 && IsWindowVisible(hwnd))
{
info->Windows.push_back(hwnd);
}
}
return true;
}
BOOL CALLBACK Monitorenumproc(HMONITOR hMonitor, HDC hdc, LPRECT lprect, LPARAM lParam)
{
std::vector<MonitorInfo> *info = reinterpret_cast<std::vector<MonitorInfo>*>(lParam);
MonitorInfo monitorInfo = { 0 };
monitorInfo.hMonitor = hMonitor;
monitorInfo.rect = *lprect;
info->push_back(monitorInfo);
return TRUE;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
// NOTE: for now this code works only when the window is not already visible
// could be easily modified to terminate existing process as required
SHELLEXECUTEINFO info = { 0 };
info.cbSize = sizeof(info);
info.fMask = SEE_MASK_NOCLOSEPROCESS;
info.lpFile = L"C:\\Program Files\\SumatraPDF\\SumatraPDF.exe";
info.nShow = SW_SHOW;
std::vector<MonitorInfo> connectedMonitors;
// Get all available displays
EnumDisplayMonitors(NULL, NULL, Monitorenumproc, reinterpret_cast<LPARAM>(&connectedMonitors));
if (ShellExecuteEx(&info))
{
WaitForInputIdle(info.hProcess, INFINITE);
ProcessWindowsInfo Info(GetProcessId(info.hProcess));
// Go though all windows from that process
EnumWindows((WNDENUMPROC)EnumProcessWindowsProc, reinterpret_cast<LPARAM>(&Info.ProcessID));
if (Info.Windows.size() == 1)
{
// only if we got at most 1 window
// NOTE: applications can have more than 1 top level window. But at least for SumtraPDF this works!
if (connectedMonitors.size() >= PREFERRED_MONITOR)
{
// only move the window if we were able to successfully detect available monitors
SetWindowPos(Info.Windows.at(0), 0, connectedMonitors.at(PREFERRED_MONITOR).rect.left, connectedMonitors.at(PREFERRED_MONITOR).rect.top, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
}
}
CloseHandle(info.hProcess);
}
return 0;
}
To emphasize one of my comments in code. This code will only work if the process in question is not already running. You can tweak the code as per your requirements otherwise.
Update: Added C# code below, as I realized OP prefers C#. This code also has the termination logic cooked in.
C# code:
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags);
private const int SWP_NOSIZE = 0x0001;
private const int SWP_NOZORDER = 0x0004;
private const int PREFERRED_MONITOR = 1;
static void Main(string[] args)
{
// NOTE: you will have to reference System.Windows.Forms and System.Drawing (or
// equivalent WPF assemblies) for Screen and Rectangle
// Terminate existing SumatraPDF process, else we will not get the MainWindowHandle by following method.
List<Process> existingProcesses = Process.GetProcessesByName("SumatraPDF").ToList();
foreach (var existingProcess in existingProcesses)
{
// Ouch! Ruthlessly kill the existing SumatraPDF instances
existingProcess.Kill();
}
// Start the new instance of SumantraPDF
Process process = Process.Start(#"C:\Program Files\SumatraPDF\SumatraPDF.exe");
// wait max 5 seconds for process to be active
process.WaitForInputIdle(5000);
if (Screen.AllScreens.Length >= PREFERRED_MONITOR)
{
SetWindowPos(process.MainWindowHandle,
IntPtr.Zero,
Screen.AllScreens[PREFERRED_MONITOR].WorkingArea.Left,
Screen.AllScreens[PREFERRED_MONITOR].WorkingArea.Top,
0, 0, SWP_NOSIZE | SWP_NOZORDER);
}
}
SEE_MASK_HMONITOR is only a request. Applications can choose their own window placement. SEE_MASK_HMONITOR only works when the executed application relies on default window placement, i.e. it creates its first top-level window with CW_USEDEFAULT
So what you want is not generally possible. Your code is as good as you can get, if you don't control the launched application.
I'm running an automated test of an application that has insufficient dependencies (missing dll), and got this error message:
When the test runner kills the application, the above message box isn't disappearing. Apparently it's owned by csrss.exe, which can't be killed.
My problem is that the only way I can get this message box to close is by manually logging in to to the machine and clicking the X on that message window. Is there another way?
Note that I can easily fix the dependency error, but I would like a robust way of terminating the application under test with all its messages when such errors are encountered.
Try avoiding the dialog in the first place: Call SetErrorMode(SEM_FAILCRITICALERRORS) in your test runner, this will cause calls to CreateProcess to fail silently if the loader cannot resolve every import.
If you want to create a query function so you can tell if the process can load, add the CREATE_SUSPENDED flag when calling CreateProcess and call TerminateProcess if it succeeded.
you can do next:
1) get csrss.exe process id in your session
2) enumerate windows via EnumWindows, for every window query it process id (GetWindowThreadProcessId), compare it with csrss.exe process id. if equal - get window class name (GetClassName ) and if it is L"#32770" - post WM_CLOSE message to this window
3) if you want be more precise - after you found L"#32770" window class, query the window caption text via GetWindowText and ensure that it begin with "module.exe - " or how your exe is named ?
struct FIND_WND_CONTEXT
{
PCWSTR szCaptionBegin;
ULONG lenCaptionBegin;
ULONG dwProcessId;
};
BOOL CALLBACK EnumWndProc(HWND hwnd, FIND_WND_CONTEXT& fwc)
{
ULONG dwProcessId;
if (GetWindowThreadProcessId(hwnd, &dwProcessId) && dwProcessId == fwc.dwProcessId)
{
PWSTR Name = (PWSTR)alloca( max(fwc.lenCaptionBegin * sizeof(WCHAR), sizeof(L"#32770") + sizeof(WCHAR)) );
if (GetClassNameW(hwnd, Name, sizeof(L"#32770") + sizeof(WCHAR)) && !wcscmp(Name, L"#32770"))
{
if (GetWindowText(hwnd, Name, fwc.lenCaptionBegin))
{
_wcslwr(Name);
if (!wcscmp(Name, fwc.szCaptionBegin))
{
PostMessage(hwnd, WM_CLOSE, 0, 0);
}
}
}
}
return TRUE;
}
void CloseTest()
{
const WCHAR module_exe[] = L"module.exe - ";
FIND_WND_CONTEXT fwc = { module_exe, RTL_NUMBER_OF(module_exe) };
if (fwc.dwProcessId = GetMySessionCsrssId())
{
EnumWindows((WNDENUMPROC)EnumWndProc, (LPARAM)&fwc);
}
}
ULONG GetMySessionCsrssId()
{
ULONG cb = 0, rcb = 0x10000;
static volatile UCHAR guz;
PVOID stack = alloca(guz);
union {
PVOID buf;
PBYTE pb;
PSYSTEM_PROCESS_INFORMATION pspi;
};
ULONG SessionId;
ProcessIdToSessionId(GetCurrentProcessId(), &SessionId);
NTSTATUS status;
do
{
if (cb < rcb)
{
cb = RtlPointerToOffset(buf = alloca(rcb - cb), stack);
}
if (0 <= (status = ZwQuerySystemInformation(SystemProcessInformation, buf, cb, &rcb)))
{
ULONG NextEntryOffset = 0;
do
{
pb += NextEntryOffset;
STATIC_UNICODE_STRING(csrss, "csrss.exe");
if (pspi->SessionId == SessionId && RtlEqualUnicodeString(&pspi->ImageName, &csrss, TRUE))
{
return PtrToUlong(pspi->UniqueProcessId);
}
} while (NextEntryOffset = pspi->NextEntryOffset);
return 0;
}
} while (status == STATUS_INFO_LENGTH_MISMATCH);
return 0;
}
somebody of course ask - why use "undocumented", ("no longer available" (this is direct lie in msdn - available from win200 to latest win10 1703), "unsupported", etc) ZwQuerySystemInformation with SystemProcessInformation instead CreateToolhelp32Snapshot + Process32First + Process32Next ? because we need got SessionId information for process. the SYSTEM_PROCESS_INFORMATION containing ULONG SessionId member, while psapi shell over this function by unknown reason drop this member - PROCESSENTRY32 - no here SessionId - it dropped. of course we can retrieve SessionId by addition call to ProcessIdToSessionId, but this function internally open process by Id, for query it SessionId. but for open csrss.exe you need SeDebugPriviledge enabled in your toke (+ 3 extra call to kernel - open process, query information, close handle)
I have the following code (main part taken from MS SDK v7.1 Sample code, which demonstrates how to start an non-elevated process from an elevated one)-
The elevated and non-elevated process will be started on an (unsupervised) server, so user interaction is a no-go (except for the configuring part, if needed, of course).
The problem is, that the non-elevated process started through IShellDispatch2->ShellExecute is still elevated (expected was to be non-elevated).
This was confirmed by using IsUserAnAdmin() API.
Any ideas why the process created through ShellExecute() does have still elevated rights?
Some more relevant details: on my machine UAC is disabled.
The VS 2013 based application is manifested:
)
(manifesting can be disabled with /MANIFEST:no linker flag, if it will help in solving this).
I'm compiling it on Windows 7 x64 using VS 2013.
#include <shlwapi.h>
#include <shlobj.h>
#include <comutil.h>
#pragma comment(lib, "shlwapi.lib")
// link with (at least) OleAut32.lib shlwapi.lib comsupp.lib shell32.lib uuid.lib
// sorry for the bad formatting
int main(void)
{
std::wstring processName(L"myapp.exe"), processParams(L"-myAppParam");
LPWSTR processNamePtr = const_cast<LPWSTR>(processName.c_str());
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if (SUCCEEDED(hr))
{
IShellView *psv;
HRESULT hr = GetShellViewForDesktop(IID_PPV_ARGS(&psv));
if (SUCCEEDED(hr))
{
IShellDispatch2 *psd;
hr = GetShellDispatchFromView(psv, IID_PPV_ARGS(&psd));
if (SUCCEEDED(hr))
{
BSTR bstrProcessName = SysAllocString(processNamePtr);
hr = bstrProcessName ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr))
{
VARIANT vtEmpty = {}; // VT_EMPTY
LPWSTR processParamsPtr = const_cast<LPWSTR>(processParams.c_str());
_bstr_t bstrProcessParams(processParamsPtr);
VARIANT varParams;
varParams.vt = VT_BSTR;
varParams.bstrVal = bstrProcessParams;
char processDir[MAX_PATH + 1];
::GetCurrentDirectory(MAX_PATH, processDir);
_bstr_t bstrProcessDir(processDir);
VARIANT varProcessDir;
varProcessDir.vt = VT_BSTR;
varProcessDir.bstrVal = bstrProcessDir;
_bstr_t bstrOperation("open");
VARIANT varOperation;
varOperation.vt = VT_BSTR;
varOperation.bstrVal = bstrOperation;
hr = psd->ShellExecute(bstrProcessName,
varParams, // reinterpret_cast<_variant_t&>(bstrProcessParams),
varProcessDir,
varOperation,
vtEmpty);
SysFreeString(bstrProcessName);
SysFreeString(bstrProcessParams);
}
psd->Release();
}
psv->Release();
}
CoUninitialize();
}
} // main()
// use the shell view for the desktop using the shell windows automation to find the
// desktop web browser and then grabs its view
//
// returns:
// IShellView, IFolderView and related interfaces
HRESULT GetShellViewForDesktop(REFIID riid, void **ppv)
{
*ppv = NULL;
IShellWindows *psw;
HRESULT hr = CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&psw));
if (SUCCEEDED(hr))
{
HWND hwnd;
IDispatch* pdisp;
VARIANT vEmpty = {}; // VT_EMPTY
if (S_OK == psw->FindWindowSW(&vEmpty, &vEmpty, SWC_DESKTOP, (long*)&hwnd, SWFO_NEEDDISPATCH, &pdisp))
{
IShellBrowser *psb;
hr = IUnknown_QueryService(pdisp, SID_STopLevelBrowser, IID_PPV_ARGS(&psb));
if (SUCCEEDED(hr))
{
IShellView *psv;
hr = psb->QueryActiveShellView(&psv);
if (SUCCEEDED(hr))
{
hr = psv->QueryInterface(riid, ppv);
psv->Release();
}
psb->Release();
}
pdisp->Release();
}
else
{
hr = E_FAIL;
}
psw->Release();
}
return hr;
} // GetShellViewForDesktop()
// From a shell view object gets its automation interface and from that gets the shell
// application object that implements IShellDispatch2 and related interfaces.
HRESULT GetShellDispatchFromView(IShellView *psv, REFIID riid, void **ppv)
{
*ppv = NULL;
IDispatch *pdispBackground;
HRESULT hr = psv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&pdispBackground));
if (SUCCEEDED(hr))
{
IShellFolderViewDual *psfvd;
hr = pdispBackground->QueryInterface(IID_PPV_ARGS(&psfvd));
if (SUCCEEDED(hr))
{
IDispatch *pdisp;
hr = psfvd->get_Application(&pdisp);
if (SUCCEEDED(hr))
{
hr = pdisp->QueryInterface(riid, ppv);
pdisp->Release();
}
psfvd->Release();
}
pdispBackground->Release();
}
return hr;
} // GetShellDispatchFromView()
On my machine UAC is disabled.
Right there is your problem. By disabling UAC you stop the system from creating processes as standard user. If the logged on user is an administrator, then processes run with a fully privileged token.
With UAC disabled, the explorer process that runs the shell is started using the full token of the interactive user, which it seems is the token of an admin user. And so when the shell starts a new process with IShellDispatch2->ShellExecute that new process runs under the same user token.
You'll need to enable UAC to allow the system to create processes with standard user tokens. When you enable UAC, the shell's explorer process will run as standard user and so IShellDispatch2->ShellExecute will create new processes running as standard user.
I have a fairly complex requirement. My STA COM object is implemented in a DLL (can't move it to out-of-process EXE). By the means of DllSurrogate I am hosting my object in a dllhost.exe process. My object has an UI attached to it (a plain modeless dialog) but I need the PreTranslateAccelerator mechanism in order for some shortcuts to work, etc. Since COM activates my object and hosts it in the default dllhost.com, I am obviously not controlling the message pump.
Is there still a way to pre-translate messages in this scenario? I doubt COM has foreseen such a specific scenario but maybe I am missing something.
Okay here it is. I hope I didn't leave out anything important. Basically, I have created a custom CMyComCreator instead of the default one. Instead of just creating a COM object and returning an interface pointer, I spin a worker UiThread. I use MyData structure to pass data across threads. Once the worker thread has finished setting up, I use the CComGITPtr to transfer the marshalled interface pointer from the UiThread back to the main. The consumers (out-of-process) end up with interface pointers that talk directly to the UiThread bypassing the main thread. You may think of CMyDialog as a modeless dialog which sends a PostQuitMessage on destruction to terminate the message loop. That's all. May look cumbersome but it works good.
struct MyData
{
ATL::CComGITPtr<IUnknown> Unk;
ATL::CEvent Event;
HRESULT hr;
MyData() : hr(E_OUTOFMEMORY), Event(FALSE, FALSE) { }
};
static CMessageLoop * MessageLoop;
class CMyComCreator
{
public:
static HRESULT WINAPI CreateInstance(
_In_opt_ void* pv,
_In_ REFIID riid,
_COM_Outptr_ LPVOID* ppv)
{
ATLASSERT(ppv != NULL);
if (ppv == NULL)
return E_POINTER;
*ppv = NULL;
HRESULT hRes = E_OUTOFMEMORY;
MyData* data = NULL;
ATLPREFAST_SUPPRESS(6014 28197)
/* prefast noise VSW 489981 */
ATLTRY(data = _ATL_NEW MyData)
ATLPREFAST_UNSUPPRESS()
if (data != NULL)
{
HANDLE thread = (HANDLE)_beginthreadex(NULL, 0, UiThread, (void *)data, 0, NULL);
if (thread)
{
WaitForSingleObject(data->Event, INFINITE);
CloseHandle(thread);
hRes = data->hr;
if (SUCCEEDED(hRes))
{
ATL::CComPtr<IUnknown> unk;
hRes = data->Unk.CopyTo(&unk);
if (SUCCEEDED(hRes))
{
hRes = unk->QueryInterface(riid, ppv);
}
}
}
delete data;
}
return hRes;
}
};
typedef CMyComCreator _CreatorClass;
static unsigned __stdcall UiThread(void * param)
{
CoInitializeEx(0, COINIT_APARTMENTTHREADED);
MyData * data = (MyData *)param;
ATL::CComObject<CMyDialog> * bb;
data->hr = ATL::CComObject<CMyDialog>::CreateInstance(&bb);
ATL::CComPtr<IUnknown> unk((IDispatch *) bb);
data->Unk = unk;
unk.Release();
data->Event.Set();
if (SUCCEEDED(data->hr))
{
CMessageLoop theLoop;
MessageLoop = &theLoop;
int nRet = theLoop.Run();
MessageLoop = NULL;
}
CoUninitialize();
return 0;
}
I needed to pack everything in a single DLL.
In which case, DllSurrogate is not the only way of doing this. There's also Rundll32:
INFO: Windows Rundll and Rundll32 Interface
This would allow you to run your own message loop inside the DLL's EntryPoint and have complete control over message processing, including PreTranslateMessage. You can copy the message loop logic from an ATL EXE server.
Bear in mind, there's still 32-bit and 64-bit version of "RunDll32.exe" in every 64-bit Windows OS. Use the one which matches the bit-ness of your DLL.