《华为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));

相关指南:
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++;//Errorreturn 0;
}

在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

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

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

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

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

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

int Foo(int *array, int size)
{assert(array != NULL);...
}

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

在MinGW里面

/* According to C99 standard (section 7.2) the assertmacro shall be redefined each time assert.h getsincluded 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) */

综上所述,使用语言自带的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.1:不要将多条语句放在同一个断言中

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

int Foo(int *array, int size)
{ASSERT(array != NULL && size > 0 && size < MAX_SIZE);...
}

应该将每个条件分开:

int Foo(int *array, int size)
{ASSERT(array != NULL);ASSERT(size > 0);ASSERT(size < MAX_SIZE);...
}

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

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

int GetServerIP(char *ip, size_t ipSize)
{ASSERT(ip != NULL);...
}

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

int GetServerIP(char *ip, size_t ipSize)
{if (ip == NULL) {...}...
}

建议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);...
}

下面的代码,由于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.5.1:禁用C++异常机制

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

int len = ...;
char *p = NULL;
try {p = new char[len];
}
catch (bad_alloc) {...abort();
}

相关指南:
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.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);}...
}

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

BOOL LoadFile(const char *filePath)
{BOOL ret = FALSE;FILE* fp = fopen(filePath, "rt");if (fp != NULL) {...}...return ret;
}

建议1.7.2:禁用abort函数

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

void FatalError(int sig)
{abort();
}
int main(int argc, char *argv[])
{signal(SIGSEGV, FatalError);...
}

规则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);

推荐做法:

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

相关指南:
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;
}

输出: 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;
}

输出: b = AB0000000

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

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

int data = ReadByte();
int a = data >> 24;

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

unsigned int data = (unsigned int)ReadByte();
unsigned int a = data >> 24;

相关指南:
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;
}

输出为:

-128
4294967168

由于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;

推荐做法:

char *ptr = ...;
uintptr_t number = (uintptr_t)ptr;

相关指南:
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)...
}

下面是正确的用法:

BOOL dealName(const char *nameA, const char *nameB)
{...if (nameA != NULL)...if (nameB == NULL)...
}

例外: 为检查地址对齐而对地址指针进行的位运算可以作为例外。
相关指南:
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 ?

《华为CC++语言安全规范》笔记相关推荐

  1. 《信贷的逻辑与常识》笔记

    序 银行信贷风险管理的反思 现状与趋势 银行贷款的质量变化与经济周期.宏观调控政策等存在很高的相关性 现在银行不良贷款的增加主要是前几年经济快速增长时企业过度投资.银行过度放贷所带来的结果. 从历史情 ...

  2. AI公开课:19.02.27周逵(投资人)《AI时代的投资逻辑》课堂笔记以及个人感悟

    AI公开课:19.02.27周逵(投资人)<AI时代的投资逻辑>课堂笔记以及个人感悟 目录 课堂PPT图片 精彩语录 个人感悟 课堂PPT图片 精彩语录 更新中-- 文件图片已经丢失-- ...

  3. 人工智能入门算法逻辑回归学习笔记

    逻辑回归是一个非常经典的算法,其中也包含了非常多的细节,曾看到一句话:如果面试官问你熟悉哪个机器学习模型,可以说 SVM,但千万别说 LR,因为细节真的太多了. 秉持着精益求精的工匠精神不断对笔记进行 ...

  4. 【逻辑回归学习笔记】

    算法描述 1.逻辑回归要做的事就是寻找分界面实现二分类. 2.问题假设:对一堆三角形和正方形分类. 3.数据输入:已知正方形和三角形的坐标和标签. 4.算法过程: 知识储备 1.分类和回归 ①分类的目 ...

  5. 逻辑回归函数学习笔记

    继续逻辑回归学习,今日笔记记录. 1.逻辑回归和线性回归的关系:对逻辑回归的概率比取自然对数,则得到的是一个线性函数,推导过程如下. 首先,看逻辑回归的定义 其次,计算两个极端y/(1-y),其值为( ...

  6. 2.2 逻辑回归-机器学习笔记-斯坦福吴恩达教授

    逻辑回归 上一节我们知道,使用线性回归来处理 0/1 分类问题总是困难重重的,因此,人们定义了逻辑回归来完成 0/1 分类问题,逻辑一词也代表了是(1) 和 非(0). Sigmoid预测函数 在逻辑 ...

  7. LVM逻辑卷分区笔记

    磁盘的静态分区有其缺点:分区大小难评估,估计不准确,当分区空间不够用的时候,系统管理员可能需要先备份整个系统,清除磁盘空间,然后重新对磁盘进行分区,然后恢复磁盘数据到新分区,且需要停机一段时间进行恢复 ...

  8. 适合理工直男的钟平老师逻辑英语学习笔记

    一切的一切都只是套路!             --鲁迅 核心公式: En: (状语1) 主(定语1) 谓(状语2) (宾)(定语2) (状语1) Ch: (状语1) (定语1)主 (状语2)谓 (定 ...

  9. 【数字逻辑】学习笔记 第四章 Part2 常用组合逻辑电路与竞争、险象

    文章目录 一.常用组合逻辑电路 1. 译码器 (1) 二进制译码器 74LS138(3/8译码器) a. 一般符号和图形符号 b. 74LS138功能表 c. 两片 `74LS138` 构成 `4-1 ...

  10. 线性回归、逻辑回归学习笔记

    学习源代码 import numpy as np import matplotlib.pyplot as plt def true_fun(X): # 这是我们设定的真实函数,即ground trut ...

最新文章

  1. 初识Tcl(三):Tcl 变量及运算符
  2. Linux+Apache+MySQL+PHP5的安装与配置与phpBB2论坛的架设
  3. 插入透明32位png格式图片支持ie5.5+、 FF、chrome、safari
  4. 成功解决ImportError: Could not find 'msvcp140.dll'. TensorFlow requires that this DLL be installed in a
  5. hive mysql编码问题_Hive中文乱码 生产环境问题解决
  6. 【POJ 1860】Currency Exchange
  7. java项目经验总结
  8. macOS上使用aircrack-ng暴力破解Wi-Fi密码
  9. 斗鱼扩展--移除广告优化页面(五)
  10. QML 导入ttf图标库
  11. ubuntu wifi bcm4322 安装驱动
  12. [内附完整源码和文档] 基于Java的学生学籍管理系统
  13. 我的世界java版变形模组下载_我的世界变形模组
  14. VIL100数据集处理
  15. python中英文字频率_python实现统计文本中单词出现的频率详解
  16. html5对代码自动排版,HTML5系列:通过JS+DIV+CSS排版布局实现选项卡效果
  17. oracle 京东,【京东工资】oracle dba待遇-看准网
  18. 同步任务和异步任务(微任务和宏任务)
  19. python3.7魔塔游戏_基于pygame的开发:魔塔小游戏开发
  20. 架构师之路-网络框架的搭建(高逼格)

热门文章

  1. 开源软件漏洞升级步骤
  2. 华为网络配置(DHCP)
  3. 智慧城市智能化建设发展现状及展望
  4. 怎么设置织梦栏目html结尾,dedecms网站栏目地址url优化成.html结尾的而不是文件夹形式结尾的。请大家来帮忙。...
  5. 好看的个人网站源码_新手想建个人网站,都要注意哪些关于自助建站源码的坑?...
  6. 连续词袋模型(Continous bag of words, CBOW)
  7. Kindle接入HomeAssistant:实现锁屏壁纸显示HA内设备信息并在HA内获取Kindle电量
  8. 用python计算100以内所有奇数的和_python怎样求1到100的奇数和
  9. 《稀缺》塞德希尔·穆来纳森 / 埃尔德·沙菲尔
  10. U3D里UI相关逻辑和文件的组织关系的各种形式的比较