Windowsの偽装トークンを正しく扱う ── スレッド単位の権限借用と安全な戻し方

· · Windows, Security, AccessToken, Impersonation, Win32, .NET, CSharp, 運用, 既存資産活用

Windowsの偽装トークンを正しく扱う ── スレッド単位の権限借用と安全な戻し方

1. 最初に押さえるべきこと

Windows アプリケーションや Windows サービスを作っていると、「この処理だけ、別のユーザーとして実行したい」という場面があります。

たとえば、次のような場面です。

  • Windows サービスから、利用者本人の権限でファイルサーバーへアクセスしたい
  • 管理用アプリケーションで、特定のユーザーに見える範囲だけを確認したい
  • Named Pipe、RPC、COM、IIS、ASP.NET Core などで、呼び出し元ユーザーの権限で一部の処理を行いたい
  • 既存資産の都合で、処理単位ごとに利用する Windows アカウントを切り替えたい

このときに出てくるのが、偽装アクセストークン偽装トークンです。

ただし、ここで最初に強調しておきたいことがあります。

Windows の偽装は、「管理者になる魔法」ではありません。アクセスチェックに使われるセキュリティコンテキストを、主にスレッド単位で切り替える仕組みです。

この違いを押さえないまま実装すると、次のような問題が起きます。

  • 偽装したつもりなのに、ファイルアクセスが Access denied になる
  • ローカルのファイルは読めるのに、ネットワーク共有だけ失敗する
  • Task.Runasync の途中で、いつの間にか元のユーザーに戻っている
  • 偽装したままログ出力や後続処理まで動いてしまい、権限の境界があいまいになる
  • プライマリトークンと偽装トークンを混同し、プロセス起動で失敗する
  • 「ユーザーが Administrators に入っているのに、なぜ書き込めないのか」で詰まる

この記事では、Windows の偽装トークンを、実務で安全に扱うための考え方を整理します。

攻撃手法や権限奪取の話ではありません。 Windows アプリケーション、Windows サービス、.NET アプリケーションで、正しく権限境界を扱うための話です。

2. アクセストークンとは何か

Windows では、ユーザーやプロセスのセキュリティコンテキストを表すために アクセストークン が使われます。

アクセストークンには、ざっくり言うと次のような情報が含まれます。

  • ユーザーの SID
  • 所属グループ
  • 権限、Privilege
  • 既定の所有者
  • 既定の DACL
  • 制限 SID
  • 整合性レベル
  • 昇格状態
  • 偽装レベル
  • トークン種別

ファイル、レジストリ、サービス、名前付きパイプ、プロセス、スレッド、イベント、ミューテックスなど、Windows の多くのオブジェクトはセキュリティ記述子を持ちます。

あるスレッドが保護対象のオブジェクトを開こうとしたとき、Windows はトークンの情報と、対象オブジェクトの ACL を照合します。

操作するスレッド
  ↓
どのセキュリティコンテキストでアクセスするか
  ↓
トークンのユーザー、グループ、権限を見る
  ↓
対象オブジェクトのACLと照合する
  ↓
許可 / 拒否を決める

この「どのセキュリティコンテキストでアクセスするか」を理解することが、偽装トークンを理解する入口です。

3. プロセスにはプライマリトークンがある

Windows の各プロセスには、通常 プライマリアクセストークン があります。

たとえば、あるユーザーがデスクトップからアプリケーションを起動すると、そのプロセスにはそのユーザーのセキュリティコンテキストを表すプライマリトークンが付きます。

Windows サービスであれば、サービスの実行アカウントのプライマリトークンが付きます。

たとえば、次のようなイメージです。

MyService.exe
  Primary Token: DOMAIN\svc-app

このサービス内のスレッドが、特に偽装していなければ、ファイルやレジストリへアクセスするときにはプロセスのプライマリトークンが使われます。

つまり、既定ではこうです。

Thread A
  Impersonation Token: なし
  ↓
アクセスチェックでは Process の Primary Token を使う

この状態で C:\Data\foo.txt を開くと、DOMAIN\svc-app に対してアクセス権があるかどうかが見られます。

4. 偽装トークンはスレッドに付く

偽装が始まると、スレッドには 偽装トークン が付きます。

ここが重要です。

偽装は、基本的に「プロセス全体が別ユーザーになる」というより、そのスレッドが別のセキュリティコンテキストでアクセスチェックを受けると考える方が正確です。

MyService.exe
  Primary Token: DOMAIN\svc-app

Thread A
  Impersonation Token: DOMAIN\alice

Thread B
  Impersonation Token: なし

このとき、Thread A がファイルを開くと、DOMAIN\alice の権限でアクセスチェックされます。

一方、Thread B は偽装していないので、DOMAIN\svc-app の権限でアクセスチェックされます。

この違いを理解していないと、次のような混乱が起きます。

// Thread A で偽装したつもり
StartImpersonation(token);

// しかし、別スレッドに処理を投げている
Task.Run(() =>
{
    File.ReadAllText(path);
});

// すぐ戻してしまう
RevertToSelf();

この場合、実際にファイルを読むスレッドが、期待した偽装状態で動くとは限りません。

偽装は、スコープ、スレッド、非同期処理との関係を明確にして扱う必要があります。

5. 「偽装」は権限昇格ではない

偽装という言葉は少し強く聞こえます。

しかし、実務で大事なのは、偽装を「権限昇格」と混同しないことです。

偽装でできることは、基本的には次のようなことです。

サーバープロセスの権限で処理する
  ↓
一部の処理だけ、クライアントユーザーの権限でアクセスチェックする

たとえば、ファイルサーバーの ACL をそのまま権限制御として使いたい場合、サーバーアプリケーションが常にサービスアカウントでファイルを読むと、利用者ごとの ACL を反映できません。

そこで、リクエスト処理の一部だけ呼び出し元ユーザーとして偽装し、ファイルアクセスを行います。

HTTP / RPC / Named Pipe リクエスト
  User: DOMAIN\alice
      ↓
サーバーアプリケーション
  Process: DOMAIN\svc-app
      ↓
ファイルアクセス部分だけ DOMAIN\alice として偽装
      ↓
ファイルサーバーのACLで許可 / 拒否される

これは、アプリケーション独自の権限判定ではなく、Windows の既存 ACL を使いたいときに役立ちます。

ただし、偽装は便利な一方、設計を誤ると権限境界が分かりにくくなります。

  • どの処理を誰として実行しているのか
  • どこで偽装を開始したのか
  • どこで確実に戻しているのか
  • どのログがどのユーザー権限で出ているのか
  • 例外時にも戻るのか
  • 非同期処理の最後まで偽装が有効なのか

このあたりをコードで明確にすることが重要です。

6. プライマリトークンと偽装トークンを分けて考える

Windows のトークンで特に混同しやすいのが、プライマリトークン偽装トークン です。

大まかには、次のように考えます。

トークン 主な用途 代表例
プライマリトークン プロセスのセキュリティコンテキストを表す プロセス起動、CreateProcessAsUser
偽装トークン スレッドが別のセキュリティコンテキストで動くために使う ImpersonateLoggedOnUserSetThreadToken、Named Pipe のクライアント偽装

特に重要なのは、プロセスを起動したい場合は、原則としてプライマリトークンが必要という点です。

偽装トークンを持っているからといって、そのまま別ユーザーのプロセス起動に使えるとは限りません。

典型的には、次のような流れになります。

クライアントを偽装する
  ↓
OpenThreadToken で偽装トークンを取得する
  ↓
DuplicateTokenEx でプライマリトークンを作る
  ↓
CreateProcessAsUser などに渡す

逆に、「このスレッドでファイルアクセスだけ別ユーザーとして行いたい」という場合は、プロセス起動ではなく偽装トークンを使う話になります。

この2つを混ぜると、API の引数は合っているのに Access deniedThe parameter is incorrect のようなエラーで迷うことになります。

7. 偽装レベルを理解する

偽装トークンには、偽装レベル があります。

代表的には次の4段階です。

偽装レベル 意味の目安
Anonymous サーバーはクライアントの識別情報を得られない
Identification サーバーはクライアントを識別できるが、その権限でオブジェクトへアクセスする用途には使えない
Impersonation サーバーはローカルシステム上でクライアントの権限として動作できる
Delegation サーバーはリモートシステムに対してもクライアントの権限を委任できる

実務でよく詰まるのは、IdentificationImpersonation の違いです。

Identification は名前のとおり、相手が誰かを知るためのレベルです。 そのユーザーの権限でファイルを開くような用途には不十分です。

そのため、次のようなことが起きます。

WindowsIdentity.GetCurrent().Name は期待したユーザー名に見える
  ↓
でもファイルアクセスは Access denied になる

この場合、名前だけを見るのではなく、偽装レベルも確認する必要があります。

.NET であれば、WindowsIdentity.ImpersonationLevel を見ると手がかりになります。

using System.Security.Principal;

WindowsIdentity identity = WindowsIdentity.GetCurrent();

Console.WriteLine(identity.Name);
Console.WriteLine(identity.ImpersonationLevel);

ネットワーク越しのアクセスでは、さらに注意が必要です。

「Web サーバーでユーザーを偽装して、そのユーザーとして別のファイルサーバーや DB サーバーへアクセスする」ような構成では、いわゆるダブルホップ問題にぶつかることがあります。

この場合、単にアプリケーションコードで偽装すれば解決するとは限りません。 Kerberos、SPN、委任、制約付き委任、サービスアカウント、接続先の認証方式などを含めて設計する必要があります。

8. 偽装の基本形

Win32 API で偽装を扱うときの概念的な形は、次のようになります。

1. 偽装に使うトークンを取得する
2. そのトークンで現在のスレッドを偽装する
3. 必要な処理だけ実行する
4. 必ず元のセキュリティコンテキストに戻す
5. トークンハンドルを閉じる

コードの形としては、必ず try / finally にします。

if (!ImpersonateLoggedOnUser(tokenHandle))
{
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

try
{
    // ここだけ偽装ユーザーとして実行する
    DoWorkAsImpersonatedUser();
}
finally
{
    if (!RevertToSelf())
    {
        // 戻せない状態は危険なので、少なくとも処理継続してはいけない
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

大事なのは、偽装の開始よりも、確実に戻すことです。

戻し忘れると、そのスレッドで後続処理が偽装ユーザーのまま動きます。

特にスレッドプールを使うアプリケーションでは、1つの処理のつもりだったものが、別のリクエストや別の処理へ影響する可能性があります。

そのため、偽装は「始めたら戻す」ではなく、小さなスコープに閉じ込めると考えるべきです。

9. .NET では WindowsIdentity.RunImpersonated を使う

.NET では、可能であれば WindowsIdentity.RunImpersonated を使うと、偽装スコープをコード上で表現しやすくなります。

SafeAccessTokenHandle を持っている場合、次のように書けます。

using Microsoft.Win32.SafeHandles;
using System.Security.Principal;

static string ReadFileAsUser(SafeAccessTokenHandle token, string path)
{
    return WindowsIdentity.RunImpersonated(token, () =>
    {
        return File.ReadAllText(path);
    });
}

この形の良いところは、偽装している範囲がラムダ式の中に閉じていることです。

RunImpersonated(token, () =>
{
    // ここだけ偽装
});

// ここから外は元のコンテキスト

ファイルアクセス、レジストリアクセス、既存ライブラリ呼び出しなど、特定の処理だけを偽装したい場合は、この形が読みやすく安全です。

非同期処理では、RunImpersonatedAsync を使います。

using Microsoft.Win32.SafeHandles;
using System.Security.Principal;

static Task WriteFileAsUserAsync(
    SafeAccessTokenHandle token,
    string path,
    string text,
    CancellationToken cancellationToken)
{
    return WindowsIdentity.RunImpersonatedAsync(token, async () =>
    {
        await File.WriteAllTextAsync(path, text, cancellationToken);
    });
}

避けたいのは、偽装スコープの内側から fire-and-forget のタスクを投げる形です。

// 良くない例
WindowsIdentity.RunImpersonated(token, () =>
{
    _ = Task.Run(() =>
    {
        File.WriteAllText(path, text);
    });
});

このコードは、「実際にファイルを書く処理」がいつ、どの実行コンテキストで動くのか分かりにくくなります。

偽装して行いたい非同期処理は、RunImpersonatedAsync の中で await し、処理が完了してからスコープを出る形にします。

10. LogonUser でトークンを得る場合

別ユーザーの資格情報からトークンを得る代表的な API に LogonUser があります。

ただし、これは慎重に扱うべき API です。

LogonUser にはユーザー名、ドメイン、パスワードを渡します。 つまり、アプリケーション側が資格情報を扱うことになります。

実務では、次の点に注意します。

  • パスワードをコードや設定ファイルに平文で置かない
  • できれば OS の認証、サービスアカウント、委任、既存の Windows 認証を使う
  • 秘密情報は適切な Secret Store や運用基盤で管理する
  • トークンハンドルは必ず閉じる
  • ログにユーザー名以外の秘密情報を出さない
  • 偽装する範囲を最小化する

最小構成の例は次のようになります。

using Microsoft.Win32.SafeHandles;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;

internal static class NativeMethods
{
    private const int LOGON32_LOGON_INTERACTIVE = 2;
    private const int LOGON32_PROVIDER_DEFAULT = 0;

    [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern bool LogonUser(
        string lpszUsername,
        string? lpszDomain,
        string lpszPassword,
        int dwLogonType,
        int dwLogonProvider,
        out SafeAccessTokenHandle phToken);

    public static SafeAccessTokenHandle Logon(
        string userName,
        string? domain,
        string password)
    {
        bool ok = LogonUser(
            userName,
            domain,
            password,
            LOGON32_LOGON_INTERACTIVE,
            LOGON32_PROVIDER_DEFAULT,
            out SafeAccessTokenHandle token);

        if (!ok)
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        return token;
    }
}

public static string ReadFileWithExplicitCredential(
    string userName,
    string? domain,
    string password,
    string path)
{
    using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);

    return WindowsIdentity.RunImpersonated(token, () =>
    {
        return File.ReadAllText(path);
    });
}

この例は、あくまで API の形を示すためのものです。

実務では、「そもそもアプリケーションがパスワードを受け取る設計でよいのか」を先に検討します。

多くの場合、次のような代替案を考えた方が安全です。

やりたいこと 代替案
サービス全体が特定リソースへアクセスしたい 専用サービスアカウントに必要最小限の ACL を付ける
利用者本人の権限でファイルにアクセスしたい Windows 認証と委任設計を使う
管理者権限が必要な操作をしたい サービス側に明確な管理 API を設け、アプリ側の認可で制御する
一部の処理だけ別アカウントにしたい 偽装範囲をメソッド単位に閉じ、監査ログを残す

11. LogonUser のログオン種別に注意する

LogonUser で得られるトークンの性質は、ログオン種別によって変わります。

特に、次のような違いを理解せずにコピペすると、期待どおりに動きません。

ログオン種別 注意点
Interactive 対話ログオンに近い形。ローカル操作に使いやすいが、実行環境や権限に依存する
Network ネットワークログオン向け。返るトークンがプロセス起動に直接使えない場合がある
NewCredentials ローカルでは現在の資格情報に近く、リモート接続時に指定資格情報を使う用途で使われることがある

ここで言いたいのは、特定のログオン種別を暗記することではありません。

大事なのは、次の2点です。

  1. ログオン種別によって、ローカルアクセス、ネットワークアクセス、プロセス起動での挙動が変わる
  2. 返ってきたトークンが、プライマリトークンなのか、偽装トークンなのかを確認する必要がある

たとえば、LOGON32_LOGON_NETWORK で得たトークンを、そのまま CreateProcessAsUser に渡して失敗する、というのは典型的な混乱です。

プロセス起動が目的なら、プライマリトークンが必要です。 必要に応じて DuplicateTokenEx でプライマリトークンを作る設計になります。

12. RevertToSelf を軽く見ない

Win32 API で偽装を開始した場合、RevertToSelf で偽装を終了します。

この戻し処理は、単なる後片付けではありません。

セキュリティ境界を元に戻すための重要な処理です。

悪い例です。

ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();

一見問題なさそうですが、DoWork() で例外が発生したら、RevertToSelf() が呼ばれません。

必ず finally にします。

if (!ImpersonateLoggedOnUser(token))
{
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

try
{
    DoWork();
}
finally
{
    if (!RevertToSelf())
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

RevertToSelf が失敗した場合に、そのまま処理を続けるのも危険です。

元の権限に戻せていないかもしれない状態で後続処理を続けると、意図しないユーザーの権限で処理が続きます。

少なくとも、その処理単位は失敗として扱い、安全側に倒す必要があります。

.NET の RunImpersonated / RunImpersonatedAsync は、この戻し忘れを避けるためのスコープ表現として有用です。

13. 偽装スコープはできるだけ小さくする

偽装で一番大事な設計原則は、必要な処理だけを偽装することです。

悪い例です。

WindowsIdentity.RunImpersonated(token, () =>
{
    ValidateRequest();
    LoadConfiguration();
    WriteDebugLog();
    ReadUserFile();
    UpdateDatabase();
    SendNotification();
});

このように広い範囲を偽装すると、どの操作がどの権限で行われるのか分かりにくくなります。

たとえば、ログ出力先へのアクセスが偽装ユーザーの権限で行われ、ログ書き込みに失敗するかもしれません。 DB 接続が、サービスアカウントではなく偽装ユーザーの資格情報で試みられるかもしれません。 通知処理や一時ファイル作成まで、不要な権限コンテキストの影響を受けるかもしれません。

良い例は、偽装が必要な操作だけを切り出す形です。

ValidateRequest();
LoadConfiguration();

string content = WindowsIdentity.RunImpersonated(token, () =>
{
    return File.ReadAllText(userFilePath);
});

UpdateDatabase(content);
WriteAuditLog(userName, userFilePath, success: true);

この形なら、偽装が必要なのは File.ReadAllText の部分だけだと分かります。

偽装は便利ですが、広げるほど読みにくく、事故りやすくなります。

14. 非同期処理では「終わるまで偽装されているか」を見る

現代の .NET アプリケーションでは、ファイル、HTTP、DB、キュー、ストレージなど、多くの処理が非同期です。

そのため、偽装と async / await の組み合わせは注意が必要です。

基本方針は次です。

偽装が必要な非同期処理は、RunImpersonatedAsync の中で await する

良い例です。

await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
    await using FileStream stream = File.OpenRead(path);
    using var reader = new StreamReader(stream);
    string text = await reader.ReadToEndAsync();

    await ProcessTextAsync(text);
});

ただし、この形でも考えるべきことがあります。

ProcessTextAsync まで偽装ユーザーで動く必要があるのか、という点です。

ファイルを読む部分だけ偽装すればよいなら、次のように分けた方が安全です。

string text = await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
    return await File.ReadAllTextAsync(path);
});

await ProcessTextAsync(text);

偽装スコープの中で await できるからといって、何でも入れてよいわけではありません。

非同期処理でも、偽装範囲は最小化します。

15. ASP.NET Core と偽装

ASP.NET Core で Windows 認証を使う場合も、偽装の扱いには注意が必要です。

「Windows 認証でログインしているから、リクエスト処理全体がそのユーザーとして動く」と思い込むと危険です。

一般に、アプリケーションのプロセス自体はアプリケーションプール ID やサービスの実行アカウントで動きます。 利用者の Windows ID は認証情報として得られますが、そのまま全処理が利用者権限になるとは限りません。

利用者の権限で特定のアクションを行う必要があるなら、明示的に RunImpersonated / RunImpersonatedAsync でスコープを作ります。

イメージとしては次のようになります。

app.MapGet("/download", async (HttpContext context) =>
{
    if (context.User.Identity is not WindowsIdentity user)
    {
        return Results.Unauthorized();
    }

    string path = GetPathFromRequest(context);

    byte[] bytes = await WindowsIdentity.RunImpersonatedAsync(
        user.AccessToken,
        async () => await File.ReadAllBytesAsync(path));

    return Results.File(bytes, "application/octet-stream");
});

この例でも、偽装しているのはファイル読み取りの部分だけです。

レスポンス生成、ログ、アプリケーション側の認可判定まで全部を偽装スコープに入れる必要があるかは、慎重に考えるべきです。

16. Access denied の見方

偽装を使った実装で、よく出るエラーは Access denied です。

このエラーが出たときに、「偽装に失敗した」とだけ考えると遠回りになります。

確認する観点を分けます。

観点 確認内容
本当に偽装できているか 偽装スコープ内で WindowsIdentity.GetCurrent().Name を確認する
偽装レベルは足りているか Identification ではなく、必要なレベルになっているか
対象リソースの ACL は正しいか 偽装ユーザーに読み取り / 書き込み権限があるか
ローカルかリモートか ローカルファイルでは成功し、UNC だけ失敗していないか
ダブルホップではないか Web サーバーからファイルサーバーへ利用者権限で行こうとしていないか
偽装スコープを抜けていないか 実際の I/O がスコープ外や別タスクで動いていないか
トークン種別は合っているか プロセス起動に偽装トークンを渡していないか
UAC / 整合性レベルの影響はないか Administrators 所属でも、非昇格トークンではないか

特に、名前だけ確認して安心しないことが大事です。

WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);

このログは有用ですが、十分ではありません。

少なくとも次も見ます。

Console.WriteLine(identity.ImpersonationLevel);
Console.WriteLine(identity.IsAuthenticated);

また、対象がネットワーク共有なら、アプリケーションコードだけでなく、認証方式、委任設定、SPN、サービスアカウント、ファイルサーバー側の ACL も確認します。

17. UAC と「管理者なのに失敗する」問題

Windows では、ユーザーが Administrators グループに所属していることと、現在のトークンが昇格済みであることは同じではありません。

UAC が有効な環境では、管理者ユーザーでも通常は制限されたトークンでプロセスが動き、管理者権限が必要な操作では昇格が必要になります。

そのため、次のようなことが起きます。

ユーザーは Administrators に所属している
  ↓
でも現在のトークンは非昇格
  ↓
Program Files や HKLM への書き込みで Access denied

偽装でも同じです。

「偽装先ユーザーが管理者だから書けるはず」と考えるのではなく、実際に渡されているトークンがどの状態なのかを確認する必要があります。

デバッグでは、次のような観点を見ます。

  • 所属グループ
  • Privilege の有効 / 無効
  • 整合性レベル
  • 昇格状態
  • 制限トークンかどうか
  • リンクされた昇格トークンがあるか

Win32 API では GetTokenInformation を使って、TokenTypeTokenImpersonationLevelTokenElevationTypeTokenIntegrityLevel などを確認できます。

ただし、実務上の設計としては、「管理者ユーザーを偽装して何でもする」よりも、専用サービスや管理 API に必要最小限の操作を閉じ込める方が安全です。

18. ネットワーク共有とダブルホップ

偽装で非常に多い相談が、ネットワーク共有へのアクセスです。

クライアントPC
  ↓ Windows認証
Webサーバー / APIサーバー
  ↓ 偽装してアクセスしたい
ファイルサーバー

この構成では、「Web サーバー上ではユーザー名が取れているのに、ファイルサーバーへ行くと失敗する」ことがあります。

これは、利用者の資格情報を別のサーバーへ再委任できるかという問題です。

ローカルサーバー上での偽装と、別サーバーへの委任は同じではありません。

Impersonation レベルではローカル操作には使えても、リモートサーバーに対してクライアントとして振る舞うには足りない場合があります。

ネットワーク越しに利用者本人の権限を使いたいなら、Kerberos 委任、制約付き委任、SPN、サービスアカウント、認証方式の設計が必要です。

一方で、業務要件によっては、利用者本人の Windows 権限でファイルサーバーへ行く必要がない場合もあります。

その場合は、次のような設計の方がシンプルです。

利用者の認証・認可はアプリケーションで行う
  ↓
ファイルサーバーへのアクセスは専用サービスアカウントで行う
  ↓
操作ログに利用者IDと対象ファイルを記録する

これは「OS の ACL を最終判断にする」のではなく、「アプリケーションの認可を最終判断にする」設計です。

どちらが正しいかは業務要件次第です。

ただし、どちらの方式を選んでいるのかを明確にしないと、偽装、委任、ACL、アプリ認可が混ざって分かりにくくなります。

19. トークンハンドルのライフタイム

トークンはカーネルオブジェクトへのハンドルです。

取得したら、不要になった時点で閉じる必要があります。

.NET では SafeAccessTokenHandle を使い、using でスコープを管理するのが基本です。

using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);

string result = WindowsIdentity.RunImpersonated(token, () =>
{
    return File.ReadAllText(path);
});

良くない例です。

// 良くない例: トークンをグローバルに保持し続ける
private static SafeAccessTokenHandle? _cachedToken;

トークンを長期間保持すると、次のような問題につながります。

  • ハンドルリーク
  • どの処理がどのトークンを使っているか分からなくなる
  • アカウント無効化や権限変更との整合性が分かりにくくなる
  • 認証情報を長く保持する設計になりやすい
  • 監査上説明しにくい

原則は次です。

必要なときに取得する
  ↓
最小範囲で使う
  ↓
必ず閉じる

もちろん、認証コストや運用要件によってはキャッシュを検討する場合もあります。 ただし、その場合でも、有効期限、破棄、アカウント変更、監査ログ、権限変更時の扱いを設計に含める必要があります。

20. 監査ログに残すべきこと

偽装を使う処理では、ログ設計も重要です。

最低限、次の情報は残せるようにしておくと、後から調査しやすくなります。

項目
要求した利用者 DOMAIN\alice
実行プロセスのアカウント DOMAIN\svc-app
偽装したアカウント DOMAIN\alice または専用アカウント
対象リソース ファイルパス、共有名、レジストリキーなど
操作 Read、Write、Delete、CreateProcess など
結果 Success、AccessDenied、Timeout、UnexpectedError
エラーコード Win32 error code、HRESULT、例外種別
偽装スコープ どのメソッド、どの操作単位で偽装したか

ただし、ログに出してはいけないものもあります。

  • パスワード
  • アクセストークンの値
  • 認証ヘッダー
  • Kerberos チケットや資格情報そのもの
  • 個人情報を含むファイル内容

ログの目的は、「誰の要求により、どのアカウントとして、何を試み、どう失敗または成功したか」を後から追えることです。

資格情報そのものを記録する必要はありません。

21. よくあるアンチパターン

偽装まわりでよく見る危ない実装を整理します。

21.1 アプリ全体を偽装する

WindowsIdentity.RunImpersonated(token, () =>
{
    RunEntireApplication();
});

アプリ全体を偽装すると、どの操作がどの権限で実行されるのか分からなくなります。

偽装は、必要な I/O や特定 API 呼び出しに限定します。

21.2 finally なしで戻す

ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();

例外時に戻らないため危険です。

必ず try / finally か、RunImpersonated を使います。

21.3 偽装中に fire-and-forget する

WindowsIdentity.RunImpersonated(token, () =>
{
    _ = Task.Run(DoWorkAsync);
});

処理完了前に偽装スコープを抜けるため、期待と違う権限で動く可能性があります。

偽装が必要なら RunImpersonatedAsync の中で await します。

21.4 ユーザー名だけで成功判定する

Console.WriteLine(WindowsIdentity.GetCurrent().Name);

ユーザー名が期待どおりでも、偽装レベルや権限が足りないことがあります。

ImpersonationLevel、対象 ACL、ログオン種別、ネットワーク委任も見ます。

21.5 管理者アカウントを便利アカウントとして偽装する

「この処理は失敗してほしくないから、管理者アカウントで偽装する」という設計は危険です。

必要最小限の権限を持つ専用アカウントを用意し、操作単位を絞る方が安全です。

21.6 パスワードを設定ファイルに置く

{
  "UserName": "DOMAIN\\admin",
  "Password": "P@ssw0rd!"
}

これは避けるべきです。

資格情報を扱うなら、Secret Store、Windows Credential Manager、DPAPI、クラウドの Key Vault、運用基盤のシークレット管理など、環境に合った仕組みを使います。

21.7 プロセス起動とファイルアクセスを同じ話として扱う

ファイルアクセスだけなら偽装トークンで足りる場合があります。

一方、別ユーザーとしてプロセスを起動したいなら、プライマリトークン、プロファイル、デスクトップ、環境変数、セッション、権限など、別の論点が出ます。

CreateProcessAsUserCreateProcessWithTokenW を使う場合は、偽装とは別の設計として扱うべきです。

22. テストの観点

偽装処理は、手元の管理者環境だけで確認すると見落としが出ます。

最低限、次のようなテストケースを用意します。

ケース 確認すること
権限ありユーザー 対象ファイルを読める / 書ける
権限なしユーザー Access denied として正しく失敗する
存在しないユーザー 認証失敗として扱える
パスワード誤り ログに秘密情報を出さず失敗する
ネットワーク共有 ローカルと UNC の挙動差を確認する
非同期処理 await の後も期待した範囲で動く
例外発生 偽装が必ず解除される
並行リクエスト 他ユーザーの偽装が混ざらない
サービス実行 開発者の対話ログオン環境ではなく、実際のサービスアカウントで動く

特に、次の2つは重要です。

成功すること
失敗すべきときに失敗すること

偽装処理では、成功ケースだけでなく、権限がないユーザーで確実に拒否されることもテストします。

拒否されるべき操作が成功してしまうなら、偽装や認可の設計が間違っている可能性があります。

23. 実装チェックリスト

実装前後に、次のチェックリストを確認します。

観点 チェック内容
目的 なぜ偽装が必要なのか説明できるか
代替案 サービスアカウントやアプリ認可ではだめか検討したか
範囲 偽装スコープは最小か
戻し 例外時にも確実に戻るか
非同期 RunImpersonatedAsync 内で完了まで await しているか
トークン プライマリトークンと偽装トークンを混同していないか
偽装レベル IdentificationImpersonation / Delegation の違いを見ているか
ネットワーク UNC、ダブルホップ、Kerberos 委任の要否を確認したか
UAC 管理者所属と昇格済みを混同していないか
秘密情報 パスワードを平文保存していないか
ハンドル SafeAccessTokenHandleusing で閉じているか
ログ 利用者、偽装先、対象、結果を追えるか
テスト 権限あり / なし / 例外 / 並行処理を確認したか

このチェックリストで引っかかる項目が多い場合は、コードを書く前に設計を見直した方がよいです。

24. 使いどころを見極める

偽装トークンは強力ですが、常に最初に選ぶべき手段ではありません。

設計としては、次のように考えると整理しやすくなります。

24.1 OS の ACL をそのまま使いたい場合

ファイルサーバーや共有フォルダーの ACL が業務ルールの中心であり、アプリケーションもその判断に従うべきなら、利用者本人としての偽装に意味があります。

利用者本人としてアクセス
  ↓
Windows の ACL が最終判断

この場合は、Windows 認証、偽装レベル、委任、ネットワーク構成まで含めて設計します。

24.2 アプリケーション側で認可したい場合

業務ルールがアプリケーション側にあり、ファイルサーバーや DB はアプリケーションの管理下にあるなら、サービスアカウントでアクセスし、アプリケーション側で認可する方が分かりやすいことがあります。

利用者を認証
  ↓
アプリケーションで認可
  ↓
サービスアカウントでリソースへアクセス
  ↓
監査ログに利用者IDを残す

この方式では、偽装を使わない代わりに、アプリケーションの認可ロジックと監査ログが重要になります。

24.3 管理操作をしたい場合

管理操作を利用者のトークンで直接実行するより、管理操作専用のサービスや API を用意し、そこで認可、入力検証、監査、ロールバックを行う方が安全なことが多いです。

クライアント
  ↓
管理APIに要求
  ↓
管理APIが認可
  ↓
必要最小限の権限で操作
  ↓
監査ログ

「とりあえず管理者を偽装する」は、短期的には楽に見えます。

しかし、長期的には、監査、障害調査、権限変更、セキュリティレビューで苦しくなります。

25. まとめ

Windows の偽装トークンは、Windows の権限管理を正しく使うための重要な仕組みです。

ただし、理解せずに使うと、成功しているように見えて危険なコードになりやすい分野でもあります。

押さえるべきポイントは次のとおりです。

  • アクセストークンは、ユーザー、グループ、権限などのセキュリティコンテキストを表す
  • プロセスにはプライマリトークンがある
  • 偽装トークンは主にスレッドに付き、アクセスチェックで使われる
  • 偽装は権限昇格ではない
  • プライマリトークンと偽装トークンは用途が違う
  • 偽装レベルによって、識別だけできるのか、実際にアクセスできるのか、リモートへ委任できるのかが変わる
  • Win32 API で偽装するなら、必ず try / finallyRevertToSelf する
  • .NET では WindowsIdentity.RunImpersonated / RunImpersonatedAsync でスコープを小さく表現する
  • LogonUser を使う場合は、資格情報管理とログオン種別に注意する
  • ネットワーク共有では、偽装だけでなく Kerberos 委任やサービスアカウント設計も関係する
  • トークンハンドルは SafeAccessTokenHandleusing で管理する
  • 成功ケースだけでなく、拒否されるべきケースもテストする

偽装トークンの実装で大事なのは、「別ユーザーで動いた」という一点ではありません。

大事なのは、次のことを説明できる状態にすることです。

どの処理を
誰の要求で
どのアカウントとして
どの範囲だけ実行し
どこで元に戻し
成功と失敗をどう記録しているか

ここまで整理できていれば、偽装は怖い仕組みではありません。

Windows の ACL、サービスアカウント、既存ファイルサーバー、社内ドメイン資産を活かすための、実務的な道具として使えるようになります。

参考

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る