26

最近,我写了一些代码来比较这样的指针:

if(p1+len < p2)

但是,有些工作人员说我应该这样写:

if(p2-p1 > len)

为了安全。这里,p1p2char *指针,len是整数。我对此一无所知。对吗?

EDIT1:当然,p1p2在乞求时指向同一个内存对象。

EDIT2:就在一分钟前,我在我的代码中发现了这个问题的bogo(大约3K行),因为len它太大了,p1+len不能存储在4字节的指针中,所以p1 + len < p2真的。但它应该实际上不是,所以我认为我们应该在某些情况下比较这样的指针:

if(p2 < p1 || (uint32_t)p2-p1 > (uint32_t)len)
4

6 回答 6

25

一般来说,只有当它们都指向同一个内存对象的部分(或对象末尾的一个位置)时,您才能安全地比较指针。当p1, p1 + len, and p2all 符合这条规则时,你的两个if-tests 是等价的,所以你不必担心。另一方面,如果 onlyp1p2已知符合此规则,并且p1 + len可能超出终点,则 onlyif(p2-p1 > len)是安全的。(但我无法想象你的情况是这样。我假设它p1指向某个内存块的开头,并p1 + len指向它结束后的位置,对吧?)

他们可能一直在考虑整数算术:如果可能i1 + i2会溢出,但您知道不会溢出,i3 - i1那么i1 + i2 < i3可以回绕(如果它们是无符号整数)或触发未定义的行为(如果它们是有符号整数)或两者(如果您的系统碰巧对有符号整数溢出执行回绕),i3 - i1 > i2而不会有这个问题。


编辑添加:在评论中,您写“len是来自 buff 的值,所以它可能是任何东西”。在这种情况下,它们是完全正确的,并且p2 - p1 > len更安全,因为p1 + len可能无效。

于 2013-07-11T03:16:13.467 回答
12

“未定义的行为”适用于此。您不能比较两个指针,除非它们都指向同一个对象或该对象末尾之后的第一个元素。这是一个例子:

void func(int len)
{
    char array[10];
    char *p = &array[0], *q = &array[10];
    if (p + len <= q)
        puts("OK");
}

你可能会想到这样的函数:

// if (p + len <= q)
// if (array + 0 + len <= array + 10)
// if (0 + len <= 10)
// if (len <= 10)
void func(int len)
{
    if (len <= 10)
        puts("OK");
}

但是,编译器知道ptr <= q对于 的所有有效值都是如此ptr,因此它可能会将函数优化为:

void func(int len)
{
    puts("OK");
}

快多了!但不是你想要的。

是的,在野外存在执行此操作的编译器。

结论

这是唯一安全的版本:减去指针并比较结果,不要比较指针。

if (p - q <= 10)
于 2013-07-11T03:31:55.357 回答
9

从技术上讲,p1并且p2必须是指向同一个数组的指针。如果它们不在同一个数组中,则行为未定义。

对于加法版本,类型len可以是任何整数类型。

对于差异版本,减法的结果是ptrdiff_t,但任何整数类型都会被适当地转换。

在这些限制下,您可以用任何一种方式编写代码;两者都不正确。在某种程度上,这取决于您要解决的问题。如果问题是“数组的这两个元素是否超过了len元素之间的距离”,那么减法是合适的。如果问题是“与(aka ) 的p2元素相同”,则添加是适当的。p1[len]p1 + len

在实践中,在许多具有统一地址空间的机器上,您可以通过减去指向不同数组的指针来摆脱困境,但您可能会得到一些有趣的效果。例如,如果指针是指向某种结构类型的指针,但不是同一数组的一部分,那么被视为字节地址的指针之间的差异可能不是结构大小的倍数。这可能会导致特殊的问题。如果它们是指向同一个数组的指针,就不会有这样的问题——这就是限制存在的原因。

于 2013-07-11T03:17:58.363 回答
0

现有答案显示了为什么if (p2-p1 > len)优于if (p1+len < p2),但仍有一个问题——如果p2碰巧指向p1缓冲区中的 BEFORE 并且len是无符号类型(例如size_t),那么p2-p1将为负数,但将转换为大的无符号值为了与 unsigned len 进行比较,所以结果可能是真的,这可能不是你想要的。

因此,您实际上可能需要类似的东西if (p1 <= p2 && p2 - p1 > len)来确保完全安全。

于 2013-07-11T06:43:59.180 回答
0

正如 Dietrich 已经说过的,比较不相关的指针是危险的,并且可以被视为未定义的行为。

假设两个指针在 0 到 2GB 的范围内(在 32 位 Windows 系统上),减去 2 个指针将得到一个介于 -2^31 和 +2^31 之间的值。这正是有符号 32 位整数的域。所以在这种情况下,减去两个指针似乎是有意义的,因为结果将始终在您期望的域内。

但是,如果在您的可执行文件中启用了 LargeAddressAware 标志(这是特定于 Windows 的,不了解 Unix),那么您的应用程序将具有 3GB 的地址空间(当在带有 /3G 标志的 32 位 Windows 中运行时)甚至 4GB(在 64 位 Windows 系统上运行时)。如果您随后开始减去两个指针,结果可能超出 32 位整数的域,并且您的比较将失败。

我认为这是地址空间最初被划分为 2GB 的 2 个相等部分的原因之一,并且 LargeAddressAware 标志仍然是可选的。但是,我的印象是当前软件(您自己的软件和您正在使用的 DLL)似乎非常安全(没有人再减去指针,不是吗?)并且我自己的应用程序默认打开了 LargeAddressAware 标志。

于 2013-07-11T08:09:18.687 回答
0

如果攻击者控制您的输入,这两种变体都不安全

该表达式p1 + len < p2编译为类似p1 + sizeof(*p1)*len < p2,并且使用指向类型的大小进行缩放可能会溢出您的指针:

int *p1 = (int*)0xc0ffeec0ffee0000;
int *p2 = (int*)0xc0ffeec0ffee0400;
int len =       0x4000000000000000;
if(p1 + len < p2) {
    printf("pwnd!\n");
}

len乘以 的大小时int,它会溢出到,0因此条件被评估为if(p1 + 0 < p2)。这显然是正确的,并且以下代码以过高的长度值执行。


好的,那怎么办p2-p1 < len。同样的事情,溢出会杀死你:

char *p1 = (char*)0xa123456789012345;
char *p2 = (char*)0x0123456789012345;
int len = 1;
if(p2-p1 < len) {
    printf("pwnd!\n");
}

在这种情况下,指针之间的差异被评估为p2-p1 = 0xa000000000000000,这被解释为符号值。因此,它比较小 then len,并且以下代码以太低的len值(或太大的指针差异)执行。


我知道在存在攻击者控制的值的情况下唯一安全的方法是使用无符号算术:

if(p1 < p2 &&
   ((uintptr_t)p2 - (uintptr_t)p1)/sizeof(*p1) < (uintptr_t)len
) {
    printf("safe\n");
}

不能产生真正负值的p1 < p2保证。p2 - p1第二个子句执行p2 - p1 < lenwhile 以非 UB 方式强制使用无符号算术的操作。Ie准确给出了 large和(uintptr_t)p2 - (uintptr_t)p1small 之间的字节数,无论涉及的值如何。p2p1

当然,您不希望在代码中看到此类比较,除非您知道需要防御坚定的攻击者。不幸的是,这是确保安全的唯一方法,如果您依赖问题中给出的任何一种形式,您就会面临攻击。

于 2019-06-15T08:02:38.867 回答