本篇总结了《C和C++安全编码》中整数相关的内容。
常见的整数安全漏洞
1 无符号整数回绕
涉及无符号操作数的计算永远不会溢出,因为回绕,一个无符号整数表达式永远无法求出小于零的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void test_integer_security_wrap_around() { unsigned int ui = UINT_MAX; fprintf(stdout, "ui value 1: %u\n", ui); ui++; fprintf(stdout, "ui value 2: %u\n", ui); ui = 0; fprintf(stdout, "ui value 3: %u\n", ui); ui--; fprintf(stdout, "ui value 4: %u\n", ui); unsigned int i = 0, j = 0, sum = 0; if (sum + i > UINT_MAX) { } if (i > UINT_MAX - sum) { } if (sum - j < 0) { } if (j > sum) { } }
|
但也并非所有无符号整数回绕都是安全缺陷。精心定义的无符号整数算术求模属性经常被特意使用,例如,在散列算法和C标准里rand()的示例实现中就都用到了这个属性。
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 28 29 30 31 32 33 34 35 36 37 38 39
| void test_integer_security_wrap_around2() { { size_t len = 1; char* src = "comment"; size_t size; size = len - 2; fprintf(stderr, "size = %u, %x, %x, %d\n", size, size, size+1, size+1); char* comment = (char*)malloc(size + 1); free(comment); } { int element_t; int count = 10; char* p = (char*)calloc(sizeof(element_t), count); free(p); } { int off = 1, len = 2; int type_name; std::cout<<"len - sizeof(type_name): "<<len - sizeof(type_name)<<std::endl; if (off > len - sizeof(type_name)) return; if ((off + sizeof(type_name)) > len) return; } }
|
2 有符号整数溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void test_integer_security_overflow() { int i = INT_MAX; i++; fprintf(stdout, "i = %d\n", i); i = INT_MIN; i--; fprintf(stdout, "i = %d\n", i); std::cout << "abs(INT_MIN): " << std::abs(INT_MIN) << std::endl; #define abs(n) ((n) < 0 ? -(n) : (n)) #undef abs }
|
3 字符类型表示整数
在把 char 类型用于数值时必须使用明确的 signed char 或 unsigned char。建议仅使用 signed char 和 unsigned char 类型存储和使用小数值,也就是范围分别在 SCHAR_MIN 和 SCHAR_MAX 之间,或 0 和 UCHAR_MAX 之间的值,因为这是可移植的保证数据的符号字符类型的唯一方式。常规的 char 不应该被用来存储数值,因为编译器有定义 char 的自由,使其要么与 signed char,要么与 unsigned char 具有相同的范围、表示和行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void test_integer_security_char() { { char c = 200; int i = 1000; fprintf(stdout, "i/c = %d\n", i / c); }
{ unsigned char c = 200; int i = 1000; fprintf(stdout, "i/c = %d\n", i / c); } }
|
4 整数类型提升
如果一个整数类型具有低于或等于 int 或 unsigned int 的整数转换级别,那么它的对象或表达式在用于一个需要 int 或 unsigned int 的表达式时,就会被提升。整数类型提升被作为普通算术转换的一个组成部分。
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 28 29 30
| void test_integer_security_promotion() { { int sum = 0; char c1 = 'a', c2 = 'b'; sum = c1 + c2; fprintf(stdout, "sum: %d\n", sum); } { signed char cresult, c1, c2, c3; c1 = 100; c2 = 3; c3 = 4; cresult = c1 * c2 / c3; fprintf(stdout, "cresult: %d\n", cresult); } { unsigned char uc = UCHAR_MAX; int i = ~uc; fprintf(stdout, "i: %0x\n", i); } }
|
5 无符号整数类型转换
从较小的无符号整数类型转换到较大的无符号整数类型始终是安全的,通常通过对其值进行零扩展(zero-extending)而完成。当表达式包含不同宽度的无符号整数操作数时,C标准要求每个操作的结果都具有其中较宽的操作数的类型(和表示范围)。假设相应的数学运算产生一个在结果类型能表示的范围内的结果,则得到的表示值就是那个数学值。如果数学结果值不能用结果类型表示,发生的情况有两类:无符号,损失精度;无符号值转换成有符号值:
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
| void test_integer_security_unsigned_conversion() { { unsigned int ui = 300; unsigned char uc = ui; fprintf(stdout, "uc: %u\n", uc); } { unsigned long int ul = ULONG_MAX; signed char sc; sc = ul; fprintf(stdout, "sc: %d\n", sc); } { unsigned long int ul = ULONG_MAX; signed char sc; if (ul <= SCHAR_MAX) { sc = (signed char)ul; } else { fprintf(stderr, "fail\n"); } } }
|
6 有符号整数类型转换
从较小的有符号整数类型转换为较大的有符号整数类型始终是安全的,并可以采用对该值进行符号扩展的方法在补码表示中实现:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| void test_integer_security_signed_conversion() { { signed long int sl = LONG_MAX; signed char sc = (signed char)sl; fprintf(stdout, "sc: %d\n", sc); } { signed long int sl = LONG_MAX; signed char sc; if ((sl < SCHAR_MIN) || (sl > SCHAR_MAX)) { fprintf(stderr, "fail\n"); } else { sc = (signed char)sl; fprintf(stdout, "sc: %d\n", sc); } } { unsigned int ui = UINT_MAX; signed char c = -1; if (c == ui) { fprintf(stderr, "why is -1 = 4294967295\n"); } } { signed int si = INT_MIN; unsigned int ui = (unsigned int)si; fprintf(stderr, "ui: %u\n", ui); } { signed int si = INT_MIN; unsigned int ui; if (si < 0) { fprintf(stderr, "fail\n"); } else { ui = (unsigned int)si; fprintf(stdout, "ui: %u\n", ui); } } }
|
下面展示了一些整数转换和截断错误:
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 28 29 30 31
| void test_integer_security_conversion_truncation() { { int size = 5; int MAX_ARRAY_SIZE = 10; if (size < MAX_ARRAY_SIZE) { char* array = (char*)malloc(size); free(array); } else { fprintf(stderr, "fail\n"); } } { char* argv[3] = {"", "abc", "123"}; unsigned short int total; total = strlen(argv[1]) + strlen(argv[2]) + 1; char* buff = (char*)malloc(total); strcpy(buff, argv[1]); strcat(buff, argv[2]); fprintf(stdout, "buff: %s\n", buff); free(buff); } }
|
7 整数操作
所有证书操作都可能会导致异常情况下的错误,如溢出、回绕和截断。当某个操作产生的结果不能在操作结果类型中表示时,就会发生异常情况。
7.1 赋值
在简单的赋值(=)中,右操作数的值被转换为赋值表达式的类型并替换存储在左操作数所指定的对象的值。用一个有符号整数为一个无符号整数赋值,或者用一个无符号整数为一个宽度相等的有符号整数赋值,都可能导致所产生的值被误解。当从一个具有较大宽度的类型向较小宽度的类型赋值或强制类型转换时,就会导致发生截断。如果该值不能用结果类型表示,那么数据可能会丢失。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| int f_5_4(void) { return 66; } void test_integer_security_assignment() { { char c; if ((c = f_5_4()) == -1) {} } { char c = 'a'; int i = 1; long l; l = (c = i); } { int si = -3; unsigned int ui = si; fprintf(stdout, "ui = %u\n", ui); fprintf(stdout, "ui = %d\n", ui); si = ui; fprintf(stdout, "si = %d\n", si); } { unsigned char sum, c1, c2; c1 = 200; c2 = 90; sum = c1 + c2; fprintf(stdout, "sum = %u\n", sum); } }
|
7.2 加法
可以用来将两个算术操作数或者将一个指针与一个整数相加。如果两个操作数都是算术类型,那么将会对它们执行普通算术转换。二元的”+”运算符的结果就是其操作数的和。递增与加1等价。如果表达式是将一个整数类型加到一个指针上,那么其结果将是一个指针,这称为指针算术运算。两个整数相加的结果总是能够用比两个操作数中较大者的宽度大1位的数来表示。任何整数操作的结果都可以用任何比其中较大者的宽度大1的类型表示。如果结果整数类型占用的位数不足以表示其结果,那么整数加法就会导致溢出或回绕。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| void test_integer_security_add() { { signed int si1, si2, sum; si1 = -40; si2 = 30; unsigned int usum = (unsigned int)si1 + si2; fprintf(stdout, "usm = %x, si1 = %x, si2 = %x, int_min = %x\n", usum, si1, si2, INT_MIN); if ((usum ^ si1) & (usum ^ si2) & INT_MIN) { fprintf(stderr, "fail\n"); } else { sum = si1 + si2; fprintf(stdout, "sum = %d\n", sum); } } { signed int si1, si2, sum; si1 = -40; si2 = 30; if ((si2 > 0 && si1 > INT_MAX - si2) || (si2 < 0 && si1 < INT_MIN - si2)) { fprintf(stderr, "fail\n"); } else { sum = si1 + si2; fprintf(stdout, "sum = %d\n", sum); } } { unsigned int ui1, ui2, usum; ui1 = 10; ui2 = 20; if (UINT_MAX - ui1 < ui2) { fprintf(stderr, "fail\n"); } else { usum = ui1 + ui2; fprintf(stdout, "usum = %u\n", usum); } } { unsigned int ui1, ui2, usum; ui1 = 10; ui2 = 20; usum = ui1 + ui2; if (usum < ui1) { fprintf(stderr, "fail\n"); } } }
|
7.3 减法
与加法类型,减法也是一种加法操作。对减法而言,两个操作数都必须是算术类型或指向兼容对象类型的指针。从一个指针中减去一个整数也是合法的。递减操作等价于减1操作。如果两个操作之差是负数,那么无符号减法会产生回绕。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| void test_integer_security_substruction() { { signed int si1, si2, result; si1 = 10; si2 = -20; if ((si1 ^ si2) & (((unsigned int)si1 - si2) ^ si1) & INT_MIN) { fprintf(stderr, "fail\n"); } else { result = si1 - si2; fprintf(stdout, "result = %d\n", result); } if ((si2 > 0 && si1 < INT_MIN + si2) || (si2 < 0 && si1 > INT_MAX + si2)) { fprintf(stderr, "fail\n"); } else { result = si1 - si2; fprintf(stdout, "result = %d\n", result); } } { unsigned int ui1, ui2, udiff; ui1 = 10; ui2 = 20; if (ui1 < ui2) { fprintf(stderr, "fail\n"); } else { udiff = ui1 - ui2; fprintf(stdout, "udiff = %u\n", udiff); } } { unsigned int ui1, ui2, udiff; ui1 = 10; ui2 = 20; udiff = ui1 - ui2; if (udiff > ui1) { fprintf(stderr, "fail\n"); } } }
|
7.4 乘法
二元运算符”*”的每个操作数都是算术类型。操作数执行普通算术转换。乘法容易产生溢出错误,因为相对较小的操作数相乘时,都可能导致一个指定的整数类型溢出。一般情况下,两个整数的操作数的积总是可以用两个操作数中较大的那个所用的位数的两倍来表示。这意味着,例如,两个8位操作数的积总是可以使用16位类表示,而两个16位操作数的积总是可以使用32位来表示。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| void test_integer_security_multiplication() { { unsigned int ui1 = 10; unsigned int ui2 = 20; unsigned int product; static_assert(sizeof(unsigned long long) >= 2 * sizeof(unsigned int), "Unable to detect wrapping after multiplication"); unsigned long long tmp = (unsigned long long)ui1 * (unsigned long long)ui2; if (tmp > UINT_MAX) { fprintf(stderr, "fail\n"); } else { product = (unsigned int)tmp; fprintf(stdout, "product = %u\n", product); } } { signed int si1 = 20, si2 = 10; signed int result; static_assert(sizeof(long long) >= 2 * sizeof(int), "Unable to detect overflow after multiplication"); long long tmp = (long long)si1 * (long long)si2; if ((tmp > INT_MAX) || (tmp < INT_MIN)) { fprintf(stderr, "fail\n"); } else { result = (int)tmp; fprintf(stdout, "result = %d\n", result); } } { unsigned int ui1 = 10, ui2 = 20; unsigned int product; if (ui1 > UINT_MAX / ui2) { fprintf(stderr, "fail\n"); } else { product = ui1 * ui2; fprintf(stdout, "product = %u\n", product); } } { signed int si1 = 10, si2 = 20; signed int product; if (si1 > 0) { if (si2 > 0) { if (si1 > (INT_MAX / si2)) { fprintf(stderr, "fail\n"); } } else { if (si2 < (INT_MIN / si1)) { fprintf(stderr, "fail\n"); } } } else { if (si2 > 0) { if (si1 < (INT_MIN / si2)) { fprintf(stderr, "fail\n"); } } else { if ((si1 != 0) && (si2 < (INT_MAX / si1))) { fprintf(stderr, "fail\n"); } } } product = si1 * si2; fprintf(stdout, "product = %d\n", product); } }
|
7.5 除法和求余
整数相除时,”/”运算符的结果是代数商的整数部分,任何小数部分都被丢弃,而”%”运算符的结果是余数。这通常称为向零截断(truncation toward zero)。在这两种运算中,如果第二个操作数的值是0,则该行为是未定义的。无符号整数除法不可能产生回绕,因为商总是小于或等于被除数。但并不总是显而易见的是,有符号整数除法也可能导致溢出,因为你可能认为商数始终小于被除数。然而,补码的最小值除以-1时会出现整数溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void test_integer_security_division_remainder() { signed long sl1 = 100, sl2 = 5; signed long quotient, result; if ((sl2 == 0) || ((sl1 == LONG_MIN) && (sl2 == -1))) { fprintf(stderr, "fail\n"); } else { quotient = sl1 / sl2; result = sl1 % sl2; fprintf(stdout, "quotient = %ld, result = %ld\n", quotient, result); } }
|
7.6 求负
对一个补码表示的有符号的整数求反,也可能产生一个符号错误,因为有符号整数类型的可能值范围是不对称的。
7.7 移位
此操作包括左移位和右移位。移位会在操作数上执行整数提升,其中每个操作数都具有整数类型。结果类型是提升后的左操作数类型。移位运算符右边的操作数提供移动的位数。如果该数值为负值或者大于或等于结果类型的位数,那么该行为是未定义的。在几乎所有情况下,试图移动一个负的位数或试图移动比操作数中存在的位数更多的位都表明一个错误(逻辑错误)。这与溢出是不同的,后者是一个表示不足。不要移动一个负的位数或移动比操作数中存在的位数更多的位。
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
| void test_integer_security_shift() { { unsigned int ui1 = 1, ui2 = 31; unsigned int uresult; if (ui2 >= sizeof(unsigned int) * CHAR_BIT) { fprintf(stderr, "fail\n"); } else { uresult = ui1 << ui2; fprintf(stdout, "uresult = %u\n", uresult); } } { int rc = 0; unsigned int stringify = 0x80000000; char buf[sizeof("256")] = {0}; rc = sprintf(buf, "%u", stringify >> 24); if (rc == -1 || rc >= sizeof(buf)) { fprintf(stderr, "fail\n"); } else { fprintf(stdout, "value: %s\n", buf); } } }
|