IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope:Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  AIX and UNIX  >

系统管理工具包: 测试系统的有效性

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 中级

Martin C Brown (questions@mcslp.com), 自由作家兼顾问, MCslp

2007 年 9 月 27 日

研究存储然后检查配置文件有效性的方法。尽管您已经拥有各种安全系统,但是某些人仍可能访问您的系统,并更改您的配置或安全设置。

关于本系列

典型的 UNIX® 管理员拥有一套经常用于辅助管理过程的关键实用工具、诀窍和系统。存在各种用于简化不同过程的关键实用工具、命令行链和脚本。其中一些工具来自于操作系统,而大部分的诀窍则来源于长期的经验积累和减轻系统管理员工作压力的要求。本系列文章主要专注于最大限度地利用各种 UNIX 环境中可用的工具,包括简化异构环境中的管理任务的方法。

digg 请访问“系统管理员工具包”系列文章

验证文件内容

建立组成 UNIX 系统的配置文件(必要时建立应用程序文件)是您希望系统长时间无故障运行的一种方法。该方法还可以构成安全流程的关键部分,这样不仅可保障系统有效,而且还可以保护系统的安全配置。

尽管您的系统可能已经非常安全,任何人都无法进入并修改配置文件,但是您需要加强自我保护,或至少能够确定某人是否进入系统,并能够验证某人是否未经授权修改了配置文件(甚至任何文件)。

那么如何验证文件内容呢?

这需要考虑许多不同的参数。如果在工作配置中更改了所有以下项目,则可能会导致问题:

  • 文件内容
  • 文件所有者
  • 组所有者
  • 文件权限
  • 修改时间
  • 创建时间

一个简单的方法是在其他计算机上保存配置文件的副本(及其关联的参数),然后对本地文件和远程文件进行常规比较。此方法带来的问题是需要占用大量的空间,更重要的是,需要花大量的时间记录和比较这些信息。此方法还可能影响以实时方式比较信息。

另一个经常提议的方法是只记录文件大小、修改时间、模式和所有权信息。因为与文件全部内容相比,该信息比较短,更易于存储和快速检查和验证。

问题是文件的大小无法确切指示内容是否已更改。请考虑下面这个只包含一行的文件:

location:/home/mcbrown

下面是一个同样大小的文件,但有一行已经更改:

location:/home/slbrown

这两个文件的长度都是 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(<DATA>)
        {
            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<scalar @origfields;$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,它们可以完全脱离计算机保存。

此解决方案的问题是您必须保持该信息是最新的。每次合法更新或更改您监控的文件后,必须更新校验和文件。

尽管该流程有些繁琐,但该文件提供的安全信息带来的好处是不可估量的。

总结

在本文中,您开发了可用于生成信息的脚本,使用它可以检查文件或整个文件目录的有效性。记录的信息包括文件路径、比较文件内容的文件校验和与识别文件差异的唯一信息(索引节点、权限、所有权信息)。

如何使用此脚本完全取决于您自己。您可以存储该信息,并经常运行脚本,以便在发生问题时立即发现问题,或者在出现问题时将该文件用作事后检查工具,来查找更改了哪些文件,从而列出要检查的文件。

共享本文……

digg 请 Digg 这个故事
del.icio.u 发布到 del.icio.u
Slashdot Slashdot 一下!



参考资料

学习

获得产品和技术
  • IBM 试用软件:从 developerWorks 可直接下载这些试用软件,您可以利用它们开发您的下一个项目。


讨论


关于作者

Martin MC Brown has been a professional writer for over eight years. He is the author of numerous books and articles across a range of topics.

Martin Brown 成为一名职业作家已经超过 8 年的时间了。他撰写了很多涉及各个主题的书籍和文章。他的专业知识涉及各种开发语言和平台(比如 Perl、Python、Java、JavaScript、Basic、Pascal、Modula-2、C、C++、Rebol、Gawk、Shellscript、Windows、Solaris、Linux、BeOS、Mac OS/X 等),以及 Web 编程、系统管理和集成。Martin 会定期在 ServerWatch.com、LinuxToday.com 和 IBM developerWorks 上发表文章,定期更新 Computerworld、Apple Blog 以及其他站点上的 blog,同时还是 Microsoft 的 Subject Matter Expert(SME)的专栏作家。您可以通过他的 Web 站点 http://www.mcslp.com 与他联系。




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?




回页首


IBM 和 AIX 是 International Business Machines Corporation 在美国和/或其他国家/地区的注册商标。 Java 和所有基于 Java 的商标都是 Sun Microsystems, Inc. 在美国和/或其他国家/地区的商标。 Linux 是 Linus Torvalds 在美国和/或其他国家/地区的商标。 Microsoft、Windows、Windows NT 和 Windows 徽标是 Microsoft Corporation 在美国和/或其他国家/地区的商标。 UNIX 是 Open Group 在美国和其他国家/地区的注册商标。 其他公司、产品或服务的名称可能是其他公司的商标或服务标志。

IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款