【问题标题】:Reversing a hash (finding collisions)反转哈希(发现冲突)
【发布时间】:2018-08-24 15:33:25
【问题描述】:

我正在尝试查找以下哈希的哈希冲突:

inline DWORD jenkins_one_at_a_time_hash(const char* value) {
    size_t len = strlen(value);
    unsigned int hash, i;
    for (hash = i = 0; i < len; ++i)
    {
        hash += tolower(value[i]);
        hash += (hash << 10);
        hash ^= (hash >> 6);
    }
    hash += (hash << 3);
    hash ^= (hash >> 11);
    hash += (hash << 15);
    return hash;
}

我对算法了解不多,我知道哈希无法反转,但我只需要一个返回特定哈希的输入字符串(不需要是原始哈希)。

我目前正在对输入字符串进行暴力破解,但我想知道除了使用随机字符串调用该函数之外,是否还有其他方法可以使暴力破解更快。(我只需要一个与哈希匹配的字符串)

【问题讨论】:

  • 尝试输入直到得到结果是蛮力算法的定义。您在问是否有比蛮力算法更快的蛮力算法。
  • 我建议坐下来用铅笔和纸做数学题。这会让你知道从哪里开始编写你的哈希反转算法
  • 加密哈希算法应该是抗冲突的,但是如果输入的数量大于输出的数量,则该算法必须有冲突。这个问题的上限是 sqrt(2^n)。如果您发现比暴力破解更便宜的方法,那么这不是一个好的散列函数。为什么需要在这里查找碰撞?
  • 除非SAT solver 可以找出解决方案,否则您会遇到蛮力。请记住,大多数散列算法专门设计为尽可能难以“反转”和发现冲突。即使是弱 MD5 哈希也难以逆转,但它足够简单,暴力破解它是微不足道的,尤其是在 GPU 加速的情况下。
  • 32 位散列的一个更大问题是,一旦字符串大小超过大约 6 个字符,就会有很多候选对象产生相同的散列。并且偶尔会出现多次反转。您将需要一个相当大的表来获取长度超过 8 的所有可能的候选者,候选者长度每增加 2 个字符,就会增加大约 3 个数量级。

标签: c++ hash brute-force


【解决方案1】:

有几种方法可以让蛮力更快。

  1. 字符串的计算和循环可以递增。
  2. 可以并行制作。例如,一个核可以检查以“a”开头的字符串,另一个核可以检查以“b”开头的字符串,依此类推。该任务具有高度的并行性,在 1000 个处理器上运行将使其速度提高大约 1000 倍。
  3. 提前预计算所有 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 暴力反转过程中的最高碰撞率。

注意事项:

  1. 如果没有将其标记为黑客工具的危险,我会将用于创建反向哈希表的代码放在 github 上。
  2. 要计算图表中的冲突率,我必须运行算法 4 次,每次都收集不同哈希范围的数据。在我的内存不足的笔记本电脑上,占用超过 2 GB RAM 的任务非常慢。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-07-30
    • 1970-01-01
    • 2011-01-21
    • 2010-10-18
    • 2012-05-13
    • 2020-10-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多