4

一个天真的实现是memcmp()这样的(来自这个答案):

int memcmp_test(const char *cs_in, const char *ct_in, size_t n)
{
   size_t i;  
   const unsigned char * cs = (const unsigned char*) cs_in;
   const unsigned char * ct = (const unsigned char*) ct_in;

   for (i = 0; i < n; i++, cs++, ct++)
   {
       if (*cs < *ct)
       {
          return -1;
       }
       else if (*cs > *ct)
       {
          return 1;
       }
   }
   return 0;
}

一旦找到第一个不匹配的字节,块遍历就会停止。这对加密应用程序不利,因为它使执行时间依赖于块内容,这可能允许定时攻击。所以 OpenSSL 使用这个(取自这里):

int CRYPTO_memcmp(const void *in_a, const void *in_b, size_t len)
{
    size_t i;
    const unsigned char *a = in_a;
    const unsigned char *b = in_b;
    unsigned char x = 0;

    for (i = 0; i < len; i++)
         x |= a[i] ^ b[i];

    return x;
}

中间没有breaks 或returns,所以这段代码必须遍历整个块的长度。至少这是本意。

现在这是一个使用示例(来自此处):

 static int des_ede3_unwrap(EVP_CIPHER_CTX *ctx,
     unsigned char *out, const unsigned char *in, size_t inl)
 {
      unsigned char icv[8], iv[8], sha1tmp[SHA_DIGEST_LENGTH];
      //whatever, unrelated then...
      if (!CRYPTO_memcmp(sha1tmp, icv, 8))
         rv = inl - 16;
      //whatever, unrelated
 }

现在,通过链接时代码生成 (Visual C++ LTCG) 或链接时优化 (gcc LTO),编译器能够同时看到CRYPTO_memcmp()实现和调用站点(即使它们位于不同的翻译单元中)。可以看到调用站点没有使用实际值,它只是将其与 null 进行比较。因此,它可以自由转换CRYPTO_memcmp(),以便在找到第一个不匹配的字节对并且“安全”版本memcmp()不再安全时立即返回。

如何memcmp()实现使得符合标准的编译器不会将其转换为有助于定时攻击的版本?

4

1 回答 1

3

有两种可能的解决方案。

第一个是绝对可移植的并且符合标准 - 声明xvolatile 基本上告诉编译器它必须保留更新序列,x因此它必须完全读取两个数据数组。它不会阻止编译器进行较大部分的读取(例如一次读取几个字节,然后以正确的顺序使用它们),但这没什么大不了的 - 编译器将不得不发出大量读取与数据数组中的字节数成正比。这种方法的问题在于它会使代码变慢——我运行的一些基准测试显示,在特定处理器和特定工具集上,速度降低了大约 50%。YMMV。

第二种可能的解决方案是将指针转换为volatile unsigned char*并通过它们访问

const volatile unsigned char * cs = (const volatile unsigned char*) cs_in;
const volatile unsigned char * ct = (const volatile unsigned char*) ct_in;
// the rest of the code is the same

它同样快,但不完全符合标准(请参阅this)。许多编译器将这种强制转换视为不应更改这些读取的提示,但标准并未对此做出任何保证,因此编译器可能会故意破坏此代码。因此,该解决方案不可移植。

于 2014-11-13T13:11:35.257 回答