レベル: 初級 Nathan Harrington (harrington.nathan@gmail.com), Programmer, IBM
2008年 3月 18日 GDM (GNOME Display Manager) を変更して、キーストローク・ダイナミックス処理によるユーザー検証をサポートできるようにしてください。そのためには、ユーザー名を入力するときのキーストローク・パターンの一方向暗号化であるハッシュを作成して保管します。そして現行のキーストローク・パターンを読み取り、その特徴が一致するとユーザーのログインを許可するためのコードを GDM に追加します。
今日の商用製品の多くは、Linux® システムでの 2 因子認証を提供しています。一般的に、これらの技術を使うには、別途ハードウェアを購入し、多くの環境には適さない限定的な実装を作成しなければなりませんが、この記事で紹介するコードとプロセスを使用すれば、ユーザーが GDM にパスワードを入力するときの特徴をベースとした低価格の認証入力システムを実装することができます。前例のない実装となるこの GDM の変更によって、コンピューターのセキュリティーを強化してください。
 |
GDM とは何か
GNU プロジェクトの一環である GNOME では、GDM (GNOME Display Manager) を、接続されたディスプレイとリモート・ディスプレイを管理するために必要なすべての重要機能を実装するソフトウェアと定義しています。これらの機能には、ユーザーの認証、ユーザー・セッションの開始、そしてユーザー・セッションの終了が含まれますが、この記事で説明するように、GDM を拡張してキーストローク・ダイナミックスをサポートさせるのもわけありません。
|
|
要件
ハードウェア
過去 10 年間に製造されたコンピューターであれば、古いプロセッサーと多少の RAM しか搭載していないとしても、この記事で説明するコードをビルドし、使用するだけの能力を十分に備えています。この記事は Intel® Pentium® 4 プロセッサーを搭載した IBM® T30 およびT42 ThinkPad をベースに作成しましたが、それよりも処理の遅いハードウェアでも差し支えありません。
ソフトウェア
最小要件は標準 Linux ディストリビューション (Ubuntu V7.10 など) です。ソースから GDM をコンパイルするにはさまざまなライブラリーが必要になります。GDM のビルドに必要なライブラリーは GTK から Xinerama に至るまで数多くあるので、自分が使いやすいパッケージ・マネージャーを使用して前提条件を満たすようにしてください (「参考文献」を参照)。
GDM のソース・コードについては「参考文献」を参照してください。この記事で使用したのは gdm-2.20.0 リリースです。このリリースは、Ubuntu V7.10 ディストリビューションのアーカイブからは直接コンパイルされないことに注意してください。プロセスを完了させるために Make ファイルに対して行う多少の変更については、この後説明します。
GDM に単純なキーストローク・ダイナミックスを採り入れる一般的手法
生体認証技術の 1 つであるキーストローク・ダイナミックスは、比較的あいまいなものです。虹彩スキャンや指紋とは異なり、その入力パターンを極めてたくさん繰り返し行った人でさえも、その毎回の入力パターンには微妙な違いがあります。したがって、キーストローク・ダイナミックスを認証、あるいは検証というコンテキストで用いる上での課題は、許容可能な入力パターンの違いと不正なクレデンシャルとを識別することです。
以下の図 1 を見てください。ここには、著者が「nathan」と入力する際、各キー入力の間でキーがリリースされていた時間を示してあります (最後の「n」が測定されていない理由は、これがストリングの最終文字だからです)。
図 1. 「nathan」というキー入力でのキーのリリース間隔
ご覧のように、このユーザーがこの特定のテキストを入力する場合、キーのリリース間隔は比較的規則的です。INDEX 19 での急峻な上昇は次のキーを入力するまでの休止が長かったことを示しますが、その他の入力に関してはほとんど同じ間隔として分類できます。したがって、平均的なユーザーが特定のキー・シーケンスの入力間隔とまったく同じ間隔で入力できないことをカバーするには、「ほとんど同じ」一致を許容するアルゴリズムが必要となります。
この記事での設計決定とテストは、いずれもネットワークを介した接続ではない GDM ログインというコンテキストで行われています。そのため、キーストローク・ダイナミックスを使用したリモート・ログインを実装する場合は慎重に行ってください。信頼性の高い検証を実現するには、ネットワーク遅延やその他多くの変数を事前に考慮しなければなりません。
この記事では、標準 Linux 認証プロセスを置き換えるのではなく、ユーザー名をベースとしたもう 1 つの検証層を追加するという手法を取ります。ユーザー名とパスワードは変わらないままなので、追加という手法であれば、この検証層を既存のセキュリティー構成に簡単に統合できます。また、アカウント・ロックアウトとパスワードの変更プロセスにも影響しないため、他に影響を与えることなくセキュリティー・ポリシーへ追加することができます。
ダイナミックスの作成と練習を目的とした xevKeyDyn.pl の変更
GDM ソースの変更に取り掛かる前に、キーストローク・ダイナミックスを作成して練習するためのテスト・プログラムを構築することを是非ともお勧めします。使用するテスト・プログラムは developerWorks の記事「キーストローク・ダイナミックスを利用してテキスト入力のオプションを広げる」(「参考文献」を参照) で初めて取り上げた xevKeyDyn.pl プログラムから派生したものです。xev と xevKeyDyn.pl を正しくインストールして構成するための手順は、この記事に記載されています。「キーストローク・ダイナミックスを利用してテキスト入力のオプションを広げる」に記載された手順を完了してから以降に説明するテスト・プログラムの構築を開始してください。あるいは「ダウンロード」セクションにスキップして新しい practice_xevKeyDyn.pl プログラムを入手するのでも構いません。
xevKeyDyn.pl プログラムについての復習
xevKeyDyn.pl プログラムは、イベントに関するさまざまなキーストロークとその間隔を測定するために設計されていることを思い出してください。以降の変更は、キーのリリース間隔のみを測定すること、そしてキーストロークの練習に役立つインターフェースを提供することに重点を置いています。
リリース間隔のみを検出するための変更
25 行目以降を、リスト 1 に記載するように変更してください。
リスト 1. xevKeyDyn.pl のメイン・ロジックの変更
## remove lines:
printPassword( \@keysOne );
readPassword( \@keysTwo, "Confirm the");
printPassword( \@keysTwo );
my $catch = <STDIN> #initial password catch
$catch = <STDIN> #confirmation password catch
next unless( charactersMatch() );
next unless( totalTimeOK() );
next unless( hasNonPrintable() );
next unless( individualKeyTimingsOK() );
$passwordOK = 1;
## insert line:
printCharacteristics();
|
31 行目にリスト 2 のコードを挿入し、メイン・ループで呼び出される printCharacteristics サブルーチンを作成します。
リスト 2. printCharacteristics サブルーチン
sub printCharacteristics
{
my $count = 0;
my $sig1 = "";
while( $count < ($#keysOne) )
{
my( $time1, undef, undef) = split " ", $keysOne[$count];
my( $time2, undef, undef) = split " ", $keysOne[$count+1];
my $timeDiff_0 = $time2 - $time1;
$sig1 .= substr( $timeDiff_0, 0, length($timeDiff_0)-1) . " ";
$count++;
}
$sig1 = substr($sig1,0,length($sig1)-1);
map { print "", (split " ", $_)[1]; } @keysOne;
my $val =`echo "$sig1" | mkpasswd -H md5 --stdin`;
print " ", substr($val,0,11) , " ";
print substr($val,11);
print "\n";
print "$sig1\n";
}#printCharacteristics
|
上記の printCharacteristics (および以降の GDM の変更) に記載されている内容は、キーのリリース間隔の最下位桁を削除するというものです。キーのリリース時間はほとんどの場合、数百ミリ秒単位で測定されますが、キー・リリースがミリ秒まで正確に一致することはめったにないからです。この特性を利用して時間差の結果から最下位桁を削除すると、データ・ポイントは有効でありながらも、その固有性は緩和されます。例えば、「n」と「a」の文字入力の時間差が 235 ミリ秒の場合、記録される値は「23」となります。その後の処理では、この相対的曖昧さを利用して、より正確な突き合わせを行います。
次に、リスト 3 に記載する変更を行って、キーのリリースのみが記録されるようにします。
リスト 3. キー・リリースのみの読み取り
#change line:
if( $inLine =~ /KeyPress event/ || $inLine =~ /KeyRelease event/ )
#to:
if( $inLine =~ /KeyRelease event/ )
|
最後に行う以下のささいな変更は、特定のキーボード構成によっては必要になると思います。
リスト 4. 確実に時間を取得して表示するための処理
#change lines:
# get the time entry
my $currTime = <XEV>
#to:
# get the time entry
my $currTime = <XEV>
# certain configurations require this additional read
$currTime = <XEV> if( $currTime !~ /time / );
|
以上の変更を practice_xevKeyDyn.pl に保存したら、以下のキーストローク・ダイナミックス「シグニチャー」をビルドして練習するための手順に進んでください。
測定値を出力してキー・リリース間隔を練習する
「キーストローク・ダイナミックスを利用してテキスト入力のオプションを広げる」(「参考文献」を参照) で説明したように、xwininfo を使用してローカル端末のウィンドウ ID を取得し、perl practice_xevKeyDyn.pl <windowId> と入力して実行してください。キーストローク・ダイナミックス・シグニチャーの作成対象とするユーザー名を入力し、Enter を 2 回押して作成された暗号ハッシュとキー・リリース間隔を表示します。リスト 5 は、practice_xevKeyDyn.pl プログラムの出力例です。
リスト 5. practice_xevKeyDyn.pl の出力例
Enter a password:
nathan
nathan $1$Ag51/gt5 $WBcCKPxP5xnbDu2S5BbNt.
24 26 13 23 11
Enter a password:
nathan
nathan $1$gva6aFYD $oZhayCi3AdVXsuyQ0uBFg0
26 26 11 22 11
Enter a password:
|
テストのために、0.2 秒間隔でキーをリリースするなど、均一で簡単に繰り返すことのできる間隔のパターンを作成するように努めてください。最後のキーを押してから Enter キーを押すまでのリリース間隔は記録されないため、必ず (合計文字数 - 1) 個の数値が出力されることに注意してください。
/etc/shadow.dynamics ファイルを作成する
繰り返し練習したキー・リリース間隔の設定が終わったら、今度は GDM の変更で読み取るリポジトリーを作成します。望ましい間隔を表す暗号ハッシュ (nathan $1$gva6aFYD $oZhayCi3AdVXsuyQ0uBFg0 など) をコピーし、これを新しい行として /etc/shadow.dynamics ファイルに挿入します。さらに chmod o-r /etc/shadow.dynamics というコマンドを実行してこのファイルのアクセス権を変更し、GDM プログラムがこのファイルを読み取れるように chown gdm /etc/shadow.dynamics というコマンドを実行して所有者を変更します。この挿入プロセスは、キーストローク・ダイナミックス検証を有効にするユーザーすべてについて繰り返してください。ただし、このプロセスは十分にテストされた堅牢なソリューションではありません。ここに記載するコードと保管メカニズムにはさまざまなセキュリティーの脆弱性が存在する可能性があるので、お使いのマシン以外で使用する場合にはご注意ください。
キーストローク・ダイナミックスをサポートするための GDM の変更
キーストローク・ダイナミックスと現行のリリース間隔の読み取り、処理を設定する
キーストローク・ダイナミックス・シグニチャーが用意できたので、今後は GDM ソースを変更して、キーストローク・ダイナミックスの基準を満たした場合にのみログインを許可するようにします。gdm-2.20.0 ソースを解凍し、gui/gdmlogin.c の 66 行目に以下のライブラリー定義と変数宣言を追加してください。
リスト 6. gdmlogin.c の変数とデータ構造体
#include <crypt.h>
int g_in_username_mode = 1; // check only username dynamics
int g_dynamics_loaded = 0; // loaded /etc/shadow.dynamics file ok
int g_name_index = -1; // current username index in /etc/shadow.dynamics
int g_matched_dynamics = 0; // username dyanmics match the stored dynamics
guint32 g_release_time[500]; // maximum five hundred key release events
int g_release_count = 0; // total number of key release events
int g_sig[500]; // maximum five hundred intra key timings
int g_range = 2; // search range for dynamics match
int g_user_count = 0; // current number of users in /etc/shadow.dynamics
struct g_user
{
char username[50];
char salt[50];
char password[50];
char salty_password[50];
};
struct g_user g_user_dynamics[500]; // maximum five hundred users
|
GDM コードが示しているように、「状態を維持しながらイベントで制御するのは難しい」ため、キーストローク・ダイナミックス関数を処理するための独自の状態監視変数とデータ構造体を追加しています。次に、以下の関数定義を 209 行目に追加します。
リスト 7. gdmlogin.c の関数宣言
void load_shadow_dynamics(void);
char * check_dynamics( char *, int);
|
データ構造体、状態変数、そして関数定義を所定の位置に配置したら、3163 行目に load_shadow_dynamics 関数を追加します。
リスト 8. load_shadow_dynamics 関数
void load_shadow_dynamics(void)
{
FILE *fp;
struct stat f_status;
int retcode;
char *shadow_file;
shadow_file = "/etc/shadow.dynamics";
fp = NULL;
VE_IGNORE_EINTR (retcode = g_lstat (shadow_file, &f_status));
// make sure it's a 'normal' file
if (retcode == 0)
{
if (S_ISREG (f_status.st_mode))
{
g_dynamics_loaded = 1;
VE_IGNORE_EINTR (fp = fopen (shadow_file, "r"));
}
}
if( fp != NULL )
{
char * line = NULL;
size_t len = 0;
ssize_t read;
while((read = getline(&line, &len, fp)) != -1 )
{
char username[50] = "";
char salt[50] = "";
char password[50] = "";
if( sscanf( line, "%s %s %s", username, salt, password) == 3 )
{
sprintf(g_user_dynamics[g_user_count].username, "%s", username);
sprintf(g_user_dynamics[g_user_count].salt, "%s", salt);
sprintf(g_user_dynamics[g_user_count].password, "%s", password);
sprintf(g_user_dynamics[g_user_count].salty_password,
"%s%s", salt, password);
g_user_count++;
}else
{
g_dynamics_loaded = 0;
}//if incorrect dynamics file
}//while line in
}//if file pointer is not null
VE_IGNORE_EINTR (fclose (fp));
}//load_shadow_dynamics
|
load_shadow_dynamics の最初の部分では、ファイル読み取り変数を設定し、ファイルの可読性に関する sanity チェックを行います。/etc/shadow.dynamics ファイルのフォーマットは「username salt password」で、while ループはこのファイルの各行を g_user_dynamics データ構造体に読み込みます。
ここでいよいよ、shadow.dynamics ファイルのロードに基づくユーザー・フィードバックを 1379 行目で有効にします。
リスト 9. ユーザー名領域のダイナミックス・フィードバック
// change:
gtk_label_set_text_with_mnemonic (GTK_LABEL (label), _("_Username:"));
// to:
if( g_dynamics_loaded == 0 )
gtk_label_set_text_with_mnemonic (GTK_LABEL (label),
_("_Username: (dynamics misconfigured)"));
else
gtk_label_set_text_with_mnemonic (GTK_LABEL (label), _("_Username:"));
g_release_count = 0;
g_in_username_mode = 1;
|
上記と同様のフィードバックを 1414 行目に追加してください。
リスト 10. パスワード領域のダイナミックス・フィードバック
// change:
gtk_label_set_text_with_mnemonic (GTK_LABEL (label), _("_Password:"));
// to:
if( g_dynamics_loaded == 0 )
gtk_label_set_text_with_mnemonic (GTK_LABEL (label),
_("_Password: (dynamics misconfigured)"));
else
gtk_label_set_text_with_mnemonic (GTK_LABEL (label), _("_Password:"));
g_in_username_mode = 0;
|
3239 行目に以下の行を挿入して dynamics ファイルのロードを呼び出します。
リスト 11. load_shadow_dynamics の呼び出し
最後に、各キー・リリース・イベントが記録されるように設定するため、2135 行目に以下の行を追加します。
リスト 12. キー・リリース時間の記録
if( g_in_username_mode == 1 )
{
g_release_time[g_release_count] = event->time;
g_release_count++;
}
|
GDM でキーストローク・ダイナミックスを処理して突き合わせを行う
ここまでのところで、プログラムは /etc/shadow.dynamics からキーストローク「シグニチャー」を読み取り、すべてのキー・リリース時間を該当するデータ構造体に配置するようになっています。次の 2 つのコード・リストは、現行のキー・リリース時間を処理し、既存のシグニチャーとの一致を検索するためのものです。まずは、リスト 13 のコードを 889 行目に追加してください。
リスト 13. メイン・ロジック・フローのダイナミックス・チェックの設定
g_matched_dynamics = 0;
if( g_in_username_mode == 1 )
{
int i =0;
g_name_index = -1;
for( i=0; i< g_user_count; i++ )
{
if( strcmp( g_user_dynamics[i].username,
gtk_entry_get_text(GTK_ENTRY(entry)) ) == 0 )
{
g_name_index = i;
i = g_user_count; // to exit the loop
}
}//for each username read from file
// require at least two key presses to measure
if( g_name_index != -1 && g_release_count >= 2 )
{
i=0;
for( i=0; i< g_release_count-1; i++ )
{
char sig_num[50] = "";
sprintf( sig_num, "%u", abs(g_release_time[i] - g_release_time[i+1]) );
char clip_str[50] = "";
strncat( clip_str, sig_num, strlen(sig_num)-1 );
g_sig[i] = atoi( clip_str );
}//for each i
check_dynamics("", 0);
}
if( g_matched_dynamics != 1 )
{
// name not found, set username to garbage value to ensure no-login
// this way you can't defeat keystroke dynamics checks by removing an
// entry from the /etc/shadow.dynamics file
// -comment this line out if you want to enable non-dynamics protected
// logins
gtk_entry_set_text (GTK_ENTRY (entry), " ");
}
}// if in username mode
|
ユーザーが OK をクリックするか、または Enter を押してユーザー名からパスワードの入力に移ると、上記に記載したロジック・ブロックが実行されます。最初の for ループは、/etc/shadow.dynamics にロードされたユーザー名のリストで現行のユーザー名を検索します。一致するユーザー名が見つかった場合には、次の for ループでキーストローク間隔の最下位桁を廃棄し、その間隔の値を使って新しい g_sig 配列を作成します。現行の間隔のシグニチャーが作成されると、check_dynamics 関数が呼び出され、この関数が、現行シグニチャーとの差が g_range 以内に収まるすべてのシグニチャーの組み合わせをウォークスルーします。次に 875 行目に挿入するリスト 14 のコードは、check_dynamics 関数を作成するためのものです。
リスト 14. check_dynamics 関数
char * check_dynamics( char * in_string, int level )
{
int start = g_sig[level] - g_range;
int stop = g_sig[level] + g_range;
int curr = start;
// eliminate the g_matched_dynamics != 1 check to enforce consistent
// processing time regardless of when the match is found. In theory, this
// will reduce the ability to perceive a dynamics match due to processing
// time - which becomes much more effective for longer usernames or greater
// g_range values
while( curr <= stop && g_matched_dynamics != 1 )
{
if( level == (g_release_count-2) )
{
// if deepest level, perform match check
char current_timing[500];
char current_salt[50];
sprintf( current_timing, "%s %d", in_string, curr);
sprintf( current_salt, "%s", g_user_dynamics[g_name_index].salt);
char * crypt_pass = crypt( current_timing, current_salt);
if( strcmp(crypt_pass, g_user_dynamics[g_name_index].salty_password) == 0)
g_matched_dynamics = 1;
}else
{
// append to the current 'signature', go to next level
char test_pass[500] = "";
if( strlen(in_string) != 0 )
sprintf(test_pass,"%s %d", in_string, curr);
else
sprintf(test_pass,"%d", curr);
check_dynamics( test_pass, level+1);
}//if at maximum level
curr++;
}//while current < stop
return("");
}//check_dynamics
|
check_dynamics 関数は再帰的に自分自身を呼び出し、g_range パラメーターで定義されたすべての可能性を含めたシグニチャーを作成します。各 in_string 変数は、単一のキーのリリース時間からユーザー名として記録されたその各文字のキー・リリース時間まで、レベルごとに作成されます。
例えば、入力したキーに対応するリリース時間が「20 20 20 20 20」である場合、check_dynamics 関数は必要な置き換えを行って「18 18 18 18 18」から「22 22 22 22 22」までのすべてのリリース時間をチェックします。
大きなコメント・ブロックを読み取った後の、あいまいな突き合わせ (高い g_range 値) や長いユーザー名の突き合わせでは、すべての可能性をチェックするために必要な時間が大幅に増えることに注意してください。攻撃者はこの処理に長くかかるという性質を突いて、比較的短い処理時間を探す可能性があります。処理全体にかかる時間が他のほとんどの場合と比べてある程度以上短い場合、ユーザーが適切なリリース時間で入力したことを確信できるからです。g_matched_dynamics != 1 のチェックを削除すると、一致が見つかったとしても処理は続行されるという犠牲はありますが、このようなアタックが不可能になります。
コードの作成と使用方法
前述したように、gdm-2.20.0 コードはアーカイブから直接コンパイルされません。./configure を実行した後に make を実行すると、以下に示すようなエラー・メッセージが表示されるはずです。
リスト 15. GDM ビルド・エラーの例
make all-recursive
make[1]: Entering directory `/home/nathan/gdm-2.20.3'
Making all in po
make[2]: Entering directory `/home/nathan/gdm-2.20.3/po'
file=`echo af | sed 's,.*/,,'`.gmo \
&& rm -f $file && -o $file af.po
/bin/sh: -o: not found
make[2]: *** [af.gmo] Error 127
make[2]: Leaving directory `/home/nathan/gdm-2.20.3/po'
make[1]: *** [all-recursive] Error 1
make[1]: Leaving directory `/home/nathan/gdm-2.20.3'
make: *** [all] Error 2
|
この問題を排除するには、gdm-2.20.0/Makefile の 331 行目をリスト 16 に記載するように変更する必要があります。
リスト 16. GDM Makefile の変更
# change:
SUBDIRS = \
po \
config \
# to:
SUBDIRS = \
config \
|
この変更によって複数言語での GDM サポートは取り除かれますが、英語は変わらずサポートされます。ここまでやれば、自分の母国語でのサポートがなくても GDM Greeter について理解できる可能性が高くなります。そこで、最終ビルド・プロセスを変更して 555 行目に $(EXTRA_DAEMON_LIBS) \ を挿入し、gdmlogin.c を作成する際に crypt.h サポートを有効にします。
もう 1 度 make を実行し、その後に続けて make install を実行してください。これによって、新しくビルドされた GDM バージョンが /usr/local/sbin/ にインストールされます。このシステムを Ubuntu V7.10 でテストするには、runlevel 1 と入力し、できれば -nodaemon オプションを指定した上で GDM をルートとして起動します。自分の (十分に練習した) 間隔に従ってユーザー名を入力するため、期待する値と一致させるだけの正確さでユーザー名を入力するには、何度か入力を繰り返すことになるはずです。
まとめとその他の例
この記事では、リリース・イベントに対してのみ、キー操作の時間間隔をサポートするように、GDM を変更しました。遠慮なく、あなたのパスワードを友達に明かしてください。そのユーザー名を入力するときに求められる正確な入力方法を知らなければ、GDM を使ってログインすることはできないはずです。この記事、そして「キーストローク・ダイナミックスを利用してテキスト入力のオプションを広げる」の記事で説明したアーキテクチャーを利用して、全体的なユーザー名入力チェック、印字不能文字の要件、あるいはキーストローク関連の機能を追加し、GDM キーストローク・ダイナミックスのサポートをさらに拡張してください。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Sample code | os-identify_gdmDynamics_0.1.zip | 27KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
- GDM V2.20.0 のソース・コードは Gnome.org、Debian.org、または Georgia Tech からダウンロードしてください。
- Ubuntu などの主要なディストリビューションは、GDM をコンパイルするために必要なビルド環境を開発し、前提条件をインストールする上で優れた管理一式となります。Ubuntu をダウンロードして、まずはこの Linux ディストリビューションを試してみてください。
- テストを簡易化する X Window System パッケージ、xev をダウンロードしてください。
-
IBM ソフトウェアの試用版を使用して、次のオープン・ソース開発プロジェクトを革新してください。ダウンロード、あるいは DVD で入手できます。
-
IBM 製品の評価版をダウンロードして、DB2®、Lotus®、Rational®、Tivoli®、および WebSphere® のアプリケーション開発ツールとミドルウェア製品を使ってみてください。
議論するために
著者について  | 
|  | Nathan Harrington は IBM のプログラマーで、現在は Linux とリソース探索技術に取り組んでいます。 |
記事の評価
|