Я разрабатываю собственный установщик для своего приложения, поскольку мне нужно настроить некоторые специфические параметры, которые не позволяют InnoSetup и другие подобные инструменты. Установщику требуются права администратора для выполнения необходимых настроек. В конце установки запускается вновь установленное приложение.
Проблема в том, что это приложение также запускается с правами администратора, наследуя права установщика. Это происходит независимо от метода запуска приложения: CreateProcess
, CreateProcessAsUser
, ShellExecute
и даже устаревшего WinExec
.
Подводя итог, мой вопрос таков: из приложения, работающего с правами администратора, как я могу запустить другое приложение с «обычными» привилегиями вошедшего в систему пользователя Windows?
Интересный. Однако пользователь по-прежнему может запустить установщик с опцией «Запуск от имени администратора», и ошибка возникнет снова. Кроме того, для этого требуется, чтобы установщик имел более одного исполняемого файла, что тоже нехорошо.
Не обязательно иметь более одного файла. Вы можете запустить второй экземпляр того же исполняемого файла с помощью RunAs, а также передать специальный параметр запуска, который укажет этому второму экземпляру перейти непосредственно к выполнению конкретной задачи вместо запуска обычным способом. Затем вы просто наблюдаете из своего первого экземпляра, чтобы увидеть, когда второй экземпляр был закрыт, чтобы продолжить работу установщика.
У Рэймонда Чена есть статьи в блоге на эту тему:
Как я могу запустить процесс без повышенных прав из моего процесса с повышенными правами и наоборот?
TLDR; попросите экземпляр Explorer пользователя запустить приложение, используя IShellDispatch2.ShellExecute().
Как мне запустить процесс без повышенных прав из моего процесса с повышенными правами, redux
TLDR; попросите CreateProcess()
сделать экземпляр Explorer пользователя родительским для приложения, используя атрибут PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
.
Самый простой способ — открыть окно оболочки, получить идентификатор процесса и использовать этот процесс в качестве родительского процесса:
inline ULONG BOOL_TO_ERROR(BOOL f)
{
return f ? NOERROR : GetLastError();
}
ULONG RunFromShell(PCWSTR lpApplicationName, PWSTR lpCommandLine)
{
HWND hwnd = GetShellWindow();
if (!hwnd)
{
return ERROR_NOT_FOUND;
}
ULONG dwProcessId;
if (!GetWindowThreadProcessId(hwnd, &dwProcessId))
{
return GetLastError();
}
STARTUPINFOEXW si = {{ sizeof(si)}};
SIZE_T s = 0;
ULONG dwError;
while (ERROR_INSUFFICIENT_BUFFER == (dwError = BOOL_TO_ERROR(
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &s))))
{
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)alloca(s);
}
if (NOERROR == dwError)
{
if (HANDLE hProcess = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, dwProcessId))
{
if (NOERROR == (dwError = BOOL_TO_ERROR(UpdateProcThreadAttribute(
si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hProcess, sizeof(hProcess), 0, 0))))
{
PROCESS_INFORMATION pi;
if (NOERROR == (dwError = BOOL_TO_ERROR(CreateProcessW(lpApplicationName, lpCommandLine,
0, 0, 0, EXTENDED_STARTUPINFO_PRESENT, 0, 0, &si.StartupInfo, &pi))))
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
}
CloseHandle(hProcess);
}
}
return dwError;
}
Другой возможный способ, вероятно, более правильный, но требует больше кода — получить токен пользователя через WTSQueryUserToken()
, а затем использовать этот токен при вызове CreateProcessAsUser()
. Для этого вам нужно иметь SeTcbPrivilege
, SeAssignPrimaryTokenPrivilege
и SeIncreaseQuotaPrivilege
. Администратор может получить эти привилегии, но процесс перечисления открывает его токены один за другим, проверяет, существуют ли необходимые привилегии в токене, если они найдены - выдает себя за этот токен и после этого вызывает WTSQueryUserToken()
+ CreateProcessAsUser()
.
#define echo(x) x
#define label(x) echo(x)##__LINE__
#define BEGIN_PRIVILEGES(name, n) static const union { TOKEN_PRIVILEGES name;\
struct { ULONG PrivilegeCount; LUID_AND_ATTRIBUTES Privileges[n];} label(_) = { n, {
#define LAA(se) {{se}, SE_PRIVILEGE_ENABLED }
#define END_PRIVILEGES }};};
BEGIN_PRIVILEGES(tp_AssignTcb, 3)
LAA(SE_TCB_PRIVILEGE),
LAA(SE_ASSIGNPRIMARYTOKEN_PRIVILEGE),
LAA(SE_INCREASE_QUOTA_PRIVILEGE),
END_PRIVILEGES
NTSTATUS ImpersonateToken(_In_ const TOKEN_PRIVILEGES* RequiredSet);
NTSTATUS RtlRevertToSelf();
HRESULT RunAsUser(PCWSTR lpApplicationName, PWSTR lpCommandLine)
{
NTSTATUS status = ImpersonateToken(&tp_AssignTcb);
if (0 <= status)
{
HANDLE hToken;
//ProcessIdToSessionId(GetCurrentProcessId(), &SessionId);
if (NOERROR == (status = BOOL_TO_ERROR(WTSQueryUserToken(RtlGetCurrentPeb()->SessionId, &hToken))))
{
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi;
if (NOERROR == (status = BOOL_TO_ERROR(CreateProcessAsUserW(hToken,
lpApplicationName, lpCommandLine, 0, 0, 0, 0, 0, 0, &si, &pi))))
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
CloseHandle(hToken);
}
RtlRevertToSelf();
}
return status;
}
NTSTATUS RtlRevertToSelf()
{
HANDLE hToken = 0;
return NtSetInformationThread(NtCurrentThread(), ThreadImpersonationToken, &hToken, sizeof(hToken));
}
extern const SECURITY_QUALITY_OF_SERVICE sqos = {
sizeof (sqos), SecurityImpersonation, SECURITY_DYNAMIC_TRACKING, FALSE
};
extern const OBJECT_ATTRIBUTES oa_sqos = { sizeof(oa_sqos), 0, 0, 0, 0, const_cast<SECURITY_QUALITY_OF_SERVICE*>(&sqos) };
NTSTATUS ImpersonateToken(_In_ PVOID buf, _In_ const TOKEN_PRIVILEGES* RequiredSet)
{
NTSTATUS status;
union {
PVOID pv;
PBYTE pb;
PSYSTEM_PROCESS_INFORMATION pspi;
};
pv = buf;
ULONG NextEntryOffset = 0;
do
{
pb += NextEntryOffset;
HANDLE hProcess, hToken, hNewToken;
CLIENT_ID ClientId = { pspi->UniqueProcessId };
if (ClientId.UniqueProcess)
{
if (0 <= NtOpenProcess(&hProcess, PROCESS_QUERY_LIMITED_INFORMATION,
const_cast<POBJECT_ATTRIBUTES>(&oa_sqos), &ClientId))
{
status = NtOpenProcessToken(hProcess, TOKEN_DUPLICATE, &hToken);
NtClose(hProcess);
if (0 <= status)
{
status = NtDuplicateToken(hToken, TOKEN_ADJUST_PRIVILEGES|TOKEN_IMPERSONATE|TOKEN_QUERY,
const_cast<POBJECT_ATTRIBUTES>(&oa_sqos), FALSE, TokenImpersonation, &hNewToken);
NtClose(hToken);
if (0 <= status)
{
status = NtAdjustPrivilegesToken(hNewToken, FALSE, const_cast<PTOKEN_PRIVILEGES>(RequiredSet), 0, 0, 0);
if (STATUS_SUCCESS == status)
{
status = NtSetInformationThread(NtCurrentThread(), ThreadImpersonationToken, &hNewToken, sizeof(hNewToken));
}
NtClose(hNewToken);
if (STATUS_SUCCESS == status)
{
return STATUS_SUCCESS;
}
}
}
}
}
} while (NextEntryOffset = pspi->NextEntryOffset);
return STATUS_UNSUCCESSFUL;
}
NTSTATUS ImpersonateToken(_In_ const TOKEN_PRIVILEGES* RequiredSet)
{
NTSTATUS status;
ULONG cb = 0x40000;
do
{
status = STATUS_INSUFFICIENT_RESOURCES;
if (PBYTE buf = new BYTE[cb += 0x1000])
{
if (0 <= (status = NtQuerySystemInformation(SystemProcessInformation, buf, cb, &cb)))
{
status = ImpersonateToken(buf, RequiredSet);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{
status = STATUS_UNSUCCESSFUL;
}
}
delete [] buf;
}
} while(status == STATUS_INFO_LENGTH_MISMATCH);
return status;
}
Ваше первое предложение - не самый простой способ. SilverWarrior предложил еще более простой подход. Что касается вашего второго предложения, пользователь с правами администратора не может звонить WTSQueryUserToken()
, звонить может только системная учетная запись. Это документация даже об этом говорит.
Администратор @RemyLebeau, конечно, может, и я описываю, как - выдавать себя за токен, у которого есть TCB (и другие привилегии для CreateProcessAsUser). Я сам делаю это огромное количество
«Для успешного вызова этой функции вызывающее приложение должно работать в контексте учетной записи LocalSystem и иметь привилегию SE_TCB_NAME».
Документация @RemyLebeau неверна. Нужно только УТС. Или вы хотите сказать, что мой код неправильный и не работает?
Спасибо за ответы на все вопросы. Вот окончательный код на их основе в Delphi.
// reimport - Windows.pas has wrong args (Delphi 11)
function InitializeProcThreadAttributeList(lpAttributeList: PProcThreadAttributeList; dwAttributeCount, dwFlags: DWORD;
var lpSize: Cardinal): ByteBool; stdcall; external kernel32;
function UpdateProcThreadAttribute(lpAttributeList: PProcThreadAttributeList; dwFlags: DWORD; Attribute: Cardinal;
lpValue: Pointer; cbSize: Cardinal; lpPreviousValue: Pointer; lpReturnSize: PCardinal): ByteBool; stdcall;
external kernel32;
procedure DeleteProcThreadAttributeList(lpAttributeList: PProcThreadAttributeList); stdcall; external kernel32;
// not declared in Windows.pas
type
TStartupInfoEx = record
StartupInfo: TStartupInfo;
lpAttributeList: PProcThreadAttributeList;
end;
const
EXTENDED_STARTUPINFO_PRESENT = $00080000;
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = $00020000;
// get explorer.exe id
function GetProcessIDFromExplorer: DWORD;
var
Handle: THandle;
ProcEntry: TProcessEntry32;
begin
Result := 0;
Handle := CreateToolHelp32SnapShot(TH32CS_SNAPALL, 0);
ProcEntry.dwSize := SizeOf(TProcessEntry32);
Process32First(Handle, ProcEntry);
repeat
if SameText(ProcEntry.szExeFile, 'explorer.exe') then
Exit(ProcEntry.th32ProcessID);
until not Process32Next(Handle, ProcEntry);
CloseHandle(Handle);
end;
procedure StartApplication(const AExeName: string);
var
StartupInfo: TStartupInfo;
ProcessInformation: TProcessInformation;
Handle: THandle;
hProcess, Pid, Size: Cardinal;
StartupInfoEx: TStartupInfoEx;
begin
ZeroMemory(@StartupInfoEx, SizeOf(StartupInfoEx));
// explorer.exe handle
// according to @RbMm you can use the line below
// GetWindowThreadProcessId(GetShellWindow, Pid);
// but for some strange reason it's not working for me. So I used
Pid := GetProcessIDFromExplorer;
hProcess := OpenProcess(PROCESS_CREATE_PROCESS, False, Pid);
InitializeProcThreadAttributeList(nil, 1, 0, Size);
GetMem(StartupInfoEx.lpAttributeList, Size);
InitializeProcThreadAttributeList(StartupInfoEx.lpAttributeList, 1, 0, Size);
UpdateProcThreadAttribute(StartupInfoEx.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, @hProcess,
SizeOf(THandle), nil, nil);
with StartupInfoEx.StartupInfo do
begin
cb := SizeOf(TStartupInfoEx);
wShowWindow := SW_SHOWNORMAL;
end;
try
if not CreateProcess(PChar(AExeName), nil, nil, nil, False,
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT, nil, nil, StartupInfoEx.StartupInfo, ProcessInformation) then
RaiseLastOSError;
finally
DeleteProcThreadAttributeList(StartupInfoEx.lpAttributeList);
FreeMem(StartupInfoEx.lpAttributeList);
CloseHandle(hProcess);
end;
end;
Перечислять процессы и искать в проводнике по имени - это именно то, чего делать не нужно, чтобы получить его id. Также может быть несколько процессов проводников в случае нескольких сеансов rdp.
Я не думал об этом. Я также могу получить ParentId текущего установщика. Думаешь, это безопаснее?
GetShellWindow + GetWindowThreadProcessId
Попробовал, вернуло 0.
Что такое возврат 0? Это может быть только в том случае, если не запущен процесс оболочки (т.е. обычно проводник). Посмотрите на мои ответы. Я вставляю сюда даже два способа (второй, вероятно, обычно не нужен, опять же, только в случае отсутствия процесса оболочки), и мой код абсолютно корректен.
И хотя я не знаю Delphi, то, как вы вызываете GetWindowThreadProcessId, выглядит неправильно. Последний параметр, вероятно, должен быть @Pid
. И главное - вам нужно использовать отладчик для отладочного кода, а не ящики сообщений.
Я использовал окно сообщений, чтобы получить видимое значение для экрана печати.
использовать отладчик всегда лучше. и ваш код неверен, если вы получите pid, равный 0
GetWindowThreadProcessId вызывается правильно; в Delphi эта процедура имеет переменный параметр, что-то вроде «передача по ссылке».
Я получу родительский идентификатор из текущего процесса, он тоже будет работать. Спасибо за вашу помощь.
если я говорю, мне лучше знать. если GetWindowThreadProcessId
возвращает не 0 идентификатор потока, pid также не может быть 0.
Почему бы не разработать свой установщик так, чтобы он вначале запускался с обычными правами пользователя, а затем из этого установщика вы запускали другой, созданный с повышенными привилегиями, чтобы он мог делать необходимые вещи. Затем все, что вам нужно сделать, это дождаться, пока экземпляр с повышенными правами завершит свою работу, и, в конце концов, вы запустите приложение из своего экземпляра без повышенных прав. Многие монтажники работают именно так.