当前位置:   article > 正文

《华为C&C++语言安全规范》笔记_华为 c&c++ 语言安全编程规范

华为 c&c++ 语言安全编程规范

《华为C&C++语言安全规范》笔记

通过阅读《华为C&C++语言安全规范》1,我了解到了我在编程中很多缺失的部分。现在记录下几个要点:

规则1.1.4:严禁对指针变量进行sizeof操作

编码人员往往由于粗心,将指针当做数组进行sizeof操作,导致实际的执行结果与预期不符。 下面的代码,buffer和path分别是指针和数组,编码人员想对这2个内存进行清0操作,但由于编码人员的疏忽,第5行代码,将内存大小误写成了sizeof,与预期不符。
如果要判断当前的指针类型大小,请使用sizeof(char *)的方式。

char *buffer = (char *)malloc(size);
char path[MAX_PATH] = {0};
...
memset(path, 0, sizeof(path));
memset(buffer, 0, sizeof(buffer));  
  • 1
  • 2
  • 3
  • 4
  • 5

相关指南:
CERT.ARR01-C. Do not apply the sizeof operator to a pointer when taking the size of an array

指针与数组名与指针有太多的相似,甚至很多时候,数组名可以作为指针使用。但是数组和指针存在差异。指针,是一个变量,存储的数据是地址。数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组,其外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量2。数组名只是大多数时候隐式转换成指向首元素的指针类型右值。这些时候不会转换:1)对其用 &;2)对其用 sizeof;3)C++中取引用3

我们先来一段代码作为演示:

#include<iostream>
using namespace std;
void func(int C[])
{
    cout<<"In function, C sizeof(C):"<<sizeof(C)<<endl;
    cout<<"C point:"<<C<<endl;
    cout<<"C &point:"<<&C<<endl;
    C++;
}
int main()
{
    int A[10];
    int* B=new int[10];
    cout<<"A sizeof(A):"<<sizeof(A)<<endl;
    cout<<"B sizeof(B):"<<sizeof(B)<<endl;
    // 取引用地址
    cout<<"A point:"<<A<<endl;
    cout<<"A &point:"<<&A<<endl;
    cout<<"B point:"<<B<<endl;
    cout<<"B &point:"<<&B<<endl;

    //调用函数
    func(A);
    //A++;//Error
    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

在X86的编译环境下,输出的结果为:

A sizeof(A):40
B sizeof(B):4
A point:0x6dfec8
A &point:0x6dfec8
B point:0x1fa838
B &point:0x6dfec4
In function, C sizeof(C):4
C point:0x6dfec8
C &point:0x6dfeb0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

显然第14行输出的是数组长度,第15行输出的是指针长度(在X86下为4字节,在x64环境下为8字节)。

第17-第20行,对数组取引用,其地址和本身的地址是一样,而指针则不一样。

第23行,当调用函数的时候,数组会转换为指针,因此长度为4,并且可以做自加运算。

规则1.2.1:断言必须使用宏定义,禁止直接调用系统提供的assert()

断言只能在调试版使用,断言被触发后,程序会立即退出,因此严禁在正式发布版本使用断言,请通过编译选项进行控制。 错误用法如:

int Foo(int *array, int size)
{
    assert(array != NULL);
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5

ASSERT()是MFC的宏4,ASSERT只有在Debug版本中才有效,如果编译为Release版本则被忽略。
assert()的功能类似,它是ANSI C标准中规定的函数,它与ASSERT的一个重要区别是可以用在Release版本中5

在msvc16里面是这样定义的:

#undef assert

#ifdef NDEBUG

    #define assert(expression) ((void)0)

#else

    _ACRTIMP void __cdecl _wassert(
        _In_z_ wchar_t const* _Message,
        _In_z_ wchar_t const* _File,
        _In_   unsigned       _Line
        );

    #define assert(expression) (void)(                                                       \
            (!!(expression)) ||                                                              \
            (_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \
        )

#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在MinGW里面

/* According to C99 standard (section 7.2) the assert
   macro shall be redefined each time assert.h gets
   included depending on the status of NDEBUG macro.  */
#undef assert
...
#ifdef NDEBUG
#define assert(_Expression) ((void)0)
#else /* !defined (NDEBUG) */
#if defined(_UNICODE) || defined(UNICODE)
#define assert(_Expression) \
 (void) \
 ((!!(_Expression)) || \
  (_wassert(_CRT_WIDE(#_Expression),_CRT_WIDE(__FILE__),__LINE__),0))
#else /* not unicode */
#define assert(_Expression) \
 (void) \
 ((!!(_Expression)) || \
  (_assert(#_Expression,__FILE__,__LINE__),0))
#endif /* _UNICODE||UNICODE */
#endif /* !defined (NDEBUG) */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

综上所述,使用语言自带的assert即可,也没有其他的选择。当在Release环境下,assert也自动编译为((void)0)对于华为的这条安全规范表示不解,希望有人能解答一下。

另外:这里还发现很有意思的东西:

/* According to C99 standard (section 7.2) the assert
macro shall be redefined each time assert.h gets
included depending on the status of NDEBUG macro. */

意思就是每次包含<assert.h>时,都会根据NDEBUG的当前状态重新定义assert宏6

规则1.2.3:严禁在断言内改变运行环境

在程序正式发布阶段,断言不会被编译进去,为了确保调试版和正式版的功能一致性,严禁在断言中使用任何赋值、修改变量、资源操作、内存申请等操作。 例如,以下的断言方式是错误的:

ASSERT(p1 = p2); //p1被修改
ASSERT(i++ > 1000); //i被修改
ASSERT(close(fd) == 0);//fd被关闭
  • 1
  • 2
  • 3

建议1.2.1:不要将多条语句放在同一个断言中

为了更加准确地发现错误的位置,每一条断言只校验一个条件。 下面的断言同时校验多个条件,在断言触发的时
候,无法判断到底是哪一个条件导致的错误:

int Foo(int *array, int size)
{
	ASSERT(array != NULL && size > 0 && size < MAX_SIZE);
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5

应该将每个条件分开:

int Foo(int *array, int size)
{
    ASSERT(array != NULL);
    ASSERT(size > 0);
    ASSERT(size < MAX_SIZE);
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

规则1.3.2:严禁对公共接口API函数的参数进行ASSERT操作

对于设计成API的函数,必须对参数进行合法性判断,严禁在API实现过程中产生RASH。对API函数的参数进行ASSERT操作是没有意义的。 例如,对于提供应用服务器IP的平台公共API接口这样实现是错误的:

int GetServerIP(char *ip, size_t ipSize)
{
    ASSERT(ip != NULL);
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5

公共接口API应当对输入参数进行代码检查:

int GetServerIP(char *ip, size_t ipSize)
{
    if (ip == NULL) {
    	...
    }
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

建议1.3.1:谨慎使用不可重入函数

不可重入函数在多线程环境下其执行结果不能达到预期效果,需谨慎使用。常见的不可重入函数包括:
rand, srand
getenv, getenv_s
strtok
strerror
asctime, ctime, localtime, gmtime
setlocale
atomic_init
tmpnam
mbrtoc16, c16rtomb, mbrtoc32, c32rtomb
gethostbyaddr
gethostbyname
inet_ntoa

在这里插入图片描述7

建议1.3.2:字符串或指针作为函数参数时,请检查参数是否为NULL

如果字符串或者指针作为函数参数,为了防止空指针引用错误,在引用前必须确保该参数不为NULL,如果上层调用者已经保证了该参数不可能为NULL,在调用本函数时,在函数开始处可以加ASSERT进行校验。 例如下面的代码,因为BYTE *p有可能为NULL,因此在使用前需要进行判断。

int Foo(int *p, int count)
{
    if (p != NULL && count > 0) {
        int c = p[0];
    }
    ...
}
int Foo2()
{
    int *arr = ...
    int count = ...
    Foo(arr, count);
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

下面的代码,由于p的合法性由调用者保证,对于Foo函数,不可能出现p为NULL的情况,因此加上ASSERT进行校验。

int Foo(int *p, int count)
{
    ASSERT(p != NULL); //ASSERT is added to verify p.
    ASSERT(count > 0);
    int c = p[0];
    	...
    }
int Foo2()
{
    int *arr = ...
    int count = ...
    ...
    if (arr != NULL && count > 0) {
    	Foo(arr, count);
    }
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

规则1.5.1:禁用C++异常机制

严禁使用C++的异常机制,所有的错误都应该通过错误值在函数之间传递并做相应的判断, 而不应该通过异常机制进行错误处理。 编码人员必须完全掌控整个编码过程,建立攻击者思维,增强安全编码意识,主动把握有可能出错的环节。而使用C++异常机制进行错误处理,会削弱编码人员的安全意识。 异常机制会打乱程序的正常执行流程,使程序结构更加复杂,原先申请的资源可能会得不到有效清理。 异常机制导致代码的复用性降低,使用了异常机制的代码,不能直接给不使用异常机制的代码复用。 异常机制在实现上依赖于编译器、操作系统、处理器,使用异常机制,导致程序执行性能降低。 在二进制层面,程序被加载后,异常处理函数增加了程序的被攻击面,攻击者可以通过覆盖异常处理函数地址,达到攻击的效果。 例外: 在接管C++语言本身抛出的异常(例如new失败、STL)、第三方库(例如IDL)抛出的异常时,可以使用异常机制,例如:

int len = ...;
char *p = NULL;
try {
	p = new char[len];
}
catch (bad_alloc) {
    ...
    abort();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

相关指南:
Google C++ Style Guide.Exceptions: We do not use C++ exceptions.

这点在《游戏引擎架构8》中也提到过。

规则1.6.3:严禁在构造函数中创建线程

构造函数内仅作成员变量的初始化工作,其他的操作通过成员函数完成。

规则1.6.5:如果类的公共接口中返回类的私有数据地址,则必须加const类

实例:

class CMsg {
public:
    CMsg();
    ~CMsg();
    Const unsigned char *GetMsg();
protected:
    int size;
    unsigned char *msg;
};
CMsg::CMsg()
{
    size = 0;
    msg = NULL;
}
const unsigned char *CMsg::GetMsg()
{
	return msg;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

规则1.7.3:禁用pthread_exit、ExitThread函数

严禁在线程内主动终止自身线程,线程函数在执行完毕后会自动、安全地退出。主动终止自身线程的操作,不仅导致代码复用性变差,同时容易导致资源泄漏错误。

建议1.7.1:禁用exit、ExitProcess函数(main函数除外)

程序应该安全退出,除了main函数以外,禁止任何地方调用exit、ExitProcess函数退出进程。直接退出进程会导致代码的复用性降低,资源得不到有效地清理。 程序应该通过错误值传递的机制进行错误处理。 以下代码加载文件,加载过程中如果出错,直接调用exit退出:

void LoadFile(const char *filePath)
{
    FILE* fp = fopen(filePath, "rt");
    if (fp == NULL) {
    	exit(0);
    }
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

正确的做法应该通过错误值传递机制,例如:

BOOL LoadFile(const char *filePath)
{
    BOOL ret = FALSE;
    FILE* fp = fopen(filePath, "rt");
    if (fp != NULL) {
    	...
    }
    ...
    return ret;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

建议1.7.2:禁用abort函数

abort会导致程序立即退出,资源得不到清理。 例外: 只有发生致命错误,程序无法继续执行的时候,在错误处理函数中使用abort退出程序,例如:

void FatalError(int sig)
{
	abort();
}
int main(int argc, char *argv[])
{
       signal(SIGSEGV, FatalError);
       ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

规则2.5:调用格式化函数时,禁止format参数由外部可控

调用格式化函数时,如果format参数由外部可控,会造成字符串格式化漏洞。 这些格式化函数有: 格式化输出函数:xxxprintf 格式化输入函数:xxxscanf 格式化错误消息函数:err(),verr(),errx(),verrx(),warn(),vwarn(),warnx(),vwarnx(),error(),error_at_line(); 格式化日志函数:syslog(),vsyslog()。
错误示例:

char *msg = GetMsg();
...
printf(msg);
  • 1
  • 2
  • 3

推荐做法:

char *msg = GetMsg();
...
printf("%s\n", msg);
  • 1
  • 2
  • 3

相关指南:
CERT.FIO47-C. Use valid format strings
MITRE.CWE-134: Use of Externally-Controlled Format String

规则4.2:整型表达式比较或赋值为一种更大类型之前必须用这种更大类型对它进行求值

由于整数在运算过程中可能溢出,当运算结果赋值给比他更大类型,或者和比他更大类型进行比较时,会导致实际结果与预期结果不符。 请观察以下二个代码及其输出:

int main(int argc, char *argv[])
{
    unsigned int a = 0x10000000;
    unsigned long long b = a * 0xab;
    printf("b = %llX\n", b);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

输出: b = B0000000

int main(int argc, char *argv[])
{
    unsigned int a = 0x10000000;
    unsigned long long b = (unsigned long long )a * 0xab;
    printf("b = %llX\n", b);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

输出: b = AB0000000

规则4.3:禁止对有符号整数进行位操作符运算

位操作符(~、>>、<<、&、^、|)应该只用于无符号整型操作数。 错误示例:

int data = ReadByte();
int a = data >> 24;
  • 1
  • 2

推荐做法:(为简化示例代码,此处假设ReadByte函数实际不存在返回值小于0的情况)

unsigned int data = (unsigned int)ReadByte();
unsigned int a = data >> 24;
  • 1
  • 2

相关指南:
CERT.INT13-C. Use bitwise operators only on unsigned operands
MISRA.C.2004.Rule 12.7 (required): Bitwise operators shall not be applied to operands whose underlyingtype is signed.

在C语言中,如果在未知的有符号上执行位操作,很可能会导致缓冲区溢出,从而在某些情况下导致攻击者执行任意代码,同时,还可能会出现出乎意料的行为或编译器定义的行为。

代码如下:

#include<stdio.h>
int main()
{
    int y=0x80000000;
	printf("%d\n",y>>24);//以十进制有符号形式输出。
	printf("%u\n",y>>24);//以十进制无符号形式输出。
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

输出为:

-128
4294967168
  • 1
  • 2

由于int类型的最高位是符号位,剩下的31位才用来存储数值,y>>24的结果应该是0xffffff80(负数右移,左补1),当我们以无符号整型输出时,为正数的0xffffff80,以有符号整型输出时,应减一再取反,结果为-1289

然而,在右移运算中,空出的位用 0 还是符号位进行填充是由于具体的 C 语言编译器实现来决定。在通常情况下,如果要进行移位的操作数是无符号类型的,那么空出的位将用 0 进行填充;如果要进行移位的操作数是有符号类型的,则 C 语言编译器实现既可选择 0 来进行填充,也可选择符号位进行填充。

因此,如果很关心一个右移运算中的空位,那么可以使用 unsigned 修饰符来声明变量,这样空位都会被设置为 0。同时,如果一个程序采用了有符号数的右移位操作,那么它就是不可移植的10

规则4.4:禁止整数与指针间的互相转化

指针的大小随着平台的不同而不同,强行进行整数与指针间的互相转化,降低了程序的兼容性,在转换过程中可能引起指针高位信息的丢失。
错误示例:

char *ptr = ...;
unsigned int number = (unsigned int)ptr;
  • 1
  • 2

推荐做法:

char *ptr = ...;
uintptr_t number = (uintptr_t)ptr;
  • 1
  • 2

相关指南:
CERT.INT36-C. Converting a pointer to integer or integer to pointer
MISRA.C.2004.Rule 11.3 (advisory): A cast should not be performed between a pointer type and an integral type.

规则4.5:禁止对指针进行逻辑或位运算(&&、||、!、~、>>、<<、&、^、|)

对指针进行逻辑运算,会导致指针的性质改变,可能产生内存非法访问的问题。 下面是错误的用法:

BOOL dealName(const char *nameA, const char *nameB)
{
    ...
    if (nameA)
    ...
    if (!nameB)
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

下面是正确的用法:

BOOL dealName(const char *nameA, const char *nameB)
{
    ...
    if (nameA != NULL)
    ...
    if (nameB == NULL)
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

例外: 为检查地址对齐而对地址指针进行的位运算可以作为例外。
相关指南:
MISRA.C.2004.Rule 12.6 (advisory): The operands of logical operators (&&, || and !) should be effectively Boolean. Expressions that are effectively Boolean should not be used as operands to operators other than (&&, ||, !, =, ==, != and ?

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/240905
推荐阅读
相关标签