NFLabs. エンジニアブログ

セキュリティやソフトウェア開発に関する情報を発信する技術者向けのブログです。

incognitoから紐解くトークン偽装攻撃の仕組み

こんにちは。NFLabs. 事業推進部の中堂です。 この記事は NFLabs. アドベントカレンダー9日目です。

今回は、Windows環境に対するペネトレーションテストで多用される「トークン偽装(Token Impersonation/Theft)」という攻撃テクニックについて解説したいと思います。

トークン偽装は、簡単に言えば別ユーザーになりすますことができる手法ですが、どのような仕組みになっているのでしょうか。

この仕組みを理解するにあたりまずアクセストークンについて焦点を当てたいと思います。

アクセストークンとは

そもそもWindows OSはどのようにプロセスを実行するユーザーを識別しているのでしょうか。

結論から言うとプロセスおよびスレッドにはアクセストークンというオブジェクトが関連づけられており、Windows OSはトークンによりプロセスおよびスレッドを実行するユーザーを識別しています。

トークンには、このプロセス・スレッドを実行するユーザーは誰なのかといったユーザー情報、どのグループに所属しているのかといったグループ情報、何ができるのかといった特権情報などのセキュリティコンテキストが格納されており、このトークンを確認することで関連づけられているプロセスおよびスレッドを実行する主体を識別しているのです。

ちなみに、コマンドプロンプト上でwhoamiコマンドを実行すると、コマンドプロンプト、つまりはcmd.exeというプロセスのトークンに記述されているユーザー情報を確認することができます。

C:\Users\david>whoami
example\david

(思いっきり余談ですが、mimikatzでsekurlsa::pthでPass-the-hashしたのちにwhoami実行してもなりすましたユーザー名が表示されないのは、Pass-the-hashはトークンの情報を書き換える攻撃ではないためです)

このようにWindows OSはトークンによりユーザーを識別しており、このトークンを用いることで端末内でのアクセスコントロールを行っているのです。

なお、後述しますがトークンはローカル端末内におけるユーザー識別に有効なオブジェクトである一方、リモート端末に対しては有効ではありません。

トークンの種類

このトークンですが、以下の二種類存在しています。

  • プライマリアクセストークン
  • 偽装トークン

全てのプロセスはプライマリアクセストークンと呼ばれるトークンが設定され、子プロセスには親プロセスのプライマリアクセストークンが継承されます。

例えば、ユーザーがログインをすると、該当ユーザーのプライマリアクセストークンが作成され、該当トークンが設定されたexplorer.exeがユーザーの操作インタフェースとして起動します。

その後、ユーザーがexplorer.exeにより新たなアプリケーションを起動すると、explorer.exeに設定されているプライマリアクセストークンがそのアプリケーションに継承されるという仕組みです。

f:id:y_chudo:20211123075654p:plain

※ 説明の簡略化のため図ではトークンにユーザー名およびグループ名の文字列を付記していますが、トークンが含むデータの実体はそれぞれのSID(アカウントや所属するグループを一意に示すID)になります。

また、このプライマリアクセストークンは、プロセス間で継承されるとともに通常プロセス内で作成されるスレッドにも継承されます。

ただし、スレッドについてはプライマリアクセストークンではなく、もう一つの種類である偽装トークンを設定することもできます。

偽装トークン

この偽装トークンは何かというと親プロセスとは別のセキュリティコンテキスト(ユーザー情報やグループ情報など)が設定されたトークンで、別ユーザーのトークンをコピーして作成できるオブジェクトです。

この偽装トークンが設定されたスレッドは、例えば親プロセスがAさんのプロセスとして振る舞っていても、そのスレッドは別のユーザーBさんとして振る舞うことが可能となります。

別ユーザーに詐称できるというのは、セキュリティ上の欠陥なのではと思ってしまうかもしれませんが、この偽装トークンという仕組みはWindowsの仕様として存在しています。

なぜこのような仕様があるかというと、サーバーアプリケーション開発者がアクセスコントロール機能の実装をWindows OSにアウトソースできるようにするためです。

例えば、FTPサーバを開発するため、接続したクライアントがファイルにアクセスしてよいかチェックする機能を実装するとします。

偽装トークンを用いずに実装する場合、接続元のクライアントは誰かという情報とアクセスするファイルのACL(アクセス制御リスト)を元に該当クライアントにアクセスを許可するか否かを判定するプログラムを書く必要があります。

f:id:y_chudo:20211123083607p:plain

一方で、偽装トークンを用いて実装する場合、接続元クライアントのトークンにより生成した偽装トークンをスレッドに設定し、そのスレッドでファイルアクセスを試行するようなプログラムを書くだけで、ファイルアクセス試行した際にWindows OS側でトークンのユーザー情報とアクセス先ファイルのACLを元にアクセス可否を勝手に判定してくれるわけです。

f:id:y_chudo:20211123084536p:plain

このように偽装トークンは、開発者が楽にアクセスコントロール機能を実装できるように存在する仕様であり、以下のWindows API関数を用いることで接続元のクライアントのトークンから偽装トークンを作成することが可能です。

  • ImpersonateLoggedOnUser
  • ImpersonateNamedPipeClient

ただし、誰でも偽装トークンを作成できるわけではなく、一般権限のユーザーが保有しないSeImpersonatePrivilegeという権限を保有するユーザーのみが可能となっています。

また、DuplicateTokenExを使用すると偽装トークンをプライマリアクセストークンに変換(逆も可)することも可能です。

トークンの権限借用レベル

このようなトークンの仕様を用いることで他人になりすますことができるわけですが、権限借用レベルというレベルによってなりすましたユーザーとして行える操作範囲が制限されており、以下の4つのレベルが存在します。

権限借用レベル 内容
SecurityAnonymous サーバーは、クライアントを偽装または識別することはできません。
SecurityIdentification サーバーはクライアントの ID と特権を取得できますが、クライアントを偽装することはできません。
SecurityImpersonation サーバーは、ローカル システム上のクライアントのセキュリティ コンテキストを偽装できます。
SecurityDelegation サーバーは、リモート システム上でクライアントのセキュリティ コンテキストを偽装できます。

(引用元:https://docs.microsoft.com/ja-jp/windows/win32/secauthz/impersonation-levels

例えば、SecurityImpersonationのレベルが設定されている場合、ローカルシステム上では別ユーザーになりすましが可能ですが、リモートのシステムに対してはなりすましができません。

一方で、SecurityDelegationである場合、ローカルシステムのみならずリモートシステムに対してもあたかも別ユーザーとしてアクセスが可能となります。

冒頭でトークンは、ローカル端末でのユーザーの識別に有効なオブジェクトであると述べました。

では、このSecurityDelegation時のリモートシステムに対するユーザーの詐称はどのように実現されているのでしょうか。

理解するにあたってログオンセッションについて焦点を当てたいと思います。

ログオンセッションとアクセストークン

ログオンセッションは、シングルサインオンなどのために存在するオブジェクトで、後述しますが一部の場合を除きユーザーのクレデンシャル(NTLMハッシュやKerberosチケットなど)がキャッシュされています。

このログオンセッションは、ユーザーがログオンすることでトークンと共に作成され、トークンはログオンセッションに関連付けされています(ログオンセッションにはLUIDという値が設定され、そのLUIDの値はAuthIDという名前でトークンに設定されることでリンクされています)

f:id:y_chudo:20211123091505p:plain

そして、プロセスまたはスレッドがリモート端末に対してアクセスする際、自動的にトークンにリンクされているログオンセッション上のクレデンシャルが送信され、リモート端末はそのクレデンシャルを用いてユーザーの認証を行い、アクセス元が誰かを識別する仕組みになっています。

f:id:y_chudo:20211126074946p:plain

なお、このログオンセッションには必ずしもクレデンシャルがキャッシュされるというわけではなく、ファイルサーバへのアクセスなどに用いられるNETWORK_LOGONというログオンタイプである場合、クレデンシャルはキャッシュされていません。

従って、このような関連付けされたログオンセッションにクレデンシャルが保持されていないトークンを持ってリモート端末にアクセスをしても、認証は失敗に終わり、リモート端末からすれば「あなたは誰?」という状態になります。

先程の権限借用レベルの話に戻すと、このようなトークンにはSecurityImpersonationレベルが設定されており、リンクするログオンセッションにはクレデンシャルがないため、あくまでローカル端末内でのみなりすましが可能である一方、リモート端末に対するなりすましは不可能です。

対して、クレデンシャルのあるログオンセッションに紐付くトークンには、SecurityDelegationレベルが設定されており、このトークンを利用すると、リモート端末にアクセスする際にクレデンシャルが自動的に使用され、ユーザーの詐称が可能となっています。

前提知識の解説が長くなってしまいましたが、本題であるトークン偽装攻撃は、このようなアクセストークンやログオンセッションの仕様を悪用し、端末に存在する別ユーザーのトークンによりローカルやリモート端末でなりすましを行う攻撃になります。

トークン偽装攻撃の方法

ここまで理論ベースでトークン偽装の方法について紹介しましたが、具体的にどのように行うのかについて有名なペネトレーションテストツールであるincognito (https://github.com/FSecureLABS/incognito) のコードをベースに掘り下げて解説したいと思います。

incognitoは、以下の通り実行することでコマンドライン引数に指定したユーザーになりすましたプロセスを起動できるツールで、ペネトレーションフレームワークのmetasploitにもモジュールとして組み込まれています(今回はスタンドアロンアプリケーション版を取り上げます)

incognito.exe execute <ドメイン名¥ユーザー名> <コマンド>

以下は実行例です。DavidというユーザーからAdministratorになりすましてcmd.exeを起動しています。

f:id:y_chudo:20211124080703p:plain

なお、このようなトークン偽装攻撃が成功する条件としては以下の通りです。

  • SeImpersonatePrivilege権限を保有すること
    • ただし、Windowsの整合性レベル(アクセス制御機構の一つ)により失敗する場合があるため、加えてSeDebugPrivilege権限があると攻撃の成功確率が高くなります

コマンドプロンプト上でwhoami /privを実行すると確認できます。

C:\Windows\system32> whoami /priv

PRIVILEGES INFORMATION
----------------------

Privilege Name                            Description                                                        State
========================================= ================================================================== ========
...
SeDebugPrivilege                          Debug programs                                                     Enabled
...
SeImpersonatePrivilege                    Impersonate a client after authentication                          Enabled
...

では、この機能はどのように実現されているのか主要な処理をピックアップして順に見ていきたいと思います。

以下の流れになります。(説明の簡略化のため一部の説明は省いています。)

  1. トークンの列挙
  2. SeAssignPrimaryProcess権限の取得
  3. 別ユーザーになりすましたプロセスの起動

1. トークンの列挙

incognito.exeのトークン偽装のやり方として、まずシステム上に存在するトークンを全て列挙するよう実装されています。

このトークン列挙ですが、処理としてはまずNtQuerySystemInformationを呼び、Kernelに問い合わせてシステム内の全てのプロセス情報を取得します。

...
    ntReturn = NtQuerySystemInformation(SystemProcessInformation, pProcessInfo, dwSize, &dwSize);
...

さらに、各プロセスが保有する全てのハンドル(hObjectに格納)に対してNTQueryObjectを実行することで、ハンドルのうちアクセストークンに対するハンドルを特定します。

...
   NTSTATUS ntReturn = NtQueryObject(hObject, objInfoClass, pObjectInfo, dwSize, &dwSize);   
...

そして、見つかったトークンのハンドル(以下ではtokenに格納)に対して、GetTokenInformationおよびLookupAccountSidAを呼び出し、トークンのユーザー情報を取得します。

...
    if (!GetTokenInformation(token, TokenUser, TokenUserInfo, BUF_SIZE, &returned_tokinfo_length))
        return FALSE;
    LookupAccountSidA(NULL, ((TOKEN_USER*)TokenUserInfo)->User.Sid, username, &user_length, domainname, &domain_length, (PSID_NAME_USE)&sid_type);
...

また、GetTokenInformationの引数にTokenImpersonationLevelを指定することで、トークンの権限借用レベルの情報も収集します。

...
    if (GetTokenInformation(token, TokenImpersonationLevel, TokenImpersonationInfo, BUF_SIZE, &returned_tokinfo_length))
    if (*((SECURITY_IMPERSONATION_LEVEL*)TokenImpersonationInfo) == SecurityDelegation)
        return TRUE;
    else
        return FALSE;
...

incognitoは、以上のような方法で端末内に存在するトークンを列挙します。

2. SeAssignPrimaryProcess権限の取得

端末内のトークンが列挙できれば、次に自分自身(incognito.exeのmainスレッド)にSeAssignPrimaryTokenPrivilege権限を有効にする処理に進みます。

この権限は何かというと、作成する子プロセスには通常親プロセスのトークンが継承されるのですが、この権限を保有することで子プロセスに別ユーザーのトークンを設定して起動できるようになるというものです。

incognitoは別ユーザーのトークンを設定した子プロセスを作成する上でこの権限が必要であるため、列挙したトークンのうちSeAssignPrimaryTokenPrivilege権限を保有するトークンを特定し、その後該当権限を持つトークンを引数にImpersonateLoggedOnUserを呼び出すよう実装されています。

...
    ImpersonateLoggedOnUser(token_list[i].token);
...

ImpersonateLoggedOnUserは引数に指定したトークンを元に偽装トークンを作成し、実行中のスレッドに設定することができる関数であり、

SeAssignPrimaryTokenPrivilege権限を持つトークンを指定してこの関数を呼び出すことで実行中のスレッドに該当権限を持つ偽装トークンを設定し、権限を昇格させることが可能となります。

ただし、このような処理を行うにあたり事前にSeImpersonatePrivilege権限が必要になるため失敗する場合はご確認いただくと良いと思います。

3. 別ユーザーになりすましたプロセスの起動

最後に指定したユーザーに詐称したプロセスを起動します。

別ユーザーになりすましを行う場合、偽装トークンをImpersonateLoggedOnuserなどを呼び出して作成する方法がありますが、偽装トークンはスレッドにのみ設定でき、プロセスに対しては設定できないため、ユーザー詐称したプロセスを立ち上げる際はプライマリアクセストークンが必要となります。

したがって、incognitoでは別ユーザーに詐称したプロセスを作成する際、TokenPrimaryを引数にDuplicateTokenExを呼ぶことで指定したユーザーのトークンを複製し、プライマリアクセストークンを作成するアプローチをとっています。

...
    // Create primary token
    if (!DuplicateTokenEx(token, TOKEN_ALL_ACCESS, NULL, impersonation_level, TokenPrimary, &primary_token))
...

なお、複製元のトークンはSecurityDelegationが設定されているトークンを優先して利用し、なければSecurityImpersonationトークンを使用する実装になっています。

その後、作成した別ユーザーのプライマリアクセストークンを引数にCreateProcessAsUserAを呼ぶことで、別ユーザーに詐称したプロセスを起動するという流れで処理は終了となります。

...
    if (CreateProcessAsUserA(
        primary_token,            // client's access token
        NULL,              // file to execute
        command,     // command line
        NULL,              // pointer to process SECURITY_ATTRIBUTES
        NULL,              // pointer to thread SECURITY_ATTRIBUTES
        FALSE,             // handles are not inheritable
        CREATE_NEW_CONSOLE,   // creation flags
        NULL,              // pointer to new environment block
        NULL,              // name of current directory
        &si,               // pointer to STARTUPINFO structure
        &pi                // receives information about new process
    ))
...

トークン偽装攻撃の対策

以上がトークン偽装攻撃の方法になりますが、この攻撃に対する対策について考えていきたいと思います。

まず前提としてこのようなトークン偽装はWindowsの仕様であるため、パッチの提供はありません。

従って、取れうる主な対策として、SeImpersonatePrivilege権限を保有するサービスアカウントやサービスとして実行されるアプリケーションの堅牢化が挙げられるかと思います。

  • サービスアカウントのパスワード強度を高める
  • アプリケーションに含まれるコマンド実行可能な脆弱性を修正する ...etc

また、トークン偽装攻撃ではDuplicateTokenExImpersonateLoggedOnUser関数が呼ばれることがあるため、これらの関数の呼び出しをEDR製品などにより監視することも攻撃の検知には有効になります。

加えて、イベントログを監視することによる検知も有効な手法です。例えば、グループポリシーのカーネルオブジェクトの監査(コンピューターの構成>Windowsの設定>セキュリティの設定>監査ポリシーの詳細な構成>システム監査ポリシー-ローカルグループポリシーオブジェクト>カーネル オブジェクトの監査)を有効にすることで、

DuplicateTokenExによるトークンの複製をイベントログ(イベントID: 4690)に記録でき検知に活用できます。

f:id:y_chudo:20211201160255p:plain

最後に

ということでトークン偽装攻撃について紹介しました。

別ユーザーになりすます攻撃としてPass-the-hash攻撃が有名ですが、lsass.exeのプロセス操作を伴わず比較的検知リスクを抑えて同じ効果が期待でき、

セキュリティフレームワーク「MITRE ATT&CK」に採番され(ID: T1134.001)、マルウェアも使用する危険な攻撃手法であるトークン偽装攻撃はあまり世間で注目されていないような気がしています。

本記事をきっかけにこの攻撃手法の理解が広まれば幸いです。

参考: