整数溢出时,(unsigned int) * (int) 的结果是无符号整数(unsigned int),而不是有符号整数(int)。

8

当整数溢出时,(unsigned int) * (int)的结果是什么?unsigned还是int? 对于char*,数组索引运算符(operator[])使用什么类型: intunsigned int或其他类型?

我在审计以下函数时,突然出现了这个问题。该函数在第17行存在漏洞。

// Create a character array and initialize it with init[] 
// repeatedly. The size of this character array is specified by 
// w*h.
char *function4(unsigned int w, unsigned int h, char *init)
{
    char *buf;
    int i;

    if (w*h > 4096)
        return (NULL);

    buf = (char *)malloc(4096+1);
    if (!buf)
        return (NULL);

    for (i=0; i<h; i++)
        memcpy(&buf[i*w], init, w);  // line 17

    buf[4096] = '\0';

    return buf;
}

考虑到 wh 都是非常大的无符号整数。第 9 行的乘法有通过验证的机会。
现在问题出在第 17 行。将 int iunsigned int w 相乘:如果结果是 int,则可能导致乘积为负数,从而访问了 buf 之前的位置。如果结果是 unsigned int,则乘积总是为正数,从而访问了 buf 之后的位置。
很难编写代码来证明这一点:int 太大了。有人有什么想法吗?
是否有任何规定产品类型的文档?我已经搜索过了,但迄今为止还没有找到任何东西。
我认为就漏洞而言,(unsigned int) * (int) 生成 unsigned intint 并不重要,因为在编译的目标文件中,它们只是字节。以下代码对于产品类型的类型都是相同的:
unsigned int x = 10;
int y = -10;

printf("%d\n", x * y);  // print x * y in signed integer
printf("%u\n", x * y);  // print x * y in unsigned integer

因此,乘法返回的类型并不重要。重要的是消费函数是否使用intunsigned
这里的问题不是函数有多差,或者如何改进函数使其更好。函数无疑存在漏洞。问题是基于标准规定的预期行为,该函数的确切行为是什么。
13个回答

4

请使用long long进行w*h的计算,检查是否大于MAX_UINT。

编辑:备选方案:如果溢出(w*h)/ h!= w(这种情况总是发生吗?!应该是吧?)


不好的建议。尝试使用 if (!h || w > 4096/h) return NULL; - R.. GitHub STOP HELPING ICE
使用 long long 进行检查更好、更快。long*long 比除法更高效。 - Mark Lakata

2
在您的情况下,w*i 的类型是无符号的。如果我正确理解标准,规则是将操作数转换为较大的类型(带有其符号),或者转换为与已知类型相应的无符号类型(在您的情况下是unsigned int)。
然而,即使它是无符号的,也不能防止溢出(写入到buf之前的内存),因为这可能是情况(在i386平台上是这样),p[-1]p[-1u]相同。无论如何,在您的情况下,buf[-1]buf[大无符号数]都会导致未定义行为,因此有符号/无符号问题并不重要。
请注意,有符号/无符号在其他情况下很重要 - 例如,(int)(x*y/2)根据xy的类型不同而产生不同的结果,即使不存在未定义的行为。
我会通过检查第9行的溢出来解决您的问题;由于4096是一个相当小的常量,并且在大多数体系结构中4096*4096不会溢出(您需要检查),所以我会这样做:
if (w>4096 || h>4096 || w*h > 4096)
     return (NULL);

这里忽略了当wh为0的情况,如果需要,您可能需要检查它。
通常,您可以像这样检查溢出:
if(w*h > 4096 || (w*h)/w!=h || (w*h)%w!=0)

2

通过限制 w 和 h 的大小,确保 w * h 不会溢出。


相反,使用 if (!h || w > 4096/h) return NULL; - R.. GitHub STOP HELPING ICE
@R - 除法操作比较耗时,因此限制 w 或 h 的大小可以提高运行速度。 - Mark Lakata

2
回答你的问题:在C/C++中,将int和unsigned int相乘的表达式类型将是unsigned int。
回答你隐含的问题,处理整数算术可能出现溢出的一种不错的方法是使用Microsoft的“IntSafe”例程集。

http://blogs.msdn.com/michael_howard/archive/2006/02/02/523392.aspx

它在SDK中可用,并包含内联实现,因此如果您在另一个平台上,可以研究它们正在做什么。

2
在C/C++中,p[n]表示法实际上是*(p+n)的简写,这种指针算术运算考虑到符号。因此,p[-1]是有效的,并且指的是*p之前的值。
因此,符号在这里真的很重要,整数运算符的结果遵循标准定义的一组规则,这称为整数提升。
请查看此页面:INT02-C. Understand integer conversion rules

1

两个改变让它更安全:

if (w >= 4096 || h >= 4096 || w*h > 4096)  return NULL;

...

unsigned i;

还要注意的是,写入或读取超出缓冲区结尾并不是个好主意。因此问题并不在于 w 可能变成负数,而是在于 0 <= ih +w <= 4096 是否成立。
因此,重要的不是类型,而是 h*i 的结果。例如,无论是(unsigned)0x80000000 还是 (int)0x80000000,程序都会崩溃。

1

对于C语言,请参考“常规算术转换”(C99:第6.3.1.8节,ANSI C K&R A6.5)了解有关如何处理数学运算符的操作数的详细信息。

在您的示例中,适用以下规则:

C99:

否则,如果带有有符号整数类型的操作数的类型可以表示带有无符号整数类型的操作数的所有值,则将带有无符号整数类型的操作数转换为带有带符号整数类型的操作数的类型。

否则,两个操作数都将转换为与带有带符号整数类型的操作数相对应的无符号整数类型。

ANSI C:

否则,如果任一操作数为unsigned int,则另一个操作数将被转换为unsigned int。


0

如果w和/或h足够大,并且以下验证可以通过,则w*h可能溢出。

9.      if (w*h > 4096)
10.         return (NULL);

在 int 和 unsigned int 混合运算中,int 会被提升为 unsigned int,这种情况下,'i' 的负值会变成一个很大的正数。在这种情况下,
&buf[i*w]

将要访问一个超出范围的值。


0

在当前的C1X草案中,有3个段落涉及到计算(UNSIGNED TYPE1)X(SIGNED TYPE2),位于6.3.1.8 Usual arithmetic coversions, N1494。

WG 14: C - Project status and milestones

否则,如果具有无符号整数类型的操作数的等级大于或等于另一个操作数的类型的等级,则具有带符号整数类型的操作数将转换为具有无符号整数类型的操作数的类型。

否则,如果具有带符号整数类型的操作数的类型可以表示无符号整数类型的所有值,则具有无符号整数类型的操作数将转换为具有带符号整数类型的操作数的类型。

否则,两个操作数都将转换为与具有带符号整数类型的操作数相对应的无符号整数类型。

因此,如果a是无符号整数,b是整数,则解析(a * b)应生成代码(a * (unsigned int)b)。如果b < 0或a * b > UINT_MAX,则会溢出。

如果a是无符号整数,b是更大尺寸的长整型,(a * b)应该生成((long)a * (long)b)。如果a * b > LONG_MAX或a * b < LONG_MIN,则会溢出。
如果a是无符号整数,b是相同尺寸的长整型,(a * b)应该生成((unsigned long)a * (unsigned long)b)。如果b < 0或a * b > ULONG_MAX,则会溢出。
关于“索引器”所期望的类型的第二个问题,答案是“整数类型”,它允许任何(有符号)整数索引。
6.5.2.1 数组下标
约束条件
1. 两个表达式中的一个应具有“指向完整对象类型”的类型,另一个表达式应具有整数类型,并且结果具有“类型”。
语义
2. 后缀表达式后跟方括号[]中的表达式是数组对象的元素的下标指定。下标运算符[]的定义是E1[E2]等同于(*((E1)+(E2)))。由于适用于二进制+运算符的转换规则,如果E1是数组对象(等效地,是数组对象的初始元素的指针)并且E2是整数,则E1[E2]指定E1的第E2个元素(从零开始计数)。
编译器需要执行静态分析并警告开发人员可能存在缓冲区溢出的可能性,当指针表达式是数组变量且索引可能为负数时。同样,在索引为正数或无符号数时,也要警告可能存在数组大小超限的情况。

0

为什么不把 i 声明为无符号整数?这样问题就解决了。

无论如何,由于代码测试了 i * w 是否小于等于 4096,因此它永远不会溢出。


w*h的值可能会溢出,小于4096。 - rampion
错误,代码没有确保 i*w 小于等于 4096,因为无符号乘法会对 UINT_MAX+1 取模。 - R.. GitHub STOP HELPING ICE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接