第5章 编写安全的代码
本章内容:
* 执行静态安全分析;
* 在项目的整个生命周期进行安全性问题的状态跟踪;
* 了解哪些编程行为会使代码易于受到攻击。
许多安全威胁来自于C或C++编程时引入的弱点。由于类型检查较弱以及可以编写直接访问内存和硬件的程序,很容易写出不安全的程序。大多数攻击可以归为两类:
* 让程序崩溃或超载的攻击。
* 通过插入外部代码而绑架代码执行的攻击。
Intel编译器的静态安全分析(Static Security analysis)将排除许多代码弱点,这些弱点将在Intel的InspectorXE中显示。将有超过250个不同的错误可以被检查,可分为如下 类型:
* 缓冲区溢出和越界。
* 未初始化的变量和对象。
* 内存泄漏。
* 错误使用指针和动态分配内存。
* 未检查输入的危险。
* 数学溢出和除数为0。
* 死代码或冗余代码。
* 错误地使用字符串、内存和格式化的库例程。
* 在程序不同的地方使用不一致的对象声明。
* 错误地使用OpenMP或Cilk Plus。
* 容易出错的C++和Fortran语言用法。
本章讨论如何对代码使用Intel Parallel Studio XE进行静态安全性分析。静态安全检查的目的在于面对安全攻击时更加安全牢靠,同时对于发现编程错误也有帮助。
5.1 一个简单的安全缺陷例子
代码5-1包含了一个可以被攻击的安全错误。攻击者可以用一个未经检查的用户输入来产生缓冲区溢出。
代码5-1 带有几个安全问题的程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// user functions
int NotePad(){printf("USER: here we launch notepad\n\n"); return 0;}
int Exit(){ exit(0);}
// system functions
int Dir(){printf("SYSTEM: here we launch dir\n\n"); return 0;}
int Delete(){printf("SYSTEM: here we launch Del\n\n"); return 0;}
int ReturnToMain(){return -1;}
int SystemMenu();
int MainMenu();
int (*user_table[])(void) = {NotePad, SystemMenu,Exit};
int (*system_table[])(void) = {Dir, Delete, ReturnToMain};
int SystemMenu()
{
char password[20];
int id;
int ret = 0;
printf("System Menu\n");
printf("Enter the Password before continuing!...\n");
scanf("%s",password);
if (strcmp(password, "PASSWORD") == 0)
{
while (ret != -1)
{
printf("Enter a number:\n");
printf("1: dir\n");
printf("2: delete everything\n");
printf("3: back to main menu\n");
scanf("%d",&id);
ret = system_table[id-1]();
}
}
else
{
printf("Invalid Password!\n");
return 0;
}
return 0;
}
int MainMenu()
{
int id;
printf("What would you like to do?\n");
printf("Enter a number:\n");
printf("1: run Notepad\n");
printf("2: go to system menu\n");
printf("3: quit\n");
scanf("%d",&id);
return user_table[id-1]();
}
int main ()
{
int ret = 0;
while( ret != -1)
ret = MainMenu();
return ret;
}
code snippet Chapter5\5-1.c
这个程序包含两个菜单:一个用户菜单和一个系统菜单。当程序刚启动时,MainMenu()函数给用户3个选择:
What would you like to do?
Enter a number:
1: run Notepad
2: go to system menu
3: quit
用户的输入由scanf()获取,然后将结果保存在id中。id的值(减1)用作数组user_table的索引,该数组是一个函数指针数组。
选择1则调用Notepad函数,选择2则会让SystemMenu()函数显示系统菜单,选择3则通过Exit()函数退出程序。
SystemMenu()函数工作原理与MainMenu()相似,使用system_table数组来跳转到Dir()、Delete()和ReturnToMain()函数。在系统菜单启动之前,用户被提示需要输入密码(PASSWORD)。如果密码错误,将显示一条消息然后将控制返回到MainMenu()函数,接着返回0给main()函数的while循环中。
从用户菜单中选择2将显示一个需要密码的系统菜单。在正确输入密码之后将显示如下的菜单:
System Menu
Enter the password before continuing!...
PASSWORD
Enter a number:
1: dir
2: delete everything
3: back to main menu
5.2 了解静态安全分析
预测攻击者将如何攻击一个程序是非常困难的。攻击者既狡猾又坏,将会利用代码中的任何弱点。编写一系列测试或者对应用程序进行调试都无法发现大量的弱点。使用上述方法时,最理想的情况下,将测试出到底如何执行,还有一些危险是无法测试的。
静态安全分析和标准调试不一样,前者指分析代码而不执行。所有可能执行到的路径都将被分析,即使那些在测试下永远不会被执行的部分。
对代码5-1执行静态安全分析将产生如下错误消息。这个问题可以用作安全性攻击的工具。
* main.c(28): error #12329——请指定格式限定符(format specifier)以避免在scanf()第二个参数上出现缓冲区溢出。
* main.c(38): error #12305——外部函数调用的返回值没有经过检验(文件:main.c 37行),该值用作system_table的索引。
* main.c(59): error #12305——外部函数调用的返回值没有经过检验(文件:main.c 58行),该值用作user_table的索引。
有人可以按如下方式进行攻击:
* 使用无效的用户输入来越过系统菜单的密码——如果在用户菜单中输入一个大于3的数值,将会执行系统菜单中的函数。不需要输入密码!
What would you like to do?
Enter a number:
1: run Notepad
2: go to system menu
3: quit
5
SYSTEM: here we would launch Del
这是因为user_table数组和system_menu数组在内存中是相邻的。其中user_table有3项。使用索引号为4意味着函数指针将在内存中超过该数组的末尾处获得,从而读到system_table的第一项。
* 使用无效的用户输入或系统输出引起程序崩溃或执行随意的代码——如果在菜单选择时输入一个非常大的数值,程序将开始执行不在这两个数组中的地址上的代码。如果你比较幸运,这些代码可能无害或者仅仅是崩溃。在最坏的情况下,可能开始执行一些有效的但是危险的代码。
* 通过输入非常长的密码来让应用程序崩溃——passwd变量可以保存20个字符。下面的例子使用了长得多的密码。当调用scanf时,超出来的字符将会破坏堆栈,从而让程序崩溃。
What would you like to do?
Enter a number:
1: run Notepad
2: go to system menu
3: quit
2
System Menu
Enter the Password before continuing!...
A_VERY_VERY_LONG_PASSWORD
Invalid Password!
... (program crashes after this)
5.2.1 虚警
并不是所有静态安全分析报告的威胁都是真正的问题——这些称为虚警(false postives)。
在下面代码中,静态安全分析器不够聪明,无法知道第一个if语句的不成功分支和第二个if语句的成功分支不可能同时被执行。
……