密码学.NET,避免定时攻击
Posted
技术标签:
【中文标题】密码学.NET,避免定时攻击【英文标题】:Cryptography .NET, Avoiding Timing Attack 【发布时间】:2013-06-06 03:24:11 【问题描述】:我在浏览crackstation.net网站时发现了这段代码,评论如下:
在长度恒定的时间内比较两个字节数组。使用这种比较方法是为了避免使用定时攻击从在线系统中提取密码哈希,然后进行离线攻击。
private static bool SlowEquals(byte[] a, byte[] b)
uint diff = (uint)a.Length ^ (uint)b.Length;
for (int i = 0; i < a.Length && i < b.Length; i++)
diff |= (uint)(a[i] ^ b[i]);
return diff == 0;
谁能解释一下这个函数实际是如何工作的,为什么我们需要将长度转换为无符号整数以及这种方法如何避免定时攻击? diff |= (uint)(a[i] ^ b[i]);
行是做什么的?
【问题讨论】:
【参考方案1】:这会根据a
和b
之间是否存在差异来设置diff
。
它通过始终遍历a
和b
两者中较短的一个来避免定时攻击,无论是否比这更早出现不匹配。
diff |= (uint)(a[i] ^ (uint)b[i])
将a
的一个字节与b
的对应字节进行异或。如果两个字节相同,则为 0,如果不同,则为非零。然后or
s 与diff
。
因此,如果在迭代中发现输入之间存在差异,diff
将在迭代中设置为非零。一旦diff
在循环的任何迭代中被赋予非零值,它将通过进一步的迭代保持非零值。
因此,如果在a
和b
的对应字节之间发现任何差异,则diff
中的最终结果将非零,并且仅当a
的所有字节(和长度)和b
相等。
但是,与典型的比较不同,这将始终执行循环,直到两个输入中较短的一个字节中的所有字节都与另一个输入中的字节进行了比较。典型的比较会提前结束,一旦发现不匹配,循环就会中断:
bool equal(byte a[], byte b[])
if (a.length() != b.length())
return false;
for (int i=0; i<a.length(); i++)
if (a[i] != b[i])
return false;
return true;
这样,根据返回 false
所花费的时间,我们可以了解(至少是近似值)在 a
和 b
之间匹配的字节数。假设长度的初始测试需要 10 ns,循环的每次迭代需要另外 10 ns。基于此,如果它在 50 ns 内返回 false,我们可以快速猜测我们的长度是正确的,并且 a
和 b
的前四个字节匹配。
即使不知道确切的时间量,我们仍然可以使用时间差异来确定正确的字符串。我们从长度为 1 的字符串开始,一次增加一个字节,直到我们看到返回 false 的时间增加。然后我们遍历第一个字节中的所有可能值,直到我们看到另一个增加,这表明它已经执行了循环的另一个迭代。对连续的字节继续使用相同的方法,直到所有字节都匹配并且我们得到 true
的返回。
原文仍然存在少许位的定时攻击——虽然我们不能轻易根据定时确定正确字符串的内容,但我们至少可以找到字符串长度基于时间。由于它只比较两个字符串中较短的一个,我们可以从长度为 1 的字符串开始,然后是 2,然后是 3,依此类推,直到时间稳定。只要时间在增加,我们建议的字符串就会比正确的字符串短。当我们给它更长的字符串,但时间保持不变时,我们知道我们的字符串比正确的字符串长。正确的字符串长度将是需要最长测试时间的最短字符串。
这是否有用取决于情况,但无论如何,它显然会泄露一些信息。为了真正实现最大的安全性,我们可能希望将随机垃圾附加到真实字符串的末尾,以使其成为用户输入的长度,因此时间与输入的长度成正比,无论它是否更短、相等到,或比正确的字符串长。
【讨论】:
当a
和b
不同时,a ^ b
的结果不一定是1
。
其实和普通的没什么区别!就秒表而言,在测量时间方面!
@Hossein 您可能需要对其进行多次迭代才能看到差异,这就是统计攻击的工作原理。如果您看到其他情况,我很乐意看到源代码。
@ntoskml 好吧,是的,它并不总是1
,但如果a != b
那么a ^ b
永远不会是0
(即它总是1 - 255)。由于 SlowEquals
不断地“或”diff
和 a
和 b
的 XOR 并检查 diff == 0
,那么您就可以明确知道 a
是否完全等于 b
。
@Miryafa:是的——使用min(a.length, b.length)
仍然可以为他们提供一些信息。他们可以尝试从 1 开始向上的长度。时间每次都会增加,直到它们达到正确的长度(然后对于大输入将保持不变)。【参考方案2】:
这个版本持续输入'a'的长度
private static bool SlowEquals(byte[] a, byte[] b)
uint diff = (uint)a.Length ^ (uint)b.Length;
byte[] c = new byte[] 0 ;
for (int i = 0; i < a.Length; i++)
diff |= (uint)(GetElem(a, i, c, 0) ^ GetElem(b, i, c, 0));
return diff == 0;
private static byte GetElem(byte[] x, int i, byte[] c, int i0)
bool ok = (i < x.Length);
return (ok ? x : c)[ok ? i : i0];
【讨论】:
我想应该没问题,但我肯定不喜欢return (ok ? x : c)[ok ? i : 0];
。我的直接反应是它似乎过于聪明了。它可能仍然不是100%有效。如果一个字符串比另一个字符串长得多,您最终会进行多次迭代,其中(GetElem
)ok
将是false
,并且分支变得更加可预测,从而导致速度提高。然而,防止由于分支预测导致的速度差异可能相当困难。
@JerryCoffin 我感谢您的反馈:)。我没有考虑分支预测。我想它不像我那么简单
是的,确定你已经很好地处理了分支预测可能很困难。老实说,我也不确定在这种情况下它是否会导致问题(但我可以看到它可能在哪里)。缓存也可能存在速度差异——重复访问c[0]
可能比访问x
的连续元素更快。以上是关于密码学.NET,避免定时攻击的主要内容,如果未能解决你的问题,请参考以下文章
如何在 .NET WCF 中保护 HTTP JSON Web 服务
攻击者可能试图从(例如,密码、消息或信用卡)窃取您的信息。 NET::ERR_CERT_COMMON_NAME_INVALID