 | 级别: 初级 Nathan Harrington (harrington.nathan@gmail.com), 程序员, IBM
2008 年 4 月 17 日 修改 GNOME Display Manager (GDM) 支持通过击键力学处理进行用户验证。在输入用户名时创建和存储一种击键模式的加密散列。把代码添加到 GDM 中来读取当前击键模式并在特征匹配时允许用户登录。
目前许多商业产品都提供在 Linux® 系统中的双因素身份验证。这些技术通常要求购买其他硬件并创建无法适用于许多环境的限定实现。使用本文提供的代码和过程可以基于用户把密码键入到 GDM 中时表现的特征来实现低成本身份验证输入系统。从示例到实现,这里提供的对 GDM 的修改使您可以增强计算机的安全性。
 |
GDM 是什么?
GNOME 是 GNU 项目的一部分,它将把 GNOME Display Manager (GDM) 定义为实现管理附带显示器和远程显示器所需的所有重要功能的软件。这包括验证用户、启动用户会话和终止用户会话。正如本文所述,随时可以扩展 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 发行版的归档进行编译。下面介绍了如何略微更改 Makefile 来完成构造过程。
GDM 中的简单击键力学的一般方法
作为一种生物识别方法,击键力学相对来说不是很精确。不同于虹膜识别或指纹识别,即使是动作最有规律的个人,其键入模式中也有微妙的差异。在身份验证或检验环境中使用击键力学的挑战是把可接受的变化与不正确的凭证辩别开来。
考虑下面的图 1,该图显示了作者键入 “nathan” 的键释放时间(未测量拖尾的 “n”,因为它是字符串中的最后一个字符)。
图 1. “nathan” 的键释放时间
正如您所见,对于此特定用户和此特定文本,按键释放之间的延迟是相对有序的。注意在位置 19 上的峰值如何表示按键之间延长的停顿,但是其余的输入按相对接近的关系分组。因此,需要使用允许 “足够接近(close-enough)” 的匹配算法来弥补普通用户不能精确匹配给定的一组击键计时这个问题。
本文中的所有设计决策和测试均发生在 GDM 登录的非联网环境中。在用击键力学实现远程登录之前注意练习,因为要进行可靠验证,需要考虑网络延迟和许多其他变量。
本文中使用的方法不会取代标准的 Linux 验证过程,而是在用户名的基础上引入另一个验证层次。这样做将允许轻松地集成现有安全配置,因为用户名和密码保留不变。这种方法还允许透明添加到安全策略中,因为帐户锁定和密码更改过程将不受影响。
修改 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 键来显示构建的密码散列和键释放间隔。清单 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:
|
出于测试目的,尝试创建一个均匀、容易重复的计时模式,例如在每次键释放之间间隔十分之二秒。注意,在不记录最后一个按键与 Enter 键之间的释放时为何总输出数字(total characters -1)。
创建 /etc/shadow.dynamics 文件
具备了已经重复练习过的键释放间隔设置,应当为要读取的 GDM 修改创建一个库。复制表示首选计时的密码散列,例如 nathan $1$gva6aFYD $oZhayCi3AdVXsuyQ0uBFg0,并将其作为新行插入到 /etc/shadow.dynamics 文件中。用 chmod o-r /etc/shadow.dynamics 命令更改对 /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 的第一部分将设置文件读取变量并对文件的可读性进行全面检查。回想一下 /etc/shadow.dynamics 文件的格式为 “username salt password”,while 循环将把该文件的所有行读取到 g_user_dynamics 数据结构中。
现在在第 1379 行,在载入 shadow dynamics 文件后启用一些用户反馈。
清单 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 行中插入以下代码来调用力学文件的装载操作。
清单 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 中读取了击键 “签名” 并把每个键释放时间放置到相应的数据结构中。接下来的两个代码清单将处理当前的键释放时间并查找与现有签名的匹配。首先,将清单 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
|
在围绕 g_range 参数所定义的全部可能范围构建签名时,check_dynamics 函数将递归调用自身。从单一的键释放时间一直到用户名中每个已记录字母的键释放时间,逐级构建每个 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,因此做出最后的构建过程更改并在第 555 行中插入 $(EXTRA_DAEMON_LIBS) \ 以在构建 gdmlogin.c 时启用 crypt.h 支持。
再次运行 make,再运行 make install,然后您将得到在 /usr/local/sbin/ 中安装的新构建的 GDM 版本。要在 Ubuntu V7.10 上测试安装,请输入 runlevel 1 并以根用户身份启动 GDM,最好连同 -nodaemon 选项一起使用。记住,在根据计时(尽量进行良好实践)输入用户名时,可能要进行多次尝试来输入用户名,以便可以精确地匹配预期的值。
结束语
GDM 现在已经被修改为仅支持释放事件的键计时。继续尝试并把您的密码告诉一个朋友。使用 GDM 时,如果不知道输入用户名所需的精确键入方法,他们仍然不能够登录。使用本文描述的架构和 “用击键力学扩展文本输入选项” 文章中所述的架构,添加完整的用户名输入检查、不可打印字符要求或其他与击键相关的功能,从而进一步增强 GDM 击键力学支持。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 样例代码 | os-identify_gdmDynamics_0.1.zip | 27KB | HTTP |
|---|
参考资料 学习
获得产品和技术
讨论
关于作者  | |  | Nathan Harrington 是 IBM 的一位程序员,目前主要从事 Linux 和资源定位技术方面的工作。 |
对本文的评价
|  |