验证文件内容建立组成 UNIX
系统的配置文件(必要时建立应用程序文件)是您希望系统长时间无故障运行的一种方法。该方法还可以构成安全流程的关键部分,这样不仅可保障系统有效,而且还可以保护系统的安全配置。
尽管您的系统可能已经非常安全,任何人都无法进入并修改配置文件,但是您需要加强自我保护,或至少能够确定某人是否进入系统,并能够验证某人是否未经授权修改了配置文件(甚至任何文件)。
那么如何验证文件内容呢?这需要考虑许多不同的参数。如果在工作配置中更改了所有以下项目,则可能会导致问题:
* 文件内容
* 文件所有者
* 组所有者
* 文件权限
* 修改时间
*
创建时间
一个简单的方法是在其他计算机上保存配置文件的副本(及其关联的参数),然后对本地文件和远程文件进行常规比较。此方法带来的问题是需要占用大量的空间,更重要的是,需要花大量的时间记录和比较这些信息。此方法还可能影响以实时方式比较信息。
另一个经常提议的方法是只记录文件大小、修改时间、模式和所有权信息。因为与文件全部内容相比,该信息比较短,更易于存储和快速检查和验证。
问题是文件的大小无法确切指示内容是否已更改。请考虑下面这个只包含一行的文件:
下面是一个同样大小的文件,但有一行已经更改:
这两个文件的长度都是 22
个字符,但是内容不同,这种看似简单的更改可能导致严重的后果,即使检查文件大小,也查不出有任何不同。
至于其他参数、修改时间、文件模式和其他信息都有可能被修改。您可以使用
touch 命令更改修改时间。甚至通过更改计算机上的时间和重新创建该文件可以伪造文件创建时间。
在 UNIX
系统上非常难以更改的一个项目是文件索引节点数值。索引节点数值是第一次创建文件时提供给文件的唯一
ID。基础文件系统驱动程序使用索引节点数值识别文件系统上的文件。在编辑文件后,索引节点通常会发生更改,因为大多数编辑器在删除旧文件并将新文件重命名为最初名称之前会创建新文件,并将新内容写入该文件。因此,进行索引节点比较是检查文件是否已编辑的很好方法。
记录这些信息片段仍然是不够的,还需要一种比较文件内容的有效方法。最好的方法可能要数使用文件校验和。
文件校验和为文件创建校验和是比较文件内容是否更改的传统方法,该方法无需物理比较每个文件的每个字节。
校验和的工作方法是对文件内容使用一个算法。该算法为文件内容生成几乎是唯一的指纹。可以通过许多不同的方法完成此任务。例如,可以将每个字节的值加起来,使用一种算法将复杂的计算应用到给定文件的各个位或位组。具体方法不在本文的讨论范围之内,并且使用哪种方法取决于您使用的校验和工具。
UNIX
包括一个简单的校验和命令 sum。此命令非常简单,但是它提供了可用于识别大多数文件之间差异的近乎唯一的数值。不过,此算法也存在一些局限性。许多现代解决方案提供了
md5 命令。后者生成一个文件的 128 位指纹,并且可以在理论上为任意大小的任何文件生成唯一的签名。
生成校验和信息的 md5
算法最初是为在加密文件之前生成文件的唯一指纹而开发的,这样可以保证解密文件的有效性。可以将 md5 生成的校验和表示为二进制字符串、十六进制字符串或
base64 编码字符串。在 MIME 电子邮件中使用了后一格式,以确保唯一地标识文件中不同的附件。
为文件创建校验和因为存在用于创建校验和信息的命令行解决方案,所以可以直接在命令行上创建任何文件的校验和。校验和信息唯一程度的一个很好示例是使用先前演示的文件示例,该示例具有相同的物理长度和内容,但只有字符不同。
您可以使用一个命令获得两个文件的校验和,如清单
1 所示。
清单 1. 使用一个命令获得两个文件的校验和
- $ sum old new
- 50093 1 old
- 62381 1 new
复制代码 即使清单 1
中只有两个字符不同,但获得的校验和数字却大相径庭。清单 2 显示了相同的文件,这次是使用 md5 检查的。
清单 2. 使用 md5 检查文件
- $ md5 old new
- MD5 (old) = 602f604720d3b57925e99bcaa7d931a4
- MD5 (new) = c3f06c217a0f26c16f8d030837d8718b
复制代码 这里的校验和明显不同,毫无疑问相关文件在某些方面存在不同。
创建校验和的另一个解决方案是使用
Perl 生成校验和信息。Perl 使用的一个模块是 Digest::MD5,该模块可以从数据的任何字符串或提供的文件生成 MD5 校验和。
清单
3 显示了一个简单的脚本,该脚本为命令行上作为十六进制字符串提供的文件返回 MD5 校验和(与清单 2 显示的格式完全相同)。
清单 3. 返回
MD5 校验和的脚本
- use Digest::MD5;
- use IO::File;
- my $chk = Digest::MD5->new();
- foreach my $file (@ARGV)
- {
- $chk->addfile(IO::File->new($file));
- print "$file -> ",$chk->hexdigest,"\n";
- }
复制代码 您可以像前面一样在相同文件上运行脚本,并且应该获得完全相同的信息,如清单
4 所示。
清单 4. 在相同的文件上运行 Digest::MD5
- $ simpmd5.pl old new
- old -> 602f604720d3b57925e99bcaa7d931a4
- new -> c3f06c217a0f26c16f8d030837d8718b
复制代码 为使此流程派上用场,需要将信息记录到文件中,这样可以将该信息与以后的信息进行比较。在执行此操作之前,将需要比较的其他信息(修改时间、文件大小、所有权、索引节点等)添加到存储数据中。
将其他数据添加到报告中Perl stat()
函数可以从给定的文件获取完整的信息(可以使用其中的大多数信息)。清单 5 显示了可以从该文件获取的信息列表。
清单 5. Perl stat()
函数
0 dev device number of filesystem
1 ino inode
number
2 mode file mode (type and permissions)
3 nlink number of
(hard) links to the file
4 uid numeric user ID of file's owner
5 gid
numeric group ID of file's owner
6 rdev the device identifier
(special files only)
7 size total size of file, in bytes
8 atime
last access time in seconds since the epoch
9 mtime last modify time in
seconds since the epoch
10 ctime inode change time in seconds since the
epoch (*)
11 blksize preferred block size for file system I/O
12 blocks
actual number of blocks
allocated
您几乎可以记录所有这些信息,但是其中一些信息是毫无使用价值的,因为这些信息或者更改得太频繁,或者在重新启动过程中不一致。应该忽略以下字段:
* rdev—因为它仅对于特殊文件是唯一的(通常为驱动器或管道),所以可以忽略它。
*
atime—每次访问文件后,文件的最后访问时间都会更改。这意味着该文件很可能会更改,即使从未使用任何方式修改过该文件也会如此。在差异报告中记录该信息可能会导致误确认。
* blksize—用于文件系统 I/O
的块大小。尽管它可能不会有更改,但是除文件修改外的其他因素也可能导致此值的更改,因此,逐文件记录该值没有任何意义。
*
blocks—在文件系统上为该文件分配的块数。此信息特定于某个文件,但是如果还记录文件大小,则同时记录二者没有什么必要。
以下字段对记录某些特定原因非常有用:
*
dev—假设您没有经常安装和卸载文件系统,则在重新引导过程中文件系统的设备号应该是一致的。如果在每次重新启动时文件系统是按同一顺序安装的,则设备号应该一致。
*
nlink—文件的硬链接数可以帮助识别是否有人在可以覆盖文件的位置创建了文件的硬链接并绕过了原始文件的权限。您无法使用与原始文件不同的所有权和权限创建文件的硬链接。
*
ctime—索引节点更改时间将随创建文件的时间或更改所有权或模式信息的时间而改变。如果此值已更改,则它可能指示上述值已改变,即使这些值稍后返回到正常值也是如此。
清单
6
显示了将文件路径、校验和与其他数据写入标准输出的脚本,并使用冒号将信息的每个字段隔开。对于校验和,不仅对文件内容执行校验和,而且还将其他信息添加到校验和数据,这样仅通过比较校验和,就可以确定是否存在差异。
清单
6. 将文件路径、校验和与其他数据写入标准输出
- #!/usr/local/bin/perl
- use Digest::MD5;
- use IO::File;
- use strict;
- use File::Find ();
- my $chksumfile = 'chksums.dat';
- use vars qw/*name *dir *prune/;
- *name = *File::Find::name;
- *dir = *File::Find::dir;
- *prune = *File::Find::prune;
- File::Find::find({wanted => \&wanted}, $ARGV[0]);
- sub wanted {
- next unless (-f $name);
- my $fileinfo = genchksuminfo($name);
- printf ("%s\n",$fileinfo);
- }
- sub genchksuminfo
- {
- my ($file) = @_;
- my $chk = Digest::MD5->new();
- my (@statinfo) = stat($file);
-
- $chk->add(@statinfo[0,1,2,3,4,5,7,9,10]);
- $chk->addfile(IO::File->new($file));
- return sprintf("%s:%s:%s",
- $file,$chk->hexdigest,
- join(':',@statinfo[0,1,2,3,4,5,9,10]));
- }
复制代码 该脚本使用
Perl 中的 File::Find 模块,该模块可以遍历目录并从基本点发现每个文件和目录。对于每个文件,都会调用 wanted()
函数,并且在针对每个文件的该函数中,都会调用 genchksuminfo() 函数。该函数获取 stat()
的信息,并在一行中创建文件路径、校验和与其他信息,然后返回该信息。在此脚本中,该信息仅输出到标准输出。
该命令接受要扫描的目录,因此可以生成校验和信息。对于
/etc,将使用清单 7 中显示的命令。
清单 7. 扫描 /etc
- $ perl savemd5.pl /etc
- /private/etc/6to4.conf:e6b1ba3e7683a0df9be21c9e9f5d1f6a:234881026:46788:
- 33188:1:0:0:1152674600:1155914028
- /private/etc/afpovertcp.cfg:dc7c89b0626d6e603131902d387816f7:234881026:30152:
- 33188:1:0:0:1151780398:1166194017
- /private/etc/aliases:de483c306c03f35dcbd45d609f8e68ce:234881026:47440:
- 33188:1:0:0:1151828538:1155914028
- /private/etc/aliases.db:aa95ae673dcb6ba89684a6f4bbe3dba5:234881026:47437:
- 33188:1:0:0:1151828588:1155914028
- /private/etc/authorization:39f7938ae1df629d422b27ec1a17f3dd:234881026:950752:
- 33188:1:0:0:1162503594:1162503594
- /private/etc/auto.mnt:3da7579cdc03c529059a42de51c6679e:234881026:1013554:
- 33188:1:0:0:1162728759:1162728759
- /private/etc/auto.mnt~:54d856aa344d03a6084d63c9dd7e1d9c:234881026:1013530:
- 33188:1:0:0:1162728576:1162728576
- /private/etc/bashrc:fb23bdcacf23f69f1ce92e3b910c03b9:234881026:42880:
- 33188:1:0:0:1151805563:1155914028
- /private/etc/compilers:363c62792a79df85cd0c8d71ff274495:234881026:821586:
- 33188:1:0:0:1159026690:1162503150
- /private/etc/crontab:b9af1eb506bd68a43465789174bfe5e1:234881026:29678:
- 33188:1:0:0:1151800085:1166193736
- ...
复制代码 此流程的最后一个阶段是存储信息,并提供将当前信息与存储的信息进行比较的方法。
验证校验和信息最后一个脚本基于清单 6
中的脚本。该脚本对原始脚本进行了显著扩展,合并了许多新功能:
* 使用 Getopt::Long
模块分析的命令行选项。这使您能够指定校验和文件(存储您计算的校验和与其他信息)、是否比较新信息和旧信息(通过阅读校验和文件的内容)和指定要搜索的基本目录。如果比较该文件,将会更新数据并仅报告差异。
* loadchksumdata() 函数,该函数以方便比较新信息和旧信息的方法加载和分析现有数据文件。
* gendiff report()
函数,该函数将所存储信息的各个字段与当前信息进行实际比较,告诉您更改了哪些内容。仅当确定已存在某种差异时,才调用此函数。
清单 8.
最终脚本
- #!/usr/local/bin/perl
- use Digest::MD5;
- use IO::File;
- use strict;
- use File::Find ();
- use Getopt::Long;
- my $chksumfile = 'chksums.dat';
- my $compare = 0;
- my $basedir = '/etc';
- use vars qw/*name *dir *prune/;
- *name = *File::Find::name;
- *dir = *File::Find::dir;
- *prune = *File::Find::prune;
- GetOptions("chksumfile=s" => \$chksumfile,
- "compare" => \$compare,
- "basedir=s" => \$basedir);
- my $chksumdata = {};
- if ($compare)
- {
- loadchksumdata($chksumfile);
- }
- my $outfile = '';
- if (!$compare)
- {
- $outfile = IO::File->new($chksumfile,"w");
- }
- File::Find::find({wanted => \&wanted}, $basedir);
- if ($compare)
- {
- foreach my $file (keys %{$chksumdata})
- {
- print STDERR "Couldn't find $file, but have the info on
record\n";
- }
- }
- sub loadchksumdata
- {
- my ($file) = @_;
- open(DATA,$file) or die "Cannot open check sum file $file:
$!\n";
- while()
- {
- chomp;
- my ($filename,$rest) = split(/:/,$_,2);
- $chksumdata->{$filename} = $_;
- }
- close(DATA);
- }
- sub wanted {
- next unless (-f $name);
- my $fileinfo = genchksuminfo($name);
- if ($compare)
- {
- if (exists($chksumdata->{$name}))
- {
- if ($chksumdata->{$name} ne $fileinfo)
- {
- print STDERR "Warning: $name differs from that on
record\n";
- gendiffreport($chksumdata->{$name}, $fileinfo);
- }
- delete($chksumdata->{$name});
- }
- else
- {
- print STDERR "Warning: Couldn't find $name in existing
records\n";
- }
- }
- else
- {
- printf $outfile ("%s\n",$fileinfo);
- }
- }
- sub gendiffreport
- {
- my ($orig,$curr) = @_;
- my @fields = qw/filename chksum device inode mode nlink uid gid
size mtime ctime/;
- my @origfields = split(/:/,$orig);
- my @currfields = split(/:/,$curr);
- for(my $i=0;$i
- {
- if ($origfields[$i] ne $currfields[$i])
- {
- print STDERR "\t$fields[$i] differ; was $origfields[$i],
- now $currfields[$i]\n";
- }
- }
- }
- sub genchksuminfo
- {
- my ($file) = @_;
- my $chk = Digest::MD5->new();
- my (@statinfo) = stat($file);
- $chk->add(@statinfo[0,1,2,3,4,5,7,9,10]);
- $chk->addfile(IO::File->new($file));
- return sprintf("%s:%s:%s",
- $file,$chk->hexdigest,
- join(':',@statinfo[0,1,2,3,4,5,9,10]));
- }
复制代码 要使用该脚本,首先需要生成一个包含基本校验和与其他数据的文件来充当基本比较文件。例如,要为
/etc 目录创建校验和数据文件,可以使用以下命令行:
- $ genmd5.pl --basedir=/etc --chksumfile=etc-chksum.dat
复制代码 现在已经有了该信息,如果编辑一个文件,然后重新运行脚本,则应得到一个差异报告。在清单
9 中,可以看到编辑 /etc/hosts 文件后的结果。
清单 9. 编辑 /etc/hosts 文件后的结果
$ genmd5.pl --basedir /private/etc --compare
Warning:
/private/etc/hosts differs from that on record
chksum differ; was
d4a23fcdaa835d98ede1875503273ce6,
now
beb50782b3fd998f35786b1e6f503d1b
inode differ; was 4879566, now
4879581
size differ; was 1186929905, now 1186930065
mtime
differ; was 1186929905, now 1186930065
Couldn't find /private/etc/hosts~, but
have the info on
record
请注意,您报告了单个文件中的差异和已删除该文件这一事实。如果已创建了新文件,则也会报告差异。
使用校验和数据使用清单 6
中的脚本可以生成用于测试和验证系统有效性的文件。当然,存在文件这一基本事实意味着您必须安全地存储该信息,否则任何人都可以更新该信息,其中包括碰巧使用计算机并更改您需要保护的文件的任何未经授权的个人。
对此信息没有任何硬性规则,但需要清楚的一点是,将创建的文件存储在生成它的同一计算机上不是一个好主意——在找到该文件后可能会将其更改。将该文件存储在同一网络中的另一台计算机上的情况与此相同。在找到该文件后,可能会将其破坏和更改。最佳解决方案是将该文件写入
CD 或
DVD,它们可以完全脱离计算机保存。
此解决方案的问题是您必须保持该信息是最新的。每次合法更新或更改您监控的文件后,必须更新校验和文件。
尽管该流程有些繁琐,但该文件提供的安全信息带来的好处是不可估量的