文章目录
Coverity代码扫描-不安全库函数的解决方案
1、引言
因C语言在大多数情况下注重效率,故C语言中的库函数不会自动进行边界检查处理。然而,获得效率的代价是C 程序员必须十分警觉以避免缓冲区溢出问题。C 语言中大多数缓冲区溢出问题可以直接追溯到标准 C 库。最有害的罪魁祸首是不进行自变量检查的、有问题的字符串操作(strcpy、strcat、sprintf 和 gets)。一般来讲,像“避免使用 strcpy()”和“永远不使用 gets()”这样严格的规则接近于这个要求。本文旨在介绍coverity代码静态扫描工具对C语言中不安全库函数的扫描过程,并针对不安全库函数扫描产生的**“越界风险”**进行处理。
2、Coverity代码扫描警告
图2-1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wh5Gct8K-1692947339129)(Coverity代码扫描-不安全库函数的解决方案.assets/20230824-111038.jpg)]
图2-2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNUTAGXe-1692947339131)(Coverity代码扫描-不安全库函数的解决方案.assets/20230824-111440.jpg)]
图2-3
上图为coverity代码静态扫描工具,在扫描C语言不安全库函数时上报的警告。当应用层或驱动层在调用strcpy、strncpy、strcat、sprintf、snprintf、memcpy和memmove等不安全库函数时,由于库函数不会自动地进行边界检查,故存在”内存越界“的风险。如使用strcpy()函数将源字符串复制到缓冲区时,没有指定要复制字符的具体数目。当源字符串碰巧作为用户输入字符串,且不限制其输入长度时,易出现源字符串长度大于目标字符串的长度,导致内存越界。即使使用strncpy()函数复制指定长度的字符串,但并不能保证目标字符串以空字符(‘\0’)终止。如果源字符串的长度大于等于指定的复制长度,目标字符串将不会被正确终止,可能导致后续对该字符串的处理出现问题。
3、C语言不安全函数表
函数 | 严重性 | 解决方案 |
---|---|---|
gets | 最危险 | 使用 fgets(buf, size, stdin)。 |
strcpy | 很危险 | 改为使用 strncpy。 |
strcat | 很危险 | 改为使用 strncat。 |
sprintf | 很危险 | 改为使用 snprintf,或者使用精度说明符。 |
scanf | 很危险 | 使用精度说明符,或自己进行解析。 |
sscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
fscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
vfscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
vsprintf | 很危险 | 改为使用 vsnprintf,或者使用精度说明符。 |
vscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
vsscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
strecpy | 很危险 | 确保分配的目的地参数大小是源参数大小的四倍。 |
strtrns | 危险 | 手工检查来查看目的地大小是否至少与源字符串相等。 |
syslog | 中等危险 | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getopt | 中等危险 | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getchar | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
fgetc | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
getc | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
read | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
memcpy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
snprintf | 低危险 | 确保缓冲区大小与它所说的一样大。 |
strncpy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
3、C语言部分不安全函数解决方式
为解决coverity静态工具扫描C语言不安全库函数产生的警告,本节将封装C语言不安全库函数,并在封装函数中进行边界检查,添加空字符(‘\0’)终止。
a. strcpy和strncpy封装
/* 代码中尽量避免直接使用strcpy字符拷贝函数 */
/* strcpy 封装函数 */
int32_t strcpy_s(char *pszDest, size_t nDestSize, const char *pszSrc)
{
size_t src_size = 0;
if ( !pszDest || !nDestSize || !pszSrc )
return ERR_PARAMETER;
src_size = strlen(pszSrc) + 1;
if ( nDestSize <= src_size )
src_size = nDestSize;
strncpy(pszDest, pszSrc, src_size);
pszDest[nDestSize - 1] = 0;
if ( src_size <= nDestSize )
return ERR_OK;
else
return ERR_BUFFERSIZE;
}
/* strncpy 封装函数 */
int32_t strncpy_s(char *pszDest, size_t nDestSize, const char *pszSrc, size_t nBytesCopy)
{
size_t src_size = 0;
size_t dest_size = 0;
if ( !pszDest || !nDestSize || !pszSrc)
return ERR_PARAMETER;
src_size = MIN(strlen(pszSrc) + 1, nBytesCopy);
if ( nDestSize <= src_size )
src_size = nDestSize;
strncpy(pszDest, pszSrc, src_size);
dest_size = nDestSize - 1;
if ( dest_size > src_size )
dest_size = src_size;
pszDest[dest_size] = '\0';
if ( nBytesCopy <= nDestSize )
return ERR_OK;
else
return ERR_BUFFERSIZE;
}
b. strcat和strncat封装
/* strcat 封装函数 */
int32_t strcat_s(char *pszDest, size_t nDestSize, const char *pszSrc)
{
size_t dest_offset = 0;
size_t src_size = 0;
if ( !pszDest || !nDestSize || !pszSrc )
return ERR_PARAMETER;
src_size = strlen(pszSrc) + 1;
dest_offset = strlen(pszDest);
strncpy(&pszDest[dest_offset], pszSrc, MIN(nDestSize - dest_offset, src_size));
pszDest[nDestSize - 1] = '\0';
if ( dest_offset + src_size <= nDestSize )
return ERR_OK;
else
return ERR_BUFFERSIZE;
}
/* strncat 封装函数 */
int32_t strncat_s(char *pszDest, size_t nDestSize, const char *pszSrc, size_t nBytesCat)
{
size_t dest_offset = 0;
size_t src_size = 0;
if ( !pszDest || !nDestSize || !pszSrc)
return ERR_PARAMETER;
src_size = MIN(strlen(pszSrc) + 1, nBytesCat);
dest_offset = strlen(pszDest);
strncpy(&pszDest[dest_offset], pszSrc, MIN(nDestSize - dest_offset, src_size));
pszDest[nDestSize - 1] = '\0';
if ( dest_offset + src_size <= nDestSize )
return ERR_OK;
else
return ERR_BUFFERSIZE;
}
c. sprintf封装
/* 使用sprintf函数,当目标字符长度较小时,易出现内存越界导致程序终止
* 故尽量使用相对安全的snprintf来替代程序中的sprintf函数。此外,尽
* 管snprintf不会崩溃退出,但当要存储的字符串长度大于目标字符串长度时,
* 会只保存目标串长度-1长度的字符串,并在末尾补\0字符串结束符,出现字
* 符截断现象,故可对snprintf函数进行封装,提高程序的健壮性。*/
int32_t snprintf_s(char *buf, size_t nDestSize, const char *format, ...)
{
va_list args;
int32_t ret = 0;
if ( !buf || !nDestSize)
return ERR_PARAMETER;
va_start(args, format);
ret = vsnprintf(buf, nDestSize, format, args);
va_end(args);
if ( ret <= nDestSize - 1)
return ERR_OK;
else
return ERR_BUFFERSIZE;
}
d. memcpy和memmove封装
/* memcpy 封装函数 */
int32_t memcpy_s(void *pDest, size_t ulDestSize, const void *pSrc, size_t ulToCopy)
{
if ( !pDest || ulDestSize < ulToCopy || !pSrc )
return ERR_PARAMETER;
memcpy(pDest, pSrc, ulToCopy);
return ERR_OK;
}
/* memmove 封装函数 */
int32_t memmove_s(void *pDest, size_t ulDestSize, const void *pSrc, size_t ulToCopy)
{
if ( !pDest || ulDestSize < ulToCopy || !pSrc )
return ERR_PARAMETER;
memmove(pDest, pSrc, ulToCopy);
return ERR_OK;
}
4、结论
综上所述,本文主要介绍了C语言不安全库函数使用过程中存在的隐患,当程序调用C语言库函数时,尽量使用危险程序较低的库函数,如(strncpy替代strcpy,strncat替代strcat、snprintf替代sprintf等)。其次,针对没有相同功能,且风险程度较高的库函数,可封装该类型的库函数,进行边界检查,以及添加空字符(‘\0’)终止符等操作,来提高代码的健壮性。