术语定义
- 规则:编程时必须遵守的约定。
- 建议:编程时必须加以考虑的约定。
- 说明:对此规则/建议进行必要的解释。
- 错误示例:对此规则/建议从反面给出例子。
- 推荐做法:对此规则/建议从正面给出例子。
通用规则
规则1:对外部输入进行校验
说明:软件最为普遍的缺陷就是对来自客户端或者外部环境的数据没有进行正确的合法性校验。这种缺陷可以导致几乎所有的程序弱点,例如Dos、内存越界、命令注入、SQL注入、缓冲区溢出、数据破坏、文件系统攻击等。这些不可信数据可能来自:
- 用户输入
- 外部调用的参数
- 进程间的通信数据
- 网络连接(甚至是一个安全的连接)、
- 用户态输入(对于内核程序)
- 上层应用(业务)输入
当这些不可信输入用于如下场景时(包括但不局限于),需要校验其合法性:
- 作为循环条件 – 可能会引发缓冲区溢出、内存越界读/写、死循环等问题。
- 作为数组下标 – 可能导致超出数组上限,从而造成非法内存访问。
- 作为内存偏移地址 – 指针偏移访问内存,可能造成非法内存访问,并可以造成进一步的危害,如任意地址读/写。
- 作为内存分配的尺寸参数 – 请参考规则
C3.1
、C3.2
和C4.5
。- 作为业务数据 – 如作为命令执行参数、拼装sql语句、拼接格式化字符串等,这会导致命令注入、SQL注入、格式化漏洞等问题。详细请参考规则
C2.1
、C5.2
和C6.3
。- 用于数据拷贝操作 – 当作为拷贝长度时,极易造成目标缓冲区溢出。详细请参考规则
C1.1
、C1.2
和C1.3
。- 影响代码逻辑 – 比如基于不可信输入做安全决策,影响代码逻辑走向。
- 会改变系统状 – 比如未加校验直接打开不可信路径,可能会导致目录遍历攻击,操作了攻击者无权操作的文件,使得系统被攻击者所控制。
输入校验可能包括如下内容(包括但不局限于):
- 校验数据长度
- 校验数据范围
- 校验数据类型和格式
- 校验输入只包含可接受的字符(可以采用『白名单』形式),尤其需要注意一些特殊情况下的特殊字符。了解更多关于特殊字符,可以参考附录A和附录B。
规则2:禁止在日志中保存口令、密钥
说明:在日志中不能保存口令和密钥,其中的口令包括明文口令和密文口令。对于敏感信息建议采取以下方法,
- 不打印在日志中;
- 若因为特殊原因必须要打印日志,则用
*
代替(不要显示出敏感信息的长度)。
规则3:及时清除存储在可复用资源中的敏感信息
说明
:存储在可复用资源中的敏感信息如果没有正确的清除则很有可能被低权限用户或者攻击者所获取和利用。因此敏感信息在可复用资源中保存应该遵循存储时间最短原则。可复用资源包括以下几个方面:
- 堆(heap)
- 栈(stack)
- 数据段(data segment)
- 数据库的映射缓存
存储口令、密钥的变量(包括加密后的变量)使用完后必须显式覆盖或清空
。
规则4:正确使用经过验证的安全的标准加密算法
说明:禁用私有算法或者弱加密算法(如DES,SHA1等),应该使用经过验证的、安全的、公开的加密算法。加密算法分为对称加密算法和非对称加密算法。推荐使用的
- 常用对称加密算法有:
- AES
- 推荐使用的常用非对称算法有:
- RSA
- 推荐使用的数字签名算法有:
- 数字签名算法(DSA)
- ECDSA
- 此外还有验证消息完整性的安全哈希算法(SHA256)等。基于哈希算法的口令安全存储必须加入盐值(参见
规则5
)。密钥长度符合最低安全要求:
- AES: 128位
- RSA: 2048位
- DSA: 2048位
规则5:基于哈希算法的口令安全存储必须加入盐值(salt)
说明:单向哈希是在一个方向上工作的哈希函数,从预映射的值很容易计算其哈希值,但要根据特定哈希值产生一个预映射的值却是非常困难的。单向哈希主要应用于加密、消息完整性校验、冗余校验等。
假如没有加入盐值,则加密原理是:密文=哈希算法(明文)
此时,若攻击者获取到密文,同时知道哈希算法,则就可以通过字典攻击来探测和获取口令。
加入盐值之后:密文= 哈希算法(明文+盐值)
其中盐值可以随机设置,这样即使相同的口令,但盐值不同,密文也不同,从而增加了口令的破解难度、增强安全性。
规则6:不要硬编码敏感信息
说明:
- 硬编码口令、服务器IP地址以及加密密钥等敏感信息可能会将这些信息暴露给攻击者。任何人都可以反编译并发现这些敏感信息。因此,除了一些特殊情况(例如在TPM环境下)之外,程序中禁止硬编码任何敏感信息。
- 硬编码敏感信息还会增加维护管理成本,当修改代码时,需要额外管理并适配这些修改。例如,要更改一个已经部署了的程序的硬编码口令,可能需要下发一个补丁。
规则7:不要在共享目录中创建临时文件
说明:程序员经常会在共享目录里创建临时文件。临时文件通常作为不需要或者不能驻留在内存中的数据的一种辅助存储方式,同时也可以作为与其它进程通过文件系统进行通信的一种方式。例如,一个进程会以一个公认的命名或者与合作进程协商好的名字在共享目录里创建临时文件,然后这些临时文件便可以在这些合作进程间共享信息。
但是,这是一个非常危险的操作。一个在共享目录里大家都知道名字的文件是很容被攻击者控制和操纵的。以下列出了几种可能的规避方法:
- 使用其它低级别进程间通信(IPC)机制,如使用sockes或者共享内存;
- 使用高级别IPC机制,如远程过程调用(remote procedure call);
- 使用一个安全的目录或者设置一个只能被程序应用实例访问的jail(确保同一平台下的多个应用程序实例不会产生竞争)。
IPC机制中有些需要使用临时文件,但是其它的不需要。例如,需要使用临时文件的IPC机制有POSIX的mmap()函数。而伯克利套接字(Berkeley Sockets)、POSIX本地IPC套接字和System V共享内存却不需要临时文件。因为共享目录的多用户属性使得它具有与生俱来的危险,因此,利用共享临时文件来实现IPC是不推荐的。
当2个以上或者一组用户对目录具有写权限时,其危险和欺骗性比少量文件的共享访问更为严重。因此,当确实需要在共享目录中创建临时文件时,必须满足如下条件:
- 创建不可预测的文件名称;
- 创建唯一的文件名称;
- 原子打开;
- 独占打开;
- 使用合适的权限打开;
- 程序退出前必须删除。
规则8:遵循最小权限原则
说明:程序在运行时可能需要不同的权限,但对于某一种权限不需要始终保留。例如,一个网络程序可能需要超级用户权限来捕获原始网络数据包,但是在执行数据报分析等其它任务时,则可能不需要相同的权限。因此程序在运行时只分配能完成其任务的最小权限。过高的权限可能会被攻击者利用并进行进一步的攻击。因此,权限在使用完毕后应该及时撤销。在撤销权限时,应该尤其注意以下两点:
- 撤销权限时应遵循正确的撤销顺序;
- 完成权限撤销操作后,应确保权限撤销成功。
C安全编程
字符串操作安全
规则C1.1:确保有足够的空间存储字符串的字符数据和’\0’结束符
说明:在分配内存或者在执行字符串复制操作时,除了要保证足够的空间可以容纳字符数据,还要预留’\0’结束符的空间,否则会造成缓冲区溢出。
错误示例1:拷贝字符串时,源字符串长度可能大于目标数组空间。
enum { BUFFERSIZE = 128 };int main(int argc, char *argv[]){ char dst[BUFFER_SIZE] = {0x00}; if (argc > 1) { strcpy(dst, argv[1]); /*【错误】当源字符串长度大于目标数组dst时,会发生缓冲区溢出 */ } return 0;}
推荐做法1:根据源字符串长度来为目标字符串分配空间。
int main(int argc, char *argv[]){ char *dst = NULL; size_t length = strnlen(argv[1], MAX_LEN); if (argc > 1) { dst = (char *)malloc(length + 1); /* 【修改】 确保目标字符串可以存储源数据 */ if(dst != NULL) { int ret = strcpy_s(dst, length + 1, argv[1]); /* 【修改】使用安全函数strcpy_s代替strcpy来拷贝字符串 */ /* 校验ret,确保安全函数执行成功 */ } } /* 使用完后释放内存 */ return 0;}
错误示例2:典型的差一错误。for循环将src中的数据拷贝到dst中,然而未考虑’\0’结束符写入数组的位置,经过循环后,’\0’会写越界,超出数组dst一个字节,从而造成缓冲区溢出和内存改写。
enum { ARRAY_SIZE = 32 };void func(void) { char dst[ARRAY_SIZE + 1] = {0x00}; char src[ARRAY_SIZE + 1] = {0x00}; size_t i = 0; for (i = 0; src[i]!= ‘\0’ && (i < (ARRAY_SIZE + 1)); ++i) /* 【错误】 结束符会写越界dst一个字节*/ { dst[i] = src[i]; } dst[i] = '\0'; /* 【错误】 结束符写越界*/}
推荐做法2:在赋值循环语句中考虑结束符的添加。
enum { ARRAY_SIZE = 32 };void func(void) { char dst[ARRAY_SIZE + 1] = {0x00}; char src[ARRAY_SIZE + 1] = {0x00}; size_t i = 0; for (i = 0; src[i] != ‘\0’ && (i < ARRAY_SIZE); ++i) /* 【修改】 考虑结束符的写入位置,不会越界 */ { dest[i] = src[i]; } dst[i] = '\0';}
错误示例3:在下面的例子中,name是可能来自用户输入、文件系统或者网络的字符串变量。代码中通过构造的一个文件名称来打开文件。然而,由于sprintf()函数没有对输入的数据进行校验,因此当name是一个非常大的字符串变量时,就会产生缓冲区溢出。
void func(const char *name){ char filename[NAME_SIZE + 1] = {0x00}; sprintf(filename, "%s.txt", name); /* 【错误】 当name的长度超过目标数组filename大小时,会发生缓冲区溢出 */}
推荐做法3:一个比较好的方法就是使用安全版本函数sprintf_s()。sprintf_s()会对入参进行校验,保证不会发生缓冲区溢出。
void func(const char *name){ char filename[NAME_SIZE + 1] = {0x00}; int ret = sprintf_s(filename, sizeof(filename), "%s.txt", name); /* 【修改】使用sprintf_s来避免缓冲区溢出 */ /* 校验ret,确保安全函数执行成功 */}
规则C1.2:字符串操作过程中确保字符串有’\0’结束符
说明:字符串结束与否是以’\0’作为标志的。没有正确地使用’\0’结束字符串可能导致字符串操作时发生缓冲区溢出。因此对于字符串或字符数组的定义、设置、复制等操作,要给’\0’预留空间,并保证字符串有’\0’结束符。
错误示例:下列代码中,在调用strncpy()之前ntca就被赋予了结束符。然而,接下来执行的strncpy()可能会将这个结束符覆写。
char ntca[NTCA_SIZE + 1];ntca[sizeof(ntca) - 1] = '\0';strncpy(ntca, source, sizeof(ntca)); /*【错误】 strncpy()不能保证字符串结尾含有结束符,因此可能将已经赋予的结束符覆写 */
推荐做法:正确的方法是依照程序员的目的而来的。如果程序员想要截断一个字符串并且保证目标字符串结尾含有结束符,那么可以使用如下方法。
char ntca[NTCA_SIZE + 1];int ret = strncpy_s(ntca, NTCA_SIZE + 1, source, NTCA_SIZE); /*【修改】 使用strncpy_s()来代替strncpy(),可以保证结尾含有结束符 *//* 校验ret,确保安全函数执行成功 */
规则C1.3:把数据复制到固定长度的内存前必须检查边界
说明:将未知长度的数据复制到固定长度的内存空间可能会造成缓冲区溢出,因此在进行复制之前应首先获取并检查数据长度,并且在任何情况下都要在明确目标缓冲区的长度之后再进行复制操作。
错误示例:输入消息长度不可预测,不加检查的复制会造成缓冲区溢出。
void MsgCopy(){ char dst[MAX_SIZE + 1] = {0x00}; char *temp = getInputMsg(); if(temp != NULL) { strcpy(dst,temp); /*【错误】当temp长度大于dst大小时,会产生缓冲区溢出 */ }}
推荐做法:
void MsgCopy(){ char dst[MAX_SIZE + 1] = {0x00}; char *temp = getInputMsg(); if(NULL == temp) return; size_t len = strlen(temp); if(len > MAX_SIZE) /* 【修改】 校验temp的长度是合法的 */ { return; } int ret = strcpy_s(dst, sizeof(dst), temp); /* 【修改】使用安全版本函数strcpy_s()来代替strcpy()来进行操作 */ /* 校验ret,确保安全函数执行成功 */}
规则C1.4:避免字符串/内存操作函数的源指针和目标指针指向内存重叠区
说明:内存重叠区是指一段确定大小及地址的内存区,该内存区被多个地址指针指向或引用,这些指针介于首地址和尾地址之间。使用函数拷贝内存重叠的对象可能导致未定义的行为,可被用来破坏数据的完整性。
错误示例1:sprintf_s()函数不当使用
void LogMessageItem(int error_type , char * error_msg){ size_t msg_length = strnlen(error_msg, MAX_LEN); int ret = sprintf_s(error_msg, msg_length, "%d:%s",error_type,error_msg); /* 【错误】 err_msg变量既是源缓冲区,又是目标缓冲区 */ /* 校验ret,确保安全函数执行成功 */ Log(error_msg);}
推荐做法1:使用不同的源和目标缓冲区来实现复制功能。
void LogMessageItem(int error_type, char * error_msg){ char tmp_msg[MAX_MESSAGE_SIZE + 1] = {0x00}; int ret = sprintf_s(tmp_msg, sizeof(tmp_msg), "%d:%s",error_type,error_msg); /* 【修改】分配另一块内存来避免内存重叠 */ /* 校验ret,确保安全函数执行成功 */ Log(tmp_msg);}
错误示例2:
unsigned char *p1 = GetCurrentMessage();unsighed char *p2 = p1 + KEY_FIELD_OFFSET; /* 【错误】 p1和p2存在重叠 */ int ret = memcpy_s(p2, MAX_SIZE, p1,KEY_FIELD_SIZE); /* 【错误】当p1和p2存在重叠时,memcpy_s()不能实现其功能 *//* 校验ret,确保安全函数执行成功 */
推荐做法2:使用 memmove_s 函数,源字符串和目标字符串所指内存区域可以重叠,但复制后目标字符串内容会被更改,该函数将返回指向目标字符串的指针。
unsigned char * p1 = GetCurrentMessage();unsigned char * p2 = p1 + KEY_FIELD_OFFSET;int ret = memmove_s(p2, MAX_SIZE, p1, KEY_FIELD_SIZE); /* 【修改】使用memmove_s来代替memcpy_s,来避免内存重叠带来的问题 *//* 校验ret,确保安全函数执行成功 */
memcpy_s
与memmove_s
的目的都是将N个字节的源内存地址的内容拷贝到目标内存地址中。但当源内存和目标内存存在重叠时,memcpy_s
不会实现其功能,而memmove_s
能正确地实施拷贝,但这也增加了一点点开销。
格式化输出安全
规则C2.1:禁止以用户输入来构造格式化字符串
说明:调用格式化函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。如果攻击者对一个格式化字符串可以部分或完全控制,将导致进程崩溃、查看栈的内容、改写内存、甚至执行任意代码等风险。这些格式化函数有:
- 格式化输出函数:printf(),fprintf(),sprintf(),snprintf(),vprintf(), vfprintf(),vsprintf(),vsnprintf(),asprintf()(GNU扩展函数),vasprintf()(GNU扩展函数)及相应宽字节版本;
- 格式化输入函数:scanf(),fscanf(),sscanf(),vscanf(),vsscanf(),vfscanf()及相应宽字节版本;
- 格式化错误消息函数:err(),verr(),errx(),verrx(),warn(),vwarn(),warnx(),vwarnx(),error(),error_at_line();
- 格式化日志函数:syslog(),vsyslog()。
备注:以上部分函数是禁用的,详细请参考规则C5.1。
错误示例: 代码中incorrect_password()函数的功能是当某个用户的用户名没有找到或者口令不正确时,展示一条错误消息。这个函数接受来自用户的参数user,而这个用户是未经过认证的,也就是不安全的外部输入。函数将user构造成一个告警展示信息,然后通过fprintf()将该信息打印到stderr中。
#include <stdio.h>#include <stdlib.h>#include <string.h>void incorrect_password(const char *user){ int ret = 0; /* 用户名称被限定在256个字节以内 */ static const char msg_format[] = "%s cannot be authenticated.\n"; size_t len = strlen(user) + sizeof(msg_format); if(len > MAX_LEN) { /* 处理错误 */ } char *msg = (char *)malloc(len); if (NULL == msg) { /* 处理错误*/ } ret = sprintf_s(msg, len, msg_format, user); if (ret < 0) { /* 处理错误 */ } else if (ret >= len) { /* 处理截断输出 */ } fprintf(stderr, msg); /* 【错误】格式化错误产生 */ free(msg);}
示例代码中首先计算了消息的长度,然后分配内存,接着利用sprintf_s()函数构造消息内容。因此消息内容中包含了msg_format的内容和用户的内容,当用户输入大量的格式符(如%s,%p等)后,fprintf()在执行时,会将msg作为一个格式化字符串来进行解析,而不是消息内容,也就是说msg不会被打印到stderr中,反而会将一些未知的数据打印其中,会引发程序崩溃等未定义的行为。这是一个非常严重的格式化漏洞。
推荐做法1:下列代码使用fputs()来代替fprintf()函数,fputs()会直接将msg的内容输出到stderr中,而不会去解析它。
void incorrect_password(const char *user){ int ret = 0; /*用户名称被限定在256个字节以内*/ static const char msg_format[] = "%s cannot be authenticated.\n"; size_t len = strlen(user) + sizeof(msg_format); if(len > MAX_LEN) { /* 处理错误 */ } char *msg = (char *)malloc(len); if (NULL == msg) { /* 处理错误 */ } ret = sprintf_s(msg, len, msg_format, user); if (ret < 0) { /* 处理错误 */ } else if (ret >= len) { /* 处理截断错误 */ } if (fputs(msg, stderr) == EOF) /* 【修改】使用fputs()函数代替fprintf() */ { /* 处理错误 */ } free(msg);}
推荐做法2:通过格式说明符“%s”将user以字符串的形式固定下来然后输出到stderr中。
void incorrect_password(const char *user){ static const char msg_format[] = "%s cannot be authenticated.\n"; /* 【修改】使用“%s”来限定msg的格式,使msg不会被解析 */ fprintf(stderr, msg_format, user); }
整数安全
规则C3.1:确保无符号整数运算时不会出现反转
说明:反转是指无法用无符号整数表示的运算结果将会根据该类型可以表示的最大值加1执行求模操作。常见的可能引起反转的操作符有:
+、-、*、++、--、+=、-=、*=、<<=、<<、-
来自于系统外部或其它不可信数据参与到上述运算中的情形,只要将运算结果用于以下之一(包括但不局限于)的用途,都应该添加校验以防止反转:
- 作为数组索引
- 指针运算
- 作为对象的长度或者大小
- 作为数组的边界
- 作为内存分配函数的实参
- 作为循环终止判定条件
- 作为拷贝长度
在上述校验场景中,若代码执行前能确定运算结果不会反转的,可以不作校验,如:
- 两个静态常量(compile-time constants)操作;
- 与0进行运算(除数不能为0);
- 任意类型的最大值减法(如UINT_MAX减去任意无符号数都是安全的);
- 任何变量乘1操作;
- 除法或者取余操作中,只要保证除数不为0;
- 右移运算时,右移位数不超过该无符号整数类型的精度,如UNIT_MAX >> x中,只要 0 <= x < 32就是合法的(假设unsigned int类型的精度是32位)。
错误示例:下列代码可能导致相加操作产生无符号数反转现象。
static int handlehdr_odc(aim_session_t * sess , aim_...){ ... unsigned int payloadlength = aimbs_get32(bs); /* payloadlength is read from an untrusted source*/ if(!(msg = calloc(1,payloadlength + 1))) /*【错误】payloadlength + 1未校验,可能反转为0,导致内存申请参数为0 */ /* potential overflow */ { ... } while(payloadlength - recvd) { if(payloadlength - recvd >= 1024) { i = aim_recv(conn->fd,&msg[recvd],1024); /*msg申请内存为0,导致读取数据消息失败 */ } else { ... } }}
推荐做法:在运算之前添加校验,确保不会产生无符号数反转。
static int handlehdr_odc(aim_session_t * sess , aim_...){ ... unsigned int payloadlength = aimbs_get32(bs); /* payloadlength is read from an untrusted source*/ if(payloadlength == 0 || payloadlength > (MAX_SIZE - 1)) /*【修改】确保payloadlength合法 */ { /* 错误处理 */ } if(!(msg = calloc(1,payloadlength + 1))) /* potential overflow */ { ... } while(payloadlength - recvd) { if(payloadlength - recvd >= 1024) { i = aim_recv(conn->fd,&msg[recvd],1024); } else { ... } }}
规则C3.2:确保有符号整数运算时不会出现溢出
说明:整数溢出是是一种未定义的行为,意味着编译器在处理有符号整数溢出时具有很多选择。以下是可能引起整数溢出的常见操作符:
+ 、-= 、- 、*=、*、/=、/、%=、%、<<=、++、<<、--、-、+=
来自于系统外部或其它不可信数据参与到上述运算中的情形,只要将运算结果用于以下之一(包括但不局限于)的用途,都应该添加校验以防止溢出:
- 作为数组索引
- 指针运算
- 作为对象的长度或者大小
- 作为数组的边界
- 作为内存分配函数的实参
- 作为循环终止判定条件
- 作为拷贝长度
错误示例:下列代码中两个有符号整数相加可能会产生溢出。
static int handlehdr_odc(aim_session_t * sess , aim_...){ ... char payloadlength = aimbs_get32(bs); /* payloadlength is read from an untrusted source*/ ... if(!(msg = calloc(1,payloadlength + 1))) /*【错误】payloadlength + 1未校验,当payloadlength为127时,则溢出为-128,calloc()函数会将其转为非常大的正整数,可导致内存申请失败 */ { ... } while(payloadlength - recvd) { if(payloadlength - recvd >= 1024) { i = aim_recv(conn->fd,&msg[recvd],1024); } else ... }}
推荐做法:在运算之前添加校验,确保不会产生有符号溢出。
static int handlehdr_odc(aim_session_t * sess , aim_...){ ... char payloadlength = aimbs_get32(bs); /* payloadlength is read from an untrusted source*/ if(payloadlength <= 0 || payloadlength > (CHAR_MAX - 1)) /*【修改】确保payloadlength合法,其中CHAR_MAX = 127 */ { /* 错误处理 */ } if(!(msg = calloc(1,payloadlength + 1))) { ... } while(payloadlength - recvd) { if(payloadlength - recvd >= 1024) { i = aim_recv(conn->fd,&msg[recvd],1024); } else ... }}
规则C3.3:确保整型转换时不会出现截断错误
说明: 将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。
截断错误会引起数据丢失,甚至可能引发安全问题。特别是将运算结果用于以下用途:作为数组索引、指针运算、作为对象的长度或者大小、作为数组的边界(如作为循环计数器)
错误示例:数据类型强制转化导致数据被截断。
void func(void) { signed long int s_a = LONG_MAX; signed char sc = (signed char)s_a; /* 【错误】不同类型强制转化会发生数据截断 */ /* ... */}
推荐做法:当不同数据类型强制转化时需要首先校验数据的范围,以确定是否会发生数据的丢失。
void func(void) { signed long int s_a = LONG_MAX; signed char sc; if ((s_a < SCHAR_MIN) || (s_a > SCHAR_MAX)) /* 【修改】进行校验以确保在进行类型转化时不会产生截断 */ { /* 处理错误 */ } else { sc = (signed char)s_a; /* Use cast to eliminate warning */ } /* ... */}
规则C3.4:确保有符号数和无符号数之间的转换符合预期
说明:有符号数和无符号数之间的转换包括:有符号数到无符号数的转换和无符号数到有符号数的转换。
将转换结果用于敏感用途(如作为数组索引、指针运算、对象的长度或大小、数组边界、内存分配函数实参等)时,一定要确保转换结果在自己的预期内,否则极易引发安全问题:
- 有符号数转换成无符号数:若有符号数为一个负数,那么转成无符号数时,将会是一个非常大的正整数。
- 无符号数转成有符号数:若无符号数为一个较大的数,那么转成有符号数时,可能会转换成一个负数。
错误示例1:使用有符号数,且未完整校验就作为内存申请函数实参。
DataPacket *packet = NULL;int numHeaders = 0; /* numHeader定义为有符号数 */PacketHeader *headers = NULL;...sock = AcceptSocketConnection();ReadPacket(packet, sock);numHeaders = packet->headers;if (numHeaders > 100) /* 只校验numHeader的上限值,未校验其下限值 */{ ExitError("too many headers!");}headers = malloc(numHeaders * sizeof(PacketHeader)); /* 【错误】 当numberHeader负数时,malloc申请的内存可能过大*/ParsePacketHeaders(packet, headers);
上述代码中,numHeaders被定义为signed int类型,且来自外部输入,校验时只校验了上限值,却未校验下限值,当其为负数时,便会产生问题。比如,若numHeaders为-1,sizeof(PacketHeader)为10,那么二者的运算为-10,当其作为malloc()入参时,malloc()会将其转化为一个非常大的无符号数(即4294967286),这样的入参,可能会让malloc()执行失败,导致程序崩溃。也可能因此申请过大内存,导致资源耗尽。
推荐做法1:添加校验,确保numHeaders不为负数,且malloc()入参符合预期。
DataPacket *packet = NULL;int numHeaders = 0; /* numHeader定义为有符号数 */PacketHeader *headers = NULL;...sock = AcceptSocketConnection();ReadPacket(packet, sock);numHeaders = packet->headers;if (numHeaders > 100 || numHeaders < 0) /* 【修改】添加校验,确保numHeaders不为负 */{ ExitError("too many headers!");}unsigned int mallocSize = numHeaders * sizeof(PacketHeader);if (mallocSize == 0|| mallocSize >= MAX_SIZE) /* 【修改】添加malloc入参校验,确保不为0,不超过最大申请值,即结果符合预期 */{ ExitError("malloc size error"); return;}headers = malloc(numHeaders * sizeof(PacketHeader)); ParsePacketHeaders(packet, headers);
错误示例2:使用有符号数,且未完整校验即作为拷贝长度,导致缓冲区溢出。
void main (int argc, char **argv) { char path[256] = {0x0}; char *input = NULL; int Length = 0; Length = GetUntrustedInt(); /* Length来自外部不可信输入,可能为负 */ if (Length > 256) /* 只校验上限,未校验下限值 */ { DiePainfully("go away!\n"); return; } input = GetUserInput("Enter pathname:"); strncpy(path, input, Length); /* 【错误】若length为负数,那么此处会被转换成一个非常大的正整数,导致path缓冲区溢出 */ path[255] = '\0'; /* 添加结束符 */ printf("Path is: %s\n", path);}
推荐做法2:添加校验,确保拷贝长度不为负数。
void main (int argc, char **argv) { char path[256] = {0x0}; char *input = NULL; int Length = 0; Length = GetUntrustedInt(); /* Length来自外部不可信输入,可能为负 */ if (Length > 256 || Length <= 0) /* 【修改】添加下限校验,确保不为负数 */ { DiePainfully("go away!\n"); return; } input = GetUserInput("Enter pathname:"); strncpy(path, input, Length); path[255] = '\0'; /* 添加结束符 */ printf("Path is: %s\n", path);}
错误示例3:无符号数隐式转换为有符号数,未经充分校验,导致非法内存访问。
char InitialChar[INITIAL_CHAR_SIZE] = {0x0};/* 初始化数组InitialChar*/...int findElement(unsigned short elementIndex){ short index = 0; char tmpChar; ... if(elementIndex >= INITIAL_CHAR_SIZE) /* 【错误】校验输入下标,确保在数组范围内。但是未考虑到index小于INITIAL_CHAR_SIZE且足够大的情况,可能导致index为负,导致内存非法访问 */ { return; } index = elementIndex; ... tmpChar = InitialChar[index]; /* 若index为负,则会造成内存非法访问 */ ... }
上述代码中,elementIndex为无符号数,只校验了是否满足数组上限值的要求,但是并未考虑该值足够大,同时小于数组上限值。比如,若elementIndex = 65533,而INITIAL_CHAR_SIZE为66000,那么elementIndex便会绕过校验,直接对index进行赋值,隐式的转为有符号数,此时index就是-3,而引用作为数组的下标,造成内存的非法访问。
推荐做法3:可以有多种修正方法,可以将index设置为unsigned short类型。也可以添加校验。
char InitialChar[INITIAL_CHAR_SIZE] = {0x0};/* 初始化数组InitialChar*/...int findElement(unsigned short elementIndex){ short index = 0; char tmpChar; ... index = elementIndex; /* 赋值 */ if(index >= INITIAL_CHAR_SIZE || index < 0) /* 【修改】校验输入下标,确保在数组范围内,且确保不为负 */ { return 0; } ... tmpChar = InitialChar[index]; ... }
规则C3.5:把整型表达式比较或赋值为一种更大类型之前必须用这种更大类型对它进行求值
说明:若一个整型表达式与一个很大长度的整数类型进行比较或者赋值为这种类型的变量,需要对该整型表达式的其中一个操作数类型显示转换为更大长度的整数类型,用这种更大的进行求值。
这里所说的更大整数类型是相对整型表达式的操作数类型而言,譬如整型表达式的操作数类型是unsigned int ,则该规则所说的更大类型是指 unsigned long long。
错误示例:下列代码为了防止无符号数反转,特意使用一个unsigned long long类型的变量alloc来存储cBlocks * 16得到的数据。
void *AllocBlocks(size_t cBlocks) { if (0 == cBlocks) { return NULL; } unsigned long long alloc = cBlocks * 16; /* 【错误】alloc < UINT_MAX会永远成立 */ return (alloc < UINT_MAX) ? malloc(cBlocks * 16) : NULL;}/*申请的内存使用后free*/
这段代码中包含2个错误。首先代码认为unsigned long long数据类型至少应该比size_t类型多4个字节。其次假设size_t代表一个32位数,而unsigned long long代表一个64位数值,那么2个32位数相乘,得到的结果仍然是一个32位数,因此最后一行代码alloc < UINT_MAX是永远成立的,判断语句无效。
推荐做法:将cBlocks提型至unsigned long long。
static_assert( CHAR_BIT *sizeof(unsigned long long) >= CHAR_BIT *sizeof(size_t) + 4, "Unable to detect wrapping after multiplication");void *AllocBlocks(size_t cBlocks){ if (0 == cBlocks) { return NULL; } unsigned long long alloc = (unsigned long long)cBlocks * 16; /*【修改】将cBlocks提型至unsigned long long类型 */ return (alloc < UINT_MAX) ? malloc(cBlocks * 16) : NULL;}/*申请的内存使用后free*/
需要注意的是,在该代码中只有当unsigned long long数据类型比size_t类型大于至少4个字节时,才能有效防止无符号反转。
建议C3.1:避免对有符号整数进行位操作符运算
说明:位操作符(~、>>、<<、&、^、|)应该只用于无符号整型操作数,因为有符号整数上的有些位操作的结果是由编译器所决定的,可能会出现出乎意料的行为或编译器定义的行为。
错误示例:对有符号数作位操作运算。
enum { BUFFER_SIZE = 4 };int rc = 0;int stringify = 0x80000000;char buf[BUFFER_SIZE] = {0x00};rc = sprintf_s(buf, sizeof(buf), "%u", stringify >> 24); /* 【不推荐】避免使用有符号数作位操作符运算 */if (rc == -1 || rc >= sizeof(buf)) { /* 处理错误 */}
代码中,stringify >> 24得到的结果为0xFFFFFF80或者写作4294967168。当转换成字符串时,值4294967168是非常巨大的,是不能全部存储到buf中去的,因此会被sprintf_s()函数将数据截断。
推荐做法:下列代码将stringify声明为无符号数据,从而就避免了数据过大带来的截断问题。
enum { BUFFER_SIZE = 4 };int rc = 0;unsigned int stringify = 0x80000000; /* 【修改】使用无符号数来执行位操作符运算 */char buf[BUFFER_SIZE] = {0x00};rc = sprintf_s(buf, sizeof(buf), "%u", stringify >> 24);if (rc == -1 || rc >= sizeof(buf)) { /* 处理错误 */}
内存管理安全
规则C4.1:禁止引用未初始化的内存
说明:有些函数如malloc()分配出来的内存是没有初始化的,可以使用memset_s()进行清零,或者使用calloc()进行内存分配,calloc()分配的内存是清零的(需要注意的是,calloc函数会多一点开销)。当然,如果后面需要对申请的内存进行全部赋值,就不要清零了,但要确保内存被引用前是被初始化的。此外,分配内存初始化,可以消除之前可能存放在内存中的敏感信息,避免敏感信息的泄露。
错误示例:如下代码没有对malloc()的result内存进行初始化,所以功能不正确。
int *CalcMetrixColomn( int **metrix ,int *param, size_t size ){ int *result = NULL; size_t bufsize = size * sizeof(int); ... /* 校验bufsize的合法性 */ result = (int *)malloc(bufsize); if(NULL == result) return NULL; for(int i = 0; i < size; i++) { for(int j = 0; j < size; j++) result[i] += metrix[i][j] * param[j]; /* 【错误】result没有被初始化 */ } ... return result;}/*申请的内存使用后free*/
推荐做法1:使用memset_s()对分配出来的内存清零。
int *CalcMetrixColomn(int **metrix ,int *param, size_t size){ int *result = NULL; size_t bufsize = size * sizeof(int); ... /* 校验bufsize的合法性 */ result = (int *)malloc(bufsize); if(NULL == result) return NULL; int ret = memset_s(result, bufsize, 0, bufsize); /*【修改】确保内存被初始化后才被引用*/ /* 校验ret,确保安全函数执行成功 */ for(int i = 0; i < size; i++) { for(int j = 0; j < size; j++) result[i] += metrix[i][j] * param[j]; } ... return result;}/*申请的内存使用后free*/
推荐做法2:使用calloc()函数来申请内存。
int *CalcMetrixColomn(int **metrix ,int *param, size_t size){ int *result = NULL; int i,j; ... result = (int *)calloc(size, sizeof(int)); /* 【修改】使用calloc函数来申请内存 */ if(NULL == result) return NULL; for(i = 0; i < size; i++) { for(j = 0; j < size; j++) result[i] += metrix[i][j] * param[j]; } ... return result;}/*申请的内存使用后free*/
规则C4.2:禁止访问已经释放的内存
说明:访问已经释放的内存,是很危险的行为,主要分为两种情况:
- 堆内存:一块内存释放了,归还内存池以后,就不应该再访问。因为这块内存可能已经被其他部分代码申请走,内容可能已经被修改;直接修改释放的内存,可能会导致其他使用该内存的功能不正常;读也不能保证数据就是释放之前写入的值。在一定的情况下,可以被利用执行恶意的代码。即使是对空指针的解引用,也可能导致任意代码执行漏洞。如果黑客事先对内存0地址内容进行恶意的构造,解引用后会指向黑客指定的地址,执行任意代码。
- 栈内存:在函数执行时,函数内局部变量的存储单元都可以在栈上创建,函数执行完毕结束时这些存储单元自动释放。如果返回这些已释放的存储单元的地址(栈地址),可能导致程序崩溃或恶意代码被利用。
错误示例1:下列代码中,ptr在执行ptr->next语句前就已经被释放,解引用一个已经释放了内存的指针,会导致未定义的行为。
struct node{ int value; struct node *next;};void free_list(struct node *head){ for (struct node *ptr = head; ptr != NULL; ptr = ptr ->next) /* 【错误】 解引用已经释放的内存 */ { free(ptr); }}
推荐做法1:释放ptr之前,将ptr->next存储在bptr中。
struct node{ int value; struct node *next;};void free_list(struct node *head){ struct node *; for (struct node *ptr = head; ptr != NULL; ptr = bptr) { bptr = ptr->next; /* 【修改】释放ptr前,见ptr->next进行保存 */ free(ptr); }}
错误示例2:返回函数局部变量。当函数返回时,局部变量name会自动释放。因此当调用者强行访问这块内存时,会发生未定义的行为。
char* GetName() { char name[STR_MAX] = {0x00}; fillInName(name); return name; /* 【错误】不能返回局部变量*/}
因此程序员在任何时候都不能返回函数的局部变量。
规则C4.3:禁止重复释放内存
说明:重复释放内存(double-free)会导致内存管理器出现问题。重复释放内存在一定情况下,有可能导致“堆溢出”漏洞,可以被用来执行恶意代码,具有很大的安全隐患。
错误示例:如下代码有可能重复释放内存ptr。
char * ptr = (char*)malloc(SIZE);if (prt) { free(ptr);}free(ptr); /* 【错误】可能会产生双重释放 */ptr = NULL;
推荐做法:确保申请的内存应该只释放一次。
char* ptr = (char*)malloc(SIZE);if (prt) { free(ptr); ptr = NULL;}else /* 【修改】确保不会发生双重释放 */{ ... free(ptr); ptr = NULL; ...}
规则C4.4:指针释放之后立即赋予新值
说明:悬挂指针可能会导致双重释放(double-free)以及访问已释放内存的危险。消除悬挂指针以及消除众多与内存相关危险的一个最为有效地方法就是当指针使用完后将其置NULL或者指向另一个合法对象。
需要注意的是,在一个特定的工程中,应当严格明确内存分配和释放分别是由调用者还是由被调用者负责。
错误示例:在如下代码中,message_type为一个整数,message为一个指向动态分配内存的指针。如果message在另外的代码片段中被引用,那么这段代码极有可能会发生双重释放的危险。
char *message = NULL;message = (char *)malloc(MESS_SIZE);if(!message){ /* 处理错误 */}int message_type;/* 初始化message和message_type */if (message_type == value){ /* 处理message_type */ free(message);}
推荐做法:利用free()去释放一个NULL指针不会产生任何行为。将message设置为NULL可以消除message被多重释放的可能。
char *message = NULL;message = (char *)malloc(MESS_SIZE);if(!message){ /* 处理错误 */}int message_type;/*初始化message和message_type */if (message_type == value){ /*处理message_type */ free(message); message = NULL; /* 【修改】 message释放后将其设置为NULL*/}
如果一个指针释放后能够马上离开作用域(如下代码),因为它已经不能被再次访问,因此可以无需对其设置新值。
void foo(void) { char *str; ... free(str); return;}
规则C4.5:必须对指定申请内存大小的整数值进行合法性校验
说明:申请内存时没有对指定的内存大小整数作合法性校验,会导致未定义的行为,主要有以下几种情况:
- 使用 0 字节长度去申请内存的行为是没有定义的,在引用内存申请函数返回的地址时会引发不可预知或不能立即发现的问题。对于可能出现申请0地址的情况,需要增加必要的判断,避免出现这种情况
- 使用负数长度去申请内存,负数会被当成一个很大的无符号整数,从而导致因申请内存过大而出现失败,造成拒绝服务。
- 申请内存过小,这可能导致后续的缓冲区溢出发生(详细可参考规则C1.1);
- 申请内存过大, 可能导致因申请内存过大而出现失败造成拒绝服务,或不必要的占用大量内存,影响系统可用性。
错误示例:下列代码进行内存分配时,没有对内存大小整数作合法性校验。
int *GetRandomArray(size_t size){ size_t *array = malloc(size * sizeof(size_t)); /* 【错误】未对malloc参数进行合法性校验 */ if(NULL == array) return NULL; for(size_t i = 0; i < size; i++) { array[i] = CreatRand(); } ...}...
推荐做法:调用malloc()之前,需要判断malloc()的参数是否合法,以避免出现申请内存过大而导致拒绝服务。
int *GetRandomArray(size_t size ){ size_t *array = NULL; if( 0 == size ||(MAX_SIZE/sizeof(size_t) <= size )) /* 【修改】校验malloc参数,确保不会超出预计范围 */ return NULL; array = malloc(size * sizeof(size_t)); if(NULL == array) return NULL; for(size_t i = 0; i < size; i++) { array[i] = CreatRand(); } ...}...
规则C4.6:禁止解引用空指针
说明:解引用空指针是一种未定义的行为。在很多平台上,解引用空指针可导致异常的程序终止。在一些特殊情况下,它可以被攻击者利用来执行任意代码。实际上,最为有效地防止空指针解引用的方法就是在指针使用前做非空校验(如下):
str = malloc(size + 1);if (NULL == str) { /* 处理分配错误 */}
错误示例:代码中,inputr_str被拷贝到动态分配的内存str中。然而当malloc()执行失败时会返回NULL给str,当str在memcpy_s()函数中被解引用时,程序会发生未定义的行为。
size_t size = strlen(input_str) + 1;str = (char *)malloc(size); /* 【错误】 没有检查malloc()的返回值 */int ret = memcpy_s(str, size, input_str, size);/* 校验ret,确保安全函数执行成功 */...free(str);str = NULL;
推荐方法:确保malloc()函数返回值不为NULL。
size_t size = strnlen(input_str, MAX_LEN) + 1;str = (char *)malloc(size);if (NULL == str) /* 【修改】确保malloc()的返回值不为NULL */{ /* 处理错误 */}errno_t rc = memcpy_s(str, size, input_str, size);/* 校验rc,确保安全函数执行成功 */...free(str);str = NULL;
注意:对于C++程序中的new操作符,直接申请完后判NULL是没有意义的,因为C++中,若new分配内存失败,默认是抛出异常的。
错误示例:对于new操作符错误的判NULL。
int *p = new int[size];if(NULL == p) /*【错误】若new分配失败,默认会直接抛出异常,不会执行此语句 */ return -1;
推荐做法1:通过捕捉异常检查new操作是否成功。
try{ int* p = new int[SIZE];}catch( const bad_alloc& e ){ return -1;}
推荐做法2:按照C++标准,可以采取抑制new抛出异常,然后采用传统方式判NULL。
int* p = new (std::nothrow) int[SIZE]; /* 【修改】抑制new抛出异常,这样如果 new 失败了,就不会抛出异常,而是返回空指针 */if ( NULL == p) /* 此时,该行语句有效 */ return -1;
规则C4.7:禁止使用realloc()函数
说明:realloc()是一个非常特殊的函数,其原型如下:void *realloc(void *ptr, size_t size)
,随着参数的不同,其行为也是不同。
- 当ptr与size均不为NULL时,该函数会重新调整内存大小,并将新的内存指针返回,并保证最小的size的内容不变;
- 参数ptr为NULL,但size不为0,那么行为等同于malloc(size);
- 参数size为0,则realloc的行为等同于free(ptr)。
由此可见,一个简单的C函数,却被赋予了3种行为,这不是一个设计良好的函数。虽然在编码中提供了一些便利性,但是却极易引发各种bug。
错误示例1:不当使用导致内存泄露。
void *ptr = realloc(ptr, NEW_SIZE); /* 【错误】当realloc()执行失败时会发生错误 */if (!ptr){ /* 错误处理 */}
这里就引出了一个内存泄露的问题,当realloc()分配失败的时候,会返回NULL。但是参数中的ptr的内存是没有被释放的,如果直接将realloc()的返回值赋给ptr,那么ptr原来指向的内存就会丢失,造成内存泄露。
错误示例2:下列代码认为当NEW_SIZE为0时,realloc()会返回一个合法的指针,后面使用free()去释放该内存。
void *new_ptr = realloc(old_ptr, NEW_SIZE); /* 【错误】不要使用realloc()函数 *//* 其它代码 */free(new_ptr);
由于错误的认识,不去检验NEW_SIZE是否为0,如果NEW_SIZE等于0,old_ptr就会被释放掉,并且会返回NULL。因此new_ptr就被置为NULL,如果new_ptr在别处被调用,就可能会发生未定义的行为(如程序崩溃)。
正确示例2:使用malloc()函数代替realloc()函数,并校验malloc()函数是否执行成功。
void *new_ptr = malloc(NEW_SIZE); /* 【修改】 使用malloc()函数代替realloc()函数 */if(NULL == new_ptr ){ /* 错误处理 */}int ret = memcpy_s(new_ptr, new_size, old_ptr, old_size);/* 校验ret,确保安全函数执行成功 *//* 申请的内存使用后free */
建议C4.1:避免使用alloca()函数申请内存
说明:POSIX和C99均未定义alloca()的行为,在不支持的平台上运行会有未定义的后果,且该函数在栈帧里申请内存,申请的大小可能越过栈的边界而无法预知。
错误示例:使用了alloca()从堆栈分配内存。
char *UnfixedMessage = NULL;UnfixedMessage = (char *)alloca(SINGLE_UNIT_SIZE); /* 【不推荐】不用使用alloc()函数 */if(NULL == UnfixedMessage) DoExit();
推荐做法:改用malloc()从堆分配内存。
char *UnfixedMessage = NULL;UnfixedMessage = (char *)malloc(SINGLE_UNIT_SIZE); /* 【修改】 使用malloc()函数来代替alloca()函数 */if(NULL == UnfixedMessage) DoExit();
禁用不安全函数
规则C5.1:禁止使用危险函数
说明:当有更加安全的函数存在时,应当禁止使用危险函数,而改用其安全版本。C标准的许多函数,没有检查目标缓冲区的大小,很容易引入缓冲区溢出的安全漏洞。并且,这些函数没有处理一些特殊情况(例如内存重叠),因此是不安全的,可能会导致意想不到的问题。
以下列出了这些危险函数:
- 内存拷贝函数:memcpy(), wmemcpy(), memmove(), wmemmove()
- 内存初始化函数:memset()
- 字符串拷贝函数:strcpy(), wcscpy(),strncpy(), wcsncpy()
- 字符串拼接函数:strcat(), wcscat(),strncat(), wcsncat()
- 字符串格式化输出函数:sprintf(), swprintf(), vsprintf(), vswprintf(), snprintf(), vsnprintf()
- 字符串格式化输入函数:scanf(), wscanf(), vscanf(), vwscanf(), fscanf(), fwscanf(),vfscanf(),vfwscanf(),sscanf(), swscanf(), vsscanf(), vswscanf()
- stdin流输入函数:gets()
这类函数是公认的危险函数,应禁止使用此类函数(微软从Windows Vista的开发开始就全面禁用了危险API)。除此之外,对于这些危险系统函数的简单封装、或者自定义的与这些函数功能类似的函数却没有进行足够输入校验的,也是禁用的。
最优选择:使用ISO/IEC TR 24731-1定义的字符串操作函数的安全版本,如strcpy_s()、strcat_s()、sprintf_s()、scanf_s()、gets_s() 等。这个版本的函数增加了以下安全检查:
- 检查源指针和目标指针是否为NULL;
- 检查目标缓冲区的最大长度是否小于源字符串的长度;
- 检查复制的源和目的对象是否重叠。
如果编译器不支持TR 24731-1,可以使用由相应软件平台实现安全版本的字符串操作函数。如VRP提供了VOS_xxx_safe版本的安全函数,推荐基于VRP的产品使用。
了解更多关于危险函数替换,可以参考附录C。
错误示例:使用不安全的函数。
int ProcessMessage(char * message){ ... char temp[MSG_MAX_SIZE + 1]= {0x00}; strcpy(temp,message); /* 【错误】不用使用strcpy()函数 */ ...}
示例代码中,temp长度是固定的MSG_MAX_SIZE + 1,而message的长度是不确定的,在message太大时就会发生缓冲区溢出。
推荐做法:使用带长度参数版本的函数或者自行实现安全版本,向目标缓冲区中复制指定长度的字符,截断超出限制的字符。
int ProcessMessage(char * message){ ... char temp[MSG_MAX_SIZE + 1] = {0x00}; int ret = strcpy_s(temp, sizeof(tem), message) /* 【修改】使用安全版本函数strcpy_s来代替不安全函数 */ /* 校验ret,确保安全函数执行成功 */ ...}
规则C5.2:禁止调用OS命令解析器执行命令或运行程序,防止命令注入
说明:命令解析器(如UNIX/Linux的bash、sh,Windows的CMD.exe)支持命令分隔符(请参考附录B),用于连续执行多个命令/程序。这是产生命令注入漏洞的根本原因。
C99函数system()/popen()的实现正是通过调用命令解析器来执行入参指定的程序/命令。如果system()/popen()的参数由用户的输入组成,恶意用户可以通过构造恶意输入,改变函数调用的行为。除非入参是硬编码的,否则禁止使用system()和popen()。替代方案可以参考如下:
在POSIX下可以使用exec系列函数。但是参数中必须禁止再次调用命令解析器(如/bin/sh)来执行命令。
#define CMD any_cmdexecl("/bin/sh", "sh", "-c", CMD, (VOS_CHAR *) 0);
上述代码中execl()函数调用“/bin/sh”来执行命令CMD,这样做与直接使用system()没有任何分别,同样会引起命令注入。
对于Windows系统,建议使用Win32 API CreateProcess()等与命令解释器无关的进程创建函数来替代。同样需要注意的是不要创建再次调用命令解析器的进程。
对于确实需要使用system()/popen()的场景,则必须做好输入校验,编写过滤函数,将命令字符串中的所有命令分隔符和特殊字符(参考附录B)进行过滤。
错误示例:直接调用system()函数执行用户命令。
void ProcessDirectory(char * input_dir ){ char command[COMMAND_SIZE + PATH_MAX + 1] = {0x00}; size_t len = strnlen(input_dir, PATH_MAX); int ret = strncpy_s(command, sizeof(command), “any_command”, COMMAND_SIZE); /* 校验ret,确保安全函数执行成功 */ ret = strncat_s(command, sizeof(command), input_dir, len); /* 校验ret,确保安全函数执行成功 */ system(command); /* 【错误】 因为参数不是硬编码的,所以不能使用system()函数 */ ...}
上述代码将用户输入作为参数,在这种情况下,一旦用户输入类似下面的恶意参数:
anyExe; useradd attacker
shell会将字符串 “anyExe; useradd attacker” 解释为两条独立的命令连续执行:
any_command anyExe
useradd attacker
这样攻击者通过注入了一条命令“useradd attacker”创建了一个新用户。这明显不是程序所希望的。
推荐做法1:使用不会调用命令解析器的函数来执行,在window下可使用CreateProcess()函数 。
void ProcessDirectory(char * input_dir ){ ... CreateProcess( "any_cmd",input_dir,NULL,NULL, FALSE,CREATE_DEFAULT_ERROR_MODE,NULL, NULL,NULL,NULL); /* 【修改】在windows下使用CreatProcess()函数来代替system() */ ...}
推荐做法2:使用不会调用命令解析器的函数来执行,在Linux下可使用exec系列函数。
void ProcessDirectory(char * input_dir ){ ... char *args[] = {"",input_dir,NULL}; char *envs[] = {NULL}; ... execve("any_cmd", args, envs) /* 【修改】在Linux下使用exec系列函数来代替system() */ ...}
文件输入/输出安全
规则C6.1:必须使用int类型来接收字符输入/输出函数的返回值
说明:字符输入/输出函数fgetc()、getc()和getchar()都从一个流读取一个字符,并把它以int值的形式返回。如果这个流到达了文件尾或者发生读取错误,函数返回EOF。fputc()、putc()、putchar()和ungetc()也返回一个字符或EOF。
如果这些I/O函数的返回值需要与EOF进行比较,不要将返回值转换为char类型。因为char是有符号8位的值,int是32位的值。如果getchar()返回的字符的ASCII值为0xFF,转换为char类型后将被解释为EOF。0xFF这个值被有符号扩展后是0xFFFFFFFF,刚好等于EOF的值。
错误示例:下列代码使用char类型来接收字符I/O的返回值,可能会导致返回值错误。
char buf[BUFFSIZE + 1] = {0x00};char c; /* 【错误】不用使用char类型来接收字符I/O的返回值 */size_t i = 0;c = getchar();while (c != ‘\n’ && c != EOF && i < BUFSIZE){ buf[i++] = c; c = getchar();}buf[i] = '\0'; /* terminate NTBS */
推荐做法:
char buf[BUFSIZE + 1] = {0x00};int c; /* 【修改】使用int类型来接收字符I/O的返回值 */int i = 0;c = getchar();while (c != ‘\n’ && c != EOF && i < BUFSIZE){ buf[i++] = c; c = getchar();}buf[i] = '\0'; /* terminate NTBS */
注意:对于sizeof(int) == sizeof(char)的平台,用int接收返回值也可能无法与EOF区分,这时要用feof()和ferror()检测文件尾和文件错误。
规则C6.2:创建文件时必须显式指定合适的文件访问权限
说明:创建文件时,如果不显式指定合适访问权限,可能会让未经授权的用户访问该文件。访问权限依赖于文件系统,但一般文件系统都会提供控制访问权限的功能。
错误示例:下列代码没有显式配置文件的访问权限。
void SaveConfigDataToFile(char * file_name){ int fd = open(file_name, O_CREAT | O_WRONLY); /*【错误】 缺少访问权限设置*/ if (-1 == fd) { /* 处理错误 */ } DoSaveData(); ...}
推荐做法:为open函数设置文件访问权限。
void SaveConfigDataToFile(char * file_name){ /* 初始化file_name和file_access_permissions */ int fd = open(file_name, O_CREAT | O_WRONLY, S_IRUSR|S_IWUSR); /* 【修改】显式配置文件访问权限 */ if (-1 == fd) { /* 处理错误 */ } DoSaveData(); ...}
对于open()函数,
- 第三个参数用于设置文件访问权限的初始值,其仅当创建新文件时(即第二个参数使用了O_CREAT时)才使用。需要注意的是,真正创建文件时的权限还会受到umask值所影响,因此文件的权限应该为(mode&(~umask))。
- 其函数本身没有用于设置访问权限的参数,所有fopen()创建的文件都默认具有权限S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH (0666),因此创建完后,可以使用fchmod()和umask()函数来显式配置文件的访问权限。
规则C6.3:文件路径验证前,必须对其进行标准化
说明:当文件路径来自非信任域时,需要先将文件路径规范化再做校验。路径在验证时会有很多干扰因素,如相对路径与绝对路径,如文件的符号链接、硬链接、快捷路径、别名等。
所以在验证路径时需要对路径进行标准化,使得路径表达唯一化、无歧义。如果没有作标准化处理,攻击者就有机会:
- 构造一个跨越目录限制的文件路径,例如“../../../etc/passwd”或“../../../boot.ini”
- 构造指向系统关键文件的链接文件,例如symlink(“/etc/shadow”,”/tmp/log”)
通过上述两种方式之一可以实现读取或修改系统重要数据文件,威胁系统安全。
错误示例:直接将用户输入作为参数并执行。
void SaveAllData(char * input_path){ SaveConfigDataToFile(input_path); /* 【错误】不能直接将用户输入作为参数并执行 */ ...}
推荐做法(Linux):Linux下对文件进行标准化,可以防止黑客通过构造指向系统关键文件的链接文件。realpath()函数返回绝对路径,删除了所有符号链接:
void SaveAllData(char * input_path){ char path[PATH_MAX + 1] = {0x00}; if( strlen(input_path) > PATH_MAX || NULL == realpath(input_path,path)) /*【修改】使用realpath()函数来规范化文件路径 */ return; ValidatePath(path); /* 校验路径符合预期 */ SaveConfigDataToFile(path);}
注意:PATH_MAX是系统定义宏,是编译器所支持的最长全路径名的长度。
推荐做法(Windows): Windows下可以使用PathCanonicalize()函数对文件路径进行标准化。
void SaveAllData(char * input_path){ char path[PATH_MAX + 1] = {0x00}; char *lppath = path; if(strlen(input_path) > PATH_MAX || PathCanonicalize (lppath,input_path) == FALSE) /*【修改】使用PathCanonicalize()函数来规范化文件路径 */ return; ValidatePath(path); /* 校验路径符合预期 */ SaveConfigDataToFile(path); ...}
需要注意的是:PathCanonicalize()函数没有对入参进行校验,使用不当极易引起缓冲区溢出,因此在使用时,一定要校验确保入参的大小不能超过目标缓冲区的大小。
同时,如果有条件可以使用微软推荐的PathCchCanonicalize()、PathCchCanonicalizeEx()或chenhPathAllocCanonicalize()函数来进行路径的规范化。
建议C6.1:访问文件时尽量使用文件描述符代替文件名作为输入,以避免竞争条件问题
说明:该建议应用场景如下,当对文件的元信息进行操作时(比如修改它的所有者、对文件进行统计,或者修改它的权限位),首先要打开该文件,然后对打开的文件进行操作。只要有可能,应尽量避免使用获取文件名的操作,而是使用获取文件描述符的操作。这样做将避免文件在程序运行时被替换(一种可能的竞争条件)。
例如,当access()和open()两者都利用一个字符串参数而不是一个文件句柄来进行相关操作时,攻击者就可以通过在access()和open()之间的间隙替换掉原来的文件,如下所示:
行式打印 攻击者
access(”/tmp/attack”) unlink(”/tmp/attack”) symlink(”/etc/shadow”, ”/tmp/attack”)open(”/tmp/attack”)
错误示例:下列代码使用access()函数,可能引发竞争条件问题。
if(!access(file_name, W_OK)) /* 【不推荐】不要使用access()函数,易引发条件竞争 */{ f = fopen(file_name, "w+"); operate(f); ... /* fclose f after operate(f)*/}else { fprintf(stderr,"Unable to open file %s.\n",file);}
推荐方法:在Linux下可以使用如下代码。
fd = creat(file_name, 0644); /* 【修改】使用文件描述符来操作文件 */if (fd == -1) return;operate(fd);/* 关闭文件操作符 */
信号
规则C7.1:不要在信号处理程序中访问共享对象
说明:在信号处理程序中访问或者修改共享对象(主要是指结构体、数组或者字符串等)可能会引发条件竞争。有两种情况例外:读写无锁原子对象(lock-free atomic objects)和读写volatile sig_atomic_t类型变量。除此之外,在信号处理程序中访问任何其它类型对象都有可能导致未定义行为。
错误示例:下列代码中,err_msg用于表明收到一个中断(SIGINT)信号。而err_msg变量是一个字符串指针而不是一个volatile sig_atomic_t类型变量。
#include <signal.h>#include <stdlib.h>#include <string.h>enum { MAX_MSG_SIZE = 24 };char *err_msg; /*【错误】字符串指针类型err_msg有可能会产生条件竞争 */void handler(int signum) { int ret = strcpy_s(err_msg, MAX_MSG_SIZE + 1, "SIGINT encountered."); /* 校验ret,确保安全函数执行成功 */}int main(void) { signal(SIGINT, handler); err_msg = (char *)malloc(MAX_MSG_SIZE + 1); if(NULL == err_msg) { /* 处理错误 */ } int ret = strcpy_s(err_msg, MAX_MSG_SIZE + 1, "No errors yet."); /* 校验ret,确保安全函数执行成功 */ /* Main code loop */ return 0;}
推荐做法1(写volatile sig_atomic_t类型):为了获得最好的可移植性,信号处理程序应该无条件的将变量类型设置为volatile sig_atomic_t类型并返回。
#include <signal.h>#include <stdlib.h>#include <string.h>enum { MAX_MSG_SIZE = 24 };volatile sig_atomic_t e_flag = 0; /*【修改】使用一个valatile sig_atomic_t变量代替之 */void handler(int signum){ e_flag = 1;}int main(void){ char *err_msg = (char *)malloc(MAX_MSG_SIZE + 1); if (NULL == err_msg) { /* 处理错误 */ } signal(SIGINT, handler); int ret = strcpy_s(err_msg, MAX_MSG_SIZE + 1, "No errors yet."); /* 校验ret,确保安全函数执行成功 */ /* Main code loop */ if (e_flag) { ret = strcpy_s(err_msg, MAX_MSG_SIZE + 1, "SIGINT received."); /* 校验ret,确保安全函数执行成功 */ } return 0;}
推荐做法2(无锁原子访问):可以使用无锁原子访问对象。
#include <signal.h>#include <stdlib.h>#include <string.h>#include <stdatomic.h>#if __STDC_NO_ATOMICS__ == 1#error "Atomics is not supported"#elif ATOMIC_INT_LOCK_FREE == 0#error "int is never lock-free"#endifatomic_int e_flag = ATOMIC_VAR_INIT(0);void handler(int signum){ eflag = 1;}int main(void){ enum { MAX_MSG_SIZE = 24 }; char err_msg[MAX_MSG_SIZE + 1];#if ATOMIC_INT_LOCK_FREE == 1 if(!atomic_is_lock_free(&e_flag)) { return EXIT_FAILURE; }#endif if(signal(SIGINT, handler) == SIG_ERR) { return EXIT_FAILURE; } int ret = strcpy_s(err_msg, MAX_MSG_SIZE + 1, "No errors yet."); /* 校验ret,确保安全函数执行成功 */ /* Main code loop */ if(e_flag) { ret = strcpy_s(err_msg, MAX_MSG_SIZE + 1, "SIGINT received."); /* 校验ret,确保安全函数执行成功 */ } return EXIT_SUCCESS;}
规则C7.2:在信号处理程序中只调用异步安全函数
说明:在信号处理程序中只能调用异步安全函数。对于要求比较严格的程序,只有C标准库中的abort(), _Exit(),quick_exit()和signal()函数可以在信号处理程序中安全的调用。使用异步不安全函数所导致的行为是未定义的。产品可能也会自定义一些异步安全函数,这些函数也可以在信号处理程序中使用。这条规则适用于库函数和程序自定义函数。
一般情况下,在信号处理程序中不要调用输入/输出(I/O)函数。程序员在任何时候,都要确保他们在信号处理程序中调用的函数都是异步安全的。
错误示例:下列代码中的信号处理程序通过log_message()函数调用了C标准库函数fprintf()和free()函数,然而这两个函数不是异步安全的。
#include <signal.h>#include <stdio.h>#include <stdlib.h>enum { MAXLINE = 1024 };char *info = NULL;void log_message(void){ fprintf(stderr, info); /*【错误】fprintf()不是异步安全的 */}void handler(int signum){ log_message(); free(info); /*【错误】free()不是异步安全的 */ info = NULL;}int main(void){ if(signal(SIGINT, handler) == SIG_ERR) { /* 处理错误 */ } info = (char *)malloc(MAXLINE); if (NULL == info) { /* 处理错误 */ } while(1) { /* Main loop program code */ log_message(); /* More program code */ } return 0;}
推荐做法:信号处理程序应尽可能的简洁,理想状态下只设置一个标志(flag)并返回。下列代码中就是只设置了一个volatile sig_atomic_t类型flag并返回。main()函数中直接调用log_message()和free()函数。
#include <signal.h>#include <stdio.h>#include <stdlib.h>enum { MAXLINE = 1024 };volatile sig_atomic_t eflag = 0;char *info = NULL;void log_message(void){ fprintf(stderr, info);}void handler(int signum){ eflag = 1;}int main(void){ if (signal(SIGINT, handler) == SIG_ERR) { /* 处理错误 */ } info = (char *)malloc(MAXLINE); if(NULL == info) { /* 处理错误 */ } while(!eflag) { /* Main loop program code */ log_message(); /* More program code */ } log_message(); free(info); info = NULL; return 0;}
错误示例(longjmp()):在信号处理程序中调用longjmp()函数可能导致未定义的行为。因此无论是longjmp()还是POSIX的siglongjmp()函数都不能在信号处理程序中调用。
下列代码执行main()中的循环来记录一些数据。一旦收到一个SIGINT信号,程序会退出循环,记录错误然后终止。然而,攻击者可以在log_message()函数中第二个if语句之前产生一个SIGINT信号来利用这个程序。导致的结果就是,longjmp()函数将控制权返回给main(),然后log_message()函数又一次被调用。但是此时第一个if语句将不会被执行(因为之前的中断,buf没有被设置为NULL),因此程序将会写入buf0所指向的无效内存地址。
#include <setjmp.h>#include <signal.h>#include <stdlib.h>enum { MAXLINE = 1024 };static jmp_buf env;void handler(int signum){ longjmp(env, 1);}void log_message(char *info1, char *info2){ static char *buf = NULL; static size_t bufsize; char buf0[MAXLINE] = {0x00}; if(buf == NULL) { buf = buf0; bufsize = sizeof(buf0); } /* * Try to fit a message into buf, else reallocate * it on the heap and then log the message. */ /* Program is vulnerable if SIGINT is raised here */ if (buf == buf0) { buf = NULL; }}int main(void){ if (signal(SIGINT, handler) == SIG_ERR) { /* Handle error */ } char *info1 = NULL; char *info2 = NULL; /* info1 and info2 are set by user input here */ if(0 == setjmp(env)) { while (1) { /* Main loop program code */ log_message(info1, info2); /* More program code */ } } else { log_message(info1, info2); } return 0;}
推荐做法:删除longjmp()的调用,信号处理程序只是设置了一个错误标志。
#include <signal.h>#include <stdlib.h>enum { MAXLINE = 1024 };volatile sig_atomic_t eflag = 0;void handler(int signum){ eflag = 1;}void log_message(char *info1, char *info2){ static char *buf = NULL; static size_t bufsize; char buf0[MAXLINE] = {0x00}; if (buf == NULL) { buf = buf0; bufsize = sizeof(buf0); } /* * Try to fit a message into buf, else reallocate * it on the heap and then log the message. */ if (buf == buf0) { buf = NULL; }}int main(void){ if (signal(SIGINT, handler) == SIG_ERR) { /* Handle error */ } char *info1 = NULL; char *info2 = NULL; /* info1 and info2 are set by user input here */ while(!eflag) { /* Main loop program code */ log_message(info1, info2); /* More program code */ } log_message(info1, info2); return 0;}
错误示例(raise()):下列代码中嵌套调用raise()函数,会导致未定义的行为。
#include <signal.h>#include <stdlib.h> void term_handler(int signum){ /* SIGTERM handler */}void int_handler(int signum){ /* SIGINT handler */ if(raise(SIGTERM) != 0) { /* Handle error */ }}int main(void){ if (signal(SIGTERM, term_handler) == SIG_ERR) { /* Handle error */ } if(signal(SIGINT, int_handler) == SIG_ERR) { /* Handle error */ } /* Program code */ if (raise(SIGINT) != 0) { /* Handle error */ } /* More code */ return EXIT_SUCCESS;}
推荐做法:int_handler()调用了term_handler()函数来代替raise(SIGTERM)。
#include <signal.h>#include <stdlib.h>void term_handler(int signum){ /* SIGTERM handler */}void int_handler(int signum){ /* SIGINT handler */ /* Pass control to the SIGTERM handler */ term_handler(SIGTERM);}int main(void){ if (signal(SIGTERM, term_handler) == SIG_ERR) { /* Handle error */ } if(signal(SIGINT, int_handler) == SIG_ERR) { /* Handle error */ } /* Program code */ if(raise(SIGINT) != 0) { /* Handle error */ } /* More code */ return EXIT_SUCCESS;}
想了解更多关于的异步安全函数,请参考附录D。
内核操作安全
规则C8.1:内核mmap接口实现中,确保对映射起始地址和大小进行合法性校验
说明:Linux内核 mmap接口中,经常使用remap_pfn_range()函数将设备物理内存映射到用户进程空间。如果映射起始地址等参数由用户态控制并缺少合法性校验,将导致用户态可通过映射读写任意内核地址。如果攻击者精心构造传入参数,甚至可在内核中执行任意代码。
错误示例:如下代码在使用remap_pfn_range()进行内存映射时,未对用户可控的映射起始地址和空间大小进行合法性校验,可导致内核崩溃或任意代码执行。
static int incorrect_mmap(struct file *file, struct vm_area_struct *vma){ unsigned long size; size = vma->vm_end - vma->vm_start; vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, size, vma->vm_page_prot)) /*【错误】未对映射起始地址、空间大小做合法性校验*/ { err_log("%s, remap_pfn_range fail", __func__); return EFAULT; } else { vma->vm_flags &= ~VM_IO; } return EOK;}
推荐做法:增加对映射起始地址等参数的合法性校验。
static int correct_mmap(struct file *file, struct vm_area_struct *vma){ unsigned long size; size = vma->vm_end - vma->vm_start; if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size)) /* 【修改】添加校验函数,验证映射起始地址、空间大小是否合法 */ { return EINVAL; } vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, size, vma->vm_page_prot)) { err_log("%s, remap_pfn_range fail", __func__); return EFAULT; } else { vma->vm_flags &= ~VM_IO; } return EOK;}
规则C8.2:内核程序中必须使用内核专用函数读写用户态缓冲区
说明:用户态与内核态之间进行数据交换时,如果在内核中不加任何校验(如校验地址范围、空指针)而直接引用用户态传入指针,当用户态传入非法指针时,可导致内核崩溃、任意地址读写等问题。因此,应当禁止使用memcpy()、sprintf()等危险函数,而是使用内核提供的专用函数:copy_from_user()、copy_to_user()、put_user()和get_user()
来读写用户态缓冲区,这些函数内部添加了入参校验功能。
所有禁用函数列表为:memcpy()、bcopy()、memmove()、strcpy()、strncpy()、strcat()、strncat()、sprintf()、vsprintf()、snprintf()、vsnprintf()、sscanf()、vsscanf()。
错误示例1:内核态直接使用用户态传入的buf指针作为snprintf()的参数,当buf为NULL时,可导致内核崩溃。
ssize_t incorrect_show(struct file *file, char __user *buf, size_t size, loff_t *data){ return snprintf(buf, size, "%ld\n", debug_level); /* 【错误】直接引用用户态传入指针,如果buf为NULL,则空指针异常导致内核崩溃 */}
推荐做法1:使用copy_to_user()函数代替snprintf()。
ssize_t correct_show (struct file *file, char __user *buf, size_t size, loff_t *data){ int ret = 0; char level_str[MAX_STR_LEN] = {0}; snprintf(level_str, MAX_STR_LEN, "%ld\n", debug_level); if(strlen(level_str) >= size) return EFAULT; ret = copy_to_user(buf, level_str, strlen(level_str)+1); /* 【修改】使用专用函数copy_to_user()将数据写入到用户态buf,并注意防止缓冲区溢出 */ return ret;}
错误示例2:内核态直接使用用户态传入的指针user_buf作为数据源进行memcpy()操作,当user_buf为NULL时,可导致内核崩溃。
size_t incorrect_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos){ ... char buf[128] = {0}; int buf_size = 0; buf_size = min(count, (sizeof(buf)-1)); memcpy(buf, user_buf, buf_size); /* 【错误】直接引用用户态传入指针,如果user_buf为NULL,则可导致内核崩溃 */ ...}
推荐做法2:使用copy_from_user()函数代替memcpy()。
ssize_t correct_write (struct file *file, const char __user *user_buf, size_t count, loff_t *ppos){ ... char buf[128] = {0}; int buf_size = 0; buf_size = min(count, (sizeof(buf)-1)); if (copy_from_user(buf, user_buf, buf_size)) /* 【修改】使用专用函数copy_from_user()将数据写入到内核态buf,并注意防止缓冲区溢出 */ return EFAULT; ...}
规则C8.3:必须对copy_from_user()拷贝长度进行校验,防止缓冲区溢出
说明:内核态从用户态拷贝数据时通常使用copy_from_user()函数,如果未对拷贝长度做校验或者校验不当,会造成内核缓冲区溢出,导致内核panic或提权。
错误示例:未校验拷贝长度。
static long gser_ioctl(struct file *fp, unsigned cmd, unsigned long arg){ char smd_write_buf[GSERIAL_BUF_LEN]; switch (cmd) { case GSERIAL_SMD_WRITE: if (copy_from_user(&smd_write_arg, argp, sizeof(smd_write_arg))) {…} copy_from_user(smd_write_buf, smd_write_arg.buf, smd_write_arg.size); /* 【错误】拷贝长度参数smd_write_arg.size由用户输入,未校验 */ ... }}
推荐做法:添加长度校验。
static long gser_ioctl(struct file *fp, unsigned cmd, unsigned long arg){ char smd_write_buf[GSERIAL_BUF_LEN]; switch (cmd) { case GSERIAL_SMD_WRITE: if (copy_from_user(&smd_write_arg, argp, sizeof(smd_write_arg))){…} if (smd_write_arg.size >= GSERIAL_BUF_LEN) {……} /*【修改】添加校验 */ copy_from_user(smd_write_buf, smd_write_arg.buf, smd_write_arg.size); ... }}
规则C8.4:必须对copy_to_user()拷贝的数据进行初始化,防止信息泄漏
说明:内核态使用copy_to_user()向用户态拷贝数据时,当数据未完全初始化(如结构体成员未赋值、字节对齐引起的内存空洞等),会导致栈上指针等敏感信息泄漏。攻击者可利用绕过kaslr等安全机制。
错误示例:未完全初始化数据结构成员。
static long rmnet_ctrl_ioctl(struct file *fp, unsigned cmd, unsigned long arg){ struct ep_info info; switch (cmd) { case FRMNET_CTRL_EP_LOOKUP: info.ph_ep_info.ep_type = DATA_EP_TYPE_HSUSB; info.ipa_ep_pair.cons_pipe_num = port->ipa_cons_idx; info.ipa_ep_pair.prod_pipe_num = port->ipa_prod_idx; ret = copy_to_user((void __user *)arg, &info, sizeof(info)); /*【错误】 info结构体有4个成员,未全部赋值 */ … }}
推荐做法:全部进行初始化。
static long rmnet_ctrl_ioctl(struct file *fp, unsigned cmd, unsigned long arg){ struct ep_info info; int ret = memset_s(&info, sizeof(ep_info), ‘\0’, sizeof(ep_info)); /* 【修改】使用memset初始化缓冲区,保证不存在因字节对齐或未赋值导致的内存空洞 */ /* 校验ret,确保安全函数执行成功 */ switch (cmd) { case FRMNET_CTRL_EP_LOOKUP: info.ph_ep_info.ep_type = DATA_EP_TYPE_HSUSB; info.ipa_ep_pair.cons_pipe_num = port->ipa_cons_idx; info.ipa_ep_pair.prod_pipe_num = port->ipa_prod_idx; ret = copy_to_user((void __user *)arg, &info, sizeof(info)); ... }}
规则C8.5:禁止在异常处理中使用BUG_ON宏,避免造成内核panic
说明:BUG_ON宏会调用内核的panic()函数,打印错误信息并主动崩溃系统,在正常逻辑处理中(如ioctl接口的cmd参数不识别)不应当使系统崩溃,禁止在此类异常处理场景中使用BUG_ON宏,推荐使用WARN_ON宏。
错误代码:正常流程中使用了BUG_ON宏
/* 判断Q6侧设置定时器是否繁忙,1-忙,0-不忙 */ static unsigned int is_modem_set_timer_busy(special_timer* smem_ptr){ int i = 0; if(NULL == smem_ptr) { printk(KERN_EMERG "%s:smem_ptr NULL!\n", __FUNCTION__); BUG_ON(1); /*【错误】系统BUG_ON宏打印调用栈后调用panic(),导致内核拒绝服务,不应在正常流程中使用 */ return 1; } ...}
推荐做法:去掉BUG_ON宏。
/* 判断Q6侧设置定时器是否繁忙,1-忙,0-不忙 */ static unsigned int is_modem_set_timer_busy(special_timer* smem_ptr){ int i = 0; if(NULL == smem_ptr) { printk(KERN_EMERG "%s:smem_ptr NULL!\n", __FUNCTION__); /* 【修改】去掉BUG_ON调用,或使用WARN_ON */ return 1; } ...}
规则C8.6:在中断处理程序或持有自旋锁的进程上下文代码中,禁止使用会引起进程休眠的函数
说明:Linux以进程为调度单位,在Linux中断上下文中,只有更高优先级的中断才能将其打断,系统在中断处理的时候不能进行进程调度。如果中断处理程序处于休眠状态,就会导致内核无法唤醒,从而使得内核处于瘫痪。自旋锁在使用时,抢占是失效的。若自旋锁在锁住以后进入睡眠,由于不能进行处理器抢占,其它进程都将因为不能获得CPU(单核CPU)而停止运行,对外表现为系统将不作任何响应,出现挂死。
因此,在中断处理程序或持有自旋锁的进程上下文代码中,应该禁止使用可能会引起休眠(如vmalloc()、msleep()等)、阻塞(如copy_from_user(),copy_to_user()等)或者耗费大量时间(如printk()等)的函数。
规则C8.7:合理使用内核栈,防止内核栈溢出
说明:Linux的内核栈大小是固定的(一般32位系统为8K,64位系统为16K),因此资源非常宝贵。不合理的使用内核栈,可能会导致栈溢出,造成系统挂死。因此需要做到以下几点:
- 在栈上申请内存空间不要超过内核栈大小;
- 注意函数的嵌套使用次数;
- 不要定义过多的变量。
错误代码:以下代码中定义的变量过大,导致栈溢出。
struct result { char name[4]; unsigned int a; unsigned int b; unsigned int c; unsigned int d;}; /* 结构体result的大小为20字节 */int func(){ struct result temp[512]; /*【错误】temp数组含有512个元素,总大小为10K,远超内核栈大小 */ int ret = memset_s(temp, sizeof(result) * 512, 0, sizeof(result) * 512); /* 校验ret,确保安全函数执行成功 */ /*use temp do something */ ... return 0;}
代码中数组temp有512个元素,总共10K大小,远超内核的8K,明显的栈溢出。
推荐做法:使用kmalloc()代替之。
struct result { char name[4]; unsigned int a; unsigned int b; unsigned int c; unsigned int d;}; /* 结构体result的大小为20字节 */int func(){ struct result *temp = NULL; temp = (result *)kmalloc(sizeof(result) * 512, GFP_KERNEL); /*【修改】使用kmalloc()申请内存 */ /* check temp is not NULL*/ (void)memset_s(temp, sizeof(result) * 512, 0, sizeof(result) * 512); ... /* free temp*/ return 0;}
规则C8.8:临时关闭地址校验机制后,在操作完成后必须及时恢复
说明:SMEP安全机制是指禁止内核执行用户空间的代码(PXN是ARM版本的SMEP)。
系统调用(如open(),write()等)本来是提供给用户空间程序访问的。默认情况下,这些函数会对传入的参数地址进行校验,如果入参是非用户空间地址则报错。因此,要在内核程序中使用这些系统调用,就必须使参数地址校验功能失效。set_fs()/get_fs()就用来解决该问题。详细说明见如下代码:
mm_segment_t old_fs;printk("Hello, I'm the module that intends to write message to file.\n");if(file == NULL) file =filp_open(MY_FILE, O_RDWR | O_APPEND | O_CREAT, 0664);if(IS_ERR(file)){ printk("Error occured while opening file %s, exiting..\n", MY_FILE); return 0;}sprintf_s(buf, BUF_SIZE, "%s", "The Message.");old_fs = get_fs(); /*get_fs()的作用是获取用户空间地址上限值 #define get_fs() (current->addr_limit */set_fs(KERNEL_DS); /* set_fs的作用是将地址空间上限扩大到KERNEL_DS,这样内核代码可以调用系统函数 */file->f_op->write(file, (char *)buf, sizeof(buf), &file->f_pos); /* 内核代码可以调用write()函数 */set_fs(old_fs); /* 使用完后及时回复原来用户空间地址限制值 */
通过上述代码,可以了解到最为关键的就是操作完成后,要及时恢复地址校验功能。否则SMEP/PXN安全机制就会失效,使得许多漏洞的利用变得很容易。
错误代码:在程序错误处理分支,未通过set_fs()恢复地址校验功能。
oldfs = get_fs();set_fs(KERNEL_DS);/* 在时间戳目录下面创建done文件 */fd = sys_open(path, O_CREAT | O_WRONLY, FILE_LIMIT);if(fd < 0){ BB_PRINT_ERR("sys_mkdir [%s] error, fd is [%d]\n", path, fd); return; /* 【错误】在错误处理程序分支未恢复地址校验机制 */}sys_close(fd);set_fs(oldfs);
推荐做法:在错误处理程序中恢复地址校验功能。
oldfs = get_fs();set_fs(KERNEL_DS);/* 在时间戳目录下面创建done文件 */fd = sys_open(path, O_CREAT | O_WRONLY, FILE_LIMIT);if(fd < 0){ BB_PRINT_ERR("sys_mkdir [%s] error, fd is [%d]\n", path, fd); set_fs(oldfs); /* 【修改】在错误处理程序分支中恢复地址校验机制 */ return;}sys_close(fd);set_fs(oldfs);
其它
规则C9.1:禁止使用不安全的C标准库函数产生用于安全用途的伪随机数
说明:C标准库函数rand()和random()产生的随机数随机性很不好,其产生的随机数序列存在一个较短的循环周期,因此它的随机数是可预测的,禁止用于安全用途。
安全用途的场景包括但不限于以下几种:
- 重要SessionID的生成;
- 挑战算法中的随机数生成;
- 验证码的随机数生成;
- 生成重要随机文件(例如存有系统信息的文件等)的随机文件名;
- 用于密码算法用途(例如用于生成IV、盐值、密钥等)的随机数生成。
错误示例:下列代码中使用不安全的随机数生成函数rand()来生成随机数。
void GenerateRandomNumber(){ enum {LEN = 12}; char SessionID[LEN + 1] = {0x00}; /* SessionID will hold the ID, starting with * the characters "ID" followed by a * random integer */ int r = 0; int num = 0; ... r = rand(); /* 【错误】rand()产生的随机数是可以被预测的 */ num = sprintf_s(SessionID, sizeof(SessionID), "ID-%d", r); /* generate the ID */ /* 校验num,确保安全函数执行成功 */ ... }
以上代码利用rand()产生一个ID的数字部分,因此这些ID是可预测的并且随机性有很大限制。
推荐做法:Unix/Linux下推荐读取/dev/random文件来获取真随机数。
void GenerateRandomNumber(){ enum {LEN = 12}; char SessionID[LEN + 1] = {0x00}; /* SessionID will hold the ID, starting with > * the characters "ID" followed by a > * random integer */ int r = 0; int num = 0; ... int fd; fd = open("/dev/random", O_RDONLY); /* 通过读取/dev/random来获取随机数 */ if (fd > 0) { read (fd,&r,sizeof (int)); } close (fd); num = sprintf_s(SessionID, sizeof(SessionID), "ID-%d", r); /* generate the ID */ /* 校验num,确保安全函数执行成功 */ ...}
Windows推荐使用随机数生成函数CryptGenRandom():
#include "Wincrypt.h"void GenerateRandomNumber(){ HCRYPTPROV hCryptProv; union { BYTE bs[sizeof(long int)]; long int li; } rand_buf; if (!CryptGenRandom(hCryptProv, sizeof(rand_buf), &rand_buf)) { /* Handle error */ } else { printf("Random number: %ld\n", rand_buf.li); }}
由于以上推荐的2种做法并不能保证主流编译环境下满足可靠性要求,对于可靠性要求很严格的产品可以使用开源组件openssl:
OpenSSL示例:
#include <openssl/rand.h> #include <stdio.h>#pragma comment(lib, "libeay32.lib") #define BUF_MAX 100void main(){ unsigned char buf[BUF_MAX + 1] = {0x00}; RAND_screen(); /* load screen data as seed */ if(RAND_status() == 1) { RAND_bytes(buf, sizeof(buf)); /* use RAND_bytes to generate random number */ for (int i = 0; i < sizeof(buf); i++) { printf("%02X", buf[i]); /* print random data */ } } RAND_cleanup();}
关于随机数的更多知识,请参考《密码算法应用规范V1.0》中的相关规则。
规则C9.2:禁止存储某些特殊函数返回的字符串指针
说明:某些特殊函数返回的指针指向的是静态分配的内存,其值很有可能会被随后的类似函数调用而改写,存储该返回值可能会导致一个危险的指针或者引用错误的数据,所以不要存储这些函数返回的字符串指针。这些返回的字符串应该被马上引用然后丢弃,如果需要后续使用该字符串,应该把该字符串拷贝到动态分配的内存里,然后在需要的时候引用该份拷贝。
例如getenv()返回指针的指向值可能会被后续的getenv(),putenv(),setenv(),unsetenv()调用所改写,或因其它操作修改了环境变量而变得无效。
除此之外,getenv()不是线程安全的,要确保处理使用该函数可能导致的竞争情况。
存在类似问题的函数还有:asctime(),localeconv(),setlocale()和strerror()。自定义函数若具有上述特性,同样需要注意。
错误示例:下列代码错误的保存了getenv()返回的字符串指针。
void InitProgramEnvironment(){ char *tmpvar = getenv("TMP"); if (!tmpvar) return -1; char *tempvar = getenv("TEMP"); /* 【错误】不要存储getenv()返回的字符串指针*/ if (!tempvar) return -1; if (0 == CompareValue(tmpvar, tempvar)) /* The two values may compare equal */ { ... } ...}
示例代码比较环境变量TMP和TEMP的值是否相同。在示例代码中,tmpvar指向的内容可能会因为第二次调用getenv()而被改写,从而导致tmpvar和tempvar指向相同的内容,即使环境变量TMP和TEMP的值并不相同。
推荐做法:使用malloc()和strcpy_s()来拷贝存储getenv()的返回值。
char *GetEnvStr(const char* env){ const char *temp = getenv(env); if (temp != NULL) { int len = strnlen(temp, MAX_LEN); tmpvar = (char *)malloc(len + 1); if (tmpvar != NULL) { int ret = strcpy_s(tmpvar, len + 1, temp); /* 校验ret,确保安全函数执行成功 */ return tmpvar; } } return NULL;}void InitProgramEnvironment(){ char *tmpvar = GetEnvStr("TMP"); if (NULL == tmpvar) { /* Handle error */ } char *tempvar = GetEnvStr("TEMP"); /* 【修改】将getenv()的字符串存储下来*/ if (NULL == tempvar) { /* Handle error */ } if (0 == CompareValue(tmpvar, tempvar)) { ... } ... /* free after tempvar */}
规则C9.3:多线程环境下只使用可重入函数
- (A) 多线程环境下,禁止std::cout与printf混用
说明:printf与std::cout分别为标准c语言与C++中的函数,两者的缓冲区机制不同(printf无缓冲区,而std::cout有),而且对于标准输出的加锁时机也略有不同:
- printf: 在对标准输出作任何处理前先加锁;
- std::cout: 在实际向标准输出打印时方才加锁;
二者存在微弱的时序差别,而多线程环境下,很多问题就是由于微弱的时序差别造成的。所以两者的混用很容易带来不可预知的错误,常见的错误有打印输出的结果不符合预期,而严重错误时甚至会导致内部缓存区溢出,导致crash。
错误示例:cout和printf混用可能导致crash。
void OutputDataToConsole(){ int j = 0; for(j = 0; j < 5; ++j) { cout << "j="; /*【错误】混用cout和printf在多线程中会导致crash */ printf("%d\n", j); }}上面代码的输出结果很可能为:1234j=j=j=j=j=
这很明显不符合程序员的预期。造成这样错误的原因就是std::cout的标准流输出是带有缓冲区的,如果没有及时清理缓冲区而在期间采用了其它系统的输出函数,可能会暴露两种输出函数的不兼容性,从而出现非预期错误。所以建议在代码中检查对于系统标准打印输出的兼容性,一定要使用统一的打印输出方法,而对于C++程序,更推荐统一使用流输出方法,而不推荐使用C风格的代码。
推荐做法:
void OutputDataToConsole(){ int j = 0; for(j = 0; j < 5; ++j) { printf("j = %d\n", j); /* 【修改】只使用printf()函数 */ }}
或者
void OutputDataToConsole(){ int j = 0; for(j = 0; j < 5; ++j) { cout << "j=" << j << endl; /*【修改】只使用cout()函数 */ }}
- (B)多线程环境下,禁止使用strtok函数
说明:strtok是一个线程不安全的函数,因为它使用了静态分配的空间来存储被分割的字符串位置。
初次调用strtok时传递一个字串的地址,比如”aaa.bbb.dddd”,将字串的地址保存在自己的静态变量中,当再次调用strtok并传递NULL时(strtok的特殊用法,第二次调用时字串传NULL表示对第一次传进去的字串继续分隔,所以要先保存字串地址),该函数就会引用保存好的字串地址。在多线程环境下,另一个线程也可能调用strtok,在这种环境下,另一个线程会在第一个线程不知道的情况下替换静态变量中的字串地址,这就会导致各种难以排除的错误出现。
错误示例:下面这个程序是一个用来确定一个文本中的每行单词个数的平均次数的错误算法。wordaverage函数用来确定每一行,不幸的是,wordcount函数也使用了strtok,这一次是用它来解析本行中的字,这时,strtok保持的内部状态信息被改变了。
#include <string.h>#define LINE_DELIMITERS "\n"#define WORD_DELIMITERS " "static int WordCount(char *s){ int count = 1; if(NULL == strtok(s,WORD_DELIMITERS)) /* 【错误】在多线程环境中使用strtok会导致crash */ return 0; while(strtok(NULL, WORD_DELIMITERS) != NULL) count++; return count;}double WordAveraget(char *s){ int linecount = 1; char *nextline = NULL; int words = 0; nextline = strtok(s, LINE_DELIMITERS); if(NULL == nextline) return 0.0; words = WordCount(nextline); while((nextline = strtok(NULL, LINE_DELIMITERS)) != NULL) { words += WordCount(nextline); linecount++; } return (double)words/linecount;}
推荐做法:带有_r的函数主要来自于UNIX平台。所有的带有_r和不带_r的函数的区别的是:带_r的函数是线程安全的,r的意思是reentrant,可重入的。
#include <string.h>#define LINE_DELIMITERS "\n"#define WORD_DELIMITERS " "static int WordCount(char* s){ int count = 1; char *lasts = NULL; if(NULL == strtok_r(s, WORD_DELIMITERS, &lasts)) /* [Modification] Use the strtok_r() function instead of strtok() */ return 0; while(strtok_r(NULL ,WORD_DELIMITERS, &lasts) != NULL) count++; return count;}double WordAverage (char *s){ int linecount = 1; char *nextline = NULL; int words = 0; char *lasts = NULL; nextline = strtok_r(s, LINE_DELIMITERS, &lasts); if(NULL == nextline) return 0.0; words = WordCount(nextline); while((nextline = strtok_r(NULL, LINE_DELIMITERS, &lasts)) != NULL) { words += WordCount(nextline); linecount++; } return (double)words/linecount;}
规则C9.4:检查返回值
说明:一些函数,如输入/输出函数和内存分配函数等,在执行完成后,要么会返回一个合法值,要么会返回一个指示错误类型的数据(例如-1或者空指针NULL)。如果都想当然的认为它们都会执行成功不会失败,那么当错误发生时,往往会出现意想不到或未定义的行为。因此,程序员必须严格的校验并合适的处理函数返回的错误值。
错误示例:代码中函数utf8_to_ucs()的作用是将UTF-8字符串转成UCS格式。函数首先调用setlocale()设置全球语言环境为“en_US.UTF-8”,但是却没有校验函数执行成功与否。setlocale()函数执行失败(比如资源缺失、语言环境没有安装等情况下)后会返回NULL指针。这就导致接下来调用的mbstowcs()函数要么执行失败,要么会将一些无法预知的宽字节字符写入到usc中。
size_t utf8_to_ucs(wchar_t *ucs, size_t n, const char *utf8){ setlocale(LC_CTYPE, "en_US.UTF-8"); /* 【错误】没有校验函数返回值*/ return mbstowcs(ucs, utf8, n);}
推荐做法:校验setlocale()的返回值,避免在该函数执行失败后调用mbstowcs()。同时,函数需要考虑在返回给调用者之前,恢复语言环境的初始化设置。
size_t utf8_to_ucs(wchar_t *ucs, size_t n, const char *utf8){ const char *save = NULL; save = setlocale(LC_CTYPE, "en_US.UTF-8"); if (NULL == save) /*【修改】调用函数返回值*/ { /* Propagate error to caller */ return (size_t)-1; } n = mbstowcs(ucs, utf8, n); if (NULL == setlocale(LC_CTYPE, save)) n = -1; return n;}
规则C9.5:禁止使用不可信数据拼接SQL命令
说明:SQL注入是指原始SQL查询被恶意动态更改成一个与程序预期完全不同的查询。执行更改后的查询可能会导致信息泄露或者数据被篡改。而SQL注入的根源就是使用不可信的数据来拼接SQL语句。C/C++语言中常见的使用不可信数据拼接SQL语句的底层场景有(包括但不局限于):
- 连接MySQL时调用mysql_query(),Execute()时的入参
- 连接SQL Server时调用db-library驱动的dbsqlexec()的入参
- 调用ODBC驱动的SQLprepare()连接数据库时的SQL语句参数
- C++程序调用OTL类库中的otl_stream(),otl_column_desc()时的入参
- C++程序连接Oracle数据库时调用ExecuteWithResSQL()的入参
因此,这些场景中切不可直接使用不可信数据来拼接SQL语句。同时,封装的SQL命令执行接口,也必须遵守同样的原则。而防止SQL注入的方法主要有以下几种:
- 参数化查询(通常也叫作预处理语句):参数化查询是一种简单有效的防止SQL注入的查询方式,应该被优先考虑使用。支持的数据库有MySQL,Oracle(OCI)。
- 参数化查询(通过ODBC驱动):支持ODBC驱动参数化查询的数据库有Oracle、SQLServer、PostgreSQL和GaussDB。
- 对不可信数据进行校验(对于每个引入的不可信数据推荐“白名单”校验)。
- 对不可信数据中的SQL特殊字符进行转义(参见附录A)。
错误示例:下列操作MySQL的代码中使用来自用户的不可信数据name来拼接SQL语句,并直接执行,可能会造成SQL注入。
MYSQL my_connection = NULL;int res = 0;char name[20] = {0x00};char sqlstatements[100] = {0x00};my_connection = mysql_init(NULL); /* validate my_connection */if (mysql_real_connect(&my_connection, "localhost", "username", "passwd", "newdatabase", 0, NULL, 0)) { printf("Connection success\n"); name = getuserinput(); /* name来自外部用户输入,且保证 < 20字节 */ res = sprintf_s(sqlstatements, 100, "SELECT childinfo FROM children WHERE name= ‘%s’", name ); /* 【错误】使用不可信数据拼接SQL语句*/ /* validate res */ res = mysql_query(&my_connection, sqlstatements); /* validate res */ ...}mysql_close(my_connection);
推荐做法:使用参数化查询。
MYSQL my_connection = NULL;int res = 0;char name[20] = {0x00};char sqlstatements[100] = {0x00};my_connection = mysql_init(NULL); /* validate my_connection */if (mysql_real_connect(&my_connection, "localhost", "username", "passwd", "newdatabase", 0, NULL, 0)) { /* 【修改】 使用预处理语句进行参数化查询 */ MYSQL_STMT *stmt = mysql_stmt_init(my_connection); /* 创建MYSQL_STMT句柄 */ char *query = "SELECT childinfo FROM children WHERE name= ?"; if(mysql_stmt_prepare(stmt, query, strlen(query))) { /* handle error */ } name = getuserinput(); /* name来自外部用户输入,且保证 < 20字节 */ MYSQL_BIND params[1]; res = memset_s(params, sizeof(params), 0, sizeof(params)); /* validate res */ params[0].buffer_type = MYSQL_TYPE_STRING; params[0].buffer = (char *)name; params[0].buffer_length = strlen(name); params[0].is_null= 0; res = mysql_stmt_bind_param(stmt, params); /* 绑定参数 */ /* validate res */ res = mysql_stmt_execute(stmt); /* 执行与语句句柄相关的预处理 */ /* validate res */ ... mysql_stmt_close(stmt); }mysql_close(my_connection);
建议C9.1:编译时应当使用编译器的最高警告等级
说明:程序员应当使用编译器的最高警告等级。在编译过程中,应该修改程序中的错误,直到警告解除。在此同时,应当使用静态和动态的分析工具来检测和清除安全缺陷。
另外,开启一些和安全相关的编译选项,可以使编译出来的程序具有更好的安全特性。
对于GCC编译器,建议开启以下安全选项:
- 使用PIE选项(gcc -fPIE / ld -pie),可以将源代码编译成和位置无关的可执行程序。
- 使用-fstack-protector-all或-fstack-protector选项,通过栈保护,来防止程序出现缓冲区溢出错误。
- Werror: 强制将所有的告警标记为错误,因此可以强制程序员定位错误;
- Wconversion: 在隐式类型转换时告警;
- -Wformat-security: 对于printf()族格式化函数进行特殊检查;
- -Wextra: 在可能发生安全危险时,产生更多告警;
- -D_FORTIFY_SOURCE=2:检测部分缓冲区溢出风险;
- -Wl, -z, relro, -z, now: 加固ELF内部数据段;
- -Wstack-protector:当函数没有防范栈溢出时进行告警;
- –param ssp-buffer-size=4:控制栈溢出保护(SSP)的最小缓冲区值。
对于VC(VS2005及以上的版本)编译器,可以开启如下和安全相关的编译选项:
- GS(栈保护):通过在栈中加入校验单元来防止出现缓冲区溢出错误。
- NXCOMPAT(与数据执行保护兼容):DEP,也就是数据执行保护,可以有效降低堆或栈上的缓存溢出漏洞的危害性。采用NXCOMPAT选项后,应用程序的运行被DEP 机制保护。在考虑兼容性的前提下,建议开发人员采用NXCOMPAT链接选项。
- ASLR(地址空间分布随机):是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。
- SafeSEH: SafeSEH会增加缓冲区利用的难度。一旦开启/SAFESEH,那么编译器生成二进制IMAGE的时候,会把所有合法的SEH函数的地址解析出来,在IMAGE里生成一张合法的SEH函数表,用于异常处理时候进行严格的匹配检查。
更多关于编译选项使用的详细内容,请参考华为《安全编译选项技术规范》。
建议C9.2:防止处理敏感数据的代码因被编译器优化而失效
说明:有时候编译器在优化时会删除一些它认为不必要的代码,但实际这些看似多余的代码是存在安全考虑。一个典型的例子就是函数返回前清除栈上敏感数据的操作,如果这些操作被删除掉,攻击者就有机会访问栈上的敏感数据。因此必须确保这些安全操作在编译器优化的场景下仍然得以执行。
错误示例:下列代码使用了可能被编译器优化掉的语句。
void SecureLogin(){ char pwd[PWD_SIZE + 1] = {0x00}; if (retrievePassword(pwd, sizeof(pwd))) { /* checking of password, secure operations, etc */ } memset(pwd, 0, sizeof(pwd)); /* 【错误】编译器优化有可能会使该语句失效 */ ...}
某些编译器在优化时候不会执行它认为不会改变程序执行结果的代码,因此memset()操作会被优化掉。
以下列出了几种可能的解决方法,其中的某些方法不具有普适性,因此需要结合实际选择相应的方法。
推荐做法1:使用公司内的安全函数库中的memset_s()函数。
void SecureLogin(){ char pwd[PWD_SIZE + 1] = {0x00}; if (retrievePassword(pwd, sizeof(pwd))) { /* checking of password, secure operations, etc */ } int ret = memset_s(pwd, sizeof(pwd), 0, sizeof(pwd)); /* 【修改】使用memset_s函数代替之 */ /* 校验ret,确保安全函数执行成功 */ ...}
推荐方法2:使用SecureZeroMemory()函数代替memset。
void SecureLogin(){ char pwd[PWD_SIZE + 1] = {0x00}; if (retrievePassword(pwd, sizeof(pwd))) { /*checking of password, secure operations, etc */ } SecureZeroMemory(pwd, sizeof(pwd)); /* 【修改】使用SecureZeroMemory()代替memset */ …}
该函数可以确保不被优化,但是只适用于Windows系统。
推荐做法3:
void SecureLogin(){ char pwd[PWD_SIZE + 1] = {0x00}; if (retrievePassword(pwd, sizeof(pwd))) { /* checking of password, secure operations, etc */ } #pragma optimize("", off) /* [【修改】禁用部分优化编译选项,确保pwd被处理*/ /* 清除内存 */ #pragma optimize("", on) …}
如果编译器支持#pragma指令,那么可以使用该指令指示编译器不要优化此处的操作。
推荐方法4:编写自定义的安全的内存清零函数。
推荐方法5:如果确实需要使用memset,可以使用如下方法。
void SecureLogin(){ char pwd[PWD_SIZE + 1] = {0x00}; if (retrievePassword(pwd, sizeof(pwd))) { /* checking of password, secure operations, etc */ } memset(pwd, 0, sizeof(pwd)); (volatile char*)pwd = *(volatile char*)pwd; /* 【修改】使用volatile自赋值语句,确保pwd被处理 */ …}
但是需要注意的是,某些编译器(如MIPSpro,GCC3.0及以上版本)虽然会执行memset语句,但是却会智能的只将目标buffer的首字节置零而其余部分却仍保持完好。此种情况下,则不可采取此种方法。
建议C9.3:函数参数定义尽量使用const
说明:如果想要避免函数(方法)中的参数被意外改写,应该使用const。
尤其是当为其它应用程序开发接口或者与其他团队合作开发时,正确使用const可以有效地避免数据覆写和误解。为了正确的使用const,应仔细检查所使用的库和头文件。即使当程序员认为确实不会修改数据时,也应该使用const加以确保。
错误示例:下列代码中,程序员忽视了一个问题,strtok_s()函数不是工作在一份拷贝上的,它会修改要分割的字符串。
char *get_second_token(char *input) { /* do dome strtok_s() that overwrites input's content! */ /* 【错误】strtok_s()会修改函数入参 */ return tok;}
推荐做法:将函数的入参加上const,这样编译器就会产生一个告警提示程序员在操作前应该做一份拷贝。
char *get_second_token(const char *input) /* 【修改】在函数入参加入const */{ /* do dome strtok_s() that overwrites input's content! */ return tok;}
C++安全编程
在C++代码中,除了要满足C安全编码要求外,还需要满足如下C++安全编码要求。
禁用不安全函数和对象
规则CPP1.1:禁止使用std::ostrstream,推荐使用std::ostringstream
说明: std::ostrstream的使用上需要特别注意几点:
- str() 会调用成员函数freeze(),它会冻结字符序列,当缓冲区不够大以至于需要分配新缓冲区时,这么做可以避免事情变得复杂。
- str()不会附加字符串终止符号(‘\0’)。
- data()返回所有字符串,没有附带‘\0’结尾字符(目前有些编译器自动调用c_str方法了)。
上面如果不注意,就可能会导致内存访问越界、缓冲区溢出等问题,所以建议不要使用ostrstream。
[C++03]标准将std::strstream标明为deprecated,替代方案是std::stringstream。ostringstream没有上述问题。
错误示例:下列代码使用了std::ostrstream,可能会导致内存访问越界等问题。
ostrstream mystr; /* 【错误】不要使用std::ostrstream */mystr << "Information is here.";cout << mystr.str() << std::endl;
推荐做法:
ostringstream mystr; /* 【修改】使用ostringstream 来代替ostrstream */mystr << "hello world";cout << mystr.str() << std::endl;
建议CPP1.1:在C++代码中优先使用C++类库函数
说明:C++是面向对象语言而C是面向过程的编程语言,正是由于这种特性,C++语言相比于C语言具有更多的面向对象特性(封装,继承,多态)和可维护性。总之C++类库函数比C函数更加安全。举例如下:
C标准的系列字符串处理函数strcpy/strcat/sprintf/scanf/gets,不检查目标缓冲区的大小,容易引入缓冲区溢出的安全漏洞。C++标准库提供了字符串类抽象的一个公共实现std::string,支持字符串的常规操作:
- 字符串拷贝
- 读写访问单个字符
- 字符串比较
- 字符串连接
- 字符串长度查询
- 字符串是否为空的判断。
因此,在C++程序中,尽可能使用std::string、std::ostringstream等替代不安全的C字符串操作函数。
错误示例:使用了C风格的字符串操作函数。
const char filename[]="some_file.txt";char cmd[BUFSIZE]="Usage:file=";strcat(cmd,filename); /* 【不推荐】在C++中不要使用C风格字符粗操作函数 */
推荐做法:
std::string cmd = "Usage:file=";cmd += "some_file.txt"; /* 【修改】使用C++标准库来代替C字符粗函数 */
对于输入/输出函数,C++类中的析构函数可以确保资源不会泄露。但是在C中需要手工进行资源释放,很容易造成资源泄露危险。
此外,混用C和C++函数是很危险的容易造成异步问题(例如规则C8.3:多线程环境下只使用可重入函数所展示的printf()和std::out的混用)。因此在C++代码中应该尽可能的使用C++类库函数。
C++类和对象安全
规则CPP2.1:禁止切分多态的类对象
说明:当一个基类有继承类时,禁止从继承类对象到基类对象实例的拷贝,也不能在多个继承类的对象之间相互拷贝,这样会导致信息的丢失,程序运行异常,从而引发DOS(denial-of-service)。
错误示例:下列代码中切分了类对象,会导致数据丢失。
class Employee {public: Employee(string theName): name(theName){}; string getName() const {return name;} virtual void print() const { cout << "Employee: " << getName() << endl; }virtual ~Employee()private: string name;};class Manager: public Employee {public: Manager(string theName, Employee theEmployee):Employee(theName), assistant(theEmployee) {}; Employee getAssistant() const {return assistant;} virtual void print() const { cout << "Manager: " << getName() << endl; cout << "Assistant: " << assistant.getName() << endl; }private: Employee assistant;};int main (){ Employee coder("Joe Smith"); Employee typist("Bill Jones"); Manager designer("Jane Doe", typist); coder = designer; /* 【错误】切分了对象designer:Jane Doe */ coder.print();}
运行结果:Employee: Jane Doe
示例代码中基类Employee,继承类Manager(增加了属性assistant),如果将Manager类的对象数据拷贝给Employee类的对象,则将发生对象切分,Manager类的assistant属性数据将丢失。
推荐做法1(引用):
int main () { Employee coder("Joe Smith"); Employee typist("Bill Jones"); Manager designer("Jane Doe", typist); Employee &toPrint = designer; /* Jane remains entire */ toPrint.print();}
推荐做法2(使用指针):
int main () { Employee *coder = new Employee("Joe Smith"); Employee *typist = new Employee("Bill Jones"); Manager *designer = new Manager("Jane Doe", *typist); coder = designer; coder->print();}
推荐做法3(使用智能指针):
int main () { auto_ptr<Employee> coder(new Employee("Joe Smith")); auto_ptr<Employee> typist(new Employee("Bill Jones")); auto_ptr<Manager> designer(new Manager("Jane Doe", *typist)); coder = designer; /* Smith deleted, Doe xferred */ coder->print(); /* everyone deleted */}
运行结果:
Manager: Jane Doe
Assistant: Bill Jones
规则CPP2.2:禁止定义基类析构函数为非虚函数,所有可能被继承类的析构函数都必须定义为virtual
说明:基类的析构函数如果不是virtual的,那么在对一个Base类型的指针进行delete时,就不会调用到派生类Derived的析构函数。而派生类里的析构函数一般会用于析构其内部的子对象,这样就可能会造成内存泄漏。
错误示例:代码中的析构函数没有被定义成虚函数。
class Base {public: ~Base(){}; /*【错误】禁止定义基类析构函数为非虚函数 */};class Derived : public Base {private: char *pc;public: Derived() { pc=new char[100]; }; ~ Derived() { delete [] pc; };};void main(){ Base *obj = new Derived(); delete obj;}
以上示例代码基类Base的析构函数不是virtual的。因为不是virtual,所以在对Base类型的指针obj进行delete时,不会调用到派生类Derived的析构函数,这样就造成内存泄漏。
推荐做法:基类Base的析构函数定义为virtual,这样确保在对Base类型的指针obj进行delete时调用派生类Derived的析构函数。
class Base {public: virtual ~Base(){};/*【修改】定义基类析构函数为虚函数 */};class Derived : public Base {private: char *pc;public: Derived() { pc=new char[100]; }; ~ Derived() { delete [] pc; };};void main(){ Base *obj = new Derived(); delete obj;}
规则CPP2.3:避免出现delete this操作
说明:对象指针应避免使用delete this语句硬删除,除非能保证this指针删除后不再被引用,并且保证对象是通过new操作符在堆上创建的。 原因有两个:
* 类的对象既可能是栈对象,也可能是堆对象。如果对栈对象的指针进行delete,即删除非动态分配的内存,会导致未定义行为;
* 二是delete this容易产生悬挂指针(dangling pointer),悬挂指针是个严重的安全漏洞,可以被攻击者利用执行任意代码。
错误示例:错误的删除this指针
class SomeClass {public: SomeClass(); ~SomeClass(); void doSomething(); void destroy(); ...};void SomeClass::destroy() { ... delete this; /*【错误】删除this指针会导致出现悬挂指针 */} void main(){ ... SomeClass sc; // 声明栈对象 ... sc.destroy(); /* 释放非动态分配的内存*/}
推荐做法:不delete this,让栈对象离开作用域后自动析构。
class SomeClass{ public: SomeClass(); ~SomeClass(); void doSomething(); void destroy(); ...}; void SomeClass::destroy() { ...}void main(){ SomeClass sc; // 声明栈对象 ...} /* 离开作用域,自动调用sc.~SomeClass() */
如果不得不使用delete this,保证类对象是堆对象,且this指针delete后置NULL,可参考如下示例代码:
class SomeClass {public: SomeClass(); void doSomething(); void destroy(); ...protected: ~SomeClass(); }; void SomeClass::destroy() { ... delete this; }...void main(){ SomeClass* sc = new SomeClass(); .... sc->destroy(); sc = NULL;}
这个示例代码中,将析构函数声明为protected,可以保证类SomeClass的对象不会在栈上生成。同时,在显示调用destory()来delete this指针后,再将指针置NULL,防止指针解引用。
规则CPP2.4:禁止在类的公共接口中返回类的私有数据地址
说明:如果一个类私有成员数据的引用或者其指针,被类的公有函数作为返回值return,则此私有数据可能会遭受到非可信代码的修改,导致引入不安全因素。
错误示例:下列代码类中的私有成员变量被公共成员函数所引用。
class Widget { public: Widget (): total(0) {} /* … */ void add (someType someParameters) { ... total ++; ... } void remove (someType someParameters) { total --; } ... int& getTotal() {return total;} /*【错误】禁止返回类的私有数据成员地址 */ ...private: int total; ...};
示例代码中,total作为类的私有成员,维护着对类方法add与remove的调用计数,但是其实际值却被类的公共成员函数getTotal对外提供了可引用的接口。
推荐做法:
class Widget {public: Widget(): total(0) {} ... void add(someType someParameters) { ... total ++; ... } void remove(someType someParameters) { ... total --; ... } ... int getTotal() const {return total;} ...private: int total; ...};
建议CPP2.1:重载后缀操作符应返回const类型
说明:C++标准中,列出了自增和自减操作符的特点:
class X{public: X& operator++(); // prefix ++a X operator++(int); // postfix a++};class Y { };Y& operator++(Y&); // prefix ++bY operator++(Y&, int); // postfix b++需要注意的是,前缀操作符返回的结果是non-const引用,而后缀操作符返回的可能是临时变量或者一个地址。传统重载自增和自减操作符的实现如下:class C { void Increment();public: C(const C&); C& operator=(const C&); C operator++(int) { C R(*this); Increment(); return R; }};
代码中返回的对象只是调用Increment()函数之前该对象的一个快照,因此,所有调用operator++(int)所操作的仅仅是一个临时变量,并不会对原始对象产生任何影响。这就要求当重载后缀操作符时,建议重载函数返回值类型为const。
错误示例:下列代码的目的是希望c自增两次得到结果2,但是由于c只是返回了一个预先值,所以c.I只自增了一次。c++返回的对象又自增了一次,但是却没有影响到原始的c对象。
#include <iostream>class C { int I;public: C() : I(0) {} C(const C &RHS) : I(RHS.I) {} C& operator=(const C &RHS) { I = RHS.I; return *this; } C operator++(int) { C R(*this); I++; return R; } int getI() const { return I; }};int main() { C c; c++++; std::cout << c.getI();}
代码的执行结果是1而不是2。
推荐做法:重载后缀操作符为const类型。
#include <iostream>class C { int I;public: C() : I(0) {} C(const C &RHS) : I(RHS.I) {} C& operator=(const C &RHS) { I = RHS.I; return *this; } const C operator++(int) /* 【修改】重载++a时返回const类型 */ { C R(*this); I++; return R; } int getI() const { return I; }};int main() { C c; c++; c++; /* 【修改】换一种自增方式 */ std::cout << c.getI();}
将自加和自减后缀操作符的返回值定义为const型后,若执行C++++这样的代码,在编译时编译器就会告警,从而避免之前错误的发生。
建议CPP2.2:显式声明的模板类应进行类型特化
说明:编译器不会严格地验证模板的参数,容易被破解者利用,并造成攻击。
错误示例:模板类使用错误。
template <typename T>class Sample{public: void funcA() { ... } void funcB() { T t; t.x = 50; }};int main() { Sample<int> a; /* 【错误】Sample<int>:: funcB有问题,int并不是class且没有成员变量x */ a. funcA();}
示例代码Sample :: funcB明显是有问题的,因为类型int并不是class,并且也没有成员变量x。很明显,模板A的设计者并不是将该模板应用于类型int。然而编译器并不会捕捉到这个错误,因此代码会被成功编译,却引入了缺陷。
推荐做法:
template <typename T>class Sample{public: void funcA() { ... } void funcB() { T t; t.x = 50; }};template class Sample<int>; /*【修改】显示声明模板类特化*/int main() { Sample<int> a; a.funcA();}
添加如上代码后,编译器会捕获到样例代码中的错误,因为模板的声明会强制编译器初始化类的所有成员,包括Sample:: funcB(),此时就会捕获到编译错误。
STL库安全
规则CPP3.1:引用容器前后元素时要确保容器元素存在
说明:没有判断是否为空就直接通过引用STL容器首尾元素,这在容器为空时会导致程序异常。
错误示例:
bool NoCompliant(const NodeKeyList &srcList, const NodeKeyList &snkList){ NodeKey srcNode = srcList.front(); NodeKey snkNode = snkList.back(); ...}
示例代码对函数的入参srcList没有判断长度直接通过front()和back()方法取了第一个和最后一个元素,在容器列表为空的情况下,会导致程序异常,与front()类似的还有通过begin()数据下标获取对应元素,比如*srcList.begin(),或者srcList.begin()->GetID(),或者是srcList0。
推荐做法:
bool CombineList(const NodeKeyList &srcList, const NodeKeyList &snkList){ if (srcList.empty() || snkList.empty()) /* 【修改】确保STL容器内有元素存在 */ { return false; } NodeKey srcNode = srcList.front(); NodeKey snkNode = snkList.back(); ...}
规则CPP3.2:迭代子使用前必须保证迭代子有效
说明:STL算法std::find()、std::find_if()和std::set::find()等有可能返回容器的end()位置,迭代子定义时可以不初始化,或者初始化指向容find()等方法返回的位置,与指针类似地,若未判断迭代子有效性,直接引用迭代子有可能导致程序崩溃。
错误示例:
void STLIterTest::IterReference_NoCompliant(int CmdCode, MAP_GENKEY_VALUE& allResult){ TEGenKey tmpKey; tmpKey.attrID = DWDMTL1_ATTRPORT_CLIENTPRO; tmpKey.objectID = it->first.objectID; MAP_GENKEY_VALUE::iterator iter = allResult.find( tmpKey ); if ("FC100" != iter->second.sValue) /* 非FC100设置为无效 */ { it->second.access = TEGenVar::ACCESS_INVALID; }}
示例代码通过map的find函数返回的迭代子iter,if语句直接通过iter->second来引用变量,如果迭代子iter指向为allResult的end()位置,则程序会崩溃。
推荐做法:
void STLIterTest::IterReference_Compliant(int CmdCode, MAP_GENKEY_VALUE& allResult){ TEGenKey tmpKey; tmpKey.attrID = DWDMTL1_ATTRPORT_CLIENTPRO; tmpKey.objectID = it->first.objectID; MAP_GENKEY_VALUE::iterator iter = allResult.find(tmpKey); if (iter != allResult.end()) /*【修改】确保迭代子有效后再进行操作 */ { if ( "FC100" != iter->second.sValue) { it->second.access = TEGenVar::ACCESS_INVALID; } }}
规则CPP3.3:必须确保迭代子指向的内容有效
说明:在理解上迭代子可以视为C指针,迭代子只有在指向了容器中一个存在的对象时,访问才是安全有效的,其他情况的访问都可能存在风险。典型问题:
对连续内存容器来说(如std::vector)会分配一块固定内存来保存连续对象,在插入新元素后(成员函数包括:reserve(),resize(), push_back(),insert()等),可能会引起容器重新分配内存和数据迁移,如果在插入元素之前使用迭代子保存了迭代子位置,那么插入新元素之后,前面保存的迭代子就可能是无效的。
错误示例1:下列代码的迭代子在操作过程中失效。
void ProcessMessageQueue(){ deque<double> d; double data[5] = { 2.3, 3.7, 1.4, 0.8, 9.6 }; deque<double>::iterator pos = d.begin(); for (size_t i = 0; i < 5; ++i) { d.insert(pos++, data[i] + 41); /*【错误】insert操作后,pos已失效 */ } }
Insert操作后,迭代子pos已经失效,执行自增操作导致异常。
推荐做法:
void ProcessMessageQueue(){ double data[5] = { 2.3, 3.7, 1.4, 0.8, 9.6 }; deque<double> d; deque<double>::iterator pos = d.begin(); for (size_t i = 0; i < 5; ++i) { pos = d.insert(pos, data[i] + 41); /*【修改】通过返回值获得新的有效的迭代子 */ ++pos; }}
std::remove和std::remove_if仅会将被删除元素后移并返回该被删除元素位置的迭代子,并没有真正从容器中删除对象,需要另配合erase函数才能删除,所以一般建议配合一起使用。
错误示例2:下列代码中错误的仅使用remove()函数来删除容器中元素。
void CleanContainer(){ vector<int> container; int value = 42; iterator end = remove( container.begin(), container.end(), value); for (iterator i = container.begin(); i != container.end(); ++i) { cout << "Container element: " << *i << endl; }}
remove() 删除任一个成员后返回值将指向任一个成员,值将不可预知。所以被删除后需要立即调用 erase()抹去,防止不可预知的数据访问。
推荐做法:
void CleanContainer(){ vector<int> container; int value = 42; container.erase(remove(container.begin(),container.end(),value), container.end()); /*【修改】remove删除成员后立即调用erase,确保迭代子指向的内容是有效的*/ for (iterator i = container.begin(); i != container.end(); ++i) { cout << "Container element: " << *i << endl; }}
规则CPP3.4:正确处理容器的erase()方法与迭代子的关系
说明:调用容器的erase(iter)方法后,迭代子指向的对象被析构,迭代子已经失效,如果再对迭代子执行递增递减或者引用操作会导致程序崩溃。
错误示例:下列代码中的迭代子在执行删除操作过程中已失效。
void STLIterTest::IterVisitContainer(){ std::map<oid,NE>::iterator it = m_mapID2NE.begin(); for (; it != m_mapID2NE.end(); ) { if (pNEInfo->GetNEState(ulNEID) == NESTATE_LOGIN) { m_mapID2NE.erase(iter); iter++; /*【错误】erase()后,iter指向的对象可能已失效 */ } else {++iter;} } }
推荐做法:将迭代子后置递增作为erase()的参数。
void STLIterTest::IterVisitContainer(){ std::map<oid,NE>::iterator it = m_mapID2NE.begin(); for (; it != m_mapID2NE.end(); ) { if ( pNEInfo->GetNEState(ulNEID) == NESTATE_LOGIN) { m_mapID2NE.erase(iter++); /*【修改】将迭代子后置递增作为erase()参数 */ } else {++iter;} }}
也可以使用earse方法的返回值来保存迭代子,因为返回的是被删除元素迭代子指向的下一个元素位置:
iter = erase(iter)。
注意这种用法可以用于list和vector的erase(),但不适用于map。因为std::map::erase()的返回值在不同STL实现版本是有差异的,有的有返回值,有的没有返回值,所以对map只能使用推荐做法。
参考资料
- Robert C.Seacord. The Cert C Secure Coding Standard. Pearson Education, 2009
- CERT C++ Secure Coding Standard. https://www.securecoding.cert.org/confluence/pages/viewpage.action?pageId=637
- Robert C.Seacord. Secure Coding in C and C++. Addison Wesley Professional, 2005
附录A
表中列出了几种常用数据库中可能导致SQL注入的特殊字符以其转义序列:
- Oracle
- ’ 单引号 \’
- MySQL
- ’ 单引号 \’
- ” 双引号 \”
- DB2
- ’ 单引号 ”
- ; 分号 .
- SQL Server
- ’ 单引号 ”
Like条件中的通配符可以按照如下方式转义:
- Oracle
- % 百分号:任意字符(>=0)
- _ 下划线:任何单字节字符
- MySQL
- \ 反斜杠 \
- % 百分号:任意字符(>=0) \%
- _ 下划线:任意单字节字符 _
- DB2
- % 百分号:任意字符(>=0)
- _ 下划线:任何单字节字符
- SQL Server
- [ 左方括号:转义字符 [[]
- _ 下划线:任意字符 [_]
- % 百分号:任意字符(>=0) [%]
- ^ 插入符号:排除下列字符 [^]
附录B 下表中列出了shell脚本中可能导致命令注入的特殊字符:
- 管道 |,连结上个指令的标准输出,作为下个指令的标准输入。
- 内联命令 ;,连续指令符号。
- & ,单一个& 符号,且放在完整指令列的最后端,即表示将该指令列放入后台中工作。
- 逻辑操作符 $,变量替换(Variable Substitution)的代表符号。
- **表达式 ∗∗,可用在 ∗ ∗ , 可 用 在 {}中作为变量的正规表达式。
- 重定向操作 >,将命令输出写入到目标文件中。
- <,将目标文件的内容发送到命令当中。
- 反引号 `, 可在‘“’之间构造命令内容并返回当前执行命令的结果。
- 倒斜线 \ ,在交互模式下的escape 字元,有几个作用
- 放在指令前,有取消 aliases的作用
- 放在特殊符号前,则该特殊符号的作用消失
- 放在指令的最末端,表示指令连接下一行。
- 感叹号 ! ,事件提示符(Event Designators),可以引用历史命令。
- 换行符 \n ,可以用在一行命令的结束,用于分隔不同的命令行。
上述字符也可能以组合方式影响命令拼接,如管道符“||”,“>>” ,“<<” ,逻辑操作符“&&”等,由于基于单个危险字符的检测可以识别这部分组合字符,因此不再列出。
另外可以 表示账户的home目录 “~”,可以表示上层目录的符号“..”,以及文件名通配符“?”(匹配文件名中除null外的单个字元),“*”(匹配文件名的任意字元)由于只影响命令本身的语义,不会引入额外的命令,因此未列入命令注入涉及的特殊字符,需根据业务本身的逻辑进行处理。
附录C
- POSIX
下表中所列的均为异步信号安全函数,来自POSIX标准。应用程序可以在信号处理程序中调用这些异步安全函数。
_Exit() fexecve() posix_trace_event() sigprocmask()_exit() fork() pselect() sigqueue()abort() fstat() pthread_kill() sigset()accept() fstatat() pthread_self() sigsuspend()access() fsync() pthread_sigmask() sleep()aio_error() ftruncate() raise() sockatmark()aio_return() futimens() read() socket()aio_suspend() getegid() readlink() socketpair()alarm() geteuid() readlinkat() stat()bind() getgid() recv() symlink()cfgetispeed() getgroups() recvfrom() symlinkat()cfgetospeed() getpeername() recvmsg() tcdrain()cfsetispeed() getpgrp() rename() tcflow()cfsetospeed() getpid() renameat() tcflush()chdir() getppid() rmdir() tcgetattr()chmod() getsockname() select() tcgetpgrp()chown() getsockopt() sem_post() tcsendbreak()clock_gettime() getuid() send() tcsetattr()close() kill() sendmsg() tcsetpgrp()connect() link() sendto() time()creat() linkat() setgid() timer_getoverrun()dup() listen() setpgid() timer_gettime()dup2() lseek() setsid() timer_settime()execl() lstat() setsockopt() times()execle() mkdir() setuid() umask()execv() mkdirat() shutdown() uname()execve() mkfifo() sigaction() unlink()faccessat() mkfifoat() sigaddset() unlinkat()fchdir() mknod() sigdelset() utime()fchmod() mknodat() sigemptyset() utimensat()fchmodat() open() sigfillset() utimes()fchown() openat() sigismember() wait()fchownat() pause() signal() waitpid()fcntl() pipe() sigpause() write()fdatasync() poll() sigpending()
- OpenBSD
OpenBSD singal()手册列出了少量异步安全函数,但是这些函数其它平台下可能不是安全的。
这些函数包括:snprintf(), vsnprintf()和syslog_r()函数(只有当syslog_data结构体初始化为本地变量的情况下才可以)。