编 写 安 全 的 软 件
一、概述
所谓安全的软件是指程序代码在设计与实现的时候能够经受得住恶意的攻击。为什么安全的软件如此重要?因为随着互联网的发展,大部分的电脑都连接到互联网上,这样就很容易受到别人从远程发起的攻击。很多黑客从道德上来说,并不认为自己这样的攻击行为有什么过错,他们可能反而会认为你的系统不安全是你自己的问题。因此,作为程序的开发人员就应该时时刻刻考虑到被攻击的威胁,在保护用户的数据以及隐私方面做出更多努力。怎样编写安全的软件呢?首先在软件架构设计时,就要考虑安全性问题;其次在代码实现中也应注意安全问题;最后测试也是一个非常重要的阶段。安全测试并非功能测试,它是以黑客的角度来测试软件,模仿黑客的攻击手段,这样可以发现软件中存在的很多安全性问题,有利于在开发的整个流程中加入安全性措施,使得整个流程具有防范威胁的机能。此外,加强程序员对于安全软件开发技术的训练,提高安全意识也非常重要。安全技术现在是一个很热门的话题,安全技术方面的人才也非常缺乏,掌握安全方面的技术对于寻找合适工作也是很有好处的。
二、安全开发的过程
攻击者的优势与防御者的困境主要表现在:
1、防御者必须防守所有的点,而攻击者则可以选择防守最薄弱的地方作为切入点。
2、防御者必须时刻警惕,不可懈怠;而攻击者可以随意进行攻击。
3、防御者仅能防守自己已知的东西,而攻击者可以研究系统最薄弱的部分。
因此,软件开发人员想要开发安全的软件就需要做好以下几件事情:
1、设计时考虑安全
设计时要有一个流程,这一流程要孕育着安全的系统,即整个流程都要保证系统的安全。要建立一个威胁模型,知道所受的威胁来自何方,并让程序以最低的权限来运行。
2、默认状态的设置安全
除常用的基本功能外,其余功能默认时应该尽量关闭,以减少可能被攻击的表面。另外也可以使用Visual Studio .NET中一些安全功能,比如编译时的/GS开关。
3、部署安装要安全
要遵循部署时所规定的安全措施,创建有关安全部署的指导文档,以及要使用一些工具来评估系统的安全性。
安全的软件开发流程图如图1所示。
三、安全设计的原则
1、减少受攻击的表面
可采取的措施有深层防守,即要有多层的安全防守;用户最小的使用权限;默认设置必须安全。
2、从以往的错误中学习
如果以往的版本中有错误并因此而受到攻击的话,就应该针对此种错误容易引起的某种攻击,那么在下一版本中就要进行相应的改进。
3、安全本身也是一种功能
在做开发计划时,还应该考虑安全方面所需花费的时间问题。
在此重点对减少受攻击的表面这一原则作简要介绍:
程序中越少的代码在默认的时候运行,那么就会大大的减少被攻击的机会。其好处还在于对系统管理员的技巧要求不高,用户只要装上就能很好的运行了,不需要很多的配置管理知识。另外,由于可被攻击的范围缩小,这样也就大大缓减了修复安全漏洞的紧迫性。
下面是一张低攻击表面与高攻击表面所采用技术的对照表:
Lower Attack Surface
Higher Attack Surface
TCP
UDP
Local-only
Remote
Authenticated
Unauthenticated
Managed Mobile Code
Native Mobile Code
Managed Code
ActiveX
Low privilege
SYSTEM
Turned off by default
Running by default
Essential features enabled
All features enabled
四、威胁的建模
通常有一个误区,那就是认为只要在软件中增加一些安全的功能,这一软件就是安全的,其实不然。增加软件安全的功能并不意味着所写的软件就是安全的,如程序中增加了加密功能,但是此段加密功能的代码又刚好存在着缓冲区溢出问题,那么攻击者照样可以进行攻击。安全功能不等于软件的安全,安全功能只是为软件安全服务的一种功能,安全功能的代码写得不好也会影响软件的安全性。为此,有必要对软件中可能存在的威胁进行建模。因为如果不明白威胁在哪,那么就很难编写出一个很安全的软件。知道了威胁在哪,就可以在软件的设计、开发以及测试等各阶段有意识的去防范这些隐患。
威胁建模的过程如下:
1、建一个数据流图或UML图,并把需要保护的信息列成一个清单。
2、把可能存在的威胁进行分类,而STRIDE通常被用于对威胁的效果进行分类。STRIDE分别是由Spoofing, Tampering, Repudiation, Info Disclosure, Denial of Service, Elevation of Privilege的首字母组成。Spoofing是指攻击者伪装成别的用户或是别的被本系统认可的系统,如张三装成李四访问系统,偷换了其自身的标识符。Tampering是指攻击者篡改了系统中的数据以达到其不轨的企图。Repudiation是指攻击者闯入系统进行了某些破坏,但是系统管理员却无法证实此件事是否发生。Info Disclosure是指信息被泄露给本无权限知道这些信息的人。Denial of Service指攻击者使系统的合法用户无法正常使用系统功能,使得系统不能及时的对合法用户的服务请求进行响应,不能服务真正应该得到服务的人。Elevation of Privilege指攻击者通过一些非法的手段,使得自身使用系统的权限被提升。
3、依据可能受到攻击的概率以及由此所造成的破坏程度来对威胁进行分级。通常对威胁进行分级的依据称为DREAD,DREAD分别是由Damage Potential、Reproducibility、Exploitability、Affected Users、Discoverability的首字母组成。Damage potential是指系统是否容易受到破坏。Reproducibility是指攻击的可重复性,即第一次这样攻击,第二次是否还可以同样的方式进行攻击。Exploitability指可利用性,即该威胁可被利用的程度。Affected Users指该威胁可影响多大范围的用户。Discoverability是指可发现性,即漏洞是否会被发现,通常只要有漏洞就一定会被发现。
有了以上所述的威胁模型作参考,写程序时就可以知道程序中最“危险”的部分是哪里,这样也就可以在做security push时进行重点的检查,而且还有助于在编码时就确定防御的机制。此外,在做软件测试时也就可以从最易受到攻击的地方入手进行测试了。
五、输入信任的问题
所有的安全漏洞可被分成两大类,Input trust issues(太信任用户的输入)和Everything else。下面将简要介绍Input trust issues方面的内容。
请记住一句话,“All input is evil, until proven otherwise!”,意思是说所有的输入都是非常坏的,直到你能证明它不坏为止。常见的利用输入进行攻击的方式有缓冲区溢出攻击、SQL Injection攻击、Cross-Site Scripting(利用跨站脚本攻击)。
1、缓冲区溢出攻击
缓冲区溢出攻击是根据程序在执行函数调用时,函数返回地址与函数的局部缓冲区都位于系统栈中这一原理,利用代码中对局部变量边界条件疏于检查的漏洞,窜改函数的返回地址,引起程序异常或改变程序执行流程,进而获得程序控制权的攻击方法。
在图2中,大家可以看到函数在栈中的具体分布情况。
(图2)
输入溢出时在栈中的情况如图3所示。
(图3)
输入溢出时在堆中的情况如图4所示。
(图4)
下面通过一个具体的例子,来了解缓冲区溢出的基本原理。程序代码如下:
void CopyStuff(string data) {
char buffer[128];
strcpy(buffer,data);
// do other stuff
}
系统栈在调用CopyStuff()函数前记录了参数data的值,然后将函数CopyStuff()调用结束后的返回地址压栈,在保存一些有用的寄存器后,将CopyStuff()函数中为局部变量分配的128个字节压栈。
这样在调用函数时,如果传入的字符串长度是8个字节,那么此次的调用不会引起任何异常,并且通过参数data所传入的字符串被复制到了缓冲区buffer中。但是,如果调用CopyStuff()函数的是一个不怀好意的人,他完全可以传入一个超出128个字节长的字符串。这时,CopyStuff()函数在调用strcpy()前,并不会对data的长度做任何检查。这样,通过参数data所传入的字符串会被strcpy()函数复制到从buffer开始的系统栈中,而超出128个字节部分会沿着系统栈继续向高位地址的方向延伸。如果该字符串足够长的话,字符串内容就会覆盖栈中最为关键的信息——返回地址。CopyStuff()函数执行完毕后,就再也找不到正确的返回地址了。
缓冲区溢出意味着灾难的降临。但具体是何种灾难,还要看调用者传入的字符串内容而定。如果比较幸运,覆盖返回地址的数据只是些不能访问的非法地址,程序就会在函数返回时异常终止,并报“非法访问”的错误。如果不太幸运,覆盖返回地址的数据指向了某个合法的代码地址,程序就会产生难以预期的运行结果。可能的情况就是,程序总处于不稳定的状态,但又很难确定究竟是哪里出了毛病。如果非常不走运,黑客利用了缓冲区溢出将一段攻击代码放入系统栈中,并将函数的返回地址改为指向攻击代码的地址。那么在函数返回时,系统就会自动执行黑客的攻击代码,黑客就可以获取系统的管理员权限,进而删除硬盘数据、随心所欲地控制计算机了。
让我们来看一下针对刚才的代码所可能会发生的黑客攻击情况。有经验的黑客传入CopyStuff()函数的字符串是一段用机器指令编写的攻击代码,该字符串的长度超过buffer的长度。在刚好覆盖返回地址的位置,黑客填写的数据正是攻击代码的起始地址,这时系统栈被完全篡改了。CopyStuff()函数一执行完毕,系统就会自动跳转到攻击代码的起始位置,开始执行黑客事先编排好的攻击代码。这样,系统就丧失了抵抗能力,黑客可以为所欲为了。
了解了缓冲区溢出问题的原理之后,可以知道解决缓冲区溢出问题的方法其实并不复杂。只要我们在编程时注意检查参数的合法性,不要随意使用未知长度的字符串,并避免使用类似strcpy()、strcat()、sprintf()这样不安全的库函数,就可以有效地防范缓冲区溢出攻击了。
此外,微软新一代的应用开发环境Visual Studio.NET也为应用软件开发提供了更好的安全性支持。拿缓冲区溢出来说,Visual Studio.NET就提供了两种解决方案。
首先,使用Visual C++.NET编译C++语言程序时,可以打开“/GS”缓冲区安全检查选项。“/GS”选项的作用是:对于容易发生缓冲区溢出的函数,编译器将修改函数的可执行代码。在进入函数时,把返回地址和一个模块加载时随机生成的安全Cookie进行异或运算,并保存运算结果;在退出函数时,通过保存的运算结果和安全Cookie检验返回地址的正确性。如果发现返回地址已被改写,则表明已经发生了缓冲区溢出,这时系统将报告错误,并终止程序的执行。
其次,可以使用.NET提供的托管代码来编写应用程序。.NET公共语言运行库为托管代码提供了强大的安全检查和安全保障机制,使用托管扩展编写的C++语言程序具备数组边界检查和自动垃圾回收等安全特性,可以有效解决缓冲区溢出问题。
2、SQL Injection攻击
SQL 嵌入攻击是一种常见的Web攻击方式,其产生的根源是程序员在使用服务端代码访问数据库时,没有对用户输入信息的合法性进行检查。下面是一段用C#语言编写的代码,我们来看一下针对此段代码SQL 嵌入攻击是如何进行的。
string Status = "No";
string sqlstring ="";
try {
SqlConnection sql= new SqlConnection(
@"data source=localhost;" +
"user id=sa;password=password;");
sql.Open();
sqlstring="SELECT *" +
" FROM OrderDetail WHERE ID='" + Id + "'";
SqlCommand cmd = new SqlCommand(sqlstring,sql);
if ((int)cmd.ExecuteScalar() != 0)
Status = "Yes";
} catch (Exception e) {
Status = e.ToString();
}
Good Guy:
ID: 518
SELECT *
FROM OrderDetail
WHERE ID=‘518'
Not so Good Guy:
ID: 518' or 1=1 --
SELECT *
FROM OrderDetail
WHERE ID= ‘518' or 1=1 -- '
Really Bad Guy:
ID: 518' drop table orders --
SELECT *
FROM OrderDetail
WHERE ID= ‘518' drop table orders -- '
Downright Evil Guy:
ID: 518' exec xp_cmdshell(‘fdisk.exe’) --
SELECT *
FROM OrderDetail
WHERE ID= ‘518' exec xp_cmdshell('fdisk.exe') -- '
其实,防范SQL嵌入攻击最重要的一点就是牢记不要相信用户输入的数据内容,对所有的输入都要进行验证。另外,可以使用parameterized query来预防SQL嵌入攻击的发生,对于上述代码的改进方法如下:
SqlDataAdapter myCommand =
new SqlDataAdapter("SELECT * FROM OrderDetail WHERE ID = @id", conn);
SqlParameter parm =
myCommand.SelectCommand.Parameters.Add("@id",SqlDbType.VarChar, 11);
parm.Value = Id.Text;
3、Cross Site Scripting
这是在网上最常见的一个漏洞,是由于服务器端做得不好,而导致客户端出现安全问题。其最根本的原因还是没有对用户的输入进行有效检查。
六、总结
开发安全的软件、建设安全的系统必须遵循的原则包括:
1、建立面向安全的过程管理机制;
2、制定产品的安全目标;
3、将安全视作产品的一个重要特性;
4、从错误中吸取经验教训;
5、只授予用户必要的使用权限;
6、假设外界环境是不安全的;
7、为失败做好准备;
8、使用安全的默认设置;
9、安全功能不等于软件的安全;
10、不要把系统安全建立在攻击者对系统不了解的假设之上。