EternalWindows
アカウント管理 / パスワードフィルタDLL

システム内で生じるパスワードの設定や変更は、 パスワードフィルタDLLと呼ばれるDLLを開発することによって検出することができます。 この方法を理解しておけば、類推しやすいパスワードが入力された場合に変更を拒否したり、 他のパスワードも関連して変更するようなことが可能になります。 後者の方法の具体例としては、データベースへのログオンに使用するパスワードが システムにログオンするパスワードと同一の場合に、 データベースのパスワードも変更することで、 両方のパスワードを常に同一に維持するという設計が考えられます。

パスワードフィルタDLLの開発で注意しなければならないのは、 このDLLがLSA(lsass.exe)というシステムプロセスによってロードされるという点です。 仮に、開発したDLLにメモリ違反を生じさせるコードが含まれていれば、 そのDLLをロードしているLSAが強制終了してしまうことになり、非常に問題となります。 また、パスワードフィルタDLLが制御を返さなければ、 パスワードの変更を行う関数なども制御を返しませんから、 時間の掛かる処理を行うことは避けるべきといえます。

LSAは、起動時にパスワードフィルタDLLが登録されているかを確認し、 登録されているDLLを自身のアドレス空間にロードします。 そして、そのDLLのInitializeChangeNotifyを呼び出し、 DLLにデータ等を初期化する機会を与えます。

BOOLEAN InitializeChangeNotify(void);

この関数に引数はありません。 戻り値は、DLLの初期化が成功した場合にTRUEを返し、 失敗した場合にFALSEを返します。 FALSEを返した場合はDLLがアンロードされ、後続の関数は呼ばれません。

パスワードがアプリケーションによって変更されようとしている場合は、 PasswordFilterという関数が呼ばれることになります。 この関数では、ユーザーのパスワードの変更を許可するかどうかを決定します。

BOOLEAN PasswordFilter(
  PUNICODE_STRING AccountName,
  PUNICODE_STRING FullName,
  PUNICODE_STRING Password,
  BOOLEAN SetOperation
);

AccountNameは、パスワードを変更しようとしているユーザーの名前が格納されます。 FullNameは、パスワードを変更しようとしているユーザーのフルネームが格納されます。 Passwordは、変更しようとしているパスワードが格納されます。 SetOperationは、パスワードが設定されたどうかを表す値が格納されます。 たとえば、NetUserSetInfoでパスワードを設定しようとした場合は1となり、 NetUserChangePasswordでパスワードを変更しようとした場合は0となります。 戻り値は、パスワードをフィルタするつもりがない場合はTRUEを返し、 フィルタする場合はFALSEを返します。 FALSEを返した場合は、後続のPasswordChangeNotifyが呼ばれることはありません。

PasswordFilterでTRUEを返し、さらに他のパスワードフィルタDLLもパスワードの変更を許可したならば、 パスワードは実際に変更されることになります。 この変更を通知する際に、PasswordChangeNotifyが呼ばれることになります。

NTSTATUS PasswordChangeNotify(
  PUNICODE_STRING UserName,
  ULONG RelativeId,
  PUNICODE_STRING NewPassword
);

UserNameは、パスワードを変更したユーザーの名前を指定します。 RelativeIdは、ユーザーのRIDが格納されます。 NewPasswordは、新しいパスワードが格納されます。 この文字列は、PasswordFilterのPasswordと同一であるはずです。 戻り値は、STATUS_SUCCESS(0)を返します。

パスワードフィルタDLLを開発したら、そのDLLをsystem32フォルダにコピーし、 次に示すレジストリキーのNotification Packagesエントリに名前を登録します。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa

エントリには、登録したいDLLの名前を書き込みます。 たとえば、登録したいDLLがsample.dllならば、拡張子を除いたsampleという名前を書き込みます。 既に何らかのDLLが書き込まれている場合は、空白の後にDLLの名前を書き込みます。 たとえば、scecliという名前が書き込まれているならば、 scecli sampleというようにします。 これで後はシステムをシャットダウンすれば、 次回起動時には登録したDLLがLSAによってロードされることになります。

次に、今回のdefファイルを示します。

EXPORTS
	InitializeChangeNotify
	PasswordFilter
	PasswordChangeNotify

このように、LSAに呼び出してもらう関数をEXPORTSキーワードでエクスポートしておきます。 次に、DLLのコードを示します。

#include <windows.h>
#include <ntsecapi.h>

void WriteLogFile(LPTSTR lpszData);

BOOLEAN NTAPI InitializeChangeNotify(void)
{
	WriteLogFile(L"InitializeChangeNotify\r\n");

	return TRUE;
}

BOOLEAN NTAPI PasswordFilter(PUNICODE_STRING AccountName, PUNICODE_STRING FullName, PUNICODE_STRING Password, BOOLEAN SetOperation)
{
	if (Password->Length < 4 * sizeof(WCHAR)) {
		WriteLogFile(L"PasswordFilter-FALSE\r\n");
		return FALSE;
	}
	
	WriteLogFile(L"PasswordFilter-TRUE\r\n");

	return TRUE;
}

NTSTATUS NTAPI PasswordChangeNotify(PUNICODE_STRING UserName, ULONG RelativeId, PUNICODE_STRING NewPassword)
{
	WCHAR szPassword[256];
	WCHAR szBuf[1024];
	
	lstrcpynW(szPassword, NewPassword->Buffer, (NewPassword->Length / sizeof(WCHAR)) + 1);

	wsprintf(szBuf, L"PasswordChangeNotify-%s\r\n", szPassword);
	WriteLogFile(szBuf);

	SecureZeroMemory(szPassword, sizeof(szPassword));
	SecureZeroMemory(szBuf, sizeof(szBuf));

	return 0;
}

void WriteLogFile(LPWSTR lpszData)
{
	TCHAR  szFilePath[] = TEXT("c:\\passwordlog.txt"); 
	HANDLE hFile;
	DWORD  dwResult;		

	hFile = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return;
	
	SetFilePointer(hFile, 0, NULL, FILE_END);
	WriteFile(hFile, lpszData, lstrlenW(lpszData) * sizeof(WCHAR), &dwResult, NULL);

	CloseHandle(hFile);
}

InitializeChangeNotifyは、LSAがDLLをロードした際に1度だけ呼び出されます。 パスワードが変更されるたびに呼ばれるわけではないので注意してください。 WriteLogFileという自作関数は、Cドライブ直下にログファイルを作成する関数です。 LSA内のコードはシステムカウントとして実行されているため、 ファイルへの書き込みでアクセスが拒否されることはないはずです。 ファイルには実行された関数の名前が書き込まれていくため、 パスワードを変更した後にこのファイルを確認することで、 実際に処理を行えているかを調べることができます。 次に、PasswordFilterの内部を示します。

BOOLEAN NTAPI PasswordFilter(PUNICODE_STRING AccountName, PUNICODE_STRING FullName, PUNICODE_STRING Password, BOOLEAN SetOperation)
{
	if (Password->Length < 4 * sizeof(WCHAR)) {
		WriteLogFile(L"PasswordFilter-FALSE\r\n");
		return FALSE;
	}
	
	WriteLogFile(L"PasswordFilter-TRUE\r\n");

	return TRUE;
}

PasswordFilterは、パスワードの変更を許可するかどうかを決定する関数です。 今回は、パスワードの文字数が4文字未満である場合は、変更を許可しないということで、 Password->Lengthが4 * sizeof(WCHAR)より低いかどうかを確認しています。 sizeof(WCHAR)を掛けているのは、 Lengthに格納されているのが文字列の文字数ではなくサイズであるからです。 PasswordFilterでTRUEを返し、さらに他のDLLのPasswordFilterでもTRUEが返された場合は、 パスワードが変更されたことを通知するためにPasswordChangeNotifyが呼び出されます。

NTSTATUS NTAPI PasswordChangeNotify(PUNICODE_STRING UserName, ULONG RelativeId, PUNICODE_STRING NewPassword)
{
	WCHAR szPassword[256];
	WCHAR szBuf[1024];
	
	lstrcpynW(szPassword, NewPassword->Buffer, (NewPassword->Length / sizeof(WCHAR)) + 1);

	wsprintf(szBuf, L"PasswordChangeNotify-%s\r\n", szPassword);
	WriteLogFile(szBuf);

	SecureZeroMemory(szPassword, sizeof(szPassword));
	SecureZeroMemory(szBuf, sizeof(szBuf));

	return 0;
}

PasswordChangeNotifyでは、変更されたパスワードをファイルに保存するため、 パスワードが格納されたNewPassword->Bufferを一度バッファにコピーします。 これは、NewPassword->Bufferに'\0'文字が含まれていないためです。 lstrcpynは、第3引数に指定した文字数だけ第1引数のバッファにコピーし、 バッファの終端に'\0'文字を追加するようになっています。 後はこのバッファを関数名と共にフォーマットし、WriteLogFileでファイルに書き込みます。 szPasswordとszBufには共にパスワードが格納されているため、 関数を終了する前にSecureZeroMemoryでバッファ内をクリアしておきます。


戻る