【问题标题】:Reverse Jenkins' one-at-a-time hash反向 Jenkins 的一次一个哈希
【发布时间】:2019-05-01 18:04:46
【问题描述】:

我将如何获取与返回的哈希匹配的任何可能的字符串值?

我不想获得所使用的确切密钥,只是传递给函数时将返回与未知密钥相同的哈希的任何密钥。

uint32_t jenkins_one_at_a_time_hash(const uint8_t* key, size_t length) {
      size_t i = 0;
      uint32_t hash = 0;
      while (i != length) {
        hash += key[i++];
        hash += hash << 10;
        hash ^= hash >> 6;
      }
      hash += hash << 3;
      hash ^= hash >> 11;
      hash += hash << 15;
      return hash;
    }

例如我将密钥作为“keynumber1”传递,该函数返回 0xA7AF2FFE。 我如何找到任何可以散列到 0xA7AF2FFE 的字符串。

【问题讨论】:

    标签: c hash cryptography


    【解决方案1】:

    虽然蛮力方法suggested by chux 可以正常工作,但实际上我们可以将其加速高达 256 倍左右(事实上,如果我们使用下面描述的所有优化,速度会更快)。

    这里的关键实现是用于计算哈希的所有操作都是可逆的。 (这是设计使然,因为它确保例如将相同的后缀附加到所有输入字符串不会增加哈希冲突的数量。)具体而言:

    • hash += hash &lt;&lt; n 操作当然等同于hash *= (1 &lt;&lt; n) + 1。我们使用的是 32 位无符号整数,所以所有这些计算都是在 modulo232 完成的。要撤消这个操作,我们需要做的就是找到(1 &lt;&lt; n) + 1 = 2n + 1 modulo 232 的模乘逆和将hash 乘以它。

      我们可以很容易地做到这一点,例如与this Python script,基于this answer here on SO。事实证明,210 + 1、23 + 1 和 215 + 1 的乘法逆元在十六进制中是 0xC00FFC01 , 分别为 0x38E38E39 和 0x3FFF8001。

    • 要找到某个常数nhash ^= hash &gt;&gt; n 的倒数,首先要注意此操作使hash 的最高n 位完全不变。下一个较低的n 位只是与最高的n 位进行异或运算,因此对于那些,只需重复操作即可撤消它。到目前为止看起来很简单,对吧?

      要恢复第三组最高n位的原始值,我们需要将它们与第二高n位的原始值进行异或,我们当然可以计算如上所述,通过对两个最高组的n 位进行异或运算。以此类推。

      这一切归结为hash ^= hash &gt;&gt; n的逆运算是:

      hash ^= (hash >> n) ^ (hash >> 2*n) ^ (hash >> 3*n) ^ (hash >> 4*n) ^ ...
      

      当然,一旦移位量等于或大于我们正在使用的整数中的位数(在这种情况下为 32),我们就可以切断序列。或者,我们可以通过多个步骤实现相同的结果,每次将移位量加倍,直到它超过我们正在处理的数字的位长,如下所示:

      hash ^= hash >> n;
      hash ^= hash >> 2*n;
      hash ^= hash >> 4*n;
      hash ^= hash >> 8*n;
      // etc.
      

      (当n 与整数大小相比较小时,多步方法可以更好地扩展,但对于中等大小的n,单步方法可能会在现代 CPU 上受到更少的管道停顿的影响。很难说哪一个实际上在任何给定情况下都更有效,无需对它们进行基准测试,并且结果可能因编译器和 CPU 模型而异。无论如何,这种微优化大多不值得太担心。)

    • 当然,最后,hash += key[i++] 的倒数就是 hash -= key[--i]

    所有这一切意味着,如果我们愿意,我们可以像这样反向运行哈希:

    uint32_t reverse_one_at_a_time_hash(const uint8_t* key, size_t length, uint32_t hash) {
      hash *= 0x3FFF8001;  // inverse of hash += hash << 15;
      hash ^= (hash >> 11) ^ (hash >> 22);
      hash *= 0x38E38E39;  // inverse of hash += hash << 3;
      size_t i = length;
      while (i > 0) {
        hash ^= (hash >> 6) ^ (hash >> 12) ^ (hash >> 18) ^ (hash >> 24) ^ (hash >> 30);
        hash *= 0xC00FFC01;  // inverse of hash += hash << 10;
        hash -= key[--i];
      }
      return hash;  // this should return 0 if the original hash was correct
    }
    

    然后调用,比如说,reverse_one_at_a_time_hash("keynumber1", 10, 0xA7AF2FFE) 应该返回零,as indeed it does


    好的,这很酷。但这对寻找原像有什么好处呢?

    好吧,一方面,如果我们猜测除了输入的第一个字节之外的所有字节,那么我们可以将第一个字节设置为零并在此输入上反向运行哈希。此时,有两种可能的结果:

      1234563我们可以将输入的第一个字节设置为该值,我们就完成了!
    1. 相反,如果反向运行哈希的结果不是有效的输入字节(例如,如果它大于 255),那么我们知道没有第一个字节可以使输入哈希的其余部分我们想要的输出,我们需要尝试另一个猜测。

    这里是an example,它会找到与 chux 的代码相同的输入(但将其打印为带引号的字符串,而不是 little-endian int):

    #define TARGET_HASH 0xA7AF2FFE
    #define INPUT_LEN 4
    
    int main() {
      uint8_t buf[INPUT_LEN+1];  // buffer for guessed input (and one more null byte at the end)
      for (int i = 0; i <= INPUT_LEN; i++) buf[i] = 0;
    
      do {
        uint32_t ch = reverse_one_at_a_time_hash(buf, INPUT_LEN, TARGET_HASH);
    
        if (ch <= 255) {
          buf[0] = ch;
          // print the input with unprintable chars nicely quoted
          printf("hash(\"");
          for (int i = 0; i < INPUT_LEN; i++) {
            if (buf[i] < 32 || buf[i] > 126 || buf[i] == '"' || buf[i] == '\\') printf("\\x%02X", buf[i]);
            else putchar(buf[i]);
          }
          printf("\") = 0x%08X\n", TARGET_HASH);
          return 0;
        }
    
        // increment buffer, starting from second byte
        for (int i = 1; ++buf[i] == 0; i++) /* nothing */;
      } while (buf[INPUT_LEN] == 0);
    
      printf("No matching input of %d bytes found for hash 0x%08X. :(", INPUT_LEN, TARGET_HASH);
      return 1;
    }
    

    here's a version that restricts the input to printable ASCII(并输出五字节字符串^U_N.):

    #define TARGET_HASH 0xA7AF2FFE
    #define MIN_INPUT_CHAR ' '
    #define MAX_INPUT_CHAR '~'
    #define INPUT_LEN 5
    
    int main() {
      uint8_t buf[INPUT_LEN+1];  // buffer for guessed input (and one more null byte at the end)
      buf[0] = buf[INPUT_LEN] = 0;
      for (int i = 1; i < INPUT_LEN; i++) buf[i] = MIN_INPUT_CHAR;
    
      do {
        uint32_t ch = reverse_one_at_a_time_hash(buf, INPUT_LEN, TARGET_HASH);
        if (ch >= MIN_INPUT_CHAR && ch <= MAX_INPUT_CHAR) {
          buf[0] = ch;
          printf("hash(\"%s\") = 0x%08X\n", buf, TARGET_HASH);
          return 0;
        }
    
        // increment buffer, starting from second byte, while keeping bytes within the valid range
        int i = 1;
        while (buf[i] >= MAX_INPUT_CHAR) buf[i++] = MIN_INPUT_CHAR;
        buf[i]++;
      } while (buf[INPUT_LEN] == 0);
    
      printf("No matching input of %d bytes found for hash 0x%08X. :(", INPUT_LEN, TARGET_HASH);
      return 1;
    }
    

    当然,很容易修改此代码以对接受哪些输入字节进行更严格的限制。例如using the following settings:

    #define TARGET_HASH 0xA7AF2FFE
    #define MIN_INPUT_CHAR 'A'
    #define MAX_INPUT_CHAR 'Z'
    #define INPUT_LEN 7
    

    产生(经过几秒钟的计算)原像KQEJZVS

    限制输入范围确实会使代码运行速度变慢,因为反向哈希计算的结果是有效输入字节的概率当然与可能的有效字节数成正比。


    有多种方法可以使此代码运行得更快。例如,我们可以combine the backwards hashing with a recursive search,这样即使只有一个字节发生变化,我们也不必重复对整个输入字符串进行哈希处理:

    #define TARGET_HASH 0xA7AF2FFE
    #define MIN_INPUT_CHAR 'A'
    #define MAX_INPUT_CHAR 'Z'
    #define INPUT_LEN 7
    
    static bool find_preimage(uint32_t hash, uint8_t *buf, int depth) {
      // first invert the hash mixing step
      hash ^= (hash >> 6) ^ (hash >> 12) ^ (hash >> 18) ^ (hash >> 24) ^ (hash >> 30);
      hash *= 0xC00FFC01;  // inverse of hash += hash << 10;
    
      // then check if we're down to the first byte
      if (depth == 0) {
        bool found = (hash >= MIN_INPUT_CHAR && hash <= MAX_INPUT_CHAR);
        if (found) buf[0] = hash;
        return found;
      }
    
      // otherwise try all possible values for this byte
      for (uint32_t ch = MIN_INPUT_CHAR; ch <= MAX_INPUT_CHAR; ch++) {
        bool found = find_preimage(hash - ch, buf, depth - 1);
        if (found) { buf[depth] = ch; return true; }
      }
      return false;
    }   
    
    int main() {
      uint8_t buf[INPUT_LEN+1];  // buffer for results
      for (int i = 0; i <= INPUT_LEN; i++) buf[INPUT_LEN] = 0;
    
      // first undo the finalization step
      uint32_t hash = TARGET_HASH;
      hash *= 0x3FFF8001;  // inverse of hash += hash << 15;
      hash ^= (hash >> 11) ^ (hash >> 22);
      hash *= 0x38E38E39;  // inverse of hash += hash << 3;
    
      // then search recursively until we find a matching input
      bool found = find_preimage(hash, buf, INPUT_LEN - 1);
      if (found) {
        printf("hash(\"%s\") = 0x%08X\n", buf, TARGET_HASH);
      } else {
        printf("No matching input of %d bytes found for hash 0x%08X. :(", INPUT_LEN, TARGET_HASH);
      }
      return !found;
    }
    

    但是等等,我们还没有完成!查看一次一次哈希的原始代码,我们可以看到循环第一次迭代后hash 的值将是((c &lt;&lt; 10) + c) ^ ((c &lt;&lt; 4) + (c &gt;&gt; 6)),其中c 是输入的第一个字节。由于c是一个8位字节,这意味着第一次迭代后只能设置hash的最低18个字节。

    事实上,如果我们对第一个字节c 的每个可能值calculate the value of hash after the first iteration,我们可以看到hash 永远不会超过1042 * c。 (实际上,hash / c 的最大值仅为 1041.015625 = 1041 + 2-6。)这意味着,如果 M 是有效输入字节的最大可能值,则第一次迭代后hash 的值不能超过1042 * M。并且添加下一个输入字节只会将hash 最多增加M

    所以我们可以通过在find_preimage() 中添加以下快捷方式检查来speed up the code above significantly

      // optimization: return early if no first two bytes can possibly match
      if (depth == 1 && hash > MAX_INPUT_CHAR * 1043) return false;
    

    其实可以用一个类似的论据来说明,在处理完前两个字节后,最多可以设置hash的最低28个字节(以及,more preciselyhash 与最大输入字节值的比值最多为 1084744.46667)。所以我们可以通过重写find_preimage() 来覆盖搜索的最后三个 阶段extend the optimization above,如下所示:

    static bool find_preimage(uint32_t hash, uint8_t *buf, int depth) {
      // first invert the hash mixing step
      hash ^= (hash >> 6) ^ (hash >> 12) ^ (hash >> 18) ^ (hash >> 24) ^ (hash >> 30);
      hash *= 0xC00FFC01;  // inverse of hash += hash << 10;
    
      // for the lowest three levels, abort early if no solution is possible    
      switch (depth) {
        case 0:
          if (hash < MIN_INPUT_CHAR || hash > MAX_INPUT_CHAR) return false;
          buf[0] = hash;
          return true;
        case 1:
          if (hash > MAX_INPUT_CHAR * 1043) return false;
          else break;
        case 2:
          if (hash > MAX_INPUT_CHAR * 1084746) return false;
          else break;
      }
    
      // otherwise try all possible values for this byte
      for (uint32_t ch = MIN_INPUT_CHAR; ch <= MAX_INPUT_CHAR; ch++) {
        bool found = find_preimage(hash - ch, buf, depth - 1);
        if (found) { buf[depth] = ch; return true; }
      }
      return false;
    }
    

    对于搜索哈希 0xA7AF2FFE 的 7 字节全大写原像的示例,此进一步优化将运行时间缩短至仅 0.075 秒(与单独的 depth == 1 快捷方式的 0.148 秒相比,2.456 秒没有快捷方式的递归搜索,非递归搜索为 15.489 秒,由 TIO 计时)。

    【讨论】:

    • 您的代码有效地反转散列的能力很好地证明了散列在密码学上不是安全的(当然它没有被吹捧为那样)-我怀疑这种“捷径可能适用”的反转,但您富有洞察力的分析做到了!干得好,第一次紫外线。
    【解决方案2】:

    如果散列函数很好,只需尝试大量的键组合,看看散列是否匹配。这就是一个好的哈希值。很难逆转。

    我估计大约 2^32 次尝试,您将有 50% 的机会找到一个。下面花了几秒钟。

    使用此哈希,可以应用捷径。

    int main() {
      const char *key1 = "keynumber1";
      uint32_t match = jenkins_one_at_a_time_hash(key1, strlen(key1));
      printf("Target 0x%lX\n", (unsigned long) match);
      uint32_t i = 0;
      do {
        uint32_t hash = jenkins_one_at_a_time_hash(&i, sizeof i);
        if (hash == match) {
          printf("0x%lX: 0x%lX\n", (unsigned long) i, (unsigned long) hash);
          fflush(stdout);
        }
      } while (++i);
    
      const char *key2 = "\x3C\xA0\x94\xB9";
      uint32_t match2 = jenkins_one_at_a_time_hash(key2, strlen(key2));
      printf("Match 0x%lX\n", (unsigned long) match2);
    }
    

    输出

    Target 0xA7AF2FFE
    0xB994A03C: 0xA7AF2FFE
    Match 0xA7AF2FFE
    

    【讨论】:

    • 如果我希望反转的值只是 ascii 字符怎么办?
    • 然后尝试只使用 ASCII 字符的键。您现在是在问如何生成随机 ASCII 字符串?
    • @JosephJones 试试PB3PT"!!
    • 嗯。我需要很长时间才能完成循环。你从 0 到 0x7FFFFFFF 需要多长时间?
    • @JosephJones 0 到 0xFFFF_FFFF 不到 5 秒。试试“yerkn11ke1”。和“SKXs0SVGGx”。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-30
    • 2017-04-24
    • 2015-08-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多