Chinaunix首页 | 论坛 | 博客
  • 博客访问: 402145
  • 博文数量: 78
  • 博客积分: 3642
  • 博客等级: 中校
  • 技术积分: 695
  • 用 户 组: 普通用户
  • 注册时间: 2006-10-23 15:33
文章分类

全部博文(78)

文章存档

2007年(53)

2006年(25)

分类:

2006-10-25 15:24:14

源自:

perlsec - Perl 安全

perlsec - Perl 安全


Perl可以轻松写出安全的程序,即使运行时有特殊权限,比如setuid或setgid程序。许多脚本的命令行里有多项替换语句,Perl却不是 这样,它使用更多传统方法而少有艰深。而且,由于perl语言有更多内在功能,它可以更少的依赖于其他(可能不可信的)程序来完成根本目的。

当Perl检测到程序中真实的用户或组ID与有效用户或组ID不同时,它自动地开启一种叫做“污染模式”特殊的安全性检测。setuid的unix的权限位是04000,setgid 的UNIX权限位是02000;它们都有可能被设置。你也可以用命令行标识-T明确地开启“污染模式”。强烈建议服务器程序或者在以其他人身份运行的程序(比如CGI脚本)使用此标识符。一旦污染模式被打开,它就为脚本的余下内容工作。

在“污染模式”中,Perl使用叫做“污染检测”的特殊预防方法来防止明显的和不易被察觉的陷阱。一些检测相当简单,如检查路径目录以确定它们对其 他人是不可写的;小心的程序员一向做此类检测。其他的检测已经得到Perl本身最好的支持,这些检测尤其使写一个set-id的Perl程序比相应的C程 序更安全。

你不可以使用来自程序之外的数据来影响程序之外的事情——至少不是偶然的。所有命令行参数,环境变量,本地信息(参见), 特定系统调用的结果(readdir(),readlink(),shmread()的变量,msgrcv()的返回信息,getpwxxx()调用返回 的密码、gcos和shell域)和所有文件输入都被标记成“污染的”。“污染的”数据既不可以直接或间接在任何调用一个子shell命令中使用,也不能 在任何修改文件、目录或进程的命令中使用,但有以下例外

  • 如果你向systemexec传递一个参数列表,该列表中的成员不会被检查是否被污染。
  • printsyswrite的参数被检查是否被污染。

来自污染的数据的任何变量也是污染的,即使从逻辑上来说以污染的数据来改变变量是不可能的。因为每一个标量和它是否被污染相关联,一个数组的部分元素可能是污染的然而其他的不是。

比如:

    $arg = shift;		# $arg 是污染的
$hid = $arg, 'bar'; # $hid 也是污染的
$line = <>; # 污染的
$line = ; # 仍旧是污染的
open FOO, "/home/me/bar" or die $!;
$line = ; # 还是污染的
$path = $ENV{'PATH'}; # 污染的, 但是请看下面
$data = 'abc'; # 非污染的

system "echo $arg"; # 不安全的
system "/bin/echo", $arg; # 安全的 (不用 sh)
system "echo $hid"; # 不安全的
system "echo $data"; # 如果PATH被设定,那么才是安全的

$path = $ENV{'PATH'}; # $path 现在是污染的

$ENV{'PATH'} = '/bin:/usr/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

$path = $ENV{'PATH'}; # $path 现在不是污染的
system "echo $data"; # 现在是安全的!

open(FOO, "< $arg"); # 没问题 - 只读文件
open(FOO, "> $arg"); # 有问题 - 试图去写

open(FOO,"echo $arg|"); # 有问题, 但是...
open(FOO,"-|")
or exec 'echo', $arg; # 没问题

$shout = `echo $arg`; # 不安全的, $shout 现在是污染的

unlink $data, $arg; # 不安全的
umask $arg; # 不安全的

exec "echo $arg"; # 不安全的
exec "echo", $arg; # 安全的 (不用 shell)
exec "sh", '-c', $arg; # 认为是安全的, 哎呀!

@files = <*.c>; # 不安全的(使用 readdir() 或者类似的)
@files = glob('*.c'); # 不安全的(使用 readdir() 或者类似的)

如果你试图做一些不安全的事情,你会得到类似"Insecure dependency"或"Insecure $ENV{PATH}"的致命错误。注意你仍然可以写一个不安全的systemexec,但 只是明确的做和上面“被认为是安全的”例子类似的事情。

测试一个变量是否含有污染的数据,谁的用法会引发一条"Insecure dependency"信息,在你附近的CPAN镜像查找Taint.pm模块,它应该在1997年左右就可以得到 。或者你可以用is_tainted()函数。

    sub is_tainted {
return ! eval {
join('',@_), kill 0;
1;
};
}

此函数利用了“表达式中任何一部分存在的污染数据致使整个表达式都被污染”。操作员测试每个参数是否被污染会使效率低下。相反,稍稍高效且稳定的方法是,只要一个表达式中任何一部分存取一个被污染的值,那么这个表达式被认为是被污染的。

但是仅仅测试数据是否被污染还不够。有时你必须清除数据的污染。唯一的通过污染机制的方法是引用正则表达式中的一个子模式。Perl假定如果你用 $1,$2等等引用一个子串,那么你就知道你在做什么。也就是说你必须思考而不是盲目的解除污染,或者违抗整个机制。校验变量是否只含有好的字符(已知的 好的字符)比检查它是否含有坏的字符要好。是因为很可能就把意料之外的坏字符漏掉。

下面的例子是一个检查数据中是否只含有单词(字母、数字、下划线)、连字符、'@'符号或者是'.'。

    if ($data =~ /^([-\@\w.]+)$/) {
$data = $1; # $data被解除污染
} else {
die "Bad data in $data"; # 把这个记录下来
}

这完全没有问题,因为/\w+/通常不匹配shell中的字符、'.'、破折号、亦或任何对于shell有特殊含义的字符。使用/.+/从理论上讲会不安全,因为它匹配任何字符,而Perl将不再检查它们。我们的经验是当你解除污染时,必须对匹配模式极其的小心。使用正则表达式清洗数据是解除污染的唯一机制,除非你使用下面才详细叙述的派生一个特权被降低的字进程的方法。

如果程序中使用了use locale,那么上面的例子将不会解除$data的污染,因为\w匹配的字符是由locale决定的。Perl认为locale的定义是不可信的,因为它们包含程序之外 的数据。如果你在写一个locale-aware的程序,并且想使用包含\w的正则表达式清洗数据,那么请在同一块内的表达式之前加上no locale。参见以获 得更多的信息。

当你使脚本程序可执行,就是可以像命令一样让它们工作时,系统会把"#!"行的开关传递给Perl。Perl检查setuid(或setgid)程序的任 何和"#!"行开关匹配的命令行开关。一些Unix或Unix-like系统环境强制在"#!"行使用一个开关,所以你也许必须用类似-wU的开关而不是-w -U。(这个问题只出现在支持#!、setuid、setgid脚本的Unix或Unix-like系统环境中)

对于"Insecure $ENV{PATH}"这样的信息,你必须把$ENV{PATH}设置为已知的,并且路径中的任何目录都对于非本用户或非本组成员不可写。你也许会在即使路径名是完全合法的情况下收到那条信息表示非常惊讶。当你没有提供程序一个完整的路径时,它不会被引起;相反,若你从未设置PATH环境变量,或者你没有把它设置安全,它就会被引起。因为Perl不能保证可疑的可执行程序是不是它本身将执行其他的依赖于PATH的程序,它确定是你设定的PATH。

PATH不是唯一可能导致问题的变量。因为一些shell会使用IFS,CDPATH,ENV和BASH_ENV,Perl在开始子进程时检查它们是否也为空或者未污染。你也许会在你的set-id和污染检测模式下的脚本程序中加入这些东西:

    delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};   # 使 %ENV 更安全

当然,无论是否使用污染变量都有可能出现麻烦。在处理任何由用户提供的文件名的文件时,要做周密的测试。必须时,可以在去掉用户(或组!)的特权之后再进行类似open的操作。Perl不阻止你打开污染的文件名并读取内容,所以要小心对待打印出的内容。污染机制的目的是防止愚蠢的错误,不是使人懒惰不去思考。

当你传递给systemexec明确的参数列表而非含有通配符的字符串时,Perl不会调用shell去扩展通配符。不幸的是,openglob,backtick(译注:backtick为反引号)函数并不提供这样的特性,所以当使用它们的时候必须非常仔细。

Perl为从一个setuid或setgid程序打开文件或管道提供了一个安全的方法:创建一个减少权限的子进程来为你完成那些“肮脏”的工作。首先,用 特殊的OPEN语法创建一个子进程,使其和父进程通过一个管道相连。现在子进程把它的ID和其他诸如环境变量,umask,当前工作目录的性质重新设置回 原始的或安全的变量。然后让该不具有任何特权的子进程来完成OPEN和其他的系统调用。最终,子进程把它成功存取的数据传递给父进程。因为文件或管道是由 运行于比父进程权限低的子进程打开的,所以它不容易被欺骗去做它不该做的事情。

这里有一个安全使用backtick的方法。注意当shell可能扩展时,exec是如何不被调用的。这是目前来调用可能被shell转义的东西最好的方法:从不调用shell。

        use English;
die "Can't fork: $!" unless defined($pid = open(KID, "-|"));
if ($pid) { # parent
while () {
# 随便做一些事儿
}
close KID;
} else {
my @temp = ($EUID, $EGID);
my $orig_uid = $UID;
my $orig_gid = $GID;
$EUID = $UID;
$EGID = $GID;
# Drop privileges
$UID = $orig_uid;
$GID = $orig_gid;
# 确定特权的确没有了
($EUID, $EGID) = @temp;
die "Can't drop privileges"
unless $UID == $EUID && $GID eq $EGID;
$ENV{PATH} = "/bin:/usr/bin"; # 最小的 PATH.
# 认为仍需清理环境
exec 'myprog', 'arg1', 'arg2'
or die "can't exec myprog: $!";
}

使用类似的策略可以让glob使用通配符扩展,虽然也可以用readdir

当你虽然相信自己并没有写有问题的程序,但并不信任程序的最终使用者不会企图让它做坏事时,污染检测最为有用。此类安全检查对set-id和以其他用户身份运行的程序(如CGI)非常有用。

若连程序的作者都不可信的话,情况就不同了。当某人给你一段程序并和你说,“给,试试看。”对于此类安全问题,使用包含在Perl发行版中的Safe模块。这个模块允许程序员建立特殊的隔间,在其中所有的系统调用都被截获,并且名字空间入口被严格控制。

安全问题

除了源于赋予像脚本一样灵活的系统特权这类明显的问题,在许多Unix版本中,set-id脚本从一开始就是天生不安全的。问题出在内核的条件竞争。在内 核打开文件来查看应该运行哪个解释器和当(现在已set-id)解释器回过头来重新打开文件并解释它的这两个事件之间,可疑的文件也许已经改变了,特别是 当系统中有符号连接时。

幸运的是,这个内核的“特性”有时可以被关闭。不幸的是,有两个方法来关闭它。系统可以简单的宣布任何含有set-id位的脚本都是不合法的,这个显然用 处不大。另一个是忽略脚本中的set-id位。如果后者被设置为真,那么当Perl注意到其它脚本中无效的setuid/gid位时,它可以模仿 setuid和setgid的机制。这是通过一个叫做suidperl的特殊程序来实现的,它在需要时自动被调用。

但是,如果内核的set-id脚本特性没有被关闭,Perl就会大声抱怨你的set-id程序是不安全的。你要么需要关闭内核的set-id脚本特 性,要么为脚本制作一个C Wrapper。一个C Wrapper就是一个除了调用你的Perl程序其他什么都不干的已编译程序。已编译程序不受此内核问题的影响去找set-id脚本的麻烦。这里有一个简 单的C Wrapper:

    #define REAL_PATH "/path/to/script"
main(ac, av)
char **av;
{
execv(REAL_PATH, av);
}

把此C Wrapper编译成可执行二进制文件,对它setuid或setgid而不是你的脚本。

近几年,软件商开始提供没有此安全问题的系统。在它们中,当内核把将要被打开的set-id脚本的名字传递给解释器时,它将不会传递可能出现问题的路径名而是传递/dev/fd/3。这是一个已经在脚本上打开的特殊文件,所以将不会出现条件竞争问题。在这些系统中,Perl需要在编译时带上-DSETUID_SCRIPTS_ARE_SECURE_NOW参数。Configure程序将自己完成这个任务,所以你永远不必要自己指出此点。现在SVR4和BSD4.4都采用此种方法来避免内核条件竞争。

在Perl 5.6.1 发行之前,suidperl的代码问题可能导致安全漏洞。

有很多种方法可以隐藏你的Perl程序源代码,它们具有不同等级的“安全性”。

首先,你不能去掉“读”权限,因为源代码必须在被读取之后才能编译和解释。(这并不意味着CGI脚本的源代码在网上是可被读取的)所以你必须把权限设置为对外界友好的0755。这使在你本地系统上的人只能查看源代码。

一些人错误的认为这是一个安全问题。如果你的程序不安全,而你依赖人们不知道如何利用这些漏洞,这是不安全的。通常某些人在没有看源代码的情况下就可以利用这些漏洞。以隐藏来实现所谓的“安全”而不是修复漏洞,是非常不安全的。

你可以试着通过源代码过滤器(CPAN上的Filter::*)来实现加密。但是骇客有可能把它解密。你可以试着使用下面描述的字节码编译器和解释 器,但是骇客有可能把它反编译。这些对想看你代码的人造成不同难度的困难。但是没有一种可以完全的避免(不光是Perl,所有语言都一样)。

如果你担心有人会通过你的程序得利,那么你可以在最低行写一个限制性的许可证来寻求法律保护。当然如果你用类似“这是某某公司的私人程序,你无权使用它”的声明来授权你的软件并发布它的话,那会是非常危险的。你应该找一个律师确定你的许可证的措辞可以在法庭上站得住脚。

阅读(2168) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~