1.1 确保所有字符串都是以NULL结束
C语言中以 '\0'
作为字符串的结束符,即NULL结束符。
没有正确使用NULL结束符会导致缓冲区溢出和其他未定义的行为。
为了避免缓冲区溢出,常常会用相对安全的限制字符数量的字符串操作函数代替一些危险函数
- strncpy()
代替 strcpy()
- strncat()
代替 strcat()
- snprintf()
代替 sprintf()
- fgets()
代替 gets()
这些函数会截断超出指定限制的字符串,但是要注意他们并不能保证目标字符串总是以NULL结尾。
//错误释放,a后面没有NULL结束符
char a[16];
strncpy(a,"0123456789abcdef",sizeof(a));
上面的代码调用strncpy()后,a的字符串中是没有NULL结束符的。
//正确写法:截断字符串,保证字符串以NULL结束
char a[16];
strncpy(a,"0123456789abcdef",sizeof(a)-1);
a[sizeof(a)-1] = '\0';
1.2 不要将不明确的字符串写到固定长度的数组中
边界不明确的字符串(例如来自 gets(), getenv(), scanf()的字符串)长度可能大于目标数组长度,直接拷贝到固定长度的数组中容易导致缓冲区溢出。
//错误示例:
char buff[256];
char *editor = getenv("EDITOR");
if(editor != NULL)
{
strcpy(buff,editor);
}
editor实际长度可能大于256导致溢出。
//正确写法:计算字符串的实际长度,使用malloc分配指定长度的内存
char *buff;
char *editor = getenv("EDITOR");
if(editor != NULL)
{
buff = malloc(strlen(editor)+1);
if(buff != NULL)
{
strcpy(buff,editor);
}
}
C99标准定义了整型提升、整型转换级别以及普通算数转换的整型操作。不过这些操作实际上也带来了安全风险。
2.1 避免整数溢出
当一个证书被增加超过起最大值时会发生整数上溢,被减小小于其最小值时会发生下溢。带符号和无符号的数都可能发生溢出。
//有符号和无符号整数的上溢和下溢
int i;
unsigned int j;
i = INT_MAX; //2147483647
i++;
printf("i = %d\n",i); i = -214748348
j = UINT_MAX; //4294967295
j++;
printf("j = %u\n", j); //0
i = INT_MIN; //-2147483648
i--;
printf("i = %d\n",i); //i = 2147483647
j = 0;
j--;
printf("j = %u\n", j); //4294967295
在长度加减时,加上长度检查
//length 可能小于 FSM_HDRLEN
unsigned int length;
length -= FSM_HDRLEN; //当length < FSM_HDRLEN时,发现下溢,返回一个很大的数
//正确写法: 判断长度
if(length < FSM_HDR_LEN)
{
return VOS_ERROR;
}
length -= FSM_HDRLEN;
2.2 避免符号错误
有时从带符号整型转换为无符号整型会发生符号错误,符号错误不会丢失数据,但是失去了原有的意义。
带符号位整型转化到无符号整型,最高位(high-order bit)会丧失其作为符号位的功能。
//错误示例:符号错误绕过长度检查
#define BUF_SIZE 10
int main(int argc, char* argv[])
{
int length;
char buf[BUF_SIZE];
if(argc != 3)
{
return -1;
}
length = atoi(argv[1]); //如果atoi返回的长度为负数
if(length < BUF_SIZE) //length 为负数,长度检查无效
{
//带符号的length被转换为size_t类型的无符号整型,负值被解释为一个极大的正整数。
//memcpy()调用时引发buf缓冲区溢出
memcpy(buf, argv[2], length);
printf("Data copied\n");
}
else
{
printf("Too many data\n");
}
return 0;
}
//正确写法1:将length声明为无符号整型
#define BUF_SIZE 10
int main(int argc, char* argv[])
{
unsigned int length;
char buf[BUF_SIZE];
if(argc != 3)
{
return -1;
}
length = atoi(argv[1]); //如果atoi返回的长度不可能为负数
if(length < BUF_SIZE) //length 不为负数,长度检查有效
{
memcpy(buf, argv[2], length);
printf("Data copied\n");
}
else
{
printf("Too many data\n");
}
return 0;
}
//正确写法2:对length的长度判断更有效的范围校验
#define BUF_SIZE 10
int main(int argc, char* argv[])
{
unsigned int length;
char buf[BUF_SIZE];
if(argc != 3)
{
return -1;
}
length = atoi(argv[1]); //如果atoi返回的长度为负数
if(length > 0 && length < BUF_SIZE) //length 为负数,长度检查有效
{
memcpy(buf, argv[2], length);
printf("Data copied\n");
}
else
{
printf("Too many data\n");
}
return 0;
}
2.3 避免截断错误
将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留,而高位被丢弃。截断错误会引起数据丢失,使用截断后的变量进行内存操作,很可能引发问题。
//错误示例:截断错误
int main(int argc, char* argv[])
{
//total 是 unsigned short, strlen()返回的时size_t 是 unsigned long.
//如果输入的参数长度是 65500 和 36
//那么 total = (65500 + 36 + 1) % 65536 = 1
//strcpy() 和 strcat() 函数会缓冲区溢出
unsigned short total = strlen(argv[1]) + strlen(argv[2]) + 1;
char * buffer = (char*) malloc(tatal);
strcpy(buffer, argv[1]);
strcat(buffer, argv[2]);
free(buffer);
return 0;
}
//正确写法:将涉及到计算的变量声明为统一的类型,并检查计算结果
int main(int argc, char* argv[])
{
size_t total = strlen(argv[1]) + strlen(argv[2]) + 1;
if( (total < strlen(ar)) || () )
char * buffer = (char*) malloc(tatal);
strcpy(buffer, argv[1]);
strcat(buffer, argv[2]);
free(buffer);
return 0;
}
3.1 确保格式字符和参数匹配
3.2 避免将用户输入作为格式化字符串的一部分或者全部
调用格式化I/O时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。
攻击者对一个格式化字符串拥有部分或完全控制,存在以下风险:进程崩溃,查看栈内容,改写内存,甚至执行恶意代码。
//错误代码示例:
char input[1000];
if(fgets(input, sizeof(input) - 1, stdin) == NULL)
{
return -1;
}
input[sizeof(input)-1] = '\0';
printf(input);
上述代码input直接来自用户输入,并作为格式化字符直接传递给printf();当用户输入的是”%s%s%s%s%s%s%s%s%s”,就可能触发无效指针或未映射的地址读取。格式化字符%s显示栈上相应参数所指定的地址的内存。这里input被当成格式化字符,而没有提供参数,因此printf()读取栈里任意内存位置,直到格式化字符耗尽或者遇到一个无效指针或未映射地址为止。
//正确写法:传两个参数,将格式化字符串定下来
char input[1000];
if(fgets(input, sizeof(input) - 1, stdin) == NULL)
{
return -1;
}
input[sizeof(input)-1] = '\0';
printf("%s\n",input);
//错误示例:
void check_password(char *user, char *password)
{
if(strcmp(password(user),password) != 0)
{
char *msg = malloc(strlen(user) + 100);
if(!msg)
{
return -1;
}
sprintf(msg,"%s login incorrect",user);
fprintf(STDERR,msg);
syslog(LOG_INFO, msg);
free(msg);
}
//...
}
上述代码检查给定用户名及其口令是否匹配,当不匹配时显示一条错误信息,并将错误信息写入日志中。
如果user为”%s%s%s%s%s%s%s%s%s%s”,经过格式化函数sprintf()的拼装后,msg指向的字符串是”%s%s%s%s%s%s%s%s%s%s login incorrect”,那么接下来的fprintf()和syslog就会出现格式化字符的问题。
//正确写法:格式化字符串由代码确定,未经检查过滤的用户输入只能作为参数。
void check_password(char *user, char *password)
{
if(strcmp(password(user),password) != 0)
{
char *msg = malloc(strlen(user) + 100);
if(!msg)
{
return -1;
}
sprintf(msg,"%s login incorrect",user);
fprintf(STDERR,"%s",msg);
syslog(LOG_INFO, "%s",msg);
free(msg);
}
//...
}
4.1 避免使用strlen()函数计算二进制数据的长度
strlen()函数用于计算字符串的长度,它返回字符串中第一个NULL结束符之前的字符的数量。
因此用strlen()处理文件I/O函数读取内容时要小心,因为这些内容可能是二进制,也可能是文本。
//错误示例:
char buf[BUF_SIZE];
if(fgets(buf,sizeof(buf),fp) == NULL)
{
//handle error
}
buf[strlen(buf)-1] = '\0';
上述代码试图从一个输入行中删除行尾的换行符(\n),如果buf的第一个字符时NULL,strlen(buf)返回0,这时buf[0-1]操作会越界。
//正确写法:在不能确定从文件读取到的类型时,不要使用依赖NULL结束符的字符串操作函数。
char buf[BUF_SIZE];
char *p;
if(fgets(buf,sizeof(kbuf),fp))
{
p = strchr(buf,'\n');
if(p)
{
*p = '\0';
}
}
else
{
//handle error
}
4.2 使用int类型变量来接受字符I/O函数的返回值
字符I/O函数 fgetc(), getc() 和 getchar() 都从一个流读取一个字符,并把它以int值的形式返回。
如果这个流到达了文件尾或者发生读取错误,函数返回EOF。
fputc(), putc(),putchar() 和 ungetc() 也返回一个字符或EOF。
如果这些I/O函数的返回值需要与EOF比较,不要将返回值转换为char 类型。
因为char 是有符号8位的值,int 是32位的值。如果getchar() 返回的字符的ASCII值为 0xFF, 转换为char类型后将被解释为EOF。因为这个值被有符号扩展为0xFFFFFFFF(EOF)执行比较。
//错误示例:
char buf[BUF_SIZE];
char ch;
int i = 0;
while((ch = getchar()) != '\n' && ch != EOF)
{
if(i < BUF_SIZE - 1)
{
buf[i++] = ch;
}
}
buf[i] = '\0';
//正确写法:使用int类型的变量接受getchar()的返回值
char buf[BUF_SIZE];
int ch;
int i = 0;
while(((ch = getchar()) != '\n') && ch != EOF)
{
if(i < BUF_SIZE-1)
{
buf[i++] = ch;
}
}
cuf[i] = '\0';
如果有些机器sizeof(int) == sizeof(char), 那么用int接收返回值也可能无法与EOF区分,这时要用feof()和ferror()检查文件尾和文件错误。
C99函数system() 通过调用一个系统定义的命令解析器(例如UNIX的shell,Windows的CMD.exe)来执行一个指定的程序/命令。类似的还有POSIX的函数popen().
如果system()的参数由用户的输入组成,恶意用户可以通过构造恶意输入,来改变system()调用的行为。
system(sprintf("any_exe %s",input));
如果用户输入: happy; useradd attacker
最终shell将字符串”any_exe happy; useradd attcker” 解释为两条独立的命令。
//正确写法:使用POSIX函数execve()代替system()
void secuExec(char *input)
{
pid_t pid;
char *const args[] = {"",input,NULL};
char *const envs[] = {NULL};
pid = fork();
if(pid == -1)
{
puts("fork error");
}
else if(pid == 0)
{
if(execve("/usr/bin/any_exe", args, envs) == -1)
{
puts("Error executing any_exe");
}
}
return;
}
Windows 环境可能对 execve() 的支持不是很完善,建议使用 Win32 API 的 CreateProcess() 代替system().