前提
- Windowsサービスの実行ユーザーはSYSTEM
- サインイン中のユーザー名はreiwachan(管理者)
- 他にheiseikun(標準ユーザー)というユーザーがある
- 対話型サービスは考えないものとする
- エラーメッセージはGetErrorMessage関数で取得する
なお、GetErrorMessage関数の実装は、単にFormatMessageA関数でメッセージを取得する程度なので自分で調べて下さい。
きっかけ
私が作っているゲームやサービスは、ソフトウェアマネージャーを使ってファイルの整合性や更新の管理を行っていまして、そのサービスを制御するアプリケーションを作りたいって思ったのがきっかけでした
最初はタスクスケジューラーに登録すればいいって思ってたんですけど、なんかそれだと汚いなっていうのと、Windowsサービス側が起動してないのに制御アプリケーションだけ動いててもしょうがないよなという本当に些細なことがこのコードを書くきっかけになりました
CreateProcessでできないの?
結論から言えば、できません
CreateProcessは、親プロセスと同じ実行ユーザー、実行セッションで子プロセスを生成します
ですのでWindowsサービスからCreateProcessを呼ぶと、SYSTEMで、Servicesセッションで実行されるのでデスクトップにはウィンドウは表示しません。
登場人物
今回使う関数は、ゲーム制作等、他のユーザーの権限での実行というものを考えない人には全く馴染みのない関数たちになります。
- WTSGetActiveConsoleSessionId
- WTSQuerySessionInformation
- WTSQueryUserToken
- GetTokenInformation
- DuplicateTokenEx
- CreateProcessAsUser
そりゃ馴染み無いでしょうね。そもそもWindowsサービスじゃないと動かない関数とかもあるくらいですし
やること
ざっくりと流れを説明すると
- サインインしているユーザー(以下、アクティブユーザー)のユーザートークンを取得する
- ユーザートークンから当該アクティブユーザーにリンクしているトークン(以下、便宜上「リンクトークン」)を取得する
- ユーザートークンまたはリンクトークンからプライマリトークンを取得する
- プライマリトークンを使用してプロセスを生成する
先に言っておくと、WTSQuerySessionInformation
関数はこのフロー内では使わないですけど、最後に1個だけ用途を紹介しておきます。まあお遊び程度に使って下さい。
さて、紹介を始めていきましょうか
アクティブユーザーのユーザートークンを取得する
ここはすごく簡単です
役者は
- WTSGetActiveConsoleSessionId
- WTSQueryUserToken
の2つです。
DWORD SessionID = WTSGetActiveConsoleSessionId();
HANDLE UserToken = nullptr;
if (!WTSQueryUserToken(SessionID, &UserToken)) throw std::runtime_error(GetErrorMessage());
はい、これだけです。
え?拍子抜け?
いや、だって本当にこれだけなんだもん。
ユーザートークンからリンクトークンを取得する
さっきので拍子抜けとか思ったそこのあなた、ここも拍子抜けだからね
役者はGetTokenInformation
関数だけです。
HANDLE LinkedToken = nullptr;
DWORD dwSize = sizeof(LinkedToken);
if (!GetTokenInformation(UserToken, TokenLinkedToken, &LinkedToken, dwSize, &dwSize)) throw std::runtime_error(GetErrorMessage());
はい、おしまい。これでリンクトークン取得完了です
プライマリトークンを取得する
ここはコード自体は長くはないんだけど、引数間違えるだけで管理者権限で実行されるか標準ユーザー権限で実行されるかが変わってしまいます。
ここで登場するのがDuplicateTokenEx
関数です。
ここまでで2つのトークンを取得してますね。何を取得したか忘れたとは言わせないよ。すぐ上に書いてあるんだから。
ちなみにこいつの定義はこんな感じです。
(前略)
WINADVAPI
BOOL
WINAPI
DuplicateTokenEx(
_In_ HANDLE hExistingToken,
_In_ DWORD dwDesiredAccess,
_In_opt_ LPSECURITY_ATTRIBUTES lpTokenAttributes,
_In_ SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
_In_ TOKEN_TYPE TokenType,
_Outptr_ PHANDLE phNewToken
);
(後略)
どの引数が変わるか分かりました?
そうです。第1引数です。
私たちの手元には今、ユーザートークンとリンクトークンの2つがあります。
第1引数にはどちらかのトークンを渡すことになります。
そして、最後の引数からプライマリトークンが吐き出されます。
このプライマリトークンは、ユーザートークンを渡すかリンクトークンを渡すかで最後にCreateProcessAsUserで生成されるプロセス自体に差が生まれます。
具体的に言うと、管理者特権を持っているか持っていないかです。
標準ユーザー権限のプロセスを生成するためのトークンを取得する
標準ユーザー権限のプロセスを生成する時は、ユーザートークンを使用します。
HANDLE PrimaryToken = nullptr;
if (!DuplicateTokenEx(UserToken, TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, TokenPrimary, &PrimaryToken)) throw std::runtime_error(GetErrorMessage());
管理者権限のプロセスを生成するためのトークンを取得する
管理者権限のプロセスを生成する時は、リンクトークンを使用します。
HANDLE PrimaryToken = nullptr;
if (!DuplicateTokenEx(LinkedToken, TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, TokenPrimary, &PrimaryToken)) throw std::runtime_error(GetErrorMessage());
プロセスを生成する
さあ、役者は揃いました。プロセスを生成しましょう。
といってもやることは至ってシンプルです。
だって引数の先頭にプライマリトークン渡すだけであとはCreateProcessと同じですから
え?はよコード載せろって?せっかちさんだなぁ…
EnvironmentBlock Env{};
DWORD dwCreationFlag = CREATE_NEW_CONSOLE | NORMAL_PRIORITY_CLASS;
if (EnvironmentBlock::Create(Env, Token.Get())) dwCreationFlag |= CREATE_UNICODE_ENVIRONMENT;
else if (Env) Env = nullptr;
STARTUPINFOA SI{ sizeof(STARTUPINFOA) };
PROCESS_INFORMATION PI{};
if (!CreateProcessAsUserA(
Token.Get(),
Application.string().c_str(),
CommandLine.data(),
nullptr,
nullptr,
FALSE,
dwCreationFlag,
Env.Get(),
WorkingDirectory.empty() ? Application.parent_path().string().c_str() : WorkingDirectory.c_str(),
&SI, &PI
)) {
const DWORD dwErrorCode = GetLastError();
SafeCloseHandle(PI.hThread);
SafeCloseHandle(PI.hProcess);
throw std::runtime_error(GetErrorMessage(dwErrorCode));
}
1行目のEnvironmentBlock
の定義は下の通りです。
#include <Windows.h>
#include <UserEnv.h>
class EnvironmentBlock {
private:
LPVOID Env;
public:
EnvironmentBlock() : EnvironmentBlock(nullptr) {}
EnvironmentBlock(std::nullptr_t) : Env(nullptr) {}
~EnvironmentBlock() {
if (this->Env != nullptr) {
DestroyEnvironmentBlock(this->Env);
this->Env = nullptr;
}
}
operator bool() const noexcept { return this->Env != nullptr; }
LPVOID Get() const noexcept { return this->Env; }
static bool Create(EnvironmentBlock& Block, const HANDLE& Token, const bool bInherit = true) {
return CreateEnvironmentBlock(&Block.Env, Token, bInherit ? TRUE : FALSE);
}
};
これだけだけど私、そんなに難しいことしてないでしょ?
プロセスの終了を待機するならハンドルをWaitForSingleObject
関数に突っ込むだけだからまあそこはお好みで
おまけ
プロセス生成の方法の紹介は以上になります。
ただ、アクティブユーザーが誰なのかってのを知りたい場面ってのもありますよね?
「GetUserName
関数を使えばいいんでしょ?」と思ったそこのあなた、試してみな。欲しい値とは違う値が取れるから
じゃあどうすんだよって?ここで活躍するのがWTSQuerySessionInformation
関数です。
その前にWTSQuerySessionInformation
関数は、中でバッファのアロケーションがあるので
TCHAR* lptszNameBuf = nullptr;
DWORD dwSize{};
if (!WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, SessionID, WTSUserName, &lpNameBuf, &dwSize)) throw std::runtime_error(GetErrorMessage());
// lptszNameBufを使ったコードを何か書く
WTSFreeMemory(lptszNameBuf);
第2引数のSessionIDは、アクティブユーザーのトークンを取得する部分の1行目の変数です。
第3引数は、WtsApi32.h
に次のように定義されています。
(前略)
typedef enum _WTS_INFO_CLASS {
WTSInitialProgram,
WTSApplicationName,
WTSWorkingDirectory,
WTSOEMId,
WTSSessionId,
WTSUserName,
WTSWinStationName,
WTSDomainName,
WTSConnectState,
WTSClientBuildNumber,
WTSClientName,
WTSClientDirectory,
WTSClientProductId,
WTSClientHardwareId,
WTSClientAddress,
WTSClientDisplay,
WTSClientProtocolType,
WTSIdleTime,
WTSLogonTime,
WTSIncomingBytes,
WTSOutgoingBytes,
WTSIncomingFrames,
WTSOutgoingFrames,
WTSClientInfo,
WTSSessionInfo,
WTSSessionInfoEx,
WTSConfigInfo,
WTSValidationInfo,
WTSSessionAddressV4,
WTSIsRemoteSession
} WTS_INFO_CLASS;
(後略)
まあ見て分かる通り、セッションIDからいろんな値が取れるみたいですね。
全部やってみるのも面白いけど大変だから私は絶対やらん。
ユーザー名が取れてしまえば、ユーザーに関する情報引っ張れるAPIはいくらでもあるので自分で調べて色々やってみて下さい。
最後に
試してみて分かったんですけど、一部の関数はWindowsサービスでなくても動かせるものもあります。
ただ、こんなの使わなくてももっとシンプルにできるAPIもあるので、Windowsサービスを作るわけじゃないのでしたら安易にこのコードを採用するのは推奨できません。闇にどっぷり浸かることになるので
あと、今回コード上ではハンドルを閉じるコードは省略してますが、実際にコードを書く際は必ず閉じて下さい。