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