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;
}
中间没有break
或return
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()
,使符合标准的编译器不会将其转换为有助于计时攻击的版本?
答案 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)。许多编译器将这样的转换视为暗示这些读取不应该被改变,但标准并没有真正对此做出任何保证,因此编译器可能会故意破坏这些代码。因此,此解决方案不可移植。