Skip to content

Latest commit

 

History

History
250 lines (187 loc) · 15.9 KB

7.6.1_アカウント権利と特権.MD

File metadata and controls

250 lines (187 loc) · 15.9 KB

アカウントの権利は、ログオン形式を規定する

プロセスが何らかのセキュリティ操作を実行できるかどうかは、オブジェクトへのアクセスチェックが成功するかどうかで決まるというのが、これまでの要諦だった。 たとえば、あるディレクトリ内にファイルを書き込むならば、書き込みアクセスが許可されていなければならないといった具合である。 しかし、システムをシャットダウンしたいようなセキュリティ操作の場合、 そもそも何のオブジェクトに対してアクセスチェックをすればよいかという話にもなってくる。

実行時にプロセスによって行われる多くの操作は、特定のオブジェクトとの対話を伴わないため、 オブジェクトアクセス保護による承認を受けることはできません。
(「インサイドWindows 第7版 上」p.748より引用)

オブジェクトへのアクセスチェックを受けられないのであれば、何をもってセキュリティ操作を可能とみなすかだが、ここで特権が関わってくる。

「特権(Privilege)」は、コンピューターのシャットダウンやシステム時刻の変更といった、特定のシステム関連の操作を実行するためにアカウントに付与される権利です。 「アカウントの権利(Account Right)」は、コンピューターへのローカルログオンや対話型ログオンといった、特定の種類のログオンを実行する能力をアカウントに許可または拒否します。
(「インサイドWindows 第7版 上」p.748より引用)

たとえば、SE_SHUTDOWN_NAMEという特権があるが、 これがユーザーまたはユーザーが属するグループに割り当てられているならば、 そのユーザーで実行するプロセスはシャットダウン可能ということになる。 また、特権と比較して話題になるものとしてアカウントの権利があるが、 これはログオンが許可されるかどうかを示すものである。 まず、アカウントの権利から詳細を見ていく。

アカウントの権利は、セキュリティ参照モニター(SRM)によって強制されることはありませんし、トークン内に格納されることもありません。
(「インサイドWindows 第7版 上」p.750より引用)

アカウントの権利がトークン内に格納されない点に注目したい。 トークンから情報を取得できるGetTokenInformationは、TokenPrivilegesを指定することで特権リストを取得できるが、 そこにアカウントの権利は含まれない(後述するLsaEnumerateAccountRightsは取得可)。 一方、トークンで識別されるアカウントが、どのようなログオン(対話かネットワークかなど)をしたかどうかは、 トークン内に格納されている。

// Interactive SIDを作成する
CreateWellKnownSid(WinInteractiveSid, NULL, pSid, &dwSidSize);

// トークンのトークングループ内にてpSidが有効であるかを調べる
CheckTokenMembership(NULL, pSid, &bResult);
if (bResult)
	printf("現在ユーザーは対話型ログオンした");
else
	printf("現在ユーザーは対話型ログオンしていない");

ユーザーが対話型ログオンをした場合、InteractiveというSIDがトークングループに含まれる。 このため、このSIDが存在していれば、そのアカウントは対話型ログオンをしたことを意味する。

アカウントがログオンする際に、アカウントの権利が調べられる

既にログオンしたユーザーがどのようにログオンしたかは、そのトークンを参照すればよいことは分かった。 それでは、ログオンする前に、そのアカウントが特定のログオンが許可されるかは、どう調べればよいだろうか。 このようなシナリオは、以下のような場合に必要になる。

LogonUser APIは、実行するログオンの種類を示すパラメーターを受け取ります。ログオンの種類には、対話型、ネットワーク、バッチジョブとして、 サービスとして、リモートデスクトップサービス(旧称、ターミナルサービス)クライアントがあります。
(「インサイドWindows 第7版 上」p.750より引用)

LogonUserが対話型ログオンを示すInteractive定数を受け取ったならば、当然ながらログオンしようとするユーザーが対話型ログオンを許可されているかを調べないといけない。 その内部動作は以下から類推できる。

ローカルセキュリティ機関(LSA)は、ログオン要求に応答し、ユーザーがそのシステムにログオンを試みた時点で、 LSAポリシーデータベースからユーザーに割り当てられた権利を取得します。 LSAはログオンの種類とログオンするユーザーアカウントに対して割り当てられたアカウントの権利をチェックし、 そのログオンの種類を許可する権利をアカウントが持たない場合、またはログオンの種類の拒否の権利を持つ場合、そのログオンを拒否します。
(「インサイドWindows 第7版 上」p.750より引用)

実はユーザーに割り当てられた権利というのは、以下に示すように取得可能になっている。

アカウントに割り当てられているアカウントの権利を調べるには、LsaEnumerateAccountRights関数を使用します。
(「インサイドWindows 第7版 上」p.750より引用)

このため、通常のアプリケーションでも、LSAが行っているようなアカウントの権利のチェックは可能である。

// 特権を列挙する場合はPOLICY_LOOKUP_NAMESを指定
LsaOpenPolicy(NULL, &objectAttributes, POLICY_LOOKUP_NAMES, &hPolicy);

// plsaStringに特権名の配列が返る
LsaEnumerateAccountRights(hPolicy, pSid, &plsaString, &uCount);

for (i = 0; i < uCount; i++) {
	if (lstrcmp(plsaString[i].Buffer, SE_INTERACTIVE_LOGON_NAME) == 0) {
		bResult1 = TRUE;
	}
	else if (lstrcmp(plsaString[i].Buffer, SE_DENY_INTERACTIVE_LOGON_NAME) == 0) {
		bResult2 = FALSE;
	}
}

LsaFreeMemory(plsaString);

return bResult1 && bResult2;

このコードでは、pSidで識別されるアカウントに割り当てられたアカウントの権利を列挙している。 そのリストの中にSE_INTERACTIVE_LOGON_NAMEが含まれており、 SE_DENY_INTERACTIVE_LOGON_NAMEが含まれていなければ対話型ログオンが可能とみなせる。

特権の有効化とその効果を確認する

アカウントの権利がログオンの許可と拒否を定めるものであったのに対して、 特権が割り当てられている事でできる操作は非常に多種多様である。 ここからは、特権について見ていく。

例えば、「プログラムのデバッグ」特権は、プロセスマネージャーによってチェックされ、この特権によりプロセスは、 Windows APIのOpenProcess関数を使用して別のプロセスへのハンドルをオープンする際のセキュリティチェックをバイパスすることができます。
(「インサイドWindows 第7版 上」p.750より引用)

システムプロセス(lsass.exeなど)に対してOpenProcessを呼び出した場合、通常は整合性レベルの問題でハンドルの取得に失敗する。 しかし、SE_DEBUG_NAME特権が割り当てられていれば、ハンドルを取得できるため、 システムプロセスに対して何らかの操作が行えるようになる。 本来のアクセスチェックを無効にするという意味で、この特権は非常に強力といえる。

特権は、現在のブートで当然一意であるローカル一意識別子(LUID)によって識別されます。
(「インサイドWindows 第7版 上」p.751より引用)

特権は内部的にはLUIDで識別されるという。 LookupPrivilegeValue関数を呼び出せば、特権の名前から関連するLUIDを取得できる。

LUID luid;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);

この関数は特権を有効にする際に呼ばれる事が多い。 特権の中には既定で無効状態になっているものもあるため、 そうした特権は事前に有効化が必要になる。

アカウントの権利とは異なり、特権は有効化または無効化できます。特権のチェックが成功するためには、その特権が指定されたトークン内に存在し、 有効化されている必要があります。 この仕組みの背後にある考え方は、特権はその使用が必要なときにのみ有効化されるべきであるということです。 そうすることで、プロセスが誤って権限のあるセキュリティ操作を実行することがなくなります。 特権の有効化または無効化は、AdjustTokenPrivileges関数を使用して行われます。
(「インサイドWindows 第7版 上」p.754より引用)

AdjustTokenPrivilegesを使用したコード例は以下である。

// まだSE_DEBUG_NAMEを有効化していないため、呼び出しは失敗する
hProcess = OpenSystemProcess();
if (hProcess == NULL) {
	tokenPrivileges.PrivilegeCount = 1;
	tokenPrivileges.Privileges[0].Luid = luid;
	tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
	
	// AdjustTokenPrivilegesでSE_DEBUG_NAMEを有効に、呼び出しは成功する
	hProcess = OpenSystemProcess();
	if (hProcess != NULL)
		printf("システムプロセスをオープンした。");
	else
		printf("システムプロセスをオープンできなかった。");
}

OpenSystemProcessはシステムプロセスをオープンする自作関数であり、1回目は失敗する。 しかし、AdjustTokenPrivilegesで特権を有効化して再度試みると、オープンには成功する。 これは正に、SE_DEBUG_NAME特権がプロセスオープンのセキュリティをバイパスしている効果といえる。

昇格されているかどうかで、割り当てられている特権も変わる

先程のSE_DEBUG_NAME特権は非常に強力なものであった。 ただし、これが可能なのはあくまでプロセスが管理者として動作している場合であり、 標準ユーザーとして動作している場合は当然ながらできない。 理由は、割り当てられていない特権は有効化できないためである。

// 特権を示す文字列をLUIDに変換
LookupPrivilegeValue(NULL, lpszPrivilege, &luid);

// トークンから特権リストを取得
GetTokenInformation(hToken, TokenPrivileges, NULL, 0, &dwLength);
pTokenPrivileges = (PTOKEN_PRIVILEGES)LocalAlloc(LPTR, dwLength);
GetTokenInformation(hToken, TokenPrivileges, pTokenPrivileges, dwLength, &dwLength);

// 特権リストの中にLUIDのものが含まれるか調べる
for (i = 0; i < pTokenPrivileges->PrivilegeCount; i++) {
	pluid = &pTokenPrivileges->Privileges[i].Luid;
	if (pluid->LowPart == luid.LowPart && pluid->HighPart == luid.HighPart) {
		bResult = TRUE;
		break;
	}
}

このコードはトークンから特権リストを取得し、lpszPrivilegeと一致する特権を探そうとしている。 lpszPrivilegeがSE_DEBUG_NAMEであった場合、標準ユーザーにはこの特権が割り当てられないため失敗する。 SE_SHUTDOWN_NAMEのような特権は標準ユーザーにも割り当てられるため、こちらを指定した場合は一致を確認できる。

コンポーネント(プロセスマネージャーなど)はプロセスが特定の特権を使用できるかを確認する際に、次の関数を呼び出すという。

コンポーネントは、特権が存在するかどうかを確認するためにトークンをチェックしたいとき、 ユーザーモードで実行中の場合はPrivilegeCheckまたはLsaEnumerateAccountRights APIを使用し、
(「インサイドWindows 第7版 上」p.754より引用)

PrivilegeCheckは特権が割り当てられていることに加えて、有効になっていることもチェックすることに注意したい。 PrivilegeCheckに失敗したとしても、特権が割り当てられていればAdjustTokenPrivilegesでいつでも有効化できる。 無効化されているけどいつでも有効にできる事を考慮したい場合は、先程のTokenPrivilegesのコードを使用する。

走査チェックのバイパス(SeChangeNotifyPrivilege)

トークンに格納された特権リストの中で唯一、既定で有効なものがSeChangeNotifyPrivilegeである。 この特権の役割は以下である。

複数階層のディレクトリを検索するとき中間ディレクトリのアクセス許可チェックを避けるために、NTFSによって使用されます。
(「インサイドWindows 第7版 上」p.751より引用)

とあるディレクトリ内にファイルに存在していたとして、そのファイルにアクセスする際、中間ディレクトリのアクセスチェックはカットされると述べている。 つまり、SeChangeNotifyPrivilegeが有効であれば、ディレクトリにはアクセス拒否されていても、ファイルにアクセス許可されていれば、アクセスは成功となる。 こういう特殊なファイル構造はあまり見かけるものではないが、以下のようにすれば作ることができる。

CreateDirectory(DUMMY_DIRECTORY, NULL);

HANDLE hFile = CreateFile(DUMMY_FILE, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
CloseHandle(hFile);

// FILE_TRAVERSEに対するアクセス拒否ACEを設定
InitializeAcl(pDacl, 1024, ACL_REVISION);
AddAccessDeniedAceEx(pDacl, 1, 0, FILE_TRAVERSE, pTokenUser->User.Sid);

// ファイルにDACLを設定
SetNamedSecurityInfo((LPWSTR)DUMMY_DIRECTORY, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, pDacl, NULL);

ディレクトリ内アクセスは、FILE_TRAVERSEアクセス権で識別される。 これに対するアクセス拒否ACEを配置するということは、アカウントにディレクトリ内アクセスを禁止する意味を持つが、 SeChangeNotifyPrivilegeが有効であればディレクトリのセキュリティ記述子は無視される。

参考文献