如何编写可靠的内容独立的memcmp()实现?

时间:2014-04-08 11:21:36

标签: c security compiler-optimization memcmp

memcmp()的简单实现就像这样(来自this answer):

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使用它(取自here):

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;
}

中间没有breakreturn s,因此此代码必须遍历整个块的长度。至少这是意图。

现在这是一个用法示例(来自here):

 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(),使符合标准的编译器不会将其转换为有助于计时攻击的版本?

1 个答案:

答案 0 :(得分:3)

有两种可能的解决方案。

第一个是绝对可移植的并且达到标准 - 声明x volatile,它基本上告诉编译器它必须保留更新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)。许多编译器将这样的转换视为暗示这些读取不应该被改变,但标准并没有真正对此做出任何保证,因此编译器可能会故意破坏这些代码。因此,此解决方案不可移植。