作者:半点闲
时间:2012-6-4 15:46
博客:blog.csdn.net/cg_i
邮箱:b_dx@sohu.com
环境:SCO_SV scosysv 3.2 5.0.6 i386
Perl v5.8.7 built for i586-pc-sco3.2v5.0
正文
在大多数Unix变种中,登录和注销这两个行为会被跟踪到名为wtmpx(或者是wtmp)的文件中①。通常,如果对某个用户登录行为产生怀疑(比如说某个用户经常从哪台主机登录,但某次他从其他地方登录),我们一般会检查这个文件。在不同的操作系统上,这个文件所处位置也可能不同(比如说,在SCO Unix上它在/etc和/var/adm②中,在Ubuntu上它在/var/log中)。
日志有很多不同的种类,最常见类型的日志文件是完全由文本行组成的。相对于易于解析的文本行,wtmpx日志的记录方式产生的是比较晦涩的拥有专门格式的二进制文件,幸运的是,Perl不怕这些看起来比较诡异的文件。
使用unpack()
Perl有个函数叫unpack(),它是被特别设计用来解析二进制和结构化数据的。让我们看看如何使用它来处理wtmpx文件。wtmp和wtmpx文件的格式在不同的Unix变种之间会有所不同。就这点,我将介绍SCO Unix上的wtmpx文件。下面(图1-1)是SCO Unix上的wtmpx文件的头两个记录的纯文本化的样子:
(图1-1 SCO Unix wtmpx记录文本化样式)
除非你已经熟悉了这种文件的结构,否则这种被称为“ASCII dump”的数据对你而言和乱码没什么区别。那么,我们该怎么去认识这种文件结构呢?要了解这种文件格式最简单的方式就是查看读写该文件的程序源代码。如果你不熟悉C语言,这个任务可能会让你感到气馁。幸运的是,实际上我们并不需要了解,甚至也不需要去查看大部分源代码,我们可以只看定义了该文件格式的那部分内容就够了。
大部分读写wtmpx文件的操作系统程序都会从一个较短的C包含文件中获取文件定义,这个文件一般是/usr/include/utmp.h或者utmpx.h。我们仅需要阅读拥有相关文件格式信息的C数据结构定义。如果你搜索structutmpx,就能找到我们需要了解的部分。struct utmpx的下面几行定义了该结构中的各个字段。这些行应该各自都有符合C注释约定/*text*/的注释行来加以说明。为了让你了解两个不同版本utmpx之间的差异,我们来比较一下这两种不同操作系统上相关的代码内容片断。
下面是SCO Unix上的utmp.h、utmpx.h的相关片段:
struct ut_exit_status {
short __e_termination ; /* Process termination status */
short __e_exit ; /* Process exit status */
};
#defined e_termination __e_termination
#defined e_exit __e_exit
/*
* Structure used to specify timeout in select(2) system call.
*/
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
struct utmpx {
char ut_user[32]; /* user login name */
char ut_id[4]; /* inittab id */
char ut_line[32]; /* device name (console, lnxx) */
#ifdef MOD_FOR_GEMINI
long ut_pid; /* process id */
#else
pid_t ut_pid; /* process id */
#endif
short ut_type; /* type of entry */
struct ut_exit_status ut_exit ;/* process termination/exit status */
struct timeval ut_tv; /* time entry was made */
long ut_session; /* session ID, used for windowing */
long ut_pad[5]; /* reserved for future use */
short ut_syslen; /* significant length of ut_host */
/* including terminating null */
char ut_host[257]; /* remote host name */
} ;
下面这个片断来自Ubuntu 12.04 LTS 64Bit的utmp.h文件:
struct utmp {
short ut_type; /* Type of record */
pid_t ut_pid; /* PID of login process */
char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */
char ut_id[4]; /* Terminal name suffix,
or inittab(5) ID */
char ut_user[UT_NAMESIZE]; /* Username */
char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or
kernel version for run-level
messages */
struct exit_status ut_exit; /* Exit status of a process
marked as DEAD_PROCESS; not
used by Linux init(8) */
/* The ut_session and ut_tv fields must be the same size when
compiled 32- and 64-bit. This allows data files and shared
memory to be shared between 32- and 64-bit applications. */
#if __WORDSIZE == 64 && defined __WORDSIZE_COMPAT32
int32_t ut_session; /* Session ID (getsid(2)),
used for windowing */
struct {
int32_t tv_sec; /* Seconds */
int32_t tv_usec;/* Microseconds */
} ut_tv; /* Time entry was made */
#else
long ut_session; /* Session ID */
struct timeval ut_tv; /* Time entry was made */
#endif
int32_t ut_addr_v6[4]; /* Internet address of remote
host; IPv4 address uses
just ut_addr_v6[0] */
char __unused[20]; /* Reserved for future use */
};
这些文件给我们提供了所有用以构造unpack()语句的必需线索。unpack()取一个数据格式模板作为它的第一个参数。然后它使用该模板来决定如何对从第二个参数取得的二进制(通常是二进制)数据进行反汇编。unpack()将按格式拆分这些数据,返回一个列表,列表中的每个元素对应于所提供的模板中的相应元素。
基于SCO Unix utmpx.h包含文件中的C数据结构,让我们一步一步来构造我们的模板。有好些个模板字母是我们可以使用的。关于这些字符,我会在表1-1中解释,但你还是应该查看perlfunc手册页中的pack()这一节来获取更多相关信息。构造模板有时候不是很直接简单的事,因为C编译器有时候会为了满足对齐的要求来填充数值。Perl自带的pstruct命令在面对这类问题的时候可以帮助我们。
表1-1:将utmpx.h的C代码转换为unpack()模板
C代码 |
unpack()模板 |
模板字母/重复数说明 |
char ut_user[32] |
A32 |
ASCII字符串,长度为32个字节(不足部分以空格填充) |
char ut_id[4] |
A4 |
ASCII字符串,长度为4个字节(不足部分以空格填充) |
char ut_line[32] |
A32 |
ASCII字符串,长度为32个字节(不足部分以空格填充) |
pid_t ut_pid |
s |
带符号的短整型数据 |
short ut_type |
s |
带符号的短整型数据 |
short e_termination |
s |
带符号的短整型数据 |
short e_exit |
s |
带符号的短整型数据 |
long tv_sec |
l |
带符号的长整型值(4个字节,和某些机器上真正的长整型大小可能不一样) |
long tv_usec |
l |
带符号的长整型值(4个字节,和某些机器上真正的长整型大小可能不一样) |
long ut_session |
l |
带符号的长整型值(4个字节,和某些机器上真正的长整型大小可能不一样) |
long ut_pad[5] |
x20 |
跳过20个字节作为空位填充 |
short ut_syslen |
s |
带符号的短整型数据 |
char ut_host[257] |
Z257 |
ASCII字符串,以空字符串结尾,包含\0,长度为257个字节 |
|
x③ |
编译器插入的空位填充(1个字节)。 |
模板构造好了,让我们把它用在真正的代码中:
#针对SCO Unix utmpx的模板
#!/usr/bin/perl –w
use strict;
my $template = ‘A32 A4 A32 s s s s l l l x20 s Z257 x’;
my $recordsize = length( pack( $template, () ) );
open my $WTMP, ‘<’, ‘/etc/wtmpx’ or die “Unable to open wtmpx:$!\n”;
my ($ut_user, $ut_id,
$ut_line, $ut_pid,
$ut_type, $ut_e_termination,
$ut_e_exit, $tv_sec,
$tv_usec, $ut_session,
$ut_syslen, $ut_host) = ();
my $record;
while( read( $WTMP, $record, $recordsize ) ) {
($ut_user, $ut_id,
$ut_line, $ut_pid,
$ut_type, $ut_e_termination,
$ut_e_exit, $tv_sec,
$tv_usec, $ut_session,
$ut_syslen, $ut_host) = unpack( $template, $record );
if( $ut_type == 8 ) {
$ut_host = '(exit)';
}
print "$ut_line:$ut_user:$ut_host:" . scalar localtime($tv_sec) . "\n";
}
close $WTMP;
下面是这段小程序的输出片断:
ttyp0:root:11.227.35.199:Sun Jun 3 10:22:54 2012
ttyp0::(exit):Sun Jun 3 10:23:41 2012
……
在继续之前,上面这段代码中的有些地方需要说明一下:read()函数的第三个参数是它将读取的字节数。相对于将要读取的记录大小(比如说“32”个字节)写死在代码里,我们更倾向于使用pack()函数的一个方便的属性,然后让它自己来告诉我们该模板对应的记录大小是多少:
my $recordsize = length( pack( $template, () ) );
调用操作系统(或其他)二进制文件
由于审查wtmpx文件是很普遍的任务,所以Unix系统都附带了名为last的命令来以可读的形式打印出这个二进制文件的内容(“Perl列出谁在系统上”who命令读取utmpx文件的例子)。下面的输出样例和前面我们所举的范例中的输出几乎是一样的:
root p0 ttyp0 13435 SunJun 3 10:22 00:00
……
我们可以很容易地在Perl中调用如last这样的二进制文件。下面的代码将不重复地显示所有在当前wtmpx文件中找到的用户名:
#last命令二进制文件的路径
my #lastexec = ‘/usr/bin/last’;
open my $LAST, ‘-|’, “#lastexec” or die “Unable to run $lastexec:$!\n”;
my %seen;
while( my $line = <$LAST>){
last if $line = ~/^$/;
my $user = (split(‘ ‘, $line))[0];
print “$user\n” unless exists $seen{$user};
$seen{$user} = ‘’;
}
close $LAST or die “Unable to properly close pipe:$!\n”;
既然unpack()可以满足我们所有的要求,那么为什么还要使用上面提到的这种方法呢?原因是可移植性。如你所见,wtmp/x文件格式的不一致,这会直接导致你之前完美的unpack()模板失效。
尽管如此,你能依靠的便是一直存在的可以阅读这个格式文件的last命令,使用它你就可以和底层格式改变相对独立,不受影响。如果你使用unpack()方法,那么针对需要解析的不同格式的wtmp/x文件,你不得不创建并管理多个单独的模板字符串。
相比于unpack(),使用这个方法的最大缺点就是在程序中解析所需要字段的复杂程序增加了。使用unpack(),所有的字段都会自动地从数据中给你抽取出来。使用我们的last范例,它也不是任何时候都管用。关于写更高级的解析器,有另外的技术,如同Perl哲学一样:“There'sMore Than One Way To Do It.(不只一种方法来做这件事)”。
注1: |
在SCO Unix共拥有utmp、utmpx、wtmp、wtmpx四个日志文件。前两个用于who命令,后者用于last命令。 |
注2: |
实际上它们都是符号链接文件指向/var/opt/K/SCO/Unix/5.0.6Ga/etc/wtmpx(或wtmp)文件。与,/var/opt/K/SCO/Unix/5.0.6Ga/etc/utmpx(或utmp)文件 |
注3: |
对不了解“C编译器字节对齐”的朋友这里只做简单说明,utmpx.h有一行宏命令#pragma pack(4)规定结构,联合的数据成员按4字节进行对齐,第一个放在偏移为0的地方,以后每个数据成员它们的起始存储位置必需能够被4整除。如不满足正确的边界对齐要求,成员之间可能出现用于填充的额外内存空间(空位填充)。utmpx结构本身是精心设计的,每个成员的起始存储位置都满足对齐要求。然,我们通过计算每条记录所占用的字节数可以发现:32+4+32+2+2+2+2+4+4+4+20+2+257=367byte从图1-1可以看出范围从0x000~0x016E(367byte),下一存储起始位置是0x016F它不满足正确的边界对齐要求,编译器进行了1个字节的空位填充(模板末尾x由来),通过分析我们得出实际每条记录共占用了368字节。 |