《华为CC++语言安全规范》笔记
《華為C&C++語言安全規范》筆記
通過閱讀《華為C&C++語言安全規范》1,我了解到了我在編程中很多缺失的部分。現在記錄下幾個要點:
規則1.1.4:嚴禁對指針變量進行sizeof操作
編碼人員往往由于粗心,將指針當做數組進行sizeof操作,導致實際的執行結果與預期不符。 下面的代碼,buffer和path分別是指針和數組,編碼人員想對這2個內存進行清0操作,但由于編碼人員的疏忽,第5行代碼,將內存大小誤寫成了sizeof,與預期不符。
如果要判斷當前的指針類型大小,請使用sizeof(char *)的方式。
相關指南:
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);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("%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 = ...; 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 ?😃.
規則5.1:內存申請前,必須對申請內存大小進行合法性校驗
內存申請的大小可能來自于外部數據,必須檢查其合法性,防止過多地、非法地申請內存。不能申請0長度的內存。 例如:
int Foo(int size) {if (size <= 0) {//error...}...char *msg = (char *)malloc(size);... }相關指南:
CERT.MEM04-C. Beware of zero-length allocations
MITRE.CWE-789: Uncontrolled Memory Allocation
規則5.2:內存分配后必須判斷是否成功
char *msg = (char *)malloc(size); if (msg != NULL) {... }相關指南:
CERT.MEM11-C. Do not assume infinite heap space
CERT.ERR33-C. Detect and handle standard library errors
CERT.MEM52-CPP. Detect and handle memory allocation errors
MITRE.CWE 252, Unchecked Return Value
MITRE.CWE 391, Unchecked Error Condition
MITRE.CWE 476, NULL Pointer Dereference
MITRE.CWE 690, Unchecked Return Value to NULL Pointer Dereference
MITRE.CWE 703, Improper Check or Handling of Exceptional Conditions
MITRE.CWE 754, Improper Check for Unusual or Exceptional Conditions
規則5.3:禁止引用未初始化的內存
malloc、new分配出來的內存沒有被初始化為0,要確保內存被引用前是被初始化的。 以下代碼使用malloc申請內存,在使用前沒有初始化:
int *CalcMetrixColomn( int **metrix ,int *param, size_t size ) {int *result = NULL;...size_t bufSize = size * sizeof(int);...result = (int *)malloc(bufSize);...result[0] += metrix[0][0] * param[0];...return result; }以下代碼使用memset_s()對分配出來的內存清零。
int *CalcMetrixColomn(int **metrix ,int *param, size_t size) {int *result = NULL;...size_t bufSize = size * sizeof(int);...result = (int *)malloc(bufSize);...int ret = memset_s(result, bufSize, 0, bufSize); //【修改】確保內存被初始化后才被引用...result[0] += metrix[0][0] * param[0];...return result; }相關指南:
CERT.EXP33-C. Do not read uninitialized memory
CERT.EXP53-CPP. Do not read uninitialized memory
規則5.4:內存釋放之后立即賦予新值
懸掛指針可能會導致雙重釋放(double-free)以及訪問已釋放內存的危險。消除懸掛指針以及消除眾多與內存相關危險的一個最為有效地方法就是當指針使用完后將其置新值。 如果一個指針釋放后能夠馬上離開作用域,因為它已經不能被再次訪問,因此可以無需對其賦予新值。
示例:
相關指南:
CERT.MEM01-C. Store a new value in pointers immediately after free()
CERT.MEM50-CPP. Do not access freed memory
規則5.5:禁止使用realloc()函數
realloc()原型如下:
void *realloc(void *ptr, size_t size);隨著參數的不同,其行為也是不同。 1) 當ptr不為NULL,且size不為0時,該函數會重新調整內存大小,并將新的內存指針返回,并保證最小的size的內容不變; 2) 參數ptr為NULL,但size不為0,那么行為等同于malloc(size); 3) 參數size為0,則realloc的行為等同于free(ptr)。 由此可見,一個簡單的C函數,卻被賦予了3種行為,這不是一個設計良好的函數。雖然在編碼中提供了一些便利性,但是卻極易引發各種bug。
相關指南:
CERT.MEM36-C. Do not modify the alignment of objects by calling realloc()
規則5.6:禁止使用alloca()函數申請棧上內存
POSIX和C99均未定義alloca()的行為,在有些平臺下不支持該函數,使用alloca會降低程序的兼容性和可移植性,該函數在棧幀里申請內存,申請的大小很可能超過棧的邊界,影響后續的代碼執行。 請使用malloc或new,從堆中動態分配內存。
規則7.1:創建文件時必須顯式指定合適的文件訪問權限
創建文件時,如果不顯式指定合適訪問權限,可能會讓未經授權的用戶訪問該文件。 下列代碼沒有顯式配置文件的訪問權限。
int fd = open(fileName, O_CREAT | O_WRONLY); //【錯誤】缺少訪問權限設置推薦做法:
int fd = open(fileName, O_CREAT | O_WRONLY, S_IRUSR|S_IWUSR);規則8.3:嚴禁使用string類存儲敏感信息
string類是C++內部定義的字符串管理類,如果口令等敏感信息通過string進行操作,在程序運行過程中,敏感信息可能會散落到內存的各個地方,并且無法清0。 以下代碼,Foo函數中獲取密碼,保存到string變量password中,隨后傳遞給VerifyPassword函數,在這個過程中,password實際上在內存中出現了2份。
int VerifyPassword(string password) {//... } int Foo() {string password = GetPassword();VerifyPassword(password);... }應該使用char或unsigned char保存敏感信息,如下代碼:
int VerifyPassword(const char *password) {//... } int Foo() {char password[MAX_PASSWORD] = {0};GetPassword(password, sizeof(password));VerifyPassword(password);... }《華為C&C++語言安全規范》 https://www.jb51.net/books/720904.html ??
C/C++數組名與指針區別深入探索_ljob2006的博客-CSDN博客 https://blog.csdn.net/ljob2006/article/details/4872167 ??
c中,數組名跟指針有區別嗎? - 知乎 https://www.zhihu.com/question/41805285 ??
大小寫 ASSERT 有什么區別_百度知道 https://zhidao.baidu.com/question/110720542.html ??
問題: 什么是ASSERT()? ASSERT()和assert()的區別是什么?_流風的專欄-CSDN博客 https://blog.csdn.net/procedurecode/article/details/1942115 ??
第一章assert.h_暮秋小屋-CSDN博客 https://blog.csdn.net/qq_49150256/article/details/112753240 ??
可重入函數對于線程安全的意義(附函數表) - 云+社區 - 騰訊云 https://cloud.tencent.com/developer/article/1685935 ??
游戲引擎架構 (豆瓣) https://book.douban.com/subject/25815142/ ??
有符號執行位操作導致的BUG_Tonson_的博客-CSDN博客 https://blog.csdn.net/weixin_44444450/article/details/109668780 ??
位操作及其使用注意事項,C語言位操作及其使用方法詳解 http://c.biancheng.net/view/362.html ??
總結
以上是生活随笔為你收集整理的《华为CC++语言安全规范》笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux--安装iRedMail惊魂记
- 下一篇: 巅峰战舰正在连接服务器,人气冲天《巅峰战