23 June, 2012

Запуск non-elevated процесса из под elevated

Неожиданно возникла проблема, что некоторые API в Windows 8 работают только из под non-elevated пользователя и только когда UAC включен. Соответственно встал вопрос как прикинуться non-elevated пользователем, если программа запущена под elevated пользователем. Сразу скажу, мы не знаем пользователький пароль.

К сожалению выяснилось, что поменять elevation у процесса нельзя. Можно только запустить новый с новыми elevation при помощи CreatePrcoesssAsUser() или CreatePrcoesssWithToken().

Были исследованы просторы интернета и к великому удивлению количество неработающих примеров зашкалило. Я думаю виной тому появление UAC в Windows Vista и то, что работало до перестало работать после:

  • Использование linked token не работает так как этот token имеет SECURITY_IMPERSONATION_LEVEL равным SecurityIdentification, что недостаточно для заруска дочернего процесса. Тебуется как минимум SecurityImpersonation.
  • Использование CreateRestrictedToken() не работает так как не меняется TokenElevationType. Наверное это бага в Windows, но он остается TokenElevationTypeFull вместо ожидаемого TokenElevationTypeLimited.
После долгих копаний был найден простой, но, к сожалению, не универсальный способ. Права доступа беруться у вашего Windows Explorer.
bool RerunAsReducedUserIfNeed(int * const pExitCode)
{
 DWORD elevationFlags;

 if (RtlQueryElevationFlags(&elevationFlags) != ERROR_SUCCESS)
  throw std::exception("Can't get elevation flags");

 if ((elevationFlags & ELEVATION_UAC_ENABLED) == 0)
  throw std::exception("UAC have to be enabled for metro-style applications");

 HandleHolder itselfToken;

 if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &itselfToken))
  throw _system_error("Can't open process token", GetLastError());

 if (GetElevationType(itselfToken.GetHandle()) != TokenElevationTypeFull)
  return false;

 HandleHolder const reducedUserHandle(GetReducedUserHandle());

 if (GetElevationType(reducedUserHandle.GetHandle()) == TokenElevationTypeFull)
  throw std::exception("Token type is still full");

 if (GetElevation(reducedUserHandle.GetHandle()))
  throw std::exception("Token is still elevated");

 STARTUPINFO si;

 memset(&si, 0, sizeof(si));
 si.cb = sizeof(si);

 PROCESS_INFORMATION pi;

 if (!CreateProcessWithTokenW(reducedUserHandle.GetHandle(), 0, nullptr, GetCommandLineW(), 0, nullptr, nullptr, &si, &pi))
  throw _system_error("Can't create child proccess", GetLastError());

 HandleHolder thread(pi.hThread);
 HandleHolder process(pi.hProcess);

 if (WaitForSingleObject(process.GetHandle(), INFINITE) != WAIT_OBJECT_0)
  throw std::exception("Failed to wait child process to be finished");

 if (pExitCode != nullptr)
  if (!GetExitCodeProcess(process.GetHandle(), reinterpret_cast(pExitCode)))
   throw _system_error("Can't get exit code for child process", GetLastError());

 return true;
}

static HANDLE GetReducedUserHandle()
{
 HWND const wnd = GetShellWindow();

 if (wnd == nullptr)
  throw std::exception("Can't find shell window");

 DWORD pid;
 
 GetWindowThreadProcessId(wnd, &pid);

 HandleHolder const shellProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);

 if (shellProcess.IsClosed())
  throw _system_error("Can't open shell process", GetLastError());

 HandleHolder shellToken;

 if (!OpenProcessToken(shellProcess.GetHandle(), TOKEN_DUPLICATE | TOKEN_QUERY, &shellToken))
  throw _system_error("Can't open shell process token", GetLastError());

 HandleHolder reducedUserHandle;

 if (!DuplicateTokenEx(shellToken.GetHandle(), MAXIMUM_ALLOWED, nullptr, SecurityImpersonation, TokenPrimary, &reducedUserHandle))
  throw _system_error("Can't duplicate shell token", GetLastError());

 return reducedUserHandle.Detach();
}
Вот реализация GetElevation() и GetElevationType():
static bool GetElevation(HANDLE const token)
{
 DWORD size;
 TOKEN_ELEVATION te;

 if (!GetTokenInformation(token, TokenElevation, &te, sizeof(te), &size))
  throw _system_error("Can't get token elevation", GetLastError());

 return te.TokenIsElevated != FALSE;
}

static TOKEN_ELEVATION_TYPE GetElevationType(HANDLE const token)
{
 DWORD size;
 TOKEN_ELEVATION_TYPE tet;

 if (!GetTokenInformation(token, TokenElevationType, &tet, sizeof(tet), &size))
  throw _system_error("Can't get token elevation type", GetLastError());

 return tet;
}
Вот реализация HandleHolder и _system_error для дотошных:
struct HandleHolder
{
 HandleHolder();
 HandleHolder(HANDLE handle);
 ~HandleHolder();

 void Close();

 operator HANDLE() const { return m_Handle; }
 HANDLE * operator &() { Close(); return &m_Handle; }

 bool operator==(HANDLE const value) const { return m_Handle == value; }
 bool operator!=(HANDLE const value) const { return m_Handle != value; }

 bool operator==(HandleHolder const & value) const { return m_Handle == value.m_Handle; }
 bool operator!=(HandleHolder const & value) const { return m_Handle != value.m_Handle; }

 void Attach(HANDLE const handle) { Close(); m_Handle = handle; }
 HANDLE Detach() { HANDLE const handle = m_Handle; m_Handle = NULL; return handle; }

private:
 HANDLE m_Handle;

 // Note: This object can't be copied
 HandleHolder(HandleHolder const & other);
 HandleHolder & operator=(HandleHolder const & other);
};

HandleHolder::HandleHolder() :
 m_Handle(NULL)
{
}

HandleHolder::HandleHolder(HANDLE const handle) :
 m_Handle(handle)
{
}

HandleHolder::~HandleHolder()
{
 if (m_Handle != NULL)
 {
  CloseHandle(m_Handle);
  m_Handle = NULL;
 }
}

void HandleHolder::Close()
{
 if (m_Handle != NULL)
 {
  if (!CloseHandle(m_Handle))
   throw _system_error("Can't close handle", GetLastError());

  m_Handle = NULL;
 }
}

struct _system_error : public std::exception
{
 _system_error(LPCSTR _What, DWORD _SystemError);

 DWORD const SystemError;
};

_system_error::_system_error(LPCSTR const _What, DWORD const _SystemError) :
 std::exception(_What),
 SystemError(_SystemError)
{
}

P.S. Критика только приветствуется!

P.P.S. Обнаружена интересная особенность вышеприведенного кода. Когда из под обычного пользователя не администратора запускается процесс администратора с полными правами, а из него метод RerunAsReducedUserIfNeed(), то GetShellWindow() возвращает HWND обычного пользователся!

No comments: