代码安全性审查方法研究

2018-11-15 09:08贺江敏相里朋
信息安全研究 2018年11期
关键词:缓冲区静态代码

贺江敏 相里朋

(工业和信息化部电子第五研究所 广州 510610)

我们知道,软件从诞生的一刻起就伴随着各种各样的缺陷,这是因为软件都是由人开发的,只要是有人参与的活动就不可避免会引入缺陷,而原因也是五花八门,如软件需求不明确、开发人员技术水平不一致、团队协作缺乏默契等.软件中的缺陷可能会导致严重的后果,如系统崩溃、财产损失甚至是人员伤亡.于是,人们又研究出了软件测试方法来发现软件中存在的缺陷.

软件测试从测试类型上分为功能测试、性能测试、可靠性测试、接口测试、界面测试等;从开发过程来看,可分为单元测试、集成测试、系统测试等;从是否执行代码来看,可分为静态测试和动态测试;从软件的内部结构又可分为白盒测试、黑盒测试和灰盒测试.在实践中,人们根据测试的目的选择不同的测试类型及测试方法.对于一些非常重要的软件或者软件中存在的缺陷可能带来十分严重的危害时,为了更加彻底地发现软件中存在的缺陷,往往需要对软件的源代码进行分析,一般采用的是白盒的静态测试方法,代码审查就是其中一种常见的方法.

1 代码审查

代码审查以软件源代码为输入,是采用人工的方式,对其中可能存在的缺陷、违反开发标准及其他可能存在的问题进行审查,一般采用的方法有代码检查单、代码走查、软件开发规范一致性检查、工具静态分析等[1].代码审查的目的是检查代码和设计的一致性、代码执行标准的情况、代码逻辑表达的正确性、代码结构的合理性以及代码的可读性[2].

代码审查依据其目的可以分为代码质量审查和代码安全性审查.代码质量审查主要对代码的质量进行度量,包括编程规则的违反情况、代码的圈复杂度、节点数、注释行、运行时错误等.代码质量审查主要用于对实时性、可用性要求较高的软件或模块,这类软件失效时可能造成严重的经济损失或人员伤害,多用于嵌入式软件;代码安全性审查主要对代码中存在的安全漏洞进行分析,如SQL注入、跨站脚本、缓冲区溢出等.软件中的安全漏洞一旦被非法利用可能导致敏感信息泄露、数据篡改、拒绝服务攻击等,多针对信息安全要求高的软件或系统,以信息系统居多.

代码审查依据审查方法又可分为人工走查及工具静态分析.人工走查就是采用人工的方式对源代码逐行进行审查,人工走查可以最大限度地发现软件中存在的问题,但是费时费力,同时又和代码审查人员的技术水平相关.若软件的代码量十分巨大,对所有代码进行人工走查往往是不现实的,这时往往对系统中十分重要的核心模块进行人工走查.人工走查主要采用检查单的形式,就是对照一份错误列表来检查代码是否存在常见错误[3].代码质量审查和代码安全性审查由于其目的不同,审查的重点往往不一样,表1以对比的方式列出了部分审查内容:

表1 代码走查检查单

工具静态分析就是采用专门的代码审查工具对软件源代码进行扫描,并对扫描的结果进行人工分析.静态分析的目的是通过对源程序分析、目测,但不执行程序,找出源代码中可能的错误和缺陷[4].工具静态分析可以代替人工发现代码中存在的通用性缺陷,并且速度快、效率高,但是针对特定领域相关的逻辑缺陷是无法发现的,比如嵌入式软件对寄存器的赋值错误、软件运行状态错误等.因此即使采用工具对源代码进行分析具有如此多的优点,但也不能完全取代代码人工走查.在实际的工程中,往往是这2种测试方法相结合,以达到更好的效果.目前市面上的代码静态分析工具也主要分成2类:针对代码质量的静态分析工具和针对代码安全性的静态分析工具.针对代码质量的静态分析工具如testbed,logiscope,C++test等,其主要功能有复杂度分析、静态数据流分析、交叉索引分析、信息流和数据对象分析、运行时错误检测等.针对代码安全性的静态分析工具如Fortify SCA,Checkmarx CxSuite,Armorize CodeSecure等.有的静态分析工具2种缺陷都可以检测,如Klocwork在对代码质量进行分析的同时也可以检测一些诸如注入、缓冲区溢出、拒绝服务等常见的安全问题.每种代码静态分析工具都有自己的特点,有的测试类型比较广泛,有的针对某个领域做得比较完善,在实际中,可以根据测试的内容、关心的方向等有目的的选择.

2 代码安全性审查

随着网络上每年爆出大量的安全事件,如软件漏洞、蠕虫病毒、木马、黑客攻击、用户信息泄露等,人们开始越来越重视软件的安全性.在《GBT 16260.1—2006 软件工程 产品质量 第1部分:质量模型》中,软件的安全保密性是属于软件6性(功能性、可靠性、易用性、效率、维护性、可移植性)中功能性的子特性,而在《GBT 25000.10—2016 系统与软件工程 系统与软件质量要求和评价(SQuaRE) 第10部分:系统与软件质量模型》中,软件的产品质量特性被划分为8个特性:功能性、性能效率、兼容性、易用性、可靠性、信息安全性、维护性和可移植性,信息安全性已被单独提出成为软件质量特性之一,并划分为保密性、完整性、抗抵赖性、可核查性、真实性、信息安全的依从性6个子特性.可见软件的安全性测试已成为软件测试中非常重要的部分.对于软件的安全性可以通过黑盒的测试方法,如安全功能测试、渗透测试等,也可以通过白盒的测试方法如代码安全性审查进行评估.前面已经对代码安全性审查的概念作了描述,下面分别从人工走查和工具静态分析量2方面对代码安全性审查的方法进行研究.

2.1 代码安全性人工走查

本文主要针对目前常见的软件安全漏洞进行分析.

2.1.1SQL注入

SQL注入(SQL injection)是一种代码注入(code injection)攻击,其根源是用户输入等不可信数据未经充分净化、过滤就被数据库引擎当作SQL代码片段执行[5],这样攻击者就可以注入任何SQL语句实现数据库的查询、修改,甚至是通过存储过程或调用外部命令实现对操作系统的操作.以下就是一个存在SQL注入漏洞代码的例子:

public class test{

public ResultSet getArticleData(ServletRequest req, Connection con) throws SQLException {

String id=request.getParameter(″id″);

String query=″SELECT * FROM articles WHERE id=′″+id+″′″;

Statement stmt=con.createStatement();

ResultSet rs=stmt.execute(query);

return rs;

}

}

这段代码的功能为:服务器接收客户端浏览器通过post或get方法传递过来的id参数,以id参数为输入,形成查询字符串,查找数据库articles表中以id为指定值的文章相关内容并返回.可以看出,代码中的查询字符串是由一个基本的查询语句和用户输入的字符串采用拼接字符串的方式组成的,如果攻击者为id输入字符串“100′ OR ′a′=′a”那么构建的查询语句就变成:

SELECT * FROM articles WHERE id=′100′ OR ′a′=′a′;

由于OR ′a′=′a′是恒成立的,于是构建的查询语句就等效为

SELECT * FROM articles;

这时返回的是articles表中的所有条目,而不是指定id的条目,当然也可以通过构建的SQL语句来执行更加复杂的操作,比如在支持采用分割符一次性执行多条SQL语句的数据库中,若攻击者为id输入字符串“100′; DELETE FROM articles; --”那么构建的查询语句就变成:

SELECT * FROM articles WHERE id=′100′;

DELETE FROM articles;

查询语句变为2个,在执行完第1个查询语句后会执行第2个查询语句,直接删除articles表.

具有安全意识的程序员会采用参数化的SQL指令来进行SQL查询,通过占位符进行参数捆绑,以便区分哪些是命令的一部分哪些只是输入的数据,捆绑的参数只会当作输入的数据,即使里面带有SQL指令也不会执行.因此,参数化SQL可以防止篡改上下文,有效避免SQL注入攻击.在Java语言中,采用PreparedStatement进行预编译,提供占位符实现参数化功能,如下所示:

public class test {

public ResultSet getArticleData(ServletRequest req, Connection con) throws SQLException {

String id=request.getParameter(″id″);

String query=″SELECT * FROM articles WHERE id=?″;

PreparedStatement stmt=

con.prepareStatement (query);

stmt.setString(1, id);

ResultSet rs=stmt.execute();

return rs;

}

}

当以上这种方式进行数据库查询时便不会产生SQL注入的问题.

2.1.2缓冲区溢出

缓冲区溢出是一种十分危险的漏洞,这是由于向内存区块中填写的数据超过了区块本身的大小,导致数据覆盖了指定区域之外的内存区域,经过精心构建的填充数据可以覆盖并重写函数的返回地址,当函数返回时直接跳转到攻击者指定的缓冲区,并执行攻击者想要执行的任意代码.即使是任意填充的随机数据也会使函数返回到未知的地址,最终导致程序的崩溃,造成拒绝服务攻击.缓冲区溢出漏洞常出现在采用C语言编写的代码中,经常与危险的字符串函数的使用相关,标准C库中有很多不进行自变量检查的字符串操作函数,在使用这类函数时一定要对操作字符串的数目进行限制[6],如gets(),scanf(),strcpy(),sprintf().内存分配函数malloc()的使用也要十分谨慎,若没有对分配的内存大小进行判断,很可能会引起缓冲区溢出漏洞,在进行代码人工走查时,以上都是需要重点关注的地方.下面是一个缓冲区溢出的例子:

void test()

{

char a[10]=″Hello Tom″;

char b[20]=″This is a example″;

strcpy(a,b);

}

这段代码中,字符数组变量a的长度为10,字符数组变量b的长度为20,通过函数strcpy将b的内容覆盖变量a,由于变量b中的内容长度大于变量a的长度,当变量a的10个字节覆盖完成后会继续覆盖其分配的内存空间之外的地址,这就形成了缓冲区溢出.

针对缓冲区溢出漏洞有以下2种解决办法:

1) 人工通过代码对上限进行判断,如下所示:

void test()

{

char a[10]=″Hello Tom″;

char b[20]=″This is a example″;

if (strlen(b) > (sizeof(a)-1)){

print(″error ″);

return;

}

strcpy(a,b);

}

2) 将无界字符串操作函数strcpy(dest,src),替换成对应的有界字符串操作函数strncpy(dest,src,n),strncpy将src中的内容复制到dest,复制的大小由n决定,如下所示:

void test()

{

char a[10]=″Hello Tom″;

char b[20]=″This is a example″;

strncpy(a,b,sizeof(a)-1);

}

这样就可以避免缓冲区溢出的问题.

2.1.3资源未释放

资源未释放一般分为2种情况:一种是文件流资源未释放;另一种是数据库连接资源未释放.文件流资源未释放就是当打开一个文件流,对文件进行读写操作后却忘记了释放这个文件流.数据库连接资源未释放就是建立了一个数据库连接,对数据库进行增、删、改、查操作后忘记释放数据库连接.资源未释放会被恶意攻击者利用,大批量并发的资源打开操作而又不释放资源,很容易导致资源的耗尽从而引发拒绝服务攻击.一个典型的文件流资源未释放的例子如下:

private void test(String fileName) throws IOException {

try

{

Int len=0;

FileInputStream fis=new FileInputStream(fileName);

byte[] Array=new byte[SIZE];

while((len=fis.read(Array))!=-1){

System.out.println(new String(Array,0,len));

}

}

catch (IOException e)

{

e.printStackTrace();

}

}

该代码建立一个文件流,读取文件中的内容并打印出来,最后却没有关闭该文件流.虽然Java有垃圾回收机制,但是垃圾收集器需要确认对象是否符合回收条件,而且什么时候回收是由系统自动判断的,这就不能保证资源的释放,导致内存使用过大.

有的程序员会在try模块中释放资源,如下所示:

private void test(String fileName) throws IOException {

try

{

Int len=0;

FileInputStream fis=new FileInputStream(fileName);

byte[] Array=new byte[SIZE];

while((len=fis.read(Array))!=-1){

System.out.println(new String(Array,0,len));

}

if(fis!=null)

{

fis.close();

}

}

catch (IOException e)

{

e.printStackTrace();

}

}

这在大多数情况下没有什么问题,但是一旦程序发生异常则会跳过后续代码的执行,资源便得不到释放.正确的做法是在finally模块中释放资源,这样,即使发生了异常也能保证资源可以得到释放,如下所示:

private void test(String fileName) throws IOException {

try

{

Int len=0;

FileInputStream fis=new FileInputStream(fileName);

byte[] Array=new byte[SIZE];

while((len=fis.read(Array))!=-1){

System.out.println(new String(Array,0,len));

}

}

catch (IOException e);

{

e.printStackTrace();

}

finally

{

if(fis!=null)

{

fis.close();

}

}

}

2.1.4路径操纵

当对文件进行操作时,若文件路径由用户可以操作的变量组成,而程序又没有对用户提交的参数进行过滤就会导致路径操纵漏洞,通过路径操纵漏洞,恶意用户可能访问操作系统中任意文件[7].

String fileName=request.getParameter(″fileName″);

test(fileName);

public void test(String filename){

try

{

amt=fis.read(Array);

out.println(Array);

}

catch(…){

}

}

因此需要限制用户输入的字符,只允许输入规定的字符,如下所示:

String fileName=request.getParameter(″fileName″);

test(fileName);

public void test(String filename){

try

{

String regex=″∧[A-Za-z0-9]+.[a-z]+$″;

if(filename.matches(regex)){

red=fis.read(Array);

out.println(Array);

}

}

catch(…){

}

}

这样若用户提交的文件名和正则表达式不匹配则不能进行文件读取的操作,防止了用户读取操作系统上的任意文件.

2.1.5不安全的密码算法

一些密码算法在其诞生之初是安全的,但随着技术的发展现在已经不再安全,比如DES算法,其密钥长度只有56 b,在云计算、并行计算及计算机运算速度发展的今天,破解其密钥只需要很短的时间.对于散列算法,像MD5,SHA-1都已经实现了碰撞,不再安全,应使用现在公认还比较安全的散列算法如SHA-256,SM3等.

public void encrypt(String str){

Cipher encryptCipher=

Cipher.getInstance(″DES″);

KeyGenerator keygen=

KeyGenerator.getInstance(″DES″);

SecretKey deskey=keygen.generateKey();

encryptCipher.init(Cipher.ENCRYPT_MODE, deskey);

byte[] src=str.getBytes();

byte[] cipherByte=

encryptCipher.doFinal(src);

}

以上代码采用DES加密方式对输入的字符串进行加密,由于DES加密算法已经不安全,需要将其改为AES加密算法,AES加密算法可以使用256 b长度的密钥,以现在的技术在可承受的时间范围内破解是不可能的.当然随着技术的发展,如量子计算,将来的某一天,也许AES加密算法也不再安全,但至少现在暂时还是安全的,为了修复该问题,只需要将以上代码修改为如下代码:

public void encrypt(String str, String

password){

Cipher encryptCipher=Cipher.getInstance(″AES″);

KeyGenerator keygen=

KeyGenerator.getInstance(″AES″);

keygen.init(256, new

SecureRandom(password.getBytes()));

SecretKey orikey=keygen.generateKey();

byte [] raw=orikey.getEncoded();

SecretKey deskey=new SecretKeySpec(raw, ″AES″);

encryptCipher.init(Cipher.ENCRYPT_MODE, deskey);

byte[] src=str.getBytes();

byte[] cipherByte=encryptCipher.doFinal(src);

}

2.2 代码安全性工具静态分析

通过工具对源代码的安全性进行静态分析并不是用工具跑一遍得出结果就可以了,我们知道只要是采用工具就有2个不可避免的问题:漏报率和误报率.漏报率可以通过人工走查的方式降低,要降低误报率,采用静态分析工具对源程序进行编码规则检查,对于工具报出的问题再由人工进行进一步的分析以确认软件问题,是一种比较有效的方法[8].

目前各种代码安全性静态分析工具都比较成熟,所采用的分析方法一般有:数据流分析、语义分析、结构分析、控制流分析、配置分析等.

2.2.1数据流分析

数据流分析就是跟踪程序中数据的传递,从而发现存在的安全问题.比如数据从一个变量传递给另一个变量,或者数据通过调用函数传递到函数内部,处理后再返回给另一个变量等.下面举一个SQL注入漏洞数据流分析的例子,如图1所示:

图1 数据流传递示意图

图1在getRawParameter()函数中用户提交的数据通过request.getParameterValues()传递到服务器,接着数据返回到createContent()函数内部,并形成SQL语句,执行查询操作.数据流在从用户提交到SQL语句执行的整个传递过程都可以很清楚看到.经过分析,在数据流的整个传递过程中都没有对数据进行过滤,在最终的执行阶段也没有采用预编译的方式通过占位符参数绑定防止SQL注入,因此,这是一个SQL注入漏洞.

通过数据流分析可以很容易发现通过数据的传递引发的安全漏洞,但是由于其只对数据流进行跟踪,对于数据流之外的防护手段是无法发现的,这就可能出现误报,因此还需要进行额外的人工分析以消除这些误报.比如采用了全局过滤器,这时在web.xml配置文件中会有如下代码:

SqlInjectionfilter

com.filter.SqlInjectionfilter

SqlInjectionfilter

同时,我们还需要检查其对应的过滤函数是否有效,查看classescomfilter目录下的SqlInjectionfilter.java文件

public class SqlInjectionfilter implements Filter {

public void destroy() {

}

public void init(FilterConfig arg0) throws ServletException {

}

public void doFilter(ServletRequest args0, ServletResponse args1, FilterChain chain) throws IOException, ServletException {

过滤代码

}

}

对于asp代码,可以通过在文件头引用具有SQL过滤功能的代码对提交的数据进行过滤,如下所示:

SqlInjectionfilter.asp文件是对提交数据进行过滤的代码,如:

<%

If Request.QueryString<>″″ Then

For Each Get_Data In Request.QueryString

对通过GET方式提交的参数进行过滤…

Next

End If

If Request.Form<>″″ Then

For Each Post_Data In Request.Form

对通过POST方式提交的参数进行过滤…

next

end if

%>

这种过滤方法同样是在数据流之外,通过数据流分析的方法无法识别,因此需要人工审查并进行剔除.

2.2.2语义分析

语义分析就是分析代码中不安全函数、API或不安全方法的使用,这对一些缓冲区溢出及格式化字符串问题十分有效,比如下面这段代码:

char a[10]″;

char b[10]″;

strcat(a,b);

由于使用了危险的函数strcat(),可能造成缓冲区溢出漏洞.

再举一例,比如以下代码:

public final static String

DATABASE_PASSWORD=″cp8gc6ka″;

将密码直接写到源代码中,这是一种不安全的做法,称为密码硬编码.因为一旦软件发布以后就不能修改这些密码,除非发布新的版本,而通过对软件进行动态调试或二进制分析也可能找到这些密码,因此应对密码进行加密并存储在外部的配置文件中.

2.2.3结构分析

结构分析就是通过对程序结构的上下文进行分析,并找出其中的安全问题,比如下面的代码:

public class test extends HttpServlet {

String name;

protected void doPost (HttpServletRequest req,HttpServletResponse res) {

username=req.getParameter(″username″);

out.println(″Hello″+username);

}

}

该段代码将用户名变量放置在成员变量中,从结构上看,这个变量在“类”中,“方法”外.当一个用户进行访问时,这段代码是没有什么问题的,但当2个用户同时访问时,由于Servlet是单实例多线程的并发处理模式,会导致第2个用户的用户名覆盖第1个用户的用户名,从而在执行到显示用户名的代码时,将第2个用户的用户名显示给第1个用户,形成竞争条件问题.

另外一个典型的结构问题是函数在finally中返回,如下所示:

public int test(int Num) {

int rt;

try

{

}

catch (Exception e)

{

}

finally

{

return rt;

}

}

函数在finally中返回会导致从try块中抛出的异常丢失,这样便无法处理可能出现的异常情况.

2.2.4控制流分析

控制流分析根据指令的执行可定义多个不同的状态,不同的状态通过控制流的路径连接起来,主要是在代码解析的基础上,分析过程内语句之间的控制依赖关系,提取程序的控制流信息[9].基本的控制语法,如if,while,case等根据状态的不同引导控制流的走向,如果某一条控制流最后的状态是一个错误状态,那么这就有可能是一个漏洞.

State state=null;

switch (Fg) {

case 1:

state=state1;

break;

case 2:

state=state2;

case 3:

state=state3;

}

state.dosomething ();

在以上代码中,若Fg不为1,2,3中的任何一个值,那么state就不会赋值,仍为null状态,这时若对state进行操作就有可能触发null引用问题,导致程序崩溃,当然若state在赋值状态,程序是没有问题的.

2.2.5配置分析

配置分析就是对配置文件的内容进行分析,找出其中可能存在的安全问题,如敏感信息、不安全的配置等.比如在application.properties配置文件中有如下配置:

jdbc.driver=

oracle.jdbc.driver.OracleDriver

jdbc.url=jdbc:oracle:thin:@127.0.0.1:1521:orcl

jdbc.username=test

jdbc.password=uuikx0k6

该配置文件描述了应用程序通过jdbc连接oracle数据库的连接字符串,包括连接的用户名和密码,可以看到密码采用了明文方式存储,任何可以访问该配置文件(包括使用非法手段)的人都可以获得数据库的访问密码,从而访问数据库,因此需要对配置文件中存储的密码进行加密.

需要注意的是,工具并不能区分出密码字符串是否经过加密,比如下面的配置:

zJQDik2PenVdnh6IZA0cqW9kX7Nz53oc=

工具仍可能会认为这可能是一个存储在配置文件中的未加密的密码,因此需要对工具分析出的结果进行人工分析.

另外一些看上去好像是加密的密码实际上并没有加密,如下所示:

jdbc.password=MTIzNDU2Nzg5MA==

这实际上只是对密码明文进行了Base64的编码,并不是加密,可以很容易还原成明文,以上都是在对工具静态分析结果进行确认时需要注意的地方.

3 结束语

在软件安全测试领域,经过多年的发展,测试方法和测试手段已经多元化,有的侧重于安全功能的实现,有的侧重于外部环境的影响,还有的需要从整个系统层面进行考量.对于特别重要的软件系统,如涉及人民生命财产安全、重要数据、甚至可能影响到国家安全的软件及信息系统,代码安全性审查就是一个十分必要的手段.虽然目前还存在人工成本大、耗时长、工具分析存在误报和漏报等不足,但是随着云计算、大数据、人工智能等技术的发展,若将其应用到对软件源代码的安全分析方面,代码安全性审查的效率将大大提高,时间和成本也会大幅度降低,成本的降低将带来技术的普及,这将为更多、更广范围的软件带来安全性的保障.

猜你喜欢
缓冲区静态代码
最新进展!中老铁路开始静态验收
静态随机存储器在轨自检算法
创世代码
创世代码
创世代码
创世代码
基于ARC的闪存数据库缓冲区算法①
一类装配支线缓冲区配置的两阶段求解方法研究
油罐车静态侧倾稳定角的多体仿真计算
初涉缓冲区