2008年12月26日金曜日

ActiveDirectoryへのパスワード変更をフックする

IdM案件をやっていると、どうしても出てくるのがパスワード同期のお話。
基本的にIdM側のパスワード変更画面を使ってもらうのがベターなのですが(システム構造的にも、機能面でも)、中にはやっぱりCTRL+ALT+DELでのパスワード変更がやめられない!というユーザさんもいらっしゃるのかと。

恐らくそのような要件を満たすためにパッケージベンダ各社はいろいろな努力をしていると思います。
例えば、
・Microsoft(Identity Lifecycle Manager 2007)
・SunMicrosystems(Sun Java System Identity Manager):
・SAP(NetWeaver Identity Management)
・エクスジェン・ネットワークス(LDAP Manager)
などではActiveDirectoryへのパスワード変更をIdM側へ取り込む機能を実装してきています。
※ILM"2"でのクライアント側へのモジュールインストールは反則かと・・・

今回はそのあたりの仕掛け(どうなっているのか?)をひも解いてみようと思います。
まず、Windowsのパスワード変更の仕組みを整理します。

ユーザからのパスワード変更要求があると、
1.Local Security Authority(LSA)が要求を受け付ける
2.レジストリに登録されているパスワードフィルタDLLでパスワード検査を行う
3.検査をパスしたらSecurity Account Manager(SAM)にパスワードを保管する
4.正常に保管できたらレジストリに登録されているパスワードフィルタDLLに変更通知を行う
という流れでパスワードが変更されます。













それぞれのプロセスを細かく見ていくと、以下のような流れになっています。

1.Local Security Authority(LSA)が要求を受け付ける
  lsass.exeというサービスプロセスが要求を受け付けます。

2.レジストリに登録されているパスワードフィルタDLLでパスワード検査を行う
  以下のレジストリに登録されているDLLに実装されているPasswordFilter()が順番に呼び出され、パスワードポリシーのチェックなどのフィルタリング処理が行われます。

  [レジストリエントリ]
  HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\Notification Packages











  この関数がFALSEを返すとその時点でパスワードポリシー違反としてユーザへ通知されます。







3.検査をパスしたらSecurity Account Manager(SAM)にパスワードを保管する
  これは文字通りアカウントDBへのデータ更新です。

4.正常に保管できたらレジストリに登録されているパスワードフィルタDLLに変更通知を行う
  これも2と同じく各DLLのPasswordChangeNotify()が呼び出されます。


最後のPasswordChangeNotify()の引数に対象ユーザのユーザ名とクリアテキストのパスワードがわたってくるのを利用して、あたかもActiveDirectoryのパスワードを吸い出しているような動きに見せている、というのが各IdM製品の実際の仕掛けです。(たぶん)
また、この動きを見るとわかりますが、あくまでlsass.exeがローカルのDLLに実装された関数を使うので、パスワード変更要求を受け付けるコンピュータ上にフィルタを配置する必要があります。
つまり、ローカルユーザのパスワードをフックする場合はローカルコンピュータ上に、ActiveDirectoryユーザの場合はすべてのドメインコントローラ上に配置する必要があります。

※ちなみに2のPasswordFilter()の引数にもユーザ名とパスワードはわたってきますが、実際にパスワードが更新される前ですし、他のライブラリを使ったパスワードポリシーチェックが完了していないのでこの段階で吸い出してもあまり意味がありません。


ということで、実際の動きをみるためにもDLLを実装してみました。(久しぶりにベタベタなアンマネージコードを書いたので恥ずかしいのですが・・・)

開発環境は、
・OS/AD:Windows Server 2003 R2
・開発ツール:Visual Studio 2005/VC++
です。

ビルドし、出来たDLLをC:\Windows\system32(%systemroot%\system32)以下にコピーし、上記のレジストリに出来上がったDLLの拡張子を除いたファイル名を登録してシステムを再起動すると配置完了です。
上手くいけばパスワードを変更するとCドライブ直下に「PasswordChangeLog.txt」というファイルが出来上がります。
中身は、
 sAMAccountName=変更したユーザ名,unicodePwd=新しいパスワード
という形で、パスワード変更がどんどん記録されていきます。
※悪用厳禁ですね。。。

以下、実際のコードです。

■PasswordHook.cpp

#include
#include
#include

#define LOGFILE "C:\\PasswordChangeLog.txt"

#ifndef STATUS_SUCCESS
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
#endif

//
// ログ出力
//
void WriteToLog(const char* str)
{
 if (NULL == str)
 {
  return;
 }
 FILE* log;
 log = fopen(LOGFILE, "a+");
 if (NULL == log)
 {
  return;
 }
 fprintf(log, "%s\r\n", str);
 fclose(log);
 return;
}

//
// パスワードの変更・保管に成功した際に呼び出される
//
NTSTATUS NTAPI PasswordChangeNotify(
  PUNICODE_STRING UserName,
  ULONG RelativeId,
  PUNICODE_STRING NewPassword
)
{
 int nLen=0;

 // 文字列変換(ワイド文字列→マルチバイト文字列)
 // UserName
 nLen = wcslen(UserName->Buffer)+1;
 LPSTR username = (LPSTR)malloc(nLen * sizeof(CHAR));
 ZeroMemory(username,nLen);
 wcstombs(username, UserName->Buffer, nLen);

 // NewPassword
 nLen = wcslen(NewPassword->Buffer)+1;
 LPSTR password = (LPSTR)malloc(nLen * sizeof(CHAR));
 ZeroMemory(password,nLen);
 wcstombs(password, NewPassword->Buffer, nLen);

 // 記録用文字列生成
 LPSTR LogEntry = (LPSTR)malloc(strlen(username)+strlen(password)+28);
 ZeroMemory(LogEntry,sizeof(LogEntry));
 sprintf(LogEntry,"sAMAccountName=%s,unicodePwd=%s",username,password);
 WriteToLog(LogEntry);

 return STATUS_SUCCESS;
}

//
// パスワード変更要求があった際に呼び出される
// パスワードポリシー等のフィルタがある場合はここに記載する
//
BOOL NTAPI PasswordFilter(
  PUNICODE_STRING AccountName,
  PUNICODE_STRING FullName,
  PUNICODE_STRING Password,
  BOOLEAN SetOperation
)
{
 return TRUE;
}

//
// DLLロード時に呼び出される(lsass.exeより呼び出される)
//
BOOL NTAPI InitializeChangeNotify(void)
{
 return TRUE;
}


■PasswordHook.def

LIBRARY "PasswordHook"

EXPORTS
 InitializeChangeNotify
 PasswordChangeNotify
 PasswordFilter

8 件のコメント:

匿名 さんのコメント...

記載されているパスワード変更をフックする方法は、マイクロソフトがどこかに公開しているのでしょうか?
もし、公開しているようでしたら、参考サイト等、教えてください。

Naohiro Fujie さんのコメント...

下記のKBが参考になります。
http://support.microsoft.com/kb/151082/ja
少し古い情報なのですが、基本的な仕組みは変わっていないようです。

Unknown さんのコメント...

記載の方法を試してみたのですが、XP SP3ではうまく自作DLLがロードされパスワードフィルタできましたが、Windows 2003 Server R2 SP2では自作DLLがロードされませんでした。

デフォルト設定されているscecli.dllなどはロードされています。

開発環境は、
OS: Windows XP SP3
ツール:Visual Studio 2008
です。

ポリシー設定
監査:システムイベントの監査→有効
パスワード:複雑性の要件・・・→有効

Windows 2003 Server R2 SP2は特殊な設定が必要なのでしょうか?

Naohiro Fujie さんのコメント...

おかしいですね。
私がWindows Server 2003R2SP2環境でやった時は正常に動きましたが。

・レジストリへのエントリ登録は行いましたか?
・ロードされない、というのはどのような状態でしょうか?

Unknown さんのコメント...

自己解決しました。

DLLのコンパイルミスだったようで、

 プロジェクトの設定
 C/C++ → コード生成 → マルチスレッド(/MT)

でコンパイルしたところ、無事自作DLLがロードされました。

こんどる さんのコメント...

突然の投稿失礼します。ActiveDirectoryパスワードフック機能、参考にさせて頂きまして、Visual Studio 2005 でDLLを作成し、無事

・Windows 2003 Server R2 SP2 (32bit)

で動作させることができました。
ところが、同プロジェクトを「x64」対応でビルドしなおして、

・Windows 2008 Server R2 SP1 (64bit)

のActiveDirectory環境で動作させようとしたところ、どうもうまく動きません。

・レジストリへの登録&再起動
・C/C++ → コード生成 → マルチスレッド(/MT)

などは実施しております。

具体的には、InitializeChangeNotify もコールされていないように見受けられました。

64bitOSあるいはWindows 2008 環境での動作実績はございますでしょうか?
いろいろ試行錯誤しているのですが解決の目途が立たないため、突然でもうしわけございませんが、ご質問させていただいた次第です。

※不足情報あればご指摘いただけると幸いです。

Naohiro Fujie さんのコメント...

こんどる様
レスが遅くてすみません。
申し訳ありませんが、64ビット環境ではまだ試したことがありません。時間ができたら試してみるので、しばらくお時間をいただければと思います。

こんどる さんのコメント...

Naohiro Fujie さま

ご返信いただきましてありがとうございます。

その後、調査した結果、VC++のランタイムのバージョンが実行環境とビルド環境でお異なっていることでロードされなかったことがわかりました。

初歩的な原因でお恥ずかしいのですがご報告させていただきます。

ありがとうございました。