Обратный Дженкинс за один раз хэш - PullRequest
0 голосов
/ 29 ноября 2018

Как мне получить любое возможное строковое значение, совпадающее с возвращенным хешем?

Я не хочу получать точный ключ, который использовался, просто любой ключ, который при передаче в функцию будетвернуть тот же хеш неизвестного ключа.

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.

Ответы [ 2 ]

0 голосов
/ 02 декабря 2018

Хотя метод грубой силы , предложенный chux , работает нормально, как он есть, на самом деле мы можем ускорить его примерно в 256 раз (и, на самом деле, намного больше, если мыиспользуйте все оптимизации, описанные ниже).

Ключевым моментом здесь является то, что все операции, используемые для вычисления хэша, являются обратимыми.(Это сделано специально, так как это гарантирует, что, например, добавление одного и того же суффикса ко всем входным строкам не увеличит число коллизий хешей.) В частности:

  • Операция hash += hash << n, конечно, эквивалентно hash *= (1 << n) + 1.Мы работаем с 32-разрядными целыми числами без знака, поэтому все эти вычисления выполняются по модулю 2 32 .Чтобы отменить эту операцию, все, что нам нужно сделать, это найти модульную мультипликативную инверсию (1 << n) + 1 = 2 n + 1 по модулю 2 32 и умножить hash этим.

    Мы можем сделать это довольно легко, например, с помощью этого скрипта Python , основанного на этом ответе здесь на SO .Оказывается, что мультипликативные инверсии 2 10 + 1, 2 3 + 1 и 2 15 + 1, в шестнадцатеричном виде, 0xC00FFC01, 0x38E38E39 и0x3FFF8001 соответственно.

  • Чтобы найти обратное значение hash ^= hash >> n для некоторой константы n, сначала обратите внимание, что эта операция оставляет старшие n биты hash полностью неизменными.Следующие младшие n биты просто XORed с самыми старшими n битами, поэтому для тех, кто просто повторяет операцию, она отменяется.Пока выглядит довольно просто, верно?

    Чтобы восстановить исходные значения третьей старшей группы из n битов, нам нужно XOR их с исходными значениями второго наивысшего n бит, которые мы, конечно, можем вычислить, используя XOR для двух старших групп n бит, как описано выше.И так далее.

    Все это сводится к тому, что обратная операция к hash ^= hash >> 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 одношаговый метод может страдать от меньшего количества конвейерных остановок на современных процессорах. Трудно сказать, какой из них на самом деле более эффективен в любой конкретной ситуации безсравнение их обоих, и результаты могут отличаться в зависимости от компиляторов и моделей процессоров. В любом случае о таких микрооптимизациях обычно не стоит беспокоиться.)

  • Наконец, конечно,обратное значение 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) должен вернуть ноль, , как на самом деле .


ОК, это круто.Но что хорошего в том, чтобы найти прообразы?

Ну, во-первых, если мы предположим всего, кроме первого байта ввода, то мы можем установить первый байт в ноль и запуститьхеш назад по этому входу.На этом этапе возможны два результата:

  1. Если выполнение хэша в обратном направлении, как это, приводит к выводу, который является действительным входным байтом (т. Е. Не больше 255, и, возможно, с другимиограничения, если, например, вы хотите, чтобы все входные байты были печатаемыми ASCII), тогда мы можем установить первый байт входного значения на это значение, и все готово!

  2. И наоборот, еслирезультат запуска хэша в обратном направлении не является допустимым входным байтом (например, если он больше 255), тогда мы знаем, что не существует первого байта, который мог бы сделать оставшуюся часть входного хэша для вывода, который мы хотим, и нам понадобитсявместо этого попробовать другое предположение.

Вот пример , который находит тот же ввод, что и код chux (но печатает его как строку в кавычках, а не как 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;
}

And вот версия, которая ограничивает ввод для печати 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;
}

Конечно, этот код легко изменить, чтобы он был еще болееограничительный набор входных байтов.Например, с использованием следующих настроек :

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

создает (после нескольких секунд вычисления) прообраз KQEJZVS.

Ограничение входного диапазона делаеткод выполняется медленнее, поскольку вероятность того, что результат обратного вычисления хеша будет действительным входным байтом, конечно, пропорционален числу возможных действительных байтов.


Существуют различные способы, которымиэтот код можно было бы сделать еще быстрее.Например, мы можем объединить обратное хеширование с рекурсивным поиском , чтобы нам не приходилось многократно хэшировать всю входную строку, даже если изменяется только один байт:

#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 << 10) + c) ^ ((c << 4) + (c >> 6)), где c - первый байт ввода.Поскольку c является восьмибитным байтом, это означает, что только первые 18 байтов hash могут быть установлены после первой итерации.

Если факт, если мы вычисляем значение hash после первой итерации для каждого возможного значения первого байта c мы видим, что hash никогда не превышает 1042 * c.(На самом деле, максимум отношения hash / c составляет всего 1041.015625 = 1041 + 2 -6 .) Это означает, что, если M - максимально возможное значение действительного входного байта, значениеhash после первой итерации не может превышать 1042 * M.И добавление следующего входного байта только увеличивает hash максимум на M.

Таким образом, мы можем значительно ускорить код выше , добавив следующую проверку ярлыка в find_preimage():

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

Фактически, аналогичный аргумент может использоваться, чтобы показать, что после обработки первых двух байтов может быть установлено самое большее 28 младших байтов hash, точнее , что отношение hash к максимальному значению входного байта составляет не более 1084744.46667).Таким образом, мы можем расширить оптимизацию выше , чтобы охватить последние три этапа поиска, переписав find_preimage() следующим образом:

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;
}

Для примера поиска7-байтовый прообраз всего верхнего регистра 0xA7AF2FFE, эта дальнейшая оптимизация сокращает время работы до 0,075 секунды (в отличие от 0,148 секунд только для быстрого вызова depth == 1, 2,456 секунд для рекурсивного поиска без ярлыков и 15,489секунд для нерекурсивного поиска, рассчитанного по TIO).

0 голосов
/ 29 ноября 2018

Если хеш-функция хороша, просто попробуйте множество комбинаций клавиш и посмотрите, совпадает ли хеш-код.В этом смысл хорошего хэша.Трудно повернуть вспять.

Я бы оценил около 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...