├── C,C++安全指南.md
├── CONTRIBUTING.md
├── Go安全指南.md
├── JavaScript安全指南.md
├── Java安全指南.md
├── LICENSE
├── Python安全指南.md
└── README.md
/C,C++安全指南.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 目录
5 |
6 | - [1 通用安全指南](#1)
7 | * [I. C/C++使用错误](#1.1)
8 | + [1.1 不得直接使用无长度限制的字符拷贝函数](#1.1.1)
9 | + [1.2 创建进程类的函数的安全规范](#1.1.2)
10 | + [1.3 尽量减少使用 _alloca 和可变长度数组](#1.1.3)
11 | + [1.4 printf系列参数必须对应](#1.1.4)
12 | + [1.5 防止泄露指针(包括%p)的值](#1.1.5)
13 | + [1.6 不应当把用户可修改的字符串作为printf系列函数的“format”参数](#1.1.6)
14 | + [1.7 对数组delete时需要使用delete[]](#1.1.7)
15 | + [1.8 注意隐式符号转换](#1.1.8)
16 | + [1.9 注意八进制问题](#1.1.9)
17 | * [II. 不推荐的编程习惯](#1.2)
18 | + [2.1 switch中应有default](#1.2.1)
19 | + [2.2 不应当在Debug或错误信息中提供过多内容](#1.2.2)
20 | + [2.3 不应该在客户端代码中硬编码对称加密秘钥](#1.2.3)
21 | + [2.4 返回栈上变量的地址](#1.2.4)
22 | + [2.5 有逻辑联系的数组必须仔细检查](#1.2.5)
23 | + [2.6 避免函数的声明和实现不同](#1.2.6)
24 | + [2.7 检查复制粘贴的重复代码](#1.2.7)
25 | + [2.8 左右一致的重复判断/永远为真或假的判断](#1.2.8)
26 | + [2.9 函数每个分支都应有返回值](#1.2.9)
27 | + [2.10 不得使用栈上未初始化的变量](#1.2.10)
28 | + [2.11 不得直接使用刚分配的未初始化的内存(如realloc)](#1.2.11)
29 | + [2.12 校验内存相关函数的返回值](#1.2.12)
30 | + [2.13 不要在if里面赋值](#1.2.13)
31 | + [2.14 确认if里面的按位操作](#1.2.14)
32 | * [III. 多线程](#1.3)
33 | + [3.1 变量应确保线程安全性](#1.3.1)
34 | + [3.2 注意signal handler导致的条件竞争](#1.3.2)
35 | + [3.3 注意Time-of-check Time-of-use条件竞争](#1.3.3)
36 | * [IV. 加密解密](#1.4)
37 | + [4.1 不得明文存储用户密码等敏感数据](#1.4.1)
38 | + [4.2 内存中的用户密码等敏感数据应该安全抹除](#1.4.2)
39 | + [4.3 rand() 类函数应正确初始化](#1.4.3)
40 | + [4.4 在需要高强度安全加密时不应使用弱PRNG函数](#1.4.4)
41 | + [4.5 自己实现的rand范围不应过小](#1.4.5)
42 | * [V. 文件操作](#1.5)
43 | + [5.1 避免路径穿越问题](#1.5.1)
44 | + [5.2 避免相对路径导致的安全问题](#1.5.2)
45 | + [5.3 文件权限控制](#1.5.3)
46 | * [Ⅵ. 内存操作](#1.6)
47 | + [6.1 防止各种越界写](#1.6.1)
48 | + [6.2 防止任意地址写](#1.6.2)
49 | * [Ⅶ. 数字操作](#1.7)
50 | + [7.1 防止整数溢出](#1.7.1)
51 | + [7.2 防止Off-By-One](#1.7.2)
52 | + [7.3 避免大小端错误](#1.7.3)
53 | + [7.4 检查除以零异常](#1.7.4)
54 | + [7.5 防止数字类型的错误强转](#1.7.5)
55 | + [7.6 比较数据大小时加上最小/最大值的校验](#1.7.6)
56 | * [Ⅷ. 指针操作](#1.8)
57 | + [8.1 检查在pointer上使用sizeof](#1.8.1)
58 | + [8.2 检查直接将数组和0比较的代码](#1.8.2)
59 | + [8.3 不应当向指针赋予写死的地址](#1.8.3)
60 | + [8.4 检查空指针](#1.8.4)
61 | + [8.5 释放完后置空指针](#1.8.5)
62 | + [8.6 防止错误的类型转换](#1.8.6)
63 | + [8.7 智能指针使用安全](#1.8.7)
64 |
65 |
66 |
67 | ## 通用安全指南
68 |
69 |
70 | ### 1 C/C++使用错误
71 |
72 |
73 | #### 1.1 【必须】不得直接使用无长度限制的字符拷贝函数
74 |
75 | 不应直接使用legacy的字符串拷贝、输入函数,如strcpy、strcat、sprintf、wcscpy、mbscpy等,这些函数的特征是:可以输出一长串字符串,而不限制长度。如果环境允许,应当使用其_s安全版本替代,或者使用n版本函数(如:snprintf,vsnprintf)。
76 |
77 | 若使用形如sscanf之类的函数时,在处理字符串输入时应当通过%10s这样的方式来严格限制字符串长度,同时确保字符串末尾有\0。如果环境允许,应当使用_s安全版本。
78 |
79 | 但是注意,虽然MSVC 2015时默认引入结尾为0版本的`snprintf`(行为等同于C99定义的`snprintf`)。但更早期的版本中,MSVC的`snprintf`可能是`_snprintf`的宏。而`_snprintf`是不保证\0结尾的(见本节后半部分)。
80 |
81 | ```c++
82 | (MSVC)
83 | Beginning with the UCRT in Visual Studio 2015 and Windows 10, snprintf is no longer identical to _snprintf. The snprintf function behavior is now C99 standard compliant.
84 |
85 | 从Visual Studio 2015和Windows 10中的UCRT开始,snprintf不再与_snprintf相同。snprintf函数行为现在符合C99标准。
86 |
87 | 请参考:https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/snprintf-snprintf-snprintf-l-snwprintf-snwprintf-l?redirectedfrom=MSDN&view=vs-2019
88 | ```
89 |
90 | 因此,在使用n系列拷贝函数时,要确保正确计算缓冲区长度,同时,如果你不确定是否代码在各个编译器下都能确保末尾有0时,建议可以适当增加1字节输入缓冲区,并将其置为\0,以保证输出的字符串结尾一定有\0。
91 |
92 | ```c++
93 | // Good
94 | char buf[101] = {0};
95 | snprintf(buf, sizeof(buf) - 1, "foobar ...", ...);
96 | ```
97 |
98 | 一些需要注意的函数,例如`strncpy`和`_snprintf`是不安全的。 `strncpy`不应当被视为`strcpy`的n系列函数,它只是恰巧与其他n系列函数名字很像而已。`strncpy`在复制时,如果复制的长度超过n,不会在结尾补\0。
99 |
100 | 同样,MSVC `_snprintf`系列函数在超过或等于n时也不会以0结尾。如果后续使用非0结尾的字符串,可能泄露相邻的内容或者导致程序崩溃。
101 |
102 | ```c++
103 | // Bad
104 | char a[4] = {0};
105 | _snprintf(a, 4, "%s", "AAAA");
106 | foo = strlen(a);
107 | ```
108 |
109 | 上述代码在MSVC中执行后, a[4] == 'A',因此字符串未以0结尾。a的内容是"AAAA",调用`strlen(a)`则会越界访问。因此,正确的操作举例如下:
110 |
111 | ```c++
112 | // Good
113 | char a[4] = {0};
114 | _snprintf(a, sizeof(a), "%s", "AAAA");
115 | a[sizeof(a) - 1] = '\0';
116 | foo = strlen(a);
117 | ```
118 |
119 | 在 C++ 中,强烈建议用 `string`、`vector` 等更高封装层次的基础组件代替原始指针和动态数组,对提高代码的可读性和安全性都有很大的帮助。
120 |
121 | 关联漏洞:
122 |
123 | `中风险-信息泄露`
124 |
125 | `低风险-拒绝服务`
126 |
127 | `高风险-缓冲区溢出`
128 |
129 |
130 | #### 1.2 【必须】创建进程类的函数的安全规范
131 |
132 | system、WinExec、CreateProcess、ShellExecute等启动进程类的函数,需要严格检查其参数。
133 |
134 | 启动进程需要加上双引号,错误例子:
135 |
136 | ```c++
137 | // Bad
138 | WinExec("D:\\program files\\my folder\\foobar.exe", SW_SHOW);
139 | ```
140 |
141 | 当存在`D:\program files\my.exe`的时候,my.exe会被启动。而foobar.exe不会启动。
142 |
143 | ```c++
144 | // Good
145 | WinExec("\"D:\\program files\\my folder\\foobar.exe\"", SW_SHOW);
146 | ```
147 |
148 | 另外,如果启动时从用户输入、环境变量读取组合命令行时,还需要注意是否可能存在命令注入。
149 |
150 | ```c++
151 | // Bad
152 | std::string cmdline = "calc ";
153 | cmdline += user_input;
154 | system(cmdline.c_str());
155 | ```
156 |
157 | 比如,当用户输入`1+1 && ls`时,执行的实际上是calc 1+1和ls 两个命令,导致命令注入。
158 |
159 | 需要检查用户输入是否含有非法数据。
160 |
161 | ```c++
162 | // Good
163 | std::string cmdline = "ls ";
164 | cmdline += user_input;
165 |
166 | if(cmdline.find_first_not_of("1234567890.+-*/e ") == std::string::npos)
167 | system(cmdline.c_str());
168 | else
169 | warning(...);
170 | ```
171 |
172 | 关联漏洞:
173 |
174 | `高风险-代码执行`
175 |
176 | `高风险-权限提升`
177 |
178 |
179 | #### 1.3 【必须】尽量减少使用 _alloca 和可变长度数组
180 |
181 | _alloca 和[可变长度数组](https://zh.wikipedia.org/wiki/%E5%8F%AF%E5%8F%98%E9%95%BF%E6%95%B0%E7%BB%84)使用的内存量在编译期间不可知。尤其是在循环中使用时,根据编译器的实现不同,可能会导致:(1)栈溢出,即拒绝服务; (2)缺少栈内存测试的编译器实现可能导致申请到非栈内存,并导致内存损坏。这在栈比较小的程序上,例如IoT设备固件上影响尤为大。对于 C++,可变长度数组也属于非标准扩展,在代码规范中禁止使用。
182 |
183 | 错误示例:
184 |
185 | ```c++
186 | // Bad
187 | for (int i = 0; i < 100000; i++) {
188 | char* foo = (char *)_alloca(0x10000);
189 | ..do something with foo ..;
190 | }
191 |
192 | void Foo(int size) {
193 | char msg[size]; // 不可控的栈溢出风险!
194 | }
195 | ```
196 |
197 | 正确示例:
198 |
199 | ```c++
200 | // Good
201 | // 改用动态分配的堆内存
202 | for (int i = 0; i < 100000; i++) {
203 | char * foo = (char *)malloc(0x10000);
204 | ..do something with foo ..;
205 | if (foo_is_no_longer_needed) {
206 | free(foo);
207 | foo = NULL;
208 | }
209 | }
210 |
211 | void Foo(int size) {
212 | std::string msg(size, '\0'); // C++
213 | char* msg = malloc(size); // C
214 | }
215 | ```
216 |
217 | 关联漏洞:
218 |
219 | `低风险-拒绝服务`
220 |
221 | `高风险-内存破坏`
222 |
223 |
224 | #### 1.4 【必须】printf系列参数必须对应
225 |
226 | 所有printf系列函数,如sprintf,snprintf,vprintf等必须对应控制符号和参数。
227 |
228 | 错误示例:
229 |
230 | ```c++
231 | // Bad
232 | const int buf_size = 1000;
233 | char buffer_send_to_remote_client[buf_size] = {0};
234 |
235 | snprintf(buffer_send_to_remote_client, buf_size, "%d: %p", id, some_string); // %p 应为 %s
236 |
237 | buffer_send_to_remote_client[buf_size - 1] = '\0';
238 | send_to_remote(buffer_send_to_remote_client);
239 | ```
240 |
241 | 正确示例:
242 |
243 | ```c++
244 | // Good
245 | const int buf_size = 1000;
246 | char buffer_send_to_remote_client[buf_size] = {0};
247 |
248 | snprintf(buffer_send_to_remote_client, buf_size, "%d: %s", id, some_string);
249 |
250 | buffer_send_to_remote_client[buf_size - 1] = '\0';
251 | send_to_remote(buffer_send_to_remote_client);
252 | ```
253 |
254 | 前者可能会让client的攻击者获取部分服务器的原始指针地址,可以用于破坏ASLR保护。
255 |
256 | 关联漏洞:
257 |
258 | `中风险-信息泄露`
259 |
260 |
261 | #### 1.5 【必须】防止泄露指针(包括%p)的值
262 |
263 | 所有printf系列函数,要防止格式化完的字符串泄露程序布局信息。例如,如果将带有%p的字符串泄露给程序,则可能会破坏ASLR的防护效果。使得攻击者更容易攻破程序。
264 |
265 | %p的值只应当在程序内使用,而不应当输出到外部或被外部以某种方式获取。
266 |
267 | 错误示例:
268 |
269 | ```c++
270 | // Bad
271 | // 如果这是暴露给客户的一个API:
272 | uint64_t GetUniqueObjectId(const Foo* pobject) {
273 | return (uint64_t)pobject;
274 | }
275 | ```
276 |
277 | 正确示例:
278 |
279 | ```c++
280 | // Good
281 | uint64_t g_object_id = 0;
282 |
283 | void Foo::Foo() {
284 | this->object_id_ = g_object_id++;
285 | }
286 |
287 | // 如果这是暴露给客户的一个API:
288 | uint64_t GetUniqueObjectId(const Foo* object) {
289 | if (object)
290 | return object->object_id_;
291 | else
292 | error(...);
293 | }
294 | ```
295 |
296 | 关联漏洞:
297 |
298 | `中风险-信息泄露`
299 |
300 |
301 | #### 1.6 【必须】不应当把用户可修改的字符串作为printf系列函数的“format”参数
302 |
303 | 如果用户可以控制字符串,则通过 %n %p 等内容,最坏情况下可以直接执行任意恶意代码。
304 |
305 | 在以下情况尤其需要注意: WIFI名,设备名……
306 |
307 | 错误:
308 |
309 | ```c++
310 | snprintf(buf, sizeof(buf), wifi_name);
311 | ```
312 |
313 | 正确:
314 |
315 | ```c++
316 | snprinf(buf, sizeof(buf), "%s", wifi_name);
317 | ```
318 |
319 | 关联漏洞:
320 |
321 | `高风险-代码执行`
322 |
323 | `高风险-内存破坏`
324 |
325 | `中风险-信息泄露`
326 |
327 | `低风险-拒绝服务`
328 |
329 |
330 | #### 1.7 【必须】对数组delete时需要使用delete[]
331 |
332 | delete []操作符用于删除数组。delete操作符用于删除非数组对象。它们分别调用operator delete[]和operator delete。
333 |
334 | ```c++
335 | // Bad
336 | Foo* b = new Foo[5];
337 | delete b; // trigger assert in DEBUG mode
338 | ```
339 |
340 | 在new[]返回的指针上调用delete将是取决于编译器的未定义行为。代码中存在对未定义行为的依赖是错误的。
341 |
342 | ```c++
343 | // Good
344 | Foo* b = new Foo[5];
345 | delete[] b;
346 | ```
347 |
348 | 在 C++ 代码中,使用 `string`、`vector`、智能指针(比如[std::unique_ptr](https://zh.cppreference.com/w/cpp/memory/unique_ptr))等可以消除绝大多数 `delete[]` 的使用场景,并且代码更清晰。
349 |
350 | 关联漏洞:
351 |
352 | `高风险-内存破坏`
353 |
354 | `中风险-逻辑漏洞`
355 |
356 | `低风险-内存泄漏`
357 |
358 | `低风险-拒绝服务`
359 |
360 |
361 | #### 1.8【必须】注意隐式符号转换
362 |
363 | 两个无符号数相减为负数时,结果应当为一个很大的无符号数,但是小于int的无符号数在运算时可能会有预期外的隐式符号转换。
364 |
365 | ```c++
366 | // 1
367 | unsigned char a = 1;
368 | unsigned char b = 2;
369 |
370 | if (a - b < 0) // a - b = -1 (signed int)
371 | a = 6;
372 | else
373 | a = 8;
374 |
375 | // 2
376 | unsigned char a = 1;
377 | unsigned short b = 2;
378 |
379 | if (a - b < 0) // a - b = -1 (signed int)
380 | a = 6;
381 | else
382 | a = 8;
383 | ```
384 |
385 | 上述结果均为a=6
386 |
387 | ```c++
388 | // 3
389 | unsigned int a = 1;
390 | unsigned short b = 2;
391 |
392 | if (a - b < 0) // a - b = 0xffffffff (unsigned int)
393 | a = 6;
394 | else
395 | a = 8;
396 |
397 | // 4
398 | unsigned int a = 1;
399 | unsigned int b = 2;
400 |
401 | if (a - b < 0) // a - b = 0xffffffff (unsigned int)
402 | a = 6;
403 | else
404 | a = 8;
405 | ```
406 |
407 | 上述结果均为a=8
408 |
409 | 如果预期为8,则错误代码:
410 |
411 | ```c++
412 | // Bad
413 | unsigned short a = 1;
414 | unsigned short b = 2;
415 |
416 | if (a - b < 0) // a - b = -1 (signed int)
417 | a = 6;
418 | else
419 | a = 8;
420 | ```
421 |
422 | 正确代码:
423 |
424 | ```c++
425 | // Good
426 | unsigned short a = 1;
427 | unsigned short b = 2;
428 |
429 | if ((unsigned int)a - (unsigned int)b < 0) // a - b = 0xffff (unsigned short)
430 | a = 6;
431 | else
432 | a = 8;
433 | ```
434 |
435 | 关联漏洞:
436 |
437 | `中风险-逻辑漏洞`
438 |
439 |
440 | #### 1.9【必须】注意八进制问题
441 |
442 | 代码对齐时应当使用空格或者编辑器自带的对齐功能,谨慎在数字前使用0来对齐代码,以免不当将某些内容转换为八进制。
443 |
444 | 例如,如果预期为20字节长度的缓冲区,则下列代码存在错误。buf2为020(OCT)长度,实际只有16(DEC)长度,在memcpy后越界:
445 |
446 | ```c++
447 | // Bad
448 | char buf1[1024] = {0};
449 | char buf2[0020] = {0};
450 |
451 | memcpy(buf2, somebuf, 19);
452 | ```
453 |
454 | 应当在使用8进制时明确注明这是八进制。
455 |
456 | ```c++
457 | // Good
458 | int access_mask = 0777; // oct, rwxrwxrwx
459 | ```
460 |
461 | 关联漏洞:
462 |
463 | `中风险-逻辑漏洞`
464 |
465 |
466 | ### 2 不推荐的编程习惯
467 |
468 |
469 | #### 2.1 【必须】switch中应有default
470 |
471 | switch中应该有default,以处理各种预期外的情况。这可以确保switch接受用户输入,或者后期在其他开发者修改函数后确保switch仍可以覆盖到所有情况,并确保逻辑正常运行。
472 |
473 | ```c++
474 | // Bad
475 | int Foo(int bar) {
476 | switch (bar & 7) {
477 | case 0:
478 | return Foobar(bar);
479 | break;
480 | case 1:
481 | return Foobar(bar * 2);
482 | break;
483 | }
484 | }
485 | ```
486 |
487 | 例如上述代码switch的取值可能从0~7,所以应当有default:
488 |
489 | ```c++
490 | // Good
491 | int Foo(int bar) {
492 | switch (bar & 7) {
493 | case 0:
494 | return Foobar(bar);
495 | break;
496 | case 1:
497 | return Foobar(bar * 2);
498 | break;
499 | default:
500 | return -1;
501 | }
502 | }
503 | ```
504 |
505 | 关联漏洞:
506 |
507 | `中风险-逻辑漏洞`
508 |
509 | `中风险-内存泄漏`
510 |
511 |
512 | #### 2.2 【必须】不应当在Debug或错误信息中提供过多内容
513 |
514 | 包含过多信息的Debug消息不应当被用户获取到。Debug信息可能会泄露一些值,例如内存数据、内存地址等内容,这些内容可以帮助攻击者在初步控制程序后,更容易地攻击程序。
515 |
516 | ```c++
517 | // Bad
518 | int Foo(int* bar) {
519 | if (bar && *bar == 5) {
520 | OutputDebugInfoToUser("Wrong value for bar %p = %d\n", bar, *bar);
521 | }
522 | }
523 | ```
524 |
525 | 而应该:
526 |
527 | ```c++
528 | // Good
529 | int foo(int* bar) {
530 |
531 | #ifdef DEBUG
532 | if (bar && *bar == 5) {
533 | OutputDebugInfo("Wrong value for bar.\n");
534 | }
535 | #endif
536 |
537 | }
538 | ```
539 |
540 | 关联漏洞:
541 |
542 | `中风险-信息泄漏`
543 |
544 |
545 | #### 2.3 【必须】不应该在客户端代码中硬编码对称加密秘钥
546 |
547 | 不应该在客户端代码中硬编码对称加密秘钥。例如:不应在客户端代码使用硬编码的 AES/ChaCha20-Poly1305/SM1 密钥,使用固定密钥的程序基本和没有加密一样。
548 |
549 | 如果业务需求是认证加密数据传输,应优先考虑直接用 HTTPS 协议。
550 |
551 | 如果是其它业务需求,可考虑由服务器端生成对称秘钥,客户端通过 HTTPS 等认证加密通信渠道从服务器拉取。
552 |
553 | 或者根据用户特定的会话信息,比如登录认证过程可以根据用户名用户密码业务上下文等信息,使用 HKDF 等算法衍生出对称秘钥。
554 |
555 | 又或者使用 RSA/ECDSA + ECDHE 等进行认证秘钥协商,生成对称秘钥。
556 |
557 |
558 | ```c++
559 | // Bad
560 | char g_aes_key[] = {...};
561 |
562 | void Foo() {
563 | ....
564 | AES_func(g_aes_key, input_data, output_data);
565 | }
566 | ```
567 |
568 | 可以考虑在线为每个用户获取不同的密钥:
569 |
570 | ```c++
571 | // Good
572 | char* g_aes_key;
573 |
574 | void Foo() {
575 | ....
576 | AES_encrypt(g_aes_key, input_data, output_data);
577 | }
578 |
579 | void Init() {
580 | g_aes_key = get_key_from_https(user_id, ...);
581 | }
582 | ```
583 |
584 | 关联漏洞:
585 |
586 | `中风险-信息泄露`
587 |
588 |
589 | #### 2.4 【必须】返回栈上变量的地址
590 |
591 | 函数不可以返回栈上的变量的地址,其内容在函数返回后就会失效。
592 |
593 | ```c++
594 | // Bad
595 | char* Foo(char* sz, int len){
596 | char a[300] = {0};
597 | if (len > 100) {
598 | memcpy(a, sz, 100);
599 | }
600 | a[len] = '\0';
601 | return a; // WRONG
602 | }
603 | ```
604 |
605 | 而应当使用堆来传递非简单类型变量。
606 |
607 | ```c++
608 | // Good
609 | char* Foo(char* sz, int len) {
610 | char* a = new char[300];
611 | if (len > 100) {
612 | memcpy(a, sz, 100);
613 | }
614 | a[len] = '\0';
615 | return a; // OK
616 | }
617 | ```
618 |
619 | 对于 C++ 程序来说,强烈建议返回 `string`、`vector` 等类型,会让代码更加简单和安全。
620 |
621 | 关联漏洞:
622 |
623 | `高风险-内存破坏`
624 |
625 |
626 | #### 2.5 【必须】有逻辑联系的数组必须仔细检查
627 |
628 | 例如下列程序将字符串转换为week day,但是两个数组并不一样长,导致程序可能会越界读一个int。
629 |
630 | ```c++
631 | // Bad
632 | int nWeekdays[] = {1, 2, 3, 4, 5, 6};
633 | const char* sWeekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
634 | for (int x = 0; x < ARRAY_SIZE(sWeekdays); x++) {
635 | if (strcmp(sWeekdays[x], input) == 0)
636 | return nWeekdays[x];
637 | }
638 | ```
639 |
640 | 应当确保有关联的nWeekdays和sWeekdays数据统一。
641 |
642 | ```c++
643 | // Good
644 | const int nWeekdays[] = {1, 2, 3, 4, 5, 6, 7};
645 | const char* sWeekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
646 | assert(ARRAY_SIZE(nWeekdays) == ARRAY_SIZE(sWeekdays));
647 | for (int x = 0; x < ARRAY_SIZE(sWeekdays); x++) {
648 | if (strcmp(sWeekdays[x], input) == 0) {
649 | return nWeekdays[x];
650 | }
651 | }
652 | ```
653 |
654 | 关联漏洞:
655 |
656 | `高风险-内存破坏`
657 |
658 |
659 | #### 2.6 【必须】避免函数的声明和实现不同
660 |
661 | 在头文件、源代码、文档中列举的函数声明应当一致,不应当出现定义内容错位的情况。
662 |
663 | 错误:
664 |
665 | foo.h
666 |
667 | ```c++
668 | int CalcArea(int width, int height);
669 | ```
670 |
671 | foo.cc
672 |
673 | ```c++
674 | int CalcArea(int height, int width) { // Different from foo.h
675 | if (height > real_height) {
676 | return 0;
677 | }
678 | return height * width;
679 | }
680 | ```
681 |
682 | 正确:
683 | foo.h
684 |
685 | ```c++
686 | int CalcArea(int height, int width);
687 | ```
688 |
689 | foo.cc
690 |
691 | ```c++
692 | int CalcArea (int height, int width) {
693 | if (height > real_height) {
694 | return 0;
695 | }
696 | return height * width;
697 | }
698 | ```
699 |
700 | 关联漏洞:
701 |
702 | `中风险-逻辑问题`
703 |
704 |
705 | #### 2.7 【必须】检查复制粘贴的重复代码(相同代码通常代表错误)
706 |
707 | 当开发中遇到较长的句子时,如果你选择了复制粘贴语句,请记得检查每一行代码,不要出现上下两句一模一样的情况,这通常代表代码哪里出现了错误:
708 |
709 | ```c++
710 | // Bad
711 | void Foobar(SomeStruct& foobase, SomeStruct& foo1, SomeStruct& foo2) {
712 | foo1.bar = (foo1.bar & 0xffff) | (foobase.base & 0xffff0000);
713 | foo1.bar = (foo1.bar & 0xffff) | (foobase.base & 0xffff0000);
714 | }
715 | ```
716 |
717 | 如上例,通常可能是:
718 |
719 | ```c++
720 | // Good
721 | void Foobar(SomeStruct& foobase, SomeStruct& foo1, SomeStruct& foo2) {
722 | foo1.bar = (foo1.bar & 0xffff) | (foobase.base & 0xffff0000);
723 | foo2.bar = (foo2.bar & 0xffff) | (foobase.base & 0xffff0000);
724 | }
725 |
726 | ```
727 |
728 | 最好是把重复的代码片段提取成函数,如果函数比较短,可以考虑定义为 `inline` 函数,在减少冗余的同时也能确保不会影响性能。
729 |
730 | 关联漏洞:
731 |
732 | `中风险-逻辑问题`
733 |
734 |
735 | #### 2.8 【必须】左右一致的重复判断/永远为真或假的判断(通常代表错误)
736 |
737 | 这通常是由于自动完成或例如Visual Assistant X之类的补全插件导致的问题。
738 |
739 | ```c++
740 | // Bad
741 | if (foo1.bar == foo1.bar) {
742 | …
743 | }
744 | ```
745 |
746 | 可能是:
747 |
748 | ```c++
749 | // Good
750 | if (foo1.bar == foo2.bar) {
751 | …
752 | }
753 | ```
754 |
755 | 关联漏洞:
756 |
757 | `中风险-逻辑问题`
758 |
759 |
760 | #### 2.9 【必须】函数每个分支都应有返回值
761 |
762 | 函数的每个分支都应该有返回值,否则如果函数走到无返回值的分支,其结果是未知的。
763 |
764 | ```c++
765 | // Bad
766 | int Foo(int bar) {
767 | if (bar > 100) {
768 | return 10;
769 | } else if (bar > 10) {
770 | return 1;
771 | }
772 | }
773 | ```
774 |
775 | 上述例子当bar<10时,其结果是未知的值。
776 |
777 | ```c++
778 | // Good
779 | int Foo(int bar) {
780 | if (bar > 100) {
781 | return 10;
782 | } else if (bar > 10) {
783 | return 1;
784 | }
785 | return 0;
786 | }
787 | ```
788 |
789 | 开启适当级别的警告(GCC 中为 `-Wreturn-type` 并已包含在 `-Wall` 中)并设置为错误,可以在编译阶段发现这类错误。
790 |
791 | 关联漏洞:
792 |
793 | `中风险-逻辑问题`
794 |
795 | `中风险-信息泄漏`
796 |
797 |
798 | #### 2.10 【必须】不得使用栈上未初始化的变量
799 |
800 | 在栈上声明的变量要注意是否在使用它之前已经初始化了
801 |
802 | ```c++
803 | // Bad
804 | void Foo() {
805 | int foo;
806 | if (Bar()) {
807 | foo = 1;
808 | }
809 | Foobar(foo); // foo可能没有初始化
810 | }
811 | ```
812 |
813 | 最好在声明的时候就立刻初始化变量,或者确保每个分支都初始化它。开启相应的编译器警告(GCC 中为 `-Wuninitialized`),并把设置为错误级别,可以在编译阶段发现这类错误。
814 |
815 | ```c++
816 | // Good
817 | void Foo() {
818 | int foo = 0;
819 | if (Bar()) {
820 | foo = 1;
821 | }
822 | Foobar(foo);
823 | }
824 | ```
825 |
826 | 关联漏洞:
827 |
828 | `中风险-逻辑问题`
829 |
830 | `中风险-信息泄漏`
831 |
832 |
833 | #### 2.11 【建议】不得直接使用刚分配的未初始化的内存(如realloc)
834 |
835 | 一些刚申请的内存通常是直接从堆上分配的,可能包含有旧数据的,直接使用它们而不初始化,可能会导致安全问题。例如,CVE-2019-13751。应确保初始化变量,或者确保未初始化的值不会泄露给用户。
836 |
837 | ```c++
838 | // Bad
839 | char* Foo() {
840 | char* a = new char[100];
841 | a[99] = '\0';
842 | memcpy(a, "char", 4);
843 | return a;
844 | }
845 | ```
846 |
847 | ```c++
848 | // Good
849 | char* Foo() {
850 | char* a = new char[100];
851 | memcpy(a, "char", 4);
852 | a[4] = '\0';
853 | return a;
854 | }
855 | ```
856 |
857 | 在 C++ 中,再次强烈推荐用 `string`、`vector` 代替手动内存分配。
858 |
859 | 关联漏洞:
860 |
861 | `中风险-逻辑问题`
862 |
863 | `中风险-信息泄漏`
864 |
865 |
866 | #### 2.12 【必须】校验内存相关函数的返回值
867 |
868 | 与内存分配相关的函数需要检查其返回值是否正确,以防导致程序崩溃或逻辑错误。
869 |
870 | ```c++
871 | // Bad
872 | void Foo() {
873 | char* bar = mmap(0, 0x800000, .....);
874 | *(bar + 0x400000) = '\x88'; // Wrong
875 | }
876 |
877 | ```
878 |
879 | 如上例mmap如果失败,bar的值将是0xffffffff (ffffffff),第二行将会往0x3ffffff写入字符,导致越界写。
880 |
881 | ```c++
882 | // Good
883 | void Foo() {
884 | char* bar = mmap(0, 0x800000, .....);
885 | if(bar == MAP_FAILED) {
886 | return;
887 | }
888 |
889 | *(bar + 0x400000) = '\x88';
890 | }
891 | ```
892 |
893 | 关联漏洞:
894 |
895 | `中风险-逻辑问题`
896 |
897 | `高风险-越界操作`
898 |
899 |
900 | #### 2.13 【必须】不要在if里面赋值
901 |
902 | if里赋值通常代表代码存在错误。
903 |
904 | ```c++
905 | // Bad
906 | void Foo() {
907 | if (bar = 0x99) ...
908 | }
909 | ```
910 |
911 | 通常应该是:
912 |
913 | ```c++
914 | // Good
915 | void Foo() {
916 | if (bar == 0x99) ...
917 | }
918 | ```
919 |
920 | 建议在构建系统中开启足够的编译器警告(GCC 中为 `-Wparentheses` 并已包含在 `-Wall` 中),并把该警告设置为错误。
921 |
922 | 关联漏洞:
923 |
924 | `中风险-逻辑问题`
925 |
926 |
927 | #### 2.14 【建议】确认if里面的按位操作
928 |
929 | if里,非bool类型和非bool类型的按位操作可能代表代码存在错误。
930 |
931 | ```c++
932 | // Bad
933 | void Foo() {
934 | int bar = 0x1; // binary 01
935 | int foobar = 0x2; // binary 10
936 |
937 | if (foobar & bar) // result = 00, false
938 | ...
939 | }
940 | ```
941 |
942 | 上述代码可能应该是:
943 |
944 | ```c++
945 | // Good
946 | void foo() {
947 | int bar = 0x1;
948 | int foobar = 0x2;
949 |
950 | if (foobar && bar) // result : true
951 | ...
952 | }
953 | ```
954 |
955 | 关联漏洞:
956 |
957 | `中风险-逻辑问题`
958 |
959 |
960 | ### 3 多线程
961 |
962 |
963 | #### 3.1 【必须】变量应确保线程安全性
964 |
965 | 当一个变量可能被多个线程使用时,应当使用原子操作或加锁操作。
966 |
967 | ```c++
968 | // Bad
969 | char g_somechar;
970 | void foo_thread1() {
971 | g_somechar += 3;
972 | }
973 |
974 | void foo_thread2() {
975 | g_somechar += 1;
976 | }
977 | ```
978 |
979 | 对于可以使用原子操作的,应当使用一些可以确保内存安全的操作,如:
980 |
981 | ```c++
982 | // Good
983 | volatile char g_somechar;
984 | void foo_thread1() {
985 | __sync_fetch_and_add(&g_somechar, 3);
986 | }
987 |
988 | void foo_thread2() {
989 | __sync_fetch_and_add(&g_somechar, 1);
990 | }
991 | ```
992 |
993 | 对于 C 代码,`C11` 后推荐使用 [atomic](https://en.cppreference.com/w/c/atomic) 标准库。
994 | 对于 C++代码,`C++11` 后,推荐使用 [`std::atomic`](https://zh.cppreference.com/w/cpp/atomic/atomic)。
995 |
996 | 关联漏洞:
997 |
998 | `高风险-内存破坏`
999 |
1000 | `中风险-逻辑问题`
1001 |
1002 |
1003 | #### 3.2 【必须】注意signal handler导致的条件竞争
1004 |
1005 | 竞争条件经常出现在信号处理程序中,因为信号处理程序支持异步操作。攻击者能够利用信号处理程序争用条件导致软件状态损坏,从而可能导致拒绝服务甚至代码执行。
1006 |
1007 | 1. 当信号处理程序中发生不可重入函数或状态敏感操作时,就会出现这些问题。因为信号处理程序中随时可以被调用。比如,当在信号处理程序中调用`free`时,通常会出现另一个信号争用条件,从而导致双重释放。即使给定指针在释放后设置为`NULL`,在释放内存和将指针设置为`NULL`之间仍然存在竞争的可能。
1008 | 2. 为多个信号设置了相同的信号处理程序,这尤其有问题——因为这意味着信号处理程序本身可能会重新进入。例如,malloc()和free()是不可重入的,因为它们可能使用全局或静态数据结构来管理内存,并且它们被syslog()等看似无害的函数间接使用;这些函数可能会导致内存损坏和代码执行。
1009 |
1010 | ```c++
1011 | // Bad
1012 | char *log_message;
1013 |
1014 | void Handler(int signum) {
1015 | syslog(LOG_NOTICE, "%s\n", log_m_essage);
1016 | free(log_message);
1017 | sleep(10);
1018 | exit(0);
1019 | }
1020 |
1021 | int main (int argc, char* argv[]) {
1022 | log_message = strdup(argv[1]);
1023 | signal(SIGHUP, Handler);
1024 | signal(SIGTERM, Handler);
1025 | sleep(10);
1026 | }
1027 | ```
1028 |
1029 | 可以借由下列操作规避问题:
1030 |
1031 | 1. 避免在多个处理函数中共享某些变量。
1032 | 2. 在信号处理程序中使用同步操作。
1033 | 3. 屏蔽不相关的信号,从而提供原子性。
1034 | 4. 避免在信号处理函数中调用不满足[异步信号安全](https://www.man7.org/linux/man-pages/man7/signal-safety.7.html)的函数。
1035 |
1036 | 关联漏洞:
1037 |
1038 | `高风险-内存破坏`
1039 |
1040 | `中风险-逻辑问题`
1041 |
1042 |
1043 | #### 3.3 【建议】注意Time-of-check Time-of-use (TOCTOU) 条件竞争
1044 |
1045 | TOCTOU: 软件在使用某个资源之前检查该资源的状态,但是该资源的状态可以在检查和使用之间更改,从而使检查结果无效。当资源处于这种意外状态时,这可能会导致软件执行错误操作。
1046 |
1047 | 当攻击者可以影响检查和使用之间的资源状态时,此问题可能与安全相关。这可能发生在共享资源(如**文件、内存**,甚至多线程程序中的**变量**)上。在编程时需要注意避免出现TOCTOU问题。
1048 |
1049 | 例如,下面的例子中,该文件可能已经在检查和lstat之间进行了更新,特别是因为printf有延迟。
1050 |
1051 | ```c++
1052 | struct stat *st;
1053 |
1054 | lstat("...", st);
1055 |
1056 | printf("foo");
1057 |
1058 | if (st->st_mtimespec == ...) {
1059 | printf("Now updating things\n");
1060 | UpdateThings();
1061 | }
1062 | ```
1063 |
1064 | TOCTOU难以修复,但是有以下缓解方案:
1065 |
1066 | 1. 限制对来自多个进程的文件的交叉操作。
1067 | 2. 如果必须在多个进程或线程之间共享对资源的访问,那么请尝试限制”检查“(CHECK)和”使用“(USE)资源之间的时间量,使他们相距尽量不要太远。这不会从根本上解决问题,但可能会使攻击更难成功。
1068 | 3. 在Use调用之后重新检查资源,以验证是否正确执行了操作。
1069 | 4. 确保一些环境锁定机制能够被用来有效保护资源。但要确保锁定是检查之前进行的,而不是在检查之后进行的,以便检查时的资源与使用时的资源相同。
1070 |
1071 | 关联漏洞:
1072 |
1073 | `高风险-内存破坏`
1074 |
1075 | `中风险-逻辑问题`
1076 |
1077 |
1078 | ### 4 加密解密
1079 |
1080 |
1081 | #### 4.1 【必须】不得明文存储用户密码等敏感数据
1082 |
1083 | 用户密码应该使用 Argon2, scrypt, bcrypt, pbkdf2 等算法做哈希之后再存入存储系统, https://password-hashing.net/
1084 |
1085 | https://libsodium.gitbook.io/doc/password_hashing/default_phf#example-2-password-storage
1086 |
1087 |
1088 | 用户敏感数据,应该做到传输过程中加密,存储状态下加密
1089 | 传输过程中加密,可以使用 HTTPS 等认证加密通信协议
1090 |
1091 | 存储状态下加密,可以使用 SQLCipher 等类似方案。
1092 |
1093 |
1094 | #### 4.2 【必须】内存中的用户密码等敏感数据应该安全抹除
1095 |
1096 | 例如用户密码等,即使是临时使用,也应在使用完成后应当将内容彻底清空。
1097 |
1098 | 错误:
1099 |
1100 | ```c++
1101 | #include
1102 | #include
1103 |
1104 | {
1105 | ...
1106 | string user_password(100, '\0');
1107 | snprintf(&user_password, "password: %s", user_password.size(), password_from_input);
1108 | ...
1109 | }
1110 | ```
1111 |
1112 | 正确:
1113 |
1114 | ```c++
1115 | {
1116 | ...
1117 | string user_password(100, '\0');
1118 | snprintf(&user_password, "password: %s", user_password.size(), password_from_input);
1119 | ...
1120 | OPENSSL_cleanse(&user_password[0], user_password.size());
1121 | }
1122 |
1123 | ```
1124 |
1125 | 关联漏洞:
1126 |
1127 | `高风险-敏感信息泄露`
1128 |
1129 |
1130 | #### 4.3 【必须】rand() 类函数应正确初始化
1131 |
1132 | rand类函数的随机性并不高。而且在使用前需要使用srand()来初始化。未初始化的随机数可能导致某些内容可预测。
1133 |
1134 | ```c++
1135 | // Bad
1136 | int main() {
1137 | int foo = rand();
1138 | return 0;
1139 | }
1140 | ```
1141 |
1142 | 上述代码执行完成后,foo的值是固定的。它等效于 `srand(1); rand();`。
1143 |
1144 | ```c++
1145 | // Good
1146 |
1147 | int main() {
1148 | srand(time(0));
1149 | int foo = rand();
1150 | return 0;
1151 | }
1152 | ```
1153 |
1154 | 关联漏洞:
1155 |
1156 | `高风险-逻辑漏洞`
1157 |
1158 |
1159 | #### 4.4 【必须】在需要高强度安全加密时不应使用弱PRNG函数
1160 |
1161 | 在需要生成 AES/SM1/HMAC 等算法的密钥/IV/Nonce, RSA/ECDSA/ECDH 等算法的私钥,这类需要高安全性的业务场景,必须使用密码学安全的随机数生成器 (Cryptographically Secure PseudoRandom Number Generator (CSPRNG) ), 不得使用 `rand()` 等无密码学安全性保证的普通随机数生成器。
1162 |
1163 | 推荐使用的 CSPRNG 有:
1164 | 1. OpenSSL 中的 `RAND_bytes()` 函数, https://www.openssl.org/docs/man1.1.1/man3/RAND_bytes.html
1165 |
1166 | 1. libsodium 中的 `randombytes_buf()` 函数
1167 |
1168 | 1. Linux kernel 的 `getrandom()` 系统调用, https://man7.org/linux/man-pages/man2/getrandom.2.html .
1169 | 或者读 /dev/urandom 文件, 或者 /dev/random 文件。
1170 |
1171 | 1. Apple IOS 的 `SecRandomCopyBytes()`, https://developer.apple.com/documentation/security/1399291-secrandomcopybytes
1172 |
1173 | 1. Windows 下的 `BCryptGenRandom()`, `CryptGenRandom()`, `RtlGenRandom()`
1174 |
1175 |
1176 | ```c++
1177 | #include
1178 | #include
1179 | #include
1180 | #include
1181 |
1182 | {
1183 | unsigned char key[16];
1184 | if (1 != RAND_bytes(&key[0], sizeof(key))) { //... 错误处理
1185 | return -1;
1186 | }
1187 |
1188 | AES_KEY aes_key;
1189 | if (0 != AES_set_encrypt_key(&key[0], sizeof(key) * 8, &aes_key)) {
1190 | // ... 错误处理
1191 | return -1;
1192 | }
1193 |
1194 | ...
1195 |
1196 | OPENSSL_cleanse(&key[0], sizeof(key));
1197 | }
1198 |
1199 | ```
1200 |
1201 |
1202 | `rand()`类函数的随机性并不高。敏感操作时,如设计加密算法时,不得使用rand()或者类似的简单线性同余伪随机数生成器来作为随机数发生器。符合该定义的比特序列的特点是,序列中“1”的数量约等于“0”的数量;同理,“01”、“00”、“10”、“11”的数量大致相同,以此类推。
1203 |
1204 | 例如 C 标准库中的 `rand()` 的实现只是简单的[线性同余算法](https://sourceware.org/git/?p=glibc.git;a=blob;f=stdlib/random_r.c;hb=glibc-2.28#l353),生成的伪随机数具有较强的可预测性。
1205 |
1206 | 当需要实现高强度加密,例如涉及通信安全时,不应当使用 `rand()` 作为随机数发生器。
1207 |
1208 | 实际应用中,[ C++11 标准提供的`random_device`保证加密的安全性和随机性](https://docs.microsoft.com/en-us/cpp/standard-library/random-device-class?redirectedfrom=MSDN&view=vs-2019#remarks)
1209 | 但是 [C++ 标准并不保证这一点](https://stackoverflow.com/questions/44867500/is-stdrandom-device-cryptographic-secure)。跨平台的代码可以考虑用 [OpenSSL](https://wiki.openssl.org/index.php/Random_Numbers) 等保证密码学安全的库里的随机数发生器。
1210 |
1211 |
1212 |
1213 | 关联漏洞:
1214 |
1215 | `高风险-敏感数据泄露`
1216 |
1217 |
1218 | #### 4.5 【必须】自己实现的rand范围不应过小
1219 |
1220 | 如果在弱安全场景相关的算法中自己实现了PRNG,请确保rand出来的随机数不会很小或可预测。
1221 |
1222 | ```c++
1223 | // Bad
1224 | int32_t val = ((state[0] * 1103515245U) + 12345U) & 999999;
1225 | ```
1226 |
1227 | 上述例子可能想生成0~999999共100万种可能的随机数,但是999999的二进制是11110100001000111111,与&运算后,0位一直是0,所以生成出的范围明显会小于100万种。
1228 |
1229 | ```c++
1230 | // Good
1231 | int32_t val = ((state[0] * 1103515245U) + 12345U) % 1000000;
1232 |
1233 | // Good
1234 | int32_t val = ((state[0] * 1103515245U) + 12345U) & 0x7fffffff;
1235 | ```
1236 |
1237 | 关联漏洞:
1238 |
1239 | `高风险-逻辑漏洞`
1240 |
1241 |
1242 | ### 5 文件操作
1243 |
1244 |
1245 | #### 5.1 【必须】避免路径穿越问题
1246 |
1247 | 在进行文件操作时,需要判断外部传入的文件名是否合法,如果文件名中包含 `../` 等特殊字符,则会造成路径穿越,导致任意文件的读写。
1248 |
1249 | 错误:
1250 |
1251 | ```c++
1252 | void Foo() {
1253 | char file_path[PATH_MAX] = "/home/user/code/";
1254 | // 如果传入的文件名包含../可导致路径穿越
1255 | // 例如"../file.txt",则可以读取到上层目录的file.txt文件
1256 | char name[20] = "../file.txt";
1257 | memcpy(file_path + strlen(file_path), name, sizeof(name));
1258 | int fd = open(file_path, O_RDONLY);
1259 | if (fd != -1) {
1260 | char data[100] = {0};
1261 | int num = 0;
1262 | memset(data, 0, sizeof(data));
1263 | num = read(fd, data, sizeof(data));
1264 | if (num > 0) {
1265 | write(STDOUT_FILENO, data, num);
1266 | }
1267 | close(fd);
1268 | }
1269 | }
1270 | ```
1271 |
1272 | 正确:
1273 |
1274 | ```c++
1275 | void Foo() {
1276 | char file_path[PATH_MAX] = "/home/user/code/";
1277 | char name[20] = "../file.txt";
1278 | // 判断传入的文件名是否非法,例如"../file.txt"中包含非法字符../,直接返回
1279 | if (strstr(name, "..") != NULL){
1280 | // 包含非法字符
1281 | return;
1282 | }
1283 | memcpy(file_path + strlen(file_path), name, sizeof(name));
1284 | int fd = open(file_path, O_RDONLY);
1285 | if (fd != -1) {
1286 | char data[100] = {0};
1287 | int num = 0;
1288 | memset(data, 0, sizeof(data));
1289 | num = read(fd, data, sizeof(data));
1290 | if (num > 0) {
1291 | write(STDOUT_FILENO, data, num);
1292 | }
1293 | close(fd);
1294 | }
1295 | }
1296 | ```
1297 |
1298 | 关联漏洞:
1299 |
1300 | `高风险-逻辑漏洞`
1301 |
1302 |
1303 | #### 5.2 【必须】避免相对路径导致的安全问题(DLL、EXE劫持等问题)
1304 |
1305 | 在程序中,使用相对路径可能导致一些安全风险,例如DLL、EXE劫持等问题。
1306 |
1307 | 例如以下代码,可能存在劫持问题:
1308 |
1309 | ```c++
1310 | int Foo() {
1311 | // 传入的是dll文件名,如果当前目录下被写入了恶意的同名dll,则可能导致dll劫持
1312 | HINSTANCE hinst = ::LoadLibrary("dll_nolib.dll");
1313 | if (hinst != NULL) {
1314 | cout<<"dll loaded!" << endl;
1315 | }
1316 | return 0;
1317 | }
1318 | ```
1319 |
1320 | 针对DLL劫持的安全编码的规范:
1321 |
1322 | 1)调用LoadLibrary,LoadLibraryEx,CreateProcess,ShellExecute等进行模块加载的函数时,指明模块的完整(全)路径,禁止使用相对路径,这样就可避免从其它目录加载DLL。
1323 | 2)在应用程序的开头调用SetDllDirectory(TEXT("")); 从而将当前目录从DLL的搜索列表中删除。结合SetDefaultDllDirectories,AddDllDirectory,RemoveDllDirectory这几个API配合使用,可以有效的规避DLL劫持问题。这些API只能在打了KB2533623补丁的Windows7,2008上使用。
1324 |
1325 | 关联漏洞:
1326 |
1327 | `中风险-逻辑漏洞`
1328 |
1329 |
1330 | #### 5.3 【必须】文件权限控制
1331 |
1332 | 在创建文件时,需要根据文件的敏感级别设置不同的访问权限,以防止敏感数据被其他恶意程序读取或写入。
1333 |
1334 | 错误:
1335 |
1336 | ```c++
1337 | int Foo() {
1338 | // 不要设置为777权限,以防止被其他恶意程序操作
1339 | if (creat("file.txt", 0777) < 0) {
1340 | printf("文件创建失败!\n");
1341 | } else {
1342 | printf("文件创建成功!\n");
1343 | }
1344 | return 0;
1345 | }
1346 | ```
1347 |
1348 | 关联漏洞:
1349 |
1350 | `中风险-逻辑漏洞`
1351 |
1352 |
1353 | ### 6 内存操作
1354 |
1355 |
1356 | #### 6.1 【必须】防止各种越界写(向前/向后)
1357 |
1358 | 错误1:
1359 |
1360 | ```c++
1361 | int a[5];
1362 | a[5] = 0;
1363 | ```
1364 |
1365 | 错误2:
1366 |
1367 | ```c++
1368 | int a[5];
1369 | int b = user_controlled_value;
1370 | a[b] = 3;
1371 | ```
1372 |
1373 | 关联漏洞:
1374 |
1375 | `高风险-内存破坏`
1376 |
1377 |
1378 | #### 6.2 【必须】防止任意地址写
1379 |
1380 | 任意地址写会导致严重的安全隐患,可能导致代码执行。因此,在编码时必须校验写入的地址。
1381 |
1382 | 错误:
1383 |
1384 | ```c++
1385 | void Write(MyStruct dst_struct) {
1386 | char payload[10] = { 0 };
1387 | memcpy(dst_struct.buf, payload, sizeof(payload));
1388 | }
1389 |
1390 | int main() {
1391 | MyStruct dst_stuct;
1392 | dst_stuct.buf = (char*)user_controlled_value;
1393 | Write(dst_stuct);
1394 | return 0;
1395 | }
1396 | ```
1397 |
1398 | 关联漏洞:
1399 |
1400 | `高风险-内存破坏`
1401 |
1402 |
1403 | ### 7 数字操作
1404 |
1405 |
1406 | #### 7.1 【必须】防止整数溢出
1407 |
1408 | 在计算时需要考虑整数溢出的可能,尤其在进行内存操作时,需要对分配、拷贝等大小进行合法校验,防止整数溢出导致的漏洞。
1409 |
1410 | 错误(该例子在计算时产生整数溢出)
1411 |
1412 | ```c++
1413 | const int kMicLen = 4;
1414 | // 整数溢出
1415 | void Foo() {
1416 | int len = 1;
1417 | char payload[10] = { 0 };
1418 | char dst[10] = { 0 };
1419 | // Bad, 由于len小于4,导致计算拷贝长度时,整数溢出
1420 | // len - kMicLen == 0xfffffffd
1421 | memcpy(dst, payload, len - kMicLen);
1422 | }
1423 | ```
1424 |
1425 | 正确例子
1426 |
1427 | ```c++
1428 | void Foo() {
1429 | int len = 1;
1430 | char payload[10] = { 0 };
1431 | char dst[10] = { 0 };
1432 | int size = len - kMicLen;
1433 | // 拷贝前对长度进行判断
1434 | if (size > 0 && size < 10) {
1435 | memcpy(dst, payload, size);
1436 | printf("memcpy good\n");
1437 | }
1438 | }
1439 | ```
1440 |
1441 | 关联漏洞:
1442 |
1443 | `高风险-内存破坏`
1444 |
1445 |
1446 | #### 7.2 【必须】防止Off-By-One
1447 |
1448 | 在进行计算或者操作时,如果使用的最大值或最小值不正确,使得该值比正确值多1或少1,可能导致安全风险。
1449 |
1450 | 错误:
1451 |
1452 | ```c++
1453 | char firstname[20];
1454 | char lastname[20];
1455 | char fullname[40];
1456 |
1457 | fullname[0] = '\0';
1458 |
1459 | strncat(fullname, firstname, 20);
1460 | // 第二次调用strncat()可能会追加另外20个字符。如果这20个字符没有终止空字符,则存在安全问题
1461 | strncat(fullname, lastname, 20);
1462 | ```
1463 |
1464 | 正确:
1465 |
1466 | ```c++
1467 | char firstname[20];
1468 | char lastname[20];
1469 | char fullname[40];
1470 |
1471 | fullname[0] = '\0';
1472 |
1473 | // 当使用像strncat()函数时,必须在缓冲区的末尾为终止空字符留下一个空字节,避免off-by-one
1474 | strncat(fullname, firstname, sizeof(fullname) - strlen(fullname) - 1);
1475 | strncat(fullname, lastname, sizeof(fullname) - strlen(fullname) - 1);
1476 | ```
1477 |
1478 | 对于 C++ 代码,再次强烈建议使用 `string`、`vector` 等组件代替原始指针和数组操作。
1479 |
1480 | 关联漏洞:
1481 |
1482 | `高风险-内存破坏`
1483 |
1484 |
1485 | #### 7.3 【必须】避免大小端错误
1486 |
1487 | 在一些涉及大小端数据处理的场景,需要进行大小端判断,例如从大端设备取出的值,要以大端进行处理,避免端序错误使用。
1488 |
1489 | 关联漏洞:
1490 |
1491 | `中风险-逻辑漏洞`
1492 |
1493 |
1494 | #### 7.4 【必须】检查除以零异常
1495 |
1496 | 在进行除法运算时,需要判断被除数是否为零,以防导致程序不符合预期或者崩溃。
1497 |
1498 | 错误:
1499 |
1500 | ```c++
1501 | int divide(int x, int y) {
1502 | return x / y;
1503 | }
1504 | ```
1505 |
1506 | 正确:
1507 |
1508 | ```c++
1509 | int divide(int x, int y) {
1510 | if (y == 0) {
1511 | throw DivideByZero;
1512 | }
1513 | return x / y;
1514 | }
1515 | ```
1516 |
1517 | 关联漏洞:
1518 |
1519 | `低风险-拒绝服务`
1520 |
1521 |
1522 | #### 7.5 【必须】防止数字类型的错误强转
1523 |
1524 | 在有符号和无符号数字参与的运算中,需要注意类型强转可能导致的逻辑错误,建议指定参与计算时数字的类型或者统一类型参与计算。
1525 |
1526 | 错误例子
1527 |
1528 | ```c++
1529 | int Foo() {
1530 | int len = 1;
1531 | unsigned int size = 9;
1532 | // 1 < 9 - 10 ? 由于运算中无符号和有符号混用,导致计算结果以无符号计算
1533 | if (len < size - 10) {
1534 | printf("Bad\n");
1535 | } else {
1536 | printf("Good\n");
1537 | }
1538 | }
1539 | ```
1540 |
1541 | 正确例子
1542 |
1543 | ```c++
1544 | void Foo() {
1545 | // 统一两者计算类型为有符号
1546 | int len = 1;
1547 | int size = 9;
1548 | if (len < size - 10) {
1549 | printf("Bad\n");
1550 | } else {
1551 | printf("Good\n");
1552 | }
1553 | }
1554 | ```
1555 |
1556 | 关联漏洞:
1557 |
1558 | `高风险-内存破坏`
1559 |
1560 | `中风险-逻辑漏洞`
1561 |
1562 |
1563 | #### 7.6 【必须】比较数据大小时加上最小/最大值的校验
1564 |
1565 | 在进行数据大小比较时,要合理地校验数据的区间范围,建议根据数字类型,对其进行最大和最小值的判断,以防止非预期错误。
1566 |
1567 | 错误:
1568 |
1569 | ```c++
1570 | void Foo(int index) {
1571 | int a[30] = {0};
1572 | // 此处index是int型,只考虑了index小于数组大小,但是并未判断是否大于等于0
1573 | if (index < 30) {
1574 | // 如果index为负数,则越界
1575 | a[index] = 1;
1576 | }
1577 | }
1578 | ```
1579 |
1580 | 正确:
1581 |
1582 | ```c++
1583 | void Foo(int index) {
1584 | int a[30] = {0};
1585 | // 判断index的最大最小值
1586 | if (index >= 0 && index < 30) {
1587 | a[index] = 1;
1588 | }
1589 | }
1590 | ```
1591 |
1592 | 关联漏洞:
1593 |
1594 | `高风险-内存破坏`
1595 |
1596 |
1597 | ### 8 指针操作
1598 |
1599 |
1600 | #### 8.1 【建议】检查在pointer上使用sizeof
1601 |
1602 | 除了测试当前指针长度,否则一般不会在pointer上使用sizeof。
1603 |
1604 | 正确:
1605 |
1606 | ```c++
1607 | size_t pointer_length = sizeof(void*);
1608 | ```
1609 |
1610 | 可能错误:
1611 |
1612 | ```c++
1613 | size_t structure_length = sizeof(Foo*);
1614 | ```
1615 |
1616 | 可能是:
1617 |
1618 | ```c++
1619 | size_t structure_length = sizeof(Foo);
1620 | ```
1621 |
1622 | 关联漏洞:
1623 |
1624 | `中风险-逻辑漏洞`
1625 |
1626 |
1627 | #### 8.2 【必须】检查直接将数组和0比较的代码
1628 |
1629 | 错误:
1630 |
1631 | ```c++
1632 | int a[3];
1633 | ...;
1634 |
1635 | if (a > 0)
1636 | ...;
1637 | ```
1638 |
1639 | 该判断永远为真,等价于:
1640 |
1641 | ```c++
1642 | int a[3];
1643 | ...;
1644 |
1645 | if (&a[0])
1646 | ...;
1647 | ```
1648 |
1649 | 可能是:
1650 |
1651 | ```c++
1652 | int a[3];
1653 | ...;
1654 |
1655 | if(a[0] > 0)
1656 | ...;
1657 | ```
1658 |
1659 | 开启足够的编译器警告(GCC 中为 `-Waddress`,并已包含在 `-Wall` 中),并设置为错误,可以在编译期间发现该问题。
1660 |
1661 | 关联漏洞:
1662 |
1663 | `中风险-逻辑漏洞`
1664 |
1665 |
1666 | #### 8.3 【必须】不应当向指针赋予写死的地址
1667 |
1668 | 特殊情况需要特殊对待(比如开发硬件固件时可能需要写死)
1669 |
1670 | 但是如果是系统驱动开发之类的,写死可能会导致后续的问题。
1671 |
1672 | 关联漏洞:
1673 |
1674 | `高风险-内存破坏`
1675 |
1676 |
1677 | #### 8.4 【必须】检查空指针
1678 |
1679 | 错误:
1680 |
1681 | ```c++
1682 | *foo = 100;
1683 |
1684 | if (!foo) {
1685 | ERROR("foobar");
1686 | }
1687 | ```
1688 |
1689 | 正确:
1690 |
1691 | ```c++
1692 | if (!foo) {
1693 | ERROR("foobar");
1694 | }
1695 |
1696 | *foo = 100;
1697 | ```
1698 |
1699 | 错误:
1700 |
1701 | ```c++
1702 | void Foo(char* bar) {
1703 | *bar = '\0';
1704 | }
1705 | ```
1706 |
1707 | 正确:
1708 |
1709 | ```c++
1710 | void Foo(char* bar) {
1711 | if(bar)
1712 | *bar = '\0';
1713 | else
1714 | ...;
1715 | }
1716 | ```
1717 |
1718 | 关联漏洞:
1719 |
1720 | `低风险-拒绝服务`
1721 |
1722 |
1723 | #### 8.5 【必须】释放完后置空指针
1724 |
1725 | 在对指针进行释放后,需要将该指针设置为NULL,以防止后续free指针的误用,导致UAF等其他内存破坏问题。尤其是在结构体、类里面存储的原始指针。
1726 |
1727 | 错误:
1728 |
1729 | ```c++
1730 | void foo() {
1731 | char* p = (char*)malloc(100);
1732 | memcpy(p, "hello", 6);
1733 | printf("%s\n", p);
1734 | free(p); // 此时p所指向的内存已被释放,但是p所指的地址仍然不变
1735 | // 未设置为NULL,可能导致UAF等内存错误
1736 |
1737 | if (p != NULL) { // 没有起到防错作用
1738 | printf("%s\n", p); // 错误使用已经释放的内存
1739 | }
1740 | }
1741 | ```
1742 |
1743 | 正确:
1744 |
1745 | ```c++
1746 | void foo() {
1747 | char* p = (char*)malloc(100);
1748 | memcpy(p, "hello", 6);
1749 | // 此时p所指向的内存已被释放,但是p所指的地址仍然不变
1750 | printf("%s\n", p);
1751 | free(p);
1752 | //释放后将指针赋值为空
1753 | p = NULL;
1754 | if (p != NULL) { // 没有起到防错作用
1755 | printf("%s\n", p); // 错误使用已经释放的内存
1756 | }
1757 | }
1758 | ```
1759 |
1760 | 对于 C++ 代码,使用 string、vector、智能指针等代替原始内存管理机制,可以大量减少这类错误。
1761 |
1762 | 关联漏洞:
1763 |
1764 | `高风险-内存破坏`
1765 |
1766 |
1767 | #### 8.6 【必须】防止错误的类型转换(type confusion)
1768 |
1769 | 在对指针、对象或变量进行操作时,需要能够正确判断所操作对象的原始类型。如果使用了与原始类型不兼容的类型进行访问,则存在安全隐患。
1770 |
1771 | 错误:
1772 |
1773 | ```c++
1774 | const int NAME_TYPE = 1;
1775 | const int ID_TYPE = 2;
1776 |
1777 | // 该类型根据 msg_type 进行区分,如果在对MessageBuffer进行操作时没有判断目标对象,则存在类型混淆
1778 | struct MessageBuffer {
1779 | int msg_type;
1780 | union {
1781 | const char *name;
1782 | int name_id;
1783 | };
1784 | };
1785 |
1786 | void Foo() {
1787 | struct MessageBuffer buf;
1788 | const char* default_message = "Hello World";
1789 | // 设置该消息类型为 NAME_TYPE,因此buf预期的类型为 msg_type + name
1790 | buf.msg_type = NAME_TYPE;
1791 | buf.name = default_message;
1792 | printf("Pointer of buf.name is %p\n", buf.name);
1793 |
1794 | // 没有判断目标消息类型是否为ID_TYPE,直接修改nameID,导致类型混淆
1795 | buf.name_id = user_controlled_value;
1796 |
1797 | if (buf.msg_type == NAME_TYPE) {
1798 | printf("Pointer of buf.name is now %p\n", buf.name);
1799 | // 以NAME_TYPE作为类型操作,可能导致非法内存读写
1800 | printf("Message: %s\n", buf.name);
1801 | } else {
1802 | printf("Message: Use ID %d\n", buf.name_id);
1803 | }
1804 | }
1805 | ```
1806 |
1807 | 正确(判断操作的目标是否是预期类型):
1808 |
1809 | ```c++
1810 | void Foo() {
1811 | struct MessageBuffer buf;
1812 | const char* default_message = "Hello World";
1813 | // 设置该消息类型为 NAME_TYPE,因此buf预期的类型为 msg_type + name
1814 | buf.msg_type = NAME_TYPE;
1815 | buf.name = default_msessage;
1816 | printf("Pointer of buf.name is %p\n", buf.name);
1817 |
1818 | // 判断目标消息类型是否为 ID_TYPE,不是预期类型则做对应操作
1819 | if (buf.msg_type == ID_TYPE)
1820 | buf.name_id = user_controlled_value;
1821 |
1822 | if (buf.msg_type == NAME_TYPE) {
1823 | printf("Pointer of buf.name is now %p\n", buf.name);
1824 | printf("Message: %s\n", buf.name);
1825 | } else {
1826 | printf("Message: Use ID %d\n", buf.name_id);
1827 | }
1828 | }
1829 | ```
1830 |
1831 | 关联漏洞:
1832 |
1833 | `高风险-内存破坏`
1834 |
1835 |
1836 | #### 8.7 【必须】智能指针使用安全
1837 |
1838 | 在使用智能指针时,防止其和原始指针的混用,否则可能导致对象生命周期问题,例如 UAF 等安全风险。
1839 |
1840 | 错误例子:
1841 |
1842 | ```c++
1843 | class Foo {
1844 | public:
1845 | explicit Foo(int num) { data_ = num; };
1846 | void Function() { printf("Obj is %p, data = %d\n", this, data_); };
1847 | private:
1848 | int data_;
1849 | };
1850 |
1851 | std::unique_ptr fool_u_ptr = nullptr;
1852 | Foo* pfool_raw_ptr = nullptr;
1853 |
1854 | void Risk() {
1855 | fool_u_ptr = make_unique(1);
1856 |
1857 | // 从独占智能指针中获取原始指针,(1)
1858 | pfool_raw_ptr = fool_u_ptr.get();
1859 | // 调用(1)的函数
1860 | pfool_raw_ptr->Function();
1861 |
1862 | // 独占智能指针重新赋值后会释放内存
1863 | fool_u_ptr = make_unique(2);
1864 | // 通过原始指针操作会导致UAF,pfool_raw_ptr指向的对象已经释放
1865 | pfool_raw_ptr->Function();
1866 | }
1867 |
1868 |
1869 | // 输出:
1870 | // Obj is 0000027943087B80, data = 1
1871 | // Obj is 0000027943087B80, data = -572662307
1872 | ```
1873 |
1874 | 正确,通过智能指针操作:
1875 |
1876 | ```c++
1877 | void Safe() {
1878 | fool_u_ptr = make_unique(1);
1879 | // 调用(1)的函数
1880 | fool_u_ptr->Function();
1881 |
1882 | fool_u_ptr = make_unique(2);
1883 | // 调用(2)的函数
1884 | fool_u_ptr->Function();
1885 | }
1886 |
1887 | // 输出:
1888 | // Obj is 000002C7BB550830, data = 1
1889 | // Obj is 000002C7BB557AF0, data = 2
1890 | ```
1891 |
1892 | 关联漏洞:
1893 |
1894 | `高风险-内存破坏`
1895 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 为 代码安全指南 作出贡献
2 |
3 | 欢迎 [提出问题或建议](issues) 或 [提交合并请求](pulls),建议在为项目作出贡献时,阅读以下指南。
4 |
5 | ### I. Commit Mesage 编写指引
6 |
7 | 为便于索引,Commit Message应包括三个部分:Header(必需),Body(可选)和 Footer(可选)。
8 |
9 | ```html
10 | ():
11 | // 空一行
12 |
13 | // 空一行
14 |