Найди кота

Всем привет!

Сегодня будет довольно странный пост — мы будем искать слово ‘cat’.

Устраивайтесь поудобнее и погнали!

Что за?

Недавно мне пришла очень странная идея — найти в последовательности псевдослучайных чисел некое заданное слово.

Сначала я думал найти что-то типа ‘hello world’, но сразу отказался от него, т.к. чем длиннее фраза, тем меньше вероятность найти её.

И решил остановится на более коротком — ‘cat’.

Задачи

Давайте сформулируем задачу:

Чтобы не было скучно решать только одну задача мне ещё подкинули найти кота в sha256 (хотя можно взять и любую другую хеш функцию).

Сформулируем и для неё задачу:

Кот в случайных числах

В данном примере будем использовать xor shift для генерации псевдослучайных чисел

// xorshift.hpp
uint32_t xorshift(uint32_t seed) {
    seed ^= seed << 13;
    seed ^= seed >> 17;
    seed ^= seed << 5;
    return seed;
}

В качестве основного триплета я взял самый часто используемый: [13, 17, 5].

С ним точно будем уверены что период генерации будет 2 ^ 32 - 1, т.е. самый худший случай для перебора составит 4_294_967_295 итераций.

Если хотите подробнее погрузиться в теорию, то читайте вики и статью Xorshift RNGs.

Утилита для проверки

Для начала нам нужна утилита, с помощью которой сможем проверить любой заданный начальный seed.

#include <iostream>

#include "xorshift.hpp"

int main(int argc, char **argv) {
    if (argc < 3) {
        std::cout << "usage: " << argv[0] << " seed count" << std::endl;
        return 0;
    }

    // получаем seed из первого аргумента
    uint32_t seed = atoi(argv[1]);
    // и длину цепочки
    uint32_t count = atoi(argv[2]);

    // генерируем результат
    std::cout << "result: ";
    while (count--) {
        seed = xorshift(seed);
        std::cout << char(seed % 255);
    }
    std::cout << std::endl;
}

Наивный алгоритм

Суть наивного алгоритма — простой перебор.

То есть берём какое-то начальное число seed и начинаем искать последовательность чисел, которые по модулю 255 будут давать слово ‘cat’.

В коде его можно представить как-то так:

#include <iostream>
#include <string>

#include "xorshift.hpp"

// условие на нахождение кота
bool is_cat(const std::string & word) {
    if (word.size() == 3) {
        return word[0] == 'c' and
               word[1] == 'a' and
               word[2] == 't';
    }
    return false;
}

int main() {
    std::string word = "";
    uint32_t seed = 42;
    uint32_t last_seed = seed;
    uint64_t steps = 0;

    while (true) {
        seed = xorshift(seed);
        steps += 1;
        uint8_t v = seed % 255;
        if (isalpha(v)) {
            // если это буква алфавита то добавляем в слово
            word.push_back(char(v));
        } else {
            // нашли кота?
            if (is_cat(word)) {
                std::cout << "found [" << last_seed << "] with " << steps << " steps: " << word << std::endl;
                return 0;
            }
            last_seed = seed;
            word = "";
        }
    }
}

Код написан не идеально, но в данном случае этого достаточно чтобы довольно быстро найти значение 357063244 пройдя 69_990_600 итераций, что довольно неплохо!

С помощью ранее представленной программы можем проверить его:

$ ./prove_xorshift 357063244 3
result: cat

Но будет не интересно останавливаться на таком простом переборе и поэтому давайте улучшим наш алгоритм!

Улучшенный алгоритм

Суть в том чтобы вместо обычного перебора брать и находить такие числа N удовлетворяющие условию N % 255 = K, где K — первая буква слова ‘cat’.

То есть нам нужно найти seed, N1, N2 и N3 по ранее поставленным условиям:

N1 = xorshift(seed)
N2 = xorshift(N1)
N3 = xorshift(N2)
N1 % 255 = 'c'
N2 % 255 = 'a'
N3 % 255 = 't'

Для нахождения N1 мы можем воспользоваться формулой из статьи на вики.

Подставляя все значения в формулу и заменяя a на N1 и q на i получим:

N1 = 255 * i + int('c')

На данном этапе мы может определить N1, N2 и N3 просто перебирая все возможные N1 и проверяя на наше условие, но остаётся ещё число seed.

Пришлось покапаться в интернете для поиска того, что может помощь найти значение seed из N1. Я наткнулся на интересную статью в которой хоть и описывается обращение алгоритма XorShift128+, но самой идеи будет достаточно чтобы реализовать аналогичное решения для нашего 32-битного варианта.

Всё что нужно нам сделать — обратить все операции алгоритма и для этого разобьём задачу на три маленьких подзадачи, где мы будем обращать одну операцию за раз, а потом объединим результаты каждого из шагов, но только в обратном порядке.

Для обращения seed ^= seed << 13 можно сразу восстановить большую часть изначального значения, а затем сделать сдвиг и восстановить всё значение

tmp = (seed ^ (seed << 13)) & 0x00FFFFFF
result = seed ^ ((tmp << 13) & 0xFFFFFFFF)

Для seed ^= seed >> 17 решение вообще элементарное

result = (seed ^ (seed >> 17)) & 0xFFFFFFFF

А вот для seed ^= seed << 5 нужно немного потрудиться, т.к. от оригинального значения осталось совсем мало

# получаем два изначальных байта
f1 = (seed ^ (seed << 5)) & 0xFF
# а теперь у нас их три
f2 = (seed ^ (  f1 << 5)) & 0xFFF
f3 = (seed ^ (  f2 << 5)) & 0xFFFF
f4 = (seed ^ (  f3 << 5)) & 0xFFFFF
f5 = (seed ^ (  f4 << 5)) & 0xFFFFFF
f6 = (seed ^ (  f5 << 5)) & 0xFFFFFFF
# и постепенно приходим к результату
result = (seed ^ (  f6 << 5)) & 0xFFFFFFFF

Если не понятно как это всё работает, то стоит почитать про операции исключающее или и логический сдвиг.

Ну, а теперь остаётся только всё это закодить

#include <iostream>
#include <limits>
#include <string>

#include "xorshift.hpp"

const uint32_t ascii_max = 255;
const char c_symb = 'c';

bool is_cat(uint32_t c, uint32_t a, uint32_t t) {
    return char(c % ascii_max) == c_symb and
           char(a % ascii_max) == 'a' and
           char(t % ascii_max) == 't';
}

uint32_t n_vals(uint32_t i) {
    return ascii_max * i + uint8_t(c_symb);
}

uint32_t xorshift_backward(uint32_t seed) {
    uint32_t f0 = (seed ^ (seed << 5)) & 0xFF;
    uint32_t f1 = (seed ^ (f0 << 5)) & 0xFFF;
    f0 = (seed ^ (f1 << 5)) & 0xFFFF;
    f1 = (seed ^ (f0 << 5)) & 0xFFFFF;
    f0 = (seed ^ (f1 << 5)) & 0xFFFFFF;
    f1 = (seed ^ (f0 << 5)) & 0xFFFFFFF;
    seed ^= f1 << 5;
    seed ^= seed >> 17;
    return seed ^ (((seed ^ (seed << 13)) & 0x00FFFFFF) << 13);
}

int main() {
    for (uint32_t i = 0; i < std::numeric_limits<uint32_t>::max(); i++) {
        uint32_t c = n_vals(i);
        uint32_t a = xorshift(c);
        uint32_t t = xorshift(a);
        if (is_cat(c, a, t)) {
            uint32_t seed = xorshift_backward(c);
            std::cout << "found " << seed << " at step " << i << std::endl;
            return 0;
        }
    }
}

В результате программа быстро (за 1315 шагов) находит первый seed, который ведёт к ‘cat’: 4140321215.

$ ./prove_xorshift 4140321215 3
result: cat

Кот в хешах

С xor shift закончим. Теперь перейдём на поиск кота в хеше.

Сразу встаёт вопрос: как будем представлять значение хеша в виде текста?

Мне сразу напросилось простое решение:

То есть хеш

2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

преобразуется в

--M-------------------B-s--b----

где ‘-‘ — символы вне диапазона.

Наивный алгоритм

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

Остаётся только преобразовать полученный хеш в читаемую строку и проверить если ли подстрока ‘cat’ в данной строке.

Замечу два момента:

К коллайдеру! То есть к коду!

# можно заменить на любую другую хеш функцию
from hashlib import sha256 as hasher
from random import random


# наш алфавит
alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'


def as_text(value):
    result = ''
    # будем идти по два символа
    for index in range(0, len(value), 2):
        # преобразуем в число
        block = int(value[index:index + 2], 16)
        # а потом в символ
        symbol = chr(block)
        # и если он есть в алфавите, то добавляем в результирующую строку
        result += symbol if symbol in alphabet else '-'
    return result


if __name__ == '__main__':
    result = ''
    # перебираем пока не найдём кота
    while 'cat' not in result:
        # генерируем
        text = str(random())
        # хешируем
        hashv = hasher(text.encode()).hexdigest()
        # и в строку
        result = as_text(hashv)
    print(f"'{text}' -> {hashv} -> {result}")

Не забываем про утилиту для проверки хешей в ручном режиме:

alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

def as_text(value):
    result = ''
    for index in range(0, len(value), 2):
        block = int(value[index:index + 2], 16)
        symbol = chr(block)
        result += symbol if symbol in alphabet else '-'
    return result

print(as_text(input('Input hash: ')))

С помощью данного наивного подхода был найден вот такой вариант

c3377fd3636174cd06a81f38550d7057e2c676a7d20dbc38f8dfb957e25574c0

на основе вот этой строки

'0.44509213875838727'

сами можете убедиться с помощью sha256sum

$ echo -en "0.44509213875838727" | sha256sum -t
c3377fd3636174cd06a81f38550d7057e2c676a7d20dbc38f8dfb957e25574c0  -

а вот так выглядит преобразованный хеш в виде строки

----cat-----U-pW--v--------W-Ut-

Заключение

Я бы сказал “Не будем останавливаться на достигнутом — реализуем более быстрый алгоритм”, но у меня пока нет идей для него! Как только я его придумаю, то напишу отдельный пост.

Весь написанный мной код можно взять по ссылке.

Всем пока!

Что почитать

  1. SHA-2
  2. Xorshift
  3. Xorshift RNGs
  4. Деление с остатком
  5. XorShift128+ Backward
  6. Исключающее «или»
  7. Логический сдвиг
Назад