击键力学 是一个相对崭新的领域,可以通过对键入模式进行统计分析来识别使用键盘的人员。以前发布在 developerWorks 中的文章已经展示了如何将击键力学的概念集成到应用程序中,以及修改 Gnome Display Manager(GDM)以要求使用正确密码及 “正确键入的” 密码的实际示例。本文提供的代码和工具可以使您超越单一的击键力学应用,而是连续监视整个 X Window System 环境以掌握键入者的特有模式。
在阅读本文后,您将可以创建一个连续的击键力学监视器,该监视器可以在没有检测到特有键入模式时锁定 X Window System 会话。
所选方法的注意事项
跟踪每个键按下情况的最有效方法也许就是使用内核级键盘记录工具(例如 THC-vlogger 或 ttyrpld)。不幸的是,这些程序是为较旧的内核级别设计的,或者目前很难在现代 Linux® 发行版中使用。键盘设备跟踪程序(例如 uberkey)是另外一种好方法,但是其释放击键和计时的不精确性使其不适合本文介绍的应用。
虽然不适用于控制台或远程会话,但是 xev 为检测在 X Window System 中运行的任意应用程序的键盘事件提供了一种健壮且轻量级的方法。
xev 会话中的每个事件都是随高精度的时间值一同输出的。在本文中,我们将在具体时窗内使用该时间值记录 R、S 和 T 键的 “停顿” 时间。停顿时间是用户的手指按住按键的时间段。将为 X Window System 桌面中的每个应用程序记录这个相对简单的度量。
在开发击键 “签名” 时,最好使用大量数据。使用模式需要匹配最常见的计算机使用情况。用下面介绍的数据跟踪选项进行试验以获得各种样例使用数据。然后,开发的签名将被转换为密码散列并存储到磁盘中,以供稍后在监视阶段中进行比较。
硬件和软件要求
2000 年以后生产的所有 PC 都应当能够处理本文中提供的代码。您将需要 X Window System 以及 xev 程序(请参阅 参考资料)。您需要使用 mkpasswd 程序(附带于大多数 Linux 分发版中)才能生成击键签名的密码散列。需要 Perl 模块 X11::GUITest、threads 和 Thread::Queue。UNIX® 和 Linux 用户请注意:如果您刚开始安装 Perl 模块,使用 Andreas J. Konig 的 CPAN 模块可以自动安装其他模块(请参阅 参考资料)。
continuousKeystrokes.pl 程序
记录在 X Window System 会话中按下的每个击键的简单方法是启动与 xwininfo -root -tree 所列出的每个窗口绑定的 xev 程序。在理论上,这种方法将对于少量窗口有效,但是最终,将达到 X 客户机的最大数目,并且需要重新编译 X Window System 才能增加允许的 X 客户机数目。更合理的解决方案是跟踪持有焦点的当前窗口并把单个 xev 程序与该窗口绑定起来。然后将记录当前持有焦点的窗口的每个键盘事件。
清单 1 显示了用于跟踪当前焦点并创建击键签名的 continuousKeystrokes.pl 程序的开头。
清单 1. continuousKeystrokes.pl 变量声明
#! perl -w
# continuousKeystrokes.pl - monitor dwell time of r,s,t for all X Window System
use strict;
use X11::GUITest qw( :ALL );
use threads;
use Thread::Queue;
die "specify mode, minimum samples" unless _cnnew1@ARGV == 2 ;
my $sleepTime = 5; # seconds between key event processing runs
my %windows = (); # hash of window keystrokes
my @samp = (); # most recent sample averages of keystrokes
my $checkRng = 10; # fuzziness of dwell time matching
my $userMatch = 0; # user or impostor?
my %keys = (); # average of key dwell times
my $mode = $ARGV[0]; # record baseline or monitor matches
my $minSamples = $ARGV[1]; # required base samples to match with
my ( $salt, $hash ) = ""; # read from keystroke.Signatures
if( $mode eq "monitor" ){ loadSignatureFile() }
在模块 include 语句和初始变量声明之后,进入主控制循环。在处于监视模式下时,将装入先前生成的签名文件。清单 2 显示了主程序循环的开头。
清单 2. 主程序循环的开始部分
# ctrl-c to exit the program and drop the threads without error
while(1)
{
my @activeId = GetInputFocus();
my $foundPipe = 0;
for my $key ( keys %windows )
{
if( $key eq "@activeId" && $windows{$key}{pipeDef} == 0 )
{
my $res = "xev -id $key |";
$windows{$key}{ input } = createPipe( $res ) or die "no pipe ";
$windows{$key}{pipeDef} = 1;
$foundPipe = 1;
}#if not a match
}#for each windows key
if( $foundPipe == 0 )
{
# if pipe doesn't already exist, add a new one
my $key = "@activeId";
if( !exists($windows{$key}) || $windows{$key}{pipeDef} == 0 )
{
my $res = "xev -id $key |";
$windows{$key}{ input } = createPipe( $res ) or die "no pipe ";
$windows{$key}{pipeDef} = 1;
}#if pipe doesn't already exist
}#foundpipe check
第一个 for 循环将搜索目前没有绑定管道的窗口散列中的已有条目。如果找到这样一个条目,则创建一个管道。维护一个由目前拥有管道的窗口组成的可用工作列表,将允许在一段时间内收集 xev 输出。xev 输出是非缓冲的,这会造成较少使用的窗口无法以足够快的速度填充输出缓冲。为了让输出数据在窗口失去焦点后继续保留,然后重新获得这些数据,窗口散列将记录输出。清单 3 显示了主逻辑循环的其余部分。
清单 3. 主逻辑循环结束
# read any available date from a pipe
for my $xevPipe( keys %windows )
{
next unless( $windows{$xevPipe}{pipeDef} == 1 );
while( $windows{$xevPipe}{input}->pending )
{
my $line = $windows{$xevPipe}{input}->dequeue or next;
$windows{$xevPipe}{keyString} .= $line;
}#while data to be added to the buffer
next unless( exists( $windows{$xevPipe}{keyString} ) );
next unless( length( $windows{$xevPipe}{keyString} ) > 8192 );
compareSignature( getKeyAverages( $windows{$xevPipe}{keyString} ) );
$windows{$xevPipe}{keyString} = "";
}#for windows keys
# kill all xevs except currently monitored
for my $key ( keys %windows )
{
next unless( $key ne "@activeId" && $windows{$key}{pipeDef} == 1 );
$windows{$key}{pipeDef} = 0;
my $cmd = qq{ps -aef | grep $key | grep xev | perl -lane '`kill \$F[1]`'};
system($cmd);
}#for each windows key
sleep( $sleepTime );
}#while main loop
在创建了管道(或者如果已经存在一个管道)后,每个管道的输出将被读入到该窗口记录的事件变量中。如果记录了足够的数据,则整个缓冲将被传递给 getKeyAverages 子例程,然后再传递给 compareSignature 子例程。接下来,如果焦点事件发生更改,则终止旧的 xev 程序。
清单 4 显示了前两个子例程:loadSignatureFile 和 createPipe。
清单 4. loadSignatureFile 和 createPipe 子例程
sub loadSignatureFile
{
open(INFILE,"keystroke.signatures") or die "no signature file";
my $line =<INFILE>;
die "empty file " unless defined $line;
chomp($line);
( undef, undef, $salt, $hash ) = split '\$', $line;
close(INFILE);
}#loadSignatureFile
sub createPipe
{
my $cmd = shift;
my $queue = new Thread::Queue;
async{
my $pid = open my $pipe, $cmd or die $!;
$queue->enqueue( $_ ) while <$pipe>;
$queue->enqueue( undef );
}->detach;
# detach causes the threads to be silently terminated on program exit
return $queue;
}#createPipe
loadSignatureFile 只是读取程序的 “record” 模式中存储的 salt 和 hash 信息。这些值稍后将用于击键签名比较。createPipe 子例程是从使用线程的管道中进行无阻塞读取的简单方法。清单 5 显示了下一个子例程: getKeyAverages。
清单 5. getKeyAverages 子例程
sub getKeyAverages
{
my %temp = (); # temporary hash to record key press and release times
my %avg = (); # average for entire buffer read key press and release times
open(my $fh, '<', \$_[0]) or die "Could not open string for reading";
while(my $inLine = <$fh> )
{
next unless( $inLine =~ /KeyPress event/ || $inLine =~ /KeyRelease event/ );
my $state = (split " ", $inLine)[0];
# get type of entry
my $eventType = (split " ", $inLine)[0];
# get the time entry
my $currTime = <$fh>;
# make sure the line exists and has the required data
next unless( defined($currTime) );
next unless( length($currTime) > 43 );
$currTime = substr( $currTime, index($currTime,"time ")+5);
$currTime = substr( $currTime, 0, index($currTime,","));
# get the key name
my $currKey = <$fh>;
next unless( defined($currKey) );
next unless( length($currKey) > 40 );
$currKey = substr( $currKey, index($currKey,"keysym ")+7);
$currKey = substr( $currKey, 0, index($currKey,"),"));
$currKey = substr( $currKey, index($currKey, ", ")+2);
next unless( $currKey eq "r" || $currKey eq "s" || $currKey eq "t" );
# add the key press
if( $state eq "KeyPress" ){ $temp{$currKey} = $currTime }
next unless ( $state eq "KeyRelease" );
if( exists( $temp{ $currKey } ) )
{
$avg{$currKey}{val} += $currTime - $temp{$currKey};
$avg{$currKey}{count} ++;
}#if a press has been recorded
# either the data has been recorded or it was a release on a key never pressed
# in this window
delete $temp{ $currKey };
}#while file handle
close( $fh );
my( $rVal, $sVal, $tVal ); $rVal = $sVal = $tVal = 0;
if( exists( $avg{"r"} ) ){ $rVal = ($avg{"r"}{val} / $avg{"r"}{count}) };
if( exists( $avg{"s"} ) ){ $sVal = ($avg{"s"}{val} / $avg{"s"}{count}) };
if( exists( $avg{"t"} ) ){ $tVal = ($avg{"t"}{val} / $avg{"t"}{count}) };
return( $rVal, $sVal, $tVal );
}#getKeyAverages
xev 程序输出将在连接的窗口中列出每个 X Window System 事件。清单 6 是该程序输出的示例。
清单 6. xev 示例程序输出
KeyPress event, serial 16, synthetic NO, window 0x2000002,
root 0x76, subw 0x2000012, time 248543985, (719,86), root:(964,107),
state 0x0, keycode 27 (keysym 0x72, r), same_screen YES,
XLookupString gives 1 bytes: (72) "r"
XmbLookupString gives 1 bytes: (72) "r"
XFilterEvent returns: False
KeyRelease event, serial 16, synthetic NO, window 0x2000002,
root 0x76, subw 0x2000012, time 248544153, (719,86), root:(964,107),
state 0x0, keycode 27 (keysym 0x72, r), same_screen YES,
XLookupString gives 1 bytes: (72) "r"
XFilterEvent returns: False
KeyPress event, serial 16, synthetic NO, window 0x2000002,
root 0x76, subw 0x2000012, time 248544206, (719,86), root:(964,107),
state 0x0, keycode 39 (keysym 0x73, s), same_screen YES,
XLookupString gives 1 bytes: (73) "s"
XmbLookupString gives 1 bytes: (73) "s"
XFilterEvent returns: False
KeyPress event, serial 16, synthetic NO, window 0x2000002,
root 0x76, subw 0x2000012, time 248544263, (719,86), root:(964,107),
state 0x0, keycode 28 (keysym 0x74, t), same_screen YES,
XLookupString gives 1 bytes: (74) "t"
XmbLookupString gives 1 bytes: (74) "t"
XFilterEvent returns: False
KeyRelease event, serial 16, synthetic NO, window 0x2000002,
root 0x76, subw 0x2000012, time 248544365, (719,86), root:(964,107),
state 0x0, keycode 39 (keysym 0x73, s), same_screen YES,
XLookupString gives 1 bytes: (73) "s"
XFilterEvent returns: False
此处的按键数据值包括键名、事件类型和时间条目。在正常输入期间,注意按下和释放不同按键的事件如何重叠。getKeyAverages 子例程中的代码将把输入字符串缓冲处理为文件句柄,然后从输入缓冲中提取相关的时间、事件类型和键名。将计算并返回整个缓冲中的每个按键停顿时间的平均值。
如主程序循环所示,将把 getKeyAverages 子例程输出发送给如下所示的 compareSignature 子例程。
清单 7. compareSignature 子例程
sub compareSignature
{
if( $_[0] ne "0" )
{
$keys{ "r" }{ val } += $_[0];
$keys{ "r" }{ count }++;
}#if r is not 0
if( $_[1] ne "0" )
{
$keys{ "s" }{ val } += $_[1];
$keys{ "s" }{ count }++;
}#if s is not 0
if( $_[2] ne "0" )
{
$keys{ "t" }{ val } += $_[2];
$keys{ "t" }{ count }++;
}#if t is not 0
return unless ( exists($keys{"r"}) );
return unless ( exists($keys{"s"}) );
return unless ( exists($keys{"t"}) );
if( $keys{ "r" }{ count } >= $minSamples &&
$keys{ "s" }{ count } >= $minSamples &&
$keys{ "t" }{ count } >= $minSamples )
{
$samp[0] = sprintf( "%0.0f", $keys{r}{val} / $keys{r}{count} );
$samp[1] = sprintf( "%0.0f", $keys{s}{val} / $keys{s}{count} );
$samp[2] = sprintf( "%0.0f", $keys{t}{val} / $keys{t}{count} );
if( $mode eq "record" )
{
#print "[@samp]\n"; # uncomment to see plain keystroke signature
print `echo "@samp" | mkpasswd -H md5 --stdin`;
}else
{
$userMatch = 0;
checkDynamics( "", 0 );
if( $userMatch == 0 )
{
print "\nno match\n";
#system( "xscreensaver-command -lock" );
}else
{
print "user verified\n";
}#if the signatures did not match
}#if in record mode
%keys = ();
}#enough samples
}#compareSignature
在记录值(如果这些值不为零)后,如果有足够的样例数据,则计算 R、S 和 T 的平均停顿时间。当处于 “record” 模式时,这些停顿时间将被扩展为字符串并用于生成签名的密码散列。在 “monitor” 模式下,将调用 checkDyanmics 子例程以确定目前的停顿时间是否与 keystroke.signatures 文件中记录的停顿时间(在允许的范围内)匹配。如果找到匹配,则不采取任何操作。如果未找到匹配,则锁定屏幕保护程序,从而可以将那些碰运气的攻击者有效地锁定在系统外部。清单 8 详细说明了 checkDynamics 子例程。
清单 8. checkDynamics 子例程
sub checkDynamics
{
my $inString = $_[0];
my $level = $_[1];
my $start = $samp[$level] - $checkRng;
my $stop = $samp[$level] + $checkRng;
my $curr = $start;
#
while( $curr <= $stop && $userMatch != 1 )
{
if( $level == 2 ) # deepest level for only three letters
{
my $res = `echo "$inString $curr" | mkpasswd -S $salt -H md5 --stdin`;
chomp($res);
if( $res eq qq/\$1\$${salt}\$${hash}/ ){ $userMatch = 1 }
}else
{
# append to the current 'signature', go to next level
my $tempStr = ""; # temporary signature string
if( length($inString) != 0 ){ $tempStr = "$inString $curr" }
else { $tempStr = $curr }
checkDynamics( $tempStr, $level+1 );
}#if at maximum level
$curr++;
}#while current less than stop
return("");
}#//checkDynamics
checkDynamics 子例程在构建包含 checkRng 参数所定义的各种可能性的签名时将递归调用自身。传递给 mkpasswd 的每个字符串都是从单个按键停顿时间一直到用户名中每个记录字母的停顿时间逐级构建的。例如,如果平均停顿时间为 “130 130 130”(分别代表 R、S、T),checkDynamics 子例程将完成必要的置换以检查 “125 125 125”、“135 135 135” 及两者之间的内容。松散匹配(带有较高的 checkRng 值)将需要更多的时间来检查所有可能性。
用法
将以上代码另存为 continuousKeystrokes.pl 并在 record 模式下运行程序以生成击键签名:perl continuousKeystrokes.pl record 10 2>/dev/null。这条命令将监视持有焦点的 X 窗口的击键,并在所有窗口中记录了 10 个平均数后输出击键停顿时间的密码散列。出于测试目的,将清单 7 中的样例输出行取消注释以在加密之前显示击键签名。虽然上面使用的 10 个平均数对于测试来说十分有用,但是最好使用更大量的数据来精确地创建签名。先尝试在普通使用场景下键入数千个单词,然后再输出密码散列。在收集到令您满意的数据后,获取已输出的散列并将其放入 keystroke.signatures 文件。
要监视当前用户的键入模式并在检测到模式中出现偏差时锁定屏幕,请用 perl continuousKeystrokes.pl monitor 10 2>>dev/null 运行程序。(stderr 重定向为 null 是由于 threads.pm 中的标量(scalar)释放问题)。如述,此程序将监视当前的键入模式并在签名不同于 keystroke.signatures 文件中记录的内容时锁定屏幕。
注意,您需要用 checkRng 参数和 minSamples 参数进行试验,找到可以在您的环境及特定键入模式下正常工作的设置。
结束语
使用本文介绍的工具和代码,您可以创建使用击键力学连续进行用户验证的自定义框架。虽然是围绕三个按键的停顿时间构建的,但是 xev 程序和本文介绍的代码将允许监视 X Window System 的键盘(和鼠标)交互的所有方面。找出在后退按键之前常用的字符,或者监视哪些 vi 或 emacs 组合键最常用。找到最容易拼错的单词并测量其他键入模式,例如给定应用程序的应用程序快捷键和常用按键。 (责任编辑:A6)
时间:2008-12-01 09:44
来源:developerWorks 中国
作者:Nathan Harrington
原文链接