安全程序设计
概述
在当前的软件行业里,太多的程序有安全问题,代码在被发布前只是经过很少的测试,即使
一些有专业测试人员的软件公司也很少进行安全编程方面的测试,原因在于缺少对安全编程
技术的了解。本文将尝试给程序员一个比较清晰的概念,安全漏洞的来源,和避免安全漏洞
的技巧,使写安全程序的过程变得轻松起来。
运用好的编程技巧是非常重要的,甚至你的代码只是将运行在限制的时期和限制的条件下。
许多程序员的程序常超越其最初的设计范围,大部分的安全漏洞出现的环境是当初程序员不
知道或没有想到的。典型的是,程序员假设当前的系统调用永不会失败,或者程序参数永远
不可能超过某个长度。因而,程序员能做到最好的事情就是对问题进行假定编程,仔细分析
它们是否正确,和想象可以使其失败的条件。
Internet发展
主机数 4300万 46%
网民数 1.54亿 55%
2001网民数 4.5亿
美国网民数
8300万 26%
2001美国网民数 1.3亿
美国人在线上税 2500万 38%
AOL用户 1700万 42%
WEB服务器 500万 128%
YAHOO每天页面浏览 2.35亿次 147%
网上新闻发布 213万 89%
在线股市交易
33.6万 125%
电子商务营业额 211亿美元 154%
导致安全漏洞的二个最根本原因
溢出
什么是溢出:
数据存储过程中超过数据结构所能容纳的实际长度都可成为溢出。
产生溢出的理论基础:
1. 平面内存结构,4GB或更大逻辑地址空间
程序运行时可以被装载到相对固定的地址空间,使得确定攻击代码地址更为方便
2. 数据与代码同处于一个地址空间,堆栈可执行
代码数据共同存储这一现代计算机模型使得溢出攻击真正可行,攻击者可以精心编制输入数
据,得到运行权
3. CPU call调用利用栈保存返回地址
Call调用使用堆栈保存返回地址,使得跟改程序返回地址成为可能
4. C函数在栈中保存局部变量
看一下现代几乎所有的编译器产生的代码,就会发现在所有调用子程序的地方都有类似代码
push ebp
mov ebp, esp
sub esp, ??
编译器为了支持函数嵌套调用都使用堆栈来保存局部变量
5. C语言无自动边界检查功能
C语言不进行数据边界检察,当数据被覆盖时也不能被发现
6. 栈从高地址往低地址生长
数据存放是从低到高存放的,而堆栈却从高到低生长,当call调用子程序时的返回地址将被
压入堆栈,这就是说当发生call调用时,程序返回地址将位于子程序数据区的高处,使恶意
覆盖返回地址成为可能,只要精心安排输入数据就可以使执行类似ret的指令时,跳转到所需
要的地址
一个溢出的例子
#include <stdio.h>
#include <string.h>
void SayHello(char* str)
{
char buffer[8];
strcpy(tmpName,
name);
printf("Hello %s\n", tmpName);
}
int main(int argc,
char** argv)
{
SayHello(argv[1]);
return 0;
}
运行:
$ ./example sunx
Hello sunx
似乎一切正常,不会有什么问题
。。。。再试一下。。。
$./example sunxsunxsunx
Hello sunxsunxsunx?????
Segmentation
fault (core dumped)
当程序打印完输入数据后崩溃了,这是为什么呢?
这个程序的函数含有一个典型的内存缓冲区编码错误.
该函数没有进行边界检查就复
制提供的字符串, 错误地使用了strcpy()而没有使用strncpy(). 如果你运行这个程序就会产
生段错误.
原因是在命令行输入的数据 “sunxsunxsunx” 长度超过了在SayHello函数中的
局部变量长度, 于是覆盖了在堆栈上方的返回地址,在print之后就崩溃了
让我们看看在调用函数时堆栈的模样:
分析:
程序的内存布局
栈
堆
数据段
代码段
0xFFFFFFFF
栈方向
0x00000000
第一次运行进入SayHello后的栈
第二次运行进入SayHello后的栈
sununxsunx
这里发生了什么事? 答案很简单: strcpy()将*str的内容(larger_string[])复制到buffer
[]里, 直到在字符串中碰到一个空字符. 显然,buffer[]比*str小很多. buffer[]只有16个字
节长, 而我们却试图向里面填入12个字节的内容.
这意味着在buffer结构之后, 堆栈中4个字
节被覆盖. 包括RET地址,我们已经把Buffer指向内存的12个字节全都填成了“sunxsunxsunx
”, 这意味着现在的返回地址是0x786e7573. 当函数返回时, 程序试图读取返回地址的下
一个指令, 此时我们就得到一个段错误.
因此缓冲区溢出允许我们更改函数的返回地址. 这样我们就可以改变程序的执行流程.
如果攻击者精心准备数据
jmp label2
label1: pop esi
mov [esi+8], esi
xor eax, eax
mov [esi+7],
al
mov [esi+12], eax
mov al, 0bh
mov ebx, esi
lea ecx, [esi+8]
lea edx, [esi+12]
int 80h
xor ebx, ebx
mov eax, ebx
inc
eax
int 80h
label2: call label1
cmd: db “/bin/sh”, 0
上面代码的机器码
char shell_code[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0”
“\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c”
“\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
如果用程序输入这些数据就可以得到一个命令行shell
溢出漏洞的实际利用方法
Remote root exploit
远程,不经认证而获得执行权,
主要针对程序:各种Daemon
HTTP 、FTP 、POP 、Sendmail …
Local root exploit
本地,利用程序的漏洞获得执行权,主要被用来提升用户权限
主要针对程序:所有的特权程序
那些程序具有特权:
Daemon
HTTP 、FTP 、POP 、Sendmail …
系统服务
一些系统相关的服务
如:Syslog …
suid/sgid程序
Unix一项特殊技术,使普通用户也能做部分只有超级用户才能执行的任务lpasswd、at、cro
ntab、ping
普通rwx之上加上s位,kernel在载入进程映象时自动将进程有效用户/组标识置为映象文件文
件属主/组
例:
ls -l /bin/eject
-r-s--x--x 1 root /usr/bin/passwd
OS本身
解决方法
更为小心的程序设计
将安全相关的功能隔离到仔细检查的代码内
非可执行栈
会导致若干技术难题
基于编译器的方法
在代码内自动增加边界检查(very slow)
运行过程中进行栈完整性检查(slight slowdown)
重新排列栈变量(no slowdown)
输入过滤
关于Perl
Perl作为CGI编程的主要语言之一,其安全性也受到很大的关注。在
W3C组织的 "WWW Secur
ity FAQ" 之 "CGI Scripts"一章中,Perl安全编程就整整占了一节。由此可见 Perl CGI
安
全编程的重要性。
---------------------
1、NULL字符
---------------------
开发人员已经习惯了C语言的工作模式
如果说 strcmp("root","root\x00")==0,相信没有什么人反对。但是在Perl中
"root"!="r
oot\0"
对于每一个希望发现CGI漏洞的安全专家或黑客来说,最常用的方法之一是通过传递特殊字符
(串),绕过CGI限制以执行系统级调用或程序。
阅读以下例子:
# parse $user_input
$database="$user_input.db";
open(FILE "<$database");
这个例子用于打开客户端指定的数据库文件。例如客户端输入"haha",则系统将打开"hah
a.db"文件考只读方式)。这种处理方式在Web应用中是很常见的。
现在,让我们在客户端输入"haha%00",在该PERL程序中$database="haha\0.db",然后
调用open函数打开该文件。但结果是什么呢?系统会试图打开"haha"文件
出现这种情况的原因是由于PERL允许在字符串变量中使用NULL空字符,因此,也就有了
"root"!="root\0"
而在C语言中字符串则以NULL字符作为字符串的结束标志,于是"root"=="root\0"(在C语言中
)。
由于Perl本身是使用C编写,因此当PERL将"backend\0.db"字符串传递到C运行库时,\0空字
符以后的字符将被忽略
这种编程缺陷的影响可大可小。试想一下,如果利用以上编程原理编写一个给系统其他管理
员修改除了root外的其他用户口令的PERL程序:
$user=$ARGV[1] # user the jr admin wants to change
if ($user ne "root"){
# do whatever needs to be done for this user }
那么,聪明的你应该知道如何绕过这个限制修改root用户口令了吧?对了,只要使
$user="
root\0",则PERL会执行上面程序中花括号内的语句。除非所有处理过程均使用PERL,否则一
旦该变量传递给系统,则会造成安全问题。如修改root用户口令等。
也许你认为很难遇到这种会造成严重安全问题的情况,那么我们能否将它作为一种寻找
网站源程序漏洞的间接手段呢?;-)
不知你有没有经常遇到这种类型的CGI程序,该程序用于打开客户端(提交的表单中)要
求的页面?如:
page.cgi?page=1
然后网站是否返回页面"1.html"呢?;-) 好,现在将其改为:
page.cgi?page=page.cgi%00 (%00 == '\0' escaped)
这样,我们就可以得到我们感兴趣的文件内容了!这种方法连PERL的"-e"参数也可绕过:
$file="/etc/passwd\0.txt.whatever.we.want";
die("hahaha! Caught
you!) if($file eq "/etc/passwd");
if (-e $file){
open (FILE, ">$file");}
绕过这段程序的后果你应该想像得到吧?:)
解决方法?最简单地,过滤NULL空字符。在PERL程序中,
$insecure_data=~s/\0//g;
------------------------
2、反斜杠(\)
------------------------
W3C 的 WWW Security FAQ 中列出了建议过滤的字符:
&;`'\"│*?~<>^()[]{}$\n\r
但在很多时候反斜杠(\)往往被遗忘了。以下是正确的过滤表达式:
s/([\&;\`'\\\│"*?~<>^\(\)\[\]\{\}\$\n\r])/\\$1/g;
但在很多商业的CGI程序中反斜杠却没有被包含进去,这可能是程序员们写程序时被这些过滤
用的匹配表达式搞迷糊了?
那么,没有过滤反斜杠会造成安全问题吗?试想一下,如果向你的程序中发送如下一行
内容:
user data `rm -rf /`
大多数情况下,程序员编写的程序会将以上内容过滤为:
user data
\`rm -rf /\`
从而保护了系统。但如果PERL程序中忘记过滤了反斜杠,当客户端向该程序提交如下内容时
:
user data \`rm -rf / \`
经过匹配表达式后为:
user data \\`rm -rf
/ \\`
怎么样,看出危险了吗?由于两个反斜杠经系统解释后为一个字符"\",但`字符却因此没有
被过滤掉,`rm -rf /
\`将被系统执行!不过,由于其中还含有一个反斜杠字符,执行时系
统会出错。你自己想办法绕过这个限制吧?;-)
利用反斜杠的另一个应用--绕过系统目录进入限制。请看以下表达式:
s/\.\.//g;
这个匹配表达式的作用非常简单,就是过滤字符串中的".."。当输入为:
/usr/tmp/../../etc/passwd
将被过滤为:
/usr/tmp///etc/passwd
这样,你将无法访问/etc/passwd文件。
(注:*nix系统允许///,试一下'ls -l/etc////passwd'命令就知道了。)
现在,让我们的“好伙伴”反斜杠来帮忙。将输入改为:
/usr/tmp/.\./.\./etc/passwd
则由于反斜杠的存在而不符合过滤表达式。当PERL中存在如下程序段时,
$file="/usr/tmp/.\\./.\\./etc/passwd";
$file=s/\.\.//g;
system("ls
-l $file");
当运行到执行系统调用时,执行的命令会是"ls -l /usr/tmp/.\./.\./etc/
passwd"。想知道会得到什么输出吗?自己在机器上试试吧。;-)
然而,以上方法只适用于系统调用或``命令中。无法绕过PERL中的'-e'命令和open函数
(非管道)。如下程序:
$file="/usr/tmp/.\\./.\\./etc/passwd";
open(FILE, "<$file") or die("No
such file");
执行时将显示"No such file"并退出。我还没有找出绕过这个限制的方法。:(
解决方法:只要别忘了过滤反斜杠字符(\),就已足够了。
--------------------------------
3、字符"│"
--------------------------------
在PERL的open函数中,如果在文件名后加上"│",则PERL将会执行这个文件,而不是打开
它。即:
open(FILE, "/bin/ls")
将打开并得到/bin/ls的二进制代码,但
open(FILE, "/bin/ls│")
将执行/bin/ls命令!
以下过滤表达式
s/(\│)/\\$1/g
可以限制这个方法。PERL会提示"unexpected
end of file"。如果你找到绕过这个限制的方
法,请告诉我。:-)
综合应用
现在让我们综合以上几种编程安全漏洞加以利用。先举个例子,$FORM是客户端需要提交
给CGI程序的变量。而在CGI程序中有如下语句:
open(FILE, "$FORM")
那我们可以将"ls│"传递给$FORM变量来获得当前目录列表。现在让我们考虑如下程序段:
$filename="/safe/dir/to/read/$FORM"
open(FILE, $filename)
如何再执行"ls"命令呢?只要能使$FORM="../../../../bin/ls│"即可。如果系统对目录操作
加入了".."过滤,则可利用反斜杠的漏洞绕过它。
在这段程序中,我们还可以在命令中加入参数。如"touch /backend│",将建立/backen
d文件。(但我不会使用这个文件名,因为它是我的名字。:-))
现在,让我们在程序段中加入更多的安全限制:
$filename="safe/dir/to/read/$FORM"
if(!(-e $filename)) die("I don't think so!")
open(FILE, $filename)
这样我们还需要绕过"-e"的限制。由于我们在$FORM变量中使用了"│"字符,当"-e"运算符检
查"ls│"文件时,因为不存在此文件而退出程序。如何当"-e"检查时去掉管道符,而调用ope
n函数时又含有管道符呢?回忆一下在前面谈到的NULL字符的利用,我们就知道应该如何做了
。只要使$FORM="ls\0│"(注:在客户端提交的表单中为"ls%00│")即可。其中的原理复习一
下前面提到的内容就会明白了。
需要说明的是,以上程序段中,我们无法象再上一段程序那样执行带参数的命令,这是
因为"-e"运算符的限制所致。举例如下:
$filename="/bin/ls /etc│"
open(FILE, $filename)
将显示/etc目录下文件列表。
$filename="/bin/ls /etc\0│"
if(!(-e $filename)) exit;
open(FILE,
$filename)
将导致因不存在文件而退出。
$filename="/bin/ls\0 /etc│"
if(!(-e $filename)) exit;
open(FILE, $filename)
将只显示当前目录下文件列表。
关于ASP
大部分网站把密码放到数据库中,在登陆验证中用以下sql,(以asp例)
sql="select
* from user where username=’"&username&"’and pass=’"& pass &’"
,
此时,您只要根据sql构造一个特殊的用户名和密码,如:ben’ or ’1’=’1
就可以进入本来你没有特权的页面。
再来看看上面那个语句吧:
sql="select * from user where username=’"&username&"’and pass=’"&
pass&’"
此时,您只要根据sql构造一个特殊的用户名和密码,如:ben’ or ’1’=’1 这样,程序将
会变成这样:
sql="select*from username where username="&ben’or’1’=1&"and pass="&pass&"
or
是一个逻辑运算符,作用是在判断两个条件的时候,只要其中一个条件成立,那么等式将会成立
.而在语言中,是以1来代表真的(成立).那么在这行语句中,原语句的"and"验证将不再继续,而
因为"1=1"和"or"令语句返回为真值.。另外我们也可以构造以下的用户名:
username=’aa’ or username<>’aa’
pass=’aa’ or pass<>’aa’
关于PHP
PHP安全举例: PHP Version
3.0是一个HTML嵌入式脚本语言。其大多数语法移植于C、J
ava和Perl并结合了
PHP的特色。这个语言可以让web开发者快速创建动态网页。
因其执行在web服务器上并允许用户执行代码,PHP内置了称为'safe_mode'的安全特性,
用于控制在允许PHP操作的webroot环境中执行命令。
其实现机制是通过强制执行shell命令的系统调用将shell命令传送到EscapeShellCmd()
函数,此函数用于确认在webroot目录外部不能执行命令。
在某些版本的PHP中,使用popen()命令时EscapeShellCmd()却失效了,造成恶意用户可
以利用'popen'系统调用进行非法操作。
--------------------------------------------------------------------------------
测试程序:
警 告:以下程序(方法)可能带有攻击性,仅供安全研究与教学之用。使用者风险自负!
<?php
$fp = popen("ls -l /opt/bin; /usr/bin/id", "r");
echo "$fp<br>n";
while($line = fgets($fp, 1024)):
printf("%s<br>n", $line=;
endwhile;
pclose($fp);
phpinfo();
?>
输出结果如下:
1
total
53
-rwxr-xr-x 1 root root 52292 Jan 3 22:05 ls
uid=30(wwwrun) gid=65534(nogroup)
groups=65534(nogroup)
and from the configuration values of phpinfo():
safe_mode 0 1
关于UNIX Shell Script
同样由例子开始:
#!/bin/sh
read name
eval echo Hello $name
运行情况:
$ ./hellod
sunx
Hello sunx
粗看起来似乎不会有什么问题,都是事情总有例外
$ ./hellod
sunx;ls;
Hello
sunx
Hellod hellod.c
可以看到输入内容 “sunx;ls” 中的内容竟然被执行了
也许这样的例子还不够严重,
进一步假设如果类似的程序被放到了网上
$ vi
在/etc/inetd.conf 增加下面一行:
ingreslock
stream tcp nowait root /tmp/hellod hellod
正常运行时候的现象:
$ telnet localhost
2000
Trying 127.0.0.1...
Connected to localhost.
Escape character
is '^]'.
Sunx
Hello sunx
Connection closed by foreign host.
被入侵者恶意利用的话:
$ telnet localhost 2000
Trying 127.0.0.1...
Connected
to localhost.
Escape character is '^]'.
What's your name?
sunx;id;
Hello sunx
uid=0(root) gid=0(root)
: command not found
Connection
closed by foreign host.
这是为什么呢?
原因就在于 “;” 这个在unix中具有特殊意义的字符,一个健壮的程序应该过滤掉如下这些
特殊字符
'&', ';', '`', ':', '│', '>', '<', '?', ')', '(', '{', '}',
'^', '~'
安全编程的原则
UNIX系统为程序员提供了许多子程序,这些子程序可存取各种安全属性.有
些是信息子程序,返回文件属性,实际的和有效的UID,GID等信息.有些子程序可
改变文件属性.UID,GID等有些处理口令文件和小组文件,还有些完成加密和解密.
本节主要讨论有关系统子程序,标准C库子程序的安全,如何写安全的C程序
并从root的角度介绍程序设计(仅能被root调用的子程序).
常用系统子程序
(1)I/O子程序
*creat():建立一个新文件或重写一个暂存文件.
需要两个参数:文件名和存取许可值(8进制方式).如:
creat("/usr/pat/read_write",0666) /* 建立存取许可方式为0666的文件
*/
调用此子程序的进程必须要有建立的文件的所在目录的写和执行许可,置
给creat()的许可方式变量将被umask()设置的文件建立屏蔽值所修改,新
文件的所有者和小组由有效的UID和GID决定.
返回值为新建文件的文件描述符.
*fstat():见后面的stat().
*open():在C程序内部打开文件.
需要两个参数:文件路径名和打开方式(I,O,I&O).
如果调用此子程序的进程没有对于要打开的文件的正确存取许可(包括文
件路径上所有目录分量的搜索许可),将会引起执行失败.
如果此子程序被调用去打开不存在的文件,除非设置了O_CREAT标志,调用
将不成功.此时,新文件的存取许可作为第三个参数(可被用户的umask修
改).
当文件被进程打开后再改变该文件或该文件所在目录的存取许可,不影响
对该文件的I/O操作.
*read():从已由open()打开并用作输入的文件中读信息.
它并不关心该文件的存取许可.一旦文件作为输入打开,即可从该文件中读
取信息.
*write():输出信息到已由open()打开并用作输出的文件中.同read()一样
它也不关心该文件的存取许可.
(2)进程控制
*exec()族:包括execl(),execv(),execle(),execve(),execlp()和execvp()
可将一可执行模快拷贝到调用进程占有的存贮空间.正被调用进
程执行的程序将不复存在,新程序取代其位置.
这是UNIX系统中一个程序被执行的唯一方式:用将执行的程序复盖原有的
程序.