偽装とは何か?
セキュリティを語る上でよく、 「偽装」 という言葉が出てきますが、いまひとつピンと来ていない方も多いのではないかと思います。 この資料では、Windows のセキュリティを考える上で非常に大切な考え方である 「偽装」 について説明します。
実際にプログラムを書いて試しながら理解しましょう。
偽装とは何か?なぜ偽装するのか?
全てのプロセスはプライマリトークンを持つ
「偽装」について説明する前に、「偽装していない状態」 について説明します。
あなたが Windows にログインする時には、ユーザー名とパスワードを使ってログインしますよね。 Windows はこの時、ログインしたのがあなたであることを認識して トークン (Token) を生成します。
このトークンを使って Windows のシェルである、エクスプローラが起動されます。
スタートメニューを使ってプログラムを起動すると、実はこのメニュー自体エクスプローラそのものなのですが、 新しく起動されたプロセスにも同じトークンがセットされます。
このように、プロセス起動時にセットされるトークンがあるのですが、これをプライマリトークン (Primary Token) といいます。
このような仕組みがあるので、あなたが対話的に起動する全てのプログラム (プロセス) が、 ログインした時に生成されたトークンを持つことになります。
トークンにはセキュリティコンテキストが関連付けられています。従ってこの結果、 そのプログラムがあなたのアクセス権限をもって動作することができるのです。
特に何もしない限り、そのプロセス内の全てのスレッドはプライマリトークンのセキュリティコンテキストで実行されます。 これが 「偽装していない状態」 です。
まとめると、あなたがログインして、あなたがプログラムを起動すると、そのプログラムには、あなたのアクセストークンが プライマリトークンとしてセットされているので、あなたのセキュリティコンテキストで実行されるというわけです。
偽装とはプライマリトークンと別のセキュリティコンテキストでプログラムを実行すること
では「偽装」 とはなんでしょうか?
偽装 (impersonate) というのは、プライマリートークンとは別のセキュリティコンテキストでプログラム (スレッド) を実行することといえます。
世間的にはよく 「産地偽装」 とか悪い意味で使われたりしますが、別に悪いことではありませんので、この言葉の使い方に気をつけてくださいね。
プログラムの中では LogonUser という API によって、プライマリトークンとは別のユーザーのログオントークンを取得することが出来ます。 例えば、私 "Keisuke" がプログラムを起動した場合、そのプロセスのプライマリトークンは私のトークンをプライマリトークンになります。 そのプログラム内で LogonUser API にユーザー名 "User1" とそのパスワードを渡して、"User1" のトークンを取得することが可能です。
そして、LogonUser で取得できたトークンを、ImpersonateLoggedOnUser という API に渡してその呼び出しが成功すると、 ImpersonateLoggedOnUser を呼び出したスレッドが "User1" として実行されます。 この時セキュリティ的には 「"User1" のセキュリティコンテキストで実行される」 という言い方をします。
このように、プライマリトークンで表されるユーザーとは異なるユーザーのセキュリティコンテキストでプログラムを実行することを、 「ユーザーを偽装する」 といいます。
IIS 等のサーバープログラムではセキュリティ確保のために偽装が利用される
では、なぜわざわざ偽装などをするのでしょうか?
これは IIS をはじめとするサーバープログラムのセキュリティを考える上で非常に大切なポイントです。
例えば IIS などの Web サーバーには世界中から不特定多数のユーザーがアクセスすることが想定されます。
このときに、IIS が SYSTEM 権限で実行されていたとしたらどうでしょうか? IIS 上で実行されるプログラムにセキュリティホールなどがあった時に、SYSTEM 権限で様々な操作をされてしまいます。
しかし、匿名ユーザーでアクセスさせる部分を低い権限のユーザーに偽装して実行していれば、 万が一セキュリティホールがあった場合にも、エクスプロイトは権限の低いユーザーのセキュリティコンテキストで動作することになるので、 被害を最小限に抑えることが可能になります。
このため、サーバーアプリケーションは極力低い権限のユーザーに偽装して実行されるように設計されます。
偽装を試してみよう!
準備 - ユーザーとアクセス権の設定
それでは実際にプログラムを書いて、偽装を試してみましょう。
まず、ユーザーを作成します。ここでは "User1" というユーザーを作成しました (パスワードは "password" です)。
次にファイルへのアクセスを試すために、C:\Temp ディレクトリに file1.txt というファイルを作成して、 そのアクセス権を自分 "Keisuke" だけに設定しました。
それではコードを書いていきましょう。
サンプルコード ~ 偽装してファイルへのアクセスを試す
次のコードを getun.cpp として保存します。
#include <windows.h> #include <lmcons.h> #include <stdio.h> ///////////////////////////////////////////////////////////////////////////// // // ファイル C:\Temp\file1.txt が開けるか試し、その結果を出力 // void TryOpenFile () { HANDLE hFile = NULL; hFile = CreateFile ( "C:\\Temp\\file1.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if ( INVALID_HANDLE_VALUE == hFile ) { printf ( "Failed. gle = %u", GetLastError() ); } else { printf ( "OK" ); CloseHandle ( hFile ); } } ///////////////////////////////////////////////////////////////////////////// void PrintUserName () { char szUserName[UNLEN + 1]; DWORD dwUNLen = UNLEN; if( !GetUserName( szUserName, &dwUNLen ) ) { printf( "Error: %u", GetLastError() ); return; } printf( "%s", szUserName ); } ///////////////////////////////////////////////////////////////////////////// void CheckAccess ( char* pszMsg ) { static int n = 0; if ( !pszMsg ) { return; } printf ( "[%d] %s - ", ++n, pszMsg ); PrintUserName(); printf ( " - " ); TryOpenFile(); printf ( "\n" ); } ///////////////////////////////////////////////////////////////////////////// int main(int argc, char argv[]) { HANDLE hToken = NULL; // // ユーザー名を出力 // CheckAccess( "Entering main" ); // // "User1" でログオンする - "User1" のトークンを取得する // if ( !LogonUser ( "User1", ".", "password", LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &hToken ) ) { printf( "LogonUser failed. gle = %u\n", GetLastError() ); return 1; } CheckAccess ( "After LogonUser" ); // // 偽装する // if ( !ImpersonateLoggedOnUser ( hToken ) ) { printf( "LogonUser failed. gle = %u\n", GetLastError() ); return 1; } CheckAccess ( "After ImpersonateLoggedOnUser" ); // // RevertToSelf を呼びプライマリトークンへ戻る // if ( !RevertToSelf() ) { printf ( "RevertToSelf failed. gle = %u\n", GetLastError() ); } CheckAccess ( "After RevertToSelf" ); // // クリーンアップ // CloseHandle ( hToken ); return 0; }
makefile は次の通りです。
TARGETNAME=imptest2 OUTDIR=.\chk LINK32=link.exe ALL : "$(OUTDIR)\$(TARGETNAME).exe" CPPFLAGS=\ /nologo\ /MT\ /W4\ /Fo"$(OUTDIR)\\"\ /Fd"$(OUTDIR)\\"\ /c\ /Zi\ /D_WIN32_WINNT=0x0500 LINK32_FLAGS=\ advapi32.lib\ /nologo\ /subsystem:console\ /pdb:"$(OUTDIR)\$(TARGETNAME).pdb"\ /machine:I386\ /out:"$(OUTDIR)\$(TARGETNAME).exe"\ /DEBUG LINK32_OBJS= \ "$(OUTDIR)\getun.obj" "$(OUTDIR)\$(TARGETNAME).exe" : "$(OUTDIR)" $(LINK32_OBJS) $(LINK32) $(LINK32_FLAGS) $(LINK32_OBJS) "$(OUTDIR)" : if not exist "$(OUTDIR)/$(NULL)" mkdir "$(OUTDIR)" .c{$(OUTDIR)}.obj: $(CPP) $(CPPFLAGS) $< .cpp{$(OUTDIR)}.obj: $(CPP) $(CPPFLAGS) $<
上記を nmake すると chk というサブディレクトリに imptest2.exe というプログラムが出来ます。 これを実行すると、次のような結果を得ます。
> .\chk\imptest2.exe [1] Entering main - Keisuke - OK [2] After LogonUser - Keisuke - OK [3] After ImpersonateLoggedOnUser - User1 - Failed. gle = 5 [4] After RevertToSelf - Keisuke - OK >
これにより、プログラム実行直後は Keisuke として実行されており、 LogonUser 呼び出し直後も未だ Keisuke のまま。 ImpersonateLoggedOnUser を呼び出すと User1 として実行され、 Keisuke のみにしかアクセス権のないファイルへのアクセスが失敗。 RevertToSelf を呼び出した後はまた Keisuke として実行された。 という一連の流れを確認できたと思います。
もちろん、あなたの環境で試せば Keisuke ではなく、あなたのユーザー名が表示されることでしょう。
ちなみに、エラーコード 5 が返ってきていますが、エラーコードからメッセージを確認するには、 次のようなコマンドが利用できます。
> net helpmsg 5 Access is denied.
私の環境は英語環境なので英語でメッセージが表示されていますが、日本語環境で上記コマンドを試せば、 日本語のメッセージが表示されるはずです。