Windowsサービス内から、UACダイアログを表示せず昇格した権限を持つプロセスを起動することはできないだろうかと試してみました。結果、以下の条件の下で実際に可能であることを確認しました。

  • 自身(起動する側)Local Systemアカウントのもとで動くプロセスで実行
  • 対象となる管理者ユーザーはログオンしている

調査を始める前、以下の手順でできるのではないかと考えました。

  1. WTSQueryUserTokenで通常状態のトークンを取得。
  2. GetTokenInformationのTokenLinkedTokenで昇格状態のトークンを取得。
  3. 必要ならDuplicateTokenExでプライマリトークンを作成。
  4. そのトークンをCreateProcessAsUserに渡して起動。

やってみたところ、一応起動しました。ただ、ユーザープロファイルのディレクトリが認識されていないなどの挙動が見られたため、LoadUserProfileとCreateEnvironmentBlockの呼び出しを追加しました。

こちらが一応の完成版です。

// RunAsAdminTest.cpp
 
#define UNICODE
#define WINVER 0x0600
#define _WIN32_WINNT 0x0600
 
#include <iostream>
#include <memory>
#include <windows.h>
#include <wtsapi32.h>
#include <userenv.h>
 
#pragma comment(lib, "wtsapi32.lib")
#pragma comment(lib, "userenv.lib")
 
template<typename F>
class ScopeExitHolder
{
public:
  ScopeExitHolder(F f) : deleter(f) {}
 
  ~ScopeExitHolder()
  {
    deleter();
  }
  // VC++2013はムーブのdefault/deleteに非対応
  //ScopeExitHolder(ScopeExitHolder&&) = default;
  ScopeExitHolder(const ScopeExitHolder&) = default;
  //ScopeExitHolder& operator=(ScopeExitHolder&&) = delete;
  ScopeExitHolder& operator=(const ScopeExitHolder&) = delete;
 
private:
  F deleter;
 
};
enum MakeScopeExitTag {};
template<typename F>
inline ScopeExitHolder<F> operator*(MakeScopeExitTag, F&& f)
{
  return std::move(f);
}
 
void OutputError(const char* function, DWORD error)
{
  std::wcout << function << ": " << error << std::endl;
}
 
#define SCOPE_EXIT_ID2(n) ScopeExit ## n
#define SCOPE_EXIT_ID(n) SCOPE_EXIT_ID2(n)
#define SCOPE_EXIT auto SCOPE_EXIT_ID(__LINE__) \
  = MakeScopeExitTag() * [&]()
 
int wmain(int argc, wchar_t** argv)
{
  if (argc <= 1)
  {
    return 1;
  }
 
  try
  {
    WTS_SESSION_INFO* sessionInfo;
    DWORD count;
    if (!WTSEnumerateSessions(
      WTS_CURRENT_SERVER_HANDLE, 0, 1, &sessionInfo, &count))
    {
      OutputError("WTSEnumerateSessions", GetLastError());
    }
    SCOPE_EXIT{ WTSFreeMemory(sessionInfo); };
    for (DWORD i = 0; i < count; ++i)
    {
      if (sessionInfo[i].State != WTSActive)
      {
        continue;
      }
      if (sessionInfo[i].SessionId == 0)
      {
        break;
      }
      std::cout << "Session: " << sessionInfo[i].SessionId << std::endl;
      HANDLE hTokenBase = nullptr;
      if (!WTSQueryUserToken(sessionInfo[i].SessionId, &hTokenBase))
      {
        OutputError("WTSQueryUserToken", GetLastError());
      }
      SCOPE_EXIT{ CloseHandle(hTokenBase); };
 
      // 本当はここでGetTokenInformation(TokenElevationType)を使い、
      // 昇格済みトークンの取得処理が必要かどうか判断すべき。
 
      TOKEN_LINKED_TOKEN tlt = {};
      DWORD length = {};
      if (!GetTokenInformation(
        hTokenBase, TokenLinkedToken, &tlt, sizeof tlt, &length))
      {
        OutputError(
          "GetTokenInformation(TokenLinkedToken)", GetLastError());
      }
      SCOPE_EXIT{ CloseHandle(tlt.LinkedToken); };
 
      HANDLE hToken;
      if (!DuplicateTokenEx(tlt.LinkedToken, MAXIMUM_ALLOWED,
        nullptr, SecurityImpersonation, TokenPrimary, &hToken))
      {
        OutputError("DuplicateTokenEx", GetLastError());
      }
      SCOPE_EXIT{ CloseHandle(hToken); };
 
      DWORD tokenUserLength;
      if (!GetTokenInformation(hToken, TokenUser,
        nullptr, 0, &tokenUserLength)
        && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
      {
        OutputError("GetTokenInformation(TokenUser) 1", GetLastError());
      }
      std::unique_ptr<BYTE[]> buffer(new BYTE[tokenUserLength]);
      if (!GetTokenInformation(hToken, TokenUser,
        buffer.get(), tokenUserLength, &tokenUserLength))
      {
        OutputError("GetTokenInformation(TokenUser) 2", GetLastError());
      }
      auto tokenUser = reinterpret_cast<const TOKEN_USER*>(buffer.get());
 
      DWORD nameLength = 0;
      DWORD domainLength = 0;
      SID_NAME_USE nameUse;
      if (!LookupAccountSid(nullptr, tokenUser->User.Sid,
        nullptr, &nameLength, nullptr, &domainLength, &nameUse)
        && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
      {
        OutputError("LookupAccountSid 1", GetLastError());
      }
      std::unique_ptr<TCHAR[]> name(new TCHAR[nameLength]);
      std::unique_ptr<TCHAR[]> domain(new TCHAR[domainLength]);
      if (!LookupAccountSid(nullptr, tokenUser->User.Sid,
        name.get(), &nameLength, domain.get(), &domainLength, &nameUse))
      {
        OutputError("LookupAccountSid 2", GetLastError());
      }
 
      std::wcout << "User: " << name.get() << std::endl;
      std::wcout << "Domain: " << domain.get() << std::endl;
 
      // 移動プロファイルのときが不安
      PROFILEINFO profileInfo = { sizeof profileInfo };
      profileInfo.lpUserName = name.get();
      if (!LoadUserProfile(hToken, &profileInfo))
      {
        OutputError("LoadUserProfile", GetLastError());
      }
      SCOPE_EXIT{ UnloadUserProfile(hToken, profileInfo.hProfile); };
 
      void* environment;
      if (!CreateEnvironmentBlock(&environment, hToken, FALSE))
      {
        OutputError("CreateEnvironmentBlock", GetLastError());
      }
      SCOPE_EXIT{ DestroyEnvironmentBlock(environment); };
 
      STARTUPINFOW si = { sizeof si };
      PROCESS_INFORMATION pi = {};
      if (!CreateProcessAsUser(hToken, nullptr, argv[1],
        nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT,
        environment, nullptr, &si, &pi))
      {
        OutputError("CreateProcessAsUser", GetLastError());
      }
      CloseHandle(pi.hThread);
      WaitForSingleObject(pi.hProcess, INFINITE);
      CloseHandle(pi.hProcess);
 
      break;
    }
  }
  catch (const std::exception& e)
  {
    std::cerr << "std::exception: " << e.what() << std::endl;
  }
}

これをコンパイルしてRunAsAdminTest.exeを作ったら、以下のようにPsExecを使ってLocal Systemアカウントのもとで実行します。本物のサービスを作って動かすのは大変なので代用しています。1番目の引数にCreateProcessAsUserに渡すコマンドラインを指定します。

PsExec -s (絶対パス)RunAsAdminTest.exe C:\Windows\System32\notepad.exe

あるいは、以下のように新しいウィンドウでコマンドプロンプトを起動すると、タイトルバーが「管理者: C:\Windows\system32\cmd.exe」となるので分かりやすいでしょう。

PsExec -s (絶対パス)RunAsAdminTest.exe "cmd /c start cmd"

もちろん、ユーザーのいるセッション上で強力な権限を持つプロセスを起動したいだけなら、ほかにも方法はあります。ユーザーアカウントに拘らなければ、自身のプロセスのトークン(Local System)を使う方法もあります。別にUACダイアログを表示して昇格したプロセスが存在するなら、そのトークンを受け渡しする方法だって考えられます。

今回の方法は、自身がLocal Systemアカウントで動作している(必要な特権が使える)ことと対象のユーザーがログオンしていることだけを必要な条件としていることが特徴です。

やってみて「Local System強い、なんでもできる」という感想を改めて抱きました。

2015年5月5日追記:PsExecまわりの記述を修正しました。Windowsサービスの代用として使っているという説明を追加しました。


スポンサード リンク

この記事のカテゴリ

  • ⇒ 昇格した権限のプロセスをサービスから起動する
  • ⇒ 昇格した権限のプロセスをサービスから起動する