安全函数解读及使用注意事项

2023年 9月 27日 93.3k 0

安全函数背景

安全函数的设计初衷是为了帮助程序员在编写代码时增加安全意识,在编程过程中时刻保持将外部数据当作不可信数据的思想,小心谨慎地处理外部的数据,并且在万一传入不正确的参数的情况下清空 destBuff,安全退出,降低程序被进一步攻击的风险。 要正确用好安全函数,发挥安全函数的价值,必须比使用原有的 memcpy 等不安全函数更加谨慎,在对参数的使用上应该更加严格。

安全函数必须正确使用,不正确的使用方法无法发挥出安全函数的价值。正确使用安全函数有助于提升代码的安全性,包括正确指定安全函数的参数,处理返回值等。

安全函数的使用前提:srcBuffer/count 或者 destBuffer/destMax 是一段合法的内存空间。 合法的内存空间是指: (1)堆 (2)栈 (3)程序静态数据区 (4)mmap 分配出来的内存页。对于不属于合法内存空间范围内的内存,例如已释放的堆内存、不可访问的内存、跨越未映射内存页的内存等等,安全函数不对合法性进行检查,在执行过程中,安全函数内部本身执行过程可能会出错。

概述

危险函数信赖于程序员对参数进行检查、或保证空间能足够容纳所产生的结果,函数本身不对这些情况进行判断,即使有问题也不会给出错误的指示。相比危险函数,安全函数进行了相应的安全增强,会对入参以及不同的错误情况进行判断,降低操作不当所引入的安全风险。 安全函数和宏接口增强的安全特性包括:

  • 强化边界检查:在接口参数中增加一个 buffer 长度的参数,在长度参数正确情况下不会出现溢出
  • 保证结果字符串以’\0’结尾,避免访问 buffer 边界之外的信息
  • 发现缓冲区溢出发生,将目的缓冲区的首字节置零
  • 增加错误返回值,便于程序员快速进行错误定位
  • 增强入参检查
  • 增加入参的内存重叠检查(memcpy_sp 宏中对于常量拷贝不进行内存重叠检查)
安全函数类型 说明 备注
xxx_s Huawei Secure C库的安全函数API 集成Huawei Secure C库即可使用
xxx_sp Huawei Secure C库的安全函数性能优化API(宏实现) 性能优化宏接口对count、destMax、strSrc为常量时有优化效果,如果是变量则优化效果不明显.宏接口使用策略:默认使用_s接口,在性能敏感的调用点受限使用_sp接口,受限场景如下: a) memset_sp/memcpy_sp使用场景:destMax和count为常量 b) strcpy_sp/strcat_sp使用场景:destMax为常量且strSrc为字面量 c) strncpy_sp/strncat_sp使用场景:destMax和count为常量且strSrc为字面量
函数类别 危险函数 安全替代函数
内存拷贝 memcpy或bcopy memcpy_s
wmemcpy wmemcpy_s
memmove memmove_s
wmemmove wmemmove_s
字符串拷贝 strcpy strcpy_s
wcscpy wcscpy_s
strncpy strncpy_s
wcsncpy wcsncpy_s
字符串串接 strcat strcat_s
wcscat wcscat_s
strncat strncat_s
wcsncat wcsncat_s
格式化输出 sprintf sprintf_s
swprintf swprintf_s
vsprintf vsprintf_s
vswprintf vswprintf_s
snprintf snprintf_s 或 snprintf_truncated_s
vsnprintf vsnprintf_s 或 vsnprintf_truncated_s
格式化输入 scanf scanf_s
wscanf wscanf_s
vscanf vscanf_s
vwscanf vwscanf_s
fscanf fscanf_s
fwscanf fwscanf_s
vfscanf vfscanf_s
vfwscanf vfwscanf_s
sscanf sscanf_s
swscanf swscanf_s
vsscanf vsscanf_s
vswscanf vswscanf_s
标准输入流输入 gets gets_s
内存初始化 memset memset_s

安全函数返回值


/* Success */
#ifndef EOK
#define EOK 0
#endif

#ifndef EINVAL
/* The src buffer is not correct and destination buffer cant not be reset */
#define EINVAL 22
#endif

#ifndef EINVAL_AND_RESET
/* Once the error is detected, the dest buffer must be reseted! Value is 22 xor 128 */
#define EINVAL_AND_RESET 150
#endif

#ifndef ERANGE
/* The destination buffer is not long enough and destination buffer can not be reset */
#define ERANGE 34
#endif

#ifndef ERANGE_AND_RESET
/* Once the error is detected, the dest buffer must be reseted! Value is 34 xor 128 */
#define ERANGE_AND_RESET 162
#endif

#ifndef EOVERLAP_AND_RESET
/* Once the buffer overlap is detected, the dest buffer must be reseted! Value is 54 xor 128 */
#define EOVERLAP_AND_RESET 182
#endif

安全函数介绍

memcpy_s

复制源缓冲区的数据到目的缓冲区。

/*
* Description: The memcpy_s function copies n characters from the object pointed to
* by src into the object pointed to by dest.
* Parameter: dest - destination address
* Parameter: destMax - The maximum length of destination buffer
* Parameter: src - source address
* Parameter: count - copies count bytes from the src
* Return: EOK if there was no runtime-constraint violation
*/
SECUREC_API errno_t memcpy_s(void *dest, size_t destMax, const void *src, size_t count);

注意

  1. 确保入参正确:
    • 拷贝长度参数和目的缓冲区大小参数要大于 0 且小于等于 SECUREC_MEM_MAX_LEN;
    • 目的缓冲区大小参数不能超过目的缓冲区的实际长度、且大于等于要拷贝长度,以便目的缓冲区有足够的空间来保存要复制的内容;
    • 源缓冲区和目的缓冲区不能为 NULL、且源和目的不存在重叠。
  2. 调用函数时,必须对返回值进行检查,确保返回值正确后再进行后续操作。 注意:在源缓冲区为 NULL、拷贝长度大于目的缓冲区大小参数、源和目的重叠的情况下,函数会将目的缓冲区大小参数确定的目的缓冲区范围清零。

memmove_s

移动源缓冲区的数据到目的缓冲区。

/*
* Description: The memmove_s function copies n characters from the object pointed to by src
* into the object pointed to by dest.
* Parameter: dest - destination address
* Parameter: destMax - The maximum length of destination buffer
* Parameter: src - source address
* Parameter: count - copies count bytes from the src
* Return: EOK if there was no runtime-constraint violation
*/
SECUREC_API errno_t memmove_s(void *dest, size_t destMax, const void *src, size_t count);

注意

  1. 确保入参正确:
    • 拷贝长度参数和目的缓冲区大小参数要大于 0 且小于等于 SECUREC_MEM_MAX_LEN;
    • 目的缓冲区大小参数不能超过目的缓冲区的实际长度;
    • 源缓冲区和目的缓冲区不能为 NULL。
  2. 调用函数时,必须对返回值进行检查,确保返回值正确后再进行后续操作。 注意:在源缓冲区为 NULL、拷贝长度大于目的缓冲区大小参数时,函数会将目的缓冲区大小参数确定的目的缓冲区范围清零。

memset_s

设置目的缓冲区为特定值。

/*
* Description: The memset_s function copies the value of c (converted to an unsigned char) into each of
* the first count characters of the object pointed to by dest.
* Parameter: dest - destination address
* Parameter: destMax - The maximum length of destination buffer
* Parameter: c - the value to be copied
* Parameter: count - copies count bytes of value to dest
* Return: EOK if there was no runtime-constraint violation
*/
SECUREC_API errno_t memset_s(void *dest, size_t destMax, int c, size_t count);

注意

  1. 确保入参正确:
    • 初始化长度参数和目的缓冲区大小参数要大于 0 且小于等于 SECUREC_MEM_MAX_LEN;
    • 目的缓冲区大小参数不能超过目的缓冲区的实际长度、且大于等于(要初始化长度参数);
    • 目的缓冲区不能为 NULL。
  2. 调用函数时,必须确保对返回值进行校验,确保返回值正确后再进行后续操作。 注意:在初始化长度参数大于目的缓冲区大小参数时,函数会将目的缓冲区大小参数确定的目的缓冲区范围设置为要设置的字符。

strcpy_s

复制源字符串到目的缓冲区。

/*
* Description: The strcpy_s function copies the string pointed to by strSrc (including
* the terminating null character) into the array pointed to by strDest
* Parameter: strDest - destination address
* Parameter: destMax - The maximum length of destination buffer(including the terminating null character)
* Parameter: strSrc - source address
* Return: EOK if there was no runtime-constraint violation
*/
SECUREC_API errno_t strcpy_s(char *strDest, size_t destMax, const char *strSrc);

注意

  1. 确保入参正确:
    • 目的缓冲区大小参数要大于 0 且小于等于 SECUREC_STRING_MAX_LEN、不能超过目的缓冲区的实际长度、且必须大于等于(源字符串的长度 + 1);
    • 源缓冲区和目的缓冲区不能为 NULL、且源和目的缓冲区不能存在重叠;
    • 源缓冲区必须含有结束符。
  2. 调用函数时,必须确保对返回值进行校验,确保返回值正确后再进行后续操作。 注意:在源缓冲区为 NULL、目的缓冲区大小参数小于(源字符串长度 + 1)、源和目的重叠的情况下,函数会将目的缓冲区的首字节置 0。

strncpy_s

复制指定长度的源字符串到目的缓冲区。

/*
* Description: The strncpy_s function copies not more than n successive characters (not including
* the terminating null character) from the array pointed to by strSrc to the array pointed to by strDest.
* Parameter: strDest - destination address
* Parameter: destMax - The maximum length of destination buffer(including the terminating null character)
* Parameter: strSrc - source address
* Parameter: count - copies count characters from the src
* Return: EOK if there was no runtime-constraint violation
*/
SECUREC_API errno_t strncpy_s(char *strDest, size_t destMax, const char *strSrc, size_t count);

注意

  1. 确保入参正确:
    • 目的缓冲区大小参数和拷贝长度参数要大于 0 且小于等于 SECUREC_STRING_MAX_LEN;
    • 目的缓冲区大小参数不能超过目的缓冲区的实际长度、且必须大于等于((源字符串实际长度,拷贝长度参数)二者最小值 + 1);
    • 源缓冲区和目的缓冲区不能为 NULL、且源和目的缓冲区不能存在重叠;
    • 源缓冲区必须含有结束符。
  2. 目的缓冲区大小参数,源字符串实际长度,拷贝长度参数之间不同组合情况下的操作:
    • 目的缓冲区大小参数小于((源字符串实际长度,拷贝长度参数)二者最小值 + 1)时,函数会将目的缓冲区的首字节置 0;
    • 拷贝长度参数小于其他两项时,拷贝 count 个字节,然后再把 strDest[count]设置成 0;
    • 源字符串实际长度小于其他两项时,把源字符串(包括结束符)拷贝到目的缓冲区;
  3. 调用函数时,必须确保对返回值进行校验,确保返回值正确后再进行后续操作。 注意:在源缓冲区为 NULL、count 大于 SECUREC_STRING_MAX_LEN、目的缓冲区大小参数小于((源字符串实际长度,拷贝长度参数)二者最小值 + 1)、源和目的重叠的情况下,函数会将目的缓冲区的首字节置 0。

strcat_s

将源字符串连接到目的字符串后面。

/*
* Description: The strcat_s function appends a copy of the string pointed to by strSrc (including
* the terminating null character) to the end of the string pointed to by strDest.
* Parameter: strDest - destination address
* Parameter: destMax - The maximum length of destination buffer(including the terminating null wide character)
* Parameter: strSrc - source address
* Return: EOK if there was no runtime-constraint violation
*/
SECUREC_API errno_t strcat_s(char *strDest, size_t destMax, const char *strSrc);

注意

  1. 确保入参正确:
    • 目的缓冲区大小的参数值要大于 0 且小于等于 SECUREC_STRING_MAX_LEN、不能超过目的缓冲区的实际长度、且必须大于等于(目的缓冲区原有字符串长度 + 源字符串长度 + 1);
    • 源缓冲区和目的缓冲区不能为 NULL、且两者不能重叠;
    • 目的缓冲区在目的缓冲区大小的参数范围内必须含有结束符、源缓冲区必须含有结束符。
  2. 调用函数时,必须确保对返回值进行校验,确保返回值正确后再对目的缓冲区进行后续操作。 注意:在源缓冲区为 NULL、目的缓冲区在目的缓冲区大小参数范围内没有结束符、目的缓冲区大小参数小于(目的缓冲区原有字符串长度 + 源字符串长度 + 1)、源和目的重叠的情况下,函数会将目的缓冲区的首字节置 0。

strncat_s

将指定长度的源字符串连接到目的字符串后面。

/*
* Description: The strncat_s function appends not more than n successive characters (not including
* the terminating null character)
* from the array pointed to by strSrc to the end of the string pointed to by strDest.
* Parameter: strDest - destination address
* Parameter: destMax - The maximum length of destination buffer(including the terminating null character)
* Parameter: strSrc - source address
* Parameter: count - copies count characters from the src
* Return: EOK if there was no runtime-constraint violation
*/
SECUREC_API errno_t strncat_s(char *strDest, size_t destMax, const char *strSrc, size_t count);

注意

  1. 确保入参正确:
    • 目的缓冲区大小参数和连接长度参数要大于 0 且小于等于 SECUREC_STRING_MAX_LEN;
    • 目的缓冲区大小参数不能超过目的缓冲区的实际长度、且必须大于等于(目的缓冲区原有字符串长度 +(源字符串实际长度,连接长度参数)二者最小值 + 1);
    • 源缓冲区和目的缓冲区不能为 NULL、且两者不能重叠;
    • 目的缓冲区在目的缓冲区大小的参数范围内必须含有结束符、源缓冲区必须含有结束符。
  2. 调用函数时,必须确保对返回值进行校验,确保返回值正确后再进行后续操作。 注意:在源缓冲区为 NULL、count 大于 SECUREC_STRING_MAX_LEN、目的缓冲区在目的缓冲区大小参数范围内没有结束符、目的缓冲区大小参数小于(目的缓冲区原有字符串长度 +(源字符串实际长度,连接长度参数)二者最小值 + 1)、源和目的重叠的情况下,函数会将目的缓冲区的首字节置 0。

snprintf_s

将数据按照指定长度格式化输出到目的缓冲区。

/*
* Description: The snprintf_s function is equivalent to the snprintf function except for
* the parameter destMax/count and the explicit runtime-constraints violation
* Parameter: strDest - produce output according to a format ,write to the character string strDest.
* Parameter: destMax - The maximum length of destination buffer(including the terminating null byte '\0')
* Parameter: count - do not write more than count bytes to strDest(not including the terminating null byte '\0')
* Parameter: format - fromat string
* Return: the number of characters printed(not including the terminating null byte '\0'),
* If an error occurred Return: -1.Pay special attention to returning -1 when truncation occurs
*/
SECUREC_API int snprintf_s(char *strDest, size_t destMax, size_t count, const char *format,
...) SECUREC_ATTRIBUTE(4, 5);

注意

  1. 确保入参正确:
    • 目的缓冲区大小参数和格式化字符个数参数要大于 0 且小于等于 SECUREC_STRING_MAX_LEN;
    • 目的缓冲区大小参数不能超过目的缓冲区的实际长度、且必须大于等于((格式化后字符串长度,格式化字符个数参数)二者最小值 + 1); - 格式化字符个数参数要大于等于要格式的源字符串的实际长度;
    • 目的缓冲区、格式说明符、要格式化数据均不能为 NULL;
    • 确保输入的源数据与目标缓冲区不存在重叠;
    • 输入源数据中若是字符串则必须含有结束符;
    • 输入源数据的类型、个数必须与格式化控制字符串(format)中的类型、个数保持一致;
    • 格式化控制字符串(format)必须是合法的。
  2. 调用函数时,必须确保对返回值进行校验,确保返回值正确后再进行后续操作。 注意:当目的缓冲区大小参数大于 SECUREC_STRING_MAX_LEN、格式说明符(format)为 NULL、目的缓冲区大小参数小于((格式化后字符串长度,格式化字符个数参数)二者最小值 + 1)时,函数会将目的缓冲区首字节置 0。

sprintf_s

将数据格式化输出到目的缓冲区。

/*
* Description: The sprintf_s function is equivalent to the sprintf function except for the parameter destMax
* and the explicit runtime-constraints violation
* Parameter: strDest - produce output according to a format ,write to the character string strDest.
* Parameter: destMax - The maximum length of destination buffer(including the terminating null byte '\0')
* Parameter: format - fromat string
* Return: the number of characters printed(not including the terminating null byte '\0'),
* If an error occurred Return: -1.
*/
SECUREC_API int sprintf_s(char *strDest, size_t destMax, const char *format, ...) SECUREC_ATTRIBUTE(3, 4);

注意

  1. 确保入参正确:
    • 目的缓冲区大小参数要大于 0 且小于等于 SECUREC_STRING_MAX_LEN(宽字节 SECUREC_WCHAR_STRING_MAX_LEN)、不能超过目的缓冲区的实际长度、且必须大于等于(格式化后字符串长度 + 1);
    • 目的缓冲区、格式说明符、要格式化数据均不能为 NULL;
    • 确保输入的源数据与目标缓冲区不存在重叠;
    • 输入源数据中若是字符串则必须含有结束符;
    • 输入源数据的类型、个数必须与格式化控制字符串(format)中的类型、个数保持一致;
    • 格式化控制字符串(format)必须是合法的。 注意:当目的缓冲区大小参数大于 SECUREC_STRING_MAX_LEN(宽字节 SECUREC_WCHAR_STRING_MAX_LEN)、格式化说明符(format)为 NULL、目的缓冲区大小参数小于(格式化后字符串长度 + 1)时,函数会将目的缓冲区首字节置 0。

校验安全函数返回值

安全函数执行完成后必须检查返回值,确保安全函数的使用正确,目前 openGauss 提供两组宏来检查返回值, securec_check/securec_check_c 以及 securec_check_ss/securec_check_ss_c。

securec_check 的实现(openGauss-server/src/include/utils/elog.h):

securec_check 主要用在 openGauss-server 端,检查返回值是否为 EOK,失败的场景一般通过 elog(ERROR) 进行长跳转。对于定义了 FRONTEND 的场景,一般是代码在 openGauss-server,但是使用场景是客户端,如部分 libpq 函数等。两者的主要区别是失败的场景下,内核通过 elog(ERROR) 的方式打印日志并进行长跳转。而客户端场景通过 printf 打印日志,并直接通过 eixt()退出。

#ifndef FRONTEND
#define securec_check(errno, charList, ...) \
{ \
if (EOK != errno) { \
freeSecurityFuncSpace(static_cast(charList), ##__VA_ARGS__); \
switch (errno) { \
case EINVAL: \
elog(ERROR, \
"%s : %d : The destination buffer is NULL or not terminate- The second case only occures in " \
"function strcat_s/strncat_s.", \
__FILE__, \
__LINE__); \
break; \
case EINVAL_AND_RESET: \
elog(ERROR, "%s : %d : The Source Buffer is NULL.", __FILE__, __LINE__); \
break; \
case ERANGE: \
elog(ERROR, \
"%s : %d : The parameter destMax is equal to zero or larger than the macro : " \
"SECUREC_STRING_MAX_LEN.", \
__FILE__, \
__LINE__); \
break; \
case ERANGE_AND_RESET: \
elog(ERROR, \
"%s : %d : The parameter destMax is too small or parameter count is larger than macro " \
"parameter SECUREC_STRING_MAX_LEN. The second case only occures in functions " \
"strncat_s/strncpy_s.", \
__FILE__, \
__LINE__); \
break; \
case EOVERLAP_AND_RESET: \
elog(ERROR, \
"%s : %d : The destination buffer and source buffer are overlappe-", \
__FILE__, \
__LINE__); \
break; \
default: \
elog(ERROR, "%s : %d : Unrecognized return typ-", __FILE__, __LINE__); \
break; \
} \
} \
}

#else

#define securec_check(errno, charList, ...) \
{ \
if (errno == -1) { \
freeSecurityFuncSpace_c(static_cast(charList), ##__VA_ARGS__); \
printf("ERROR at %s : %d : The destination buffer or format is a NULL pointer or the invalid parameter " \
"handle is invoke-.\n", \
__FILE__, \
__LINE__); \
exit(1); \
} \
}

#endif

securec_check_ss 主要用于 sprintf_s 和 scanf_s 两个格式化函数,因为他们失败是返回-1 而不是 EOK(0)。失败场景下同样通过 elog(ERROR) 进行长跳转。

#define securec_check_ss(errno, charList, ...) \
{ \
if (errno == -1) { \
freeSecurityFuncSpace(static_cast(charList), ##__VA_ARGS__); \
elog(ERROR, \
"%s : %d : The destination buffer or format is a NULL pointer or the invalid parameter handle is " \
"invoke-", \
__FILE__, \
__LINE__); \
} \
}

securec_check_c/securec_check_ss_c 和不带 _c 的功能基本一致,不过这两个宏是供非内核(如 gsql 等工具)的代码使用的。因为非内核场景没有长跳转功能,失败场景下通过 printf 打印日志,失败直接 exit 退出。

代码: openGauss-server/src/include/securec_check.h

#define securec_check_c(errno, str1, str2) \
{ \
errno_t e_for_securec_check_c = (errno); \
if (EOK != e_for_securec_check_c) { \
freeSecurityFuncSpace_c(str1, str2); \
switch (e_for_securec_check_c) { \
case EINVAL: \
printf("ERROR at %s : %d : The destination buffer is NULL or not terminate- The second case " \
"only occures in function strcat_s/strncat_s.\n", \
__FILE__, \
__LINE__); \
break; \
case EINVAL_AND_RESET: \
printf("ERROR at %s : %d : The source buffer is NULL.\n", __FILE__, __LINE__); \
break; \
case ERANGE: \
printf("ERROR at %s : %d : The parameter destMax is equal to zero or larger than the macro : " \
"SECUREC_STRING_MAX_LEN.\n", \
__FILE__, \
__LINE__); \
break; \
case ERANGE_AND_RESET: \
printf("ERROR at %s : %d : The parameter destMax is too small or parameter count is larger than " \
"macro parameter SECUREC_STRING_MAX_LEN. The second case only occures in functions " \
"strncat_s/strncpy_s.\n", \
__FILE__, \
__LINE__); \
break; \
case EOVERLAP_AND_RESET: \
printf("ERROR at %s : %d : The destination buffer and source buffer are overlappe-\n", \
__FILE__, \
__LINE__); \
break; \
default: \
printf("ERROR at %s : %d : Unrecognized return typ-\n", __FILE__, __LINE__); \
break; \
} \
exit(1); \
} \
}

#define securec_check_ss_c(errno, str1, str2) \
do { \
if (errno == -1) { \
freeSecurityFuncSpace_c(str1, str2); \
printf("ERROR at %s : %d : The destination buffer or format is a NULL pointer or the invalid parameter " \
"handle is invoke-.\n", \
__FILE__, \
__LINE__); \
exit(1); \
} \
} while (0)

需要注意的是,绝大部分场景下安全函数都不应该执行失败。如果执行过程中,出现安全函数执行失败的报错,大概率是程序 bug,需要分析定位。

安全函数使用注意事项

以下内容复制自 openGauss 安全编码说明: https://gitee.com/opengauss/security/blob/master/guide/SecureCoding.md

必须检查安全函数返回值,并进行正确的处理

原则上,如果使用了安全函数,需要进行返回值检查。如果返回值表示错误,那么本函数一般情况下应该立即返回,不能继续执行。

安全函数有多个错误返回值,如果安全函数返回失败,在本函数返回前,根据产品具体场景的不同,执行以下一个或多个措施:

  • 记录日志
  • 返回错误
  • 调用 abort 立即退出程序

【正例】

bool ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...
if (destBuff == NULL || destMax == 0) {
return false; // 返回失败
}
errno_t ret = memcpy_s(destBuff, destMax, src, srcLen);
if (ret != EOK) {
Log("memcpy_s failed, err = %d\n", err);
return false; // 返回失败
}
...
return true;
}

【例外 1】

在安全函数返回值检查错误处理代码中,如果又调用了安全函数,可以不进行返回值检查。

例如,在以下代码中,调用 strcpy_s 失败后,错误处理代码试图记录日志,在错误处理代码内又调用了 sprintf_s 安全函数,此时不需要进行返回值检查。(程序员需要仔细检查该语句不会产生安全问题)

errno_t ret = strcpy_s(dest, sizeof(dest), src);
if (ret != EOK) {
char buff[MAX_BUFF];
sprintf_s(buff, sizeof(buff), ...);
Log(buff);
return -1;
}
...

【影响】

忽略检查安全函数返回值,可能会导致程序执行了错误流程以及处理了错误的数据。

正确设置安全函数中的 destMax 参数

【描述】 安全函数的 destMax 参数设置应当准确,有效。

【反例】

以下代码,destBuff 跨越到不可访问内存,属于误用:

#define BUFF_SIZE 100
...
char destBuff[BUFF_SIZE];
char *src = ...
size_t srcLen = ...
memcpy_s(destBuff, 0x7fffffff, src, srcLen); // 不符合

destMax 参数设置原则

(为聚焦于说明 destMax 的用法,示例代码中省略了安全函数的返回值检查以及其他检查)

1. destBuff 为 char destBuff[BUFF_SIZE] 形式的局部变量的情况

1.1 使用时,destMax 必须设置为 sizeof(destBuff) 或 BUFF_SIZE

【反例】

#define BUFF_SIZE 100
...
char destBuff[BUFF_SIZE];
char *src = ...
size_t srcLen = ...
...
memcpy_s(destBuff, 100, src, srcLen); // 不符合
memcpy_s(destBuff, srcLen, src, srcLen); // 不符合

【正例】

#define BUFF_SIZE 100
...
char destBuff[BUFF_SIZE];
char *src = ...
size_t srcLen = ...
char strDest[BUFF_SIZE];
...
memcpy_s(destBuff, sizeof(destBuff), src, srcLen); // 符合
memcpy_s(destBuff, BUFF_SIZE, src, srcLen); // 符合
sprintf_s(strDest, sizeof(strDest), "Hello, world"); // 符合
...
memset_s(strDest, BUFF_SIZE, 0, BUFF_SIZE); // 符合
scanf_s("%s", strDest, sizeof(strDest)); // 符合

1.2 如果 destBuff 作为参数跨函数传递,必须将 destBuff 的实际大小作为参数进行传递

【反例】

#define BUFF_SIZE 100

int Foo(void)
{
char destBuff[BUFF_SIZE];
...
ParseBuff(destBuff, 100); // 不符合
ParseBuff2(destBuff); // 不符合:必须增加destMax参数
...
}

int ParseBuff(char *destBuff, size_t destMax)
{
char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, BUFF_SIZE, src, srcLen); // 不符合
memcpy_s(destBuff, sizeof(destBuff), src, srcLen); // 不符合
memcpy_s(destBuff, 100, src, srcLen); // 不符合
memcpy_s(destBuff, srcLen, src, srcLen); // 不符合
...
}

【正例】

#define BUFF_SIZE 100

int Foo(void)
{
char destBuff[BUFF_SIZE];
...
ParseBuff(destBuff, BUFF_SIZE); // 符合:传递BUFF_SIZE
...
}

int ParseBuff(char *destBuff, size_t destMax)
{
char *src = ...
size_t srcLen = ...
memcpy_s(destBuff, destMax, src, srcLen); // 符合
...
}

2. destBuff 为动态分配的堆内存的情况

使用时,destMax 应设置为当初分配时的内存大小

【反例】

#define BUFF_SIZE 100
...
unsigned char *destBuff = (unsigned char *)malloc(BUFF_SIZE);
if (destBuff == NULL) {
... // 错误处理
}
...
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, 100, src, srcLen); // 不符合
memcpy_s(destBuff, srcLen, src, srcLen); // 不符合

【正例】

#define BUFF_SIZE 100
...
unsigned char *destBuff = (unsigned char *)malloc(BUFF_SIZE);
if (destBuff == NULL) {
... // 错误处理
}
...
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, BUFF_SIZE, src, srcLen); // 符合

2.1 如果 destBuff 的大小通过动态计算获得

【反例】

...
size_t destMax = ...
...
if (destMax > BUFF_SIZE) {
...
}
unsigned char *destBuff = (unsigned char *)malloc(destMax);
...
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, srcLen, src, srcLen); // 不符合

【正例】

...
size_t destMax = ...
...
if (destMax > BUFF_SIZE) {
...
}
unsigned char *destBuff = (unsigned char *)malloc(destMax);
...
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, destMax, src, srcLen); // 符合

2.2 如果 destBuff 的大小与 srcLen 相同

【正例】

...
unsigned char *src = ...
size_t srcLen = ...
...
unsigned char *destBuff = (unsigned char *)malloc(srcLen);
if (destBuff == NULL) {
... // 错误处理
}
...
memcpy_s(destBuff, srcLen, src, srcLen); // 符合

2.3 如果 destBuff 作为参数跨函数传递,必须将 destBuff 的实际大小作为参数进行传递

【反例】

int Foo(void)
{
...
size_t destMax = ...
...
unsigned char *destBuff = (unsigned char *)malloc(destMax);
if (destBuff == NULL) {
... // 错误处理
}

ParseBuff(destBuff); // 不符合:未传递destBuff的大小
...
}

int ParseBuff(unsigned char *destBuff)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, srcLen, src, srcLen); // 不符合
...
}

【反例】

int Foo(void)
{
...
size_t destMax = ...
...
unsigned char *destBuff = (unsigned char *)malloc(destMax);
if (destBuff == NULL) {
... // 错误处理
}

ParseBuff(destBuff, destMax);
...
}

int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, srcLen, src, srcLen); // 不符合:未使用正确的destMax
...
}

【正例】

int Foo(void)
{
...
size_t destMax = ...
...
unsigned char *destBuff = (unsigned char *)malloc(destMax);
if (destBuff == NULL) {
... // 错误处理
}

ParseBuff(destBuff, destMax); // 符合
...
}

int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, destMax, src, srcLen); // 符合
...
}

3. destBuff 为 struct 结构的局部变量的情况

使用时,destMax 必须设置为 sizeof(变量名称) 或 sizeof(结构名称)。

【反例】

typedef struct {
int a;
int b;
int c;
} SomeStru;
...
SomeStru destBuff;
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(&destBuff, 12, src, srcLen); // 不符合
memcpy_s(&destBuff, srcLen, src, srcLen); // 不符合

【正例】

typedef struct {
int a;
int b;
int c;
} SomeStru;
...
SomeStru destBuff;
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(&destBuff, sizeof(destBuff), src, srcLen); // 符合
memcpy_s(&destBuff, sizeof(SomeStru), src, srcLen); // 符合

4. struct 结构作为子函数参数的情况

如果参数类型是结构体指针,可以不传递 destMax 参数,否则必须将 destBuff 的实际大小作为参数进行传递。

【反例】

typedef struct {
int a;
int b;
int c;
} SomeStru;
...
int Foo(void)
{
SomeStru destBuff;

ParseBuff((unsigned char *)&destBuff, 12); // 不符合
ParseBuff2((unsigned char *)&destBuff); // 不符合,必须传递DestMax
ParseBuff3(&destBuff);
}

int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, sizeof(destBuff), src, srcLen); // 不符合
memcpy_s(destBuff, sizeof(SomeStru), src, srcLen); // 不符合
memcpy_s(destBuff, 12, src, srcLen); // 不符合
memcpy_s(destBuff, srcLen, src, srcLen); // 不符合
...
}

// 不符合:unsigned char *作为参数时,必须传递destMax
int ParseBuff2(unsigned char *destBuff)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, sizeof(destBuff), src, srcLen); // 不符合
memcpy_s(destBuff, sizeof(SomeStru), src, srcLen); // 不符合
memcpy_s(destBuff, 12, src, srcLen); // 不符合
memcpy_s(destBuff, srcLen, src, srcLen); // 不符合
...
}

// 函数的参数类型是结构体指针
int ParseBuff3(SomeStru *destBuff)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, sizeof(destBuff), src, srcLen); // 不符合
memcpy_s(destBuff, 12, src, srcLen); // 不符合
memcpy_s(destBuff, srcLen, src, srcLen); // 不符合
...
}

【正例】

typedef struct {
int a;
int b;
int c;
} SomeStru;
...
int Foo(void)
{
SomeStru destBuff;

ParseBuff((unsigned char *)&destBuff, sizeof(destBuff)); // 符合
ParseBuff((unsigned char *)&destBuff, sizeof(SomeStru)); // 符合
ParseBuff3(&destBuff); // 符合:结构体指针作为函数参数时,不需要传递destMax
}

int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, destMax, src, srcLen); // 符合
...
}

// 结构体指针作为函数参数时,不需要传递destMax
int ParseBuff3(SomeStru *destBuff)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, sizeof(SomeStru), src, srcLen); // 符合
...
}

5. struct 结构数组作为子函数参数的情况

如果参数类型是结构体指针,必须指定数组的数量,否则必须将 destBuff 的实际大小作为参数进行传递。

【反例】

typedef struct {
int a;
int b;
int c;
} SomeStru;
...
int Foo(void)
{
size_t count = ...
...
SomeStru *destBuff = (SomeStru *)malloc(sizeof(SomeStru) * count);
if (destBuff == NULL) {
... // 错误处理
}
...
ParseBuff((unsigned char *)destBuff, sizeof(SomeStru) * count);
ParseBuff2((unsigned char *)destBuff); // 不符合
}

int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, srcLen, src, srcLen); // 不符合
}

// 不符合:unsigned char *作为参数时,必须传递destMax
int ParseBuff2(unsigned char *destBuff)
{
...
}

【正例】

typedef struct {
int a;
int b;
int c;
} SomeStru;
...
int Foo(void)
{
size_t count = ...
...
SomeStru *destBuff = (SomeStru *)malloc(sizeof(SomeStru) * count);
if (destBuff == NULL) {
... // 错误处理
}
...
ParseBuff((unsigned char *)destBuff, sizeof(SomeStru) * count); // 符合
ParseBuff2(destBuff, count); // 符合
}

// 符合:非结构体指针时,传递destBuff的实际大小
int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, destMax, src, srcLen); // 符合
}

// 符合:结构体指针时,必须传递结构体数组数量,如:此处count为SomeStru的数量
int ParseBuff2(SomeStru *destBuff, size_t count)
{
unsigned char *src = ...
size_t srcLen = ...
...
memcpy_s(destBuff, sizeof(SomeStru) * count, src, srcLen); // 符合
}

6. destBuff 为 struct 结构中的成员变量的情况

destMax 必须设置为该成员变量的实际大小,而不是整个结构体大小。

【反例】

#define BUFF_SIZE 100
typedef struct {
int a;
int b;
int c;
unsigned char buffer[BUFF_SIZE];
} SomeStru;
...

SomeStru destBuff;
unsigned char *src = ...
size_t srcLen = ...
memcpy_s(destBuff.buffer, sizeof(destBuff), src, srcLen); // 不符合
memcpy_s(destBuff.buffer, 100, src, srcLen); // 不符合
memcpy_s(destBuff.buffer, srcLen, src, srcLen); // 不符合

【正例】

#define BUFF_SIZE 100
typedef struct {
int a;
int b;
int c;
unsigned char buffer[BUFF_SIZE];
} SomeStru;
...

SomeStru destBuff;
unsigned char *src = ...
size_t srcLen = ...
memcpy_s(destBuff.buffer, sizeof(destBuff.buffer), src, srcLen); // 符合
memcpy_s(destBuff.buffer, BUFF_SIZE, src, srcLen); // 符合

7. destBuff 为变长 struct 结构中的变长成员变量

destMax 必须设置为该成员变量的实际大小,结构体中必须有描述变长部分长度的成员变量。

【反例】

typedef struct {
int a;
int b;
int c;
size_t buffLen;
unsigned char buff[0];
} SomeStru;
...
size_t buffLen = ...
unsigned char *src = ...
size_t srcLen = ...
SomeStru *destBuff = (SomeStru *)malloc(sizeof(SomeStru) + buffLen);
...
destBuff->buffLen = buffLen;
...
memcpy_s(destBuff->buff, srcLen, src, srcLen); // 不符合

【正例】

typedef struct {
int a;
int b;
int c;
size_t buffLen;
unsigned char buff[0];
} SomeStru;
...
size_t buffLen = ...
unsigned char *src = ...
size_t srcLen = ...
SomeStru *destBuff = (SomeStru *)malloc(sizeof(SomeStru) + buffLen);
...
destBuff->buffLen = buffLen;
...
memcpy_s(destBuff->buff, destBuff->buffLen, src, srcLen); // 符合

如果要将变长成员变量作为子函数的参数进行传递,必须将 buffLen 作为参数一起传递。具体用例“6. destBuff 为 struct 结构中的成员变量的情况”。

8. struct 结构中的成员变量作为子函数参数的情况

destMax 必须设置为该成员变量的实际大小,而不是整个结构体大小。

【反例】

#define BUFF_SIZE 100
typedef struct {
int a;
int b;
int c;
unsigned char buffer[BUFF_SIZE];
} SomeStru;

int Foo(void)
{
...
SomeStru destBuff;

ParseBuff(destBuff.buffer, sizeof(destBuff)); // 不符合:入参大小有误
ParseBuff2(destBuff.buffer); // 不符合:函数入参缺少buffer的实际大小
...
}

int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, BUFF_SIZE, src, srcLen); // 不符合
memcpy_s(destBuff, sizeof(SomeStru), src, srcLen); // 不符合
memcpy_s(destBuff, srcLen, src, srcLen); // 不符合
...
}

int ParseBuff2(unsigned char *destBuff)
{
...
}

【正例】

#define BUFF_SIZE 100
typedef struct {
int a;
int b;
int c;
unsigned char buffer[BUFF_SIZE];
} SomeStru;

int Foo(void)
{
...
SomeStru destBuff;
ParseBuff(destBuff.buffer, BUFF_SIZE); // 符合:入参是该成员变量的实际大小
ParseBuff(destBuff.buffer, sizeof(destBuff.buffer)); // 符合
...
}

int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...
memcpy_s(destBuff, destMax, src, srcLen); // 符合
...
}

9. struct 结构中的 struct 结构成员变量作为子函数参数的情况

destMax 必须设置为该成员变量的实际大小,而不是整个结构体大小。以下面 struct 结构体为例:

#define BUFF_SIZE 100
typedef struct {
int a1;
int b1;
int c1;
} SomeStru1;

typedef struct {
int a2;
int b2;
int c2;
} SomeStru2;

typedef struct {
SomeStru1 s1;
int a;
int b;
int c;
unsigned char buffer[BUFF_SIZE];
SomeStru2 s2;
} SomeStru;

【反例】

int Foo(void)
{
...
SomeStru destBuff;

ParseBuff((unsigned char *)&destBuff.s2, sizeof(destBuff)); // 不符合:xx
ParseBuff((unsigned char *)&destBuff.s1, sizeof(destBuff)); // 不符合:xx
...
}

int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, BUFF_SIZE, src, srcLen); // 不符合:xx
memcpy_s(destBuff, sizeof(SomeStru), src, srcLen); // 不符合:xx
memcpy_s(destBuff, srcLen, src, srcLen); // 不符合:xx
}

int ParseBuff2(SomeStru2 *destBuff)
{
unsigned char *src = ...
size_t srcLen = ...

memcpy_s(destBuff, srcLen, src, srcLen); // 不符合:xx
}

【正例】

int Foo(void)
{
...
SomeStru destBuff;
ParseBuff((unsigned char *)&destBuff.s2, sizeof(destBuff.s2)); // 符合
ParseBuff((unsigned char *)&destBuff.s2, sizeof(SomeStru2)); // 符合
ParseBuff2(&destBuff.s2); // 符合
...
}

int ParseBuff(unsigned char *destBuff, size_t destMax)
{
unsigned char *src = ...
size_t srcLen = ...
memcpy_s(destBuff, destMax, src, srcLen); // 符合
...
}

int ParseBuff2(SomeStru2 *destBuff)
{
unsigned char *src = ...
size_t srcLen = ...
memcpy_s(destBuff, sizeof(SomeStru2), src, srcLen); // 符合
...
}

10.对 destBuff 的局部范围进行操作

原则上,destMax 必须设置为 destBuff 操作范围的内存大小,如果计算过程比较复杂,允许将 destMax 设置为 destBuffer 最初分配的大小,例如:

...
int destMax = ...
...
if (destMax ...) {
...
}
// 以下代码分别将src1和src2复制到destBuff进行拼接
unsigned char *destBuff = (unsigned char *)malloc(destMax);
if (destBuff == NULL) {
...
}
...
unsigned char *src1 = ...
size_t srcLen1 = ...
unsigned char *src2 = ...
size_t srcLen2 = ...
...
// count为外部传入的src1中的整数元素最大个数
if (count > ...) {
...
}
size_t offset = sizeof(int) * count;
...
if (offset >= destMax) {
...
}
memcpy_s(destBuff, offset, src1, srcLen1); // 符合
memcpy_s(destBuff, srcLen1, src1, srcLen1); // 不符合
memcpy_s(destBuff + offset, destMax - offset, src2, srcLen2); // 符合
memcpy_s(destBuff + offset, srcLen2, src2, srcLen2); // 不符合

// 将destMax设置为destBuffer最初分配的大小,不建议使用
memcpy_s(destBuff, destMax, src1, srcLen1);

【影响】

安全函数预防缓冲区溢出的前提是正确设置 deatMax 参数,如果该参数不正确,则可能导致缓冲区溢出,在某些情况下可以造成任意代码执行漏洞。

禁止封装安全函数

【描述】 保留所有安全检查和返回值信息的封装,不必要的增加了函数调用开销;对入参检查及返回值进行修改的封装,丢失了安全函数的部分安全特性。因此安全函数不允许进行封装。

以函数封装的形式重新封装安全函数或不安全函数时,忽略安全函数的 destMax 参数,或用 count 参数直接代替 destMax 参数,具有额外的安全风险。

【反例】 错误示例 1:使用类似不安全函数的接口封装安全函数,destMax 与 count 参数使用相同参数

void *XXX_memcpy(void *dest, const void *src, size_t count)
{
...
memcpy_s(dest, count, src, count);
...
}

错误示例 2:使用类似安全函数的接口封装安全函数,调用安全函数时,忽略了 destMax 入参,调用安全函数时 destMax 与 count 参数使用相同参数。

errno_t XXX_memcpy_s(void *dest, size_t destMax, const void *src, size_t count)
{
...
memcpy_s(dest, count, src, count);
...
}

错误示例 3:使用类似安全函数的名字的函数,但是参数与安全函数不同

errno_t XXX_memcpy_s(void *dest, const void *src, size_t count)
{
...
}

错误示例 4:使用类似安全函数的接口封装不安全函数,忽略了 destMax 参数

errno_t XXX_memcpy_s(void *dest, size_t destmax, const void *src, size_t count)
{
...
memcpy(dest, src, count);
...
}

错误示例 5:用函数实现自定义不安全函数

errno_t XXX_strncpy(char *dest, size_t destMax, const char *src)
{
...
}

【影响】

不正确的封装或自实现安全函数,使安全函数的安全增强功能丧失,可能导致缓冲区溢出,在某些情况下可以造成任意代码执行漏洞。

禁止用宏重命名安全函数

【描述】 使用宏重命名安全函数不利于静态代码扫描工具(非编译型)定制针对安全函数误用的规则,同时,由于命名风格多样,也不利于提示程序员函数的真实用途,容易造成对代码的误解及重命名安全函数的误用。重命名安全函数不会改变安全函数本身的检查能力。

错误示例 1:代码中未直接调用安全函数,而是以宏的方式调用安全函数

#define XXX_memcpy_s memcpy_s
#define SEC_MEM_CPY memcpy_s
#define XX_memset_s(dst, dstMax, val, n) memset_s((dst), (dstMax), (val), (n))

以宏的方式重定义安全函数时,忽略安全函数的 destMax 参数,或用 count 参数直接代替 destMax 参数,或者用不安全函数仿冒安全函数,这些编程方式具有额外的安全风险。

错误示例 2:使用类似不安全函数接口的宏,重定义安全函数,destMax 与 count 参数使用相同参数

#define XXX_memcpy(dst, src, count) memcpy_s(dst, count, src, count)

错误示例 3:使用类似安全函数接口的宏,重定义安全函数,destMax 与 count 参数使用相同参数

#define XXX_memcpy_s(dst, destMax, src, count) memcpy_s(dst, count, src, count)

错误示例 4:使用类安全函数接口的宏,重定义不安全函数,忽略了 destMax 参数

#define XXX_memcpy(dst, destMax, src, count) memcpy(dst, src, count)

错误示例 5:使用类似安全函数的名字的宏或者函数,但是参数与安全函数不同

#define XXX_memcpy_s(dst, src, count) ...

错误示例 6:宏定义伪安全函数,名称为安全函数,对应的函数实际不是安全函数

#define my_snprintf_s snprintf

错误示例 7:用宏实现自定义不安全函数

#define XXX_strncpy(dst, destMax, src) ...

【影响】

不正确的封装或自实现安全函数,使安全函数的安全增强功能丧失,可能导致缓冲区溢出,在某些情况下可以造成任意代码执行漏洞。

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论