Запуск приложения с правами обычного пользователя из экземпляра с правами администратора

Я разрабатываю собственный установщик для своего приложения, поскольку мне нужно настроить некоторые специфические параметры, которые не позволяют InnoSetup и другие подобные инструменты. Установщику требуются права администратора для выполнения необходимых настроек. В конце установки запускается вновь установленное приложение.

Проблема в том, что это приложение также запускается с правами администратора, наследуя права установщика. Это происходит независимо от метода запуска приложения: CreateProcess, CreateProcessAsUser, ShellExecute и даже устаревшего WinExec.

Подводя итог, мой вопрос таков: из приложения, работающего с правами администратора, как я могу запустить другое приложение с «обычными» привилегиями вошедшего в систему пользователя Windows?

Почему бы не разработать свой установщик так, чтобы он вначале запускался с обычными правами пользователя, а затем из этого установщика вы запускали другой, созданный с повышенными привилегиями, чтобы он мог делать необходимые вещи. Затем все, что вам нужно сделать, это дождаться, пока экземпляр с повышенными правами завершит свою работу, и, в конце концов, вы запустите приложение из своего экземпляра без повышенных прав. Многие монтажники работают именно так.

SilverWarior 04.07.2024 08:54

Интересный. Однако пользователь по-прежнему может запустить установщик с опцией «Запуск от имени администратора», и ошибка возникнет снова. Кроме того, для этого требуется, чтобы установщик имел более одного исполняемого файла, что тоже нехорошо.

marciel.deg 09.07.2024 21:49

Не обязательно иметь более одного файла. Вы можете запустить второй экземпляр того же исполняемого файла с помощью RunAs, а также передать специальный параметр запуска, который укажет этому второму экземпляру перейти непосредственно к выполнению конкретной задачи вместо запуска обычным способом. Затем вы просто наблюдаете из своего первого экземпляра, чтобы увидеть, когда второй экземпляр был закрыт, чтобы продолжить работу установщика.

SilverWarior 10.07.2024 20:57
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
3
168
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

У Рэймонда Чена есть статьи в блоге на эту тему:

Как я могу запустить процесс без повышенных прав из моего процесса с повышенными правами и наоборот?

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(), звонить может только системная учетная запись. Это документация даже об этом говорит.

Remy Lebeau 04.07.2024 18:08

Администратор @RemyLebeau, конечно, может, и я описываю, как - выдавать себя за токен, у которого есть TCB (и другие привилегии для CreateProcessAsUser). Я сам делаю это огромное количество

RbMm 04.07.2024 18:30

«Для успешного вызова этой функции вызывающее приложение должно работать в контексте учетной записи LocalSystem и иметь привилегию SE_TCB_NAME».

Remy Lebeau 05.07.2024 00:12

Документация @RemyLebeau неверна. Нужно только УТС. Или вы хотите сказать, что мой код неправильный и не работает?

RbMm 05.07.2024 06:06

Спасибо за ответы на все вопросы. Вот окончательный код на их основе в 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.

RbMm 10.07.2024 22:15

Я не думал об этом. Я также могу получить ParentId текущего установщика. Думаешь, это безопаснее?

marciel.deg 10.07.2024 22:21

GetShellWindow + GetWindowThreadProcessId

RbMm 10.07.2024 22:36

Попробовал, вернуло 0.

marciel.deg 10.07.2024 22:44
ibb.co/mS17zF6
marciel.deg 10.07.2024 22:51

Что такое возврат 0? Это может быть только в том случае, если не запущен процесс оболочки (т.е. обычно проводник). Посмотрите на мои ответы. Я вставляю сюда даже два способа (второй, вероятно, обычно не нужен, опять же, только в случае отсутствия процесса оболочки), и мой код абсолютно корректен.

RbMm 10.07.2024 23:07

И хотя я не знаю Delphi, то, как вы вызываете GetWindowThreadProcessId, выглядит неправильно. Последний параметр, вероятно, должен быть @Pid. И главное - вам нужно использовать отладчик для отладочного кода, а не ящики сообщений.

RbMm 10.07.2024 23:17

Я использовал окно сообщений, чтобы получить видимое значение для экрана печати.

marciel.deg 11.07.2024 13:14

использовать отладчик всегда лучше. и ваш код неверен, если вы получите pid, равный 0

RbMm 11.07.2024 13:20

GetWindowThreadProcessId вызывается правильно; в Delphi эта процедура имеет переменный параметр, что-то вроде «передача по ссылке».

marciel.deg 11.07.2024 13:21

Я получу родительский идентификатор из текущего процесса, он тоже будет работать. Спасибо за вашу помощь.

marciel.deg 11.07.2024 13:23

если я говорю, мне лучше знать. если GetWindowThreadProcessId возвращает не 0 идентификатор потока, pid также не может быть 0.

RbMm 11.07.2024 13:28

Другие вопросы по теме