• 标 题:[转贴]安全程序设计 (24千字)
  • 作 者:doskey
  • 时 间:2002-12-1 10:50:26
  • 链 接:http://bbs.pediy.com

安全程序设计

概述
在当前的软件行业里,太多的程序有安全问题,代码在被发布前只是经过很少的测试,即使
一些有专业测试人员的软件公司也很少进行安全编程方面的测试,原因在于缺少对安全编程
技术的了解。本文将尝试给程序员一个比较清晰的概念,安全漏洞的来源,和避免安全漏洞
的技巧,使写安全程序的过程变得轻松起来。
运用好的编程技巧是非常重要的,甚至你的代码只是将运行在限制的时期和限制的条件下。
许多程序员的程序常超越其最初的设计范围,大部分的安全漏洞出现的环境是当初程序员不
知道或没有想到的。典型的是,程序员假设当前的系统调用永不会失败,或者程序参数永远
不可能超过某个长度。因而,程序员能做到最好的事情就是对问题进行假定编程,仔细分析
它们是否正确,和想象可以使其失败的条件。

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系统中一个程序被执行的唯一方式:用将执行的程序复盖原有的
程序.

  • 标 题:第二部分 (16千字)
  • 作 者:doskey
  • 时 间:2002-12-1 10:52:11
  • 链 接:http://bbs.pediy.com

安全注意事项:
. 实际的和有效的UID和GID传递给由exec()调入的不具有SUID和SGID许
可的程序.
. 如果由exec()调入的程序有SUID和SGID许可,则有效的UID和GID将设
置给该程序的所有者或小组.
. 文件建立屏蔽值将传递给新程序.
. 除设了对exec()关闭标志的文件外,所有打开的文件都传递给新程序.
用fcntl()子程序可设置对exec()的关闭标志.
*fork():用来建立新进程.其建立的子进程是与调用fork()的进程(父进程)
完全相同的拷贝(除了进程号外)
安全注意事项:
. 子进程将继承父进程的实际和有效的UID和GID.
. 子进程继承文件方式建立屏蔽值.
. 所有打开的文件传给子进程.
*signal():允许进程处理可能发生的意外事件和中断.
需要两个参数:信号编号和信号发生时要调用的子程序.
信号编号定义在signal.h中.
信号发生时要调用的子程序可由用户编写,也可用系统给的值,如:SIG_IGN
则信号将被忽略,SIG_DFL则信号将按系统的缺省方式处理.
如许多与安全有关的程序禁止终端发中断信息(BREAK和DELETE),以免自己
被用户终端终止运行.
有些信号使UNIX系统的产生进程的核心转储(进程接收到信号时所占内存
的内容,有时含有重要信息),此系统子程序可用于禁止核心转储.

(3)文件属性
*access():检测指定文件的存取能力是否符合指定的存取类型.
需要两个参数:文件名和要检测的存取类型(整数).
下面还有喔 (19%) │ 结束 ← │ ↑/↓/PgUp/PgDn 移动 │ ? 辅助说明 │
需要两个参数:文件名和要检测的存取类型(整数).
存取类型定义如下:
0: 检查文件是否存在
1: 检查是否可执行(搜索)
2: 检查是否可写
3: 检查是否可写和执行
4: 检查是否可读
5: 检查是否可读和执行
6: 检查是否可读可写可执行
这些数字的意义和chmod命令中规定许可方式的数字意义相同.
此子程序使用实际的UID和GID检测文件的存取能力(一般有效的UID和GID
用于检查文件存取能力).
返回值: 0:许可 -1:不许可.
*chmod():将指定文件或目录的存取许可方式改成新的许可方式.
需要两个参数:文件名和新的存取许可方式.
*chown():同时改变指定文件的所有者和小组的UID和GID.(与chown命令不
同).
由于此子程序同时改变文件的所有者和小组,故必须取消所操作文件的SUID
和SGID许可,以防止用户建立SUID和SGID程序,然后运行chown()去获得别
人的权限.
*stat():返回文件的状态(属性).
需要两个参数:文件路径名和一个结构指针,指向状态信息的存放
的位置.
结构定义如下:
st_mode: 文件类型和存取许可方式
st_ino: I节点号
st_dev: 文件所在设备的ID
st_rdev: 特别文件的ID
st_nlink: 文件链接数
st_uid: 文件所有者的UID
st_gid: 文件小组的GID
st_size: 按字节计数的文件大小
st_atime: 最后存取时间(读)
st_mtime: 最后修改时间(写)和最后状态的改变
st_ctime: 最后的状态修改时间
返回值: 0:成功 1:失败
*umask():将调用进程及其子进程的文件建立屏蔽值设置为指定的存取许可.
需要一个参数: 新的文件建立屏值.

(4)UID和GID的处理
*getuid():返回进程的实际UID.
*getgid():返回进程的实际GID.
以上两个子程序可用于确定是谁在运行进程.
*geteuid():返回进程的有效UID.
*getegid():返回进程的有效GID.
以上两个子程序可在一个程序不得不确定它是否在运行某用户而不是运行
它的用户的SUID程序时很有用,可调用它们来检查确认本程序的确是以该
用户的SUID许可在运行.
*setuid():用于改变有效的UID.
对于一般用户,此子程序仅对要在有效和实际的UID之间变换的SUID程序才
有用(从原有效UID变换为实际UID),以保护进程不受到安全危害.实际上该
进程不再是SUID方式运行.
*setgid():用于改变有效的GID.

标准C库
(1)标准I/O
*fopen():打开一个文件供读或写,安全方面的考虑同open()一样.
*fread(),getc(),fgetc(),gets(),scanf()和fscanf():从已由fopen()打
开供读的文件中读取信息.它们并不关心文件的存取许可.这一点
同read().
*fwrite(),put(),fputc(),puts,fputs(),printf(),fprintf():写信息到
已由fopen()打开供写的文件中.它们也不关心文件的存取许可.
同write().
*getpass():从终端上读至多8个字符长的口令,不回显用户输入的字符.
需要一个参数: 提示信息.
该子程序将提示信息显示在终端上,禁止字符回显功能,从/dev/tty读取口
令,然后再恢复字符回显功能,返回刚敲入的口令的指针.
*popen():将在(5)运行shell中介绍.
(2)/etc/passwd处理
有一组子程序可对/etc/passwd文件进行方便的存取,可对文件读取到入口
项或写新的入口项或更新等等.
*getpwuid():从/etc/passwd文件中获取指定的UID的入口项.
*getpwnam():对于指定的登录名,在/etc/passwd文件检索入口项.
以上两个子程序返回一指向passwd结构的指针,该结构定义在
/usr/include/pwd.h中,定义如下:
struct passwd {
char * pw_name; /* 登录名 */
char * pw_passwd; /* 加密后的口令 */
uid_t pw_uid; /* UID */
gid_t pw_gid; /* GID */
char * pw_age; /* 代理信息 */
char * pw_comment; /* 注释 */
char * pw_gecos;
char * pw_dir; /* 主目录 */
char * pw_shell; /* 使用的shell */
};
*getpwent(),setpwent(),endpwent():对口令文件作后续处理.
首次调用getpwent(),打开/etc/passwd并返回指向文件中第一个入口项的
指针,保持调用之间文件的打开状态.
再调用getpwent()可顺序地返回口令文件中的各入口项.
调用setpwent()把口令文件的指针重新置为文件的开始处.
使用完口令文件后调用endpwent()关闭口令文件.
*putpwent():修改或增加/etc/passwd文件中的入口项.
此子程序将入口项写到一个指定的文件中,一般是一个临时文件,直接写口
令文件是很危险的.最好在执行前做文件封锁,使两个程序不能同时写一个
文件.算法如下:
. 建立一个独立的临时文件,即/etc/passnnn,nnn是PID号.
. 建立新产生的临时文件和标准临时文件/etc/ptmp的链,若建链失败,
则为有人正在使用/etc/ptmp,等待直到/etc/ptmp可用为止或退出.
. 将/etc/passwd拷贝到/etc/ptmp,可对此文件做任何修改.
. 将/etc/passwd移到备份文件/etc/opasswd.
. 建立/etc/ptmp和/etc/passwd的链.
. 断开/etc/passnnn与/etc/ptmp的链.
注意:临时文件应建立在/etc目录,才能保证文件处于同一文件系统中,建
链才能成功,且临时文件不会不安全.此外,若新文件已存在,即便建
链的是root用户,也将失败,从而保证了一旦临时文件成功地建链后
没有人能再插进来干扰.当然,使用临时文件的程序应确保清除所有
临时文件,正确地捕捉信号.

(3)/etc/group的处理
有一组类似于前面的子程序处理/etc/group的信息,使用时必须用include
语句将/usr/include/grp.h文件加入到自己的程序中.该文件定义了group
结构,将由getgrnam(),getgrgid(),getgrent()返回group结构指针.
*getgrnam():在/etc/group文件中搜索指定的小组名,然后返回指向小组入
口项的指针.
*getgrgid():类似于前一子程序,不同的是搜索指定的GID.
*getgrent():返回group文件中的下一个入口项.
*setgrent():将group文件的文件指针恢复到文件的起点.
*endgrent():用于完成工作后,关闭group文件.
*getuid():返回调用进程的实际UID.
*getpruid():以getuid()返回的实际UID为参数,确定与实际UID相应的登录
名,或指定一UID为参数.
*getlogin():返回在终端上登录的用户的指针.
系统依次检查STDIN,STDOUT,STDERR是否与终端相联,与终端相联的标准输
入用于确定终端名,终端名用于查找列于/etc/utmp文件中的用户,该文件
由login维护,由who程序用来确认用户.
*cuserid():首先调用getlogin(),若getlogin()返回NULL指针,再调用
getpwuid(getuid()).
*以下为命令:
*logname:列出登录进终端的用户名.
*who am i:显示出运行这条命令的用户的登录名.
*id:显示实际的UID和GID(若有效的UID和GID和实际的不同时也显示有效的
UID和GID)和相应的登录名.
(4)加密子程序
1977年1月,NBS宣布一个用于美国联邦政府ADP系统的网络的标准加密法:数
据加密标准即DES用于非机密应用方面.DES一次处理64BITS的块,56位的加
密键.
*setkey(),encrypt():提供用户对DES的存取.
此两子程序都取64BITS长的字符数组,数组中的每个元素代表一个位,为0
或1.setkey()设置将按DES处理的加密键,忽略每第8位构成一个56位的加
密键.encrypt()然后加密或解密给定的64BITS长的一块,加密或解密取决
于该子程序的第二个变元,0:加密 1:解密.
*crypt():是UNIX系统中的口令加密程序,也被/usr/lib/makekey命令调用.
crypt()子程序与crypt命令无关,它与/usr/lib/makekey一样取8个字符长
的关键词,2个salt字符.关键词送给setkey(),salt字符用于混合encrypt()
中的DES算法,最终调用encrypt()重复25次加密一个相同的字符串.
返回加密后的字符串指针.
(5)运行shell
*system():运行/bin/sh执行其参数指定的命令,当指定命令完成时返回.
*popen():类似于system(),不同的是命令运行时,其标准输入或输出联到由
popen()返回的文件指针.
二者都调用fork(),exec(),popen()还调用pipe(),完成各自的工作,因而
fork()和exec()的安全方面的考虑开始起作用.


写安全的C程序

一般有两方面的安全问题,在写程序时必须考虑:
(1)确保自己建立的任何临时文件不含有机密数据,如果有机密数据,设置临时文件仅对自己可
读/写.确保建立临时文件的目录仅对自己可写.

(2)确保自己要运行的任何命令(通过system(),popen(),execlp(),execvp()运行的命令)的确
是自己要运行的命令,而不是其它什么命
令,尤其是自己的程序为SUID或SGID许可时要小心.
第一方面比较简单,在程序开始前调用umask(077).若要使文件对其他人可读,可再调chmod()
,也可用下述语名建立一个"不可见"的临时文件.
creat("/tmp/xxx",0);
file=open("/tmp/xxx",O_RDWR);
unlink("/tmp/xxx");
文件/tmp/xxx建立后,打开,然后断开链,但是分配给该文件的存储器并未删除,直到最终指向
该文件的文件通道被关闭时才被删除.打开该文件的进程和它的任何子进程都可存取这个临时
文件,而其它进程不能存取该文件,因为它在/tmp中的目录项已被unlink()删除.
第二方面比较复杂而微妙,由于system(),popen(),execlp(),execvp()执行时,若不给出执行
命令的全路径,就能"骗"用户的程序去执行不同的命令.因为系统子程序是根据PATH变量确定
哪种顺序搜索哪些目录,以寻找指定的命
令,这称为SUID陷井.最安全的办法是在调用system()前将有效UID改变成实际UID,另一种比较
好的方法是以全路径名命令作为参数.execl(),execv(), execle(),execve()都要求全路径名
作为参数.有关SUID陷井的另一方式是在程序中设置PATH,由于system()和popen()都启动she
ll,故可使用shell句法.如:
system("PATH=/bin:/usr/bin cd");
这样允许用户运行系统命令而不必知道要执行的命令在哪个目录中,但这种方法不能用于exe
clp(),execvp()中,因为它们不能启动shell执行调用序列传递的命令字符串.

关于shell解释传递给system()和popen()的命令行的方式,有两个其它的问题:

*shell使用IFS shell变量中的字符,将命令行分解成单词(通常这个shell变量中是空格,tab
,换行),如IFS中是/,字符串/bin/ed被解释成单词bin,接下来是单词ed,从而引起命令行的曲
解.

再强调一次:在通过自己的程序运行另一个程序前,应将有效UID改为实际的UID,等另一个程序
退出后,再将有效UID改回原来的有效UID.SUID/SGID程序指导准则

(1)不要写SUID/SGID程序,大多数时候无此必要.
(2)设置SGID许可,不要设置SUID许可.应独自建立一个新的小组.
(3)不要用exec()执行任何程序.记住exec()也被system()和popen()调用.
. 若要调用exec()(或system(),popen()),应事先用setgid(getgid())将有效GID置加实际GI
D.
. 若不能用setgid(),则调用system()或popen()时,应设置IFS:
popen("IFS=\t\n;export IFS;/bin/ls","r");
. 使用要执行的命令的全路径名.
. 若不能使用全路径名,则应在命令前先设置PATH:popen("IFS=\t\n;export IFS;PATH=/bin
:/usr/bin;/bin/ls","r");

. 不要将用户规定的参数传给system()或popen();若无法避免则应检查变元字符串中是否有
特殊的shell字符.

. 若用户有个大程序,调用exec()执行许多其它程序,这种情况下不要将大程序设置为SGID许
可.可以写一个(或多个)更小,更简单的SGID程序执行必须具有SGID许可的任务,然后由大程序
执行这些小SGID程序.

(4)若用户必须使用SUID而不是SGID,以相同的顺序记住(2),(3)项内容,并相应调整.不要设置
root的SUID许可.选一个其它户头.

(5)若用户想给予其他人执行自己的shell程序的许可,但又不想让他们能读该程序,可将程序
设置为仅执行许可,并只能通过自己的shell程序来运行.

编译,安装SUID/SGID程序时应按下面的方法
(1)确保所有的SUID(SGID)程序是对于小组和其他用户都是不可写的,存取权限的限制低于47
55(2755)将带来麻烦.只能更严格.4111(2111)将使其他人无法寻找程序中的安全漏洞.
(2)警惕外来的编码和make/install方法. 某些make/install方法不加选择地建立SUID/SGID
程序.
. 检查违背上述指导原则的SUID/SGID许可的编码.
. 检查makefile文件中可能建立SUID/SGID文件的命令.
root程序的设计

有若干个子程序可以从有效UID为0的进程中调用.许多前面提到的子程序,
当从root进程中调用时,将完成和原来不同的处理.主要是忽略了许可权限的检查.
由root用户运行的程序当然是root进程(SUID除外),因有效UID用于确定文件的存取权限,所以
从具有root的程序中,调用fork()产生的进程,也是root进程.
(1)setuid():从root进程调用setuid()时,其处理有所不同,setuid()将把有效的和实际的UI
D都置为指定的值.这个值可以是任何整型数.而对非root进程则仅能以实际UID或本进程原来
有效的UID为变量值调用setuid().
(2)setgid():在系统进程中调用setgid()时,与setuid()类似,将实际和有效的GID都改变成其
参数指定的值.
* 调用以上两个子程序时,应当注意下面几点:
. 调用一次setuid()(setgid())将同时设置有效和实际UID(GID),独立分别设置有效或实际U
ID(GID)固然很好,但无法做到这点.
. setuid()(setgid())可将有效和实际UID(GID)设置成任何整型数,其数值不必一定与/etc/
passwd(/etc/group)中用户(小组)相关联.
. 一旦程序以一个用户的UID了setuid(),该程序就不再做为root运行,也不可能再获root特权
.
(3)chown():当root进程运行chown(),chown()将不删除文件的SUID和/或SGID许可,但当非ro
ot进程运行chown()时,chown()将取消文件的SUID和/或SGID许可.
(4)chroot():改变进程对根目录的概念,调用chroot()后,进程就不能把当前工作目录改变到
新的根目录以上的任一目录,所有以/开始的路径搜索,都从新的根目录开始.
(5)mknod():用于建立一个文件,类似于creat(),差别是mknod()不返回所打开文件的文件描述
符,并且能建立任何类型的文件(普通文件,特殊文件,目录文件).若从非root进程调用mknod(
)将执行失败,只有建立FIFO特别文件(有名管道文件)时例外,其它任何情况下,必须从root进
程调用mknod().由于creat()仅能建立普通文件,mknod()是建立目录文件的唯一途径,因而仅
有root能建立目录,这就是为什么mkdir命令具有SUID许可并属root所有.

一般不从程序中调用mknod().通常用/etc/mknod命令建立特别设备文件而这些文件一般不能
在使用着时建立和删除,mkdir命令用于建立目录.当用mknod()建立特别文件时,应当注意确从
所建的特别文件不允许存取内存,磁盘,终端和其它设备.

(6)unlink():用于删除文件.参数是要删除文件的路径名指针.当指定了目录时,必须从root进
程调用unlink(),这是必须从root进程调用unlink()的唯一情况,这就是为什么rmdir命令具有
root的SGID许可的原因.

(7)mount(),umount():由root进程调用,分别用于安装和拆卸文件系统.这两个子程序也被mo
unt和umount命令调用,其参数基本和命令的参数相同.调用mount(),需要给出一个特别文件和
一个目录的指针,特别文件上的文件系统就将安装在该目录下,调用时还要给出一个标识选项
,指定被安装的文件系统要被读/写<0>还是仅读<1>.umount()的参数是要一个要拆卸的特别文
件的指针.

系统设计法则

不管使用何种编程语言、程序用途、和什么技巧写的,下面的法则可
以帮助你断定程序是否是bug-free的

1. 最小权限. 编制和使用最少且足够的权限去完成任务,问自己,
"软件*必需*要什么权限?",而不是"软件需要什么权限"。
2. 结构经济. 短,简单的代码产生的Bug当然比长和复杂的少,用尽
可能少的代码实现系统。
3. 完全检测. 检查访问对象的所有途径,所有调用的返回代码,和关
键点的变量值。
4. 开放设计. 不要用隐晦的方法来保证安全
5. 特权分开. 在不同的程序或函数里不同时间只给出最需要的权限。
6. 最少公用机制. 应该给用户最少的共享资源。
7. 心理可接受性. 安全控制必须容易使用,否则容易被用户绕过去不
去使用安全特性。
8. 默认的错误防护. 默认拒绝,和错误关闭
9. 代码重用. 尽可能使用以前测试过的代码
10. 不信任未知的. 所有从用户那里得到的信息,或者从外面的程序都
是可以怀疑的.
11. 在问题出现前作出预测. 在开始写程序之前确定你的程序功能和设
计可能会出现什么安全问题。

安全编程方法
1. 检查所有的命令行参数
2. 检查所有的系统调用参数和返回代码
3. 检查环境参数,不要依靠Unix环境变量
4. 确定所有的缓存都被检查过
5. 在变量的内容被拷贝到本地缓存之前对变量进行边界检查
6. 如果创建一个新文件,使用O_EXCL和O_CREATE标志来确定文件没有已经存在
7. 使用lstat()来确定文件不是一个符号连接
8. 使用下面的这些库调用: fgets(), strncpy(), strncat(), snprintf()
而不是其它类似的函数,可以说,只使用检查了长度的函数.
9. 同样的,小心的使用execve(),如果你必须衍生一个进程
10.在程序开始时显式的更改目录(chdir())到适当的地方
11.限制当程序失败时产生的core文件,core文件里有可能含有密码和其它内存状态信息.
12.如果使用临时文件,考虑使用系统调用tmpfile()或mktemp()来创建它们
(虽然很多mktemp()库调用可能有race condition的情况)
13.内部有做完整性检查的代码
14.做大量的日志记录,包括日期,时间,uid和effective uid,gid和effe
ctive gid,终端信息,pid,命令行参数,错误和主机名
15.使程序的核心尽可能小和简单
16.永远用全路径名做文件参数
17.检查用户的输入,确保只有"好"的字符
18.使用好的工具如lint
19.理解race conditions,包括死锁状态和顺序状态
20.在网络读请求的程序里设置timeouts和负荷级别的限制.
21.在网络写请求里放置timeouts
22.使用会话加密来避免会话抢劫和隐藏验证信息
23.尽可能使用chroot()设置程序环境
24.如果可能,静态连接安全程序
25.当需要主机名时使用DNS逆向解释
26.在网络服务程序里分散和限制过多的负载
27.在网络的读和写里放置适当的timeout限制
28.如果合适,防止服务程序运行超过一个以上的拷贝

不安全的编程方法
1. 防止使用在处理字符串时不检查buffer边界的函数,如gets(),
strcpy(), strcat(), sprintf(),fscanf(), scanf(), vsprintf(), realp
ath(), getopt(), getpass(), streadd(), strecpy(),和strtrns()
2. 同样,避免使用execlp()和execvp()
3. 永远不要用system()和popen()系统调用
4. 不要将文件创建文件在全部人可写的目录里
5. 通常,不要设置setuid或者setgid的shell scripts
6. 不要假想端口号码,应该用getservbyname()函数
7. 不要假设来自小数字的端口号的连接是合法和可信任的
8. 不要相信任何IP地址,如果要验证,用密码算法
9. 不要用明文方式验证信息
l0 不要常识从严重的错误中恢复,要输出详细信息然后中断
l 1 考虑使用perl -T或taintperl写setuid的perl程序

测试程序安全
1. 用cracker的方法来做软件测试:
2. 尝试使程