当前位置:Linux教程 - Linux - 安全编程详解v1.00

安全编程详解v1.00

v安全编程详解v1.00

翻译:By [email protected] --> 此原文是来自www.securityfocus.com中的Secure Programming v1.0,本人建议能读原文读原文,如果你英语实在不过关,就读此文吧,并且感谢你能花不少时间来看这篇我敲了很长时间的文章。
--------------------------------------------------------------------------------
0.0 目的

1.0 一般情况
1.1 缓冲溢出

1.1.1 strcpy
1.1.2 strcat
1.1.3 sprintf
1.1.4 gets
1.1.5 scanf, sscanf, fscanf
1.1.6 memcpy
1.1.7 C++

1.2 用户数据合法性检查
1.3 DNS名字合法性检查
1.4 返回值(Return Values)
1.5 设备驱动程序(Device Drivers)

2.0 UNIX 操作系统

2.0.1 Real UID(实际UID), effective UID(有效UID), 和saved UID(保留UID)
2.0.2 setuid 和setgid 程序

2.1环境变量
2.2默认文件权限 (umask)
2.3 不安全的使用临时文件

2.3.1 tmpfile()
2.3.2 mkstemp()
2.3.3 mktemp()
2.3.4 一些额外的解决方法

2.4 文件竞争条件问题
2.5 chroot环境实现
2.6 丢弃权利

2.6.1 In a network service
2.6.2 In a local setuid program

2.7 Generating Random Numbers

2.7.1 Linux
2.7.2 OpenBSD

2.8 Invoking Child Processes
2.9 Resource Limitations
2.9.1 Core files


3.0 Windows Operating Systems

3.1 Access Control Lists
3.2 Registry Keys
3.3 Files
3.4 Generating Random Numbers


4.0 Designing Protocols

4.1 Authentication
4.2 Confidentiality
4.3 Pitfalls

4.3.1 Replay Attacks


5.0 Tools

5.1 Unix Tools
5.2 Windows Tools


6.0 References and Resources

6.1 Contributors

本文目的

此文档的目的是为了使读者了解怎样安全编程,帮助用户能很快的运行这些技术应用于到编程里面去。我们这里叙述了关于设计安全应用程序的内容,如软件产品实现中一些容易犯的错误和缺陷。
在处于繁忙紧张的软件开放周期今天,只有少量或者没有安全经验编程的人参加开发程序是很常见的,个人的在对应用程序中实现占有重要地位与安全很有关系,不光光个人会出现错误,一般组织,团体,为了短时间里完成产品开发,也很少会重视到安全问题,最后受到更多损失的还是团体。一个基本的原则,安全问题存在与任何类型的应用程序,不光光是一般应用程序,连一些有关安全的应用程序也会存在某些安全问题(如ISS开发的安全产品中也存在缓冲溢出等问题)。

漏洞大多数是会导致程序强制操作一些不同与原来作者本来意图的行为,而这些缺陷和错误一般遵循着的固定的样式,我们这文章的目的是示范怎样组织它们,多数情况下,我们的思路是考虑”假如这样然后会怎样?”和仿效攻击者的思路。
对于计算机软件可靠性方面已经有不少方面的研究,威斯康星大学研究所对许多不同UNIX版本操作系统基本工具程序进行过测试。在他们的“Fuzz Revisited: A Re-examination of the Reliability of UNIX Utilities and Services”一文中,研究人员通过传递随机数据到程序中对这些程序进行过测试,在1990年开始的测试过程中,发现普通的程序超过”%40”和X-WINDOWS程序存在问题而导致崩溃或者挂起,不过在1995年的测试一中,同样的失败率得到一定程度的改良,不过仍旧在18-23%之间,不过发现商业类型操作系统比一些开放代码的系统如LINUX高很多,一般在2倍左右,GNU软件的故障率最低,只有7%。
而研究表明问题一般存在与传递随机的数据往往导致错误的产生,而这些错误可以通过构建精确的结构数据而利用并导致恶意的结果(如ROOT权利的获得)。
This document is currently maintained by Oliver Friedrichs [email protected]. This document is a work in progress and is expanded upon by discussions that have taken place on the SECPROG mailing list at http://www.securityfocus.com/forums/secprog/.

1.0 一般情况
这部分章节讨论的安全编程技术可以同时应用于UNIX和WINDOW操作系统,涵盖的主题可以在多种语言和操作系统的得到应用,这些技术能在绝大多数计算机上应用的编程的定例和概念。

1.1 缓冲溢出

至今为止最通常的一种安全漏洞—缓冲溢出在今天的用种应用程序中存在,不过这个问题不是新问题,相反它在前10年前就存在于一些操作系统和应用程序,而1近10年来此类型问题的发现和在重要软件上被利用的频率飞快的增加,这不是问题不存在,而在于这种攻击的理念的性质而不被以前多数用户感兴趣,今天随着极具机械式的怎样利用这些问题的教育和一些可以说“异端“人的增加,导致使用个技术简单话。
此部分目的不是描述怎样挖掘一个有缓冲溢出的漏洞,而是理解这种类型问题的重要性和运行这种程序会导致怎样的严重后果。
缓冲溢出出现在当一些数据拷贝到内存区域中产生,如果内存的缓冲区不能够容纳这些数据,到拷贝成功的时候,目标内存的边界部分就会被覆盖。程序中的变量可以被在程序栈(stack)中也可以在程序堆(heap)中,因此我们也可以常听到这些字汇,栈溢出和堆溢出,相比较这两个溢出类型,栈溢出的利用多数情况下会比较容易点。
让我们看看一段存在缓冲溢出错误的源代码:
void function(char *str)
{
char buffer[16];
strcpy(buffer,str);
}

void main()
{
char large_string[256];
int i;

for( i = 0; i < 255; i++)
large_string[i] = ''A'';

function(large_string);
}
在上面的程序中,func()函数拷贝一有255字节组成的字符串到只有16字节大小的缓冲中,并且由于使用了strcpy()而没有检查字节数,这样你编译运行这个程序的话,运行的时候将导致段冲突。
255个字符串拷贝到缓冲区时,缓冲在开头16个字节填充后就被填满,这样,多出来的字符串将覆盖堆栈中的其他变量,包括函数的返回地址,这返回地址是指当前函数返回时程序所需继续执行的内存地址,通过修改这个地址,攻击者可让程序在不同的位置后续执行,而如果在刚才递交给缓冲中的数据带有指令代码,且被覆盖的地址又切切指向这段代码,此代码就会被执行.
通过使用strncpy()函数代替,就能比较安全的拷贝字符串,因为它只会将大小同目标缓冲区的数据拷贝.
现在许多有缓冲溢出倾向的函数都存在相对安全的副本函数,它们都通过传递大小参数来限制多少数据被调用

 strcpy() 此函数没有执行任意长度的确认,而使用strncpy()函数可以限制长度,当使
用strncpy()的时候,你需要含蓄地以NULL终结字符串,因为有NULL终
止符暗示定义源缓冲区的大小

不正确情况 正确的情况

void func(char *str) void func(char *str)
{ {
char buffer[256]; char buffer[256];
strcpy(buffer, str); strncpy(buffer, str, sizeof(buffer) -1);
return; return;
} }

strcat() 此函数类似与strcpy(),也不检查字符串的长度,使用strncat()可以很好的限制
被拷贝的长度,strncat()函数只会追加定义在大小参数中的长度,NULL终止符将终止字
符串.

不正确情况 正确的情况

void func(char *str) void func(char *str)
{ {
char buffer[256]; char buffer[256];
strccat(buffer, str); strncat(buffer, str, sizeof(buffer) -1); return; return;
} }

sprintf() 此函数用来格式化字符串变量,是很容易产生缓冲溢出的地方,前几个月出现
过很多严重的缓冲溢出,如wuftp,就是使用了这个函数的缘故.使用snprintf()
可以很好的限制拷贝到缓冲中的打印数据长度,snprintf()函数返回要传递过
去的数据字符总数,如果它大于能传递字节大小的值,那么没有任何数据写到
缓冲中,用户必须检查snprintf()的值以保证要被打印的缓冲.

不正确情况 正确的情况

void func(char *str) void func(char *str)
{ {
char buffer[256]; char buffer[256];
sprintf(buffer,”%s”, str); if (snprintf(target, sizeof(target) - 1, "%s", return; string) > sizeof(target) - 1)
} /* 缓冲发生 */
return;
}
gets 此函数天生就存在漏洞,你在应用程序中应该永远不要使用,一般在你编译的时候
也会被警告,gets()没有规定任意长度,永远将导致缓冲溢出,它从标准输入中读取数
据并一直到换行符或者EOF标识接收到,所读的数据全部填充到定义的缓冲,并不
检查任何长度.

不正确情况 正确的情况

void func(char *str) void func(char *str)
{ {
char buffer[256]; char buffer[256];
gets(buffer, str); fgets(buffer, sizeof(buffer) –1,stdin);
return; return;
} }
而上面的fgets()函数用来代替不安全的gets()函数,此函数会至多读取’sizeof(buffer) – 1’
的文件流 .当然你中途读取到EOF或者换行符也会停止.

scanf(), 在使用这些函数的时候你需要小心读取要放到固定长度缓冲区的数据,你须确保规sscanf(), 定要读到缓冲区的数据长度,下面很好的演示了有问题和安全的两个版本:
fscanf()
有漏洞的版本 安全的版本
char buffer[256]; char buffer[256];
int num; int num;
num = fscanf(stdin, "%s", buffer); num = fscanf(stdin, "%255s", buffer);
你可以看到上面安全的那个示例,规定了只能读取255个字符到规定的缓冲中,如果类似
上面所示第一个示例,就会无限制的读取,并导致函数的堆栈被破坏.

Memcpy() 由于不安全的使用memcpy()会导致不少溢出,当规定了长度的memcpy()函数
可以受外界操作的时候,就可能发生,所以保证拷贝到内存结构中的数据不大于
规定大小将非常重要,下面一个示例将说明缓冲是怎样产生的:
unsigned long copyaddress(struct hosten *hp) {
unsigned long address;

memcpy(&address, hp->h_addr_list[0], hp->h_length);
}
上面的示例来自与一个出现在BIND (Berkeley 网路名字守护程序)上的实际
漏洞,在这里我们为了教育目的简单花了,上面的函数拷贝hp->h_length字节
到’address’变量中(4字节),在正常情况下, 因为是网路地址hp->h_length一般
都是4字节,但是,攻击者确可以操作利用h_length变量,如果他伪造一个假
的DNS回应,他就可以提供更长的地址通过hp-h_addr_list变量传递,这将导致
超过4个字节大小拷贝到’address’变量,溢出变量,拷贝多的数据到堆栈.

所以你确保在执行这种操作前检查长度,如:

unsigned long copyaddress(struct hosten *hp) {
unsigned long address;

if (hp->h_length > sizeof(address))
return 0;

memcpy(&address, hp->h_addr_list[0], hp->h_length);

return address;
}
此漏洞在BIND系统中已经修补.
C++ C++语言存在一些独特的额外的问题,如下面的例子会导致程序产生缓冲溢出,极类
似C中的gets()函数.
char buf[128];
cin >> buf;
上面函数中在读字符串的时候没有长度检查,需确保使用cn.width()函数来规定最
大缓冲大小.

1.2 用户数据合法性检查

在某些情况下需要确认用户输入,并去除不合法的字符或者数据.读取用户名来验证就是一个合法的例子,要尽可能的剔除我们所能知道的不合法的字符,如高位字符,空格或者数字.比较好的方法就是简单的剔除所有除了自己需要的数据,这样就不需要我们来猜测所有危险的字符,而只要我们知道安全的字符.这种错误一般在使用SHELL命令时传递数据到第二个程序的时候发生.

这种问题最突出的例子是发生在基于WEB的CGI应用程序,如phf,这个CGI程序在以前NCSA和APACHE WEB服务器中是默认安装的,phf程序通过popen()函数来传递数据到程序中剔除了一些已知的不良的字符,但是,它遗漏了其中一个字符=换行符( ),其中在HTML查询中是通过%0a代替的,通过使用这个字符把数据传递给程序的时候,攻击者可以在远程主机中执行任意命令,当在远程主机使用SHELL解程序解析的时候,换行符扮演了命令分隔符,把分行符前字符串作为一个命令,而把换行符后面的字符串作为一个新命令,通过请求下面的ULR,就可能在主机上执行”cat /etc/passwd”的命令,而攻击者就可以通过WEB服务程序的响应得到密码文件内容:

/cgi-bin/phf?Qalias=hell%0acat%20/etc/passwd%0a

在这个漏洞被发现之后,在一般的库函数中做了修补来清除输入,如果在换行符后面
增加了字符串将被截断.这样的好情况持续了一段时间直到有人开发了
BASH(Bourne Again Shell),这个很常用的UNIX SHELL解释程序却允许ASCII码255
作为命令分隔符,导致了相同的攻击产生,并导致不少操作系统受到攻击,因为这个
SHELL解释程序在LINUX下是默认安装的.要正确修补这个问题,最好就是只允许
已知的没有危害的字符输入.

不正确情况 正确的情况

#define BAD "/ ;[]<>& " #define OK "abcdefghijklmnopqrstuvwxyz BCDEFGHIJKLMNOPQRSTUVWXYZ
1234567890_-.@";

char *query() char *query()
{ {
char *user_data, *cp; char *user_data, *cp;
/* 获取数据 */ /* 获取数据 */
user_data = getenv("QUERY_STRING"); user_data = getenv("QUERY_STRING");
/* 剔除不良数据 */ /* 去除除了良性数据的所有数据*/
for (cp = user_data; *(cp += strcspn(cp, BAD)); ) for (cp = user_data; *(cp += strspn(cp, OK));)
*cp = ''_''; *cp = ''_'';
return user_data; return user_data;
} }

在不正确的例子中,只是把认为不良的数据中查询字符串中剔除,而可能存在未知的隐
患字符,正确的方法就是去掉所有字符,只保留一些我们知道的正确的数据.

1.3 DNS名字合法性检查
这里建议你永远不要在任何类型的应用程序中使用DNS名字,唯一安全的认证机制
就是使用加密.但是你如果必须使用到DNS,很重要的一条你必须记住校验主机名.

这里假设你希望对你写的一特殊网络服务限制使用单一授权的主机名,当连接接受到
的时候我们从accept()调用获得连接地址,捅咕这个地址,我们可以判断连接主机的
DNS名字,我们把这个地址传递到一确认的函数.

不正确情况 正确的情况






















在不正确的例子中,对传递的IP地址进行反向查询的.一旦反向查询完成,被查询的主机名会和我们”允许”主机名做比较,如果它们匹配,认证成功,这里就存在漏洞可以导致攻击者绕过这个安全机制,如果合法用户连接,将发生下面的情况:

a: 用户从1.2.3.4的合法地址连接
b: 服务程序对1.2.3.4执行反向查询,会对1.2.3.4网络中的DNS服务器发送一查询,并
接收到为client.allowed.com正确的响应.
C: 服务程序进行比较,并发现client.allowed.com是允许的主机名.

攻击者可以通过下面的步骤利用里面的漏洞:

a: 攻击者从合法地址5.6.7.8连接
b: 攻击者完全控制它自己C段网络中的DNS服务器,并修改它的记录使任何对地址
5.6.7.8的查询会返回client.allowed.com的主机名.
C: 服务程序对5.6.7.8执行反向查询,对5.6.7.8网络中的DNS服务器发送查询,而这个
DNS服务器是攻击者控制的,并接收5.6.7.8的正确响应是client.allowed.com.
D: 服务程序进行比较,发现client.allowed.com是允许的主机名.

正确的例子中执行对主机名做一正向查询来保证主机名的解析是正连接着的IP地址.由于主机名可以有多个IP地址,它们将全部被检查,这个检查就会让攻击者执行上面所述的攻击难度大大增加,这时攻击者还需要控制服务allowed.com域的DNS服务器,而这个服务器不是它他们自己的网络上的.他们必须先渗透远程主机的网络来控制DNS服务器,这样使控制困难大大增加.

1.4 返回值(Return Values)

检查函数返回的值看起来很符合逻辑的,但在某些情况下,它往往不会注意到,在一些安全性比较重要的应用程序中检查对所有函数的返回值是非常重要的,让我们看看下面的例子会带来什么样的安全问题:

char maketemp()
{
char *name = “/tmp/tempfile”;

creat(name, 0644);
chown(name, 0, 0);
return name;
}

在上面的情况下对没有检查函数的返回值是很大的错误,让我们来看例子中相关的返回值.如果攻击者拷贝UNIX SHELL -/bin/sh为/tmp/tempfile,creat()函数会不成功执行,这是由于文件已经存在, creat()的返回值没有被检查,函数会很高心的运行下去,这时攻击者安排文件setuid,因为不检查creat()返回值,程序下续命令就会改变文件的属主为ROOT,并递送攻击者一个setuid ROOT SHELL,提供超级用户访问.不过有些操作系统在chown被执行的时候重新设置setuid来防止这种情况,我们上面知识简单的用来演示一下.

永远确保年检查返回值的成功与否,在UNIX中,当系统调用失败的时候,程序的全局errno变量将被设置来指示出错误的原因.

1.5 设备驱动程序(Device Drivers)
设备驱动程序介入了新的安全问题,设备驱动程序拥有唯一特性,因为它们在操作系统内核运行,这意味着设备驱动程序完全控制着操作系统和操作系统内存,设备驱动程序和一般的系统进程有区别,它能访问所有系统内存,如果设备驱动程序中存在缺陷湖者错误将导致整个操作系统故障和崩溃,在UNIX下表现为内核出错,而在WINDOWS操作系统下,表现为蓝屏.一般进程中有这种问题将导致进程崩溃.
设备驱动程序通常把接口或者API”暴露”在用户空间中,如果没有对其采取适当的预防将导致不可预期的影响.如果系统调用-即把操作系统内核部分的功能”暴露”给用户,也会存在这个问题,不光光你自己你对操作系统增加新的系统调用会靠不住,有时在操作系统中默认的标准系统调用也会出现这种问题.
当写有外界输入的设备驱动程序的时候,你必须-一定的很小心的对所有输入变量进行净化处理和检查,下面的情况你需要预防输入数据确认:
a:地址检查-当指向结构或者内存位置的指针通过ioctl()调用传递给设备驱动程序的时
候,必须确认只很必须有效而不是指向非法内存,如果一个不合法的指针被传递和引用,
结果往往造成操作系统崩溃.
B: 符号/无符号不匹配 – 这个问题在设备驱动程序和系统调用执行没有正确处理无符
号变量时普遍存在.如果用户传递很大的数字到函数调用,并希望作为无符号数字处
理,但是有些不完善的函数会把它当作一有符号的数字,这样这个值将被转换为不可预
期的负值,如果把这个数字作为偏移量或者内存中的指数,很严重的问题将发生.

一个有趣的历史性错误例子就是多年前[4]基于BSD控制台驱动程序,驱动程序在内存中包含一屏幕缓冲,但没有正确验证特殊的键值,如backspace(倒退)键,用户可以用backspace(倒退)键返回到屏幕开始处,然后继续操作backspace(倒退)键知道屏幕缓冲之外,导致覆盖缓冲前面的内存数值.

2.0 UNIX操作系统
这部分我们讨论应用在基于UNIX操作系统的安全编程问题,UNIX家族的操作系统包含的一些安全机制不会出现在其他如WINDOWS操作系统上,这些不同处是软件工作者在开发基于UNIX软件中需要注意的.

2.0.1 Real UID(实际UID), effective UID(有效UID), 和saved UID(保留UID)

所有运行在UNIX操作系统上的程序在特殊系统用户和组权利下运行,每个进程拥有自己的一系列信任内容,它是操作系统用来执行访问验证检查.文件,目录和系统资源的权限是基于使用它们的用户分配的.操作系统内核自身是执行访问验证检查来判断一进程是否有权利访问资源的实体.了解信任系统是怎样工作非常重要,许多安全漏洞是没有完全理解这些内容而出现的.
当前POSIX操作系统对每个进行采用3类信任方式:

Real UID 和 Real GID :实际用户ID和实际组ID作为进程运行,只有进程以超级用户
或如果用户ID或组ID它们想变为相同的有效用户ID或组ID(见下面)才能改变运行着的uid和gid一进程要想改变运行中
的uid和gid.

有效UID和有效GID 每个进程有一个有效用户ID,在通常的环境中最初是设置成和
实际用户ID一样的值,但是当一setuid文件被执行以后,有效用户ID属于文件的属主,因此在执行setuid root程序中,进程的有效uid会变为0(root),而此时实际uid会仍旧是用户UID.这是系统调用用于判断进程是否有足够的权利访问一资源.

保留UID和保留GID 每个进程有一个保留用户ID和组ID,这是当setuid或者setgid
被执行的时候设置有效用户ID和有效组ID的,这些保留ID
允许进程临时丢弃权利(通过设置有效uid或gid给一个权利低
的用户)并在以后重新获取这些权利使用.

注意:没有系统调用可以直接获得或设置保留uid或gid,由于保留ID在程序执行段相等于有
效ID,你可以通过使用geteuid() 和getegid()来确定保留ID

2.0.2 SetUID 和SetGID程序

通常情况下一用户执行程序时,程序以当前用户的属性来运行,而UNIX操作系统支持用户以文件属主或组的属性来执行,这在文件设置了”setuid”和”setgid”位时发生.就是说当文件设置了”setuid”位,程序可以以文件属主的权利运行,而文件被设置了”setgid”,程序就可以以文件属组的权利运行,这两个属性可以同时在一个文件上设置.请看下例:
-rwsr-sr-x 1 root wheel 32045 Jun 6 10:15 program
上面的”s”表示程序设置了”setuid”和”setgid”,不管程序是谁运行,它可以以root用户和wheel组来执行,这里相关了一个安全问题,如果程序有一个缺陷,就可能导致把一个普通用户提升为root用户.
Setuid和setgid程序在此文中直接影响所有问题,下面是一些必须采用的常规方法:
1,尽可能缩小程序的权利.
2,当你完成需要这些权利的任务时马上丢弃.
3,定义一个健康的API,它必须在涵盖程序内部所有功能,所谓用户接口提供给用户,尽可
能检查这个API的安全问题.
4,不要信任任何外部输入,确保你使用他们之前验证和净化数据.

2.1 环境变量
环境变量类似命令行选项,提供了一种提供任何数据给程序的方法,环境变量的安全问题在本地系统程序中超过网络服务,因为网络服务一般情况下不接受环境参数(少数除外).环境变量问题可以造成许多不合法的程序响应:
1,缓冲溢出 – 此问题可其他情况下一样,没有处理好对其的检查,下面是一个常见的类型:

char *s, buf[128];

if (!(s = getenv(“HOME”)))
return –1;

strcpy(buf, s);

上面的函数中,没有考虑到要获取的”HOME”环境变量的长度,超过128字节参数提供给缓冲就造成溢出.
一个严重的问题在FREEBSD系统中发现过,Thomas Ptacek发现在FREEBSD的C实时运行库(C runtime library)中对环境变量处理有问题,而C实时运行库(C runtime library)静态连接了系统每一个程序,结果造成系统上每个setuid/setgid存在缓冲溢出.
下面一段话是引用1997年2月Ptacek发表在Bugtraq上的:
“在FREEBSD2.1.5上存在一个严重的安全问题, C实时运行库(C runtime library)可让任
何人控制进程的环境变量而导致执行任意代码,所有SUID程序都受此漏洞影响.

问题存在” startup_setrunelocale()",如果部分环境变量被设置,会直接拷
贝”PATH_LOCALE”的值到1024缓冲中,攻击者只需简单的把机器码和虚拟内存地址
插入到”PATH_LOCALE”中,就可以启动locale处理和运行SUID程序.

2,继承性问题 – 环境变量会被继承到通过主进程派生的子进程,因此当传递环境变量到
子进程时必须很小心.你可以使用execle() 和execve()系统调用来运行程序并定义它的
环境变量.

2.2 默认属性(umask)

当你建立文件或者运行程序时,它建立一系列默认的文件属性,这些属性通过设置进程的umask来规定,它可以从登陆SHELL或者当前进程父进程中继承下来.很重要的一点你必须确保文件建立时没有不安全的属性,不允许未授权用户访问他们.

Umask设置可以通过使用umask()库调用来调整,它接受设置位作参数,这些位用来在建立的文件中清除相关联的位,如:
umask(0) results in -rw-rw-rw-

上面没有清楚新建立文件的任何位.
umask(022) results in -rw-r--r--

上面部分清除了文件属性中组和其他人的可写位.

umask(066) results in -rw-------
上面例子清除了组和其他人的读和写位.

2.3 不安全的使用临时文件
许多漏洞发生在程序访问知名或可猜测文件上,一个程序可能包含这样一个漏洞:它在文件临时目录中打开文件,盲目的写数据到文件中,通过利用符号连接,攻击者经常可以重定向数据到其他文件中,这种类型的攻击往往发生在下面2个情况下:

1,一个有特权的程序可以被攻击者执行.这个有特权的程序在系统上以超级用户在系统临时目录中打开了一文件:
/tmp/program.temp
通过在执行前建立一个符号连接,攻击者可以把这个文件指向其他特殊文件:
ln –s /etc/passwd /tmp/program.temp
当程序写数据到文件的时候,由于符号连接的作用,实际上写到了/etc/passwd中,如果写
的数据能被攻击者控制,那就可能获得ROOT的权利

2,攻击者期望系统上其他用户执行一已经知道有临时文件处理问题的非特权程序,攻击
者建立如下连接:
ln –s /etc/passwd /tmp/program.temp
并等待其他用户执行程序,密码文件可以被写.

上面两种情况主要区别在于第一个情况,程序设置了setuid root而以超级用户执行,而第
个情况是利用其他用户的权利来执行.

对这些问题有几种解决方法:

1,不要在/tmp建立临时文件.
2,如果要建立临时文件,其名字必须随机或者不可猜测.
3,利用一系统提供的接口(mkstemp())来建立临时文件.

2.3.1 tmpfile()

tmpfile()函数可以建立临时文件并返回一打开的文件句柄给文件流,tmpfile()可以避免在产生临时文件名和建立文件时产生竞争条件问题,tmpfile()函数描述如下:

FILE *tmpfile(void);
在许多操作系统中,tmpfile()会使用mkstemp()函数来获得和建立临时文件名,然后在unlink()文件,再fdopen()文件描述符来返回流句柄.

此函数的好处
1,临时文件可以安全的建立.
2,避免在产生临时文件名和打开这个文件之间的环境竞争.
3,临时文件一旦建立变不能连接,防止文件在以后被任何人访问和打开-一允许tmpfile()
返回的流(stream)才能访问文件.

此函数的约束之处:

1,用户不能控制文件建立的位置,默认情况在在/var/tmp或者/tmp.
2,文件突然被设置为不能连接(unlinked),因此它不能被另一个进程打开.
3,当文件描述符关闭的时候,文件就消失(因为它被设置unlinked),所有数据丢失.

2.3.2 mkstemp()
mkstemp()函数建立临时文件,返回文件描述符给文件,mkstemp()避免在产生临时文件名和建立文件之间产生环境竞争问题.mkstemp()函数描述如下:

int mkstemp(char *template);

提供给mkstemp()函数的template提供了临时文件的路径,其中template是一系列X结尾,必须由随机值填充来建立随机的临时文件名,如:

fd = mkstemp(“/tmp/tempfileXXXXXX”);

当使用这个函数的时候,你需要保证X的尾数至少为6位,有些系统支持超过6为以增加随机文件名.

此函数的好处:
1,临时文件可以安全的被建立,并返回时打开文件描述符给文件.
2, 避免在产生临时文件名和打开这个文件之间的环境竞争.
3,一些操作系统选用运行进程的ID来产生文件名,这造成了文件名可猜测,可是这个函数仍旧可以对环境竞争问题免疫.

此函数的约束之处:

1,此文件对于其他进程可见,并且不是为unlinked状态.
2,如果文件权限设置不安全,其他进程可以查看或者修改文件内容.

2.3.3 mktemp()
mktemp()函数用来产生唯一文件名而没有实际建立文件,mktemp()定义如下:

char *mktemp(char *template);
函数类似mkstemp()接受template参数, 其中template是一系列X结尾,必须由随机值填充来建立随机的临时文件名,如:

fd = mkstemp(“/tmp/tempfileXXXXXX”);

当使用这个函数的时候,你需要保证X的尾数至少为6位,有些系统支持超过6为以增加随机文件名.

此函数好处:
1,临时文件可以被获得(文件没有建立).
此函数约束之处:
1,在绝大多数操作系统中,因此随机数只有进程ID产生文件可以简单的被猜测,这可能导致产生竞争环境问题.
2,文件名是唯一的,mktemp()没有验证是否存在.
3,如果使用不正确,在文件名产生和文件由程序实际产生之时产生竞争环境问题.

当使用mktemp()时候需要在文件产生后打开文件,在打开文件的时候需要小心检查:

open(filename, O_WRONLY | O_CREAT, 0644);

上面的调用就不安全,即使文件已经存在它也会建立文件.造成存在文件被覆盖.

open(filename, O_WRONLY | O_CREAT | O_EXCL, 0644);
上面的操作就可以安全的建立文件,因为如果文件存在调用操作将不成功.

2.3.4 一些额外的解决方法:
在建立临时文件的时候在/tmp建立一个额外的目录.如你使用mktemp()的时候产生一个临时文件名,可是你可以使用这个作为目录名,然后在其目录内建立临时文件.但是你需要确保建立的目录权限.通过umask()来设置一个安全的值。


2.4 文件竞争条件问题(Race Conditions)

在很多不同条件下会产生竞争条件问题,给予攻击者攻击的机会,这里是一个普通的模式可以更安全的操作:
1,利用一个文件名的时候进行属性检查或者状态检查.
2,当文件操作时,同样对相同文件名进行操作.

这种类型的攻击通常利用符号连接来实施,让我们看看下面存在于不安全的setuid root程序中的一段代码:

int unsafeopen(char *filename)
{
struct stat st;
int fd;

/* obtain the files status information */

if (stat(filename, &st) != 0)
return -1;

/* make sure that the file is owned by root – uid 0 */

if (st.st_uid != 0)
return -1;

fd = open(filename, O_RDWR, 0);
if (fd < 0)
return -1;

return fd;
}

上面的函数进行了如下操作:

1,检查是否文件名存在并检查是否属主为ROOT(UID 0)
2,打开文件.

由于他们是两个独立的系统调用,造成在2个特定的操作存在一个时间的延迟,在这个延迟段中,就可能造成文件和系统特性被改变,攻击者可以用下面的方法利用这个问题:

1,他可以建立一个符号连接把/tmp/filename指向一属于ROOT的文件,如/etc/passwd
2,stat()调用会遵循符号连接,并返回信息给属性为root用户(UID=0)的/etc/passwd.
3,攻击者去掉符号连接并把它指向一个属主为他自己的文件.
4,程序很自然的打开/tmp/filename(此文件这时已经指向攻击者的文件)来读操作,这个读取的数据本来应该是另一属于ROOT进程的文件数据.

这个函数的安全版本可如下所示:

int safeopen(char *filename)
{
struct stat st, st2;
int fd;

/*获得文件状态信息 */

if (lstat(filename, &st) != 0)
return -1;

/* 确保文件是一个常规文件 */

if (!S_ISREG(st.st_mode))
return -1;

/* 确保这个文件属主是root – uid 0 */

if (st.st_uid != 0)
return -1;

/* 打开文件 */

fd = open(filename, O_RDWR, 0);
if (fd < 0)
return -1;

/* 现在我们fstat() 文件,确保它还是同一文件! */

if (fstat(fd, &st2) != 0) {
close(fd);
return -1;
}

/*这里确保inode和device号码等于我们实际打开的文件,
*比较我们执行的文件是开始lstat()调用的文件.
*/

if (st.st_ino != st2.st_ino || st.st_dev != st2.st_dev) {
close(fd);
return -1;
}

return fd;
}
上面函数中使用lstat()代替stat(),假如指定文件名发生符号俩界,它返回连接的状态.然后打开文件,或者打开文件描述符的状态,比较inode 和device号状态信息,如果发现它们不是同样的,函数将异常中断.

2.5 chroot环境实现
当实现的网络服务对外提供接口时,就存在着一定危险.在开放者对所有代码进行检查确保他们代码正确或者没有明显漏洞同时,外界因素如有问题的操作系统库调用也会把未知的漏洞带给这个服务.而UNIX系统就可以限制这种因素的存在,这可以通过所谓的chroot系统调用完成.

Chroot调用定义如下:

int chroot(const char *path);

调用chroot的程序可以改变当前文件系统的访问范围,注意这仅仅是限制了文件系统访问,进程仍然可以被操作系统其他部分访问(如系统和网络调用),当使用这个调用时,所提供的path(路径)参数将被指定为文件系统新的ROOT目录,这样进程就不能再访问在这个新ROOT目录以为的文件或者目录.只有超级用户可以执行chroot系统调用.

一旦你使用了chroot系统调用,你需要变换为新的ROOT目录,使用方法如下:

if (chroot(“/jail”) < 0 || chdir("/") < 0)
perror(“Failure setting new root directory”);

在许多状况下,一旦你使用了chroot调用,它会丢弃超级用户权利.这种运作方式可以限制如果你的程序存在问题也可以有一定的限制.

如果一用户在chroot环境中是超级用户的话,他就可能通过访问一些系统的关键功能而突破chroot环境:
1,如果可以通过mknod()来创建一些重要的操作系统设备,如raw disk和内核内存设备,这样一旦建立,就有可能直接访问磁盘和内存,导致突破chroot环境.
2,如果有能力依附在么有运行在chroot环境中的其他系统进程和影响他们的操作(如插入恶意代码在里面,如有过的突破FTP环境).就可能突破受限环境.在绝大多数操作系统中ptace()系统接口存在这个功能,此函数设置用来允许对系统进程进行调试(当然也可以用在不正当用途).
3,有能力发送信号给其他进程和影响它们的操作,这即使是没有超级权利的用户也可能发生,如果用户的权利等同与想要发生给信号的进程.

有些操作系统通过一种叫securelevel的安全机制提供对上面这些情况的保护(如FREEBSD),这种机制可以让操作系统运行在更高的安全级别上.如不能访问/dev等.

对于程序来说在执行chroot后丢弃权利和以”nobody”用户来运行是比较不错的操作,不过你需要注意即使你以”nobody”用户来运行,当前进程还可能影响其他进程的操作,这里建议建立一个特殊的用户给要运行的进程,而这个进程没有用在其他任何目的,没有其他任何操作.

确保chroot环境没有任何setuid或者setgid来使进入了chroot的攻击者提高权利.

2.5 丢弃权利(Dropping Privileges)
大多数程序需要权利只能通过超级用户或者其他特殊帐户获得访问系统资源的权利,在网络服务中,经常需要分配有一定权利的TCP或者UDP口,而这个操作不是一般用户可以绑定的,象BIND程序需要ROOT权利绑定53端口来服务域名查询.

而这个特殊的权利一般只在初始化的时候的时候需要,而许多大的程序,包括几十万行复杂的代码,也存在从头到尾没有丢弃特定的权利(这经常发生在一些商业化程序中).

有时程序往往提供了这个特殊权利后就没有深入考虑,而造成不少可利用漏洞,当询问开发者为何不丢弃这危险的权利时,他们回答往往是为了一些简单的文件访问或者访问一些受限系统而已,不过他们没有想到用其他方法来解决.

任何需要特殊权利的程序设计用来在初始化时分配所有资源,然后在丢弃这些权利是非常重要的,在OPENBSD中很多这样的程序为了这个目的而重新被设计过.

要丢弃这些特殊权利,进程只需要设置它们相对应低权利的有效用户ID和组ID即可.这可以通过使用seteuid 和setegid系统调用而临时完成,而通过setuid() 和setgid()程序永久性的丢弃权利(更多信息将会在下面的setuid程序部分解释).

警告:当丢弃特殊权利时,确保你先改变进程的组ID,如果你先设置了用户ID,程序就不再以超级用户运行,因此它就没有权利在更改组ID,这意味着组ID权利没有被丢弃,如果程序有漏洞的话任何人还可以通过组ID获得超级用户的权利.

记住:检查setuid和setgid调用的返回值是很重要的,当你丢弃特殊权利和调用失败的话,特殊权利也是不会被成功丢弃,因此这时返回值没有很好检查的话,程序依旧是以超级用户的权利在运行.

2.6.1 在网络服务中需要注意的问题

在网络服务中要丢弃特殊权利,你需要在丢弃权利的时候选择一用户来运行程序,在许多请情况下,一般选择”nobody”用户,因为这个用户往往对操作系统访问的权利最小:

int drop()
{ struct passwd *pep = getpwnam("nobody");
if (!pep)
return –1;

if (setgid(pep->pw_gid) < 0)
return –1;
if (setuid(pep->pw_uid) < 0)
return –1;
return 0;
}

2.6.2 在本地setuid程序中需要注意的问题

当setuid 和setgid程序执行的时候,进程的实际用户ID(UID)和实际组(GID)和执行用户UID的UID,GID是相等的,但是有效ID(EUID),有效组ID(EGID),保留用户ID和保留组ID在执行的时候还是以文件属主相同.你可以根据你的目的来永久或临时丢弃权利.

要永久性的丢弃权利,程序需要设置euid,egid,saved uid 和saved gid为实际的UID和GID,
setuid()和setgid()调用会设置实际ID,有效ID和保留ID为同一特定值,由于实际UID和GID是那些没有特殊权利的用户,下面的例子将会设置所有3个ID为当前实际ID:

if (setgid(getgid()) < 0)
return –1;
if (setuid(getuid)) < 0)
return –1;

要临时丢弃特殊权利,而在后续的操作中还要继续获得这些权利,你可以设置有效ID为所需值.因为有效ID是用来执行系统调用和权限检查的,而保留ID还是同样的ID,允许你在后续操作重新获得特殊权利.不过你要确保保存ID的值以便后续操作使用:

struct passwd *pep = getpwnam("nobody");
uid_t saved_uid;
gid_t saved_gid;

if (!pep)
return –1;

saved_uid = geteuid();
saved_gid = getegid();
if (setegid(pep->pw_gid) < 0)
return –1;
if (seteuid(pep->pw_uid) < 0)
return –1;
if (setegid(saved_gid) < 0)
return –1;
if (seteuid(saved_uid) < 0)
return –1;

2.7 产生随机数

随机数发生器没有一个很容易的解决方案,大多数系统提供一个pseudo-radom数字发生器库调用,但需要知道这个”pseudo”是”假的,冒充的”的意思.有些操作系统提供内置随机数字发生器,一般通过设备驱动程序提供随机数据—一般通过内核驱动程序混合和杂乱系统上的各种事件和变量.我们这里将讲述各种操作系统获取随机数据的方法.

2.7.1 Linux
当前Linux系统提供/dev/random和/dev/urandom设备,这些设备是通过基于各种系统状态,收集并杂乱它们而产生随机数.

/dev/random和/dev/urandom对于产生加密KEY,和其他应用程序所需的随机数已经足够安全了,一般来说不可能猜测下一个随机号码.

两个不同之出是/dev/random会用完随机字节而后的”读取者”必须等待一段时候才可用,这样如果系统上没有足够的活动来产生额外的随机数据,就会导致需要等待比较长时间来获得新数据.

2.7.2 OpenBSD

OpenBSD内核使用鼠标间断时候,网络数据间隔反应时间,击键间隔时间和磁盘IO信息来填充信息量池,随机数由内核收集并通过设备传递给用户空间程序,而这个设备就是/dev/random.

下面的信息来自OPENBSD手册:

各种随机设备数据产生的数据取决于不同随机量,信息量数据可从系统活动中获得,并通过各种hash或信息摘要函数产生输出.
/dev/random 保留设备为以后支持硬件随机发生器.
/dev/srandom强壮的随机数据,这个设备返回可依赖的随机数据,如果数据的信息量不充足的时候,驱动程序会暂停直到更多数据被采集,平均信息量池数据使用MD5转换为输出数据.
/dev/urandom 和上面一样,但它不保证数据的强度, 平均信息量池数据使用MD5转换为输出数据.当数据不充足时,驱动程序会继续输出数据。
/dev/prandom 类似pseudo-random发生器.
/dev/arandom 作为需要,数据由ARC4发生器发生,然后产生高质量pseudo-random输出数据,arc4random(3)函数从这个设备在用户空间库提供第二级别的ARC4 hashed数据。

2.8调用子进程

一般情况下一个程序需要执行另一个程序,当在一个拥有特殊权利的程序(如setuid或网络服务中)需要这样的操作时,你务必小心应付.
1,永远不要使用下面的库调用:
system()
popen()
上面两个函数通过使用UNIX系统的SHELL解释程序/bin/sh来执行特定的程序,而这两个程序牵涉到大范围的安全问题,你必须使用execl 或者execv调用代替.

2,确保所有文件描述符关闭,以防止子进程继承父进程权利来访问重要文件.

3,确保指定程序的全部路径,防止伪造程序的木马被执行.

4,在执行之前丢弃特殊权利,而使子进程不能继承父进程权利.

5,确保传递给子进程的环境变量是净化过的,并且只传递所需要的环境变量,因为环境变量可以被多次定义,造成安全问题的发生.

2.9 资源限制

2.9.1 core文件
core文件在UNIX程序执行异常时产生,这些异常包括内存或者堆栈被破坏,或者非法内存或不合理结构被访问.当这些异常发生时,操作系统把当前执行程序的内存数据写到一磁盘文件中,通常叫’core(核心倾倒文件)’或’program.core’,一般这个’core’文件是用来分析程序信息和帮助判断程序为何出错.此文件的建立往往造成2个安全问题:

1,由于程序所有内存内容被写到文件中,有可能保存重要的,敏感的信息,如密码.

2,过去一些操作系统在建立这些core文件时没有检查此文件是否在系统中存在,这样可以导致攻击者建立符号连接,如果执行的程序是setuid/setgid root程序,就可以覆盖系统上重要的文件,造成拒绝服务攻击,或者其他更严重安全问题.

一个很好的例子是以前Solaris操作系统中的FTP服务程序,攻击者可以连接到FTP服务程序,在执行其他命令前提送PASV命令,造成FTP服务器崩溃.在这个崩溃中,FTP服务器会倾倒出内存内容到一个文件,并存放在系统ROOT目录,通过分析这个文件,攻击者可以获得加密过的HASH字段,通过暴力破解可以获得用户名和密码.

如果你的程序在存在中会包含一些安全信息,明智的做法是不允许异常情况下建立core文件.你可以使用setrlimit()函数调用来完成它:

int setrlimit(int resource, const struct rlimit *rlp);

通过设置resource类型为RLIMIT_CORE,你可以设置被建立core文件大小,如果你定义大小为0,就防止了core文件被建立.

int nocore()
{
struct rlimit rlp;

rlp->rlim_cur = 0;
rlp->rlim_max = 0;

return(setrlimit(RLIMIT_CORE, &rlp));
}

3.0 WINDOWS操作系统
此部分讨论的安全编程技术牵涉到WINDOWS操作系统,我们集中讨论WINDOWS NT系统,不过很多内容同样适用与其他版本WINDOWS系统,如WIN95和WIN98,也适合于WINDOW 2000系统.

3.1 访问控制列表 (Access Control Lists)
WINDOWS NT的安全多数通过基于访问控制列表(ACL),ACL可以在许多不同类型对象上指派,如:

1,NTFS文件系统上的本地或者远程文件和目录.
2,有名管道.
3,本地或者远程打印机.
4,本地或者远程WIN32服务.
5,网络共享.
6,注册表键值.
7,信号,事件, 互斥通信(mutex)和时间.
8,进程,线程,文件映射对象.
9,WINDWO工作站和桌面
10,目录服务对象

ACL定义对象的属主是谁,谁可以访问对象,在多数情况下,当建立一新对象时,ACL指派给对象的权利不一定安全.一般有4种不同类型的安全信息应用在对象上,当对一对象设置或者获取安全信息时,你必须非常熟悉它们:

OWNER_SECURITY_INFORMATION

这选项指定对象的属主,在多数情况下,默认设置为建立此对象用户.

GROUP_SECURITY_INFORMATION

这选项说明了应用于对象上的组权限,定义那些组可以访问对象.

DACL_SECURITY_INFORMATION

此选项设置”任意”访问控制列表在指定的注册表键上.

SACL_SECURITY_INFORMATION

此选项设置”系统”访问控制列表在指定注册表键上.

WINDOWS NT提供SetSecurityInfo()函数用来指派安全描述符给上面任意一个对象,这函数描述如下:

DWORD SetSecurityInfo(
HANDLE handle, // 对象句柄
SE_OBJECT_TYPE ObjectType, // 对象类型
SECURITY_INFORMATION SecurityInfo, // 要设置的安全信息类型
PSID psidOwner, // 新属主SID指针
PSID psidGroup, // 新组SID指针
PACL pDacl, //新DACL指针
PACL pSacl // 新 SACL指针
);

SetSecurityInfo()之设置ACL(属主,组,DACL,SACL)的其中之一,而不是全部,你必须在SecurityInfo变量中指定要设置的ACL,你也必须在ObjectType里指定对象类型的句柄(注册表键值,文件等).

另外这个函数有多个特殊对象函数可以用来指派安全描述符给各自的对象类型,注册表,有自己的设置函数用来对注册表键值设置ACL,本质上用途和SetSecurityInfo()函数相同.

其中一个最大问题就是当多数对象建立时候,它们的ACL往往允许”Everyone”来访问它们的对象,这是应用程序开发者必须特别小心的.

3.2 注册表键值

WINDOW使用注册表存储重要的配置信息来操作你的程序,而多数许多重要的安全信息保存在注册表中.更要命的是,对注册表键的保护往往不够充分而导致攻击者可以读或修改注册表值.不过存在一些特殊函数可以管理注册键的ACL. WINDOWS提供下面2个函数来支持查看和设置注册表键值权限.

LONG RegGetKeySecurity(
HKEY hKey, // 要设置的键句柄
SECURITY_INFORMATION SecurityInformation, // 描述符内容
PSECURITY_DESCRIPTOR pSecurityDescriptor, //键描述符地址
LPDWORD lpcbSecurityDescriptor //描述符缓冲大小
);

LONG RegSetKeySecurity(
HKEY hKey, // 要设置的键句柄
SECURITY_INFORMATION SecurityInformation, // 描述符内容
PSECURITY_DESCRIPTOR pSecurityDescriptor // 键描述符地址
);

3.3 文件
确保使用ACL充分保护你程序的文件,下面是两个重要的应用情况:

1,应用程序需使用于所有用户
n 由于所有用户要有权利访问它们,所有不能限制应用程序和目录的访问.
n 限制所有应用程序产生的文件,因为它们是每个用户独立产生的,确保只有产生文件的用户可访问.
2,应用程序只用于管理员
--- 限制所有其他用户不能访问应用程序和它的目录,可以在安装过程中指派.
n 限制所有应用程序产生的文件,因为它们是每个用户独立产生的,确保只有产生文件的用户可访问.

下面是这两个函数的描述:

BOOL GetFileSecurity(
LPCTSTR lpFileName, //文件名字符串地址
SECURITY_INFORMATION RequestedInformation, // 请求信息 PSECURITY_DESCRIPTOR pSecurityDescriptor, // 安全描述符地址
DWORD nLength, // 安全描述符缓冲地址
LPDWORD lpnLengthNeeded // 缓冲地址大小
);

BOOL SetFileSecurity(
LPCTSTR lpFileName, // 文件名字符串地址 SECURITY_INFORMATION SecurityInformation, // 要设置的信息类型
PSECURITY_DESCRIPTOR pSecurityDescriptor // 安全描述符地址);
};

3.4 产生随机数
WINDOWS NT 提供CryptoAPI库用来获得随机数, CryptoAPI提供CryptGenRandom()函数可以指定所需随机数所需字节数:
BOOL WINAPI CryptGenRandom(
HCRYPTPROV hProv,
DWORD dwLen,
BYTE pbBuffer,
);
下面是Mcirosoft MSDN(Copyright 2000)的引用:

“通过cryptographically random这个功能产生数据,比一般随机数发生器如C编译器中自带的函数更有随机性.

这个函数用来产生随机初始化向量和salt值.

所有软件随机发生器工作方式基本相同,它们采用 叫seed的”种子”作为随机数,然后使用算法来产生pseudo-random位序列,其中处理困难的部分是获得的seed是否真正随机.这基于用户输入反应时间或者一个或者多个硬件成分.

如果应用车工女婿要访问一个好的随机资源,它可以在调用CryptGenRandom使用随机数据填充pbBuffer缓冲,CSP然后使用这个数据更进一步随机化它的内在seed.

4.0 设计协议
网络协议定义了网络设备之间的通信格式,当设计网络协议的时,有许多因此牵涉到安全问题,下面是考虑设计网络协议时需要注意的问题:

1,攻击者可能被动性的监视网络,检查网络上的所有数据.
2,攻击者可能截取通信,修改数据,再重发修改的通信数据.
3,攻击者可能从另一主机上伪造信息包.
4,攻击者可能发送不遵循协议规格的信息包.

上面几条是设置网络协议需要注意的问题,通过对数据加密,认证和合法数据的组合,多数问题可以被避免.

4.1 认证
认证在网络协议中有轻重之分,如下:
1,通过登陆帐户认证
这种认证通常在会话开始阶段产生,如POP, IMAP, NNTP, telnet, ftp 服务都是通 过连接后对输入的用户名和密码进行检查操作.多数情况下,用户名和密码信息在网络上是通过明文发送,往往可以被嗅探到.

2,加密认证
这种认证方法通常利用公开KEY加密法来认证用户或者客户,如果双方都被配置有公共KEY也可以用secret-key算法.

4.2机密性
如果你的协议设计用来携带敏感信息那保护协议数据被监视就尤为重要,SSL协议就是在WWW上用来加密电子商务事务的安全协议.

5.0 工具
手工来查看数千万行的代码是枯燥而又缺乏安全的工作,现在有不少工具可以用来帮助发现WINDOWS和UNIX下源代码的安全问题.它们通常通过分析能提供数据来指出有潜在问题的调用.

除了帮助发现源代码安全问题的工具,现在也有不少工具可以防止有漏洞程序的执行,这些工具有不少种类,包括提供在编译时增加内容防止缓冲溢出,和在实时库调用时截获危险的库调用.

5.1 UNIX工具

ITS4

http://www.rstcorp.com/its4
“Software Security Group设计了设计,分析和测试有安全问题的软件,我们开发的ITS4自动帮助检查源代码问题,ITS4是一个静态扫描C和C++源代码中存在的安全漏洞的简单工具,它以命令行方式工作在UNIX环境中,如果你在WINDOWS下安装了CygWin,也可以在工作在WINDOWS下.

StackGuard from Immunix

http://www.immunix.org

“StackGuard是编译处理方式来防护那些存在堆栈破坏的程序和系统攻击,堆栈破坏是最普通的安全漏洞攻击方式,使用StackGuard环境下编译的程序可以不需要源代码改变的情况下对堆栈破坏攻击方式免疫.当一个程序中的漏洞被利用时, StackGuard可以检测到进程中的攻击,阻止程序的执行.

Libsafe from Bell Labs

http://www.bell-labs.com/org/11356/libsafe.html
“缓冲溢出攻击在近年是一种很流行的攻击方法,我们提供新的方法来探测和处理这些攻击.比较以前的方法,我们的方法不需要操作系统的任何修改并对所有已经存在的两进制程序适用.我们的方法不需要访问有缺陷程序的源代码,不需要重新编译或者停止程序进程.而且它可以在系统广泛的透明的实现.我们的解决方法基于在软件层中部截获有可能存在漏洞的库函数的系统调用.使用相应函数的替换版本来实现原来的函数功能,在某种意义上来说,我们假定在当前栈帧中包含着缓冲溢出,而通过安全函数的替代,阻止了对堆栈中返回地址的破坏.

5.2 WINDOW工具
下面所列的工具使用在WINDOWS系统下保护软件.需要注意的是这些工具的专业性非常难于确定,所以你在使用它们的时候必须足够小心.
BOWall

http://developer.nizhny.ru/bo/eng/BOWall
“BOWall实现对WINNT4.0下的程序提供缓冲溢出攻击包括,其中提供了两种方法:

1,监视有漏洞的函数,有潜在漏洞的DLL将被更新,并替代如strcpy, wstrcpy, strncpy, wstrncpy, strcat, wcscat, strncat, wstrncat, memcpy, memmove, sprintf, swprintf, scanf, wscanf., gets, getws, fgets, fgetws函数,更新内容还包括代码完整性检查,如检查栈基指针本地变量.
2,防止动态库函数在数据段和堆栈段中执行,这种方法通过额外代码对函数调用中的地址进行检查,如果调用属于数据或者堆栈段,程序将被停止执行.

6.0 参考和资源

[1] Smashing the Stack for Fun and Profit By Aleph One

http://www.securityfocus.com/data/library/P49-14.txt
[2] Fuzz Revisited: A Re-examination of the Reliablity of UNIX Utilities and Services

ftp://grilled.cs.wisc.edu/technical_papers/fuzz-revisited.ps.Z
[3] The Unix Secure Programming FAQ, Tips on security design principles, programming methods, and testing By Peter Galvin

http://www.sunworld.com/sunworldonline/swol-08-1998/swol-08-security.html
[4] Communications with Theo de Raadt ([email protected])

[5] Thomas Ptacek Bugtraq message regarding FreeBSD C runtime library overflow


感谢http://www.securityfocus.com,此文章原文来自securityfocus的邮件列表.