作者:半點鐘閑
時間: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 {longtv_sec;/* seconds */longtv_usec;/* and microseconds */};struct utmpx {charut_user[32];/* user login name */charut_id[4]; /* inittab id */charut_line[32];/* device name (console, lnxx) */#ifdef MOD_FOR_GEMINIlongut_pid;/* process id */#elsepid_tut_pid;/* process id */#endifshortut_type; /* type of entry */struct ut_exit_status ut_exit ;/* process termination/exit status */structtimeval ut_tv;/* time entry was made */longut_session;/* session ID, used for windowing */longut_pad[5];/* reserved for future use */shortut_syslen; /* significant length of ut_host *//* including terminating null */charut_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 whencompiled 32- and 64-bit. This allows data files and sharedmemory 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字串,以Null 字元串結尾,包含\0,長度為257個位元組 |
|
x③ |
編譯器插入的空位填充(1個位元組)。 |
模板構造好了,讓我們把它用在真正的代碼中:
#針對SCO Unix utmpx的模板#!/usr/bin/perl –wuse 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位元組。 |