有几种方法可以让蛮力更快。
- 字符串的计算和循环可以递增。
- 可以并行制作。例如,一个核可以检查以“a”开头的字符串,另一个核可以检查以“b”开头的字符串,依此类推。该任务具有高度的并行性,在 1000 个处理器上运行将使其速度提高大约 1000 倍。
- 提前预计算所有 232=4,294,967,296 种反转可能性,并在需要时简单地获取反转 Jenkins。
循环(上面列表中的选项 1)可以通过以下方式完成:
using text_buf_t = std::array<char, 8>;
// std::string, unlike char[], has no '\0' as the last element.
const std::string CHARS = " !\"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\\]^_`{|}~";
uint32_t step(char ch, uint32_t prev=0)
{
auto hash = prev;
hash += ch;
hash += (hash << 10);
hash ^= (hash >> 6);
return hash;
}
const uint32_t finalize(uint32_t hash)
{
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
return hash;
}
bool brute_reverse(uint32_t target_hash, text_buf_t & buf,
unsigned length,
unsigned depth = 0, uint32_t temp_hash = 0)
{
if (depth == length)
{
auto hash = finalize(temp_hash);
return hash == target_hash;
}
for (auto ch: CHARS)
{
buf[depth] = ch;
if (brute_reverse(target_hash, buf, length,
depth+1, step(ch, temp_hash)))
{
return true;
}
}
return false;
}
std::string brute_reverse(uint32_t target_hash)
{
text_buf_t buf{};
for (int length = 0 ; length < 8 ; ++length)
{
if (brute_reverse(target_hash, buf, length))
return buf.data();
}
return "";
}
现在,情况有多糟糕? brute_reverse(2380561940) 运行了将近 10 分钟才能返回 " /o9c*y"。这是最难反转的值。在 1,000 个内核上运行此程序将使此运行时间为 10 毫秒。所以并行化听起来很合理。
如果需要经常进行反转,那么反转表开始听起来是个好主意。为了检查反向表的可行性,必须考虑其内存占用。 232 个哈希中的任何一个进行反转所需的最长字符串有 7 个字符。字符串可以压缩到 5 个字节,因为每个字符都有 69 种可能性的有限范围。
整个表占用 5*4GB == 20 GB。对于拥有 32 GB RAM 的人来说,反向可以驻留在 RAM 中并允许每秒数百万次哈希反转。我的旧电脑只有 4 GB,所以该表驻留在磁盘上。从表中的随机位置获取一个值大约需要 10 毫秒,这与在 1,000 个内核上运行的蛮力相当。
如果计算机内存不足,则可以使用 map-reduce 算法的变体创建反向表。该算法循环遍历所有字符串,就像蛮力算法一样,并将字符串、哈希对存储在 32 个 1.2GB 文件中(每个文件包含具有不同高位的哈希值)。一旦生成了所有组合,reduce 阶段将每个文件转换为从 hash->string 的映射。任何时候都不需要超过 600 MB 的 RAM。
这里是一些碰撞统计信息(问题的标题是“反转哈希(查找碰撞)”)。
所有 232 个反向哈希字符串的长度
len:条目(占总数的百分比)
----------------------------
0:1 (2.3e-08%)
1: 69 (1.6e-06%)
2:4761(0.00011%)
3:328488(0.0076%)
4:21848661(0.51%)
5:1170033315(27.24%)
6:3102751727(72.24%)
7: 274 (6.4e-06%)
创建表格的运行时间:在我老旧的笔记本电脑上用时不到 2 小时。由于高碰撞率,它必须计算 134,039,399,920 个字符串的哈希值。每个散列遇到 31.2085 次(大约 平均 30 次冲突),直到 " /o9c*y" 的散列计算出最大的字符串。
下图显示了有多少次冲突(repetitions-1=collisions):
这张表显示大约有 2.5*108 个不同的哈希值,碰撞率为 26。由于很难读取 Y 轴上的低值,让我们看看同一张图的对数比例:
有 364 个哈希值只计算了一次(没有冲突),但这只是 232 个哈希值中的一小部分。在另一个极端,有 86 个不同的字符串创建了一个哈希。这是 Jenkins hash 暴力反转过程中的最高碰撞率。
注意事项:
- 如果没有将其标记为黑客工具的危险,我会将用于创建反向哈希表的代码放在 github 上。
- 要计算图表中的冲突率,我必须运行算法 4 次,每次都收集不同哈希范围的数据。在我的内存不足的笔记本电脑上,占用超过 2 GB RAM 的任务非常慢。