我正在浏览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]);
行做了什么?
答案 0 :(得分:16)
根据diff
和a
之间是否存在差异,设置b
。
它总是走过a
和b
两个中较短的一个,避免了时间攻击,无论是否存在不匹配的错误。
diff |= (uint)(a[i] ^ (uint)b[i])
采用a
的异或,其字节为b
。如果两个字节相同则为0,如果它们不同则为非零。然后使用or
diff
进行diff
。
因此,如果在该迭代中的输入之间发现差异,则diff
将在迭代中设置为非零。一旦diff
在循环的任何迭代中被赋予非零值,它将通过进一步的迭代保留非零值。
因此,如果在a
和b
的相应字节之间发现任何差异,a
中的最终结果将为非零,并且仅当所有字节(和长度)都为0时)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
和a
之间匹配的字节数。假设长度的初始测试需要10 ns,并且循环的每次迭代需要另外10 ns。基于此,如果它在50 ns内返回false,我们可以快速猜测我们有正确的长度,b
和true
的前四个字节匹配。
即使不知道确切的时间量,我们仍然可以使用时序差来确定正确的字符串。我们从一个长度为1的字符串开始,一次增加一个字节,直到我们看到返回false所花费的时间增加。然后我们遍历第一个字节中的所有可能值,直到我们看到另一个增加,表明它已经执行了循环的另一次迭代。对于连续的字节继续相同,直到所有字节匹配,并返回{{1}}。
原始版本仍然对定时攻击的小位开放 - 虽然我们无法根据时序轻松确定正确字符串的内容,但我们至少可以找到字符串< em> length 基于时间。因为它只比较两个字符串中较短的字符串,所以我们可以从长度为1,然后是2,然后是3的字符串开始,依此类推,直到时间变得稳定。只要时间增加,我们建议的字符串就比正确的字符串短。当我们给它更长的字符串,但时间保持不变时,我们知道我们的字符串比正确的字符串长。正确的字符串长度将是最短的字符串,用于测试最长持续时间。
这是否有用取决于具体情况,但无论如何,它显然都会泄露一些信息。为了真正实现最大的安全性,我们可能希望在实际字符串的末尾添加随机垃圾,使其成为用户输入的长度,因此时间与输入的长度成正比,无论它是否更短,相等比正确的字符串更长或更长。
答案 1 :(得分:0)
此版本持续输入'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];
}